-
[UE5] D3D12 ResourceAllocation 리뷰(2/2)UE4 & UE5/Rendering 2023. 8. 24. 22:43
[UE5] D3D12 ResourceAllocation 리뷰 (2/2)
최초 작성 : 2023-08-24
마지막 수정 : 2023-08-24
최재호목차
1. 목표
2. 내용
2.1. Non-TransientHeap 방식
2.1.1. 리소스 할당 코드
2.1.1.1. FD3D12Buffer 생성2.1.1.2. FD3D12Texture 생성
2.1.1.3. FD3D12UniformBuffer2.1.1.4. FD3D12UploadHeapAllocator 의 메모리 할당
2.1.1.5. FD3D12DefaultBufferAllocator 의 메모리 할당
2.1.1.6. FD3D12TextureAllocatorPool 의 메모리 할당
2.1.1.7. FD3D12FastConstantAllocator 의 메모리 할당2.1.2. 리소스 해제 코드
2.1.2.1. eStandAlone
2.1.2.1. eSubAllocation, AT_Pool
2.1.2.2. eSubAllocation, AT_Default
2.1.2.3. eFastAllocation2.1.2.5. eHeapAliased
2.1.2.6. DeferDelete 된 리소스의 해제
2.1.2.7. EndFrame 에서의 리소스 해제
2.1.2.8. FD3D12UploadHeapAllocator 의 리소스 해제
2.1.2.9. FD3D12TextureAllocatorPool 의 리소스 해제
2.1.2.10. FD3D12TextureAllocatorPool 의 리소스 해제
2.2. TransientHeap 방식 (RDG 에서 사용)
2.2.1. 리소스 할당 코드2.2.1.1. FRDGTexture, FRDGBuffer 생성
2.2.1.2. FRDGUniformBuffer 생성
2.2.2. 리소스 해제 코드
2.2.2.1. FRDGTexture, FRDGBuffer 의 리소스 해제
2.2.2.2. 사용하지 않는 리소스들의 주기적인 정리
3. 레퍼런스1. 목표
이전 글에서는 UE5 의 D3D12 Buffer, Texture 생성/해제에 필요한 클래스 UML 을 봤습니다. 이번 글에서는 이전 글에서 얻은 클래스 관계를 기반해서 실제 코드가 어떻게 실행되는지 보면서 실제 필요한 메모리 할당/해제 방식을 확인해 봅시다.
이 글은 총 2개로 구성되어 있습니다.
'[UE5] D3D12 ResourceAllocation 리뷰 (1/2)' 는 리소스 생성에 필요한 클래스들의 관계 확인합니다.
'[UE5] D3D12 ResourceAllocation 리뷰 (2/2)' 는 실제 리소스 할당 코드에서 리소스 할당/해제 방식을 확인합니다.첫 번째 글을 먼저 읽고 보시는 것을 추천해 드립니다.
이 글을 UE5.2 (407acc04a93f09ecb42c07c98b74fd00cc967100) 코드를 기반으로 작성되었습니다.
사전지식
DirectX12 의 ID3D12Resource 에 관해
UE4/5 RDG(RenderDependencyGraph)훑어보는 코드의 양이 상당히 많습니다. 혹시 코드의 설명과 코드의 본문을 서로 교차해서 보기에 부담스러울 만큼 긴 경우 창을 두 개 띄워서 하나는 설명 하나는 코드를 보는 것을 추천해 드립니다.
개인적으로 분석한 내용이라 틀린 점이 있을 수 있습니다. 그런 부분은 알려주시면 감사하겠습니다.
2.1. Non-TransientHeap 방식
2.1.1. 리소스 할당 코드
2.1.1.1. FD3D12Buffer 생성
- RHIStructuredBuffer 와 같은 함수를 사용해 버퍼를 만들게 되면 DirectX12 의 경우 CreateD3D12Buffer 함수를 통해 버퍼를 생성합니다.
- CreateD3D12Buffer 에서는 BUF_AnyDynamic 옵션 여부에 따라서 Upload or Default heap 을 설정합니다.
- BUF_AnyDynmaic 여부에 상관없이 결국 동일한 AllocatorBuffer 함수를 실행합니다.
- AllocateBuffer 내에서는 BUF_AnyDynmaic 의 경우 FD3D12UploadHeapAllocator 로부터 메모리를 할당하는 것을 볼 수 있습니다. 그림4 에서 FD3D12UploadHeapAllocator 의 메모리 할당 코드를 계속해서 추적합니다.
- ResourceAllocator 가 있는 경우는 TransientHeap 방식의 Allocator 를 사용하는 경우입니다. 여기서는 사용하지 않습니다. 이 부분의 처리는 '2.2. TransientHeap 방식 (RDG 에서 사용)' 에서 확인해 봅시다.
- FD3D12DefaultBufferAllocator 로 부터 메모리를 할당하는 것을 볼 수 있습니다. 그림5 에서 FD3D12DefaultBufferAllocator 의 메모리 할당 코드를 계속해서 추적합니다.
2.1.1.2. FD3D12Texture 생성
- 텍스처 생성의 경우 결국 CreateD3D12Texture 함수로 들어옵니다.
- 텍스쳐의 옵션에 따라 Clear 값이 필요한 경우 설정해 줍니다.
- FD3D12Texture 를 생성합니다. 이 객체의 생성 단계에서 실제 메모리가 할당되진 않기 때문에 이후 과정에서 메모리 할당을 시작합니다.
- ResourceAllocator 가 있는 경우는 TransientHeap 방식의 Allocator 를 사용하는 경우입니다. 여기서는 사용하지 않습니다. 이 부분의 처리는 '2.2. TransientHeap 방식 (RDG 에서 사용)' 에서 확인해 봅시다.
- 3D Texture 를 사용하는 경우는 FD3D12TextureAllocatorPool 를 사용해 메모리를 할당합니다. 그림6 에서 FD3D12TextureAllocatorPool 의 메모리 할당 코드를 계속해서 추적합니다.
- SafeCreateTexture2D 의 경우 ‘Readback 가능한 텍스처 인가?’ 여부에 따라서 생성 방식이 틀려집니다.
- 텍스처 옵션의 Readback 가능 여부를 확인하고 그에 맞는 Heap 타입을 선택합니다.
- Readback 가능한 경우 CreateBuffer 를 사용하여 메모리를 할당받습니다. 그리고 FD3D12Resource 를 eStandAlone 으로 설정합니다. 이때 OutTexture2D 는 FD3D12ResourceLocation 입니다.
- Readback 하지 않는 경우는 3D Texture 와 같이 FD3D12TextureAllocatorPool 를 사용해 메모리를 할당합니다. 그림6 에서 FD3D12TextureAllocatorPool 의 메모리 할당 코드를 계속해서 추적합니다.
2.1.1.3. FD3D12UniformBuffer 생성
- RHICreateUniformBuffer 를 사용해 UniformBuffer 를 생성합니다.
- FD3D12UniformBuffer 를 생성했지만 이 객체는 실제 메모리는 FD3D12ResourceLocation 에 할당될 것이기 때문에 코드를 좀 더 진행해 봅시다.
- EUniformBufferUsage::UniformBuffer_MultiFrame 인 경우 FD3D12UploadHeapAllocator 를 사용하여 메모리를 할당합니다. 그림4 에서 FD3D12UploadHeapAllocator 의 메모리 할당 코드를 계속해서 추적합니다.
- EUniformBufferUsage::UniformBuffer_SingleFrame 또는 EUniformBufferUsage::UniformBuffer_SingleDraw 인 경우 FD3D12FastConstantAllocator 를 사용하여 메모리를 할당합니다. FD3D12FastConstantAllocator 도 결국 내부에서 메모리를 할당할 때, FD3D12UploadHeapAllocator 의 FastConstantPageAllocator(FD3D12MultiBuddyAllocator) 를 사용하여 메모리를 할당합니다.
2.1.1.4. FD3D12UploadHeapAllocator 의 메모리 할당
FD3D12UploadHeapAllocator 의 AllocUploadResource 함수를 따라가 봅시다.
1. AllocUploadResource 함수를 통해서 메모리 할당을 시작합니다. UploadHeap 은 3개의 Allocator 를 가지는데 여기서는 SmallBlockAllocator, BigBlockAllocator 위주로 먼저 알아봅니다. 나머지 하나인 FD3D12FastConstantAllocator 는 '2.1.1.7. FD3D12FastConstantAllocator 의 메모리 할당' 를 확인해 주세요.
2. GD3D12UploadHeapSmallBlockMaxAllocationSize(65536) 이하의 메모리 할당 요청은 SmallBlockAllocator 를 통해서 메모리를 할당합니다.
2.1. TryAllocate 를 따라가 봅시다.SmallBlockAllocator 는 FD3D12MultiBuddyAllocator 입니다. 이 Allocator 는 여러 FD3D12BuddyAllocator 를 관리하며 메모리를 할당합니다.
2.2. 보유하고 있는 FD3D12BuddyAllocator 중 할당 가능한 것이 있으면 할당합니다.
2.3. 만약 할당 가능한 Allocator 가 없다면 새로운 FD3D12BuddyAllocator 를 할당하고 거기서 메모리를 할당합니다.
3. GD3D12UploadHeapSmallBlockMaxAllocationSize(65536) 초과된 메모리 할당은 요청은 BigBlockAllocator 를 통해서 메모리를 할당합니다.
3.1. BigBlockAllocator 의 AllocateResource 를 따라가 봅시다.
3.2. GD3D12UploadHeapBigBlockMaxAllocationSize(64 * 1024 * 1024) 이하의 메모리만 PoolAllocator 를 통해서 할당 가능하며, PoolAllocator 사용 가능하면서 AllocateStrategy 가 kPlacedResource 라면 CreatePlacedResource 로 ID3D12Heap 의 일부분을 ID3D12Resource 로 생성합니다. kManualSubAllocation 타입도 있는데, 이 타입의 경우 한개의 ID3D12Resource 를 할당한 다음 Offset 을 사용하여 적절하게 잘라서 사용합니다.
3.3. PoolAllocator 를 사용 가능하면 eSubAllocation 방식으로 지정되며, 그렇지 않은 경우는 PoolAllocator 를 사용할 수 있는 조건이 안되 상화일 것이므로 eStandAlone 방식으로 할당합니다.
3.4. PoolAllocator 를 사용 가능 상황을 먼저 확인해봅시다. FD3D12ResourceLocation 에 eSubAllocator 로 설정하고, 현재 사용중인 PoolAllocator 를 저장합니다.
3.5. AllocateStrategy 가 kManualSubAllocation, kPlacedResource 여부에 따라서 적절하게 리소스를 할당합니다.
3.6. PoolAllocator 를 사용하지 않는 경우는 CreatePlacedResource를 사용하는 경우도 단독 Heap 을 따로 생성해서 사용합니다.
3.7. 3.6 에서 CreatePlacedResource 를 사용하지 않는 경우 CreateCommittedResource 를 사용합니다. 이 경우는 암묵적으로 이 Heap 을 만들어 단독으로 사용합니다.
3.8. PoolAllocator 를 사용하지 않는 경우 단독 리소스이기 때문에 eStandAlone 으로 설정합니다.
2.1.1.5. FD3D12DefaultBufferAllocator 의 메모리 할당
다음으로 FD3D12DefaultBufferAllocator 의 AllocDefaultResource 함수를 따라가 봅시다.
1. AllocateDefaultResource 함수는 Upload Buffer 타입이 아닌 버퍼 리소스를 할당하는 데 사용됩니다. 다양한 버퍼 타입을 생성할 수 있습니다.
2. FD3D12DefaultBufferAllocator 는 여러 개의 FD3D12BufferPool 을 관리합니다. 이 때 FD3D12BufferPool 은 FD3D12PoolAllocator 를 사용합니다.
3. 현재 보유 중인 FD3D12PoolAllocator 가 요청받은 타입의 버퍼 리소스를 할당할 수 있는지 확인합니다.
4. 만약 요청받은 타입의 버퍼를 생성을 지원하는 FD3D12PoolAllocator 가 없다면 새로 만듭니다.
5. FD3D12PoolAllocator 의 AllocDefaultResource 를 호출하에 메모리를 할당합니다. AllocDefaultResource 는 결국 AllocateResource 함수를 그대로 호출합니다. AllocateResource 함수의 처리 과정은 그림4 의 3.1. 에서 봤었습니다.
2.1.1.6. FD3D12TextureAllocatorPool 의 메모리 할당
다음으로 FD3D12TextureAllocatorPool 와 리소스 할당에 대해서 알아봅시다. 이 Allocator 는 미리 지원 가능한 텍스쳐 타입의 PoolAllocator 를 만들어 두고 시작합니다. PoolAllocator 할당 코드와 리소스 할당 코드를 차례로 확인해 봅시다.
1. 실제 텍스쳐 메모리를 할당하는 함수인 AllocateTexture 입니다.
2. 4 종의 FD3D12PoolAllocator 의 타입입니다
3. 4 종의 FD3D12PoolAllocator 를 담는 배열입니다.
4. FD3D12TextureAllocatorPool 의 생성자에서 FD3D12PoolAllocator 를 생성합니다.
5~8. 각각의 타입에 맞는 FD3D12PoolAllocator 를 생성합니다.
9. AllocateTexture 함수를 사용하여 텍스쳐 생성에 필요한 리소스를 할당합니다.
10. FD3D12TextureAllocatorPool 가 소유한 FD3D12PoolAllocator 중 텍스쳐 타입에 따라 적절한 FD3D12PoolAllocator 를 선택합니다. 그 후 FD3D12PoolAllocator 의 AllocateResource 함수를 호출하여 필요한 리소스를 생성합니다. FD3D12PoolAllocator 의 AllocateResource 는 그림4 의 3.1 에서 봤었습니다.
2.1.1.7. FD3D12FastConstantAllocator 의 메모리 할당
마지막으로 FD3D12FastConstantAllocator 리소스 할당 방식을 확인해 봅시다. 이 할당 방식은 UniformBuffer_SingleFrame, UniformBuffer_SingleDraw 에서 사용되는 방식입니다.
1. Allocate 함수를 사용하여 요청한 크기의 리소스를 할당받습니다.
2. UnderlyingResource 는 현재 사용 중인 리소스입니다. 이 리소스는 GD3D12FastConstantAllocatorPageSize(65536) 크기로 할당되며, 요청에 따라 이 리소스의 일부분을 할당합니다. Offset 은 현재 사용가능한 메모리의 위치를 가리킵니다. 메모리 할당 요청이 들어오면 Offset 위치부터 할당됩니다. 이런 방식은 kManualSubAllocation 과 같은 방식으로 보입니다.
3. 요청된 메모리가 할당 가능 범위를 벗어난 경우 FD3D12UploadHeapAllocator 의 FastConstantPageAllocator(FD3D12MultiBuddyAllocator) 로 부터 새로 리소스를 할당합니다. 이때 이 리소스의 크기는 GD3D12FastConstantAllocatorPageSize(65536) 입니다.
4. AllocFastConstantAllocationPage 는 FD3D12UploadHeapAllocator 가 가지는 사용하는 3가지 Allocator 우리가 다루지 않았던 마지막 Allocator 입니다.(그림4 에서는 SmallBlockAllocator, BigBlockAllocator 를 봤음).
5. 여기서 ResourceLocation.Clear() 함수를 호출하면 FD3D12ResourceLocation 의 ReleaseResource 함수가 즉시 호출됩니다. 이 ResourceLocation 은 이전에 FD3D12UploadHeapAllocator 의 FastConstantPageAllocator(FD3D12MultiBuddyAllocator) 에서 할당했던 리소스가 담겨있습니다. FD3D12MultiBuddyAllocator 에서 할당 리소스의 해제는 '2.1.2.3. eSubAllocation, AT_Default' 에서 확인할 수 있습니다.
6. 새로운 GD3D12FastConstantAllocatorPageSize(65536) 크기의 리소스를 할당합니다.
7. 새로 할당한 UnderlayingResource 로부터 필요한 만큼의 메모리를 할당받습니다. 그리고 eFastAllocation 으로 설정합니다.
2.1.2. 리소스 해제 코드 파악
FD3D12Buffer, FD3D12Texture, FD3D12UniformBuffer 가 소멸되는 경우 각 클래스들이 소유하고 있던, FD3D12ResourceLocation 도 함께 소멸될 것입니다. 이 시점에서 할당되었던 메모리는 해제됩니다. 해제 방식은 ResourceLocationType 에 따라 달라지는데 처리 방법이 어떻게 다른지 확인해 봅시다.
2.1.2.1. eStandAlone
- eStandAlone 방식의 소멸은 이 부분으로 들어옵니다.
- 대부분의 경우 DeferDelete() 를 호출하는 쪽으로 실행됩니다.
- DeferDelete() 하지 않는 경우는 여기서 즉시 리소스 해제를 실행합니다.
- DeferDelete 가 실행되면 FD3D12DynamicRHI::DeferredDelete 가 호출 차례로 호출됩니다.
- ObjectsToDelete 에 지워질 리소스를 등록합니다. 이후 과정은 그림13 에서 알아봅시다.
2.1.2.2. eSubAllocation, AT_Pool
- eSubAllocation 방식의 소멸은 이 부분으로 들어옵니다. eSubAllocation 은 FD3D12PoolAllocator 또는 FD3D12MultiBuddyAllocator 으로 메모리가 할당된 경우입니다.
- AT_Pool 로 설정된 경우, FD3D12PoolAllocator 입니다. 리소스 해제는 즉시 일어나지 않고 FrameFencedOperations 에 등록하여 지연시킵니다. 이후 과정에 FrameFencedOperations 를 한 번에 처리하는데 그림15 의 2.1. 에서 확인해 봅시다.
2.1.2.3. eSubAllocation, AT_Default
- eSubAllocation 방식의 소멸은 이 부분으로 들어옵니다. eSubAllocation 은 FD3D12PoolAllocator 또는 FD3D12MultiBuddyAllocator 으로 메모리가 할당된 경우입니다.
- AT_Default 로 설정된 경우, Allocator 는 FD3D12BuddyAllocator 입니다. 이 경우도 즉시 리소스를 해제하지 않고, DeferredDeletionQueue 에 등록하여 지연시킵니다. 이후 과정에서 DeferredDeletionQueue 를 한 번에 처리하는데 그림15 의 1.1. 에서 확인해 봅시다.
2.1.2.4. eFastAllocation
- eFastAllocation 방식의 소멸은 여기서 아무것도 하지 않습니다. 이전 글의 ‘2.1.2. 리소스 해제 방식 정리’ 의 eFastAllocation 에서 해제 관련 설명을 확인할 수 있습니다. 대신 다른 곳에서 할당된 리소스를 정리합니다. 바로 그림7 의 3 과정에서 할당할 메모리가 부족할 때 해제 요청을 합니다. 이 리소스는 FD3D12FastConstantAllocator(FD3D12MultiBuddyAllocator) 로부터 할당받은 리소스를 사용합니다. FD3D12MultiBodyAllocator 의 리소스 해제 방식은 그림15 의 1.1 에서 확인할 수 있습니다.
2.1.2.5. eHeapAliased
1. 이 타입의 경우 TransientHeap 에서 부터 할당된 리소스입니다. 해제 방식은 그림8 에서 본 eStandAlone 방식과 동일합니다.
2.1.2.6. DeferDelete 된 리소스의 해제
FD3D12ResourceLocation 에서 eStandAlone, eHeapAliased 타입의 경우 DeferDelete 함수를 통해 ObjectsToDelete 등록됩니다. 추후에 한 번에 리소스를 해제시키는데, 어떤 과정을 거치는지 확인해 봅시다. FlushPendingDeletes 함수가 호출되면 지워야 할 리소스들을 정리하는데, FlushRenderingCommands 를 호출하거나 ImmeidateFlush(EImmediateFlushType::FlushRHIThreadFlushResources() 를 호출해도 결국 FlushPendingDeletes 를 호출하여 지울 리소스를 정리합니다.
1. FlushRenderingCommands() 에서는 ImmeidateFlush(EImmediateFlushType::FlushRHIThreadFlushResources() 를 호출합니다.
2. 계속해서 FlushPendingDeletes 가 호출됩니다.
3. FlushPendingDeletes 에서는 RHIPerFrameRHIFlushComplete 가 호출됩니다.
4. 해제가 지연되어 있는 리소스를 갖고 있는 ObjectsToDelete 배열을 Local 배열로 옮겨 담습니다.
5. EnqueueEndOfPipeTask 함수에 전달하여 현재 갖고 있는 모든 GPU Queue 에 제출되었던 작업들이 완전히 실행을 마친 뒤 ObjectsToDelete 에 있는 리소스를 해제하도록 합니다. ObjectsToDelete 의 해제는 EnqueueEndOfPipeTask 에 전달된 람다 함수를 통해서 수행됩니다.
2.1.2.7. EndFrame 에서의 리소스 해제
계속해서 eSubAllocation 으로 생성된 타입이 어떤 식으로 지연된 리소스들을 해제시키는지 확인해 봅시다. 현재 프레임의 끝에서 정리할 수 있는 리소스를 정리합니다. RHIEndFrame 에서 코드가 시작됩니다.
1. FD3D12UploadHeapAllocator 의 메모리 정리는 CleanUpAllocations 함수로 수행됩니다. ParaentAdaptor->EndFrame 를 호출하면, 결구 FD3D12UploadHeapAllocator 의 CleanUpAllocations 가 호출되며 여기서 해제 가능한 리소스를 정리합니다. FD3D12UploadHeapAllocator.CleanUpAllocations 함수의 진행은 그림15 에서 확인했습니다.
2. FD3D12TextureAllocatorPool 의 메모리 정리는 CleanUpAllocations 함수로 수행됩니다. FD3D12TextureAllocatorPool 는 FD3D12PoolAllocator 만 갖고 있습니다. CleanUpAllocations 함수의 진행은 그림15 의 2.1 에서 확인했습니다.
3. FD3D12DefaultBufferPool 의 메모리 정리는 CleanupFreeBlocks 함수로 수행됩니다. FD3D12DefaultBufferPool 또한 FD3D12PoolAllocator 만 갖고 있습니다. CleanupFreeBlocks 함수 내부에서 호출되는 FD3DPoolAllocator.CleanUpAllocations 진행은 그림15 의 2.1 에서 확인했습니다.
2.1.2.8. FD3D12UploadHeapAllocator 의 리소스 해제
FD3D12UploadHeapAllocator 의 리소스 해제 과정입니다. 여기서는 FD3D12UploadHeapAllocator 은 FD3D12PoolAllocator(파랑네모) 와 FD3D12MultiBuddyAllocator(빨강네모) 둘 다 사용합니다.
1. FD3D12MultiBuddyAllocator 의 리소스 해제 과정을 먼저 확인해 봅시다. SmallBlockAllocator, FastConstantPageAlloctor 둘 다 FD3D12MultiBuddyAllocator 입니다.
1.1. FD3D12MultiBuddyAllocator 의 CleanUpAllocations 를 호출합니다.
1.2. FD3D12MultiBuddyAllocator 는 필요에 따라 여러 FD3D12BuddyAllocator 를 관리합니다. 각각의 FD3D12BuddyAllocator 에 CleanUpAllocations 를 호출합니다. 이 곳에서 DeferredDeletionQueue 담겨 있는 리소스의 해제 작업을 수행합니다.
1.3. FD3D12BuddyAllocator Allocator 를 정리한 뒤 비어있는 FD3D12BuddyAllocator 를 정리해줍니다. 바로 제거하지 않고, 마지막으로 사용한 시간 이후로 FrameLag 만큼 기다려도 더 이상 FD3D12BuddyAllocator 를 사용하지 않는 경우 제거합니다.
1.4. FD3D12BuddyAllocator 의 CleanUpAllocations 으로 가봅시다.
1.5. DeferredDeletionQueue 는 해제시킬 리소스들을 순회합니다.
1.6. FrameFence 를 사용하여 이 리소스를 사용한 프레임의 실행이 완료되었는지 확인합니다. 사용 완료 된 경우 DeallocateInternal 함수를 호출하여 최종적으로 리소스를 해제합니다.
1.7. DeallocateInternal 내부로 들어왔습니다.
1.8. DeallocateBlock 을 호출하여 현재 할당받은 메모리 블럭을 해제합니다.
1.9. kManualSubAllocation 의 경우라면 1 개의 ID3D12Resouce 를 할당하여 Offset 을 사용하여 SubAllocation 을 만들어내기 때문에 리소스 해제는 FD3D12BuddyAllocator 가 해제 될 때 수행되면 될 것입니다. kPlacedResource 타입의 경우 SubAllocation 마다 ID3D12Resource 가 있기 때문에 여기서 최종적으로 리소스를 해제합니다.
2. 다음으로 FD3D12PoolAllocator 를 확인해 봅시다.
2.1. FD3D12PoolAllocator 의 CleanUpAllocations 로 진입합니다.
2.2. FD3D12PoolAllocator 는 FrameFencedOperations 에 해제할 리소스가 담겨있고 이것을 순회합니다.
2.3. FrameFencedOperations 도 마찬가지로 FrameFence 를 사용하여 이 리소스를 사용한 프레임의 실행이 완료되었는지 확인합니다.
2.4. 사용 완료된 경우 DeallocateInternal 함수를 호출하여 할당했던 메모리 블럭을 반환합니다.
2.5. 여기서도 1.9 에서 본 것과 마찬가지로 kPlacedResource 타입의 경우만 리소스 해제를 진행합니다.
2.6. FD3D12PoolAllocator 도 필요에 따라 추가 FD3D12MemoryPool 을 할당할 수 있습니다. 메모리 블럭을 해제 한 다음 FD3D12MemoryPool 이 비어있고, FrameLag 시간만큼 사용되지 않는다면 FD3D12MemoryPool 는 제거됩니다.
2.1.2.9. FD3D12TextureAllocatorPool 의 리소스 해제
FD3D12TextureAllocatorPool 의 리소스 해제 과정입니다. 모든 PoolAllocators 가 FD3D12PoolAllocator 로 구성되어 있습니다. FD3D12PoolAllocator 의 리소스 해제 과정은 그림15 의 2.1. 부분에서 확인했었습니다.
2.1.2.10. FD3D12TextureAllocatorPool 의 리소스 해제
FD3D12DefaultBufferAllocator 의 리소스 해제 과정입니다. 여기에 나오는 FD3D12BufferPool 은 특별한 설정을 하지 않았다면 FD3D12PoolAllocator 입니다. FD3D12PoolAllocator 의 리소스 해제 과정은 그림15 의 2.1. 부분에서 확인했었습니다.
2.2. TransientHeap 방식 (RDG 에서 사용)
2.2.1. 리소스 할당 코드
2.2.1.1. FRDGTexture, FRDGBuffer 생성
FRDGTexture, FRDGBuffer 의 생성
FD3D12TransientResourceHeapAllocator 를 통해서 FRHITransientTexture, FRHITransientBuffer 를 생성하는 과정은 BeginResourceRHI 함수에서 수행됩니다. Texture, Buffer 모두 거의 동일한 방식으로 생성하기 때문에 Texture 부분만 확인해 봅시다.
그림18 를 보면 FD3D12TransientResourceHeapAllocator 에서 부터 FRHITransientTexture 를 생성하는 것을 볼 수 있습니다. 그런 뒤 SetRHI 함수를 호출하여 FRDGTexture 의 ResourceRHI 와 TransientTexture 에 생성한 FRHITransientTexture를 할당하는 것을 볼 수 있습니다. 이렇게 할당된 FRHITransientTexture, FRHITransientBuffer 의 FD3D12ResourceLocation::ResourceLocationType 은 eHeapAliased 입니다. 이 타입은 eStandAlone 과 동일하게 DeferDelete 함수를 호출하여 해제하도록 합니다. 그래서 DeferDelete 이후 과정은 그림13 에서 확인할 수 있습니다.
1. FRDGBuilder 가 가지고 있는 TransientResourceAllocator 의 CreateTexture 함수를 호출하여 FRHITransientTexture 를 할당받습니다.
2. 할당한 FRHITransientTexture 를 FRDGTexture 에 할당합니다. 이제부터 FRDGTexture 는 실제 리소스를 소유합니다.
3. TransientResourceAllocator->CreateTexture 코드를 계속해서 따라가 봅시다.
4. 내부에서 CreateTextureInternal 을 호출합니다. 호출 시 람다함수가 하나 전달되는데 이 함수는 FRHITransientHeap::FResourceInitializer 의 Heap 을 사용하여 CreateD3D12Texture 를 호출합니다. 그림19 의 코드와 같이 ResourceAllocator 가 nullptr 이 아니면, 이 Allocator 를 통해서 리소스를 합니다. 여기서 ResourceAllocator 는 대부분의 경우 TransientResourceAllocator 입니다. 이 과정은 Buffer 를 할당할 때도 동일합니다.
5. CreateTextureInternal 로 진입합니다.
6. 이미 생성해 둔 FRHITransientHeap 이 요청받은 타입의 텍스쳐를 할당할 수 있는지 확인하고 가능하면 생성합니다.
7. 생성 불가능 한 경우 새로운 FRHITransientHeap 를 할당하여 텍스쳐를 할당합니다.
8. 텍스처 할당 과정을 보기 위해서 FRHITransientHeap::CreateTexture 로 진입합니다.
9. FRHITransientHeap 은 특이하게 생성했던 Texture, Buffer 를 직접 관리하고 있습니다. TRHITransientResourceCache<FRHITransientTexture>::Acquire 함수를 호출하여 FRHITransientTexture 를 얻어 옵니다. 캐시 된 텍스쳐가 있는 경우 그것을 사용하고 그렇지 않은 경우 코드 4에서 전달받은 람다 함수를 사용하여 새로운 리소스를 할당합니다.
10. 캐시 된 텍스쳐를 사용할지 새로 생성할지 여부를 결정하는 코드 입니다. 캐시된 텍스쳐를 사용한다면, 람다 함수 실행 없이 기존에 할당되어 있던 FRHITransientTexture 를 돌려줍니다. 캐시 된 텍스쳐가 없는 경우 전달받은 람다 함수를 사용하여 새로운 FRHITransientTexture 를 할당하여 돌려줍니다.
11. FRHITransientTexture 를 만든 뒤, FRHITransientTexture::Acquire 함수를 호출하여 이 텍스처가 사용 중이라는 정보를 설정해 줍니다.
12. 계속해서 FRHITransientResource::Acquire 함수의 내용을 봅시다.
13. DiscardPasses.Min == kInvalidPassIndex 이면 FRHITransientResource 가 사용 중인 상태입니다. 여기서는 Min, Max 둘 다 kInvalidPassIndex 로 설정하고 있습니다.
아래 코드를 보면 텍스쳐 또는 버퍼의 리소스를 생성하는 도중 ResourceAllocator 를 사용하는 경우는 대부분 TransientResourceAllocator 입니다. 텍스쳐와 버퍼 모두 TransientHeap 으로부터 리소스를 할당받을 수 있음을 알 수 있습니다.
2.2.1.2. FRDGUniformBuffer 생성
그림20 을 보면 CreateUniformBuffers 함수를 통해서 UniformBuffer 를 생성하는 것을 볼 수 있습니다. InitRHI 단계에서 RHICreateUniformBuffer 를 호출하며 Single frame lifetime 가지는 형태로 생성하는 것을 볼 수 있습니다. 이후 과정은 그림3 의 과정과 동일하기 때문에 해당 부분을 참고해 주세요.
2.2.2. 리소스 해제 코드
2.2.2.1. FRDGTexture, FRDGBuffer 의 리소스 해제
- RDG 의 리소스들은 EndResourceRHI 에서 해제됩니다. Buffer 와 Texture 모두 거의 비슷한 과정을 거치므로 여기서는 Texture 만 확인해 봅시다.
- FRDGTexture 가 가지는 FRHITransientTexture 를 FD3D12TransientResourceHeapAllocator 로 넘겨 해제 처리를 시작합니다.
- FRHITransientTexture 를 할당받은 Heap 에다 DeallocateMemory 를 호출합니다. 내부에서는 FRHITransientTexture 에 Discard 함수를 호출합니다. 이때 PassIndex 는 kInvalidPassIndex 아닌 값이 넘어올 것입니다.
- 계속해서 Heap 의 DeallocateMemory 를 호출합니다.
- FRHITransientResource(FRHITransientTexture) 에 Discard 를 호출하는데 이때 아까 전달받은 PassIndex 를 설정합니다. 이렇게 되면 FRHITransientTexture 의 IsAcquired() 는 false 가 됩니다. 아직 리소스를 해제하지 않은 채로 Acquire 여부만 변경해 두고 그대로 둡니다. 실제 해제는 FRDGBuilder 의 실행시마다 수행하며, 이 과정은 그림22 에서 계속해서 보겠습니다.
2.2.2.2. 사용하지 않는 리소스들의 주기적인 정리
매번 FRDGBuilder 가 Execute 함수가 호출될 때마다 TransientResourceAllocator 의 Flush 함수가 호출됩니다. 여기서 정리가 필요한 리소스를 해제합니다.
1. FRDGBuilder 의 Execute 에서 시작합니다.
2. TransientResourceAllocator->Flush 함수를 호출하여서 사용하지 않는 리소스 중 정리 가능한 것들을 정리합니다.
3. TransientResourceAllocator 가 보유한 Heap 에 대해서 Flush 함수를 호출합니다.
4. TransientResourceAllocator 가 보유한 Heap 의 리소스 정리를 마친 뒤 할당된 리소스가 없는 Heap 이 발견되면 이 부분들을 모아서 HeapCache 에서 정리할 수 있도록 HeapCache.Forfeit 함수에 넘겨줍니다.
4.1. HeapCache 는 전달받은 사용하지 않는 Heap 들을 FreeList 에 담아둡니다. 이 후에 FreeList 에 있는 Heap 을 정리하는 것은 그림23 를 확인해 주세요.
5. 다시 3. 과정에서 본 Heap->Flush 함수를 따라가 봅시다. Heap 에서 할당된 리소스들인 Textures, Buffers 에 대해서 Forfeit 함수를 호출하는 것을 볼 수 있습니다.
6. Textures 는 TRHITransientResourceCache<FRHITransientTexture> 타입입니다. 이 클래스의 Forfeit 내부로 들어가 봅시다.
7. 할당된 리소스들을 정렬하는 데 사용 중인 것을 앞쪽, 사용되지 않고 있는 리소스를 뒤쪽으로 정렬합니다. 이때 사용되고 있는 리소스 여부의 기준은 그림21 의 6 에서 본 IsAcquired 함수입니다.
8. 사용하지 않는 리소스들을 Cache 배열에 담아둡니다.
9. 사용하지 않은 리소스들을 마지막 사용한 시간 기준으로 정렬합니다. 이렇게 하여 사용하지 않는 리소스의 해제 시도가 실패하는 지점까지만 리소스들을 순회할 수 있게 해 줍니다. 사용하지 않는 리소스가 있다 하더라도 Capacity 개수 까지는 소멸시키지 않고 보유합니다.
10. 리소스 해제 가능한지 확인하는 것은 TryReleaseItem 으로 수행합니다.
11. 리소스 해제 가능한지 해제 조건은 Capacity 보다 더 많은 리소스를 가지고 있으며(9 에서 이미 확인), 할당한 리소스가 마지막 사용 후 GarbageCollectLatency 프레임이 지난 후입니다.
그림22의 코드에서 본 것처럼 Transient 타입의 Buffer, Texture 는 사용하지 않게 된 후부터 GarbageCollectLatency 프레임 동안 해제되지 않기 때문에 안전하게 해제될 수 있습니다.
FRDGUniformBuffer 리소스 할당 방식은 FD3D12UniformBuffer 와 같이 FD3D12FastConstantAllocator 를 그대로 사용합니다. 그래서 리소스 해제는 그림11 과 동일합니다.
마지막으로 FD3D12TransientHeap 을 관리하는 HeapCache 에서 아무것도 할당되지 않은 FRHITransientHeap 을 마지막으로 사용된 후 일정 프레임 이후에 해제하여 필요한 FD3D12TransientHeap 의 수를 적절하게 유지해 주는 것을 볼 수 있습니다. 이 과정은 EndFrame 에서 수행됩니다.
3. 레퍼런스
1. http://simonstechblog.blogspot.com/2019/06/d3d12-descriptor-heap-management.html
2. https://developer.nvidia.com/dx12-dos-and-donts
3. https://learn.microsoft.com/en-us/windows/win32/direct3d12/descriptors-overview
4. http://diligentgraphics.com/diligent-engine/architecture/d3d12/managing-descriptor-heaps
5. https://learn.microsoft.com/en-us/windows/win32/direct3d12/hardware-support?redirectedfrom=MSDN
6. https://github.com/Microsoft/DirectX-Graphics-Samples/issues/114
7. https://learn.microsoft.com/ko-kr/windows/win32/api/d3d12/nf-d3d12-id3d12device-createshaderresourceview'UE4 & UE5 > Rendering' 카테고리의 다른 글
[UE5] Nanite (2/5) (0) 2024.03.21 [UE5] Nanite (1/5) (1) 2024.03.14 [UE5] D3D12 ResourceAllocation 리뷰 (1/2) (0) 2023.08.23 [UE5] Auto Exposure (2/2) - Histogram (0) 2022.12.15 [UE5] Auto Exposure (1/2) (1) 2022.11.29