Files
DX12/docs/wiki/D3D12学习Wiki.md
SpecialX 95d8893182 feat(d3d12): 实现交换链与渲染表面管理
- 新增 d3d12_surface 类,管理交换链和渲染目标
- 实现三重缓冲后台缓冲区管理
- 添加视口和裁剪矩形配置
- 修复 GraphicsPlatformInterface.h 循环包含问题
- 添加完整的中文 Doxygen 注释
- 更新 D3D12 学习 Wiki,添加交换链章节
2026-03-31 11:12:11 +08:00

22 KiB
Raw Blame History

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. DXGIDirectX Graphics Infrastructure

2.1 DXGI 的作用

DXGI 是 DirectX 与图形硬件之间的抽象层,负责:

  • 枚举显示适配器
  • 管理显示输出
  • 创建交换链
  • 处理全屏/窗口切换

2.2 关键接口

IDXGIFactory6    // DXGI 工厂,创建其他 DXGI 对象
IDXGIAdapter4    // 代表物理 GPU 适配器
IDXGIOutput      // 代表显示器输出
IDXGISwapChain   // 交换链接口

2.3 项目中的使用

D3D12CommonHeader.h 中引入了 dxgi_6.h

#include <dxgi_6.h>  // 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 已实现完整的设备初始化:

namespace {
    ID3D12Device8* main_device{ nullptr };
    IDXGIFactory7* dxgi_factory{ nullptr };
}

bool initialize() {
    // 1. 启用调试层 (DEBUG 模式)
    // 2. 创建 DXGI 工厂
    // 3. 枚举并选择 GPU 适配器
    // 4. 获取最高特性级别
    // 5. 创建 D3D12 设备
    // 6. 配置信息队列 (DEBUG 模式)
}

3.3 调试宏

项目定义了两个重要的调试宏:

// 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

COMComponent Object Model是微软的组件对象模型D3D12 对象都是 COM 对象。

4.2 智能指针

项目使用 WRL 库的 ComPtr 管理 COM 对象生命周期:

#include <wrl.h>  // 提供 Microsoft::WRL::ComPtr

// 使用示例
Microsoft::WRL::ComPtr<ID3D12Device8> device;

4.3 ComPtr 的优势

  • 自动引用计数:无需手动 AddRef/Release
  • 异常安全:即使发生异常也能正确释放
  • 代码简洁:减少内存管理代码

5. 平台抽象设计

5.1 设计理念

项目采用平台抽象层设计,将图形 API 的差异封装在统一接口后:

┌────────────────────────────────────┐
│         上层渲染代码                │
│   graphics::initialize(platform)   │
└──────────────┬─────────────────────┘
               │
               ▼
┌────────────────────────────────────┐
│      platform_interface            │
│   - initialize()                   │
│   - shutdown()                     │
└──────────────┬─────────────────────┘
               │
    ┌──────────┼──────────┐
    ▼          ▼          ▼
┌───────┐  ┌───────┐  ┌───────┐
│ D3D12 │  │Vulkan │  │OpenGL │
└───────┘  └───────┘  └───────┘

5.2 接口绑定机制

// 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 类

项目实现了命令队列管理类,支持多帧缓冲渲染:

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 多帧缓冲原理

constexpr u32 frame_buffer_count{ 3 };

采用三重缓冲设计:

  • CPU 提前录制命令
  • GPU 异步执行
  • 最大化硬件利用率

6.3 帧索引轮转

_frame_index = (_frame_index + 1) % frame_buffer_count;

环形缓冲区管理帧资源,确保 CPU 不会超前 GPU 超过 3 帧。

6.4 Fence 同步机制

项目实现了 Fence围栏同步确保 CPU-GPU 帧同步:

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 发送信号
// 帧结束信号
++_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 结构

项目封装了描述符句柄:

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 类

class descriptor_heap
{
    bool initialize(u32 capacity, bool is_shader_visible);
    descriptor_handle allocate();  // 分配描述符
    void free(descriptor_handle);  // 释放描述符
    
private:
    ID3D12DescriptorHeap* _heap;
    std::unique_ptr<u32[]> _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 栈式索引管理

核心数据结构

// 空闲索引栈(预分配数组)
std::unique_ptr<u32[]> _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                │
└─────────────────────────────────────┘

分配算法

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)

释放算法(延迟释放)

// 释放时不立即回收,放入延迟队列
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 已完成使用

数据结构

// 每帧一个延迟释放队列
utl::vector<u32> _deferred_free_indices[frame_buffer_count]{};

// 全局延迟释放资源队列(用于 COM 对象)
utl::vector<IUnknown*> 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 线程安全

描述符堆可能被多线程并发访问,使用互斥锁保护:

std::mutex _mutex{};

