# Direct3D 12 学习 Wiki ## 概述 本文档是项目中 Direct3D 12 学习的基础知识汇总,帮助理解 D3D12 的核心概念和项目中的实现方式。 ## 1. D3D12 核心概念 ### 1.1 什么是 Direct3D 12? Direct3D 12 是微软推出的底层图形 API,相比 D3D11,它提供了: - **更底层的硬件控制**:开发者可以更精细地控制 GPU - **更低的 CPU 开销**:减少驱动程序的 CPU 时间 - **更好的多线程支持**:支持多线程命令录制 - **显式的资源管理**:开发者完全控制资源生命周期 ### 1.2 核心组件 | 组件 | 说明 | |------|------| | **Device(设备)** | 代表物理 GPU 的逻辑抽象,用于创建所有 D3D12 对象 | | **Command Queue(命令队列)** | GPU 执行命令的队列 | | **Command List(命令列表)** | 记录 GPU 命令的容器 | | **Swap Chain(交换链)** | 管理后台缓冲区和前台显示 | | **Descriptor Heap(描述符堆)** | 存储资源描述符(视图)的内存池 | | **Root Signature(根签名)** | 定义着色器如何访问资源 | ## 2. DXGI(DirectX Graphics Infrastructure) ### 2.1 DXGI 的作用 DXGI 是 DirectX 与图形硬件之间的抽象层,负责: - 枚举显示适配器 - 管理显示输出 - 创建交换链 - 处理全屏/窗口切换 ### 2.2 关键接口 ```cpp IDXGIFactory6 // DXGI 工厂,创建其他 DXGI 对象 IDXGIAdapter4 // 代表物理 GPU 适配器 IDXGIOutput // 代表显示器输出 IDXGISwapChain // 交换链接口 ``` ### 2.3 项目中的使用 在 [D3D12CommonHeader.h](file:///d:/AllWX/AllC/FeatureExtractDemo/Engine/Graphics/Direct3D12/D3D12CommonHeader.h) 中引入了 `dxgi_6.h`: ```cpp #include // DXGI 6.0,支持枚举 GPU、查询显示模式 ``` ## 3. D3D12 初始化流程 ### 3.1 标准初始化步骤 ``` 1. 创建 DXGI Factory ↓ 2. 枚举并选择适配器(GPU) ↓ 3. 创建 D3D12 Device ↓ 4. 创建命令队列 ↓ 5. 创建交换链 ↓ 6. 创建描述符堆 ↓ 7. 创建命令分配器和命令列表 ↓ 8. 创建同步对象(Fence) ``` ### 3.2 项目当前状态 [D3D12Core.cpp](file:///d:/AllWX/AllC/FeatureExtractDemo/Engine/Graphics/Direct3D12/D3D12Core.cpp) 已实现完整的设备初始化: ```cpp namespace { ID3D12Device8* main_device{ nullptr }; IDXGIFactory7* dxgi_factory{ nullptr }; } bool initialize() { // 1. 启用调试层 (DEBUG 模式) // 2. 创建 DXGI 工厂 // 3. 枚举并选择 GPU 适配器 // 4. 获取最高特性级别 // 5. 创建 D3D12 设备 // 6. 配置信息队列 (DEBUG 模式) } ``` ### 3.3 调试宏 项目定义了两个重要的调试宏: ```cpp // DXCall - 检查 HRESULT 并断点 #define DXCall(x) if(FAILED(x)) { ... __debugbreak(); } // NAME_D3D12_OBJECT - 为对象设置调试名称 #define NAME_D3D12_OBJECT(obj, name) obj->SetName(name); ``` ## 4. COM 对象管理 ### 4.1 什么是 COM? COM(Component Object Model)是微软的组件对象模型,D3D12 对象都是 COM 对象。 ### 4.2 智能指针 项目使用 WRL 库的 `ComPtr` 管理 COM 对象生命周期: ```cpp #include // 提供 Microsoft::WRL::ComPtr // 使用示例 Microsoft::WRL::ComPtr device; ``` ### 4.3 ComPtr 的优势 - **自动引用计数**:无需手动 AddRef/Release - **异常安全**:即使发生异常也能正确释放 - **代码简洁**:减少内存管理代码 ## 5. 平台抽象设计 ### 5.1 设计理念 项目采用**平台抽象层**设计,将图形 API 的差异封装在统一接口后: ``` ┌────────────────────────────────────┐ │ 上层渲染代码 │ │ graphics::initialize(platform) │ └──────────────┬─────────────────────┘ │ ▼ ┌────────────────────────────────────┐ │ platform_interface │ │ - initialize() │ │ - shutdown() │ └──────────────┬─────────────────────┘ │ ┌──────────┼──────────┐ ▼ ▼ ▼ ┌───────┐ ┌───────┐ ┌───────┐ │ D3D12 │ │Vulkan │ │OpenGL │ └───────┘ └───────┘ └───────┘ ``` ### 5.2 接口绑定机制 ```cpp // D3D12Interface.cpp void get_platform_interface(platform_interface& pi) { pi.initialize = core::initialize; pi.shutdown = core::shutdown; pi.render = core::render; } ``` 这种设计允许: - 编译时或运行时切换图形后端 - 各后端独立开发和测试 - 上层代码与具体 API 解耦 ## 6. 命令队列与多帧缓冲 ### 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 帧。 ### 6.4 Fence 同步机制 项目实现了 Fence(围栏)同步,确保 CPU-GPU 帧同步: ```cpp struct command_frame { ID3D12CommandAllocator* cmd_allocator{ nullptr }; u64 fence_value{ 0 }; // 该帧的围栏值 void wait(HANDLE fence_event, ID3D12Fence1* fence); }; ``` **同步流程**: 1. `begin_frame()` - 检查 GPU 是否完成当前帧,未完成则等待 2. `end_frame()` - 递增围栏值,向 GPU 发送信号 ```cpp // 帧结束信号 ++_fence_value; _cmd_frames[_frame_index].fence_value = _fence_value; _cmd_queue->Signal(_fence, _fence_value); ``` **围栏值溢出**:64位无符号整数,每秒1000帧需要5.8亿年才回绕,无需担心。 ## 7. 描述符堆 ### 7.1 什么是描述符堆? 描述符堆是一块连续内存,用于存储描述符(Descriptor)。描述符是告诉 GPU 如何访问资源的数据结构。 ### 7.2 描述符堆类型 | 类型 | 用途 | 着色器可见 | |------|------|------------| | `CBV_SRV_UAV` | 常量缓冲区、着色器资源、无序访问 | 可选 | | `SAMPLER` | 采样器 | 可选 | | `RTV` | 渲染目标视图 | 否 | | `DSV` | 深度模板视图 | 否 | ### 7.3 descriptor_handle 结构 项目封装了描述符句柄: ```cpp struct descriptor_handle { D3D12_CPU_DESCRIPTOR_HANDLE cpu{}; // CPU 句柄 D3D12_GPU_DESCRIPTOR_HANDLE gpu{}; // GPU 句柄 constexpr bool is_valid() const { return cpu.ptr != 0; } constexpr bool is_shader_visible() const { return gpu.ptr != 0; } }; ``` ### 7.4 descriptor_heap 类 ```cpp class descriptor_heap { bool initialize(u32 capacity, bool is_shader_visible); descriptor_handle allocate(); // 分配描述符 void free(descriptor_handle); // 释放描述符 private: ID3D12DescriptorHeap* _heap; std::unique_ptr _free_handles{}; // 空闲索引池 std::mutex _mutex; // 线程安全 }; ``` ### 7.5 内存模型 ``` 描述符堆内存布局: ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │...│ │ │ │ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ ↑ _cpu_start / _gpu_start _free_handles[] = [0, 1, 2, 3, ...] // 空闲索引池 ``` ### 7.6 FreeList 栈式索引管理 #### 核心数据结构 ```cpp // 空闲索引栈(预分配数组) std::unique_ptr _free_handles{}; // 栈顶指针(同时也是已分配数量) u32 _size{0}; // 总容量 u32 _capacity{0}; ``` #### 工作原理 `_free_handles` 是一个**预分配数组**,同时充当**栈**的角色: | 概念 | 说明 | |------|------| | `_free_handles` | 存储所有可用索引的数组 | | `_size` | 已分配数量 + 栈顶指针 | | 分配 | `_free_handles[_size++]`,从栈顶弹出 | | 释放 | `_free_handles[--_size] = index`,压入栈顶 | #### 状态演变示例 ``` 初始化状态(capacity = 5): ┌─────────────────────────────────────┐ │ _free_handles = [0, 1, 2, 3, 4] │ │ _size = 0 (栈顶指向位置 0) │ │ 可用索引: 0, 1, 2, 3, 4 │ └─────────────────────────────────────┘ 分配 2 个描述符后: ┌─────────────────────────────────────┐ │ _free_handles = [0, 1, 2, 3, 4] │ │ _size = 2 (栈顶指向位置 2) │ │ 已分配索引: 0, 1 │ │ 可用索引: 2, 3, 4 │ └─────────────────────────────────────┘ 释放索引 0 后(延迟处理完成): ┌─────────────────────────────────────┐ │ _free_handles = [0, 0, 2, 3, 4] │ │ _size = 1 (栈顶指向位置 1) │ │ 已分配索引: 1 │ │ 可用索引: 0, 2, 3, 4 │ └─────────────────────────────────────┘ ``` #### 分配算法 ```cpp descriptor_handle allocate() { std::lock_guard lock(_mutex); // 从栈顶取出索引 const u32 index = _free_handles[_size]; ++_size; // 栈顶指针上移 // 计算句柄地址 const u32 offset = index * _descriptor_size; handle.cpu.ptr = _cpu_start.ptr + offset; return handle; } ``` **时间复杂度**: O(1) #### 释放算法(延迟释放) ```cpp // 释放时不立即回收,放入延迟队列 void free(descriptor_handle handle) { const u32 frame_index = current_frame_index(); _deferred_free_indices[frame_index].push_back(handle.index); } // GPU 完成帧后处理延迟释放 void process_deferred_release(u32 frame_index) { for(auto index : _deferred_free_indices[frame_index]) { --_size; // 栈顶指针下移 _free_handles[_size] = index; // 索引压入栈顶 } } ``` #### 设计优势 | 特性 | 说明 | |------|------| | **O(1) 分配** | 栈顶弹出,无需遍历查找 | | **O(1) 释放** | 栈顶压入,无需查找位置 | | **内存高效** | 预分配数组,无动态分配开销 | | **简单可靠** | 栈结构天然保证索引不重复 | ### 7.7 延迟释放机制 #### 为什么需要延迟释放? ``` 问题场景(无延迟释放): 帧 0: CPU 分配描述符索引 0 → 绑定纹理 A 帧 1: CPU 释放描述符索引 0 → 立即重用 → 绑定纹理 B 帧 0: GPU 还在执行,访问描述符索引 0 → 读到纹理 B 的数据! ↑ GPU 危险!数据竞争! ``` #### 解决方案 ``` 帧 0: CPU 分配描述符索引 0 → 绑定纹理 A 帧 1: CPU 释放描述符索引 0 → 放入延迟队列(帧 1) 帧 2: CPU 继续工作... 帧 0: GPU 完成帧 0 的执行(Fence 同步确认) 帧 3: CPU 处理帧 0 的延迟释放 → 索引 0 回到空闲池 ↑ 安全!GPU 已完成使用 ``` #### 数据结构 ```cpp // 每帧一个延迟释放队列 utl::vector _deferred_free_indices[frame_buffer_count]{}; // 全局延迟释放资源队列(用于 COM 对象) utl::vector deferred_releases[frame_buffer_count]{}; // 延迟释放标志 u32 deferred_release_flag[frame_buffer_count]{}; ``` #### 完整流程图 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 初始化 │ │ _free_handles = [0, 1, 2, 3, 4], _size = 0 │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ allocate() │ │ index = _free_handles[_size] // 取索引 0 │ │ _size++ // _size = 1 │ │ 返回描述符句柄(索引 0) │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ free(索引 0) │ │ 放入延迟队列: _deferred_free_indices[当前帧].push_back(0) │ │ _size 不变,_free_handles 不变 │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ GPU 完成帧(Fence 同步) │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ process_deferred_release(frame_index) │ │ --_size // _size = 0 │ │ _free_handles[_size] = 0 // 索引 0 压入栈顶 │ │ _free_handles = [0, 1, 2, 3, 4] // 索引 0 可重用 │ └─────────────────────────────────────────────────────────────────┘ ``` ### 7.8 线程安全 描述符堆可能被多线程并发访问,使用互斥锁保护: ```cpp std::mutex _mutex{}; void allocate() { std::lock_guard lock(_mutex); // 自动加锁 // ... 操作 } // 自动解锁 ``` ## 8. 渲染表面与窗口 ### 8.1 render_surface 结构 ```cpp struct render_surface { platform::window window{}; // 平台窗口 surface surface{}; // 渲染表面 }; ``` ### 8.2 多窗口支持 TestRenderer 测试展示了多窗口渲染: ```cpp graphics::render_surface _surfaces[4]; // 支持 4 个窗口 // 创建多个渲染表面 for (u32 i{0}; i < _countof(_surfaces); ++i) { create_render_surface(_surfaces[i], info[i]); } ``` ### 8.3 全屏切换 通过 `WM_SYSCHAR` 消息处理 Alt+Enter: ```cpp if (wparam == VK_RETURN && (HIWORD(lparam) & KF_ALTDOWN)) { win.set_fullscreen(!win.is_fullscreen()); } ``` ## 9. 后续学习路径 ### 9.1 基础阶段 - [x] 完成设备创建和适配器枚举 - [x] 创建命令队列和命令列表 - [x] 描述符堆管理 - [ ] 实现交换链和后台缓冲区 - [ ] 渲染第一个三角形 ### 9.2 进阶阶段 - [ ] 根签名和管线状态对象 - [ ] 资源屏障和同步 - [ ] 常量缓冲区和着色器资源 ### 9.3 高级阶段 - [ ] 多线程渲染 - [ ] 资源绑定策略 - [ ] 动态资源管理 - [ ] 性能优化 ## 10. 参考资源 ### 10.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) ### 10.2 推荐书籍 - 《Introduction to 3D Game Programming with DirectX 12》 - 《Real-Time 3D Rendering with DirectX and HLSL》 ### 10.3 项目相关文档 - [Graphics渲染架构分析](./Graphics渲染架构分析.md) - [项目约定规范](./项目约定规范.md)