Simulation de gravité n-corps multi-moteur

L’exemple D3D12nBodyGravity montre comment effectuer un travail de calcul de façon asynchrone. L’exemple lance un certain nombre de threads chacun avec une file d’attente de commandes de calcul et planifie le travail de calcul sur le GPU qui effectue une simulation de gravité à n corps. Chaque thread fonctionne sur deux mémoires tampons pleines de données de position et de vélocité. À chaque itération, le nuanceur de calcul lit les données de position et de vélocité actuelles d’une mémoire tampon et écrit l’itération suivante dans l’autre mémoire tampon. Une fois l’itération terminée, le nuanceur de calcul permute la mémoire tampon qui est le SRV pour la lecture des données de position/vélocité et qui est l’UAV pour écrire des mises à jour de position/vélocité en modifiant l’état de la ressource sur chaque mémoire tampon.

Créer les signatures racines

Nous commençons par créer à la fois un graphique et une signature racine de calcul, dans la méthode LoadAssets . Les deux signatures racines ont une vue de mémoire tampon constante racine (CBV) et une table de descripteur de vue de ressources de nuanceur (SRV). La signature racine de calcul a également une table de descripteur de vue d’accès non ordonné (UAV).

 // 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)));
       }
Flux des appels Paramètres
CD3DX12_DESCRIPTOR_RANGE D3D12_DESCRIPTOR_RANGE_TYPE
CD3DX12_ROOT_PARAMETER D3D12_SHADER_VISIBILITY
CD3DX12_ROOT_SIGNATURE_DESC D3D12_ROOT_SIGNATURE_FLAGS
ID3DBlob
D3D12SerializeRootSignature D3D_ROOT_SIGNATURE_VERSION
CreateRootSignature
CD3DX12_ROOT_SIGNATURE_DESC
D3D12SerializeRootSignature D3D_ROOT_SIGNATURE_VERSION
CreateRootSignature

 

Créer les mémoires tampons SRV et UAV

Les mémoires tampons SRV et UAV se composent d’un tableau de données de position et de vélocité.

 // 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;
       };
Flux des appels Paramètres
XMFLOAT4

 

Créer les mémoires tampons cbV et vertex

Pour le pipeline graphique, le CBV est un struct contenant deux matrices utilisées par le nuanceur geometry. Le nuanceur géométrique prend la position de chaque particule dans le système et génère un quad pour la représenter à l’aide de ces matrices.

 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];
       };
Flux des appels Paramètres
XMMATRIX

 

Par conséquent, la mémoire tampon de vertex utilisée par le nuanceur de vertex ne contient en fait aucune donnée de position.

 // "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;
       };
Flux des appels Paramètres
XMFLOAT4

 

Pour le pipeline de calcul, le CBV est un struct contenant certaines constantes utilisées par la simulation de gravité à n corps dans le nuanceur de calcul.

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

Synchroniser les threads de rendu et de calcul

Une fois les mémoires tampons initialisées, le travail de rendu et de calcul commence. Le thread de calcul modifie l’état des deux mémoires tampons de position/vélocité entre SRV et UAV au fur et à mesure qu’il itère sur la simulation, et le thread de rendu doit s’assurer qu’il planifie le travail sur le pipeline graphique qui fonctionne sur le SRV. Les clôtures sont utilisées pour synchroniser l’accès aux deux mémoires tampons.

Sur le thread render :

// 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();
}
Flux des appels Paramètres
InterlockedExchange
InterlockedGetValue
GetCompletedValue
Wait
ID3D12CommandList
ExecuteCommandLists
IDXGISwapChain1::Present1

 

Pour simplifier un peu l’exemple, le thread de calcul attend que le GPU termine chaque itération avant de planifier d’autres tâches de calcul. Dans la pratique, les applications souhaitent probablement conserver la file d’attente de calcul complète pour obtenir des performances maximales à partir du GPU.

Sur le thread de calcul :

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;
}
Flux des appels Paramètres
ID3D12CommandQueue
ID3D12CommandAllocator
ID3D12GraphicsCommandList
ID3D12Fence
InterlockedGetValue
Close
ID3D12CommandList
ExecuteCommandLists
InterlockedIncrement
Signal
SetEventOnCompletion
WaitForSingleObject
InterlockedGetValue
GetCompletedValue
Wait
InterlockedExchange
ID3D12CommandAllocator::Reset
ID3D12GraphicsCommandList::Reset

 

Exécution de l'exemple

capture d’écran de la simulation finale de la gravité de n corps

Walk-Throughs de code D3D12

Synchronisation multi-moteur