建立基本 Direct3D 12 元件
注意
請參閱適用於 DirectX 12 圖形的 DirectX-Graphics-Samples 存放庫,以示範如何建置適用於 Windows 10 的圖形密集型應用程式。
本主題描述建立基本 Direct3D 12 元件的呼叫流程。
簡單應用程式的程式代碼流程
D3D 12 程式的最外層迴圈遵循非常標準的圖形程式:
提示
Direct3D 12 的新功能附有註解。
初始化
初始化牽涉到先設定全域變數和類別,而初始化函式必須準備管線和資產。
- 初始化管線。
啟用偵錯層。
建立裝置。
建立命令佇列。
建立交換鏈。
建立呈現目標檢視 (RTV) 描述子堆疊。
建立框架資源(為每個框架建立渲染目標檢視)。
建立命令配置器。
注意
命令分配器會管理 命令清單和命令束的底層儲存空間。
- 初始化資產。
請參閱 類別 D3D12HelloTriangle、OnInit、LoadPipeline 和 LoadAssets。
更新
更新自上一個畫面以來應該更新的所有內容。
- 視需要修改常數、頂點、索引緩衝區和其他所有內容。
請參閱 OnUpdate。
渲染
繪製新的世界。
- 填入命令清單。
重設命令清單配置器。
注意
重複使用與命令配置器相關聯的記憶體。
重設命令清單。
設定圖形根簽章。
注意
設定要用於目前命令清單的圖形根簽章。
設定檢視區及剪刀矩形。
設定資源屏障,指出後台緩衝區要當做轉譯目標使用。
注意
資源屏障可用來管理資源轉換。
將命令記錄到命令清單中。
表示執行命令清單之後,將會使用後台緩衝區呈現。
注意
另一個設定資源屏障的呼叫。
關閉命令清單以進一步錄製。
- 執行命令清單。
- 呈現框架。
- 等候 GPU 完成。
注意
繼續更新並檢查柵欄。
請參閱 OnRender。
摧毀
完全關閉應用程式。
等候 GPU 完成。
注意
對圍欄進行最後檢查。
關閉事件控制代碼。
請參閱 OnDestroy。
簡單應用程式的程式代碼範例
下列會展開上述程式代碼流程,以包含基本範例所需的程序代碼。
類別 D3D12HelloTriangle
首先,使用 結構定義頭檔中的 類別,包括檢視區、剪刀矩形和頂點緩衝區:
#include "DXSample.h"
using namespace DirectX;
using namespace Microsoft::WRL;
class D3D12HelloTriangle : public DXSample
{
public:
D3D12HelloTriangle(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;
};
// Pipeline objects.
D3D12_VIEWPORT m_viewport;
D3D12_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<ID3D12PipelineState> m_pipelineState;
ComPtr<ID3D12GraphicsCommandList> m_commandList;
UINT m_rtvDescriptorSize;
// App resources.
ComPtr<ID3D12Resource> m_vertexBuffer;
D3D12_VERTEX_BUFFER_VIEW m_vertexBufferView;
// Synchronization objects.
UINT m_frameIndex;
HANDLE m_fenceEvent;
ComPtr<ID3D12Fence> m_fence;
UINT64 m_fenceValue;
void LoadPipeline();
void LoadAssets();
void PopulateCommandList();
void WaitForPreviousFrame();
};
OnInit()
在專案主要原始程式檔中,開始初始化物件:
void D3D12HelloTriangle::OnInit()
{
LoadPipeline();
LoadAssets();
}
LoadPipeline()
下列程式代碼會建立圖形管線的基本結構。 建立裝置和交換鏈結的程式與 Direct3D 11 非常類似。
- 啟用偵錯層,並呼叫下列專案:
D3D12GetDebugInterface
ID3D12Debug::EnableDebugLayer
- 建立裝置:
CreateDXGIFactory1
D3D12CreateDevice
- 填寫命令佇列描述,然後建立命令佇列:
D3D12_COMMAND_QUEUE_DESC
ID3D12Device::CreateCommandQueue
- 填寫交換鏈描述,然後建立交換鏈結:
DXGI_SWAP_CHAIN_DESC
IDXGIFactory::CreateSwapChain
IDXGISwapChain3::GetCurrentBackBufferIndex
- 填寫堆積描述。 然後建立描述元堆積:
D3D12_DESCRIPTOR_HEAP_DESC
ID3D12Device::CreateDescriptorHeap
ID3D12Device::GetDescriptorHandleIncrementSize
- 建立渲染目標視圖:
CD3DX12_CPU_DESCRIPTOR_HANDLE
GetCPUDescriptorHandleForHeapStart
IDXGISwapChain::GetBuffer
ID3D12Device::CreateRenderTargetView
- 建立命令設定器:ID3D12Device::CreateCommandAllocator。
在後續步驟中,命令清單會從命令配置器取得,並提交至命令佇列。
載入渲染管線的相依性(請注意,建立軟體 WARP 裝置是完全可選的)。
void D3D12HelloTriangle::LoadPipeline()
{
#if defined(_DEBUG)
// Enable the D3D12 debug layer.
{
ComPtr<ID3D12Debug> debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
debugController->EnableDebugLayer();
}
}
#endif
ComPtr<IDXGIFactory4> factory;
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&factory)));
if (m_useWarpDevice)
{
ComPtr<IDXGIAdapter> warpAdapter;
ThrowIfFailed(factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)));
ThrowIfFailed(D3D12CreateDevice(
warpAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&m_device)
));
}
else
{
ComPtr<IDXGIAdapter1> hardwareAdapter;
GetHardwareAdapter(factory.Get(), &hardwareAdapter);
ThrowIfFailed(D3D12CreateDevice(
hardwareAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&m_device)
));
}
// Describe and create the command queue.
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ThrowIfFailed(m_device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_commandQueue)));
// Describe and create the swap chain.
DXGI_SWAP_CHAIN_DESC swapChainDesc = {};
swapChainDesc.BufferCount = FrameCount;
swapChainDesc.BufferDesc.Width = m_width;
swapChainDesc.BufferDesc.Height = m_height;
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapChainDesc.OutputWindow = Win32Application::GetHwnd();
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.Windowed = TRUE;
ComPtr<IDXGISwapChain> swapChain;
ThrowIfFailed(factory->CreateSwapChain(
m_commandQueue.Get(), // Swap chain needs the queue so that it can force a flush on it.
&swapChainDesc,
&swapChain
));
ThrowIfFailed(swapChain.As(&m_swapChain));
// This sample does not support fullscreen transitions.
ThrowIfFailed(factory->MakeWindowAssociation(Win32Application::GetHwnd(), DXGI_MWA_NO_ALT_ENTER));
m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();
// 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);
}
// Create frame resources.
{
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart());
// Create a RTV for each frame.
for (UINT n = 0; n < FrameCount; n++)
{
ThrowIfFailed(m_swapChain->GetBuffer(n, IID_PPV_ARGS(&m_renderTargets[n])));
m_device->CreateRenderTargetView(m_renderTargets[n].Get(), nullptr, rtvHandle);
rtvHandle.Offset(1, m_rtvDescriptorSize);
}
}
ThrowIfFailed(m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&m_commandAllocator)));
}
LoadAssets()
載入和準備資產是一個冗長的過程。 其中許多階段類似於 D3D 11,但有些階段是 D3D 12 的新功能。
在 Direct3D 12 中,必要的管線狀態會透過 管線狀態物件 (PSO) 附加至命令清單。 此範例示範如何建立 PSO。 您可以將 PSO 儲存為成員變數,並視需要重複使用它多次。
描述堆疊定義檢視以及如何存取資源(例如,渲染目標檢視)。
使用命令清單配置器和 PSO,您可以建立實際的命令清單,以便稍後執行。
下列 API 和進程會連續呼叫。
- 使用可用的協助程序結構建立空的根簽章:
CD3DX12_ROOT_SIGNATURE_DESC
D3D12SerializeRootSignature
ID3D12Device::CreateRootSignature
- 載入和編譯著色器:D3DCompileFromFile。
- 建立頂點輸入設定:D3D12_INPUT_ELEMENT_DESC。
- 使用可用的協助程式結構填寫管線狀態描述,然後建立圖形管線狀態:
D3D12_GRAPHICS_PIPELINE_STATE_DESC
CD3DX12_RASTERIZER_DESC
CD3DX12_BLEND_DESC
ID3D12Device::CreateGraphicsPipelineState
- 建立命令清單,然後關閉:
ID3D12Device::CreateCommandList
ID3D12GraphicsCommandList::Close
- 建立頂點緩衝區:ID3D12Device::CreateCommittedResource。
- 將頂點資料複製到頂點緩衝區:
ID3D12Resource::Map
ID3D12Resource::Unmap
- 初始化頂點緩衝區檢視:GetGPUVirtualAddress。
- 建立與初始化柵欄:ID3D12Device::CreateFence。
- 建立事件句柄,以便與畫面同步處理搭配使用。
- 等候 GPU 完成。
void D3D12HelloTriangle::LoadAssets()
{
// Create an empty root signature.
{
CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Init(0, nullptr, 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 the pipeline state, which includes compiling and loading shaders.
{
ComPtr<ID3DBlob> vertexShader;
ComPtr<ID3DBlob> pixelShader;
#if defined(_DEBUG)
// Enable better shader debugging with the graphics debugging tools.
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
ThrowIfFailed(D3DCompileFromFile(GetAssetFullPath(L"shaders.hlsl").c_str(), nullptr, nullptr, "VSMain", "vs_5_0", compileFlags, 0, &vertexShader, nullptr));
ThrowIfFailed(D3DCompileFromFile(GetAssetFullPath(L"shaders.hlsl").c_str(), nullptr, nullptr, "PSMain", "ps_5_0", compileFlags, 0, &pixelShader, nullptr));
// Define the vertex input layout.
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
// 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 = { reinterpret_cast<UINT8*>(vertexShader->GetBufferPointer()), vertexShader->GetBufferSize() };
psoDesc.PS = { reinterpret_cast<UINT8*>(pixelShader->GetBufferPointer()), pixelShader->GetBufferSize() };
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.
ThrowIfFailed(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator.Get(), m_pipelineState.Get(), IID_PPV_ARGS(&m_commandList)));
// Command lists are created in the recording state, but there is nothing
// to record yet. The main loop expects it to be closed, so close it now.
ThrowIfFailed(m_commandList->Close());
// Create the vertex buffer.
{
// Define the geometry for a triangle.
Vertex triangleVertices[] =
{
{ { 0.0f, 0.25f * m_aspectRatio, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
{ { 0.25f, -0.25f * m_aspectRatio, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
{ { -0.25f, -0.25f * m_aspectRatio, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }
};
const UINT vertexBufferSize = sizeof(triangleVertices);
// Note: using upload heaps to transfer static data like vert buffers is not
// recommended. Every time the GPU needs it, the upload heap will be marshalled
// over. Please read up on Default Heap usage. An upload heap is used here for
// code simplicity and because there are very few verts to actually transfer.
CD3DX12_HEAP_PROPERTIES heapProps(D3D12_HEAP_TYPE_UPLOAD);
auto desc = CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize);
ThrowIfFailed(m_device->CreateCommittedResource(
&heapProps,
D3D12_HEAP_FLAG_NONE,
&desc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&m_vertexBuffer)));
// Copy the triangle data to the vertex buffer.
UINT8* pVertexDataBegin;
CD3DX12_RANGE readRange(0, 0); // We do not intend to read from this resource on the CPU.
ThrowIfFailed(m_vertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&pVertexDataBegin)));
memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices));
m_vertexBuffer->Unmap(0, nullptr);
// Initialize the vertex buffer view.
m_vertexBufferView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress();
m_vertexBufferView.StrideInBytes = sizeof(Vertex);
m_vertexBufferView.SizeInBytes = vertexBufferSize;
}
// Create synchronization objects and wait until assets have been uploaded to the GPU.
{
ThrowIfFailed(m_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence)));
m_fenceValue = 1;
// Create an event handle to use for frame synchronization.
m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (m_fenceEvent == nullptr)
{
ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
}
// Wait for the command list to execute; we are reusing the same command
// list in our main loop but for now, we just want to wait for setup to
// complete before continuing.
WaitForPreviousFrame();
}
}
OnUpdate()
對於一個簡單的例子,沒有任何更新。
void D3D12HelloTriangle::OnUpdate()
{
}
OnRender()
在設定期間,成員變數 m_commandList 用來記錄和執行所有設定命令。 您現在可以在主要轉譯迴圈中重複使用該成員。
渲染需要呼叫來填入命令清單,然後就可以執行命令清單,並在交換鏈中呈現下一個緩衝區。
- 填充命令清單。
- 執行命令清單:ID3D12CommandQueue::ExecuteCommandLists。
- IDXGISwapChain1::Present1 該畫面。
- 等候 GPU 完成。
void D3D12HelloTriangle::OnRender()
{
// 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(1, 0));
WaitForPreviousFrame();
}
PopulateCommandList()
您必須先重設命令清單配置器和命令清單本身,才能重複使用它們。 在更進階的案例中,每數個幀重設配置器可能很合理。 記憶體與執行命令清單之後無法立即釋放的配置器相關聯。 此範例示範如何在每一幀之後重設配置器。
現在,重複使用目前框架的命令清單。 將檢視區重新附加至命令清單(每當重設命令清單時,以及執行命令清單之前,都必須這麼做),表示資源將用來做為轉譯目標、記錄命令,然後指出當命令清單執行完成時,轉譯目標將會用來呈現。
命令列的填充會依次呼叫下列方法和過程:
- 重設命令設定器和命令清單:
ID3D12CommandAllocator::Reset
ID3D12GraphicsCommandList::Reset
- 設定根簽章、檢視區及剪刀矩形:
ID3D12GraphicsCommandList::SetGraphicsRootSignature
ID3D12GraphicsCommandList::RSSetViewports
ID3D12GraphicsCommandList::RSSetScissorRects
- 指出後台緩衝區要當做轉譯目標使用:
ID3D12GraphicsCommandList::ResourceBarrier
ID3D12DescriptorHeap:GetCPUDescriptorHandleForHeapStart
ID3D12GraphicsCommandList::OMSetRenderTargets
- 記錄命令:
ID3D12GraphicsCommandList::ClearRenderTargetView
ID3D12GraphicsCommandList::IASetPrimitiveTopology
ID3D12GraphicsCommandList::IASetVertexBuffers
ID3D12GraphicsCommandList::DrawInstanced
- 指出後端緩衝區現在將用來呈現:ID3D12GraphicsCommandList::ResourceBarrier。
- 關閉命令清單: ID3D12GraphicsCommandList::Close。
void D3D12HelloTriangle::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());
m_commandList->RSSetViewports(1, &m_viewport);
m_commandList->RSSetScissorRects(1, &m_scissorRect);
// Indicate that the back buffer will be used as a render target.
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);
m_commandList->ResourceBarrier(1, &barrier);
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.
barrier = CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);
m_commandList->ResourceBarrier(1, &barrier);
ThrowIfFailed(m_commandList->Close());
}
WaitForPreviousFrame()
以下程式碼顯示了過於簡化的界限使用。
注意
等待框架完成對大多數應用程式來說太沒有效率。
系統會依序呼叫下列 API 和流程:
- ID3D12CommandQueue::Signal
- ID3D12Fence::GetCompletedValue
- ID3D12Fence::SetEventOnCompletion
- 等候事件。
- 更新框架索引:IDXGISwapChain3::GetCurrentBackBufferIndex。
void D3D12HelloTriangle::WaitForPreviousFrame()
{
// WAITING FOR THE FRAME TO COMPLETE BEFORE CONTINUING IS NOT BEST PRACTICE.
// This is code implemented as such for simplicity. More advanced samples
// illustrate how to use fences for efficient resource usage.
// Signal and increment the fence value.
const UINT64 fence = m_fenceValue;
ThrowIfFailed(m_commandQueue->Signal(m_fence.Get(), fence));
m_fenceValue++;
// Wait until the previous frame is finished.
if (m_fence->GetCompletedValue() < fence)
{
ThrowIfFailed(m_fence->SetEventOnCompletion(fence, m_fenceEvent));
WaitForSingleObject(m_fenceEvent, INFINITE);
}
m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();
}
OnDestroy()
完全關閉應用程式。
- 等候 GPU 完成。
- 結束活動。
void D3D12HelloTriangle::OnDestroy()
{
// Wait for the GPU to be done with all resources.
WaitForPreviousFrame();
CloseHandle(m_fenceEvent);
}
相關主題