DirectX 12 游戏开发入门与实践 – wiki基地


DirectX 12 游戏开发:入门与实践

引言:为什么选择 DirectX 12?

在现代游戏开发领域,图形 API 是连接游戏逻辑与 GPU 硬件的桥梁。作为微软主导的图形技术,DirectX 在 Windows 平台上占据着核心地位。自 DirectX 12 (DX12) 于 2015 年发布以来,它以其底层控制能力和性能潜力,成为了高端游戏开发的首选 API 之一。

相较于前辈 DirectX 11 (DX11),DX12 最大的特点是提供了更接近硬件的底层控制。这包括了对内存管理、资源同步、指令提交等方面的精细控制。这种控制权的转移,虽然带来了更高的学习曲线和开发复杂度,但也赋予了开发者更大的优化空间,尤其是在多线程处理和 CPU 性能瓶颈的缓解上,DX12 展现出了显著优势。

对于追求极致性能、希望充分挖掘现代 GPU 潜力的开发者来说,学习 DX12 是非常有价值的。它不仅能帮助你开发出更高效、更复杂的图形效果,也能让你更深入地理解现代图形管线和 GPU 的工作原理。

本文将带领你踏上 DX12 游戏开发的入门之路,从环境搭建到核心概念,再到基础的渲染流程,为你提供一个全面的概览和实践指导。

第一部分:准备工作与环境搭建

在开始 DX12 开发之前,你需要准备好相应的软硬件环境。

  1. 操作系统: Windows 10 或更高版本(推荐 Windows 11),因为 DX12 是这些操作系统的原生特性。确保你的系统是 64 位。
  2. 硬件支持: 你的显卡必须支持 DirectX 12。大多数近些年发布的显卡都已支持。你可以通过运行 dxdiag 命令来查看系统信息中的 DirectX 版本。
  3. 开发工具:
    • Visual Studio: 推荐使用 Visual Studio 2019 或 2022 的社区版、专业版或企业版。这是 Windows 平台下最主要的 C++ 开发 IDE。
    • Windows SDK: 安装与你的 Windows 操作系统版本相匹配的 Windows SDK。在安装 Visual Studio 时,确保勾选了“使用 C++ 的桌面开发”以及相应的 Windows SDK 组件。SDK 包含了 DX12 的头文件、库文件以及相关的开发工具(如 FXC/DXC 着色器编译器)。
  4. 编程语言: C++。DX12 API 是一个 COM (Component Object Model) 接口,主要通过 C++ 进行调用。对 C++ 有扎实的基础是学习 DX12 的前提。

环境搭建步骤:

  1. 安装 Visual Studio。
  2. 在 Visual Studio 安装器中,选择“工作负载”,勾选“使用 C++ 的桌面开发”。
  3. 在右侧的安装详情中,展开“使用 C++ 的桌面开发”,确保勾选了适合你操作系统的最新版 Windows SDK。
  4. 完成安装。

创建一个新的 Visual Studio 项目:

  1. 打开 Visual Studio。
  2. 创建新项目,选择“空项目 (C++)”。
  3. 配置项目属性:
    • 确保项目平台为 x64 (64 位)。
    • 在“配置属性”->“链接器”->“输入”->“附加依赖项”中,添加 d3d12.libdxgi.lib。这两个是 DX12 和 DXGI (DirectX Graphics Infrastructure) 的核心库文件。
    • 根据需要,可能还需要其他库,例如用于数学运算的 DirectXMath,或者用于着色器编译的 d3dcompiler.lib
    • 为了方便调试,建议在“配置属性”->“调试”->“环境”中设置 D3D12_ENABLE_DEBUG_LAYER=1。这会启用 DX12 的调试层,帮助你发现 API 使用错误。

至此,你的 DX12 开发环境就基本搭建好了。

第二部分:DirectX 12 核心概念概览

