ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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. FD3D12UniformBuffer

          2.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. eFastAllocation

          2.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 생성

    1. RHIStructuredBuffer 와 같은 함수를 사용해 버퍼를 만들게 되면 DirectX12 의 경우 CreateD3D12Buffer 함수를 통해 버퍼를 생성합니다.
    2. CreateD3D12Buffer 에서는 BUF_AnyDynamic 옵션 여부에 따라서 Upload or Default heap 을 설정합니다.
    3. BUF_AnyDynmaic 여부에 상관없이 결국 동일한 AllocatorBuffer 함수를 실행합니다.
    4. AllocateBuffer 내에서는 BUF_AnyDynmaic 의 경우 FD3D12UploadHeapAllocator 로부터 메모리를 할당하는 것을 볼 수 있습니다. 그림4 에서 FD3D12UploadHeapAllocator 의 메모리 할당 코드를 계속해서 추적합니다.
    5. ResourceAllocator 가 있는 경우는 TransientHeap 방식의 Allocator 를 사용하는 경우입니다. 여기서는 사용하지 않습니다. 이 부분의 처리는  '2.2. TransientHeap 방식 (RDG 에서 사용)' 에서 확인해 봅시다.
    6. FD3D12DefaultBufferAllocator 로 부터 메모리를 할당하는 것을 볼 수 있습니다. 그림5 에서 FD3D12DefaultBufferAllocator 의 메모리 할당 코드를 계속해서 추적합니다.

    그림1. FD3D12Buffer 생성

     

    2.1.1.2. FD3D12Texture 생성

    1. 텍스처 생성의 경우 결국 CreateD3D12Texture 함수로 들어옵니다.
    2. 텍스쳐의 옵션에 따라 Clear 값이 필요한 경우 설정해 줍니다.
    3. FD3D12Texture 를 생성합니다. 이 객체의 생성 단계에서 실제 메모리가 할당되진 않기 때문에 이후 과정에서 메모리 할당을 시작합니다.
    4. ResourceAllocator 가 있는 경우는 TransientHeap 방식의 Allocator 를 사용하는 경우입니다. 여기서는 사용하지 않습니다. 이 부분의 처리는  '2.2. TransientHeap 방식 (RDG 에서 사용)' 에서 확인해 봅시다.
    5. 3D Texture 를 사용하는 경우는 FD3D12TextureAllocatorPool 를 사용해 메모리를 할당합니다. 그림6 에서 FD3D12TextureAllocatorPool 의 메모리 할당 코드를 계속해서 추적합니다.
    6. SafeCreateTexture2D 의 경우 ‘Readback 가능한 텍스처 인가?’ 여부에 따라서 생성 방식이 틀려집니다.
    7. 텍스처 옵션의 Readback 가능 여부를 확인하고 그에 맞는 Heap 타입을 선택합니다.
    8. Readback 가능한 경우 CreateBuffer 를 사용하여 메모리를 할당받습니다. 그리고 FD3D12Resource 를 eStandAlone 으로 설정합니다. 이때 OutTexture2D 는 FD3D12ResourceLocation 입니다.
    9. Readback 하지 않는 경우는 3D Texture 와 같이 FD3D12TextureAllocatorPool 를 사용해 메모리를 할당합니다. 그림6 에서 FD3D12TextureAllocatorPool 의 메모리 할당 코드를 계속해서 추적합니다.

    그림2. FD3D12Texture 생성

     

    2.1.1.3. FD3D12UniformBuffer 생성

    1. RHICreateUniformBuffer 를 사용해 UniformBuffer 를 생성합니다.
    2. FD3D12UniformBuffer 를 생성했지만 이 객체는 실제 메모리는 FD3D12ResourceLocation 에 할당될 것이기 때문에 코드를 좀 더 진행해 봅시다.
    3. EUniformBufferUsage::UniformBuffer_MultiFrame 인 경우 FD3D12UploadHeapAllocator 를 사용하여 메모리를 할당합니다. 그림4 에서 FD3D12UploadHeapAllocator 의 메모리 할당 코드를 계속해서 추적합니다.
    4. EUniformBufferUsage::UniformBuffer_SingleFrame 또는 EUniformBufferUsage::UniformBuffer_SingleDraw 인 경우 FD3D12FastConstantAllocator 를 사용하여 메모리를 할당합니다. FD3D12FastConstantAllocator 도 결국 내부에서 메모리를 할당할 때, FD3D12UploadHeapAllocator 의 FastConstantPageAllocator(FD3D12MultiBuddyAllocator) 를 사용하여 메모리를 할당합니다.

    그림3. FD3D12UniformBuffer 생성

     

    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 으로 설정합니다.

    그림4. FD3D12UploadHeapAllocator 와 그것을 구성하는 FD3D12PoolAllocator, FD3D12MultiBuddyAllocator 의 메모리 할당

     

    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. 에서 봤었습니다.

    그림5. FD3D12DefaultBufferAllocator 의 메모리 할당

     

    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 에서 봤었습니다.

    그림6. FD3D12TextureAllocatorPool 을 사용한 텍스처 메모리 할당

     

    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 으로 설정합니다.

    그림7. FD3D12FastConstantAllocator 를 사용한 SingleFrame lifetime 의 메모리 할당

     

    2.1.2. 리소스 해제 코드 파악

    FD3D12Buffer, FD3D12Texture, FD3D12UniformBuffer 가 소멸되는 경우 각 클래스들이 소유하고 있던, FD3D12ResourceLocation 도 함께 소멸될 것입니다. 이 시점에서 할당되었던 메모리는 해제됩니다. 해제 방식은 ResourceLocationType 에 따라 달라지는데 처리 방법이 어떻게 다른지 확인해 봅시다.

     

    2.1.2.1. eStandAlone

    1. eStandAlone 방식의 소멸은 이 부분으로 들어옵니다.
    2. 대부분의 경우 DeferDelete() 를 호출하는 쪽으로 실행됩니다.
    3. DeferDelete() 하지 않는 경우는 여기서 즉시 리소스 해제를 실행합니다.
    4. DeferDelete 가 실행되면 FD3D12DynamicRHI::DeferredDelete 가 호출 차례로 호출됩니다.
    5. ObjectsToDelete 에 지워질 리소스를 등록합니다. 이후 과정은 그림13 에서 알아봅시다.

    그림8. eStandAlone 방식의 리소스 해제

     

    2.1.2.2. eSubAllocation, AT_Pool

    1. eSubAllocation 방식의 소멸은 이 부분으로 들어옵니다. eSubAllocation 은 FD3D12PoolAllocator 또는 FD3D12MultiBuddyAllocator 으로 메모리가 할당된 경우입니다.
    2. AT_Pool 로 설정된 경우, FD3D12PoolAllocator 입니다. 리소스 해제는 즉시 일어나지 않고 FrameFencedOperations 에 등록하여 지연시킵니다. 이후 과정에 FrameFencedOperations 를 한 번에 처리하는데 그림15 의 2.1. 에서 확인해 봅시다.

    그림9. FD3D12ResourceLocation 의 eSubAllocation, AT_Pool 방식의 소멸

     

    2.1.2.3. eSubAllocation, AT_Default

    1. eSubAllocation 방식의 소멸은 이 부분으로 들어옵니다. eSubAllocation 은 FD3D12PoolAllocator 또는 FD3D12MultiBuddyAllocator 으로 메모리가 할당된 경우입니다.
    2. AT_Default 로 설정된 경우, Allocator 는 FD3D12BuddyAllocator 입니다. 이 경우도 즉시 리소스를 해제하지 않고, DeferredDeletionQueue 에 등록하여 지연시킵니다. 이후 과정에서 DeferredDeletionQueue 를 한 번에 처리하는데 그림15 의 1.1. 에서 확인해 봅시다.

    그림10. FD3D12ResourceLocation 의 eSubAllocation, AT_Default 방식의 소멸

    2.1.2.4. eFastAllocation

    1. eFastAllocation 방식의 소멸은 여기서 아무것도 하지 않습니다. 이전 글의 ‘2.1.2. 리소스 해제 방식 정리’ 의 eFastAllocation 에서 해제 관련 설명을 확인할 수 있습니다. 대신 다른 곳에서 할당된 리소스를 정리합니다. 바로 그림7 의 3 과정에서 할당할 메모리가 부족할 때 해제 요청을 합니다. 이 리소스는 FD3D12FastConstantAllocator(FD3D12MultiBuddyAllocator) 로부터 할당받은 리소스를 사용합니다. FD3D12MultiBodyAllocator 의 리소스 해제 방식은  그림15 의 1.1 에서 확인할 수 있습니다.

    그림11. FD3D12ResourceLocation 의 eFastAllocation 방식의 소멸은 아무것도 하지 않음.

    2.1.2.5. eHeapAliased

    1. 이 타입의 경우 TransientHeap 에서 부터 할당된 리소스입니다. 해제 방식은 그림8 에서 본 eStandAlone 방식과 동일합니다.

    그림12. FD3D12ResourceLocation 의 eHeapAliased 방식의 소멸은 아무것도 하지 않음.

     

    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 에 전달된 람다 함수를 통해서 수행됩니다.

    그림13. ObjectsToDelete 에 있는 해제 대기 중인 리소스를 정리하는 코드

     

    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 에서 확인했습니다.

    그림14. RHIEndFrame 에서 FD3D12UploadHeapAllocator, FD3D12TextureAllocatorPool, FD3D12DefaultBufferPool 이 보유한 해제 보류된 리소스를 처리함.

     

    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 는 제거됩니다.

    그림15. FD3D12UploadHeapAllocator 의 리소스 해제

     

    2.1.2.9. FD3D12TextureAllocatorPool 의 리소스 해제

    FD3D12TextureAllocatorPool 의 리소스 해제 과정입니다. 모든 PoolAllocators 가 FD3D12PoolAllocator 로 구성되어 있습니다. FD3D12PoolAllocator 의 리소스 해제 과정은 그림15 의 2.1. 부분에서 확인했었습니다.

    그림16. FD3D12TextureAllocatorPool 의 리소스 해제

     

    2.1.2.10. FD3D12TextureAllocatorPool 의 리소스 해제

    FD3D12DefaultBufferAllocator 의 리소스 해제 과정입니다. 여기에 나오는 FD3D12BufferPool 은 특별한 설정을 하지 않았다면 FD3D12PoolAllocator 입니다. FD3D12PoolAllocator 의 리소스 해제 과정은 그림15 의 2.1. 부분에서 확인했었습니다.

    그림17. FD3D12DefaultBufferAllocator 의 리소스 해제

     

    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 로 설정하고 있습니다.

    그림18. FD3D12TransientResourceHeapAllocator 로 부터 FTransientTexture 를 할당하는 과정

     

    아래 코드를 보면 텍스쳐 또는 버퍼의 리소스를 생성하는 도중 ResourceAllocator 를 사용하는 경우는 대부분 TransientResourceAllocator 입니다. 텍스쳐와 버퍼 모두 TransientHeap 으로부터 리소스를 할당받을 수 있음을 알 수 있습니다. 

    그림19. Texture, Buffer 의 리소스를 생성할 때 FD3D12TransientResourceHeapAllocator 를 사용하여 생성하는 코드

     

    2.2.1.2. FRDGUniformBuffer 생성

    그림20 을 보면 CreateUniformBuffers 함수를 통해서 UniformBuffer 를 생성하는 것을 볼 수 있습니다. InitRHI 단계에서 RHICreateUniformBuffer 를 호출하며 Single frame lifetime 가지는 형태로 생성하는 것을 볼 수 있습니다. 이후 과정은 그림3 의 과정과 동일하기 때문에 해당 부분을 참고해 주세요.

    그림20. FRDGUniformBuffer 의 리소스 생성 과정

     

    2.2.2. 리소스 해제 코드

    2.2.2.1. FRDGTexture, FRDGBuffer 의 리소스 해제

    1. RDG 의 리소스들은 EndResourceRHI 에서 해제됩니다. Buffer 와 Texture 모두 거의 비슷한 과정을 거치므로 여기서는 Texture 만 확인해 봅시다.
    2. FRDGTexture 가 가지는 FRHITransientTexture 를 FD3D12TransientResourceHeapAllocator 로 넘겨 해제 처리를 시작합니다.
    3. FRHITransientTexture 를 할당받은 Heap 에다 DeallocateMemory 를 호출합니다. 내부에서는 FRHITransientTexture 에 Discard 함수를 호출합니다. 이때 PassIndex 는 kInvalidPassIndex 아닌 값이 넘어올 것입니다.
    4. 계속해서 Heap 의 DeallocateMemory 를 호출합니다.
    5. FRHITransientResource(FRHITransientTexture) 에 Discard 를 호출하는데 이때 아까 전달받은 PassIndex 를 설정합니다. 이렇게 되면 FRHITransientTexture 의 IsAcquired() 는 false 가 됩니다. 아직 리소스를 해제하지 않은 채로 Acquire 여부만 변경해 두고 그대로 둡니다. 실제 해제는 FRDGBuilder 의 실행시마다 수행하며, 이 과정은 그림22 에서 계속해서 보겠습니다.

    그림21. FRHITransientTexture 가 해제되는 과정, FRHITransientBuffer 또한 비슷한 과정을 거침

     

    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. FD3D12TransientResourceHeapAllocator 에서 사용하지 않는 리소스를 주기적으로 정리해주는 코드

     

    그림22의 코드에서 본 것처럼 Transient 타입의 Buffer, Texture 는 사용하지 않게 된 후부터 GarbageCollectLatency 프레임 동안 해제되지 않기 때문에 안전하게 해제될 수 있습니다.

     

    FRDGUniformBuffer 리소스 할당 방식은 FD3D12UniformBuffer 와 같이 FD3D12FastConstantAllocator 를 그대로 사용합니다. 그래서 리소스 해제는 그림11 과 동일합니다.

     

    마지막으로 FD3D12TransientHeap 을 관리하는 HeapCache 에서 아무것도 할당되지 않은 FRHITransientHeap 을 마지막으로 사용된 후 일정 프레임 이후에 해제하여 필요한 FD3D12TransientHeap 의 수를 적절하게 유지해 주는 것을 볼 수 있습니다. 이 과정은 EndFrame 에서 수행됩니다.

    그림23. FRHITransientHeapCache 가 불필요한 FD3D12TransientHeap 을 정리하는 코드

     

     

    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

    댓글

Designed by Tistory & scahp.