DirectX 12 - 根描述符无法正常工作

DirectX 12 - Root Descriptor not working properly

在我的测试应用程序中,我将模型、视图和投影矩阵作为 32 位常量传递给着色器。现在我想切换到根描述符以减少我的根签名大小。我想将两个常量缓冲区传递给着色器。第一个包含模型矩阵(一个 4x4 矩阵),第二个包含视图和投影矩阵(两个 4x4 矩阵)。然而,视图和投影矩阵使用根描述符工作得非常好。一旦我将模型矩阵从 32 位常量切换为根描述符,场景就不再渲染,尽管两个常量缓冲区的过程完全相同。 DirectX 没有显示错误,甚至在调试层也没有。

根参数代码

// Root Parameter: "CB_ModelMatrix"
rootParameters[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
rootParameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;
rootParameters[0].Descriptor.ShaderRegister = 0;
rootParameters[0].Descriptor.RegisterSpace = 0;

// Root Parameter: "CB_ViewProjectionMatrices"
rootParameters[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
rootParameters[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;
rootParameters[1].Descriptor.ShaderRegister = 1;
rootParameters[1].Descriptor.RegisterSpace = 0;

资源创建代码

// model matrix resource

D3D12_HEAP_PROPERTIES heapProperties = {};
heapProperties.Type = D3D12_HEAP_TYPE_UPLOAD;
heapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
heapProperties.CreationNodeMask = 1;
heapProperties.VisibleNodeMask = 1;

D3D12_RESOURCE_DESC resourceDescription = {};
resourceDescription.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
resourceDescription.Alignment = 0;
resourceDescription.Width = (sizeof(t_ConstantBufferData_ModelMatrix) + 255) & ~255;
resourceDescription.Height = 1;
resourceDescription.DepthOrArraySize = 1;
resourceDescription.MipLevels = 1;
resourceDescription.Format = DXGI_FORMAT_UNKNOWN;
resourceDescription.SampleDesc.Count = 1;
resourceDescription.SampleDesc.Quality = 0;
resourceDescription.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
resourceDescription.Flags = D3D12_RESOURCE_FLAG_NONE;

ThrowIfFailed(g_GraphicsDevice->CreateCommittedResource(&heapProperties, D3D12_HEAP_FLAG_NONE,
  &resourceDescription, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&g_ConstantBuffer_ModelMatrix)));

ThrowIfFailed(g_ConstantBuffer_ModelMatrix->Map(0, nullptr, reinterpret_cast<void**>(&g_ConstantBufferPointer_ModelMatrix)));

// view and projection matrices resource

D3D12_HEAP_PROPERTIES heapProperties = {};
heapProperties.Type = D3D12_HEAP_TYPE_UPLOAD;
heapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
heapProperties.CreationNodeMask = 1;
heapProperties.VisibleNodeMask = 1;

D3D12_RESOURCE_DESC resourceDescription = {};
resourceDescription.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
resourceDescription.Alignment = 0;
resourceDescription.Width = (sizeof(t_ConstantBufferData_ViewProjectionMatrices) + 255) & ~255;
resourceDescription.Height = 1;
resourceDescription.DepthOrArraySize = 1;
resourceDescription.MipLevels = 1;
resourceDescription.Format = DXGI_FORMAT_UNKNOWN;
resourceDescription.SampleDesc.Count = 1;
resourceDescription.SampleDesc.Quality = 0;
resourceDescription.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
resourceDescription.Flags = D3D12_RESOURCE_FLAG_NONE;

ThrowIfFailed(g_GraphicsDevice->CreateCommittedResource(&heapProperties, D3D12_HEAP_FLAG_NONE,
  &resourceDescription, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&g_ConstantBuffer_ViewProjectionMatrices)));

ThrowIfFailed(g_ConstantBuffer_ViewProjectionMatrices->Map(0, nullptr, reinterpret_cast<void**>(&g_ConstantBufferPointer_ViewProjectionMatrices)));

资源更新(模型矩阵)

t_ConstantBufferData_ModelMatrix CB_ModelMatrix = {};

// ...

std::memcpy(g_ConstantBufferPointer_ModelMatrix, &CB_ModelMatrix, sizeof(CB_ModelMatrix));
g_CommandList->SetGraphicsRootConstantBufferView(0, g_ConstantBuffer_ModelMatrix->GetGPUVirtualAddress());

资源更新(视图和投影矩阵)

t_ConstantBufferData_ViewProjectionMatrices CB_ViewProjectionMatrices = {};

// ...

std::memcpy(g_ConstantBufferPointer_ViewProjectionMatrices, &CB_ViewProjectionMatrices, sizeof(CB_ViewProjectionMatrices));
g_CommandList->SetGraphicsRootConstantBufferView(1, g_ConstantBuffer_ViewProjectionMatrices->GetGPUVirtualAddress());

顶点着色器中的常量缓冲区

struct t_ConstantBufferData_ModelMatrix
{
  float4x4 ModelMatrix;
};

struct t_ConstantBufferData_ViewProjectionMatrices
{
  float4x4 ViewMatrix;
  float4x4 ProjectionMatrix;
};

ConstantBuffer<t_ConstantBufferData_ModelMatrix> CB_ModelMatrix : register(b0, space0);
ConstantBuffer<t_ConstantBufferData_ViewProjectionMatrices> CB_ViewProjectionMatrices : register(b1, space0);

资源在程序开始时创建和映射,在程序结束时取消映射。视图和投影矩阵资源每帧更新一次。模型矩阵资源每帧更新多次,因为我有多个具有不同变换的游戏对象。

我不明白为什么根描述符适用于视图和投影矩阵但不适用于模型矩阵。也许我忽略了根描述符的基本机制?如果有人能告诉我我错过了什么,我将不胜感激。

顺便问一个附带问题:在应用程序的整个生命周期内保持资源映射是否安全?我想我可以通过它获得更好的性能,而不是每次都映射和取消映射资源,它们会被更新。

编辑:

场景由 27 个立方体组成,它们在场景中心以 3x3x3 网格呈现。其中一些立方体在相机后面渲染。在我用相机更精确地探索场景并借助图形调试工具后,我意识到,只有最后一个立方体被渲染了。为了渲染立方体网格,我在渲染函数中使用了以下循环:

for (unsigned char t = 0; t < 27; t++)
{
  t_ConstantBufferData_ModelMatrix CB_ModelMatrix = {};
  CB_ModelMatrix.ModelMatrix = createModelMatrix(g_Transforms[t]);

  std::memcpy(g_ConstantBufferPointer_ModelMatrix, &CB_ModelMatrix, sizeof(CB_ModelMatrix));
  
  g_CommandList->SetGraphicsRootConstantBufferView(0, g_ConstantBuffer_ModelMatrix->GetGPUVirtualAddress());
  g_CommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);
}

