Indirektes Zeichnen und GPU-Culling
Im D3D12ExecuteIndirect-Beispiel wird veranschaulicht, wie indirekte Befehle zum Zeichnen von Inhalten verwendet werden. Außerdem wird veranschaulicht, wie diese Befehle auf der GPU in einem Compute-Shader bearbeitet werden können, bevor sie ausgegeben werden.
- Definieren der indirekten Befehle
- Erstellen einer Grafik- und Computestammsignatur
- Erstellen einer Shaderressourcensicht (SRV) für den Compute-Shader
- Erstellen der indirekten Befehlspuffer
- Erstellen der Compute-UAVs
- Zeichnen des Rahmens
- Ausführen des Beispiels
- Zugehörige Themen
Im Beispiel wird ein Befehlspuffer erstellt, der 1024 Zeichnungsaufrufe beschreibt. Jeder Zeichnungsaufruf rendert ein Dreieck mit einer zufälligen Farbe, Position und Geschwindigkeit. Die Dreiecke animieren endlos über den Bildschirm. In diesem Beispiel gibt es zwei Modi. Im ersten Modus überprüft ein Compute-Shader die indirekten Befehle und entscheidet, ob dieser Befehl einer ungeordneten Zugriffssicht (UAV) hinzugefügt werden soll, die beschreibt, welche Befehle ausgeführt werden sollen. Im zweiten Modus werden alle Befehle einfach ausgeführt. Wenn Sie die Leertaste drücken, wechseln Sie zwischen den Modi.
Definieren der indirekten Befehle
Zunächst definieren wir, wie die indirekten Befehle aussehen sollen. In diesem Beispiel sollen folgende Befehle ausgeführt werden:
- 1. Aktualisieren Sie die Konstantenpuffersicht (CBV).
2. Zeichnen Sie das Dreieck.
Diese Zeichnungsbefehle werden durch die folgende Struktur in der D3D12ExecuteIndirect-Klassendefinition dargestellt. Befehle werden sequenziell in der Reihenfolge ausgeführt, in der sie in dieser Struktur definiert sind.
// Data structure to match the command signature used for ExecuteIndirect.
struct IndirectCommand
{
D3D12_GPU_VIRTUAL_ADDRESS cbv;
D3D12_DRAW_ARGUMENTS drawArguments;
};
Anrufverlauf | Parameter |
---|---|
D3D12_GPU_VIRTUAL_ADDRESS (einfach ein UINT64) | |
D3D12_DRAW_ARGUMENTS |
Zur Begleitung der Datenstruktur wird auch eine Befehlssignatur erstellt, die die GPU anweist, wie die an die ExecuteIndirect-API übergebenen Daten interpretiert werden. Dieser und der größte Teil des folgenden Codes wird der LoadAssets-Methode hinzugefügt.
// Create the command signature used for indirect drawing.
{
// Each command consists of a CBV update and a DrawInstanced call.
D3D12_INDIRECT_ARGUMENT_DESC argumentDescs[2] = {};
argumentDescs[0].Type = D3D12_INDIRECT_ARGUMENT_TYPE_CONSTANT_BUFFER_VIEW;
argumentDescs[0].ConstantBufferView.RootParameterIndex = Cbv;
argumentDescs[1].Type = D3D12_INDIRECT_ARGUMENT_TYPE_DRAW;
D3D12_COMMAND_SIGNATURE_DESC commandSignatureDesc = {};
commandSignatureDesc.pArgumentDescs = argumentDescs;
commandSignatureDesc.NumArgumentDescs = _countof(argumentDescs);
commandSignatureDesc.ByteStride = sizeof(IndirectCommand);
ThrowIfFailed(m_device->CreateCommandSignature(&commandSignatureDesc, m_rootSignature.Get(), IID_PPV_ARGS(&m_commandSignature)));
}
Anrufverlauf | Parameter |
---|---|
D3D12_INDIRECT_ARGUMENT_DESC | D3D12_INDIRECT_ARGUMENT_TYPE |
D3D12_COMMAND_SIGNATURE_DESC | |
CreateCommandSignature |
Erstellen einer Grafik- und Computestammsignatur
Außerdem erstellen wir sowohl eine Grafik als auch eine Computestammsignatur. Die Grafikstammsignatur definiert nur eine Stamm-CBV. Beachten Sie, dass wir den Index dieses Stammparameters im D3D12_INDIRECT_ARGUMENT_DESC (siehe oben) zuordnen, wenn die Befehlssignatur definiert ist. Die Computestammsignatur definiert Folgendes:
- Eine allgemeine Deskriptortabelle mit drei Slots (zwei SRV und ein UAV):
- Eine SRV macht die Konstantenpuffer für den Compute-Shader verfügbar.
- Eine SRV macht den Befehlspuffer für den Compute-Shader verfügbar.
- Im UAV speichert der Compute-Shader die Befehle für die sichtbaren Dreiecke.
- Vier Stammkonstanten:
- Halbe Breite einer Seite des Dreiecks
- Die z-Position der Dreieckspunkte
- Der +/- x-Offset der Kullingsebene im homogenen Raum [-1,1]
- Die Anzahl indirekter Befehle im Befehlspuffer
// Create the root signatures.
{
CD3DX12_ROOT_PARAMETER rootParameters[GraphicsRootParametersCount];
rootParameters[Cbv].InitAsConstantBufferView(0, 0, D3D12_SHADER_VISIBILITY_VERTEX);
CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Init(_countof(rootParameters), rootParameters, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
ComPtr<ID3DBlob> signature;
ComPtr<ID3DBlob> error;
ThrowIfFailed(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));
ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_rootSignature)));
// Create compute signature.
CD3DX12_DESCRIPTOR_RANGE ranges[2];
ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 2, 0);
ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0);
CD3DX12_ROOT_PARAMETER computeRootParameters[ComputeRootParametersCount];
computeRootParameters[SrvUavTable].InitAsDescriptorTable(2, ranges);
computeRootParameters[RootConstants].InitAsConstants(4, 0);
CD3DX12_ROOT_SIGNATURE_DESC computeRootSignatureDesc;
computeRootSignatureDesc.Init(_countof(computeRootParameters), computeRootParameters);
ThrowIfFailed(D3D12SerializeRootSignature(&computeRootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));
ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_computeRootSignature)));
}
Erstellen einer Shaderressourcensicht (SRV) für den Compute-Shader
Nach dem Erstellen der Pipelinezustandsobjekte, Vertexpuffer, einer Tiefenschablone und der Konstantenpuffer erstellt das Beispiel dann eine Shaderressourcensicht (SRV) des Konstantenpuffers, sodass der Compute-Shader auf die Daten im Konstantenpuffer zugreifen kann.
// Create shader resource views (SRV) of the constant buffers for the
// compute shader to read from.
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Format = DXGI_FORMAT_UNKNOWN;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_BUFFER;
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Buffer.NumElements = TriangleCount;
srvDesc.Buffer.StructureByteStride = sizeof(ConstantBufferData);
srvDesc.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_NONE;
CD3DX12_CPU_DESCRIPTOR_HANDLE cbvSrvHandle(m_cbvSrvUavHeap->GetCPUDescriptorHandleForHeapStart(), CbvSrvOffset, m_cbvSrvUavDescriptorSize);
for (UINT frame = 0; frame < FrameCount; frame++)
{
srvDesc.Buffer.FirstElement = frame * TriangleCount;
m_device->CreateShaderResourceView(m_constantBuffer.Get(), &srvDesc, cbvSrvHandle);
cbvSrvHandle.Offset(CbvSrvUavDescriptorCountPerFrame, m_cbvSrvUavDescriptorSize);
}
Anrufverlauf | Parameter |
---|---|
D3D12_SHADER_RESOURCE_VIEW_DESC | |
CD3DX12_CPU_DESCRIPTOR_HANDLE | GetCPUDescriptorHandleForHeapStart |
CreateShaderResourceView |
Erstellen der indirekten Befehlspuffer
Anschließend erstellen wir die indirekten Befehlspuffer und definieren ihren Inhalt mithilfe des folgenden Codes. Wir zeichnen dieselben Dreieckspunkte 1024 Mal, zeigen aber bei jedem Zeichnungsaufruf auf einen anderen konstanten Pufferspeicherort.
D3D12_GPU_VIRTUAL_ADDRESS gpuAddress = m_constantBuffer->GetGPUVirtualAddress();
UINT commandIndex = 0;
for (UINT frame = 0; frame < FrameCount; frame++)
{
for (UINT n = 0; n < TriangleCount; n++)
{
commands[commandIndex].cbv = gpuAddress;
commands[commandIndex].drawArguments.VertexCountPerInstance = 3;
commands[commandIndex].drawArguments.InstanceCount = 1;
commands[commandIndex].drawArguments.StartVertexLocation = 0;
commands[commandIndex].drawArguments.StartInstanceLocation = 0;
commandIndex++;
gpuAddress += sizeof(ConstantBufferData);
}
}
Anrufverlauf | Parameter |
---|---|
D3D12_GPU_VIRTUAL_ADDRESS | GetGPUVirtualAddress |
Nach dem Hochladen der Befehlspuffer in die GPU erstellen wir auch eine SRV davon, aus denen der Compute-Shader lesen kann. Dies ähnelt sehr der SRV, die aus dem Konstantenpuffer erstellt wird.
// Create SRVs for the command buffers.
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Format = DXGI_FORMAT_UNKNOWN;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_BUFFER;
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Buffer.NumElements = TriangleCount;
srvDesc.Buffer.StructureByteStride = sizeof(IndirectCommand);
srvDesc.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_NONE;
CD3DX12_CPU_DESCRIPTOR_HANDLE commandsHandle(m_cbvSrvUavHeap->GetCPUDescriptorHandleForHeapStart(), CommandsOffset, m_cbvSrvUavDescriptorSize);
for (UINT frame = 0; frame < FrameCount; frame++)
{
srvDesc.Buffer.FirstElement = frame * TriangleCount;
m_device->CreateShaderResourceView(m_commandBuffer.Get(), &srvDesc, commandsHandle);
commandsHandle.Offset(CbvSrvUavDescriptorCountPerFrame, m_cbvSrvUavDescriptorSize);
}
Anrufverlauf | Parameter |
---|---|
D3D12_SHADER_RESOURCE_VIEW_DESC | |
CD3DX12_CPU_DESCRIPTOR_HANDLE | GetCPUDescriptorHandleForHeapStart |
CreateShaderResourceView |
Erstellen der Compute-UAVs
Wir müssen die UAVs erstellen, in denen die Ergebnisse der Computearbeit gespeichert werden. Wenn ein Dreieck vom Compute-Shader als sichtbar für das Renderziel angesehen wird, wird es an dieses UAV angefügt und dann von der ExecuteIndirect-API verwendet.
CD3DX12_CPU_DESCRIPTOR_HANDLE processedCommandsHandle(m_cbvSrvUavHeap->GetCPUDescriptorHandleForHeapStart(), ProcessedCommandsOffset, m_cbvSrvUavDescriptorSize);
for (UINT frame = 0; frame < FrameCount; frame++)
{
// Allocate a buffer large enough to hold all of the indirect commands
// for a single frame as well as a UAV counter.
commandBufferDesc = CD3DX12_RESOURCE_DESC::Buffer(CommandBufferSizePerFrame + sizeof(UINT), D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
CD3DX12_HEAP_PROPERTIES heapProps(D3D12_HEAP_TYPE_DEFAULT);
ThrowIfFailed(m_device->CreateCommittedResource(
&heapProps,
D3D12_HEAP_FLAG_NONE,
&commandBufferDesc,
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr,
IID_PPV_ARGS(&m_processedCommandBuffers[frame])));
D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {};
uavDesc.Format = DXGI_FORMAT_UNKNOWN;
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_BUFFER;
uavDesc.Buffer.FirstElement = 0;
uavDesc.Buffer.NumElements = TriangleCount;
uavDesc.Buffer.StructureByteStride = sizeof(IndirectCommand);
uavDesc.Buffer.CounterOffsetInBytes = CommandBufferSizePerFrame;
uavDesc.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_NONE;
m_device->CreateUnorderedAccessView(
m_processedCommandBuffers[frame].Get(),
m_processedCommandBuffers[frame].Get(),
&uavDesc,
processedCommandsHandle);
processedCommandsHandle.Offset(CbvSrvUavDescriptorCountPerFrame, m_cbvSrvUavDescriptorSize);
}
Zeichnen des Rahmens
Wenn es an der Zeit ist, den Frame zu zeichnen, wenn wir uns in dem Modus befinden, in dem der Compute-Shader aufgerufen wird und indirekte Befehle von der GPU verarbeitet werden, werden wir zuerst dispatchen , um unseren Befehlspuffer für ExecuteIndirect aufzufüllen. Die folgenden Codeausschnitte werden der PopulateCommandLists-Methode hinzugefügt.
// Record the compute commands that will cull triangles and prevent them from being processed by the vertex shader.
if (m_enableCulling)
{
UINT frameDescriptorOffset = m_frameIndex * CbvSrvUavDescriptorCountPerFrame;
D3D12_GPU_DESCRIPTOR_HANDLE cbvSrvUavHandle = m_cbvSrvUavHeap->GetGPUDescriptorHandleForHeapStart();
m_computeCommandList->SetComputeRootSignature(m_computeRootSignature.Get());
ID3D12DescriptorHeap* ppHeaps[] = { m_cbvSrvUavHeap.Get() };
m_computeCommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
m_computeCommandList->SetComputeRootDescriptorTable(
SrvUavTable,
CD3DX12_GPU_DESCRIPTOR_HANDLE(cbvSrvUavHandle, CbvSrvOffset + frameDescriptorOffset, m_cbvSrvUavDescriptorSize));
m_computeCommandList->SetComputeRoot32BitConstants(RootConstants, 4, reinterpret_cast<void*>(&m_csRootConstants), 0);
// Reset the UAV counter for this frame.
m_computeCommandList->CopyBufferRegion(m_processedCommandBuffers[m_frameIndex].Get(), CommandBufferSizePerFrame, m_processedCommandBufferCounterReset.Get(), 0, sizeof(UINT));
D3D12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(m_processedCommandBuffers[m_frameIndex].Get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
m_computeCommandList->ResourceBarrier(1, &barrier);
m_computeCommandList->Dispatch(static_cast<UINT>(ceil(TriangleCount / float(ComputeThreadBlockSize))), 1, 1);
}
ThrowIfFailed(m_computeCommandList->Close());
Anschließend führen wir die Befehle entweder im UAV (GPU-Culling aktiviert) oder im vollständigen Befehlspuffer (GPU-Culling deaktiviert) aus.
// Record the rendering commands.
{
// Set necessary state.
m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
ID3D12DescriptorHeap* ppHeaps[] = { m_cbvSrvUavHeap.Get() };
m_commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
m_commandList->RSSetViewports(1, &m_viewport);
m_commandList->RSSetScissorRects(1, m_enableCulling ? &m_cullingScissorRect : &m_scissorRect);
// Indicate that the command buffer will be used for indirect drawing
// and that the back buffer will be used as a render target.
D3D12_RESOURCE_BARRIER barriers[2] = {
CD3DX12_RESOURCE_BARRIER::Transition(
m_enableCulling ? m_processedCommandBuffers[m_frameIndex].Get() : m_commandBuffer.Get(),
m_enableCulling ? D3D12_RESOURCE_STATE_UNORDERED_ACCESS : D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE,
D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT),
CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET)
};
m_commandList->ResourceBarrier(_countof(barriers), barriers);
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart(), m_frameIndex, m_rtvDescriptorSize);
CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(m_dsvHeap->GetCPUDescriptorHandleForHeapStart());
m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);
// Record commands.
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
m_commandList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
m_commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
m_commandList->IASetVertexBuffers(0, 1, &m_vertexBufferView);
if (m_enableCulling)
{
// Draw the triangles that have not been culled.
m_commandList->ExecuteIndirect(
m_commandSignature.Get(),
TriangleCount,
m_processedCommandBuffers[m_frameIndex].Get(),
0,
m_processedCommandBuffers[m_frameIndex].Get(),
CommandBufferSizePerFrame);
}
else
{
// Draw all of the triangles.
m_commandList->ExecuteIndirect(
m_commandSignature.Get(),
TriangleCount,
m_commandBuffer.Get(),
CommandBufferSizePerFrame * m_frameIndex,
nullptr,
0);
}
// Indicate that the command buffer may be used by the compute shader
// and that the back buffer will now be used to present.
barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT;
barriers[0].Transition.StateAfter = m_enableCulling ? D3D12_RESOURCE_STATE_COPY_DEST : D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
m_commandList->ResourceBarrier(_countof(barriers), barriers);
ThrowIfFailed(m_commandList->Close());
}
Wenn wir uns im GPU-Culling-Modus befinden, muss die Grafikbefehlswarteschlange warten, bis die Computearbeit abgeschlossen ist, bevor sie mit der Ausführung der indirekten Befehle beginnt. In der OnRender-Methode wird der folgende Codeausschnitt hinzugefügt.
// Execute the compute work.
if (m_enableCulling)
{
ID3D12CommandList* ppCommandLists[] = { m_computeCommandList.Get() };
m_computeCommandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
m_computeCommandQueue->Signal(m_computeFence.Get(), m_fenceValues[m_frameIndex]);
// Execute the rendering work only when the compute work is complete.
m_commandQueue->Wait(m_computeFence.Get(), m_fenceValues[m_frameIndex]);
}
// Execute the rendering work.
ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
Anrufverlauf | Parameter |
---|---|
ID3D12CommandList | |
ExecuteCommandLists | |
Signal | |
Wait | |
ID3D12CommandList | |
ExecuteCommandLists |
Ausführen des Beispiels
Das Beispiel mit primitiver GPU-Culling.
Das Beispiel ohne primitive GPU-Culling.
Zugehörige Themen