DirectX 12 交换链教程:从代码到概念详解
本教程旨在深入解析 DirectX 12 (D3D12) 中交换链 (Swap Chain) 的实现,并以提供的 C++ 代码 (d3d12_surface
类) 为例,进行详细的讲解。我们将从交换链的基本概念出发,逐步分析代码结构,并补充代码中为了完整性可能缺失的部分,最终帮助读者全面掌握 D3D12 交换链的创建、使用和管理。
1. 什么是交换链 (Swap Chain)?
在现代图形渲染管线中,为了实现流畅的动画效果,通常采用双缓冲或三缓冲技术。交换链正是实现这种缓冲技术的关键组件。简单来说,交换链是由多个缓冲区(通常是后缓冲区和前缓冲区)组成的链表,用于存储渲染结果并在屏幕上显示。
后缓冲区 (Back Buffer): 渲染命令在此缓冲区中执行,GPU 在这里绘制下一帧的图像。
前缓冲区 (Front Buffer): 显示设备(如显示器)正在读取并显示的缓冲区。
交换链的核心操作是交换 (Present)。当后缓冲区完成渲染后,交换链会将后缓冲区的内容与前缓冲区进行交换,从而将新渲染的帧显示到屏幕上。这个过程通常与显示器的垂直同步信号 (VSync) 同步,以避免画面撕裂,并提供更稳定的视觉体验。
2. 代码结构概览:d3d12_surface
类
提供的代码定义了一个名为 d3d12_surface
的 C++ 类,它封装了 D3D12 交换链的创建和管理逻辑。以下是该类的主要组成部分:
头文件 (
D3D12Surface.h
):类定义:
d3d12_surface
类的声明,包括构造函数、析构函数、公有方法(如create_swap_chain
,present
,resize
)和私有成员变量及方法。成员变量:
_swap_chain
: 指向IDXGISwapChain4
接口的指针,代表交换链对象。_render_target_data
: 一个结构体数组,用于存储后缓冲区资源 (ID3D12Resource
) 和对应的渲染目标视图 (RTV) 句柄 (descriptor_handle
)._window
:platform::window
类型的对象,代表渲染目标窗口。_current_bb_index
: 当前后缓冲区索引。_allow_tearing
,_present_flags
: 用于控制 tearing 特性的标志。_viewport
,_scissor_rect
: 定义渲染视口和裁剪矩形。
render_target_data
结构体: 用于组织后缓冲区资源和 RTV 句柄。
源文件 (
D3D12Surface.cpp
):create_swap_chain
方法: 负责创建IDXGISwapChain4
交换链对象,并初始化后缓冲区和 RTV。present
方法: 执行交换链的 Present 操作,将渲染结果显示到屏幕。resize
方法: (代码中为空) 本应处理窗口大小改变时的交换链和相关资源调整。release
方法: 释放交换链和后缓冲区资源。finalize
方法: 创建后缓冲区的渲染目标视图 (RTVs),并设置视口和裁剪矩形。匿名命名空间: 包含辅助函数,如
to_non_srgb
,用于转换颜色格式。
3. 核心方法详解
3.1 create_swap_chain
方法
此方法是创建交换链的关键。它接受 IDXGIFactory7
接口指针、ID3D12CommandQueue
接口指针和 DXGI_FORMAT
格式作为参数。
C++
void
d3d12_surface::create_swap_chain(IDXGIFactory7* factory, ID3D12CommandQueue* cmd_queue, DXGI_FORMAT format)
{
assert(factory && cmd_queue);
release(); // 释放之前可能存在的交换链
// 检查是否支持 tearing 特性 (可选,用于无 VSync 时减少延迟)
if (SUCCEEDED(factory->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &_allow_tearing, sizeof(u32)) && _allow_tearing))
{
_present_flags = DXGI_PRESENT_ALLOW_TEARING;
}
else
{
_allow_tearing = _present_flags = 0; // 不支持 tearing 或检查失败,禁用
}
DXGI_SWAP_CHAIN_DESC1 desc{}; // 交换链描述结构体
desc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED; // Alpha 模式,通常设置为 unspecified
desc.BufferCount = frame_buffer_count; // 后缓冲区数量,通常为 2 (双缓冲) 或 3 (三缓冲),frame_buffer_count 需要在别处定义,例如 #define frame_buffer_count 2
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // 后缓冲区用途为渲染目标输出
desc.Flags = _allow_tearing ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0; // tearing 支持标志
desc.Format = to_non_srgb(format); // 后缓冲区格式,转换为非 SRGB 格式
desc.Height = _window.height(); // 窗口高度
desc.Width = _window.width(); // 窗口宽度
desc.SampleDesc.Count = 1; // 多重采样设置,1 为禁用 MSAA
desc.SampleDesc.Quality = 0; // 多重采样质量级别
desc.Scaling = DXGI_SCALING_STRETCH; // 缩放模式,这里设置为拉伸
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; // 交换效果,Flip 模型,discard 模式
desc.Stereo = false; // 立体渲染,禁用
IDXGISwapChain1* swap_chain; // 临时交换链接口指针
HWND hwnd{ (HWND)_window.handle() }; // 获取窗口句柄
DXCall(factory->CreateSwapChainForHwnd(cmd_queue, hwnd, &desc, nullptr, nullptr, &swap_chain)); // 创建针对 HWND 的交换链
DXCall(factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER)); // 禁用 Alt+Enter 全屏切换,防止与 Flip 模型交换链冲突
DXCall(swap_chain->QueryInterface(IID_PPV_ARGS(&_swap_chain))); // 查询 IDXGISwapChain4 接口
core::release(swap_chain); // 释放临时 IDXGISwapChain1 指针
_current_bb_index = _swap_chain->GetCurrentBackBufferIndex(); // 获取当前后缓冲区索引
for (u32 i{ 0 }; i < frame_buffer_count; ++i)
{
_render_target_data[i].rtv = core::rtv_heap().allocate(); // 从 RTV 堆中分配描述符句柄
}
finalize(); // 调用 finalize 方法完成后续初始化
}
代码解释:
断言和资源释放:
assert(factory && cmd_queue);
确保传入的工厂和命令队列指针有效。release();
释放之前可能存在的交换链资源,避免资源泄漏。Tearing 支持检测: 代码尝试检测是否支持 tearing 特性,如果支持则设置
_present_flags = DXGI_PRESENT_ALLOW_TEARING;
。Tearing 允许在不等待 VSync 的情况下 Present,可以减少延迟,但可能导致画面撕裂。此处代码中_allow_tearing = _present_flags = 0;
实际上禁用了 tearing 特性,可能需要根据需求修改。DXGI_SWAP_CHAIN_DESC1
结构体: 此结构体描述了交换链的各种属性,例如缓冲区数量、格式、用途、缩放模式、交换效果等。BufferCount
: 定义交换链中缓冲区的数量。通常设置为 2 (双缓冲) 或 3 (三缓冲)。需要在代码的其他地方定义frame_buffer_count
常量,例如#define frame_buffer_count 2
.Format
: 后缓冲区使用的像素格式。to_non_srgb(format)
函数用于将 SRGB 格式转换为非 SRGB 格式,因为渲染目标通常使用线性颜色空间。SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
: 使用 Flip 模式和 Discard 交换效果。Flip 模型是现代 D3D12 应用推荐的交换链模型,通常更高效。DISCARD
效果表示当 Present 时,前缓冲区内容将被丢弃,驱动程序可以更灵活地管理内存。
创建交换链:
factory->CreateSwapChainForHwnd
函数使用提供的描述结构体、命令队列、窗口句柄等参数创建IDXGISwapChain1
接口的交换链对象。禁用 Alt+Enter:
factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER);
禁用 Alt+Enter 全屏切换,因为 Flip 模型交换链通常需要程序自行处理全屏切换。查询
IDXGISwapChain4
接口:swap_chain->QueryInterface(IID_PPV_ARGS(&_swap_chain));
从IDXGISwapChain1
查询更高级的IDXGISwapChain4
接口。IDXGISwapChain4
提供了更多功能,例如 GetCurrentBackBufferIndex2。获取当前后缓冲区索引:
_swap_chain->GetCurrentBackBufferIndex();
获取当前正在使用的后缓冲区索引,用于后续渲染目标操作。分配 RTV 句柄: 循环
frame_buffer_count
次,为每个后缓冲区从 RTV 堆中分配一个描述符句柄_render_target_data[i].rtv = core::rtv_heap().allocate();
。需要确保代码中存在core::rtv_heap()
和descriptor_handle
的定义和实现,以及 RTV 堆的初始化和管理逻辑。调用
finalize()
: 调用finalize
方法执行后续的初始化操作,例如创建 RTV 和设置视口。
3.2 finalize
方法
finalize
方法在 create_swap_chain
方法的末尾被调用,用于完成交换链的初始化工作。
C++
void
d3d12_surface::finalize()
{
// Create RTVS for back-buffers
for (u32 i{ 0 }; i < frame_buffer_count; ++i)
{
render_target_data& data{ _render_target_data[i] };
assert(!data.resource);
DXCall(_swap_chain->GetBuffer(i, IID_PPV_ARGS(&data.resource))); // 获取后缓冲区资源接口
D3D12_RENDER_TARGET_VIEW_DESC desc{}; // RTV 描述结构体
desc.Format = core::default_render_target_format(); // RTV 格式,使用默认渲染目标格式,需要在别处定义 core::default_render_target_format()
desc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D; // RTV 维度为 2D 纹理
core::device()->CreateRenderTargetView(data.resource, &desc, data.rtv.cpu); // 创建 RTV
}
DXGI_SWAP_CHAIN_DESC desc{};
DXCall(_swap_chain->GetDesc(&desc)); // 获取交换链描述信息
const u32 width{ desc.BufferDesc.Width }; // 从描述信息中获取宽度
const u32 height{ desc.BufferDesc.Height }; // 从描述信息中获取高度
assert(_window.height() == height && _window.width() == width); // 窗口尺寸与交换链描述尺寸一致性检查
_viewport.TopLeftX = 0.f; // 视口左上角 X 坐标
_viewport.TopLeftY = 0.f; // 视口左上角 Y 坐标
_viewport.Width = (float)width; // 视口宽度
_viewport.Height = (float)height; // 视口高度
_viewport.MinDepth = 0.f; // 视口最小深度
_viewport.MaxDepth = 1.f; // 视口最大深度
_scissor_rect = { 0,0,(s32)width, (s32)height }; // 裁剪矩形,设置为整个后缓冲区
}
代码解释:
创建 RTVs: 循环
frame_buffer_count
次,为每个后缓冲区创建渲染目标视图 (RTV)。_swap_chain->GetBuffer(i, IID_PPV_ARGS(&data.resource));
: 获取交换链中索引为i
的后缓冲区资源接口ID3D12Resource
。D3D12_RENDER_TARGET_VIEW_DESC desc{};
: 创建 RTV 描述结构体。desc.Format = core::default_render_target_format();
: 设置 RTV 格式。core::default_render_target_format()
需要在代码的其他地方定义,通常返回与交换链后缓冲区格式一致的格式。desc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
: 指定 RTV 维度为 2D 纹理。core::device()->CreateRenderTargetView(data.resource, &desc, data.rtv.cpu);
: 使用 D3D12 设备创建 RTV,并将描述符句柄存储到data.rtv.cpu
中。core::device()
需要在代码的其他地方定义,返回ID3D12Device
接口指针。
获取交换链描述信息:
_swap_chain->GetDesc(&desc));
获取交换链的描述信息,用于获取后缓冲区的宽度和高度。设置视口 (Viewport): 初始化
_viewport
结构体,定义渲染视口。视口通常设置为覆盖整个后缓冲区。设置裁剪矩形 (Scissor Rect): 初始化
_scissor_rect
结构体,定义裁剪矩形。裁剪矩形也设置为整个后缓冲区,表示不进行裁剪。尺寸一致性检查:
assert(_window.height() == height && _window.width() == width);
断言窗口的尺寸与交换链描述中的尺寸一致,确保交换链创建成功且尺寸正确。
3.3 present
方法
present
方法负责将渲染结果呈现到屏幕上。
C++
void
d3d12_surface::present()const
{
assert(_swap_chain);
DXCall(_swap_chain->Present(0, _present_flags)); // 执行 Present 操作
_current_bb_index = _swap_chain->GetCurrentBackBufferIndex(); // 更新当前后缓冲区索引
}
代码解释:
断言:
assert(_swap_chain);
确保交换链对象已创建。执行 Present:
_swap_chain->Present(0, _present_flags);
调用IDXGISwapChain4::Present
方法执行 Present 操作。第一个参数
0
通常设置为0
,表示同步间隔 (sync interval)。0
表示不等待 VSync,立即 Present (如果 tearing enabled 可能导致撕裂)。1
表示等待一次 VSync。第二个参数
_present_flags
传递 Present 标志,例如DXGI_PRESENT_ALLOW_TEARING
(如果 tearing enabled)。
更新后缓冲区索引:
_current_bb_index = _swap_chain->GetCurrentBackBufferIndex();
在 Present 后,需要更新_current_bb_index
以指向下一个可用的后缓冲区,为下一帧的渲染做准备。
3.4 resize
方法
C++
void
d3d12_surface::resize()
{
}
代码解释:
当前为空:
resize
方法在提供的代码中是空的。这是一个缺失的部分,需要补充完整的功能。
resize
方法的完整实现应该包含以下步骤:
释放现有资源: 在调整大小之前,需要先释放当前交换链和后缓冲区资源,避免资源泄漏。可以调用
release()
方法来完成。调整后缓冲区尺寸: 获取新的窗口尺寸。
调整交换链尺寸: 调用
IDXGISwapChain4::ResizeBuffers
方法来调整交换链的后缓冲区尺寸。需要传入新的缓冲区数量、宽度、高度和格式等参数。重新创建 RTVs: 调整缓冲区尺寸后,需要重新获取后缓冲区资源并创建新的 RTVs。可以再次调用
finalize()
方法来完成 RTV 的创建和视口、裁剪矩形的设置。
补充完整的 resize
方法代码示例 (需要根据实际 core
命名空间下的资源管理方式进行调整):
C++
void
d3d12_surface::resize()
{
if (_swap_chain == nullptr) return; // 如果交换链未创建,则直接返回
release(); // 释放现有资源
DXGI_SWAP_CHAIN_DESC desc{};
DXCall(_swap_chain->GetDesc(&desc));
desc.BufferDesc.Width = _window.width(); // 获取新的窗口宽度
desc.BufferDesc.Height = _window.height(); // 获取新的窗口高度
HRESULT hr = _swap_chain->ResizeBuffers(
frame_buffer_count, // 保持缓冲区数量不变
desc.BufferDesc.Width,
desc.BufferDesc.Height,
desc.BufferDesc.Format,
desc.Flags
);
if (FAILED(hr))
{
// ResizeBuffers 可能失败,例如窗口最小化时,需要根据实际情况处理错误
// 这里简单处理,重新创建 swap chain
IDXGIFactory7* factory = nullptr; // 需要获取 IDXGIFactory7 指针,例如从 core 获取
ID3D12CommandQueue* cmd_queue = nullptr; // 需要获取 ID3D12CommandQueue 指针,例如从 core 获取
DXGI_FORMAT format = core::default_render_target_format(); // 获取默认渲染目标格式
// ** 假设 core::get_factory() 和 core::get_command_queue() 可以获取 factory 和 cmd_queue **
factory = core::get_factory();
cmd_queue = core::get_command_queue();
if (factory && cmd_queue)
{
create_swap_chain(factory, cmd_queue, format); // 重新创建 swap chain
return; // Resize 成功后直接返回
} else {
// 错误处理,例如日志输出或抛出异常
assert(false && "Failed to get factory or command queue for resize.");
return;
}
}
for (u32 i{ 0 }; i < frame_buffer_count; ++i)
{
_render_target_data[i].rtv = core::rtv_heap().allocate(); // 重新分配 RTV 句柄
}
finalize(); // 重新 finalize
}
注意: 上述 resize
方法代码示例中,需要根据你的 core
命名空间下的资源管理方式,正确获取 IDXGIFactory7
和 ID3D12CommandQueue
指针,以及默认渲染目标格式。 错误处理部分也需要根据实际情况进行完善。
3.5 release
方法
release
方法负责释放交换链和后缓冲区相关的资源。
C++
void
d3d12_surface::release()
{
for (u32 i{ 0 }; i < frame_buffer_count; ++i)
{
render_target_data& data{ _render_target_data[i]};
core::release(data.resource); // 释放后缓冲区资源
core::rtv_heap().free(data.rtv); // 释放 RTV 描述符句柄,归还到 RTV 堆中
}
core::release(_swap_chain); // 释放交换链对象
}
代码解释:
释放后缓冲区资源和 RTVs: 循环
frame_buffer_count
次,释放每个后缓冲区资源 (data.resource
) 和对应的 RTV 描述符句柄 (data.rtv
)。core::release(data.resource);
: 释放后缓冲区资源。core::release
需要在代码的其他地方定义,用于安全释放 COM 接口指针。core::rtv_heap().free(data.rtv);
: 将 RTV 描述符句柄归还到 RTV 描述符堆中,以便后续复用。需要确保core::rtv_heap()
返回的是 RTV 描述符堆管理器的实例,并且实现了free
方法。
释放交换链:
core::release(_swap_chain);
释放IDXGISwapChain4
接口指针,销毁交换链对象。
4. 总结与后续步骤
通过 d3d12_surface
类,我们了解了 D3D12 交换链的创建、Present、Resize 和 Release 的基本流程。 交换链是 DirectX 12 渲染管线中至关重要的组成部分,它负责将渲染结果最终显示到屏幕上。
后续学习和实践方向:
完善
resize
方法: 根据实际应用场景,完善resize
方法的错误处理和资源重新创建逻辑。理解双缓冲和三缓冲: 深入学习双缓冲和三缓冲的工作原理,以及它们对性能和延迟的影响。
Tearing 特性: 研究 tearing 特性的使用场景和优缺点,并根据需求决定是否启用。
Flip 模型交换链: 深入了解 Flip 模型交换链的特性和优势。
交换链格式选择: 学习如何选择合适的交换链格式,例如
DXGI_FORMAT_R8G8B8A8_UNORM
或DXGI_FORMAT_R8G8B8A8_UNORM_SRGB
。结合渲染管线: 将交换链集成到完整的 D3D12 渲染管线中,实现实际的渲染应用。
希望本教程能够帮助您理解 DirectX 12 交换链的核心概念和代码实现。 通过实践和深入学习,您将能够更好地掌握 D3D12 图形编程技术。