Files
DX12/Engine/Graphics/Direct3D12/D3D12Core.cpp
SpecialX 54916b0ac6 feat: implement descriptor heap with thread-safe allocation
D3D12 Resources:
- Add descriptor_handle struct with CPU/GPU handles
- Add descriptor_heap class for descriptor management
- Implement allocate() and free() methods
- Add mutex for thread-safe access
- Support all D3D12 descriptor heap types

D3D12 Core:
- Add device() function to expose main device
- Add release() template function for COM objects

Documentation:
- Add changelog for descriptor heap implementation
- Update D3D12 Wiki with descriptor heap section
- Mark descriptor heap task as completed
2026-03-30 14:04:34 +08:00

432 lines
16 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"
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);
}
};
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;
// 最小支持的 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;
}
}// anonymous namespace
bool
initialize()
{
if (main_device) shutdown();
// 在DEBUG模式下,捕获 DXGI 可能抛出的异常
u32 dxgi_factory_flag{ 0 };
// 开启调试层, 需要graphics tools支持
#ifdef _DEBUG
{
ComPtr<ID3D12Debug3> debug_interface;
DXCall(D3D12GetDebugInterface(IID_PPV_ARGS(&debug_interface)));
debug_interface->EnableDebugLayer();
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();
// 为 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;
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
return true;
}
void
shutdown()
{
gfx_command.release();
release(dxgi_factory);
#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() };
// 记录命令
//
// 完成命令记录,立即提交命令列表到命令队列执行
// 为下一帧标记并增加围栏值
gfx_command.end_frame();
}
ID3D12Device *const
device()
{
return main_device;
}
}// namespace XEngine::graphics::d3d12::core