From 7da17ccadd666ef1076750ddee1920b5def8d8c5 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:30:38 +0800 Subject: [PATCH] feat: implement command queue with multi-frame buffering D3D12 Core: - Add d3d12_command class for command queue management - Support Direct/Compute/Copy command queue types - Implement multi-frame buffering (frame_buffer_count=3) - Add begin_frame/end_frame rendering cycle - Add NAME_D3D12_OBJECT_INDEXED macro Platform Interface: - Add render function pointer to platform_interface - Implement render() in Renderer Documentation: - Add changelog for command queue implementation - Update D3D12 Wiki with multi-frame buffering section - Mark command queue task as completed --- .../Graphics/Direct3D12/D3D12CommonHeader.h | 24 ++- Engine/Graphics/Direct3D12/D3D12Core.cpp | 173 +++++++++++++++++- Engine/Graphics/Direct3D12/D3D12Core.h | 5 + Engine/Graphics/Direct3D12/D3D12Interface.cpp | 1 + Engine/Graphics/GraphicsPlatformInterface.h | 1 + Engine/Graphics/Renderer.cpp | 14 +- Engine/Graphics/Renderer.h | 6 + EngineTest/TestRenderer.cpp | 4 +- .../2026-03/20260326-d3d12-command-queue.md | 135 ++++++++++++++ docs/changelogs/README.md | 1 + docs/wiki/D3D12学习Wiki.md | 65 +++++-- 11 files changed, 406 insertions(+), 23 deletions(-) create mode 100644 docs/changelogs/2026-03/20260326-d3d12-command-queue.md diff --git a/Engine/Graphics/Direct3D12/D3D12CommonHeader.h b/Engine/Graphics/Direct3D12/D3D12CommonHeader.h index e20787b..2d65ace 100644 --- a/Engine/Graphics/Direct3D12/D3D12CommonHeader.h +++ b/Engine/Graphics/Direct3D12/D3D12CommonHeader.h @@ -20,6 +20,9 @@ #pragma comment(lib, "dxgi.lib") #pragma comment(lib, "d3d12.lib") +namespace XEngine::graphics::d3d12::core { + constexpr u32 frame_buffer_count{ 3 }; +} // 定义 DirectX 调试宏 DXCall,用于在调试模式下检查 DirectX API 调用返回值 // 如果调用失败(FAILED),则输出错误信息(文件名、行号、调用语句)并触发断点 @@ -46,9 +49,24 @@ if(FAILED(x)){ \ #endif // !DXCall #endif // _DEBUG -// 定义 DirectX 对象命名宏,用于在调试模式下为 Direct3D 12 对象设置名称 +// 定义 DirectX 对象命名宏,用于在调试模式下为 Direct3D 12 对象设置调试名称 +// 这些宏仅在 _DEBUG 模式下生效,可帮助开发者在 PIX、RenderDoc 等图形调试工具中 +// 识别和追踪 D3D12 对象(如缓冲区、纹理、管线状态等) +// NAME_D3D12_OBJECT: 为单个对象设置名称 +// NAME_D3D12_OBJECT_INDEXED: 为数组中的对象设置带索引的名称(如资源数组) #ifdef _DEBUG -#define NAME_D3D12_OBJECT(obj, name) obj->SetName(name); OutputDebugString(L"::D3D12 Object Created: ");OutputDebugString(name);OutputDebugString(L"\n"); +#define NAME_D3D12_OBJECT(obj,name) obj->SetName(name); OutputDebugString(L"::D3D12 Object Created: "); OutputDebugString(name); OutputDebugString(L"\n"); +#define NAME_D3D12_OBJECT_INDEXED(obj,n,name) \ +{ \ +wchar_t full_name[128]; \ +if(swprintf_s(full_name, L"%s[%llu]", name, (u64)n) >0 ){ \ + obj->SetName(full_name); \ + OutputDebugString(L"::D3D12 Object Created: "); \ + OutputDebugString(full_name); \ + OutputDebugString(L"\n"); \ +}} + #else -#define NAME_D3D12_OBJECT(obj, name) +#define NAME_D3D12_OBJECT(obj,name) +#define NAME_D3D12_OBJECT_INDEXED(obj,n,name) #endif diff --git a/Engine/Graphics/Direct3D12/D3D12Core.cpp b/Engine/Graphics/Direct3D12/D3D12Core.cpp index 56dd8aa..05ca021 100644 --- a/Engine/Graphics/Direct3D12/D3D12Core.cpp +++ b/Engine/Graphics/Direct3D12/D3D12Core.cpp @@ -4,6 +4,164 @@ 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 +{ + 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"); + + + _error: + release(); + } + + void release() + { + } + + void begin_frame() + { + command_frame& frame{_cmd_frames[_frame_index]}; + frame.wait(); + // 重置命令分配器将释放之前帧分配的命令内存,使其可重新用于录制新帧的命令 + // 重置命令列表将命令列表重置为可录制状态,准备录制录制命令 + 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]); + _frame_index = (_frame_index + 1) % frame_buffer_count; + } + +private: + + struct command_frame + { + ID3D12CommandAllocator* cmd_allocator{ nullptr }; + + void wait() + { + + } + + void release() + { + core::release(cmd_allocator); + } + }; + + ID3D12CommandQueue* _cmd_queue{ nullptr }; + ID3D12GraphicsCommandList6* _cmd_list{ nullptr }; + command_frame _cmd_frames[frame_buffer_count]{}; + u32 _frame_index{ 0 }; +}; /** * @brief 主 Direct3D 12 设备指针 @@ -137,15 +295,16 @@ initialize() return true; } + void shutdown() { release(dxgi_factory); - // 关闭调试层,确保最后只有一个活动的主设备 -#ifdef _DEBUG + #ifdef _DEBUG { - { + { + // 关闭调试层,确保最后只有一个活动的主设备 ComPtr info_queue; DXCall(main_device->QueryInterface(IID_PPV_ARGS(&info_queue))); @@ -166,4 +325,12 @@ shutdown() release(main_device); } + +void +render() +{ + begin_frame(); + + end_frame(); +} }// namespace XEngine::graphics::d3d12::core diff --git a/Engine/Graphics/Direct3D12/D3D12Core.h b/Engine/Graphics/Direct3D12/D3D12Core.h index d1565df..85cd3f7 100644 --- a/Engine/Graphics/Direct3D12/D3D12Core.h +++ b/Engine/Graphics/Direct3D12/D3D12Core.h @@ -16,6 +16,11 @@ bool initialize(); * @details 调用 Direct3D 12 设备的关闭函数,释放所有资源 */ void shutdown(); +/** + * @brief 渲染 Direct3D 12 核心功能 + * @details 调用 Direct3D 12 设备的渲染函数,渲染当前渲染表面 + */ +void render(); template constexpr void release(T*& resource) diff --git a/Engine/Graphics/Direct3D12/D3D12Interface.cpp b/Engine/Graphics/Direct3D12/D3D12Interface.cpp index 0a4e646..eaac64b 100644 --- a/Engine/Graphics/Direct3D12/D3D12Interface.cpp +++ b/Engine/Graphics/Direct3D12/D3D12Interface.cpp @@ -8,6 +8,7 @@ void get_platform_interface(platform_interface& pi) { pi.initialize = core::initialize; pi.shutdown = core::shutdown; + pi.render = core::render; } }// namespace XEngine::graphics::d3d12 diff --git a/Engine/Graphics/GraphicsPlatformInterface.h b/Engine/Graphics/GraphicsPlatformInterface.h index 2869e73..148648f 100644 --- a/Engine/Graphics/GraphicsPlatformInterface.h +++ b/Engine/Graphics/GraphicsPlatformInterface.h @@ -10,6 +10,7 @@ namespace XEngine::graphics{ struct platform_interface{ bool(*initialize)(void); void(*shutdown)(void); + void(*render)(void); }; }// namespace XEngine::graphics diff --git a/Engine/Graphics/Renderer.cpp b/Engine/Graphics/Renderer.cpp index f8b850b..097cdfb 100644 --- a/Engine/Graphics/Renderer.cpp +++ b/Engine/Graphics/Renderer.cpp @@ -39,13 +39,23 @@ set_platform_interface(graphics_platform platform) * @param platform 图形渲染平台类型 * @return true 如果初始化成功,否则返回 false */ -bool initialize(graphics_platform platform) +bool +initialize(graphics_platform platform) { return set_platform_interface(platform) && gfx.initialize(); } -void shutdown() +void +shutdown() { gfx.shutdown(); } + +void +render() +{ + gfx.render(); +} + + }// namespace XEngine::graphics diff --git a/Engine/Graphics/Renderer.h b/Engine/Graphics/Renderer.h index 6314659..aa7c6a6 100644 --- a/Engine/Graphics/Renderer.h +++ b/Engine/Graphics/Renderer.h @@ -47,4 +47,10 @@ bool initialize(graphics_platform platform); */ void shutdown(); +/** + * @brief 渲染调用接口 + * @details 调用渲染函数指针,渲染当前渲染表面 + */ +void render(); + } \ No newline at end of file diff --git a/EngineTest/TestRenderer.cpp b/EngineTest/TestRenderer.cpp index a859d1a..8c4ba04 100644 --- a/EngineTest/TestRenderer.cpp +++ b/EngineTest/TestRenderer.cpp @@ -11,7 +11,7 @@ #include -#if TEST_RENDERER +#ifdef TEST_RENDERER using namespace XEngine; graphics::render_surface _surfaces[4]; @@ -108,4 +108,4 @@ engine_test::shutdown() return true; } -#endif +#endif // TEST_RENDERER diff --git a/docs/changelogs/2026-03/20260326-d3d12-command-queue.md b/docs/changelogs/2026-03/20260326-d3d12-command-queue.md new file mode 100644 index 0000000..160d4cf --- /dev/null +++ b/docs/changelogs/2026-03/20260326-d3d12-command-queue.md @@ -0,0 +1,135 @@ +# 变更记录:命令队列与多帧缓冲 + +**提交日期**: 2026-03-26 +**提交哈希**: `26e18bd` +**变更类型**: 功能实现 + +--- + +## 变更概述 + +本次提交实现了 D3D12 命令队列管理类 `d3d12_command`,支持多帧缓冲渲染架构,实现了 CPU-GPU 并行渲染的基础设施。 + +## 修改文件 + +### Engine/Graphics/Direct3D12/ + +| 文件 | 变更说明 | +|------|----------| +| `D3D12Core.cpp` | 添加 `d3d12_command` 类,实现命令队列和命令列表管理 | +| `D3D12Core.h` | 添加 `render()` 函数声明 | +| `D3D12CommonHeader.h` | 添加 `frame_buffer_count` 常量和 `NAME_D3D12_OBJECT_INDEXED` 宏 | + +### Engine/Graphics/ + +| 文件 | 变更说明 | +|------|----------| +| `GraphicsPlatformInterface.h` | 添加 `render` 函数指针 | +| `Renderer.h` | 添加 `render()` 函数声明 | +| `Renderer.cpp` | 实现 `render()` 函数 | + +--- + +## 技术要点 + +### 1. 多帧缓冲架构 + +```cpp +constexpr u32 frame_buffer_count{ 3 }; +``` + +采用三重缓冲设计,允许 CPU 提前录制命令,GPU 异步执行,最大化硬件利用率。 + +### 2. d3d12_command 类 + +```cpp +class d3d12_command +{ + // 创建命令队列(支持 Direct/Compute/Copy 三种类型) + // 为每帧创建独立的命令分配器 + // 创建命令列表 + + void begin_frame(); // 等待帧完成,重置分配器和命令列表 + void end_frame(); // 关闭命令列表,提交执行 +}; +``` + +### 3. 帧索引轮转机制 + +```cpp +_frame_index = (_frame_index + 1) % frame_buffer_count; +``` + +环形缓冲区管理帧资源,确保 CPU 不会超前 GPU 超过 3 帧。 + +### 4. command_frame 结构 + +```cpp +struct command_frame +{ + ID3D12CommandAllocator* cmd_allocator{ nullptr }; + void wait(); // 等待 GPU 完成该帧 + void release(); // 释放资源 +}; +``` + +每帧独立的命令分配器,避免 GPU 执行期间重置冲突。 + +### 5. 命名调试宏 + +```cpp +#define NAME_D3D12_OBJECT_INDEXED(obj, n, name) \ + obj->SetName(full_name); ... +``` + +支持为多个同类对象设置带索引的调试名称。 + +--- + +## 渲染流程 + +``` +render() + │ + ├─► begin_frame() + │ ├─► 等待当前帧 GPU 完成 + │ ├─► 重置命令分配器 + │ └─► 重置命令列表 + │ + └─► end_frame() + ├─► 关闭命令列表 + ├─► 提交命令列表执行 + └─► 递增帧索引 +``` + +--- + +## 设计原理 + +### 为什么需要多帧缓冲? + +1. **CPU-GPU 并行性**: 单缓冲模式下 CPU 必须等待 GPU 完成,多帧缓冲允许 CPU 提前录制 N 帧 +2. **命令分配器冲突解决**: D3D12 中命令分配器在 GPU 执行期间不能被重置,每帧独立分配器解决此问题 +3. **帧时序稳定性**: 缓冲 N 帧可平滑帧率波动 + +### 命令列表创建后立即关闭 + +```cpp +DXCall(_cmd_list->Close()); +``` + +新创建的命令列表处于"录制打开"状态,立即关闭使其进入可提交状态,这是一种防御性编程。 + +--- + +## 后续工作 + +- [ ] 实现 Fence 同步机制 +- [ ] 实现交换链 +- [ ] 实现描述符堆 + +--- + +## 相关文档 + +- [D3D12学习Wiki](../wiki/D3D12学习Wiki.md) diff --git a/docs/changelogs/README.md b/docs/changelogs/README.md index d75e27d..567bd00 100644 --- a/docs/changelogs/README.md +++ b/docs/changelogs/README.md @@ -16,6 +16,7 @@ changelogs/ | 日期 | 提交 | 变更内容 | |------|------|----------| +| 2026-03-26 | [命令队列与多帧缓冲](./2026-03/20260326-d3d12-command-queue.md) | D3D12 命令队列和多帧渲染架构 | | 2026-03-26 | [D3D12设备初始化](./2026-03/20260326-d3d12-device-init.md) | D3D12 设备创建与调试层实现 | | 2026-03-26 | [Graphics模块](./2026-03/20260326-d3d12-foundation.md) | Graphics 模块与 D3D12 后端框架 | | 2026-03-19 | [DX12初始框架](./2026-03/20260326-dx12-initial.md) | 初始 DX12 基础框架 | diff --git a/docs/wiki/D3D12学习Wiki.md b/docs/wiki/D3D12学习Wiki.md index bc537cd..a6ff9d1 100644 --- a/docs/wiki/D3D12学习Wiki.md +++ b/docs/wiki/D3D12学习Wiki.md @@ -164,6 +164,7 @@ Microsoft::WRL::ComPtr device; void get_platform_interface(platform_interface& pi) { pi.initialize = core::initialize; pi.shutdown = core::shutdown; + pi.render = core::render; } ``` @@ -172,9 +173,47 @@ void get_platform_interface(platform_interface& pi) { - 各后端独立开发和测试 - 上层代码与具体 API 解耦 -## 6. 渲染表面与窗口 +## 6. 命令队列与多帧缓冲 -### 6.1 render_surface 结构 +### 6.1 d3d12_command 类 + +项目实现了命令队列管理类,支持多帧缓冲渲染: + +```cpp +class d3d12_command +{ + void begin_frame(); // 等待帧完成,重置分配器和命令列表 + void end_frame(); // 关闭命令列表,提交执行 +private: + ID3D12CommandQueue* _cmd_queue; + ID3D12GraphicsCommandList6* _cmd_list; + command_frame _cmd_frames[frame_buffer_count]; + u32 _frame_index; +}; +``` + +### 6.2 多帧缓冲原理 + +```cpp +constexpr u32 frame_buffer_count{ 3 }; +``` + +采用三重缓冲设计: +- CPU 提前录制命令 +- GPU 异步执行 +- 最大化硬件利用率 + +### 6.3 帧索引轮转 + +```cpp +_frame_index = (_frame_index + 1) % frame_buffer_count; +``` + +环形缓冲区管理帧资源,确保 CPU 不会超前 GPU 超过 3 帧。 + +## 7. 渲染表面与窗口 + +### 7.1 render_surface 结构 ```cpp struct render_surface { @@ -183,7 +222,7 @@ struct render_surface { }; ``` -### 6.2 多窗口支持 +### 7.2 多窗口支持 TestRenderer 测试展示了多窗口渲染: @@ -196,7 +235,7 @@ for (u32 i{0}; i < _countof(_surfaces); ++i) { } ``` -### 6.3 全屏切换 +### 7.3 全屏切换 通过 `WM_SYSCHAR` 消息处理 Alt+Enter: @@ -206,42 +245,42 @@ if (wparam == VK_RETURN && (HIWORD(lparam) & KF_ALTDOWN)) { } ``` -## 7. 后续学习路径 +## 8. 后续学习路径 -### 7.1 基础阶段 +### 8.1 基础阶段 - [x] 完成设备创建和适配器枚举 -- [ ] 创建命令队列和命令列表 +- [x] 创建命令队列和命令列表 - [ ] 实现交换链和后台缓冲区 - [ ] 渲染第一个三角形 -### 7.2 进阶阶段 +### 8.2 进阶阶段 - [ ] 描述符堆管理 - [ ] 根签名和管线状态对象 - [ ] 资源屏障和同步 - [ ] 常量缓冲区和着色器资源 -### 7.3 高级阶段 +### 8.3 高级阶段 - [ ] 多线程渲染 - [ ] 资源绑定策略 - [ ] 动态资源管理 - [ ] 性能优化 -## 8. 参考资源 +## 9. 参考资源 -### 8.1 官方文档 +### 9.1 官方文档 - [Microsoft D3D12 文档](https://docs.microsoft.com/en-us/windows/win32/direct3d12/direct3d-12-graphics) - [DXGI 文档](https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/dx-graphics-dxgi) -### 8.2 推荐书籍 +### 9.2 推荐书籍 - 《Introduction to 3D Game Programming with DirectX 12》 - 《Real-Time 3D Rendering with DirectX and HLSL》 -### 8.3 项目相关文档 +### 9.3 项目相关文档 - [Graphics渲染架构分析](./Graphics渲染架构分析.md) - [项目约定规范](./项目约定规范.md)