Files
DX12/Engine/Graphics/Direct3D12/D3D12Core.cpp
SpecialX b6c0211d6a feat(d3d12): 完善描述符堆延迟释放机制与FreeList栈式索引管理
- 添加完整的中文Doxygen注释文档
- 实现process_deferred_release()延迟释放处理
- 添加deferred_release模板函数和current_frame_index()
- 实现延迟释放队列和帧索引管理
- 详细说明FreeList栈式索引分配/释放算法
- 更新D3D12学习Wiki,添加延迟释放机制章节
2026-03-30 16:59:27 +08:00

572 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "D3D12Core.h"
#include "D3D12CommonHeader.h"
#include "D3D12Resources.h"
using namespace Microsoft::WRL;
namespace XEngine::graphics::d3d12::core {
namespace {
/**
* @brief D3D12命令管理类设计说明
* @details 本类采用RAII设计模式封装Direct3D 12的命令队列和命令列表提供类型安全的GPU命令提交机制
*
* ## 整体设计思路
*
* 1. **类型安全封装**: 通过模板或构造函数参数区分Direct/Compute/Copy命令类型
* 避免运行时类型错误,编译期即可确定队列类型
*
* 2. **资源生命周期管理**:
* - 构造函数负责创建命令队列和命令列表
* - release()方法统一释放资源,支持异常安全(构造函数失败时调用)
* - 析构函数自动调用release(),防止资源泄漏
*
* 3. **帧同步机制**: 内部command_frame结构体管理每帧的命令分配器
* 支持CPU-GPU帧同步避免命令分配器重置冲突
*
* 4. **命名调试支持**: 使用NAME_D3D12_OBJECT宏为D3D对象设置调试名称
* 便于GPU调试工具PIX/RenderDoc识别
*
* ## 核心优势
*
* - **异常安全**: 构造函数采用goto错误处理模式任何步骤失败都会自动清理已分配资源
* - **多队列支持**: 单一类支持D3D12三种命令队列类型Direct/Compute/Copy代码复用率高
* - **现代D3D12 API**: 使用ID3D12GraphicsCommandList6接口支持最新渲染特性如Mesh Shader
* - **零开销抽象**: 轻量级封装不引入额外运行时开销直接操作底层D3D12对象
* - **可扩展性**: 预留frame管理接口可轻松扩展为多缓冲帧循环、命令列表录制状态追踪等功能
*/
//
// ## 多帧渲染架构设计原理
//
// 现代GPU渲染采用"生产者-消费者"模型CPU作为命令生产者录制渲染命令
// GPU作为消费者异步执行。为避免CPU等待GPU完成需要引入帧缓冲机制。
//
// ### 为什么需要多帧缓冲?
//
// 1. **CPU-GPU并行性**: 单缓冲模式下CPU必须等待GPU完成当前帧才能录制下一帧
// 导致CPU空闲等待。多帧缓冲允许CPU提前录制N帧GPU异步执行最大化硬件利用率
//
// 2. **命令分配器冲突解决**: D3D12中ID3D12CommandAllocator在GPU执行期间
// 不能被重置。每帧使用独立的分配器当前帧提交GPU后CPU可立即重置
// (frame_buffer_count-1)帧之前的分配器,实现无等待循环
//
// 3. **帧时序稳定性**: 缓冲N帧可平滑帧率波动避免单帧卡顿影响整体流畅度
//
// ### 帧索引轮转机制
//
// 使用环形缓冲区(ring buffer)管理帧资源:
// - 当前帧索引: current_frame_index % frame_buffer_count
// - 每帧提交后递增索引到达frame_buffer_count时归零
// - 确保CPU不会超前GPU超过frame_buffer_count帧防止资源冲突
//
// ### 命令列表创建策略
//
// 创建时使用_cmd_frames[0].cmd_allocator作为初始分配器原因
// - 命令列表创建时必须绑定一个分配器即使后续通过Reset()切换
// - 选择索引0确保初始化阶段有确定的资源状态
// - 实际录制前会调用Reset()绑定当前帧对应的分配器
//
// ### 立即关闭命令列表的设计考量
//
// 创建后立即调用Close(),因为:
// - 新创建的命令列表处于"录制打开"状态
// - 但实际渲染前需要重新Reset()绑定正确的帧分配器
// - 先Close()使列表进入可提交状态,避免状态不一致
// - 这是一种防御性编程,确保对象始终处于有效状态
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 };
// 创建命令队列
D3D12_COMMAND_QUEUE_DESC desc{};
desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; // 无特殊标志
desc.NodeMask = 0; // 单GPU节点
desc.Type = type; // 命令队列类型Direct/Compute/Copy
desc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL; // 普通优先级
DXCall(hr = device->CreateCommandQueue(&desc, IID_PPV_ARGS(&_cmd_queue)));
if(FAILED(hr)) goto _error;
NAME_D3D12_OBJECT(_cmd_queue, type == D3D12_COMMAND_LIST_TYPE_DIRECT ?
L"Direct Command Queue" :
type == D3D12_COMMAND_LIST_TYPE_COMPUTE ?
L"Compute Command Queue" : L" Command Queue");
// 为所有帧创建命令分配器
for(u32 i{ 0 }; i < frame_buffer_count; ++i)
{
command_frame& frame{_cmd_frames[i]};
DXCall(hr = device->CreateCommandAllocator(type, IID_PPV_ARGS(&frame.cmd_allocator)));
if(FAILED(hr)) goto _error;
NAME_D3D12_OBJECT_INDEXED(frame.cmd_allocator, i, type == D3D12_COMMAND_LIST_TYPE_DIRECT ?
L"Direct Command Allocator" :
type == D3D12_COMMAND_LIST_TYPE_COMPUTE ?
L"Compute Command Allocator" : L" Command Allocator");
}
// 创建命令列表 - 采用多帧缓冲设计实现CPU-GPU并行渲染,传入第一帧的分配器作为初始分配器,并立即关闭命令列表
DXCall(hr = device->CreateCommandList(0,type,_cmd_frames[0].cmd_allocator,nullptr,IID_PPV_ARGS(&_cmd_list)));
if(FAILED(hr)) goto _error;
DXCall(_cmd_list->Close());
NAME_D3D12_OBJECT(_cmd_list, type == D3D12_COMMAND_LIST_TYPE_DIRECT ?
L"Direct Command List" :
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();
}
~d3d12_command()
{
assert(!_cmd_queue && !_cmd_list && !_fence);
}
// 等待当前帧被标记为完成信号,并重置命令分配器和命令列表
void begin_frame()
{
command_frame& frame{_cmd_frames[_frame_index]};
frame.wait(_fence_event, _fence);
// 重置命令分配器将释放之前帧分配的命令内存,使其可重新用于录制新帧的命令
// 重置命令列表将命令列表重置为可录制状态,准备录制录制命令
DXCall(frame.cmd_allocator->Reset());
DXCall(_cmd_list->Reset(frame.cmd_allocator, nullptr));
}
// 使用新的围栏值来标记这个围栏
void end_frame()
{
//在提交命令列表前,先关闭命令列表,确保命令列表进入可提交状态
DXCall(_cmd_list->Close());
// 将命令列表转为数组形式提交给命令队列执行
// 虽然目前只有单个命令列表且为单线程工作模式,但仍采用数组方式以保持代码的扩展性
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 };
u64 fence_value{ 0 };
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()
{
core::release(cmd_allocator);
fence_value = 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 };
/**
* @brief DXGI 工厂指针
* @details 指向 DXGI 工厂的智能指针,用于创建 Direct3D 12 设备
*/
IDXGIFactory7* dxgi_factory{ nullptr };
/**
* @brief 命令管理类实例
* @details 用于管理 Direct3D 12 命令队列和命令列表,提供类型安全的 GPU命令提交机制
*/
d3d12_command gfx_command;
/**
* @brief RTV 描述符堆
* @details 用于存储渲染目标视图的描述符,用于渲染管线中的输出
*/
descriptor_heap rtv_descriptor_heap{D3D12_DESCRIPTOR_HEAP_TYPE_RTV};
/**
* @brief DSV 描述符堆
* @details 用于存储深度模板视图的描述符,用于渲染管线中的深度模板测试
*/
descriptor_heap dsv_descriptor_heap{D3D12_DESCRIPTOR_HEAP_TYPE_DSV};
/**
* @brief SRV 描述符堆
* @details 用于存储着色器资源视图的描述符,用于渲染管线中的着色器资源访问
*/
descriptor_heap srv_descriptor_heap{D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV};
/**
* @brief UAV 描述符堆
* @details 用于存储无序访问视图的描述符,用于渲染管线中的无序访问
*/
descriptor_heap uav_descriptor_heap{D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV};
/**
* @brief 延迟释放队列
* @details 每个帧缓冲区对应一个待释放资源的队列,用于在渲染完成后释放资源
*/
utl::vector<IUnknown*> deferred_releases[frame_buffer_count]{};
/**
* @brief 延迟释放标志数组
* @details 每个帧缓冲区对应一个标志位用于记录是否需要延迟释放资源
*/
u32 deferred_release_flag[frame_buffer_count]{};
/**
* @brief 延迟释放互斥锁
* @details 用于保护延迟释放标志数组的并发访问,确保线程安全
*/
std::mutex deferred_release_mutex{};
// 最小支持的 Direct3D 特本级别
constexpr D3D_FEATURE_LEVEL minumum_feature_level{ D3D_FEATURE_LEVEL_11_0 };
bool
failed_init()
{
shutdown();
return false;
}
/**
* @brief 确定要使用的 GPU
* @details 枚举所有可用的 GPU选择支持最小特征级别的 Direct3D 12 设备
* 注意:该功能可通过以下方式扩展:例如,检查是否有任何
* 输出设备(即显示器)已连接,枚举支持的分辨率,提供
* 一种机制供用户在多适配器环境中选择要使用的适配器等
* @return IDXGIAdapter4* 指向确定的 GPU 的智能指针
*/
IDXGIAdapter4*
determine_main_adapter()
{
IDXGIAdapter4* adapter;
for (u32 i{ 0 };
dxgi_factory->EnumAdapterByGpuPreference(i, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter)) != DXGI_ERROR_NOT_FOUND;
++i)
{
//获取支持最小特征级别的 Direct3D 12 设备
if (SUCCEEDED(D3D12CreateDevice(adapter, minumum_feature_level, __uuidof(ID3D12Device8), nullptr)))
{
return adapter;
}
release(adapter);
}
return nullptr;
}
/**
* @brief 获取指定适配器支持的最高 Direct3D 特性级别
* @details 检查指定适配器是否支持 Direct3D 12 特性级别
* @param adapter 指向要检查的适配器的智能指针
* @return D3D_FEATURE_LEVEL 支持的最高 Direct3D 特性级别
*/
D3D_FEATURE_LEVEL
get_max_feature_level(IDXGIAdapter4* adapter)
{
constexpr D3D_FEATURE_LEVEL feature_levels[4]{
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_12_0,
D3D_FEATURE_LEVEL_12_1,
};
D3D12_FEATURE_DATA_FEATURE_LEVELS feature_level_info{};
feature_level_info.NumFeatureLevels = (_countof(feature_levels));
feature_level_info.pFeatureLevelsRequested = feature_levels;
ComPtr<ID3D12Device> device;
DXCall(D3D12CreateDevice(adapter, minumum_feature_level, IID_PPV_ARGS(&device)));
DXCall(device->CheckFeatureSupport(D3D12_FEATURE_FEATURE_LEVELS, &feature_level_info, sizeof(feature_level_info)));
return feature_level_info.MaxSupportedFeatureLevel;
}
/**
* @brief 处理延迟释放资源
* @details 遍历指定帧索引的延迟释放资源索引数组,释放每个资源
* @param frame_index 要处理的帧索引
* @note 使用 __declspec(noinline) 防止编译器内联此函数,确保在调试时能够准确断点
*/
void __declspec(noinline)
process_deferred_release(u32 frame_index)
{
std::lock_guard lock{ deferred_release_mutex };
// 我们在开始的的时候清楚这个帧的标志位,如果我们在结尾的时候清除,
// 他可能被其他线程重写.
deferred_release_flag[frame_index] = 0;
rtv_descriptor_heap.process_deferred_release(frame_index);
dsv_descriptor_heap.process_deferred_release(frame_index);
srv_descriptor_heap.process_deferred_release(frame_index);
uav_descriptor_heap.process_deferred_release(frame_index);
utl::vector<IUnknown*>& resources{ deferred_releases[frame_index] };
if(!resources.empty())
{
for(auto& resource : resources) release(resource);
resources.clear();
}
}
}// anonymous namespace
namespace detail{
void
deferred_release(IUnknown* resource)
{
const u32 frmae_idx {current_frame_index()};
std::lock_guard lock{ deferred_release_mutex };
deferred_releases[frmae_idx].push_back(resource);
set_deferred_release_flag();
}
} // detail namespace
bool
initialize()
{
if (main_device) shutdown();
// 在DEBUG模式下,捕获 DXGI 可能抛出的异常
u32 dxgi_factory_flag{ 0 };
// 开启调试层, 需要graphics tools支持
#ifdef _DEBUG
{
ComPtr<ID3D12Debug3> debug_interface;
if(SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debug_interface))))
{
debug_interface->EnableDebugLayer();
}
else
{
OutputDebugStringA("warning: d3d12 debug interface is not available\n");
}
dxgi_factory_flag |= DXGI_CREATE_FACTORY_DEBUG;
}
#endif
// 创建 DXGI 工厂实例,用于枚举显卡适配器和创建交换链等操作
HRESULT hr{ S_OK };
DXCall(hr = CreateDXGIFactory2(dxgi_factory_flag, IID_PPV_ARGS(&dxgi_factory)));
if (FAILED(hr)) return failed_init();
// 确定要使用的 GPU
ComPtr<IDXGIAdapter4> main_adapter;
main_adapter.Attach(determine_main_adapter());
if (!main_adapter) return failed_init();
// 获取主适配器支持的最高 Direct3D 特性级别,并且不得低于最小要求
D3D_FEATURE_LEVEL max_feature_level{ get_max_feature_level(main_adapter.Get()) };
assert(max_feature_level >= minumum_feature_level);
if (max_feature_level < minumum_feature_level) return failed_init();
// 使用最高适配特性级别创建Direct3D 12 设备
DXCall(hr = D3D12CreateDevice(main_adapter.Get(), max_feature_level, IID_PPV_ARGS(&main_device)));
if (FAILED(hr)) return failed_init();
#ifdef _DEBUG
{
ComPtr<ID3D12InfoQueue> info_queue;
DXCall(main_device->QueryInterface(IID_PPV_ARGS(&info_queue)));
info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_CORRUPTION, true);
info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_WARNING, true);
info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_ERROR, true);
}
#endif // _DEBUG
// 使用 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();
bool result{true};
result &= rtv_descriptor_heap.initialize(512, false);
result &= dsv_descriptor_heap.initialize(512, false);
result &= srv_descriptor_heap.initialize(4096, false);
result &= uav_descriptor_heap.initialize(512, false);
if(!result) return failed_init();
// 为 Direct3D 12 设备设置名称
NAME_D3D12_OBJECT(main_device, L"Main Device");
NAME_D3D12_OBJECT(rtv_descriptor_heap.heap(), L"RTV Descriptor Heap");
NAME_D3D12_OBJECT(dsv_descriptor_heap.heap(), L"DSV Descriptor Heap");
NAME_D3D12_OBJECT(srv_descriptor_heap.heap(), L"SRV Descriptor Heap");
NAME_D3D12_OBJECT(uav_descriptor_heap.heap(), L"UAV Descriptor Heap");
return true;
}
void
shutdown()
{
gfx_command.release();
// 注意,我们需要在所有的依赖资源之前调用延迟释放函数.
// 否则会导致依赖当基础前资源(贴图等)的资源(着色器等)不能被释放.
for(u32 i{ 0 }; i < frame_buffer_count; ++i)
{
process_deferred_release(i);
}
release(dxgi_factory);
rtv_descriptor_heap.release();
dsv_descriptor_heap.release();
srv_descriptor_heap.release();
uav_descriptor_heap.release();
// 某些类型仅在 shutdown/reset/clear 时使用延迟释放机制来释放资源,
// 为了确保这些资源被正确释放,需要在 shutdown 最后额外调用一次延迟释放函数。
process_deferred_release(0);
#ifdef _DEBUG
{
{
// 关闭调试层,确保最后只有一个活动的主设备
ComPtr<ID3D12InfoQueue> info_queue;
DXCall(main_device->QueryInterface(IID_PPV_ARGS(&info_queue)));
info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_CORRUPTION, false);
info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_WARNING, false);
info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_ERROR, false);
}
ComPtr<ID3D12DebugDevice2> debug_device;
DXCall(main_device->QueryInterface(IID_PPV_ARGS(&debug_device)));
release(main_device);
DXCall(debug_device->ReportLiveDeviceObjects(
D3D12_RLDO_SUMMARY | D3D12_RLDO_DETAIL | D3D12_RLDO_IGNORE_INTERNAL
));
}
#endif // _DEBUG
release(main_device);
}
void
render()
{
// 等待GPU完成命令列表,并重置命令分配器和命令列表
gfx_command.begin_frame();
ID3D12GraphicsCommandList6* cmd_list{ gfx_command.command_list() };
const u32 frame_index{ current_frame_index() };
if(deferred_release_flag[frame_index])
{
process_deferred_release(frame_index);
}
// 记录命令
//
// 完成命令记录,立即提交命令列表到命令队列执行
// 为下一帧标记并增加围栏值
gfx_command.end_frame();
}
ID3D12Device *const
device()
{
return main_device;
}
u32
current_frame_index()
{
return gfx_command.frame_index();
}
// X86结构上的整数访问权架构原子的,所以不需要加锁
void
set_deferred_release_flag()
{
deferred_release_flag[current_frame_index()] = 1;
}
}// namespace XEngine::graphics::d3d12::core