N-Körper-Schwerkraftsimulation mit mehreren Modulen

Das Beispiel D3D12nBody Computing veranschaulicht, wie Computearbeit asynchron ausgeführt wird. Im Beispiel werden eine Reihe von Threads mit jeweils einer Computebefehlswarteschlange eingerichtet und Computearbeit auf der GPU geplant, die eine n-body-Schwerkraftsimulation ausführt. Jeder Thread arbeitet mit zwei Puffern mit Positions- und Geschwindigkeitsdaten. Bei jeder Iteration liest der Compute-Shader die aktuellen Positions- und Geschwindigkeitsdaten aus einem Puffer und schreibt die nächste Iteration in den anderen Puffer. Nach Abschluss der Iteration tauscht der Compute-Shader aus, welcher Puffer der SRV zum Lesen von Positions-/Geschwindigkeitsdaten ist und welcher UAV zum Schreiben von Positions-/Geschwindigkeitsaktualisierungen durch Ändern des Ressourcenzustands für jeden Puffer ist.

Erstellen der Stammsignaturen

Zunächst erstellen wir sowohl eine Grafik als auch eine Computestammsignatur in der LoadAssets-Methode. Beide Stammsignaturen verfügen über eine Stammkonst constant buffer view (CBV) und eine Shader resource view (SRV)-Deskriptortabelle. Die Computestammsignatur verfügt auch über eine Unordered Access View (UAV)-Deskriptortabelle.

 // Create the root signatures.
       {
              CD3DX12_DESCRIPTOR_RANGE ranges[2];
              ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
              ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0);

              CD3DX12_ROOT_PARAMETER rootParameters[RootParametersCount];
              rootParameters[RootParameterCB].InitAsConstantBufferView(0, 0, D3D12_SHADER_VISIBILITY_ALL);
              rootParameters[RootParameterSRV].InitAsDescriptorTable(1, &ranges[0], D3D12_SHADER_VISIBILITY_VERTEX);
              rootParameters[RootParameterUAV].InitAsDescriptorTable(1, &ranges[1], D3D12_SHADER_VISIBILITY_ALL);

              // The rendering pipeline does not need the UAV parameter.
              CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
              rootSignatureDesc.Init(_countof(rootParameters) - 1, 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. Must change visibility for the SRV.
              rootParameters[RootParameterSRV].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;

              CD3DX12_ROOT_SIGNATURE_DESC computeRootSignatureDesc(_countof(rootParameters), rootParameters, 0, nullptr);
              ThrowIfFailed(D3D12SerializeRootSignature(&computeRootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));

              ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_computeRootSignature)));
       }
Anruffluss Parameter
_CD3DX12-DESKRIPTORBEREICH _ _D3D12-DESKRIPTORBEREICHSTYP _ _
CD3DX12 _ ROOT _ PARAMETER D3D12 _ _ SHADER-SICHTBARKEIT
CD3DX12 _ ROOT _ SIGNATURE _ DESC D3D12 _ ROOT _ SIGNATURE _ FLAGS
ID3DBlob
D3D12SerializeRootSignature _ _ D3D-STAMMSIGNATURVERSION _
CreateRootSignature
CD3DX12 _ ROOT _ SIGNATURE _ DESC
D3D12SerializeRootSignature _ _ D3D-STAMMSIGNATURVERSION _
CreateRootSignature

Erstellen der SRV- und UAV-Puffer

Die SRV- und UAV-Puffer bestehen aus einem Array von Positions- und Geschwindigkeitsdaten.

 // Position and velocity data for the particles in the system.
       // Two buffers full of Particle data are utilized in this sample.
       // The compute thread alternates writing to each of them.
       // The render thread renders using the buffer that is not currently
       // in use by the compute shader.
       struct Particle
       {
              XMFLOAT4 position;
              XMFLOAT4 velocity;
       };
Anruffluss Parameter
XMFLOAT4

Erstellen der CBV- und Scheitelpunktpuffer

Für die Grafikpipeline ist die CBV eine Struktur, die zwei Matrizen enthält, die vom Geometrie-Shader verwendet werden. Der Geometrie-Shader nimmt die Position jedes Partikels im System an und generiert ein Quader, um es mithilfe dieser Matrizen darstellen zu können.

 struct ConstantBufferGS
       {
              XMMATRIX worldViewProjection;
              XMMATRIX inverseView;

              // Constant buffers are 256-byte aligned in GPU memory. Padding is added
              // for convenience when computing the struct's size.
              float padding[32];
       };
Anruffluss Parameter
XMMATRIX

Daher enthält der vom Vertex-Shader verwendete Scheitelpunktpuffer keine Positionsdaten.

 // "Vertex" definition for particles. Triangle vertices are generated 
       // by the geometry shader. Color data will be assigned to those 
       // vertices via this struct.
       struct ParticleVertex
       {
              XMFLOAT4 color;
       };
Anruffluss Parameter
XMFLOAT4

Für die Computepipeline ist die CBV eine Struktur, die einige Konstanten enthält, die von der n-body-Schwerkraftsimulation im Compute-Shader verwendet werden.

 struct ConstantBufferCS
       {
              UINT param[4];
              float paramf[4];
       };