我根据当前立方体的变换创建模型矩阵,并将该矩阵复制到常量缓冲区。之后,绘制立方体。模型矩阵已正确创建,常量缓冲区的内存按预期更改。但是,只绘制了最后一个立方体。

我的假设是,命令列表中的绘制命令仅存储指向常量缓冲区的指针,并且就在执行命令列表之前,缓冲区中仅存在最后一个立方体的模型矩阵。这可能是问题的原因吗?如果是这样,可以做些什么来解决它?

简单的解决方案是让每个对象保留自己的常量缓冲区。请记住,您需要 NUMBER_OF_OBJECTS * FRAME_COUNT 个 cbuffer,因为有可能覆盖 GPU 仍在使用的旧 cbuffer。 (只有当你有超过 1 帧在飞行时,即你没有在每帧结束时等待 GPU 完成)。

更好的解决方案是每次需要时从全局大型上传缓冲区分配。这里有两种策略:线性分配器或环形分配器。我将在这里解释线性的,但是您可以在两种实现的 post 末尾看到 link。

它可能看起来像这样:

template<typename CBuffer>
inline constexpr uint32_t GetCBufferSize()
{
    return (sizeof(CBuffer) + (D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT - 1)) & ~(D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT - 1);
}
struct Allocation
{
    ID3D12Resource* buffer = nullptr;
    void* cpu_address = nullptr;
    D3D12_GPU_VIRTUAL_ADDRESS gpu_address = 0;
    size_t offset = 0;
    size_t size = 0;

    void Update(void* data, size_t size)
    {
        memcpy(cpu_address, data, size);
    }

    template<typename T>
    void Update(T const& data)
    {
        memcpy(cpu_address, &data, sizeof(T));
    }
};

class UploadBuffer
{
public:

    UploadBuffer(ID3D12Device* device, SIZE_T max_size_in_bytes)
    {
    auto heap_properties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
        auto buffer_desc = CD3DX12_RESOURCE_DESC::Buffer(max_size_in_bytes);
        BREAK_IF_FAILED(device->CreateCommittedResource(
        &heap_properties,
        D3D12_HEAP_FLAG_NONE,
        &buffer_desc,
        D3D12_RESOURCE_STATE_GENERIC_READ,
        nullptr,
        IID_PPV_ARGS(&buffer)));

        CD3DX12_RANGE read_range(0, 0);
        BREAK_IF_FAILED(buffer->Map(0, &read_range, reinterpret_cast<void**>(&cpu_address)));
        gpu_address = buffer->GetGPUVirtualAddress();
    }

