feat: implement Fence synchronization for CPU-GPU frame sync

D3D12 Core:
- Add ID3D12Fence1 and fence_value to d3d12_command
- Add fence_event for CPU waiting
- Implement wait() in command_frame for frame sync
- Implement flush() to wait all frames complete
- Add fence_value tracking per frame
- Signal fence at end_frame with incremented value

TestRenderer:
- Call graphics::render() in run()

Documentation:
- Add changelog for Fence sync implementation
- Update D3D12 Wiki with Fence sync section
This commit is contained in:
SpecialX
2026-03-27 18:56:03 +08:00
parent 7da17ccadd
commit f1584ec3c6
5 changed files with 287 additions and 16 deletions

View File

@@ -72,6 +72,9 @@ namespace {
// - 这是一种防御性编程,确保对象始终处于有效状态
class d3d12_command
{
public:
d3d12_command() = default;
DISABLE_COPY_AND_MOVE(d3d12_command)
explicit d3d12_command(ID3D12Device8 *const device, D3D12_COMMAND_LIST_TYPE type)
{
HRESULT hr{ S_OK };
@@ -110,25 +113,36 @@ class d3d12_command
type == D3D12_COMMAND_LIST_TYPE_COMPUTE ?
L"Compute Command List" : L" Command List");
DXCall(hr = device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&_fence)));
if(FAILED(hr)) goto _error;
NAME_D3D12_OBJECT(_fence, L"D3D12 Fence");
_fence_event = CreateEventEx(nullptr, nullptr, 0, EVENT_ALL_ACCESS);
assert(_fence_event);
return;
_error:
release();
}
void release()
{
}
~d3d12_command()
{
assert(!_cmd_queue && !_cmd_list && !_fence);
}
// 等待当前帧被标记为完成信号,并重置命令分配器和命令列表
void begin_frame()
{
command_frame& frame{_cmd_frames[_frame_index]};
frame.wait();
frame.wait(_fence_event, _fence);
// 重置命令分配器将释放之前帧分配的命令内存,使其可重新用于录制新帧的命令
// 重置命令列表将命令列表重置为可录制状态,准备录制录制命令
DXCall(frame.cmd_allocator->Reset());
DXCall(_cmd_list->Reset(frame.cmd_allocator, nullptr));
}
// 使用新的围栏值来标记这个围栏
void end_frame()
{
//在提交命令列表前,先关闭命令列表,确保命令列表进入可提交状态
@@ -137,18 +151,66 @@ class d3d12_command
// 虽然目前只有单个命令列表且为单线程工作模式,但仍采用数组方式以保持代码的扩展性
ID3D12CommandList *const cmd_lists[]{_cmd_list};
_cmd_queue->ExecuteCommandLists(_countof(cmd_lists), &cmd_lists[0]);
u64& fence_value{_fence_value};
++fence_value;
command_frame& frame{_cmd_frames[_frame_index]};
frame.fence_value = fence_value;
_cmd_queue->Signal(_fence, fence_value);
_frame_index = (_frame_index + 1) % frame_buffer_count;
}
/**
* @brief 等待所有帧的命令列表执行完成
* @details 确保所有帧的命令列表执行完成,避免资源冲突
*/
void flush()
{
for(u32 i{ 0 }; i < frame_buffer_count; ++i)
{
_cmd_frames[i].wait(_fence_event, _fence);
}
_frame_index = 0;
}
void release()
{
flush();
core::release(_fence);
_fence_value = 0;
CloseHandle(_fence_event);
_fence_event = nullptr;
core::release(_cmd_queue);
core::release(_cmd_list);
for(u32 i{ 0 }; i < frame_buffer_count; ++i)
{
_cmd_frames[i].release();
}
}
constexpr ID3D12CommandQueue *const command_queue() const {return _cmd_queue;}
constexpr ID3D12GraphicsCommandList6 *const command_list() const {return _cmd_list;}
constexpr u32 frame_index() const {return _frame_index;}
private:
struct command_frame
{
ID3D12CommandAllocator* cmd_allocator{ nullptr };
ID3D12CommandAllocator* cmd_allocator{ nullptr };
u64 fence_value{ 0 };
void wait()
void wait(HANDLE fence_event, ID3D12Fence1* fence)
{
assert(fence && fence_event);
// 如果当前的Fence值小于目标值说明GPU还没有执行完成当前的命令列表
if(fence->GetCompletedValue() < fence_value)
{
// 我们需要等待GPU执行当前的命令列表设置事件并等待事件触发
DXCall(fence->SetEventOnCompletion(fence_value, fence_event));
WaitForSingleObject(fence_event, INFINITE);
}
}
void release()
@@ -157,23 +219,33 @@ private:
}
};
ID3D12CommandQueue* _cmd_queue{ nullptr };
ID3D12GraphicsCommandList6* _cmd_list{ nullptr };
command_frame _cmd_frames[frame_buffer_count]{};
u32 _frame_index{ 0 };
ID3D12CommandQueue* _cmd_queue{ nullptr };
ID3D12GraphicsCommandList6* _cmd_list{ nullptr };
ID3D12Fence1* _fence{ nullptr };
// 对于围栏值来说他是64位无符号整型,有2^64-1个值,即便每秒1000帧,也需要5.8亿年才能回绕,所以不需要担心一直递增导致溢出的问题
u64 _fence_value{ 0 };
command_frame _cmd_frames[frame_buffer_count]{};
HANDLE _fence_event{ nullptr };
u32 _frame_index{ 0 };
};
/**
* @brief 主 Direct3D 12 设备指针
* @details 指向 Direct3D 12 设备的智能指针,用于创建渲染管线、管理资源与 GPU 命令
*/
ID3D12Device8* main_device{ nullptr };
ID3D12Device8* main_device{ nullptr };
/**
* @brief DXGI 工厂指针
* @details 指向 DXGI 工厂的智能指针,用于创建 Direct3D 12 设备
*/
IDXGIFactory7* dxgi_factory{ nullptr };
IDXGIFactory7* dxgi_factory{ nullptr };
/**
* @brief 命令管理类实例
* @details 用于管理 Direct3D 12 命令队列和命令列表,提供类型安全的 GPU命令提交机制
*/
d3d12_command gfx_command;
// 最小支持的 Direct3D 特本级别
constexpr D3D_FEATURE_LEVEL minumum_feature_level{ D3D_FEATURE_LEVEL_11_0 };
@@ -282,6 +354,15 @@ initialize()
// 为 Direct3D 12 设备设置名称
NAME_D3D12_OBJECT(main_device, L"Main Device");
// 使用 placement new 在已分配的内存上构造对象
// new (&gfx_command) 表示在 gfx_command 的地址处调用构造函数
// 这种用法允许我们在不分配新内存的情况下,在指定内存位置构造对象
// 常用于需要在特定内存地址构造对象,或重新初始化已存在的对象
// 这里 gfx_command 是一个类成员变量,我们直接在其内存位置上构造 d3d12_command 对象
// 避免了额外的内存分配同时可以传递构造参数main_device 和命令队列类型)
new (&gfx_command) d3d12_command(main_device, D3D12_COMMAND_LIST_TYPE_DIRECT);
if(!gfx_command.command_queue()) return failed_init();
#ifdef _DEBUG
{
ComPtr<ID3D12InfoQueue> info_queue;
@@ -299,6 +380,7 @@ initialize()
void
shutdown()
{
gfx_command.release();
release(dxgi_factory);
#ifdef _DEBUG
@@ -329,8 +411,14 @@ shutdown()
void
render()
{
begin_frame();
// 等待GPU完成命令列表,并重置命令分配器和命令列表
gfx_command.begin_frame();
ID3D12GraphicsCommandList6* cmd_list{ gfx_command.command_list() };
end_frame();
// 记录命令
//
// 完成命令记录,立即提交命令列表到命令队列执行
// 为下一帧标记并增加围栏值
gfx_command.end_frame();
}
}// namespace XEngine::graphics::d3d12::core