dev.log2013. 2. 22. 18:55

우연인지 몰라도 내가 다니는 회사마나 나한테 파일 및 리소스 로딩을 좀 손봐달라는 요구를 한다. 내가 파일을 잘 읽게 생겼나?  여튼, 언리얼 엔진에서만 두번째로 백그라운드 로딩을 만드는 일이 나한테 떨어졌다.


언리얼 리소스 로딩 시스템을 첫번째로 손봤던 것은 G사에 다닐 때의 일로, 그때는 MMORPG를 만들고 있었기 때문에, 요구사항이 그렇게 빡세지 않았다. "옆에서 캐릭터 스폰될때 막 1,2초씩 끊기지 않게 해주세요" 정도? 즉, Mutually Exclusive한 작업을 해야 할 때 그 1/10인 100ms 정도는 락을 잡고 있어도 무방하다는 뜻이고, 100ms정도면 많은 일을 할 수 있는 시간이다. 이 회사에서 언리얼 로딩 시스템을 손봄으로써 두번째 손을 대게 된 것인데, 이번 회사에서 만들고 있는 것은 FPS이고, 여기서의 요구사항은 "다른 플레이어가 난입할 때 프레임 저하가 안생기게 해주세요."였다. 이말인 즉슨, 90fps로 게임을 하는 것을 기준으로, 10ms 길이의 락도 길다는 것이다. OMG...


언리얼의 로딩 과정을 대략적으로 표현하면 이렇다.


load( name )

{

o = create( name );        // 생성

for each property in o

   read( property );         // 읽기

postload();                    // 정리

}


여기서 가장 오래 걸리는 작업은 대부분의 경우 (당연히) 읽기다. 시간에 따른 바의 길이로 표현하면 이런식.

 A생성

A읽기 

A정리 



그런데 언리얼의 리소스 패키지 관리 구조는 내가 "고구마줄기"라고 평하는 디펜던시 그래프에 기반한 구조다. 즉, 뭐 하나 로딩하려면 그놈이 의존하고 있는 것들을 따라가면서 줄줄이 로딩하는 구조다.

캐릭터를 하나 로딩한다고 치자. 당연히 캐릭터의 메쉬가 있어야 겠고, 메쉬에는 매터리얼이 붙어 있을 테고, 매터리얼에는 다수의 텍스쳐가 붙어 있고... 그뿐인가? 캐릭터 애니메이션도 불러 올라 치면 애니메이션에는 노티파이도 붙어 있고, 노티파이에서는 파티클도 뿌려대고, 사운드도 틀어주고, 파티클에는 메쉬를 뿌려주는 놈들이 있고 그 메쉬에는 또 매터리얼이 붙어있고.... 등등.

게다가 매터리얼에는 셰이더를 구성하는 매터리얼 표현식이, 애니메이션에는 각각의 애니메이션 시퀀스가, 사운드에는 개별 사운드 노드가 일일이 다 별개의 객체로 들어 있기 때문에 뭐 하나 로딩할라 치면 백여개의 서브 오브젝트가 생성되고, 로딩되어야 하는 실정.


그러니까, 이 고구마 줄기를 위의 슈도 코드에 반영하면, 이렇게 된다.


load( name )

{

o = create( name );

for each property in o

   read( property );

   if ( property is a object )

        load( property.name );

postload();

}


재귀적 구조 때문에 실제 타임라인은 이렇게 된다.

 A생성

A읽기 

A중단 

.... 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 B생성

B읽기 

B중단 

.... 

.... 

 B읽기

 B중단

.... 

 

 

 

 

.... 

 B읽기

 

 

 

 

C생성

C읽기 

C정리 

 

 D생성

 D읽기

D중단 

..... 

.... 

 D읽기

 D정리

 

 

 

 

 

 

 

 

 

 

 

 E생성

E읽기 

E정리 

 

 

 


이런 단계를 거쳐도 아직 A가 완료되려면 멀었다 ....


위 그림에서 '읽기' 단계의 길이가 좀 짧게 그려진 감이 있는데, 실제로는 모든 '읽기' 단계는 10~30배 가량 길다고 봐야 한다.  물론, 이런 오퍼레이션과 메인 쓰레드의 동작이 concurrent하게 진행될 수 있다면 아무 문제가 없지만, 불행히도 회사에서 쓰고 있는 언리얼 엔진은 모든 오브젝트를 하나의 거대한 전역 배열에 담아두고 사용하기 때문에, 그 배열에 변경을 가하고자 할 때에는 메인 쓰레드에 락을 걸고 붙잡고 있어야 한다. 게다가 이론상 '생성' 단계가 끝나면 전역 배열의 락은 풀어도 되지만, 우리 게임은 불행한 역사적 사건에 의해 '정리'단계가 끝날 때 까지 락을 잡고 있어야만 하는 상황이었다. 이래서는 뭐 하나 로딩하고자 할 때 필요한 시간이 10ms가 될지 100ms가 될지 아무런 보장도 할 수 없다.

G사에서 처음 로딩을 손볼 때에는 이런 구조를 그냥 놔 둬도 시간 제약이 좀 널럴한 편이어서 그냥저냥 쓸만 했는데, 시간 제약이 빡빡한 상황에서 그대로 쓸 수는 없다.

결국 시간을 잡아먹는 것은 '읽기' 단계인데, 이 '읽기' 단계에서 디스크에서 읽어올 내용을 모아서 한번에 처리해서 메모리에 담아 두었다가, 실제 '읽기' 단계에서 메모리 복사로 대체하면 필요한 락의 길이를 대폭 줄일 수 있다는 것이 내가 고친 내용의 핵심이다.

그런데 문제가 하나 있다. 디스크의 어디에서 무엇을 읽어올 지는 오브젝트를 하나하나 까보기 전에는 모른다는 점.

 - 계속

Posted by uhm