DX12 与 DX11 的编程模型有很大区别。理解这些核心概念是掌握 DX12 的关键。

  1. Explicit Control (显式控制): 这是 DX12 的灵魂。它将许多原本由驱动程序自动管理的任务(如状态管理、资源同步、内存上传)交给了开发者。这意味着你需要更关心底层细节,但能获得更高的灵活性和性能潜力。
  2. Command Lists (命令列表) 和 Command Queues (命令队列):
    • Command List (ID3D12GraphicsCommandList): 记录 GPU 需要执行的一系列命令(如设置管线状态、绑定资源、绘制调用)。它可以由 CPU 在多个线程上并行生成。生成后的命令列表是不可变的(或在记录后可关闭),可以提交到队列中执行。
    • Command Allocator (ID3D12CommandAllocator): 命令列表所需的内存分配器。一个命令列表必须从一个命令分配器中获取内存。为了支持多线程,通常每个线程或每帧都有自己的命令分配器。
    • Command Queue (ID3D12CommandQueue): GPU 执行命令的队列。CPU 通过将封闭的命令列表提交到命令队列来指示 GPU 工作。GPU 按提交顺序执行队列中的命令列表。
  3. Resources (资源): 显存中的数据,如顶点缓冲区、索引缓冲区、纹理、常量缓冲区等。在 DX12 中,资源由 ID3D12Resource 接口表示。资源的创建、状态转换(如从写入状态到读取状态)都需要显式管理。
  4. Descriptors (描述符) 和 Descriptor Heaps (描述符堆):
    • Descriptor (描述符): 对资源的轻量级描述,包含了 GPU 如何访问资源的信息。它不是资源本身,而是一个指向资源的“句柄”或“视图”。常见的描述符类型有:
      • SRV (Shader Resource View): 着色器读取资源的描述符(如纹理、缓冲区)。
      • UAV (Unordered Access View): 着色器无序读写资源的描述符。
      • CBV (Constant Buffer View): 常量缓冲区的描述符。
      • RTV (Render Target View): 渲染目标(帧缓冲)的描述符,用于输出像素。
      • DSV (Depth Stencil View): 深度/模板缓冲区的描述符。
      • Sampler (采样器): 定义纹理采样方式(过滤、寻址模式)的描述符。
    • Descriptor Heap (ID3D12DescriptorHeap): 存储描述符的显存区域。GPU 不能直接通过资源指针访问资源,必须通过描述符堆中的描述符来访问。有不同类型的描述符堆,有的仅供 CPU 写入(如 RTV, DSV),有的供 CPU 写入并供 GPU 读取(如 SRV, UAV, CBV, Sampler)。
  5. Root Signature (根签名): 定义了 GPU 如何访问描述符堆中的描述符以及绑定内联常量(Root Constants)的布局。它是 CPU 和 GPU 之间关于资源绑定的“约定”。根签名是绑定到命令列表上的,并且必须与正在使用的管线状态对象 (PSO) 兼容。
  6. Pipeline State Objects (PSO) (管线状态对象): 封装了渲染管线的绝大多数固定和可编程状态,包括顶点着色器、像素着色器、混合状态、光栅化状态、深度/模板状态、输入布局等。在 DX11 中,这些状态是动态设置的,而在 DX12 中,它们被打包成一个不可变的对象。切换 PSO 通常比在 DX11 中设置多个动态状态更快。
  7. Synchronization (同步): 由于命令的异步执行和多线程,同步变得至关重要。
    • Fences (ID3D12Fence): 用于 CPU-GPU 或 GPU-GPU 之间的同步。CPU 可以向命令队列插入一个信号 (Signal) 命令,当 GPU 执行到这个命令时,会触发一个 Fence 值。CPU 可以等待 (WaitFor) 这个 Fence 值,或者查询它的完成状态。
    • Resource Barriers (D3D12_RESOURCE_BARRIER): 用于管理资源的转换状态。资源在使用前必须处于正确的状态(如作为常量缓冲区读取、作为渲染目标写入、作为纹理采样读取等)。转换资源状态(Transition Barrier)是同步的一种形式,确保 GPU 的不同部分或不同命令列表不会在资源处于不一致状态时访问它。DX12 要求开发者显式地插入资源屏障。
  8. Memory Management (内存管理): 开发者需要更直接地管理显存的分配和使用。包括选择合适的堆类型(如上传堆 D3D12_HEAP_TYPE_UPLOAD 用于 CPU-GPU 传输,默认堆 D3D12_HEAP_TYPE_DEFAULT 用于 GPU 访问),以及使用映射 (Map) 和解除映射 (Unmap) 来访问 CPU 可见的显存区域。

第三部分:基础渲染流程实践 (概念性描述)

