The raster pipeline is the oldest and arguably most often used execution pipeline for GPUs. If you want to render primitives for meshes and UIs quickly and cheaply this is the way to do it. That being said, this may get phased out gradually in the distant future with the meshlet pipeline, though it's worth reviewing the pipeline that has made up the majority of all graphics cards over the past few decades:
Descriptor Heaps
Descriptor heaps are objects that handle memory allocation required for storing the descriptions of objects that shaders reference.
ID3D12DescriptorHeap* renderTargetViewHeap;
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
rtvHeapDesc.NumDescriptors = backbufferCount;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
ThrowIfFailed(device->CreateDescriptorHeap(
&rtvHeapDesc, IID_PPV_ARGS(&renderTargetViewHeap)));Root Signature
Root Signatures are objects that define what type of resources are accessible to your shaders, be it constant buffers, structured buffers, samplers, textures, structured buffers, etc.
// 👋 Declare Handles
ID3D12RootSignature* rootSignature;
// 🔎 Determine if we can get Root Signature Version 1.1:
D3D12_FEATURE_DATA_ROOT_SIGNATURE featureData = {};
featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1;
if (FAILED(device->CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE,
&featureData, sizeof(featureData))))
{
featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0;
}
// 📂 Individual GPU Resources
D3D12_DESCRIPTOR_RANGE1 ranges[1];
ranges[0].BaseShaderRegister = 0;
ranges[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
ranges[0].NumDescriptors = 1;
ranges[0].RegisterSpace = 0;
ranges[0].OffsetInDescriptorsFromTableStart = 0;
ranges[0].Flags = D3D12_DESCRIPTOR_RANGE_FLAG_NONE;
//🗄️ Groups of GPU Resources
D3D12_ROOT_PARAMETER1 rootParameters[1];
rootParameters[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rootParameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;
rootParameters[0].DescriptorTable.NumDescriptorRanges = 1;
rootParameters[0].DescriptorTable.pDescriptorRanges = ranges;
// 🏢 Overall Layout
D3D12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Version = D3D_ROOT_SIGNATURE_VERSION_1_1;
rootSignatureDesc.Desc_1_1.Flags =
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
rootSignatureDesc.Desc_1_1.NumParameters = 1;
rootSignatureDesc.Desc_1_1.pParameters = rootParameters;
rootSignatureDesc.Desc_1_1.NumStaticSamplers = 0;
rootSignatureDesc.Desc_1_1.pStaticSamplers = nullptr;
ID3DBlob* signature;
ID3DBlob* error;
try
{
// 🌱 Create the root signature
ThrowIfFailed(D3D12SerializeVersionedRootSignature(&rootSignatureDesc,
&signature, &error));
ThrowIfFailed(device->CreateRootSignature(0, signature->GetBufferPointer(),
signature->GetBufferSize(),
IID_PPV_ARGS(&rootSignature)));
rootSignature->SetName(L"Hello Triangle Root Signature");
}
catch (std::exception e)
{
const char* errStr = (const char*)error->GetBufferPointer();
std::cout << errStr;
error->Release();
error = nullptr;
}
if (signature)
{
signature->Release();
signature = nullptr;
}While these work well enough, using bindless resources is significantly more easy, Matt Pettineo (@MyNameIsMJP) wrote a blog post and chapter in Ray Tracing Gems 2 about this.
Heaps
Heaps are objects that encompass GPU memory. They can be used to upload resources like vertex buffers or textures to GPU exclusive memory.
// 🔼 Upload:
// 👋 Declare Handles
ID3D12Resource* uploadBuffer;
std::vector<unsigned char> sourceData;
D3D12_HEAP_PROPERTIES uploadHeapProps = {D3D12_HEAP_TYPE_UPLOAD,
D3D12_CPU_PAGE_PROPERTY_UNKNOWN,
D3D12_MEMORY_POOL_UNKNOWN, 1u, 1u};
D3D12_RESOURCE_DESC uploadBufferDesc = {D3D12_RESOURCE_DIMENSION_BUFFER,
65536ull,
65536ull,
1u,
1,
1,
DXGI_FORMAT_UNKNOWN,
{1u, 0u},
D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
D3D12_RESOURCE_FLAG_NONE};
result = device->CreateCommittedResource(
&uploadHeapProps, D3D12_HEAP_FLAG_NONE, &uploadBufferDesc,
D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, __uuidof(ID3D12Resource),
((void**)&uploadBuffer));
uint8_t* data = nullptr;
D3D12_RANGE range{0, SIZE_T(chunkSize)};
auto hr = spStaging -> Map(0, &range, reinterpret_cast<void**>(&data));
if (FAILED(hr))
{
std::cout << "Could not map resource";
}
// Copy data
if (resourceDesc.Dimension == D3D12_RESOURCE_DIMENSION_BUFFER)
{
memcpy(data, sourceData.data(), sourceData.size());
}
// 📖 Readback:
// 👋 Declare Handles
ID3D12Resource* readbackBuffer;
D3D12_HEAP_PROPERTIES heapPropsRead = {D3D12_HEAP_TYPE_READBACK,
D3D12_CPU_PAGE_PROPERTY_UNKNOWN,
D3D12_MEMORY_POOL_UNKNOWN, 1u, 1u};
D3D12_RESOURCE_DESC resourceDescDimBuffer = {
D3D12_RESOURCE_DIMENSION_BUFFER,
D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT,
2725888ull,
1u,
1,
1,
DXGI_FORMAT_UNKNOWN,
{1u, 0u},
D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
D3D12_RESOURCE_FLAG_DENY_SHADER_RESOURCE};
result = device->CreateCommittedResource(
&heapPropsRead, D3D12_HEAP_FLAG_NONE, &resourceDescDimBuffer,
D3D12_RESOURCE_STATE_COPY_DEST, nullptr, __uuidof(ID3D12Resource),
((void**)&readbackBuffer));You can be even more granular with your memory management by creating your own heaps. It can be a handful to manage heaps though, so you could use a memory allocation library instead.
Vertex Buffer
A Vertex Buffer stores the per vertex information available as attributes in your Vertex Shader. All buffers are ID3D12Resource objects in DirectX 12, be it Vertex Buffers, Index Buffers, Constant Buffers, etc.
// 💾 Declare Data
struct Vertex
{
float position[3];
float color[3];
};
Vertex vertexBufferData[3] = {{{1.0f, -1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}},
{{-1.0f, -1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}},
{{0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}}};
// 👋 Declare Handles
ID3D12Resource* vertexBuffer;
D3D12_VERTEX_BUFFER_VIEW vertexBufferView;
const UINT vertexBufferSize = sizeof(vertexBufferData);
D3D12_HEAP_PROPERTIES heapProps;
heapProps.Type = D3D12_HEAP_TYPE_UPLOAD;
heapProps.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapProps.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
heapProps.CreationNodeMask = 1;
heapProps.VisibleNodeMask = 1;
D3D12_RESOURCE_DESC vertexBufferResourceDesc;
vertexBufferResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
vertexBufferResourceDesc.Alignment = 0;
vertexBufferResourceDesc.Width = vertexBufferSize;
vertexBufferResourceDesc.Height = 1;
vertexBufferResourceDesc.DepthOrArraySize = 1;
vertexBufferResourceDesc.MipLevels = 1;
vertexBufferResourceDesc.Format = DXGI_FORMAT_UNKNOWN;
vertexBufferResourceDesc.SampleDesc.Count = 1;
vertexBufferResourceDesc.SampleDesc.Quality = 0;
vertexBufferResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
vertexBufferResourceDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
ThrowIfFailed(device->CreateCommittedResource(
&heapProps, D3D12_HEAP_FLAG_NONE, &vertexBufferResourceDesc,
D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&vertexBuffer)));
// 📄 Copy the triangle data to the vertex buffer.
UINT8* pVertexDataBegin;
// 🙈 We do not intend to read from this resource on the CPU.
D3D12_RANGE readRange;
readRange.Begin = 0;
readRange.End = 0;
ThrowIfFailed(vertexBuffer->Map(0, &readRange,
reinterpret_cast<void**>(&pVertexDataBegin)));
memcpy(pVertexDataBegin, vertexBufferData, sizeof(vertexBufferData));
vertexBuffer->Unmap(0, nullptr);
// 👀 Initialize the vertex buffer view.
vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress();
vertexBufferView.StrideInBytes = sizeof(Vertex);
vertexBufferView.SizeInBytes = vertexBufferSize;Index Buffer
An Index Buffer contains the individual indices of each triangle/line/point that you intend to draw.
// 💾 Declare Data
uint32_t indexBufferData[3] = {0, 1, 2};
// 👋 Declare Handles
ID3D12Resource* indexBuffer;
D3D12_INDEX_BUFFER_VIEW indexBufferView;
const UINT indexBufferSize = sizeof(indexBufferData);
D3D12_HEAP_PROPERTIES heapProps;
heapProps.Type = D3D12_HEAP_TYPE_UPLOAD;
heapProps.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapProps.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
heapProps.CreationNodeMask = 1;
heapProps.VisibleNodeMask = 1;
D3D12_RESOURCE_DESC vertexBufferResourceDesc;
vertexBufferResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
vertexBufferResourceDesc.Alignment = 0;
vertexBufferResourceDesc.Width = indexBufferSize;
vertexBufferResourceDesc.Height = 1;
vertexBufferResourceDesc.DepthOrArraySize = 1;
vertexBufferResourceDesc.MipLevels = 1;
vertexBufferResourceDesc.Format = DXGI_FORMAT_UNKNOWN;
vertexBufferResourceDesc.SampleDesc.Count = 1;
vertexBufferResourceDesc.SampleDesc.Quality = 0;
vertexBufferResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
vertexBufferResourceDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
ThrowIfFailed(device->CreateCommittedResource(
&heapProps, D3D12_HEAP_FLAG_NONE, &vertexBufferResourceDesc,
D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&indexBuffer)));
// 📄 Copy data to DirectX 12 driver memory:
UINT8* pVertexDataBegin;
// 🙈 We do not intend to read from this resource on the CPU.
D3D12_RANGE readRange;
readRange.Begin = 0;
readRange.End = 0;
ThrowIfFailed(indexBuffer->Map(0, &readRange,
reinterpret_cast<void**>(&pVertexDataBegin)));
memcpy(pVertexDataBegin, indexBufferData, sizeof(indexBufferData));
indexBuffer->Unmap(0, nullptr);
// 👀 Initialize the index buffer view.
indexBufferView.BufferLocation = indexBuffer->GetGPUVirtualAddress();
indexBufferView.Format = DXGI_FORMAT_R32_UINT;
indexBufferView.SizeInBytes = indexBufferSize;Constant Buffer
A Constant Buffer describes data that we'll be sending to shader stages when drawing. Normally you would put Model View Projection Matrices or any specific variable data like colors, sliders, etc. here.
// 💾 Declare Data
struct
{
glm::mat4 projectionMatrix;
glm::mat4 modelMatrix;
glm::mat4 viewMatrix;
} cbVS;
// 👋 Declare Handles
ID3D12Resource* constantBuffer;
ID3D12DescriptorHeap* constantBufferHeap;
UINT8* mappedConstantBuffer;
// 🧊 Create the Constant Buffer
D3D12_HEAP_PROPERTIES heapProps;
heapProps.Type = D3D12_HEAP_TYPE_UPLOAD;
heapProps.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapProps.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
heapProps.CreationNodeMask = 1;
heapProps.VisibleNodeMask = 1;
D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {};
heapDesc.NumDescriptors = 1;
heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
ThrowIfFailed(device->CreateDescriptorHeap(&heapDesc,
IID_PPV_ARGS(&constantBufferHeap)));
D3D12_RESOURCE_DESC cbResourceDesc;
cbResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
cbResourceDesc.Alignment = 0;
cbResourceDesc.Width = (sizeof(cbVS) + 255) & ~255;
cbResourceDesc.Height = 1;
cbResourceDesc.DepthOrArraySize = 1;
cbResourceDesc.MipLevels = 1;
cbResourceDesc.Format = DXGI_FORMAT_UNKNOWN;
cbResourceDesc.SampleDesc.Count = 1;
cbResourceDesc.SampleDesc.Quality = 0;
cbResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
cbResourceDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
ThrowIfFailed(device->CreateCommittedResource(
&heapProps, D3D12_HEAP_FLAG_NONE, &cbResourceDesc,
D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&constantBuffer)));
constantBufferHeap->SetName(L"Constant Buffer Upload Resource Heap");
// 👓 Create our Constant Buffer View
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
cbvDesc.BufferLocation = constantBuffer->GetGPUVirtualAddress();
cbvDesc.SizeInBytes =
(sizeof(cbVS) + 255) & ~255; // CB size is required to be 256-byte aligned.
D3D12_CPU_DESCRIPTOR_HANDLE
cbvHandle(constantBufferHeap->GetCPUDescriptorHandleForHeapStart());
cbvHandle.ptr = cbvHandle.ptr + device->GetDescriptorHandleIncrementSize(
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV) *
0;
device->CreateConstantBufferView(&cbvDesc, cbvHandle);
// 🙈 We do not intend to read from this resource on the CPU.
D3D12_RANGE readRange;
readRange.Begin = 0;
readRange.End = 0;
ThrowIfFailed(constantBuffer->Map(
0, &readRange, reinterpret_cast<void**>(&mappedConstantBuffer)));
memcpy(mappedConstantBuffer, &cbVS, sizeof(cbVS));
constantBuffer->Unmap(0, &readRange);Vertex Shader
Vertex Shaders execute per vertex, and are perfect for transforming a given object, performing per vertex animations with blend shapes, GPU skinning, etc.
cbuffer cb : register(b0)
{
row_major float4x4 projectionMatrix : packoffset(c0);
row_major float4x4 modelMatrix : packoffset(c4);
row_major float4x4 viewMatrix : packoffset(c8);
};
struct VertexInput
{
float3 inPos : POSITION;
float3 inColor : COLOR;
};
struct VertexOutput
{
float3 color : COLOR;
float4 position : SV_Position;
};
VertexOutput main(VertexInput vertexInput)
{
float3 inColor = vertexInput.inColor;
float3 inPos = vertexInput.inPos;
float3 outColor = inColor;
float4 position = mul(float4(inPos, 1.0f), mul(modelMatrix, mul(viewMatrix, projectionMatrix)));
VertexOutput output;
output.position = position;
output.color = outColor;
return output;
}You could compile shaders using the legacy DirectX Shader Compiler bundled with the DirectX 11/12 API, but it's best to use the newer official compiler.
dxc.exe -T lib_6_3 -Fo assets/triangle.vert.dxil assets/triangle.vert.hlsl
Then you could load the shader as a binary file:
// Helper to read a binary file:
inline std::vector<char> readFile(const std::string& filename)
{
std::ifstream file(filename, std::ios::ate | std::ios::binary);
bool exists = (bool)file;
if (!exists || !file.is_open())
{
throw std::runtime_error("failed to open file!");
}
size_t fileSize = (size_t)file.tellg();
std::vector<char> buffer(fileSize);
file.seekg(0);
file.read(buffer.data(), fileSize);
file.close();
return buffer;
};
// 👋 Declare handles
D3D12_SHADER_BYTECODE vsBytecode;
std::string compiledPath;
std::vector<char> vsBytecodeData = readFile(compCompiledPath);
vsBytecode.pShaderBytecode = vsBytecodeData.data();
vsBytecode.BytecodeLength = vsBytecodeData.size();Pixel Shader
Pixel Shaders execute per each pixel of your output, including the other attachments that correspond to that pixel coordinate.
struct PixelInput
{
float3 color : COLOR;
};
struct PixelOutput
{
float4 attachment0 : SV_Target0;
};
PixelOutput main(PixelInput pixelInput)
{
float3 inColor = pixelInput.color;
PixelOutput output;
output.attachment0 = float4(inColor, 1.0f);
return output;
}Pipeline State
The Pipeline State describes everything necessary to execute a given raster based draw call.
// 👋 Declare handles
ID3D12PipelineState* pipelineState;
// ⚗️ Define the Graphics Pipeline
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
// 🔣 Input Assembly
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}};
psoDesc.InputLayout = {inputElementDescs, _countof(inputElementDescs)};
// 🦄 Resources
psoDesc.pRootSignature = rootSignature;
// 🔺 Vertex Shader
D3D12_SHADER_BYTECODE vsBytecode;
vsBytecode.pShaderBytecode = vertexShaderBlob->GetBufferPointer();
vsBytecode.BytecodeLength = vertexShaderBlob->GetBufferSize();
psoDesc.VS = vsBytecode;
// 🖌️ Pixel Shader
D3D12_SHADER_BYTECODE psBytecode;
psBytecode.pShaderBytecode = pixelShaderBlob->GetBufferPointer();
psBytecode.BytecodeLength = pixelShaderBlob->GetBufferSize();
psoDesc.PS = psBytecode;
// 🟨 Rasterization
D3D12_RASTERIZER_DESC rasterDesc;
rasterDesc.FillMode = D3D12_FILL_MODE_SOLID;
rasterDesc.CullMode = D3D12_CULL_MODE_NONE;
rasterDesc.FrontCounterClockwise = FALSE;
rasterDesc.DepthBias = D3D12_DEFAULT_DEPTH_BIAS;
rasterDesc.DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP;
rasterDesc.SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS;
rasterDesc.DepthClipEnable = TRUE;
rasterDesc.MultisampleEnable = FALSE;
rasterDesc.AntialiasedLineEnable = FALSE;
rasterDesc.ForcedSampleCount = 0;
rasterDesc.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF;
psoDesc.RasterizerState = rasterDesc;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
// 🌀 Color/Blend
D3D12_BLEND_DESC blendDesc;
blendDesc.AlphaToCoverageEnable = FALSE;
blendDesc.IndependentBlendEnable = FALSE;
const D3D12_RENDER_TARGET_BLEND_DESC defaultRenderTargetBlendDesc = {
FALSE,
FALSE,
D3D12_BLEND_ONE,
D3D12_BLEND_ZERO,
D3D12_BLEND_OP_ADD,
D3D12_BLEND_ONE,
D3D12_BLEND_ZERO,
D3D12_BLEND_OP_ADD,
D3D12_LOGIC_OP_NOOP,
D3D12_COLOR_WRITE_ENABLE_ALL,
};
for (UINT i = 0; i < D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT; ++i)
blendDesc.RenderTarget[i] = defaultRenderTargetBlendDesc;
psoDesc.BlendState = blendDesc;
// 🌑 Depth/Stencil State
psoDesc.DepthStencilState.DepthEnable = FALSE;
psoDesc.DepthStencilState.StencilEnable = FALSE;
psoDesc.SampleMask = UINT_MAX;
// 🖼️ Output
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
psoDesc.SampleDesc.Count = 1;
// 🌟 Create the raster pipeline state
try
{
ThrowIfFailed(device->CreateGraphicsPipelineState(
&psoDesc, IID_PPV_ARGS(&pipelineState)));
}
catch (std::exception e)
{
std::cout << "Failed to create Graphics Pipeline!";
}Encoding Commands
In order to execute draw calls, you're going to need a place to write commands. A Command List is an object that can encode a number of commands to be executed by the GPU, be it configuring barriers, setting root signatures, etc.
// 👋 Declare handles
ID3D12CommandAllocator* commandAllocator;
ID3D12PipelineState* initialPipelineState;
ID3D12GraphicsCommandList* commandList;
// 📃 Create the command list.
ThrowIfFailed(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT,
commandAllocator, initialPipelineState,
IID_PPV_ARGS(&commandList)));Later, encode those commands and submit them:
// 🚿 Reset the command list and add new commands.
ThrowIfFailed(commandAllocator->Reset());
// 🖌️ Begin using the Raster Graphics Pipeline
ThrowIfFailed(commandList->Reset(commandAllocator, pipelineState));
// 🔳 Setup Resources
commandList->SetGraphicsRootSignature(rootSignature);
ID3D12DescriptorHeap* pDescriptorHeaps[] = {constantBufferHeap};
commandList->SetDescriptorHeaps(_countof(pDescriptorHeaps), pDescriptorHeaps);
D3D12_GPU_DESCRIPTOR_HANDLE
cbvHandle(constantBufferHeap->GetGPUDescriptorHandleForHeapStart());
commandList->SetGraphicsRootDescriptorTable(0, cbvHandle);
// 🖼️ Indicate that the back buffer will be used as a render target.
D3D12_RESOURCE_BARRIER renderTargetBarrier;
renderTargetBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
renderTargetBarrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
renderTargetBarrier.Transition.pResource = renderTargets[frameIndex];
renderTargetBarrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
renderTargetBarrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
renderTargetBarrier.Transition.Subresource =
D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
commandList->ResourceBarrier(1, &renderTargetBarrier);
D3D12_CPU_DESCRIPTOR_HANDLE
rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart());
rtvHandle.ptr = rtvHandle.ptr + (frameIndex * rtvDescriptorSize);
commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
// 🎥 Record raster commands.
const float clearColor[] = {0.2f, 0.2f, 0.2f, 1.0f};
commandList->RSSetViewports(1, &viewport);
commandList->RSSetScissorRects(1, &surfaceSize);
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commandList->IASetVertexBuffers(0, 1, &vertexBufferView);
commandList->IASetIndexBuffer(&indexBufferView);
commandList->DrawIndexedInstanced(3, 1, 0, 0, 0);
// 🖼️ Indicate that the back buffer will now be used to present.
D3D12_RESOURCE_BARRIER presentBarrier;
presentBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
presentBarrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
presentBarrier.Transition.pResource = renderTargets[frameIndex];
presentBarrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
presentBarrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
presentBarrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
commandList->ResourceBarrier(1, &presentBarrier);
ThrowIfFailed(commandList->Close());Render

