Skip to content

01.D Hello Constant Buffers

PAMinerva edited this page Sep 6, 2022 · 33 revisions

Image


1 - Introduction

The sample we are going to review in this tutorial (D3D12HelloConstBuffers) makes use of a constant buffer to pass data from CPU to GPU (that is, from CPU system memory allocated and used by our C++ app to a GPU-visible heap), so that the shader programs can access the related constant buffer data.

You know that shaders receive input data from previous stages, but they can also access resources stored in GPU heaps with the help of root signatures. We already studied some of the theory in a previous tutorial. Now, it’s time to use root signatures in practice. However, you may still wonder why we need to pass data from CPU to GPU. Well, there are several reasons to do that. For example, in this sample we will pass data to relocate the position of a triangle over time. That way, we can simulate a simple animation (as shown in the image above).



2 - Constant buffers

Constant buffers are buffers optimized for constant-variable usage, which is characterized by lower-latency access and more frequent update from the CPU; therefore, they have additional size, layout, and access restrictions.
Constant variables which are updated from the CPU may seem a funny way of defining constant values. However, if you thing about it, constant variables still need to be initialized. So, in this context, by update we mean initialize a constant buffer with a value, which will be constant during the execution of a shader program.

Each constant buffer can hold up to 4096 constants, where each constant contains up to four 32-bit values. Constant buffers need to be 256-byte aligned (that is, the starting address has to be a multiple of 256). The simplest way to meet this requirement is to create constant buffers multiple of 256 bytes. That way, you can even allocate space for a whole array of constant buffers on GPU heaps without worrying about alignment. Indeed, CreateCommittedResource usually allocate memory space on GPU heaps so that it is aligned at 4KB or 64KB (which are both multiple of 256 bytes).

Declaring a constant buffer looks very much like a structure declaration in C\C++. However, unlike C\C++, we can’t allocate GPU memory space by simply defining an instance of a constant buffer in the code of shader programs. Constant buffers in HLSL can only be defined and referenced through the related views. Usually, in our C++ app we first define a CPU version of the constant buffer to store temporary data during a frame creation on the CPU timeline. Then, we allocate enough GPU memory space on the upload heap to hold the constant buffer, which can be mapped to the virtual address space of our C++ app since the upload heap is CPU-visible memory. So, we can use the CPU constant buffer as a source and the memory mapped constant buffer as a destination of a copy operation to initialize the constant buffer on the upload heap. If we need to update the constant buffer data from the CPU timeline, we use a root signature to bind a view to the constant buffer on the upload heap. At that point, the shader code can access it through the related slot (virtual register). This is basically what D3D12HelloConstBuffers does to both initialize and update the triangle position over time (more on this in the next section).

If the constant buffer data is static, we usually use the constant buffer on the upload heap as a source in a copy operation to update the real constant buffer on the default heap. We will return to this in a later tutorial.



3 - D3D12HelloConstBuffers: code review

Let’s start with the shader code.


cbuffer SceneConstantBuffer : register(b0)
{
    float4 offset;
    float4 padding[15];
};
 
struct PSInput
{
    float4 position : SV_POSITION;
    float4 color : COLOR;
};


 
PSInput VSMain(float4 position : POSITION, float4 color : COLOR)
{
    PSInput result;
 
    result.position = position + offset;
    result.color = color;
 
    return result;
}
 


float4 PSMain(PSInput input) : SV_TARGET
{
    return input.color;
}

cbuffer is the keyword used in HLSL to declare a constant buffer, while register is used to specify the virtual register (link name) where we want to bind the related constant buffer view. In this case, we want to bind to the slot 0 reserved for constant buffer views, so we will bind to the virtual register b0. The first field of SceneConstantBuffer (the constant buffer defined in the above code) is the offset we will use to translate the vertices of the triangle. The second field is used pad the structure until the constant buffer is 256 bytes in size.
Observe that we don’t need to explicitly pad constant buffers to 256 bytes in HLSL as this is done implicitly. Also, as you can see, the scope\visibility of the fields of a constant buffer is similar to that of enumerator constants in a C enumeration.

