ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Bindless Resource - DX12, Vulkan
    Graphics/기본 2024. 1. 17. 23:55

     

    Bindless Resource - DX12, Vulkan


    최초 작성 : 2024-01-17
    마지막 수정 : 2024-01-17
    최재호

     

    목차

    1. 목표
    2. 내용
    2.1. Bindless Resource 에 대해서
    2.2. API 별 Bindless Resource
      2.2.1. Vulkan Bindless Resource
      2.2.2. DX12 Bindless Resource
      2.2.3. DX12 Dynamic Resources
    3. 레퍼런스

     

    1. 목표

    레이트레이싱을 시작하면서 Bindless Resource 필요성을 느껴서 관련해서 알아본 것을 정리합니다.

    Bindless 리소스가 무엇인지 알아보고 어떤 장점이 있는지 알아봅시다.

    DX12, Vulkan 의 Bindless Resource 의 사용법과 특징을 알아봅시다.

    두 API 모두 HLSL 을 기준으로 설명합니다.

     

    사전지식

    - Vulkan 의 Resource Binding 방식의 이해(DescriptorSetLayout, DescriptorSet 생성과 DescriptorSet 에 리소스 기록 방식)

    - DX12 의 RootSignaure, DescriptorHeap(Shader visible 에 대한 것은 레퍼런스4 참고) 에 대한 이해

     

    2. 내용

    2.1. Bindless Resource 에 대해서

    Shader 를 실행하려면, C++ 코드에서 Shader 에 사용할 리소스들(시그니처)들을 전달해줘야 합니다. 이 리소스를 정의할 때 보통은 아래와 같은 형태로 어떤 리소스가 들어갈 것인지? 몇 개의 리소스가 들어갈 것인지 명시하게 됩니다.

    RaytracingAccelerationStructure Scene : register(t0, space0); // Raytracing 용 AccelerationStructure
    RWTexture2D<float4> RenderTarget[2] : register(u1, space0); // RenderTarget 2개
    ConstantBuffer<SceneConstantBuffer> g_sceneCB : register(b2, space0); // Scene 에 대한 정보를 담는 ConstantBuffer
    SamplerState AlbedoTextureSampler : register(s3, space0); // Texture fetch 를 위한 SamplerState

     

    Bindless 타입은 아래와 같은 형태로 리소스를 선언하며 몇개의 리소스가 들어갈지는 C++ 코드에서 바인딩하는 수에 맡기게 됩니다.

    StructuredBuffer<RenderObjectUniformBuffer> RenderObjParamArray[] : register(t0, space5); // RenderObject 의 UniformBuffer
    ByteAddressBuffer VerticesBindlessArray[] : register(t0, space6); // VertexBuffer list
    Texture2D AlbedoTextureArray[] : register(t0, space7); // Abedo texture list

     

    Bindless 를 사용하는 경우 제약이 하나 생기는데 아래와 같이 Bindless 는 register space 1 개에 매핑할 수 있습니다. 왜냐하면 Bindless 로 바인딩된 리소스는 몇 개의 리소스가 있을지 모르기 때문에 아래 코드처럼 t0 에 매핑을 했지만 t10 까지 사용할지 t1000 까지 사용할지는 알 수 없기 때문입니다.

     

    Bindless 를 사용하게 되면 얻게 되는 가장 큰 장점은 C++ 에서 명시해줘야 하는 Resource Binding 정보를 최소화할 수 있다는 것입니다. DX12 의 RootSignature 나 Vulkan 의 DescriptorLayout 을 만들 때 훨씬 더 간결하게 만들 수 있습니다. 그리고 Shader code 에서는 Bindless resource 를 적절하게 인덱싱하여 사용하면 됩니다.

     

    2.2. API 별 Bindless Resource

    DX12 와 Vulkan 모두 Bindless Resource 를 지원하며, DX12 는 여기서 한 단계 더 나아가서 Dynamic Resources 를 사용할 수 있습니다. Dynamic Resources 를 사용하면 RootSignature 에 Resource 시그니처의 명세가 필요하지 않습니다. 이 기능도 뒤에서 알아봅시다.

     

    2.2.1. Vulkan Bindless Resource

    Vulkan 의 Bindless Resource 사용을 위해서 생각보다 기존 방식에 비해서 큰 변화는 없습니다.

    단, 하나 아쉬웠던 점은 Vulkan 의 경우 register space 를 0~N 개까지 순차적으로 할당할 수 있는 점입니다. DX12 의 경우 register space 를 자유롭게 배정할 수 있어서 "register space 100 번 이후는 Bindless reousrce 로 사용하자" 와 같은 가정을 할 수 있지만 Vulkan 은 그런 것은 안됩니다.

     

    그럼 실제 C++ 코드에서 Bindless Resource 를 위한 코드를 봅시다.

    Bindless Resource 를 사용하기 위해서 API 단에서 활성화해야 할 것들이 있습니다.

    // API 에서 bindless resource 를 위한 기능을 지원하는지 확인
    VkPhysicalDeviceDescriptorIndexingFeatures indexing_features{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT, nullptr };
    VkPhysicalDeviceFeatures2 device_features{ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &indexing_features };
    vkGetPhysicalDeviceFeatures2(PhysicalDevice, &device_features);
    
    bool bindless_supported = indexing_features.descriptorBindingPartiallyBound && indexing_features.runtimeDescriptorArray;
    check(bindless_supported);

     

    VkDevice 를 생성하기 위한 VkDeviceCreateInfo 에도 아래 코드와 같이 Feature 를 활성화해줍니다.

    VkPhysicalDeviceDescriptorIndexingFeaturesEXT physicalDeviceDescriptorIndexingFeatures{};
    physicalDeviceDescriptorIndexingFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT;
    physicalDeviceDescriptorIndexingFeatures.shaderInputAttachmentArrayDynamicIndexing = true;
    physicalDeviceDescriptorIndexingFeatures.shaderUniformTexelBufferArrayDynamicIndexing = true;
    physicalDeviceDescriptorIndexingFeatures.shaderStorageTexelBufferArrayDynamicIndexing = true;
    physicalDeviceDescriptorIndexingFeatures.shaderUniformBufferArrayNonUniformIndexing = true;
    physicalDeviceDescriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing = true;
    physicalDeviceDescriptorIndexingFeatures.shaderStorageBufferArrayNonUniformIndexing = true;
    physicalDeviceDescriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing = true;
    physicalDeviceDescriptorIndexingFeatures.shaderInputAttachmentArrayNonUniformIndexing = true;
    physicalDeviceDescriptorIndexingFeatures.shaderUniformTexelBufferArrayNonUniformIndexing = true;
    physicalDeviceDescriptorIndexingFeatures.shaderStorageTexelBufferArrayNonUniformIndexing = true;
    physicalDeviceDescriptorIndexingFeatures.descriptorBindingUniformBufferUpdateAfterBind = true;
    physicalDeviceDescriptorIndexingFeatures.descriptorBindingSampledImageUpdateAfterBind = true;
    physicalDeviceDescriptorIndexingFeatures.descriptorBindingStorageImageUpdateAfterBind = true;
    physicalDeviceDescriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind = true;
    physicalDeviceDescriptorIndexingFeatures.descriptorBindingUniformTexelBufferUpdateAfterBind = true;
    physicalDeviceDescriptorIndexingFeatures.descriptorBindingStorageTexelBufferUpdateAfterBind = true;
    physicalDeviceDescriptorIndexingFeatures.descriptorBindingUpdateUnusedWhilePending = true;
    physicalDeviceDescriptorIndexingFeatures.descriptorBindingPartiallyBound = true;
    physicalDeviceDescriptorIndexingFeatures.descriptorBindingVariableDescriptorCount = true;
    physicalDeviceDescriptorIndexingFeatures.runtimeDescriptorArray = true;
    physicalDeviceDescriptorIndexingFeatures.pNext = (void*)createInfo.pNext;
    createInfo.pNext = &physicalDeviceDescriptorIndexingFeatures;

     

    Vulkan 은 DescriptorPool 을 통해서 Descriptor 를 할당합니다. 이 Descriptor 또한 Bindless Resource 지원에 필요한 옵션을 사용하여 생성해야 합니다.

    VkDescriptorPoolCreateInfo PoolInfo{};
    PoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
    PoolInfo.poolSizeCount = NumOfPoolSize;
    PoolInfo.pPoolSizes = Types;
    PoolInfo.maxSets = InMaxDescriptorSets;
    PoolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT;       // for bindless resources
    
    verify(VK_SUCCESS == vkCreateDescriptorPool(g_rhi_vk->Device, &PoolInfo, nullptr, &DescriptorPool));

     

    이제 DescriptorSetLayout 과 DescriptorSet 의 할당을 확인해 봅시다.

    // Descriptor Layout Creation 
    std::vector<VkDescriptorSetLayoutBinding> bindings;
    bindings.reserve(InShaderBindingArray.NumOfData);
    
    std::vector<VkDescriptorBindingFlagsEXT> bindingFlags;
    bindingFlags.reserve(InShaderBindingArray.NumOfData);
    
    // Iteration for Shader Binding Resource List
    for (int32 i = 0; i < (int32)InShaderBindingArray.NumOfData; ++i)
    {
        VkDescriptorSetLayoutBinding binding = {};
        ...
        bindings.push_back(binding);
        if (IsBindless)
            bindingFlags.push_back(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT);
        else
            bindingFlags.push_back(0);
    }
    
    VkDescriptorSetLayoutCreateInfo layoutInfo = {};
    layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
    layoutInfo.bindingCount = static_cast<uint32>(bindings.size());                     // Number of bindless resources here!
    layoutInfo.pBindings = bindings.data();
    layoutInfo.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT;      // for bindless resources
    
    // for bindless resources
    if (bindingFlags.size() > 0)
    {
        VkDescriptorSetLayoutBindingFlagsCreateInfoEXT setLayoutBindingFlags{};
        setLayoutBindingFlags.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO_EXT;
        setLayoutBindingFlags.bindingCount = (uint32)bindingFlags.size();
        setLayoutBindingFlags.pBindingFlags = bindingFlags.data();
        layoutInfo.pNext = &setLayoutBindingFlags;
    }
    
    // DescriptorSet Creation - nothing different
    VkDescriptorSetAllocateInfo DescriptorSetAllocateInfo{};
    DescriptorSetAllocateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
    DescriptorSetAllocateInfo.descriptorPool = DescriptorPool;
    DescriptorSetAllocateInfo.descriptorSetCount = 1;
    DescriptorSetAllocateInfo.pSetLayouts = &InLayout;
    
    VkDescriptorSet NewDescriptorSet = nullptr;
    vkAllocateDescriptorSets(g_rhi_vk->Device, &DescriptorSetAllocateInfo, &NewDescriptorSet);

     

    마지막으로 DescriptorSet 에 Resource 를 실제로 바인딩하는 과정입니다.

    // Bindless resource 에 리소스 할당
    for(int32 bindlessIndex = 0;bindlessIndex<NumOfBindlessResources;++bindlessIndex)
    {
        VkWriteDescriptorSet& CurDescriptorWrite = descriptorWrites[k];
        CurDescriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
        CurDescriptorWrite.dstSet = InDescriptorSet;
        CurDescriptorWrite.dstBinding = bindingIndex;
        CurDescriptorWrite.dstArrayElement = bindlessIndex;         // bindless resources array index
        CurDescriptorWrite.descriptorType = GetVulkanShaderBindingType(InShaderBindingArray[i]->BindingType);
        CurDescriptorWrite.descriptorCount = 1;
    }
    vkUpdateDescriptorSets(g_rhi_vk->Device, static_cast<uint32>(CurDescriptorWrite.size())
        , CurDescriptorWrite.data(), 0, nullptr);

     

     

    2.2.2. DX12 Bindless Resource

    DX12 의 Bindless Resource 도 기존 방식에 비해서 큰 변화는 없습니다. 하지만 Vulkan 에 비해서 조금 더 고려해야 할 점은 Resource 에 대한 Descriptor 를 DescriptorHeap 에 바인딩할 때, 해당 Descriptor 의 Offset 이 얼마인지 잘 계산해줘야 한다는 점입니다. 이런 부분들을 레퍼런스7 을 참고해 보면, 별도의 CBV 를 통해서 사용하고자 하는 Bindless Resource 의 Index 를 추가 리소스로 바인딩하기도 합니다. 이렇게 별도의 디스크립터의 Offset 를 CBV 로 전달하는 방식은 2.2.3. DX12 Dynamic Resources 에서는 반드시 필요합니다. 아래 그림1을 참고해 주세요.

    그림1. CBV 를 통해서 Bindless Resource 의 시작 지점을 레퍼런싱 해줌 (출처 : 레퍼런스8)

     

    아쉽게도 Vulkan 에서는 Dynamic Resources 방식을 지원하지 않기 때문에 jEngine 의 Raytracing 작업을 추가할 때는 이 기능을 사용하지 않았습니다. 그 대신 Descriptor Index CBV 를 만들지 않고, D3D12_DESCRIPTOR_RANGE1 의 OffsetInDescriptorsFromTableStart 에 해당 Bindless 리소스의 시작 인덱스를 명시하는 방식을 사용하여 추가 CBV 생성을 피했습니다.

     

    DX12 의 DescriptorHeap 이 Vulkan 의 DescriptorPool 에 비해 좀 더 어려웠던 점은 SetGraphicsRootDescriptorTable 의  통해 바인딩한 DescriptorHeap 으로 부터 Descriptor 의 Offset 이 얼마나 떨어져 있는지 여부를 모두 고려해줘야 한다는 점입니다. Index 를 관리하는 CBV 버퍼 같은 부분은 Vulkan 에서는 사용하지 않아도 되는 점을 고려해 보면 Vulkan 이 Bindless Resource 를 사용하기 더 편한 것 같다고 생각됩니다.

     

    그럼 실제 Bindless Resource 사용을 위해 변경되는 점을 확인해 봅시다.

     

    먼저 Descriptor 의 생성과 RootSignature 의 생성입니다. 아래 코드를 참고해 주세요.

    - Bindless Resource 는 DescriptorTable 을 통해 생성할 수 있는데 이 경우 Descriptor 1 개는 D3D12_DESCRIPTOR_RANGE1 로 정의 됩니다. 여기서 Bindless Resource 의 Offset 을 정의해 주기 위해서 OffsetInDescriptorsFromTableStart 에 해당 Descriptor 가 DescriptorHeap 의 시작위치에서 얼마나 떨어져 있는지 명시해 주면 됩니다. (DescriptorHeap 의 시작위치는 SetGraphicsRootDescriptorTable 을 통해 바인딩한 DescriptorHeap 의 D3D12_GPU_DESCRIPTOR_HANDLE 기준)

    - DX12 의 경우 RootSignaure 생성 시  D3D12_ROOT_SIGNATURE_FLAG_CBV_SRV_UAV_HEAP_DIRECTLY_INDEXED 를 명시해줘야 합니다.

    // SamplerState 에 대한 Descriptor 를 정의함. DescriptorTable 에 사용될 예정
    D3D12_DESCRIPTOR_RANGE1 range = {};
    range.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER;
    range.NumDescriptors = ShaderBinding->NumOfDescriptors;
    range.BaseShaderRegister = BindingIndex;
    range.RegisterSpace = InRegisterSpace;
    if (IsBindless)
    {
        range.OffsetInDescriptorsFromTableStart = InOutSamplerDescriptorOffset; // Bindless resource 의 시작점이 DescriptorHeap 에서 얼마나 떨어져있는지 명시!
        range.Flags = D3D12_DESCRIPTOR_RANGE_FLAG_DESCRIPTORS_VOLATILE;
    }
    else
    {
        range.OffsetInDescriptorsFromTableStart = InOutSamplerDescriptorOffset;
        range.Flags = D3D12_DESCRIPTOR_RANGE_FLAG_NONE;
    }
    InOutSamplerDescriptorOffset += ShaderBinding->NumOfDescriptors; // 다음 리소스를 위해서 Descriptor Offset 을 누적시킴
    SamplerDescriptors.emplace_back(range);
    
    
    // RootSignature 생성
    D3D12_ROOT_SIGNATURE_DESC1 rootSignatureDesc = {};
    rootSignatureDesc.NumParameters = (uint32)DescriptorExtractor.RootParameters.size();
    rootSignatureDesc.pParameters = &DescriptorExtractor.RootParameters[0];
    rootSignatureDesc.NumStaticSamplers = 0;
    rootSignatureDesc.pStaticSamplers = nullptr;
    rootSignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT
        | D3D12_ROOT_SIGNATURE_FLAG_CBV_SRV_UAV_HEAP_DIRECTLY_INDEXED | D3D12_ROOT_SIGNATURE_FLAG_SAMPLER_HEAP_DIRECTLY_INDEXED;        // Support for BindlessResource
    
    D3D12_VERSIONED_ROOT_SIGNATURE_DESC versionedDesc = { };
    versionedDesc.Version = D3D_ROOT_SIGNATURE_VERSION_1_1;
    versionedDesc.Desc_1_1 = rootSignatureDesc;
    
    ComPtr<ID3DBlob> signature;
    ComPtr<ID3DBlob> error;
    if (JFAIL_E(D3D12SerializeVersionedRootSignature(&versionedDesc, &signature, &error), error))
    {
        return nullptr;
    }
    
    ComPtr<ID3D12RootSignature> RootSignature;
    if (JFAIL(g_rhi_dx12->Device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&RootSignature))))
        return nullptr;

     

    나머지 과정은 기존과정과 동일합니다. Descriptor 를 Shader Visible Descriptor Heap 에 복사하여 RootSignature 에 명시한 Descriptor 의 Layout 과 일치하도록 DescriptorHeap 을 구성해 주면 됩니다.

    // SRV,UAV,CBV Shader Visible Descriptor Heap
    if (Descriptors.size() > 0)
    {
    ...
        for (int32 i = 0; i < Descriptors.size(); ++i)
        {
            SrcDescriptor.Add(Descriptors[i].Descriptor.CPUHandle);
    
            // // Get next descriptor handle from shader visible descriptor heap
            jDescriptor_DX12 Descriptor = InCommandList->OnlineDescriptorHeap->Alloc();
            DestDescriptor.Add(Descriptor.CPUHandle); // CPUHandle == D3D12_CPU_DESCRIPTOR_HANDLE
        }
    
        g_rhi_dx12->Device->CopyDescriptors((uint32)DestDescriptor.NumOfData, &DestDescriptor[0], nullptr
            , (uint32)SrcDescriptor.NumOfData, &SrcDescriptor[0], nullptr, D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
    }

     

    2.2.3. DX12 Dynamic Resources

    마지막으로 알아볼 것은 Dynamic Resources 입니다. 여기서부터는 더 이상 RootSignaure 가 필요 없습니다. 프로그래머는 그냥 DescriptorHeap 에 원하는 방식대로 Descriptor 를 구성해두고 Shader 내에서는 ResourceDescriptorHeap 를 인덱싱하여 SRV, UAV, CBV 의 리소스를 참조할 수 있으며 SamplerState 의 경우 SamplerDescriptorHeap 를 사용할 수 있습니다. 그래서 이 경우 별도의 CBV 를 통해 내가 DescriptorHeap 에 배치한 리소스의 시작 인덱스를 Shader 에 알려줘야 합니다. 

    ResourceDescriptorHeap 과 SamplerDescriptorHeap 은 다양한 형태의 리소스로 자유롭게 형변환 됩니다. 아래 코드는 Dynamic Resource 의 예제입니다.

    // SRV,UAV,CBV
    StructuredBuffer<RenderObjectUniformBuffer> RenderObjParam = ResourceDescriptorHeap[(PerPrimOffset++) + InstanceIdx * Stride];
    ByteAddressBuffer VerticesBindless = ResourceDescriptorHeap[(PerPrimOffset++) + InstanceIdx * Stride];
    Texture2D AlbedoTexture = ResourceDescriptorHeap[(PerPrimOffset++) + InstanceIdx * Stride];
    
    // SamplerState
    SamplerState AlbedoTextureSampler = SamplerDescriptorHeap[(PerPrimOffset++) + InstanceIdx * Stride];

     

    개인적으로는 RootSignaure 를 구성하는 게 굉장히 고통스러운 과정이라 이 부분 자체를 없애준 것에 대해서 굉장히 반가운 기능인 것 같습니다. 하지만 이 방식은 CBV 의 인덱스를 사용하여 암묵적으로 리소스를 변환시켜 사용하기 때문에 이슈가 발생했을 때 디버깅하기는 어려울 것 같습니다.

     

    Bindless Reosurce 는 Raytracing 에서 여러 오브젝트들의 Vertex 나 Index 정보를 전달하는 데 사용할 수 있었습니다.

    그림2. Bindless Resource 를 사용한 DXR, Vulkan RaytracingInOneWeekend 예제 (출처 : 직접구현)

     

    그림3. Bindless Resource 를 사용하여 여러 종류의 메시를 Vertex 와 Index 정보를 Shader 에서 InstanceID 를 기반으로 Fetch 하여 렌더링. DXR, VkRaytracing 기능 테스트용 예제 (출처 : 직접구현)

     

    그림2 의 구현코드 : https://github.com/scahp/jEngine/tree/RaytracingOneWeekend

    그림3 의 구현코드 : https://github.com/scahp/jEngine/tree/64afe3394d3224dc2d430a8f0fd5023821462819

     

    3. 레퍼런스

    1. https://alextardif.com/Bindless.html

    2. https://jorenjoestar.github.io/post/vulkan_bindless_texture/

    3. https://microsoft.github.io/DirectX-Specs/d3d/HLSL_SM_6_6_DynamicResources.html

    4. https://scahp.tistory.com/117

    5. https://rtarun9.github.io/blogs/bindless_rendering/

    6. https://wickedengine.net/2021/04/06/bindless-descriptors/

    7. https://github.com/TheRealMJP/DeferredTexturing

    8. https://developer.nvidia.com/ray-tracing-gems-ii

     

    'Graphics > 기본' 카테고리의 다른 글

    AsyncCompute - DX12, Vulkan  (0) 2024.02.13
    DX12 Shader Visible Descriptor Heap  (0) 2023.08.05
    BCn Texture Compression Formats 정리  (0) 2023.04.21
    Wave Intrinsics  (0) 2022.09.27
    Variable Shading Rate(VRS)  (2) 2022.09.27

    댓글

Designed by Tistory & scahp.