Rendering in DirectX 12 is a simple matter of changing any constant buffer data you intend to update, submitting your command lists to be executed, presenting the swapchain so your Win32 or UWP window updates, and signaling your application that you've finished rendering.
// 👋 declare handles
std::chrono::time_point<std::chrono::steady_clock> tStart, tEnd;
float elapsedTime = 0.0f;
void render()
{
// ⌚ Frame limit set to 60 fps
tEnd = std::chrono::high_resolution_clock::now();
float time =
std::chrono::duration<float, std::milli>(tEnd - tStart).count();
if (time < (1000.0f / 60.0f))
{
return;
}
tStart = std::chrono::high_resolution_clock::now();
// 🎛️ Update Uniforms
elapsedTime += 0.001f * time;
elapsedTime = fmodf(elapsedTime, 6.283185307179586f);
cbVS.modelMatrix = Matrix4::rotationY(elapsedTime);
D3D12_RANGE readRange;
readRange.Begin = 0;
readRange.End = 0;
ThrowIfFailed(constantBuffer->Map(
0, &readRange, reinterpret_cast<void**>(&mappedConstantBuffer)));
memcpy(mappedConstantBuffer, &cbVS, sizeof(cbVS));
constantBuffer->Unmap(0, &readRange);
setupCommands();
ID3D12CommandList* ppCommandLists[] = {commandList};
commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
// 🎥 Present, then wait till finished to continue execution
swapchain->Present(1, 0);
const UINT64 fence = fenceValue;
ThrowIfFailed(commandQueue->Signal(fence, fence));
fenceValue++;
if (fence->GetCompletedValue() < fence)
{
ThrowIfFailed(fence->SetEventOnCompletion(fence, fenceEvent));
WaitForSingleObject(fenceEvent, INFINITE);
}
frameIndex = swapchain->GetCurrentBackBufferIndex();
}Destroy Handles
If you're using ComPtr<T> data structures, then just like with shared pointers, you don't need to worry about destroying any handles you create. If you aren't, you can call the Release() function built into every DirectX data structure.
Alain Galvan