In this tutorial we will bind the view that describes SceneConstantBuffer through a root table in the root signature. However, in theory we could also pass a CBV as a root argument to a root descriptor, which is just a GPU virtual addresses to the related resource. For this reason, the definition of a constant buffer in HLSL is essential for the GPU to correctly access the related constant buffer data.

Now, we can see the C++ code, starting from the application class.


class D3D12HelloConstBuffers : public DXSample
{
public:
    D3D12HelloConstBuffers(UINT width, UINT height, std::wstring name);
 
    virtual void OnInit();
    virtual void OnUpdate();
    virtual void OnRender();
    virtual void OnDestroy();
 
private:
    static const UINT FrameCount = 2;
 
    struct Vertex
    {
        XMFLOAT3 position;
        XMFLOAT4 color;
    };
 
    struct SceneConstantBuffer
    {
        XMFLOAT4 offset;
        float padding[60]; // Padding so the constant buffer is 256-byte aligned.
    };
    static_assert((sizeof(SceneConstantBuffer) % 256) == 0, "Constant Buffer size must be 256-byte aligned");
 
    // Pipeline objects.
    CD3DX12_VIEWPORT m_viewport;
    CD3DX12_RECT m_scissorRect;
    ComPtr<IDXGISwapChain3> m_swapChain;
    ComPtr<ID3D12Device> m_device;
    ComPtr<ID3D12Resource> m_renderTargets[FrameCount];
    ComPtr<ID3D12CommandAllocator> m_commandAllocator;
    ComPtr<ID3D12CommandQueue> m_commandQueue;
    ComPtr<ID3D12RootSignature> m_rootSignature;
    ComPtr<ID3D12DescriptorHeap> m_rtvHeap;
    ComPtr<ID3D12DescriptorHeap> m_cbvHeap;
    ComPtr<ID3D12PipelineState> m_pipelineState;
    ComPtr<ID3D12GraphicsCommandList> m_commandList;
    UINT m_rtvDescriptorSize;
 
    // App resources.
    ComPtr<ID3D12Resource> m_vertexBuffer;
    D3D12_VERTEX_BUFFER_VIEW m_vertexBufferView;
    ComPtr<ID3D12Resource> m_constantBuffer;
    SceneConstantBuffer m_constantBufferData;
    UINT8* m_pCbvDataBegin;
 
    // Synchronization objects.
    UINT m_frameIndex;
    HANDLE m_fenceEvent;
    ComPtr<ID3D12Fence> m_fence;
    UINT64 m_fenceValue;
 
    void LoadPipeline();
    void LoadAssets();
    void PopulateCommandList();
    void WaitForPreviousFrame();
};

We must define a structure similar to the constant buffer in the shader code as we need an instance where to store the result of our computation on the constant buffer data that will be used to initialize\update the constant buffer on the upload heap. In this case, we explicitly need to pad\extend the structure to 256 bytes.
The transfer of data between CPU memory and GPU heaps is a mere bit stream. This can be a problem since C++ and HLSL use different rules to pack data. HLSL packs data into 4-byte boundaries, provided that it doesn't cross a 16-byte boundary. As we know, a shader register is the basic unit to store data in shader cores. A register is composed of four 32-bit components (16 bytes in total). Now, imagine having the following C++ structure definition.


struct S
{
    XMFLOAT3 mu;
    XMFLOAT2 mv;
    float mw;
};

and a similar definition in HLSL


cbuffer S
{
    float3 u;
    float2 v;
    float w;
};

In C++ we have the following layout of data in memory.


mu.x, mu.y, mu.z, mv.x, mv.y, mw.x

Since data transfer is a mere bit stream from CPU to GPU, after loading data into shader registers, we will have the following layout for the constant buffer in HLSL.


Register1: (mu.x, mu.y, mu.z, mv.x)
Register2: (mv.y, mw.x, empty, empty)

As you can see, the field mv is split between two registers. The rules of HLSL state that data is packed so that they can’t cross a 16-byte boundary (that is, the size of a register). So, we always must define C++ structures with the HLSL packing rules in mind. For example, a better C++ definition for the S structure is


struct S
{
    XMFLOAT3 mu;
    float pack;
    XMFLOAT2 mv;
    float mw;
};

