DirectX 12多交换链场景下VSync的正确处理方案问询
DirectX 12多窗口VSync优化方案
问题背景
开发基于DirectX 12的多窗口合作游戏,每个玩家对应独立窗口(部署在多显示器上防止窥屏作弊),但调用交换链Present时遇到VSync相关的严重掉帧问题。当前代码通过关闭VSync(syncInterval=0)规避问题,但需要实现正确的VSync同步逻辑,同时避免帧率暴跌。
核心问题分析
开启VSync(syncInterval=1)时,循环调用每个交换链的Present会触发多次阻塞等待垂直同步:每个Present(syncInterval=1)都会阻塞到当前显示器的下一次垂直刷新,4个窗口就会触发4次等待,最终帧率被拉低到显示器刷新率的1/4左右。此外,多个交换链共用同一命令队列时,渲染与Present的同步逻辑不合理也会加剧性能损耗。
解决方案
- 统一垂直同步等待,避免重复阻塞:只在第一个交换链
Present时等待垂直同步,后续交换链直接提交而不重复等待。DXGI特性支持:当多个交换链共享同一命令队列且渲染工作已同步完成时,第一个Present(syncInterval=1)会等待到垂直刷新,后续Present(syncInterval=0)可直接完成,不会额外阻塞。 - 绑定渲染与垂直同步周期:使用D3D12的Fence机制,将所有玩家的渲染命令执行与垂直同步周期绑定,确保所有渲染工作在垂直刷新前完成,避免Present时出现无意义的等待延迟。
- 适配多显示器刷新率差异:若窗口分布在不同刷新率的显示器上,需为每个显示器单独处理VSync——针对每个显示器创建独立的同步Fence,等待对应显示器的垂直刷新信号后,再执行该显示器上窗口的
Present。
代码修改示例
调整Present逻辑(基础优化)
while(bRunning) { // ... 执行所有玩家的渲染命令 sRENDERER.gfxCommandQueue->ExecuteCommandLists(dwNumPlayers, sRENDERER.gfxCommandList[dwFrame]); SignalCurrentFrame(); uint32_t syncInterval = 1; uint32_t presentFlags = 0; // 第一个窗口执行带VSync的Present,触发一次垂直同步等待 if (FAILED(swapChain[0]->Present(syncInterval, presentFlags))) { logError("Error presenting swap chain buffer!\n"); break; } // 后续窗口跳过VSync等待,直接提交 syncInterval = 0; for(uint32_t dwPlayer = 1; dwPlayer < dwNumPlayers; ++dwPlayer) { if (FAILED(swapChain[dwPlayer]->Present(syncInterval, presentFlags))) { logError("Error presenting swap chain buffer!\n"); break; } } }
结合Fence同步渲染周期(精准控制)
// 初始化时创建Fence和事件 ID3D12Fence* pFence = nullptr; HANDLE hFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); UINT64 fenceValue = 0; // 初始化交换链时添加帧延迟控制 swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING | DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; // ... 创建交换链后设置最大帧延迟 swapChain[dwPlayer]->SetMaximumFrameLatency(1); while(bRunning) { // ... 执行所有玩家的渲染命令 sRENDERER.gfxCommandQueue->ExecuteCommandLists(dwNumPlayers, sRENDERER.gfxCommandList[dwFrame]); // 标记命令队列完成点并等待执行完成 UINT64 currentFenceValue = ++fenceValue; sRENDERER.gfxCommandQueue->Signal(pFence, currentFenceValue); if(pFence->GetCompletedValue() < currentFenceValue) { pFence->SetEventOnCompletion(currentFenceValue, hFenceEvent); WaitForSingleObject(hFenceEvent, INFINITE); } // 批量执行Present,无需重复等待VSync uint32_t syncInterval = 0; uint32_t presentFlags = 0; for(uint32_t dwPlayer = 0; dwPlayer < dwNumPlayers; ++dwPlayer) { if (FAILED(swapChain[dwPlayer]->Present(syncInterval, presentFlags))) { logError("Error presenting swap chain buffer!\n"); break; } } }
关键注意事项
- 若窗口分布在不同显示器,必须为每个显示器单独处理VSync,避免跨显示器同步导致的帧率异常。
- 移除不必要的窗口样式:如
CS_HREDRAW | CS_VREDRAW会触发额外的窗口重绘开销,若游戏自身处理渲染逻辑可直接移除。 - 确保所有交换链共享同一个DXGI Adapter,避免跨Adapter同步带来的性能损耗。
内容的提问来源于stack exchange,提问作者yosmo78
相关产品推荐
相关产品推荐