Synchronisieren der Rendering- und Computethreads

Nachdem alle Puffer initialisiert wurden, beginnen die Rendering- und Computearbeit. Der Computethread ändert den Zustand der beiden Positions-/Geschwindigkeitspuffer zwischen SRV und UAV, während er die Simulation durch iteriert, und der Renderingthread muss sicherstellen, dass er die Arbeit an der Grafikpipeline geplant, die auf dem SRV ausgeführt wird. Fences werden verwendet, um den Zugriff auf die beiden Puffer zu synchronisieren.

Im Renderthread:

// Render the scene.
void D3D12nBodyGravity::OnRender()
{
       // Let the compute thread know that a new frame is being rendered.
       for (int n = 0; n < ThreadCount; n++)
       {
              InterlockedExchange(&m_renderContextFenceValues[n], m_renderContextFenceValue);
       }

       // Compute work must be completed before the frame can render or else the SRV 
       // will be in the wrong state.
       for (UINT n = 0; n < ThreadCount; n++)
       {
              UINT64 threadFenceValue = InterlockedGetValue(&m_threadFenceValues[n]);
              if (m_threadFences[n]->GetCompletedValue() < threadFenceValue)
              {
                     // Instruct the rendering command queue to wait for the current 
                     // compute work to complete.
                     ThrowIfFailed(m_commandQueue->Wait(m_threadFences[n].Get(), threadFenceValue));
              }
       }

       // Record all the commands we need to render the scene into the command list.
       PopulateCommandList();

       // Execute the command list.
       ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
       m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);

       // Present the frame.
       ThrowIfFailed(m_swapChain->Present(0, 0));

       MoveToNextFrame();
}
Anruffluss Parameter
InterlockedExchange
InterlockedGetValue
GetCompletedValue
Wait
ID3D12CommandList
ExecuteCommandLists
IDXGISwapChain1::Present1

Um die Stichprobe etwas zu vereinfachen, wartet der Computethread, bis die GPU jede Iteration abgeschlossen hat, bevor weitere Computearbeit geplant wird. In der Praxis möchten Anwendungen die Computewarteschlange wahrscheinlich voll halten, um die maximale Leistung der GPU zu erzielen.

Im Computethread:

DWORD D3D12nBodyGravity::AsyncComputeThreadProc(int threadIndex)
{
       ID3D12CommandQueue* pCommandQueue = m_computeCommandQueue[threadIndex].Get();
       ID3D12CommandAllocator* pCommandAllocator = m_computeAllocator[threadIndex].Get();
       ID3D12GraphicsCommandList* pCommandList = m_computeCommandList[threadIndex].Get();
       ID3D12Fence* pFence = m_threadFences[threadIndex].Get();

       while (0 == InterlockedGetValue(&m_terminating))
       {
              // Run the particle simulation.
              Simulate(threadIndex);

              // Close and execute the command list.
              ThrowIfFailed(pCommandList->Close());
              ID3D12CommandList* ppCommandLists[] = { pCommandList };

              pCommandQueue->ExecuteCommandLists(1, ppCommandLists);

              // Wait for the compute shader to complete the simulation.
              UINT64 threadFenceValue = InterlockedIncrement(&m_threadFenceValues[threadIndex]);
              ThrowIfFailed(pCommandQueue->Signal(pFence, threadFenceValue));
              ThrowIfFailed(pFence->SetEventOnCompletion(threadFenceValue, m_threadFenceEvents[threadIndex]));
              WaitForSingleObject(m_threadFenceEvents[threadIndex], INFINITE);

              // Wait for the render thread to be done with the SRV so that
              // the next frame in the simulation can run.
              UINT64 renderContextFenceValue = InterlockedGetValue(&m_renderContextFenceValues[threadIndex]);
              if (m_renderContextFence->GetCompletedValue() < renderContextFenceValue)
              {
                     ThrowIfFailed(pCommandQueue->Wait(m_renderContextFence.Get(), renderContextFenceValue));
                     InterlockedExchange(&m_renderContextFenceValues[threadIndex], 0);
              }

              // Swap the indices to the SRV and UAV.
              m_srvIndex[threadIndex] = 1 - m_srvIndex[threadIndex];

              // Prepare for the next frame.
              ThrowIfFailed(pCommandAllocator->Reset());
              ThrowIfFailed(pCommandList->Reset(pCommandAllocator, m_computeState.Get()));
       }

       return 0;
}
Anruffluss Parameter
ID3D12CommandQueue
ID3D12CommandAllocator
ID3D12GraphicsCommandList
ID3D12Fence
InterlockedGetValue
Schließen
ID3D12CommandList
ExecuteCommandLists
InterlockedIncrement
Signal
SetEventOnCompletion
Waitforsingleobject
InterlockedGetValue
GetCompletedValue
Wait
InterlockedExchange
ID3D12CommandAllocator::Reset
ID3D12GraphicsCommandList::Reset

Ausführen des Beispiels

Screenshot der endgültigen Simulation der Schwerkraft des n-Körpers

Exemplarische Vorgehensweisen zu D3D12-Code

Synchronisierung mit mehreren Modulen