so that in HLSL we avoid splitting fields among shader registers.


Register1: (mu.x, mu.y, mu.z, pack.x)
Register2: (mv.x, mv.y, mw.x, empty)

We can have more than a field in the same register. Indeed, as you can see above, mv and mw are in the same register since they don’t cross the 16-byte boundary of a register.

The HLSL packing rules also apply to both input and output parameters of the shader programs as well, except for the input parameters of the vertex shader. Indeed, the input assembler cannot unpack data, and simply passes each vertex attribute in a different input register. This explains why we can use both float3 and float4 for the first parameter of VSMain (position), as pointed out in the previous tutorial. Indeed, either way, the next parameter (color) will be put in the next input register. However, this still doesn’t explain why we need a float4 for the position in the PSInput structure. We will return to this in a later tutorial.

The constant buffer used in D3D12HelloConstBuffers is a resource accessed by the vertex shader, so we need to describe it with a root parameter in a root signature. In this case, we will opt for a root table with a range of a single descriptor: a CBV (Constant Buffer View) that describes the constant buffer to the GPU. For this purpose, in the application class we have a new descriptor heap (m_cbvHeap) as we can't reuse m_rtvHeap, that holds the descriptors of the two render targets (as RTVs and CBVs have different size, so that they can't share the same descriptor heap). Also, in the application class you can find new private members.

m_constantBuffer is the object we can use from our C++ application to reference the constant buffer on the upload heap. We will use it to map the constant buffer to the virtual address space of our C++ application.

m_pCbvDataBegin is the starting virtual address where we will map the constant buffer (from the upload heap) in our C++ application.

m_constantBufferData is an instance of the constant buffer structure defined in the application class. It is where we will temporarily store the constant buffer data we will copy to the memory mapped constant buffer. That way, we will initialize the constant buffer on the upload heap since the physical memory that holds the constant buffer is the same.


Now, we can take a brief look at LoadPipeline to see what's new.


// Load the rendering pipeline dependencies.
void D3D12HelloConstBuffers::LoadPipeline()
{

    // ...

 
    // Create descriptor heaps.
    {
        // Describe and create a render target view (RTV) descriptor heap.
        D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
        rtvHeapDesc.NumDescriptors = FrameCount;
        rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
        rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
        ThrowIfFailed(m_device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&m_rtvHeap)));
 
        m_rtvDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
 
        // Describe and create a constant buffer view (CBV) descriptor heap.
        // Flags indicate that this descriptor heap can be bound to the pipeline 
        // and that descriptors contained in it can be referenced by a root table.
        D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc = {};
        cbvHeapDesc.NumDescriptors = 1;
        cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
        cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
        ThrowIfFailed(m_device->CreateDescriptorHeap(&cbvHeapDesc, IID_PPV_ARGS(&m_cbvHeap)));
    }

 
    // ...

    
}

We create the two descriptor heaps we need in this sample: one for the two RTVs that describe the buffers in the swap chain and the other for the CBV that describes the constant buffer used by the vertex shader.
Observe that we set the flag D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE to specify that m_cbvHeap will be a shader visible descriptor heap associated to the command list, so that the GPU can access to the related descriptors through the byte offset passed as a root argument to a root parameter (root table) in the root signature.

Descriptor heaps created without this flag allow applications the option to stage descriptors in CPU memory before copying them to a shader visible descriptor heap, as a convenience. But it is also fine for applications to directly create descriptors into shader visible descriptor heaps with no requirement to stage anything on the CPU.


Now, we can review the code of LoadAssets.