下面是一个使用 DX12 渲染一个简单三角形的典型流程,我们将侧重于 API 调用的逻辑和顺序,而不是具体的代码实现(代码会非常冗长)。

1. 初始化阶段

  • 创建 DXGI 工厂 (IDXGIFactory): 用于枚举适配器(显卡)和创建交换链。
  • 枚举适配器 (IDXGIAdapter): 选择一个合适的显卡设备。通常选择支持 DX12 的高性能适配器。
  • 创建 D3D12 设备 (ID3D12Device): 这是 DX12 API 的核心对象,用于创建所有其他 DX12 资源和对象。通过 D3D12CreateDevice 函数创建。通常也会在这里启用调试层。
  • 创建命令队列 (ID3D12CommandQueue): 用于提交命令列表到 GPU。需要指定队列类型(如图形、计算、复制)。
  • 创建交换链 (IDXGISwapChain3/4): 管理前后缓冲区,用于显示渲染结果。需要绑定到一个窗口句柄,并关联到命令队列。交换链会创建渲染目标资源 (ID3D12Resource),通常至少有两个缓冲区(双缓冲)。
  • 创建渲染目标视图 (RTV) 描述符堆: 用于存储交换链后缓冲区的 RTV 描述符。由于 RTV 堆只供 CPU 写入,通常不需要 GPU 可见。
  • 为交换链的每个后缓冲区创建 RTV 描述符: 调用 ID3D12Device::CreateRenderTargetView,将后缓冲区资源关联到 RTV 描述符堆中的一个位置。
  • 创建深度/模板缓冲资源 (ID3D12Resource) 和 DSV 描述符堆 (可选但常见): 用于深度测试和模板测试。创建资源时需要指定 DSV 资源状态和合适的格式。创建 DSV 描述符堆并为深度缓冲区创建 DSV 描述符。
  • 创建命令分配器 (ID3D12CommandAllocator): 用于后续命令列表的记录。通常每一帧都会“重置”这个分配器,或者使用一个分配器的循环池。
  • 创建命令列表 (ID3D12GraphicsCommandList): 从命令分配器创建。初始化时通常处于记录状态。

2. 资源创建阶段

  • 定义顶点数据和索引数据: 在 CPU 内存中准备三角形的顶点数据(位置、颜色、纹理坐标等)和索引数据(顶点连接顺序)。
  • 创建上传堆资源 (ID3D12Resource): 在 CPU 可写、GPU 可读/写的上传堆中创建缓冲区,用于临时存放顶点和索引数据,以便传输到 GPU 的默认堆。使用 D3D12_HEAP_TYPE_UPLOAD
  • 创建默认堆资源 (ID3D12Resource): 在 GPU 专用的默认堆中创建缓冲区,这将是 GPU 实际读取顶点和索引数据的地方。使用 D3D12_HEAP_TYPE_DEFAULT
  • 上传数据: 将 CPU 内存中的顶点/索引数据拷贝到上传堆资源中(通过 Map 获取 CPU 可访问指针)。然后,使用命令列表记录 CopyBufferRegion 命令,将数据从上传堆资源拷贝到默认堆资源。这个拷贝命令需要在 GPU 上执行。
  • 创建着色器程序: 编写 HLSL (High-Level Shading Language) 代码,包括顶点着色器和像素着色器。使用 DXC 或 FXC 编译器将 HLSL 代码编译成字节码。
  • 创建根签名 (ID3D12RootSignature): 定义着色器如何访问资源的布局。例如,如果着色器需要读取一个常量缓冲区和一个纹理,根签名就需要包含 CBV 描述符和 SRV 描述符的定义,并指定它们在描述符堆中的绑定方式(如通过描述符表或内联)。
  • 创建描述符堆 (GPU 可见): 如果根签名需要 GPU 访问描述符(如 SRV, CBV, Sampler),则需要创建 GPU 可见的描述符堆(D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE)。为常量缓冲区、纹理等创建相应的 SRV/CBV/Sampler 描述符,并将它们放置在这个 GPU 可见堆中。
  • 创建管线状态对象 (PSO) (ID3D12PipelineState): 填充 D3D12_GRAPHICS_PIPELINE_STATE_DESC 结构体,指定顶点着色器字节码、像素着色器字节码、输入布局、根签名、混合状态、光栅化状态、深度/模板状态、图元拓扑类型等信息。调用 ID3D12Device::CreateGraphicsPipelineState 创建 PSO。

