dev.log2013. 10. 21. 21:32

part 1에서, 언리얼 리소스 로딩은 재귀적 알고리듬으로 짜여 있다고 한 바 있다. 재귀적 알고리듬 자체는 문제가 아니지만, 내가 할 작업에 있어서는 저런 큰 덩어리의 작업이 재귀적으로 짜여 있으면 락을 큰 단위로만 걸어야 하므로 문제가 있다. 게다가 읽기 작업이 언제 어디서 몇회나 일어날 지 모르므로, 이에 대한 컨트롤을 위해서 재귀적 알고리듬을 반복적 알고리듬으로 바꿔야만 했다.


재귀적 알고리듬을 반복적 알고리듬으로 바꾸는 건, 대학교 2학년때 배우는 알고리듬 수업만 해도 충분하다. 펑션 파라미터를 모아서 스택을 구성하고, 재귀 호출이 일어날 때 스택에 파라미터를 push하고 루프를 돌리면 된다. 이 과정을 슈도 코드로 써보면 이렇게 된다.


load( name )

{

stack.push( name )

while !stack.empty()

{

o = create( stack.top );

for each property in o

read( property );       // 디스크 읽기!!!

if ( property is a object )

push( property.name ); // 재귀호출이 일어나는 곳은 스택으로 대체


if ( stack.top == o.name )

o.postload();

stack.pop();

}

}


그리고 이 수도코드에 크리티컬 섹션으로 락을 걸면 실제 프로젝트에 들어가 있는 코드와 대충 비슷하다. 이제 문제는 실제 읽기 작업을 크리티컬 섹션 밖에서 할 수 있도록 빼는 것이다. 이 작업의 핵심 아이디어는, 실제 파일에서 읽어들인 내용물을 메모리 버퍼에 담아놓고 오브젝트 로딩 과정에서는 디스크보다는 속도가 훨씬 빠른 메모리에서 값을 가져오도록 하는 것이다.
(load_buffer는 락 바깥에 있어야 한다는 점에 주의!!!)


load( name )

{

stack.push( name )

while !stack.empty()

{

buffer = load_buffer( stack.top ); // 디스크 읽기!!

lock();

o = create( stack.top );

for each property in o

buffer.read( property ); // 메모리 복사

if ( property is a object )

push( property.name );


if ( stack.top() == o.name )

o.postload();

stack.pop();

unlock();

}

}


이제 남은 문제는 버퍼를 어디에서 읽어야 하는지를 파고들어가서 그걸 내손으로 구현하는 일이다.


언리얼 엔진에서 쓰는 패키지 파일을 설계한 아저씨는 (아마도 팀 스위니겠지만) 여러 면에서 DLL에 대한 은유를 많이 사용했다. 각 패키지 안에 들어있는 모든 오브젝트는 외부 패키지에서 접근할 수 있도록 export 테이블에서 노출되고, 또한 다른 패키지에서 참조하는 오브젝트는 import 테이블을 통해 관리한다. 이들 오브젝트의 레퍼런스를 resolve해주는 과정을 link라고 부르는 것은 덤.


언리얼 패키지에서 오브젝트 하나를 로딩하는 절차는 UDN에 대충 나와 있긴 하고, 소스 코드를 보면 누구나 알 수 있긴 하지만, 정리해 보면,


(시작)

1> 오브젝트 이름에서 패키지 이름 추출

2> 패키지 summary 읽기 

3> 패키지 export 테이블 읽기 

4> 패키지 import 테이블 읽기

5> 패키지 export 테이블에서 오브젝트 검색하고 대상 오브젝트 생성

6> export 테이블에서 지정된 대로 오브젝트 읽기

7> 읽는 동안 패키지 내부의 오브젝트 참조가 발생하면 export 테이블에서 검색

8> 읽는 동안 외부 패키지의 오브젝트 참조가 발생나면 import 테이블에서 검색

9> 모든 참조가 resolve되면 종료.


part 1에서 생성 -> 읽기 -> 정리의 과정은 위에서 5~9 사이의 과정이라고 할 수 있다. 물론, 2~4 사이의 과정은 패키지가 이미 열려져 있는 상태라면 생략되도록 구성되어 있다. (에픽 아저씨들이 바보는 아니니까) 이 절차를 정리해 본 것은, 패키지 summary와 export/import  테이블은 언리얼 리소스 로딩 과정을 개조하기 위해 필수적으로 건드려야 하는 개념이기 때문이다.


summary에는 각 오브젝트의 정보보다는, 패키지 자체에 대한 정보가 있지만, 여기서 가장 중요한 것은 export/import 테이블의 옵셋과 사이즈를 알려준다는 점이다. export 테이블에는 패키지 내에 저장돼 있는 모든 오브젝트의 옵셋과 사이즈, 이름, 클래스 등에 대한 정보가 담겨 있다. 오브젝트를 로딩하기 위한 핵심 정보이다. import 테이블은 전체적으로는 export 테이블과 비슷한데, 차이점이라면 외부 패키지의 오브젝트에 대한 참조이기 때문에 패키지 이름이 추가돼 있다는 정도?


이제 읽어야 할 것에 대한 정보가 어디 들어있는지를 알았으므로, 이 지식을 코드에 반영하면 된다.


load_buffer( name )

{

if ( name is internal )

e = export.find( name )

else

e = import.find( name )


buffer = create( e.size );

read( buffer, e.offset, e.size );

return buffer;

}


여기까지 하고, load 펑션을 백그라운드 쓰레드로 돌리면, 디스크 읽기로 인해 메인 쓰레드가 멈추는 일 없이, 실제 공유자원인 전역 오브젝트 배열에 대한 락에 필요한 시간보다 크게 길지 않게 락을 잡고 작업을 병렬화 할 수 있을 것이고, 실제로 동작하긴 했다.


그런데 무수히 많은 크래쉬와 데드락이 일어났다.


- 계속



Posted by uhm