本教程旨在深入解析 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 方法完成后续初始化

}

代码解释:

  1. 断言和资源释放: assert(factory && cmd_queue); 确保传入的工厂和命令队列指针有效。 release(); 释放之前可能存在的交换链资源,避免资源泄漏。

  2. Tearing 支持检测: 代码尝试检测是否支持 tearing 特性,如果支持则设置 _present_flags = DXGI_PRESENT_ALLOW_TEARING;。Tearing 允许在不等待 VSync 的情况下 Present,可以减少延迟,但可能导致画面撕裂。此处代码中 _allow_tearing = _present_flags = 0; 实际上禁用了 tearing 特性,可能需要根据需求修改。

  3. 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 时,前缓冲区内容将被丢弃,驱动程序可以更灵活地管理内存。

  4. 创建交换链: factory->CreateSwapChainForHwnd 函数使用提供的描述结构体、命令队列、窗口句柄等参数创建 IDXGISwapChain1 接口的交换链对象。

  5. 禁用 Alt+Enter: factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER); 禁用 Alt+Enter 全屏切换,因为 Flip 模型交换链通常需要程序自行处理全屏切换。

  6. 查询 IDXGISwapChain4 接口: swap_chain->QueryInterface(IID_PPV_ARGS(&_swap_chain));IDXGISwapChain1 查询更高级的 IDXGISwapChain4 接口。IDXGISwapChain4 提供了更多功能,例如 GetCurrentBackBufferIndex2。

  7. 获取当前后缓冲区索引: _swap_chain->GetCurrentBackBufferIndex(); 获取当前正在使用的后缓冲区索引,用于后续渲染目标操作。

  8. 分配 RTV 句柄: 循环 frame_buffer_count 次,为每个后缓冲区从 RTV 堆中分配一个描述符句柄 _render_target_data[i].rtv = core::rtv_heap().allocate();需要确保代码中存在 core::rtv_heap()descriptor_handle 的定义和实现,以及 RTV 堆的初始化和管理逻辑。

  9. 调用 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 }; // 裁剪矩形,设置为整个后缓冲区

}

代码解释:

  1. 创建 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 接口指针。

  2. 获取交换链描述信息: _swap_chain->GetDesc(&desc)); 获取交换链的描述信息,用于获取后缓冲区的宽度和高度。

  3. 设置视口 (Viewport): 初始化 _viewport 结构体,定义渲染视口。视口通常设置为覆盖整个后缓冲区。

  4. 设置裁剪矩形 (Scissor Rect): 初始化 _scissor_rect 结构体,定义裁剪矩形。裁剪矩形也设置为整个后缓冲区,表示不进行裁剪。

  5. 尺寸一致性检查: 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(); // 更新当前后缓冲区索引
}

代码解释:

  1. 断言: assert(_swap_chain); 确保交换链对象已创建。

  2. 执行 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)。

  3. 更新后缓冲区索引: _current_bb_index = _swap_chain->GetCurrentBackBufferIndex(); 在 Present 后,需要更新 _current_bb_index 以指向下一个可用的后缓冲区,为下一帧的渲染做准备。

3.4 resize 方法

C++

void
d3d12_surface::resize()
{
}

代码解释:

  • 当前为空: resize 方法在提供的代码中是空的。这是一个缺失的部分,需要补充完整的功能。

resize 方法的完整实现应该包含以下步骤:

  1. 释放现有资源: 在调整大小之前,需要先释放当前交换链和后缓冲区资源,避免资源泄漏。可以调用 release() 方法来完成。

  2. 调整后缓冲区尺寸: 获取新的窗口尺寸。

  3. 调整交换链尺寸: 调用 IDXGISwapChain4::ResizeBuffers 方法来调整交换链的后缓冲区尺寸。需要传入新的缓冲区数量、宽度、高度和格式等参数。

  4. 重新创建 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 命名空间下的资源管理方式,正确获取 IDXGIFactory7ID3D12CommandQueue 指针,以及默认渲染目标格式。 错误处理部分也需要根据实际情况进行完善。

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); // 释放交换链对象
}

代码解释:

  1. 释放后缓冲区资源和 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 方法。

  2. 释放交换链: core::release(_swap_chain); 释放 IDXGISwapChain4 接口指针,销毁交换链对象。

4. 总结与后续步骤

通过 d3d12_surface 类,我们了解了 D3D12 交换链的创建、Present、Resize 和 Release 的基本流程。 交换链是 DirectX 12 渲染管线中至关重要的组成部分,它负责将渲染结果最终显示到屏幕上。

后续学习和实践方向:

  • 完善 resize 方法: 根据实际应用场景,完善 resize 方法的错误处理和资源重新创建逻辑。

  • 理解双缓冲和三缓冲: 深入学习双缓冲和三缓冲的工作原理,以及它们对性能和延迟的影响。

  • Tearing 特性: 研究 tearing 特性的使用场景和优缺点,并根据需求决定是否启用。

  • Flip 模型交换链: 深入了解 Flip 模型交换链的特性和优势。

  • 交换链格式选择: 学习如何选择合适的交换链格式,例如 DXGI_FORMAT_R8G8B8A8_UNORMDXGI_FORMAT_R8G8B8A8_UNORM_SRGB

  • 结合渲染管线: 将交换链集成到完整的 D3D12 渲染管线中,实现实际的渲染应用。

希望本教程能够帮助您理解 DirectX 12 交换链的核心概念和代码实现。 通过实践和深入学习,您将能够更好地掌握 D3D12 图形编程技术。