// Load the sample assets.
void D3D12HelloConstBuffers::LoadAssets()
{
    // Create a root signature consisting of a descriptor table with a single CBV.
    {
        D3D12_FEATURE_DATA_ROOT_SIGNATURE featureData = {};
 
        // This is the highest version the sample supports. If CheckFeatureSupport succeeds, the HighestVersion returned will not be greater than this.
        featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1;
 
        if (FAILED(m_device->CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, &featureData, sizeof(featureData))))
        {
            featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0;
        }
 
        CD3DX12_DESCRIPTOR_RANGE1 ranges[1];
        CD3DX12_ROOT_PARAMETER1 rootParameters[1];
 
        ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC);
        rootParameters[0].InitAsDescriptorTable(1, &ranges[0], D3D12_SHADER_VISIBILITY_VERTEX);
 
        // Allow input layout and deny uneccessary access to certain pipeline stages.
        D3D12_ROOT_SIGNATURE_FLAGS rootSignatureFlags =
            D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT |
            D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS |
            D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS |
            D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS |
            D3D12_ROOT_SIGNATURE_FLAG_DENY_PIXEL_SHADER_ROOT_ACCESS;
 
        CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDesc;
        rootSignatureDesc.Init_1_1(_countof(rootParameters), rootParameters, 0, nullptr, rootSignatureFlags);
 
        ComPtr<ID3DBlob> signature;
        ComPtr<ID3DBlob> error;
        ThrowIfFailed(D3DX12SerializeVersionedRootSignature(&rootSignatureDesc, featureData.HighestVersion, &signature, &error));
        ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_rootSignature)));
    }
 
    // Create the pipeline state, which includes compiling and loading shaders.
    {

        // ...

 
        // Describe and create the graphics pipeline state object (PSO).
        D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
        psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) };
        psoDesc.pRootSignature = m_rootSignature.Get();
        psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShader.Get());
        psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShader.Get());
        psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
        psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
        psoDesc.DepthStencilState.DepthEnable = FALSE;
        psoDesc.DepthStencilState.StencilEnable = FALSE;
        psoDesc.SampleMask = UINT_MAX;
        psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
        psoDesc.NumRenderTargets = 1;
        psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
        psoDesc.SampleDesc.Count = 1;
 
        ThrowIfFailed(m_device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&m_pipelineState)));
    }
 
    // Create the command list. 

    // ...

 
    // Create the vertex buffer.    

    // ...

 
    // Create the constant buffer.
    {
        const UINT constantBufferSize = sizeof(SceneConstantBuffer);    // CB size is required to be 256-byte aligned.
 
        ThrowIfFailed(m_device->CreateCommittedResource(
            &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
            D3D12_HEAP_FLAG_NONE,
            &CD3DX12_RESOURCE_DESC::Buffer(constantBufferSize),
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&m_constantBuffer)));
 
        // Describe and create a constant buffer view.
        D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
        cbvDesc.BufferLocation = m_constantBuffer->GetGPUVirtualAddress();
        cbvDesc.SizeInBytes = constantBufferSize;
        m_device->CreateConstantBufferView(&cbvDesc, m_cbvHeap->GetCPUDescriptorHandleForHeapStart());
 
        // Map and initialize the constant buffer. We don't unmap this until the
        // app closes. Keeping things mapped for the lifetime of the resource is okay.
        CD3DX12_RANGE readRange(0, 0);        // We do not intend to read from this resource on the CPU.
        ThrowIfFailed(m_constantBuffer->Map(0, &readRange, reinterpret_cast<void**>(&m_pCbvDataBegin)));
        memcpy(m_pCbvDataBegin, &m_constantBufferData, sizeof(m_constantBufferData));
    }
 
    // Create synchronization objects and wait until assets have been uploaded to the GPU.

    // ...

}

We first check the highest root signature version available on the system (1.0 and 1.1 are the only versions available at the time of this writing). CheckFeatureSupport gets information about the features that are supported by the current graphics driver. The first parameter is a constant describing the feature(s) that we want to query for support. The second parameter is a pointer to a data structure whose type depends on the value passed as first parameter. The second parameter is used both as input and output parameter to pass\receive information to\from CheckFeatureSupport. The third parameter is the size of the structure passed in the second parameter.

