DirectX12 CPU/GPU同步:微软与英伟达示例的差异及优劣解析
关于DirectX12 CPU/GPU同步机制的两种帧同步实现解析
在理解DirectX12的CPU/GPU同步机制时,针对帧缓冲同步的两种典型实现存在困惑,下面分别解析微软HelloFrameBuffering示例与英伟达示例的逻辑设计及优缺点。
微软HelloFrameBuffering实现
// Prepare to render the next frame. void D3D12HelloFrameBuffering::MoveToNextFrame() { // Schedule a Signal command in the queue. const UINT64 currentFenceValue = m_fenceValues[m_frameIndex]; ThrowIfFailed(m_commandQueue->Signal(m_fence.Get(), currentFenceValue)); // Update the frame index. m_frameIndex = m_swapChain->GetCurrentBackBufferIndex(); // If the next frame is not ready to be rendered yet, wait until it is ready. if (m_fence->GetCompletedValue() < m_fenceValues[m_frameIndex]) { ThrowIfFailed(m_fence->SetEventOnCompletion(m_fenceValues[m_frameIndex], m_fenceEvent)); WaitForSingleObjectEx(m_fenceEvent, INFINITE, FALSE); } // Set the fence value for the next frame. m_fenceValues[m_frameIndex] = currentFenceValue + 1; }
该实现的核心是复用全局单个Fence,为每个帧缓冲维护独立的Fence值:
- 第一步用当前帧索引对应的Fence值向命令队列发送
Signal,标记当前帧的GPU任务完成后需触发的Fence状态。 - 紧接着更新帧索引到下一个待使用的缓冲,此时等待的是下一个缓冲对应的Fence值——这是因为下一个缓冲可能还在被GPU占用,必须等它的上一轮GPU任务完成(Fence值被触发),CPU才能安全复用该缓冲的资源。
- 最后给下一个缓冲的Fence值递增,为下一次循环做准备。
这种设计的本质是:当前帧的Signal标记的是上一轮该缓冲的GPU任务结束,而等待的是下一个缓冲的上一轮任务结束,确保CPU不会在GPU仍在使用缓冲时覆盖数据。
英伟达示例实现
struct FrameContext { ComPtr<ID3D12CommandAllocator> m_allocator; ComPtr<ID3D12CommandAllocator> m_computeAllocator; ComPtr<ID3D12Fence> m_fence; uint64_t m_fenceValue = 0; }; void DeviceResources::MoveToNextFrame() { FrameContext* ctx = &m_frameContext[m_frameIndex]; DX::ThrowIfFailed(m_commandQueue->Signal(ctx->m_fence.Get(), ctx->m_fenceValue)); m_frameIndex = m_swapChain->GetCurrentBackBufferIndex(); if (ctx->m_fence->GetCompletedValue() < ctx->m_fenceValue) { DX::ThrowIfFailed(ctx->m_fence->SetEventOnCompletion(ctx->m_fenceValue, m_fenceEvent.Get())); WaitForSingleObjectEx(m_fenceEvent.Get(), INFINITE, false); } ctx->m_fenceValue++; }
该实现采用每个帧缓冲绑定独立Fence实例的设计,每个FrameContext包含专属的Fence和对应的Fence值:
- 第一步用当前帧上下文的Fence和Fence值发送
Signal,标记当前帧的GPU任务完成条件。 - 更新帧索引后,等待的是当前帧上下文的Fence值——也就是刚刚发送
Signal的那个Fence,确保当前帧的GPU任务完成后,CPU才能复用该上下文的资源(比如命令分配器)。 - 最后递增当前帧上下文的Fence值,为下一次使用该缓冲时做准备。
两种实现的优缺点对比
微软实现(全局单Fence+多Fence值)
- 优点:
- 资源占用低:仅需创建一个Fence实例,节省GPU/CPU资源,适合轻量级场景。
- 状态集中:所有帧的Fence状态统一存储在数组中,便于集中管理。
- 缺点:
- 理解成本高:帧索引更新与Fence值的对应关系较为绕,新手容易混淆。
- 扩展性弱:若需为不同类型队列(如图形、计算)做同步,单Fence易出现冲突,需额外区分逻辑。
英伟达实现(每个帧缓冲独立Fence)
- 优点:
- 逻辑清晰:每个帧的同步状态完全独立,
Signal与Wait的对应关系一目了然,新手易理解。 - 扩展性强:可为每个帧上下文的不同队列分配独立Fence,实现精细化同步不同类型GPU任务。
- 隔离性好:单个帧的同步问题不会影响其他帧,调试更便捷。
- 逻辑清晰:每个帧的同步状态完全独立,
- 缺点:
- 资源占用略高:每个帧缓冲都需创建独立Fence实例,帧缓冲数量较多时会增加少量资源开销,但现代硬件下可忽略不计。
内容的提问来源于stack exchange,提问作者ThIsJaCk
相关产品推荐
相关产品推荐