    Allocation Allocate(SIZE_T size_in_bytes, SIZE_T alignment)
    {
        offset_in_buffer = linear_allocator.Allocate(size_in_bytes, alignment);
        Allocation allocation{}; 
        allocation.buffer = buffer.Get();
        allocation.cpu_address = reinterpret_cast<uint8*>(cpu_address) + offset_in_buffer;
        allocation.gpu_address = gpu_address + offset_in_buffer;
        allocation.offset_in_buffer = offset_in_buffer;
        allocation.size = size_in_bytes;

        return allocation;
    }


    void Clear()
    {
        linear_allocator.Clear(); 
    }

private:
    LinearAllocator allocator;
    ComPtr<ID3D12Resource> buffer;
    uint8_t* cpu_address = nullptr;
    D3D12_GPU_VIRTUAL_ADDRESS gpu_address = 0;
};

用法示例:

//initalization
for(size_t i = 0; i < FRAMES_IN_FLIGHT; ++i)
{
    upload_buffers[i] = UploadBuffer(device, MAX_UPLOAD_BUFFER_SIZE);
}
//frame
UploadBuffer upload_buffer = GetUploadBufferForThisFrame();
upload_buffer.Clear(); 
//...
for(auto&& object : scene)
{
    //...
    model_matrix_cbuf.model_matrix = object.model_matrix;
    
    object_allocation = upload_buffer->Allocate(GetCBufferSize<ModelMatrixCBuffer>(), D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT);
    object_allocation.Update(model_matrix_cbuf);
    cmd_list->SetGraphicsRootConstantBufferView(0, object_allocation.gpu_address); //or whatever root parameter index your cbuffer is 
}

请记住,这在一定程度上进行了简化,因此更容易理解。

此外,如果您的应用程序是多线程的,您将需要使用互斥体或使用原子来保护分配器调用。例如实现的例子,你可以看到这个repo,一些但不是所有的相关文件是:LinearUploadBuffer.h/cpp, RingUploadBuffer.h/cpp, LinearAllocator.h/cpp, RingAllocator. h/cpp, DynamicAllocation.h.

我再次进行了一些研究,假设我的方法不正确。我搜索了使用不同变换多次渲染同一组顶点的技术。最后,我偶然发现了“Instancing”或“Ins​​tanced Drawing”。我知道,存在这样的技术,但在我阅读的大多数教程中,它被归类为“高级图形编程”,稍后将讨论。在 this 教程(最初是为 DirectX 11 编写的,但可以很容易地移植到 DirectX 12)的帮助下,我能够使用 3D 对象的多个实例正确渲染场景。除此之外,我可以避免将我的模型矩阵作为根参数传递给图形管道。使用实例化时渲染性能也会提高。

对于那些不熟悉这个概念的人,这里有一个简短的总结:

使用实例化,如果多个对象共享同一组顶点但具有不同的变换(以及根据教程的一些其他属性),则需要绘制。不是使用单独的绘制调用来渲染每个对象,而是可以仅使用单个绘制调用来渲染所有这些对象。这需要通过管道的输入数据布局将一些额外信息(如转换)传递给着色器。这些布局参数可以配置,由着色器接收,不是按顶点而是按实例。当需要渲染许多对象时(成百上千个对象),实例化将大大提高性能。

技术细节

在实现这个概念时,可以使用多种技术。

实例数据可以直接在着色器中计算。这是通过根据实例编号操纵顶点数据来完成的,可以使用 SV_InstanceID 着色器语义检索实例编号。这种方法可能不是很灵活。

也可以使用常量缓冲区将其他数据传递给着色器。

最后一种方法是使用“实例缓冲区”。实例缓冲区的创建与顶点缓冲区完全相同。它是一种资源,必须创建、填充数据(例如转换数据)并上传到 GPU。此外,需要在管道的输入布局中引用实例缓冲区。渲染帧时,必须使用 ID3D12GraphicsCommandList::IASetVertexBuffers 函数在管道中设置此缓冲区。最后,必须调用 ID3D12GraphicsCommandList::DrawIndexedInstanced,它将实例计数作为参数。数据的进一步处理然后由着色器实现。