Then, we create the root signature. The only root parameter is a descriptor table with a range of a single descriptor: the CBV that describes the constant buffer we are going to use in the vertex shader.
CD3DX12_DESCRIPTOR_RANGE1 is a helper structure to enable easy initialization of a D3D12_DESCRIPTOR_RANGE1 structure, which describes a descriptor range despite the root signature version used (that is, it allows to specify the volatility of both descriptors and data).
CD3DX12_ROOT_PARAMETER1 is a helper structure to enable easy initialization of a D3D12_ROOT_PARAMETER1 structure, which describes a descriptor table despite the root signature version used (that is, as a collection of D3D12_DESCRIPTOR_RANGE1).
CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC is a helper structure to enable easy initialization of a D3D12_VERSIONED_ROOT_SIGNATURE_DESC structure, which describe root signatures of any version (that is, root signatures that can hold root parameters of both D3D12_ROOT_PARAMETER and D3D12_ROOT_PARAMETER1).

CD3DX12_DESCRIPTOR_RANGE1::Init takes five parameters.
The first parameter specifies the type of descriptor range, while the second parameter indicates the number of descriptors in the range. In this case we have a range with a single CBV.
The third parameter specifies the slot where we want to start binding the descriptors in the range. In this case we specify 0, so the only constant buffer view we have in the range will be bound to the virtual register b0.
The fourth parameter is the register space. This is nothing more than a way to help uniquely identify a bind point without appearing to overlap with others. For now you can simply pass 0. We will return to register space in a later tutorial.
The flag D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC specifies that both descriptors and data they reference are static. That is, once we set the root table with a root argument in the command list, both the descriptors in the range and the data they reference won't change until the GPU ends with the execution of the command list. This maximizes the potential for driver optimization.

CD3DX12_ROOT_PARAMETER1:: InitAsDescriptorTable takes the number of ranges in the descriptor table, the array of ranges and the shader visibility. This last parameter specifies which shaders need to access the root table. On some hardware, there can be a performance gain from only making descriptors visible to the shaders that require them.

During the creation of the root signature, we can pass a flag the specifies which shaders we want to deny access to the root signature. In this case, we deny access for most of the stages except the vertex shader, which needs to access the constant buffer view in the root signature. Denying access for shaders that don’t need the root signature to access resources can save the hardware some work. That is, if, for example, the D3D12_SHADER_VISIBILITY_ALL flag has been be set to broadcast the root parameters to all shader stages. Then, denying access can overrule this and save the hardware some work. Alternatively, if the shader is so simple that no root signature resources are needed, then denying access could be used here too.

D3DX12SerializeVersionedRootSignature helps to serialize a root signature of any version that can be passed to ID3D12Device::CreateRootSignature.

After creating the root signature, a PSO, a command list and the vertex buffer, then we create the constant buffer.
First, we allocate memory space on the upload heap as we need to update the constant buffer from our C++ application over time. Then, we use the GPU virtual address of the constant buffer and its size to create the related CBV, that will be stored it in the first descriptor of m_cbvHeap.
We map the constant buffer to the virtual address space of our C++ app so that we can use this mapped version to initialize the constant buffer from the CPU timeline. As the comment states, keeping things mapped for the lifetime of the resource is okay. The resource will be automatically unmapped when we close the application. We use memcpy to copy the constant buffer data (m_constantBufferData) to the CPU memory mapped constant buffer. That way, we initialize\update the constant buffer on the upload heap as well, since both are backed by the same physical memory. If you see the complete code of D3D12HelloConstBuffers you can verify that m_constantBufferData is default initialized in the constructor of the application class, so the constant buffer fields will be all zero at first.


Now let’s see what happens in PopulateCommandList.


// Fill the command list with all the render commands and dependent state.
void D3D12HelloConstBuffers::PopulateCommandList()
{
    // Command list allocators can only be reset when the associated 
    // command lists have finished execution on the GPU; apps should use 
    // fences to determine GPU execution progress.
    ThrowIfFailed(m_commandAllocator->Reset());
 
    // However, when ExecuteCommandList() is called on a particular command 
    // list, that command list can then be reset at any time and must be before 
    // re-recording.
    ThrowIfFailed(m_commandList->Reset(m_commandAllocator.Get(), m_pipelineState.Get()));
 
    // Set necessary state.
    m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
 
    ID3D12DescriptorHeap* ppHeaps[] = { m_cbvHeap.Get() };
    m_commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
 
    m_commandList->SetGraphicsRootDescriptorTable(0, m_cbvHeap->GetGPUDescriptorHandleForHeapStart());
    m_commandList->RSSetViewports(1, &m_viewport);
    m_commandList->RSSetScissorRects(1, &m_scissorRect);
 
    // Indicate that the back buffer will be used as a render target.
    m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
 
    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart(), m_frameIndex, m_rtvDescriptorSize);
    m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
 
    // Record commands.
    const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
    m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
    m_commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    m_commandList->IASetVertexBuffers(0, 1, &m_vertexBufferView);
    m_commandList->DrawInstanced(3, 1, 0, 0);
 
    // Indicate that the back buffer will now be used to present.
    m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
 
    ThrowIfFailed(m_commandList->Close());
}