3. 渲染循环阶段 (每帧)

这个阶段是周期性执行的,通常与显示器的刷新率同步。

  • 获取当前后缓冲区索引: 从交换链获取当前应该渲染的后缓冲区的索引 (IDXGISwapChain3::GetCurrentBackBufferIndex)。
  • 重置命令分配器: 调用 ID3D12CommandAllocator::Reset()。只有当与该分配器关联的所有命令列表都已在 GPU 上执行完毕后才能重置。
  • 重置命令列表: 调用 ID3D12GraphicsCommandList::Reset(commandAllocator, pipelineStateObject)。将命令列表与其关联的分配器重新绑定,并可以选择设置一个初始的 PSO。
  • 设置视口和裁剪矩形: 调用 SetViewportSetScissorRects 设置渲染区域。
  • 插入资源屏障 (Transition Barrier): 将当前后缓冲区资源从呈现状态 (D3D12_RESOURCE_STATE_PRESENT) 转换到渲染目标状态 (D3D12_RESOURCE_STATE_RENDER_TARGET)。如果使用深度缓冲区,也需要将其转换为深度写入状态 (D3D12_RESOURCE_STATE_DEPTH_WRITE)。
  • 设置渲染目标: 调用 OMSetRenderTargets 绑定当前后缓冲区的 RTV 描述符和深度缓冲区的 DSV 描述符。
  • 清除渲染目标和深度缓冲区: 调用 ClearRenderTargetViewClearDepthStencilView,用指定的颜色和深度值清除缓冲区。
  • 设置根签名: 调用 SetGraphicsRootSignature
  • 设置描述符堆 (GPU 可见): 如果使用了 GPU 可见的描述符堆,需要调用 SetDescriptorHeaps 绑定它们。
  • 绑定资源: 根据根签名和 PSO 的要求,使用 SetGraphicsRootDescriptorTable, SetGraphicsRootConstantBufferView, SetGraphicsRoot32BitConstants 等方法绑定描述符堆中的描述符或上传内联常量。
  • 设置图元拓扑类型: 调用 IASetPrimitiveTopology(如 D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST)。
  • 设置顶点/索引缓冲区: 调用 IASetVertexBuffersIASetIndexBuffer,使用之前创建的默认堆资源及其视图(如 D3D12_VERTEX_BUFFER_VIEW, D3D12_INDEX_BUFFER_VIEW)。
  • 绘制调用: 调用 DrawIndexedInstanced (如果使用索引缓冲区) 或 DrawInstanced (如果不使用索引缓冲区) 来触发 GPU 的绘制操作。
  • 插入资源屏障 (Transition Barrier): 将当前后缓冲区资源从渲染目标状态 (D3D12_RESOURCE_STATE_RENDER_TARGET) 转换回呈现状态 (D3D12_RESOURCE_STATE_PRESENT)。
  • 关闭命令列表: 调用 ID3D12GraphicsCommandList::Close()。关闭后的命令列表才能被提交到队列。
  • 执行命令列表: 创建一个包含要执行的命令列表的数组,调用 ID3D12CommandQueue::ExecuteCommandLists() 提交到命令队列。
  • 呈现: 调用 IDXGISwapChain::Present() 将后缓冲区显示到屏幕上。
  • 同步 (等待): 为了避免 CPU 跑得太快导致 GPU 工作来不及,需要进行同步。通常是向命令队列插入一个 Fence 信号 (ID3D12CommandQueue::Signal),然后 CPU 等待前几帧的 Fence 值 (ID3D12Fence::WaitForCompletion),确保 GPU 完成了相应的工作,回收资源(如命令分配器)供新的一帧使用。这实现了 CPU 和 GPU 之间的流水线并行。

4. 清理阶段

  • 在程序退出时,释放所有创建的 DX12 和 DXGI 对象。由于 DX12 使用 COM 对象,需要调用每个接口的 Release() 方法来减少引用计数,直到对象被销毁。确保在释放 DXGI 工厂之前释放所有依赖它的对象(如交换链、设备)。

第四部分:DX12 的优势与挑战

