核心变更: - 实现 d3d12_render_texture 类,支持多 Mip 级别 RTV - 实现 d3d12_depth_buffer 类,支持 DSV 和 SRV 双视图 - 为所有纹理类添加析构函数,确保资源自动释放 - 深度缓冲区格式转换处理(D32_FLOAT -> R32_TYPELESS) 文档完善: - 新增变更记录文档 - 更新 D3D12 学习 Wiki,添加渲染目标纹理和深度缓冲区章节
43 KiB
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 关键接口
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?
COM(Component 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);
};
同步流程:
begin_frame()- 检查 GPU 是否完成当前帧,未完成则等待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); // 自动加锁
// ... 操作
} // 自动解锁
7.9 着色器资源视图(SRV)
CreateShaderResourceView 函数
void CreateShaderResourceView(
ID3D12Resource *pResource,
const D3D12_SHADER_RESOURCE_VIEW_DESC *pDesc,
D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor
);
| 参数 | 说明 |
|---|---|
pResource |
要创建 SRV 的 GPU 资源指针(必须是有效资源,不能为 nullptr) |
pDesc |
视图描述符,指定格式、维度、Mip 级别范围等。为 nullptr 时使用资源默认属性 |
DestDescriptor |
SRV 描述符堆中 CPU 描述符句柄的位置 |
空描述符初始化
当 pDesc 为 nullptr 时,视图描述符默认使用资源本身的格式和全部子资源:
// 使用默认视图属性
device->CreateShaderResourceView(texture, nullptr, rtv_handle);
// 等价于显式指定完整描述
D3D12_SHADER_RESOURCE_VIEW_DESC desc{};
desc.Format = texture->GetDesc().Format;
desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
desc.Texture2D.MostDetailedMip = 0;
desc.Texture2D.MipLevels = texture->GetDesc().MipLevels;
device->CreateShaderResourceView(texture, &desc, rtv_handle);
多视图支持
同一个资源可以创建多个不同的 SRV:
// 原始格式视图
device->CreateShaderResourceView(texture, nullptr, srv_handle0);
// 不同格式视图(如 R32_FLOAT 作为 RGBA 视图)
D3D12_SHADER_RESOURCE_VIEW_DESC rgba_desc{};
rgba_desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
// ...
device->CreateShaderResourceView(texture, &rgba_desc, srv_handle1);
// 特定 Mip 切片视图
D3D12_SHADER_RESOURCE_VIEW_DESC mip_desc{};
mip_desc.Texture2D.MostDetailedMip = 2;
mip_desc.Texture2D.MipLevels = 1;
// ...
device->CreateShaderResourceView(texture, &mip_desc, srv_handle2);
7.10 资源创建方式
D3D12 提供三种资源创建函数,对应不同的堆管理策略:
函数对比
| 函数 | 堆类型 | 说明 |
|---|---|---|
CreateCommittedResource |
隐式堆 | D3D12 自动分配堆,资源直接映射。适用于大多数常规资源 |
CreatePlacedResource |
显式堆 | 资源放置在用户创建的堆的特定偏移位置。用于精确控制内存布局 |
CreateReservedResource |
预留资源 | 仅预留虚拟地址,不提交物理内存。用于稀疏资源,支持流式加载 |
CreateCommittedResource(最常用)
// 系统自动管理堆,最简单的方式
D3D12_HEAP_PROPERTIES heap_props{
.Type = D3D12_HEAP_TYPE_DEFAULT
};
D3D12_RESOURCE_DESC resource_desc{
.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D,
.Width = width,
.Height = height,
.Format = DXGI_FORMAT_R8G8B8A8_UNORM,
// ...
};
ID3D12Resource* texture;
device->CreateCommittedResource(
&heap_props,
D3D12_HEAP_FLAG_NONE,
&resource_desc,
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr,
IID_PPV_ARGS(&texture)
);
CreatePlacedResource(精确控制)
// 先创建堆
ID3D12Heap* heap;
D3D12_HEAP_DESC heap_desc{
.SizeInBytes = heap_size,
.Properties = { .Type = D3D12_HEAP_TYPE_DEFAULT },
// ...
};
device->CreateHeap(&heap_desc, IID_PPV_ARGS(&heap));
// 在堆的特定偏移放置资源
ID3D12Resource* texture;
device->CreatePlacedResource(
heap,
0, // 偏移量(必须满足对齐要求)
&resource_desc,
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr,
IID_PPV_ARGS(&texture)
);
适用场景:
- 资源复用(同一堆位置放置不同资源)
- 精确内存对齐
- 自定义内存管理
CreateReservedResource(稀疏资源)
// 仅预留虚拟地址,不分配物理内存
ID3D12Resource* sparse_texture;
device->CreateReservedResource(
&resource_desc,
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(&sparse_texture)
);
// 后续按需提交物理内存页面
// 使用 UpdateTileMappings 和 MakeResident
适用场景:
- 超大纹理(如地形纹理)按需加载
- 流式资源管理
- 虚拟纹理系统
选择建议
┌─────────────────────────────────────────────────────────────┐
│ 资源创建方式选择 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 常规纹理/缓冲区? │
│ │ │
│ ├── 是 ──► CreateCommittedResource │
│ │ (简单、自动管理) │
│ │ │
│ └── 否 ──► 需要精确内存控制? │
│ │ │
│ ├── 是 ──► CreatePlacedResource │
│ │ (资源复用、对齐控制) │
│ │ │
│ └── 否 ──► 超大资源按需加载? │
│ │ │
│ ├── 是 ──► │
│ │ CreateReservedResource│
│ │ (稀疏资源) │
│ │ │
│ └── 否 ──► 重新评估需求 │
│ │
└─────────────────────────────────────────────────────────────┘
7.11 纹理资源类
d3d12_texture_init_info 结构
纹理初始化信息结构,统一管理三种资源创建方式的参数:
struct d3d12_texture_init_info
{
ID3D12Heap1* heap{nullptr}; // 显式堆(Placed Resource)
ID3D12Resource* resource{nullptr}; // 已有资源(直接使用)
D3D12_SHADER_RESOURCE_VIEW_DESC* srv_desc{nullptr}; // SRV 描述(nullptr 使用默认)
D3D12_RESOURCE_DESC* desc{nullptr}; // 资源描述
D3D12_RESOURCE_ALLOCATION_INFO1 allocation_info{}; // 分配信息(偏移量)
D3D12_RESOURCE_STATES initial_state{}; // 初始状态
D3D12_CLEAR_VALUE clear_value{}; // 清除值(RTV/DSV)
};
d3d12_texture 类
基础纹理类,封装资源创建和 SRV 绑定:
class d3d12_texture
{
public:
constexpr static u32 max_mips{ 14 };
explicit d3d12_texture(d3d12_texture_init_info info);
// 移动语义
d3d12_texture(d3d12_texture&& o);
d3d12_texture& operator=(d3d12_texture&& o);
// 禁用拷贝
DISABLE_COPY(d3d12_texture);
void release();
ID3D12Resource* resource() const;
descriptor_handle srv() const;
private:
ID3D12Resource* _resource{nullptr};
descriptor_handle _srv;
};
资源创建优先级
┌─────────────────────────────────────────────────────────────┐
│ 纹理资源创建优先级 │
├─────────────────────────────────────────────────────────────┤
│ │
│ info.resource != nullptr ? │
│ │ │
│ ├── 是 ──► 直接使用已有资源 │
│ │ (适用于外部管理的资源) │
│ │ │
│ └── 否 ──► info.heap != nullptr ? │
│ │ │
│ ├── 是 ──► CreatePlacedResource │
│ │ (显式堆,精确控制) │
│ │ │
│ └── 否 ──► CreateCommittedResource │
│ (隐式堆,最常用) │
│ │
└─────────────────────────────────────────────────────────────┘
d3dx::heap_properties 辅助结构
提供常用的堆属性配置:
namespace XEngine::graphics::d3d12::d3dx
{
constexpr struct {
D3D12_HEAP_PROPERTIES default_heap{
D3D12_HEAP_TYPE_DEFAULT, // GPU 可读写,CPU 不可访问
D3D12_CPU_PAGE_PROPERTY_UNKNOWN,
D3D12_MEMORY_POOL_UNKNOWN,
0, // 单 GPU 系统
0
};
} heap_properties;
}
使用示例
// 创建默认纹理(Committed Resource)
D3D12_RESOURCE_DESC desc{
.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D,
.Width = 1024,
.Height = 1024,
.Format = DXGI_FORMAT_R8G8B8A8_UNORM,
// ...
};
d3d12_texture_init_info info{
.desc = &desc,
.initial_state = D3D12_RESOURCE_STATE_COPY_DEST,
};
d3d12_texture texture{info};
// 获取资源用于后续操作
ID3D12Resource* resource = texture.resource();
descriptor_handle srv = texture.srv();
7.12 渲染目标纹理(d3d12_render_texture)
功能说明
渲染目标纹理类,支持多 Mip 级别的渲染目标视图(RTV),用于离屏渲染:
class d3d12_render_texture
{
public:
explicit d3d12_render_texture(d3d12_texture_init_info info);
~d3d12_render_texture() { release(); }
void release();
u32 mip_count() const;
D3D12_CPU_DESCRIPTOR_HANDLE rtv(u32 mip) const; // 获取指定 Mip 的 RTV
descriptor_handle srv() const; // 获取 SRV
ID3D12Resource* resource() const;
private:
d3d12_texture _texture{};
descriptor_handle _rtv[d3d12_texture::max_mips]{}; // 每个 Mip 一个 RTV
u32 _mip_count{0};
};
多 Mip RTV 创建
d3d12_render_texture::d3d12_render_texture(d3d12_texture_init_info info)
: _texture(info)
{
_mip_count = resource()->GetDesc().MipLevels;
D3D12_RENDER_TARGET_VIEW_DESC desc{};
desc.Format = info.desc->Format;
desc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
desc.Texture2D.MipSlice = 0;
for(u32 i = 0; i < _mip_count; ++i)
{
_rtv[i] = rtv_heap.allocate();
device->CreateRenderTargetView(resource(), &desc, _rtv[i].cpu);
++desc.Texture2D.MipSlice;
}
}
使用场景
| 场景 | 说明 |
|---|---|
| 离屏渲染 | 将场景渲染到纹理而非屏幕 |
| 多级渐远纹理 | 生成 Mip Chain |
| 后处理 | 渲染结果作为后处理输入 |
7.13 深度缓冲区(d3d12_depth_buffer)
功能说明
深度缓冲区类,同时提供深度模板视图(DSV)和着色器资源视图(SRV):
class d3d12_depth_buffer
{
public:
explicit d3d12_depth_buffer(d3d12_texture_init_info info);
~d3d12_depth_buffer() { release(); }
void release();
D3D12_CPU_DESCRIPTOR_HANDLE dsv() const; // 深度模板视图
descriptor_handle srv() const; // 着色器资源视图
ID3D12Resource* resource() const;
private:
d3d12_texture _texture{};
descriptor_handle _dsv{};
};
格式转换处理
深度缓冲区需要特殊处理格式,以支持同时作为 DSV 和 SRV 使用:
┌─────────────────────────────────────────────────────────────┐
│ 深度缓冲区格式处理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户指定格式:DXGI_FORMAT_D32_FLOAT │
│ │ │
│ ▼ │
│ 资源格式:DXGI_FORMAT_R32_TYPELESS(无类型) │
│ │ │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ DSV 格式:D32_FLOAT SRV 格式:R32_FLOAT │
│ (深度测试用) (着色器采样用) │
│ │
└─────────────────────────────────────────────────────────────┘
构造实现
d3d12_depth_buffer::d3d12_depth_buffer(d3d12_texture_init_info info)
{
D3D12_SHADER_RESOURCE_VIEW_DESC srv_desc{};
if(info.desc->Format == DXGI_FORMAT_D32_FLOAT)
{
info.desc->Format = DXGI_FORMAT_R32_TYPELESS;
srv_desc.Format = DXGI_FORMAT_R32_FLOAT;
}
srv_desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srv_desc.Texture2D.MipLevels = 1;
info.srv_desc = &srv_desc;
_texture = d3d12_texture(info);
D3D12_DEPTH_STENCIL_VIEW_DESC dsv_desc{};
dsv_desc.Format = DXGI_FORMAT_D32_FLOAT;
dsv_desc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
_dsv = dsv_heap.allocate();
device->CreateDepthStencilView(resource(), &dsv_desc, _dsv.cpu);
}
使用场景
| 场景 | 说明 |
|---|---|
| 深度测试 | 作为 DSV 用于深度缓冲 |
| 阴影映射 | 作为 SRV 采样深度值 |
| SSAO | 采样深度重建位置 |
8. 交换链(Swap Chain)
8.1 什么是交换链?
交换链是 DXGI 提供的机制,用于管理前后缓冲区的交换,实现流畅的画面显示。
核心职责:
- 连接窗口与渲染管线:将渲染输出与显示器关联
- 管理后台缓冲区:分配和维护一组用于渲染的缓冲区
- 缓冲区翻转:通过
Present()实现前后缓冲区交换,将绘制内容显示到窗口
┌─────────────────────────────────────────────────────────────┐
│ 交换链工作原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 后台缓冲 │ │ 后台缓冲 │ │ 后台缓冲 │ │
│ │ 0 │ │ 1 │ │ 2 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Present() │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 前台缓冲 │ ───► 显示器 │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
8.2 交换链的职责边界
交换链只负责缓冲区的分配与翻转,以下操作需开发者显式完成:
| 操作 | 负责方 | 说明 |
|---|---|---|
| 缓冲区分配 | 交换链 | 创建指定数量的后台缓冲区 |
| 缓冲区翻转 | 交换链 | Present() 切换前后缓冲区 |
| 渲染目标绑定 | 开发者 | OMSetRenderTargets() 绑定 RTV |
| GPU 同步 | 开发者 | 使用 Fence 确保 GPU 完成渲染 |
| 状态转换 | 开发者 | 资源屏障管理缓冲区状态 |
| 窗口大小调整 | 开发者 | 调用 ResizeBuffers() 重新分配 |
8.3 标准渲染流程
┌─────────────────────────────────────────────────────────────┐
│ 交换链标准使用流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 获取当前后台缓冲区 │
│ ┌─────────────────────────────────────────┐ │
│ │ ID3D12Resource* buffer = swap_chain-> │ │
│ │ GetBuffer(current_index); │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 2. 创建并绑定渲染目标视图 │
│ ┌─────────────────────────────────────────┐ │
│ │ device->CreateRenderTargetView( │ │
│ │ buffer, nullptr, rtv_handle); │ │
│ │ cmd_list->OMSetRenderTargets( │ │
│ │ 1, &rtv_handle, nullptr); │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 3. 执行绘制命令(写入后台缓冲区) │
│ ┌─────────────────────────────────────────┐ │
│ │ cmd_list->ClearRenderTargetView(...); │ │
│ │ cmd_list->DrawInstanced(...); │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 4. 提交命令并同步 │
│ ┌─────────────────────────────────────────┐ │
│ │ cmd_queue->ExecuteCommandLists(...); │ │
│ │ // 等待 GPU 完成(Fence 同步) │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 5. 呈现(翻转缓冲区) │
│ ┌─────────────────────────────────────────┐ │
│ │ swap_chain->Present(sync_interval, 0); │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
8.5 三重缓冲
项目使用三重缓冲(frame_buffer_count = 3):
| 特性 | 说明 |
|---|---|
| 减少撕裂 | 前台缓冲独立于后台缓冲,避免部分更新 |
| 提高并行性 | CPU 可提前录制多帧命令 |
| 平滑帧率 | 缓冲区平滑帧时间波动 |
8.6 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.7 交换链创建流程
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.8 视口与裁剪矩形
// 视口:定义光栅化区域
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};
8.9 Surface 管理与 free_list
问题:Vector 扩容导致的资源重复释放
使用 utl::vector 管理 surface 时,扩容会触发元素移动:
vector 扩容流程:
1. 分配新内存块
2. 移动元素到新内存(默认移动是浅拷贝)
3. 析构旧位置的元素 → 调用 release()
4. 新位置的元素持有悬空指针 → 崩溃!
解决方案:使用 free_list
utl::free_list 是带槽位复用机制的容器:
// 定义 surface 集合类型
using surface_collection = utl::free_list<d3d12_surface>;
surface_collection surfaces;
// 创建 surface
surface create_surface(platform::window window)
{
surfaces.emplace_back(window);
surface_id id{ (u32)surfaces.size() - 1 };
surfaces[id].create_swap_chain(...);
return surface{id};
}
// 删除 surface(槽位被回收)
void remove_surface(surface_id id)
{
gfx_command.flush();
surfaces.remove(id);
}
free_list 数据结构
初始状态:
_array: [ 空 | 空 | 空 | 空 ]
_next_free_index = invalid_id
添加元素 A、B、C 后:
_array: [ A | B | C | 空 ]
_next_free_index = invalid_id
删除元素 B 后:
_array: [ A | ->2 | C | 空 ] // 槽位1存储下一个空闲索引
_next_free_index = 1
添加新元素 D:
_array: [ D | ->2 | C | 空 ] // 复用槽位1
_next_free_index = 2
free_list vs vector
| 特性 | free_list | vector |
|---|---|---|
| 删除复杂度 | O(1) | O(n) |
| 索引稳定性 | 删除后可复用 | 删除后失效 |
| 内存管理 | 槽位复用 | 可能扩容移动 |
| 适用场景 | 资源句柄管理 | 顺序数据存储 |
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》