void allocate() {
    std::lock_guard lock(_mutex);  // 自动加锁
    // ... 操作
}  // 自动解锁

8. 交换链Swap Chain

8.1 什么是交换链?

交换链是 DXGI 提供的机制,用于管理前后缓冲区的交换,实现流畅的画面显示。

┌─────────────────────────────────────────────────────────────┐
│                      交换链工作原理                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌──────────┐  ┌──────────┐  ┌──────────┐                │
│   │ 后台缓冲 │  │ 后台缓冲 │  │ 后台缓冲 │                │
│   │    0     │  │    1     │  │    2     │                │
│   └────┬─────┘  └────┬─────┘  └────┬─────┘                │
│        │             │             │                       │
│        └─────────────┼─────────────┘                       │
│                      │                                     │
│                      ▼                                     │
│              ┌──────────────┐                              │
│              │   Present()  │                              │
│              └──────┬───────┘                              │
│                     │                                      │
│                     ▼                                      │
│              ┌──────────────┐                              │
│              │  前台缓冲    │  ───► 显示器                 │
│              └──────────────┘                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

8.2 三重缓冲

项目使用三重缓冲(frame_buffer_count = 3

特性 说明
减少撕裂 前台缓冲独立于后台缓冲,避免部分更新
提高并行性 CPU 可提前录制多帧命令
平滑帧率 缓冲区平滑帧时间波动

8.3 d3d12_surface 类

class d3d12_surface
{
public:
    // 创建交换链
    void create_swap_chain(IDXGIFactory7* factory, 
                          ID3D12CommandQueue* cmd_queue, 
                          DXGI_FORMAT format);
    
    // 呈现当前帧
    void present() const;
    
    // 调整大小
    void resize();
    
    // 获取当前后台缓冲区
    ID3D12Resource* back_buffer() const;
    descriptor_handle rtv() const;
    
private:
    IDXGISwapChain4* _swap_chain;
    render_target_data _render_target_data[frame_buffer_count];
    D3D12_VIEWPORT _viewport;
    D3D12_RECT _scissor_rect;
};

8.4 交换链创建流程

void create_swap_chain(...)
{
    // 1. 配置描述
    DXGI_SWAP_CHAIN_DESC1 desc{};
    desc.BufferCount = frame_buffer_count;
    desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    desc.Width = window.width();
    desc.Height = window.height();
    desc.SampleDesc.Count = 1;  // 禁用 MSAA
    
    // 2. 创建交换链
    factory->CreateSwapChainForHwnd(cmd_queue, hwnd, &desc, ...);
    
    // 3. 禁用 Alt+Enter由应用处理
    factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER);
    
    // 4. 创建渲染目标视图
    for(u32 i = 0; i < frame_buffer_count; ++i)
    {
        _swap_chain->GetBuffer(i, &resource);
        device->CreateRenderTargetView(resource, &desc, rtv.cpu);
    }
}

8.5 视口与裁剪矩形

// 视口:定义光栅化区域
D3D12_VIEWPORT viewport{
    .TopLeftX = 0.0f,
    .TopLeftY = 0.0f,
    .Width = (float)width,
    .Height = (float)height,
    .MinDepth = 0.0f,
    .MaxDepth = 1.0f
};

// 裁剪矩形:定义像素输出区域
D3D12_RECT scissor_rect{0, 0, width, height};

9. 渲染表面与窗口

9.1 render_surface 结构

struct render_surface {
    platform::window window{};  // 平台窗口
    surface surface{};          // 渲染表面
};

9.2 多窗口支持

TestRenderer 测试展示了多窗口渲染:

graphics::render_surface _surfaces[4];  // 支持 4 个窗口

// 创建多个渲染表面
for (u32 i{0}; i < _countof(_surfaces); ++i) {
    create_render_surface(_surfaces[i], info[i]);
}

9.3 全屏切换

通过 WM_SYSCHAR 消息处理 Alt+Enter

if (wparam == VK_RETURN && (HIWORD(lparam) & KF_ALTDOWN)) {
    win.set_fullscreen(!win.is_fullscreen());
}

10. 后续学习路径

10.1 基础阶段

  • 完成设备创建和适配器枚举
  • 创建命令队列和命令列表
  • 描述符堆管理
  • 实现交换链和后台缓冲区
  • 渲染第一个三角形

10.2 进阶阶段

  • 根签名和管线状态对象
  • 资源屏障和同步
  • 常量缓冲区和着色器资源

10.3 高级阶段

  • 多线程渲染
  • 资源绑定策略
  • 动态资源管理
  • 性能优化

11. 参考资源

11.1 官方文档

11.2 推荐书籍

  • 《Introduction to 3D Game Programming with DirectX 12》
  • 《Real-Time 3D Rendering with DirectX and HLSL》

11.3 项目相关文档