优势:

  1. CPU 性能提升: 通过降低驱动程序开销、支持多线程命令列表生成、改进资源管理,显著减少了 CPU 在图形提交上的开销,尤其在绘制调用密集或 CPU 负载重的场景下。
  2. 更好的多线程利用: 开发者可以在多个 CPU 线程上并行记录命令列表,充分利用多核处理器的性能。
  3. 更精细的内存控制: 开发者可以更灵活地管理显存分配、上传和资源状态,有助于优化内存带宽和延迟。
  4. 更低的延迟: 部分底层控制(如显式的资源同步)可以帮助减少渲染管线中的延迟。
  5. 新特性支持: DX12 支持一些 DX11 不具备或不完善的高级特性,如异步计算、光线追踪 (DXR)、可变速率着色 (VRS) 等。

挑战:

  1. 更高的学习曲线: DX12 的抽象层次更低,概念更多,对开发者的底层知识要求更高。
  2. 更高的开发复杂度: 原本由驱动程序处理的任务现在需要开发者自己管理,如资源状态管理、内存上传同步、多线程同步等,代码量和复杂度显著增加。
  3. 调试难度: 显式控制意味着错误更容易发生,而且底层错误可能更难追踪。虽然 DX12 提供了强大的调试层和工具(如 PIX),但仍然需要开发者具备更强的调试能力。
  4. 需要更多 Boilerplate Code: 许多基础设置(如创建描述符堆、管理资源状态)需要大量的重复性代码。框架和引擎可以帮助封装这些细节。

第五部分:进阶方向与学习资源

掌握了 DX12 的基础后,你可以进一步探索以下方向:

  • 多线程渲染: 实现命令列表的并行记录和提交。
  • 异步计算 (Async Compute): 利用 GPU 的计算单元与图形单元并行执行任务,提高 GPU 利用率。
  • 资源管理: 实现高效的显存管理器,包括资源池化、上传和流式加载。
  • 图形管线优化: 深入理解 GPU 架构和 DX12 的性能特性,进行更底层的优化。
  • 高级渲染技术: 基于 DX12 实现延迟渲染、基于物理的渲染 (PBR)、全局光照等。
  • DirectX Raytracing (DXR): 学习如何使用 DX12 的光线追踪扩展,实现更真实的渲染效果。
  • 使用辅助库和框架: 学习使用 D3D12MA (D3D12 Memory Allocator)、Effect-Composer 等辅助库,或者研究大型游戏引擎(如 Unreal Engine 5, Unity)中 DX12 的实现。

学习资源推荐:

  1. Microsoft 官方文档 (Microsoft Learn): 这是最权威、最全面的资料来源,包含了 DX12 API 的详细说明、概念指南和示例代码。
  2. 书籍: Frank Luna 的《Introduction to 3D Game Programming with DirectX 12》是经典的入门教材,提供了详细的示例代码和讲解。
  3. 在线教程和博客: 社区中有很多优秀的 DX12 教程和博客,可以搜索关键词 “DirectX 12 tutorial” 或 “DX12 入门”。
  4. 开源项目: 阅读一些优秀的开源图形引擎或渲染器的 DX12 部分代码,学习其设计和实现方式。
  5. 调试工具 PIX: PIX 是微软提供的强大的图形调试和性能分析工具,对于理解 DX12 的执行流程和优化性能至关重要。

结论

DirectX 12 是一个强大但复杂的图形 API。它将更多的控制权交给了开发者,带来了显著的性能提升潜力,尤其是在 CPU 端和多线程方面。然而,这种控制权也带来了更高的学习门槛和开发复杂度。

入门 DX12 需要扎实的 C++ 基础、对线性代数和 3D 图形基本概念的理解,以及投入大量时间和精力去理解其独特的编程模型(命令列表、描述符、根签名、PSO 等)和底层机制(资源状态、同步、内存管理)。

通过本文的介绍,希望你能对 DX12 的核心概念和基础开发流程有一个初步的认识。实践是掌握任何技术的最佳途径。建议从简单的例子开始,逐步构建一个基本的 DX12 渲染框架,然后尝试添加更复杂的特性和渲染技术。

DX12 的学习之旅充满挑战,但也充满回报。一旦掌握了它,你将能够开发出性能更优异、视觉效果更震撼的现代游戏和图形应用。祝你学习顺利!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部