몇 번인가 언급한적 있는데 DX12에선 모든 리소스가 64KB얼라인을 사용한다. 이게 문제가 뭐냐면 4Bytes짜리 Vertex Buffer 한개를 만들어도 내부적으로 64KB짜리 버퍼가 할당된다. 처음부터 Vertex Buffer, Index Buffer를 많이 사용하는게 아니라면 별 문제가 아니지만 지금 진행중인 Voxel Horizon처럼 오브젝트를 2만개 이상 할당하는 경우는 크게 문제가 된다.
실제로 크게 문제가 됐다. DX11렌더러가 이미 잘 작동하고 있으므로 DX12렌더러를 그냥 버릴까도 생각했다. 하지만 그간 짠 코드가 아깝기도 하고, 이 문제를 잘 해결하면 결과적으로 다른 API의 렌더러에도 도움이 될것 같아서 이 문제를 해결하기로 했다.
방법은 간단하다.
D3DResource(Buffer)를 생성할때 64KB단위로 만든다. 그리고 이걸 heap에다 맵핑한다. heap이란 흔히 사용하는 malloc/free, new/delete로 사용하는 동적 메모리 할당 구조를 말한다. heap은 기본적으로 큰 메모리 블럭 안에서 최대한 남김없이 메모리를 할당해준다. 물론 메모리를 해제하면 인접 블럭을 체크해서 해제되어있는 경우 병합해서 더 큰 메모리 블럭으로 만든다.
이러한 heap구조를 만들어서64KB 단위로 할당한 D3DResource를 여기에 맵핑할 것이다.
내가 heap을 만든다고 하면 malloc/free보다 잘 만들순 없을것이다. 하지만 그래도 직접 만들어야 한다. 왜냐하면 malloc을 호출하면 실제로 메모리를 할당-소모 하기 때문이다. 필요한건 단지 어드레스 뿐이다. 시스템 메모리를 마구 낭비해도 좋다면 malloc을 이용해도 된다. 하지만 내가 원하는 것은 그것이 아니다.
작업 순서를 보자.
- Heap자료구조를 만든다.
- Alloc(size N)을 호출하면 메모리를 할당해주는게 아니고 base address를 주면 그에 대한 상대주소를 리턴한다.
- 다양한 사이즈를 할당할 수 있어야한다.
- 해제시 인접 블록이 해제된 상태라면 병합할 수 있어야 한다.
- Heap의 어드레스 범위는 0 – D3DResource의 size * 버퍼의 최대 개수
- 예를 들어 D3DResource(Buffer)를 64KB씩 할당해서 최대 1024개를 사용한다면 heap의 addres범위는 0 – 65536*1024이다.
- D3DResource의 최대개수만큼의 ptr 배열을 만든다. 정확히는 포인터와 ref count를 담을 수 있는 구조체 배열이다.
- Vertex Buffer or Index Buffer를 할당할때 위에서 만든 Heap->Alloc() 호출
- 이렇게 얻은 address – base addr(일반적으로 0)로 상대 address를 얻는다. 상대 address로 맵핑될 D3DResource의 index를 결정한다.
- 선택된 D3DResource의 ptr배열이 null인지 체크. Null이면 D3D API를 사용해서 D3DResource(Buffer)를 만든다.
- Null이 아닌 경우 D3DResource의 ref count증가.
- VB_DESC에 D3DResource의 ptr을 설정.
- 상대 address를 이용해서 VB_DESC에 StartIndex도 설정.
- VB_DESC를 리턴.
- Draw…()호출할때 VB_DESC의 D3DResource의 ptr과 StartIndex를 파라미터로 넣어준다.
- 해제하는 경우 heap으로부터 할당할때와 마찬가지로 D3DResource ptr배열에서 해당하는 D3DResource Index를 찾은 후 ref count를 감소시키고 0이 되면 해당 D3DResource를 Release()한다.
이렇게 해서 IndexBuffer와 Vertex버퍼로 잡아먹던 2GB이상의 GPU메모리를 300MB이하로 줄였다.
또한 D3DResource생성(CreateCommitedResource)과 해제(Release)에 걸리는 시간이 꽤 길었는데 이 또한 대폭 줄었다.
이 기법은 D3D12 외에 D3D9/11에서도 사용 가능하다. DX11에선 굳이 사용할 필요가 없지만 D3DResource 생성/해제에 걸리는 시간은 줄일 수 있다.