ID3D12GraphicsCommandList::SetDescriptorHeaps changes the currently bound descriptor heaps that are associated with a command list. At most one CBV/SRV/UAV combined heap and one Sampler heap can be bound at any one time. Whenever we have at least a shader that accesses a resource through a descriptor in a range of a descriptor table, we need to associate the related descriptor heap with the command list. In this case, we only set m_cbvHeap as shader visible descriptor heap as we won't use dynamic samplers.

The descriptor table state is undefined at the beginning of a command list, and after descriptor heaps are changed on a command list. After a descriptor heap is set in a command list, subsequent calls that define descriptor tables refer to the current descriptor heap.

With ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable we set a descriptor table into the graphics root signature (that is, we pass a root table to a root parameter). The first parameter is the index of the root parameter in the root signature we want to set. The second parameter is a GPU handle to the first descriptor of the first range in the descriptor table.

A descriptor table knows both the number of ranges and the number of descriptors each range contains. So, we can simply pass the first descriptor of the first range as second argument to SetGraphicsRootDescriptorTable. The only requirement is that the descriptors in each range of the descriptor table are contiguous in the descriptor heap (as shown in the figure below). Although, we can have non-contiguous ranges of descriptors. We will return to this in a later tutorial.


Image


At this point, when DrawInstanced is executed by the GPU, the vertex shader can read the constant buffer as the related view is bound to the slot b0 as specified in CD3DX12_ROOT_PARAMETER1:: InitAsDescriptorTable. The problem is that we initialized the members of the constant buffer to zero, so we won’t see any animation. We need to constantly update the constant buffer data over time to see any effect on the screen.
If you remember what we stated in the first tutorial, a WM_PAINT message is returned by PeekMessage when there are no other messages in the application's message queue. That way, the window’s client area is constantly repainted, and we can call both OnUpdate and OnRender in the WM_PAINT message handler to render updated geometries on the screen. So far, we just wasted time calling OnUpdate as an empty method. Now, we can use it to update the constant buffer data over time before calling OnRender.


// Update frame-based values.
void D3D12HelloConstBuffers::OnUpdate()
{
    const float translationSpeed = 0.015f;
    const float offsetBounds = 1.25f;
 
    m_constantBufferData.offset.x += translationSpeed;
    if (m_constantBufferData.offset.x > offsetBounds)
    {
        m_constantBufferData.offset.x = -offsetBounds;
    }
    memcpy(m_pCbvDataBegin, &m_constantBufferData, sizeof(m_constantBufferData));
}

Here we update the constant buffer data stored in the instance of the constant buffer defined in the application class (m_constantBufferData). Then we copy it to the memory mapped constant buffer (m_pCbvDataBegin). This also updates the constant buffer on the upload heap (m_constantBuffer) as both are backed by the same physical memory.

As stated in the previous tutorial, a primitive is rendered on the screen as long as the x- and y-coordinates are in the range $[−1,\ 1]$. In OnUpdate we restrict both these coordinates in the interval $[−1.25,\ +1.25]$ so that the triangle exits one side and enter the other one without disappearing.



Source code: D3D12HelloWorld (DirectX-Graphics-Samples)


References

[1] DirectX graphics and gaming | Microsoft Docs
[2] DirectX-Specs | Microsoft Docs



If you found the content of this tutorial somewhat useful or interesting, please consider supporting this project by clicking on the Sponsor button. Whether a small tip, a one time donation, or a recurring payment, it's all welcome! Thank you!


Clone this wiki locally