D3D12 中的资源和堆
本文是对 DirectX 12 龙书第二版(Introduction to 3D Game Programming with DirectX 12 Second Edition)第四章新增内容的整理,主要涉及 4.1.13 节的资源和 4.1.14 节的常见堆类型。
这两小节是第一版完全没有的内容。要理解后面的 CreateCommittedResource、常量缓冲为什么要放 UPLOAD 堆、纹理为什么要先上传到 UPLOAD 再拷贝到 DEFAULT,都必须先搞懂这里。
4.1.13 资源(Resources)
在 Direct3D 12 里,我们要和两类资源(resources)打交道:缓冲(buffers)和纹理(textures)。
- 缓冲(buffer):某种类型数据元素的数组。比如顶点数据、索引数据、常量数据,本质上就是一块结构化内存。
- 纹理(texture):§4.1.3 已经简单提过,第九章会详细讲。现在可以把它理解为一张按二维网格排列的图像数据。
为了性能,我们当然希望资源放在显存(VRAM)里,因为 GPU 访问显存的带宽非常高。不过 GPU 也可以通过 PCI Express 总线去读系统内存里的数据,只是慢得多。
Direct3D 里会反复出现一个词:堆(heap)。它其实就是“一块内存”。描述符堆(descriptor heap)是放描述符的内存;资源堆(resource heap)是放资源(缓冲和纹理)的内存。下面要说的三种资源创建方式,核心差别就在于“内存(堆)”和“资源对象”之间是绑定还是分离。
已提交资源(Committed Resources)
已提交资源是最简单、最常用的创建方式。调用一次 API(ID3D12Device::CreateCommittedResource),就会同时创建资源和它背后的堆,二者被绑定成一个整体。
这种方式的缺点是创建本身比较重——因为它要真的去分配内存。所以最好是在初始化或加载资源时一次性创建好,不要在渲染一帧的过程中临时创建。
具体 API 细节在 §4.3.7 讲。
放置资源(Placed Resources)
放置资源的思路是:先创建一个大堆,然后往这个堆里“放置”资源。换句话说,它把分配内存和创建资源对象这两步拆开了。
这很像 CPU 上的内存池:你先一次性申请一大块内存,然后在这块内存里分配变量。因为堆已经存在,创建/销毁资源时只需要在堆里找一个位置,所以比已提交资源快得多,适合运行时频繁创建和销毁资源的场景。
放置资源的另一个好处是内存别名(memory aliasing):如果若干资源不会在同一时间被使用,可以让它们在内存中重叠(overlap),从而复用同一块显存。下面图 4.6 就是解释这个。

图 4.6 内存别名:GPU 内存中有一个堆。我们先在堆里放置资源 R1、R2 和 R3;之后又在同一堆里放置 R4 和 R5,其中 R4 与 R1 重叠、与 R2 部分重叠,R5 与 R3 重叠。
放置资源对任务图 / 帧图(task/frame graph)系统特别有用,因为这类系统可以自动计算哪些资源可以复用、哪些可以别名。
堆用 ID3D12Device::CreateHeap 创建,放置资源用 ID3D12Device::CreatePlacedResource 创建。本书后面不会实际使用放置资源,因为它更像是一种优化手段,深入讲会分散注意力;但在商业项目里你很可能会用到,至少要心里有数。
保留资源(Reserved Resources)
保留资源是另一种高级资源类型,本书只简单介绍,不会实际使用。
它为 GPU 资源提供了一种类似虚拟内存的机制:你可以创建一个非常大的资源,但创建时它并不占用任何物理内存。这个大资源被划分成一个个瓦片(tile),Direct3D 提供 API 让程序根据当前需要把瓦片移入或移出物理内存。
典型的例子是一张巨大的卫星地图。你当前只盯着洛杉矶市中心看,那大部分地图根本不需要进物理内存;随着地图平移,再把不同的瓦片换进换出。
Direct3D 11 里这类资源叫“平铺资源(tiled resources)”,概念和保留资源是一样的。想了解更多可以参考 [Sandy14] 和 [Cebenoyan14] 的在线视频。
本节理解
- 已提交资源 = 一站式:最省心,但分配开销大,适合初始化时创建静态资源。
- 放置资源 = 自己管理内存池:适合频繁创建/销毁,能通过别名大幅节省显存,但代码更复杂。
- 保留资源 = GPU 虚拟内存:适合超大资源按需加载,是更高级的优化。
对于初学阶段,先把已提交资源用熟。放置资源和保留资源暂时知道存在即可,等你能熟练写 Demo 后再回来研究。
4.1.14 常见堆类型(Common Heap Types)
前面说过,堆就是一块内存。但这块内存到底住在哪里?在有独显的平台上,我们有 CPU 内存(系统内存 RAM)和 GPU 内存(显存 VRAM)。Direct3D 12 用不同的堆类型来区分它们。
1. D3D12_HEAP_TYPE_DEFAULT:默认堆 / 显存堆
DEFAULT 堆就是把资源放在显存(VRAM)里。
- GPU 可以直接高速读写。
- CPU 不能直接读写这里面的资源。
- 适合静态资源,比如纹理、3D 模型数据:加载一次,反复读取。
对于这类资源,通常的做法是:先在 CPU 可写的 UPLOAD 堆里准备好数据,再通过拷贝命令把它复制到 DEFAULT 堆里的资源上。因为 GPU 访问 VRAM 的带宽非常高,所以我们应该尽可能让资源待在这里。
2. D3D12_HEAP_TYPE_UPLOAD:上传堆
UPLOAD 堆是系统内存,CPU 可以写,GPU 可以通过 PCI Express 总线读。
- 使用 write combining(写合并):把零散的写操作攒成较大的块再提交,提高通过 PCIe 写的效率。
- 这块内存对 CPU 是 uncached(不可缓存) 的,所以不要在 CPU 侧读取它。一个很隐蔽的性能 bug 是在上传堆里做
+=这种“先读后写”的操作。 - GPU 从上传堆读取的速度受 PCIe 带宽限制,通常比读 VRAM 慢不少。但如果 GPU 会多次读取同一份数据,它可以缓存这些数据,避免反复跨 PCIe 去取。
凡是 CPU 每帧都要更新、GPU 又要读的数据(比如常量缓冲、每帧变化的顶点数据),一般就放在 UPLOAD 堆里。
另外还有一种做法:把数据先写到 UPLOAD 堆,再拷贝到 DEFAULT 堆。如果 GPU 在同一帧里会多次读取这份数据,那么多花一次拷贝、换来在 VRAM 里高速读取,往往更划算。
3. D3D12_HEAP_TYPE_READBACK:回读堆
READBACK 堆用于 GPU 把数据写回 CPU 的场景。
- 资源分配在系统内存里。
- 需要 GPU→CPU 的拷贝,同样受 PCIe 带宽限制。
- 很重要的一点:当你发出一个 GPU→CPU 的拷贝命令后,拷贝不会立刻完成。GPU 是异步工作的,CPU 不能马上假设数据已经可读。
因此代码里通常要避免阻塞等待 GPU 完成拷贝。常见的做法是允许回读数据比 GPU 当前正在渲染的内容晚一两帧。换句话说,CPU 读到的可能是上一帧或上两帧的数据,这样大家都不会卡住。
4. D3D12_HEAP_TYPE_GPU_UPLOAD:GPU 上传堆(预览特性)
截至本书写作时,这个堆类型还处于开发者预览阶段。使用它需要检查 D3D12_FEATURE_DATA_D3D12_OPTIONS16::GPUUploadHeapSupported(通过 ID3D12Device::CheckFeatureSupport),并且可能需要 BIOS 配置。
它的特殊之处在于:CPU 可以直接拿到一个指向 VRAM 的指针,并通过 PCIe 直接往显存里写。这样:
- 非常适合 CPU 每帧都要写的资源:CPU 直接写到 VRAM,然后 GPU 以高带宽直接读取。
- 也可以用来直接初始化静态 GPU 资源,省掉先建 UPLOAD 堆再拷贝到 DEFAULT 堆那一步。
当然,CPU 写到 VRAM 仍然受 PCIe 带宽限制;但一旦数据进了 VRAM,GPU 访问就非常快。
五种数据流动(图 4.7)
下面这张图概括了 CPU、GPU、RAM、VRAM 之间的读写关系:

图 4.7 堆类型总览:
- (a) CPU 可以读写系统内存。
- (b) GPU 可以高速读写显存;放在 VRAM 里的资源属于 DEFAULT 堆。
- (c) 资源可以先在系统内存的 UPLOAD 堆里创建,再拷贝到 VRAM 的 DEFAULT 堆资源里;反过来,数据也可以通过 READBACK 从 GPU 回读到 CPU。
- (d) GPU 可以直接读系统内存里的 UPLOAD 堆,但速度较慢,受 PCIe 带宽限制。
- (e) 使用 D3D12_HEAP_TYPE_GPU_UPLOAD 时,CPU 可以直接通过 PCIe 往 VRAM 里写;这也受 PCIe 带宽限制,但数据一旦进入 VRAM,GPU 访问就会非常快。
想了解更多 GPU 内存细节,可以看看 [Sawicki21] 的短片和 [Pettineo22] 的博客。
本节理解
- DEFAULT 堆:GPU 读最快的家。静态纹理、模型的顶点和索引数据尽量放这里;CPU 要写必须先走 UPLOAD 中转。
- UPLOAD 堆:CPU 每帧写、GPU 读的中转站。常量缓冲、动态顶点数据典型住户;CPU 不要读它。
- READBACK 堆:GPU 写回 CPU 的出口。注意异步延迟,别让 CPU 傻等。
- GPU_UPLOAD 堆:CPU 直写 VRAM 的预览特性。可以理解为 UPLOAD 堆的“升级版”,但还不是所有平台都支持。
这四种堆型是后面所有 Demo 的基石:纹理为什么要 CopyBufferRegion / CopyTextureRegion、常量缓冲为什么直接放 UPLOAD 堆、CPU 为什么不能直接改纹理内容——答案都在这里。
整体总结
这两节把 Direct3D 12 里“资源到底存在哪里”这件事讲透了:
| 概念 | 一句话 |
|---|---|
| 已提交资源 | 一次 API 同时创建资源和堆,简单但分配重 |
| 放置资源 | 先建堆再往里面放资源,支持别名复用显存 |
| 保留资源 | GPU 虚拟内存,适合超大资源流式加载 |
| DEFAULT 堆 | 显存,GPU 读最快,CPU 写不了 |
| UPLOAD 堆 | 系统内存,CPU 写、GPU 读,适合每帧更新的数据 |
| READBACK 堆 | 系统内存,GPU 写回 CPU,注意异步延迟 |
| GPU_UPLOAD 堆 | CPU 直写显存的预览特性 |
把这两节吃透之后,再看后面创建交换链、创建深度模板缓冲、上传纹理、管理常量缓冲,都会清晰很多。