§0一页结论
每一行是一类资源(或一种 CPU 访问操作)的最终落点,按 DEFAULT Buffer、UPLOAD、初始数据、Lock、READBACK、光追、Texture、RDG / Reserved 分组。创建路由按 §3、§4 的顺序判定,先命中者生效;池内分配若超过池的单次上限,一律回退到 Committed。落点名称与代码保持一致,配色含义见页首。
| 资源 / 操作 | 判定条件 | 落点 | 关键参数 | 设计理由 |
|---|---|---|---|---|
| DEFAULT Heap · Buffer | ||||
| 静态只读 BufferVB · IB · 只读 Structured / ByteAddress | 非 Dynamic;Alignment ≤ 256;无 UAV | 共享 Buffer 池 | 32MB Committed Buffer · 256B 粒度 · 单次 ≤16MB · 可整理 | 数量多、创建后状态不变,可以共用一个 ID3D12Resource;粒度细到 256B |
| 可写 / 高对齐 BufferUAV · LCM(Stride, 16) > 256 · MultiState | 需要独立资源状态,或偏移满足不了对齐 | Placed 池 | 32MB Heap · 64KB 粒度 · 单次 ≤16MB · 可整理 | 每个资源各有一个 ID3D12Resource 和状态;Placed Resource 的视图偏移恒为 0,任何 stride 都自然对齐 |
| 大 Buffer · Shared Buffer | > 16MB,或带 Shared | Committed | 按 64KB 取整 | 大分配频率低,池化收益小 源码注释;Shared 需要 HEAP_FLAG_SHARED |
| UPLOAD Heap · FD3D12UploadHeapAllocator 与常量 | ||||
| Dynamic / Volatile Buffer | Usage 含 AnyDynamic | AllocUploadResourceSmallBlockAllocator BigBlockAllocator Committed | ≤64KB → MultiBuddy(4MB Buffer) ≤64MB → PoolAllocator(32MB Buffer · 512B) 更大 → Committed UPLOAD Buffer 不经过 FastConstantPageAllocator | GPU 直接读 UPLOAD Heap,CPU 常驻 Map 零拷贝写;再次整块写入时换一块新地址(Rename) |
| Uniform Buffer(MultiFrame) | UniformBuffer_MultiFrame | AllocUploadResourceSmallBlockAllocator | 与 Dynamic Buffer 同一入口 · 256B 对齐 UB ≤64KB(4096 × 16B)→ 必然命中 SmallBlockAllocator | 路由策略与 Dynamic Buffer 相同,只是尺寸上限让它到不了 BigBlockAllocator;跨帧、生命周期不定,必须能逐个释放 |
| Uniform Buffer(SingleFrame · SingleDraw)以及命令上下文的松散常量 | SingleFrame · SingleDraw | FD3D12FastConstantAllocator | 64KB Page 取自 FastConstantPageAllocator · Page 内 256B 对齐 UB:线程局部 FTransientUniformBufferAllocator 松散常量:FD3D12CommandContext::ConstantsAllocator | 一帧内用完:只推 offset,旧 Page 随 fence 回收,没有逐个释放 |
| 初始数据 · 拷进目标后即丢弃 | ||||
| Buffer 初始数据RHICreateBufferInitializer | 按调用线程分流;Dynamic Buffer 直接写自身映射地址 | FD3D12FastAllocatorAllocUploadResource | 渲染 / RHI 线程 → FastAllocator 4MB Page(>4MB 转 AllocUploadResource) 其他线程 → AllocUploadResource | 工作线程上的 Staging 可能跨帧存活,不适合按帧回收的 Bump Page 推断 |
| Texture 初始数据RHICreateTextureInitializer · RHIAsyncCreateTexture2D | Initializer 不看线程;异步创建单独一条路 | FD3D12FastAllocatorAllocUploadResource | Initializer → FastAllocator(>4MB 转 AllocUploadResource) RHIAsyncCreateTexture2D → AllocUploadResource · Copy Queue | 与 Buffer 不同,Initializer 路径没有线程判断(§4.6) |
| Lock / Unlock · 写 | ||||
| Dynamic Buffer · Lock 写 | 首次 Lock 或 RLM_WriteOnly_NoOverwrite;否则(再次 RLM_WriteOnly) | 自身映射地址AllocUploadResource + Rename | 新块 = 整个 Buffer · BufferAlignment 旧块挂 fence 延迟释放 | 等价 D3D11 的 MAP_WRITE_DISCARD,不改写 GPU 可能正在读的旧数据 |
| 静态 Buffer · Lock 写 | 非 Dynamic · RLM_WriteOnly(不看线程) | AllocUploadResource | 512B 对齐 · Unlock 时排队 CopyBufferRegion | Staging 只活到拷贝完成 |
| Texture · Lock 写含 UpdateTexture2D · BeginUpdateTexture3D | RLM_WriteOnly(不看线程) | FD3D12FastAllocator | 512B 对齐 · >4MB 转 AllocUploadResource Unlock 时排队 CopyTextureRegion | Dynamic 纹理的 Staging 若来自 BigBlockAllocator 或 StandAlone,Unlock 后回收给该纹理复用 源码注释 |
| READBACK Heap · 回读(创建途径分散,见 §5.5) | ||||
| FRHIStagingBufferRHICopyToStagingBuffer · FRHIGPUBufferReadback | 容量不足时才重新分配 | Readback Pool Committed | ≤64KB → 4MB Committed READBACK Buffer 子分配 · 不整理 >64KB → Committed | READBACK 资源恒为 COPY_DEST,可以共享一个 Buffer |
| CPUReadback 纹理TexCreate_CPUReadback · FRHIGPUTextureReadback | 创建时带 CPUReadback | Committed | READBACK Buffer · 大小 = GetCopyableFootprints RHIMapStagingSurface 等 Fence 后 Map | 常驻的拷贝目标,可反复使用 |
| Lock 读LockBuffer · FD3D12Texture::Lock 的 RLM_ReadOnly | 非 Dynamic Buffer;任意纹理 | Committed | 每次新建 READBACK Buffer · SubmitAndBlockUntilGPUIdle | 同步等待 GPU,代价很高 |
| ReadSurfaceData 系列RHIReadSurfaceData · ReadSurfaceFloatData · Read3DSurfaceFloatData | 每次调用 | Committed | 临时建 READBACK Buffer · 同步读回 | 唯一例外:非 MSAA 的 RHIReadSurfaceData 发现源纹理已在 READBACK Heap 时直接复用 |
| Query 结果Timestamp · Occlusion · PipelineStatistics | 创建 FD3D12QueryHeap | Committed | CreateCommittedResource(READBACK · COPY_DEST)· 常驻 Map | 每个 Query Heap 一块结果 Buffer,长期复用 |
| DEFAULT Heap · 光追 Buffer | ||||
| 光追加速结构(AS) | AccelerationStructure · SingleState · 256B 对齐 | 共享 Buffer 池(AS 专用) | 32MB Committed Buffer · 单次 ≤16MB · 不整理 · 驻留优先级 HIGH | AS 状态永远不变,可共享 Buffer;AS 创建后未必立刻构建,拷贝未构建的 AS 会让 GPU 崩溃,所以禁用整理 源码注释 |
| 光追 Scratch | UAV · 状态模式 Default | Placed 池 | 64KB 粒度 · 驻留优先级 HIGH | UAV 推导为 MultiState |
| DEFAULT Heap · Texture | ||||
| 只读小纹理 | 非 RT/DS、单采样、非 Reserved,最高 mip ≤16 个 4KB tile | ReadOnly4K 池 | 4MB Heap · 4KB 粒度 · 不整理 | 避免小纹理被 64KB 对齐放大;块都是 4KB 的倍数,几乎不产生碎片 源码注释 |
| 只读纹理 | 其余只读(仅 SRV)纹理 | ReadOnly 池 | 64MB Heap · 64KB 粒度 · 单次 ≤64MB · 可整理 | 流送纹理数量大、创建销毁交错,需要整理才能回收空 Heap |
| RT · DS · UAV 纹理以及 MSAA、Shared 纹理 | — | Committed | RT/UAV 池默认大小为 0 | 源码没写不池化的理由(分析见 §4.4 推断);MSAA 需要 4MB 对齐,超过池对齐 |
| RDG · Reserved | ||||
| RDG Transient Resource | r.RDG.TransientAllocator = 1 | Transient Heap | ≥128MB Heap · 64KB 对齐 · 按生命周期复用 | 同一帧内,pass 生命周期不重叠的资源共享同一段物理内存 |
| Reserved Buffer / 纹理 | ReservedResource 标记 | Reserved | 64KB tile · 16MB Backing Heap | 虚拟地址一次预留,物理内存按需增减,不需要连续大块 |
§1为什么需要这么多分配器
D3D12 只给了三个创建资源的 API(Committed、Placed、Reserved),引擎又在 Committed Buffer 之上自造了第四种用法:共享 Buffer 子分配。每种都有代价,没有一种能同时做到创建快、不浪费、状态独立、释放安全。UE 的做法是按资源特征,把每个请求路由到代价最小的那一种。
1.1四种落点
ID3D12Resource、谁拥有物理内存、资源状态以什么为单位。后文所有分配器,都是这四种落点之一,再加上一种 CPU 侧的切分算法。严格说 D3D12 API 只有第一、二、四列三种;第三列是在 CreateCommittedResource 建出的大 Buffer 上由 UE 做 CPU 侧切分,UploadHeapAllocator 的 Buddy、FastAllocator 的 Bump Page 也属于这一类。1.2四个硬约束
约束 ① 创建与驻留有成本。每个 Committed 资源、每个 ID3D12Heap 都对应一次 WDDM 显存分配(要进内核),在驻留管理里也各是一个独立对象。若每帧成百上千个小资源都这样创建,CPU 开销和驻留簿记都扛不住,所以要「建一次大 Heap 或大 Buffer,反复切」。
约束 ② 放置有对齐粒度。Buffer 的 Alignment 只能是 64KB 或 0(等价 64KB);不使用 Tight Alignment 时,Buffer 实际占用是不小于 Width 的最小 64KB 倍数 官方文档。纹理默认 64KB、MSAA 默认 4MB,满足「小资源」条件的纹理可降到 4KB、MSAA 纹理可降到 64KB 官方文档。于是一个 1KB 的常量缓冲,不管 Committed 还是 Placed,都要占掉 64KB:
约束 ③ 资源状态以 ID3D12Resource 为单位。在 legacy barrier 模型里,一个 Buffer 只有一个状态,UE 的状态跟踪也挂在 FD3D12Resource 上。多个逻辑 Buffer 共用一个 ID3D12Resource,就只能共用一个状态,所以共享 Buffer 子分配只适用于创建后状态不变的资源:只读 Buffer(GENERIC_READ)、UPLOAD Heap(GENERIC_READ)、READBACK Heap(COPY_DEST)、加速结构(RAYTRACING_ACCELERATION_STRUCTURE)。需要在 SRV、UAV、CopyDest 之间切换的资源必须拥有独立的 ID3D12Resource,只能 Placed 或 Committed 源码注释。本版本已支持 Enhanced Barriers,但分配器仍按单状态 / 多状态划分。
约束 ④ GPU 异步执行。CPU 侧释放资源时,GPU 可能还在执行引用它的命令,所以任何释放都要等对应帧的 fence 完成(§8.1)。反过来,只活一帧的数据(常量、Upload Staging)根本不需要逐个释放:在 Page 内顺序分配,等 GPU 跑完这一帧,整个 Page 直接复用。
1.3由约束推出的四个路由维度
| 维度 | 取值 | 决定了什么 | 来自约束 |
|---|---|---|---|
| 状态可变性 | SingleState / MultiStateDefault 模式下,带 UAV 即推导为 MultiState | 共享 Buffer,还是独立 ID3D12Resource(Placed / Committed) | ③ |
| 对齐 | 16…256B · 4KB · 64KB · 4MB | 能否进 256B 共享池、能否进 4K 纹理池;MSAA 只能 Committed | ② |
| 尺寸 | ≤ 池的单次上限 / 超出 | 进池,还是 Committed | ① |
| 生命周期 | 单帧 · 图内临时 · 持久 · 可伸缩 | Bump Page、Transient Heap、池、Reserved | ① ④ |
一句话小而多的进池;可写的要独立状态;只活一帧的用 Bump Page 按页回收;太大或对齐特殊的直接 Committed;需要伸缩的走 Reserved。
§2分配器家族与尺寸阈值
先认识所有分配器和它们的尺寸分界,后面各节再逐个拆开机制。
2.1谁拥有什么
FD3D12DefaultBufferAllocator
- 归属
- 每个 Device 一个
- 后备
- 按配置惰性创建多个
FD3D12PoolAllocator:32MB Committed Buffer(共享)或 32MBID3D12Heap(Placed) - 算法
- Best-fit 空闲链表 · 帧间整理
- 服务
- DEFAULT Heap Buffer、光追 AS / Scratch、FRHIStagingBuffer(READBACK)
FD3D12TextureAllocatorPool
- 归属
- 每个 Device 一个
- 后备
- 4 个
FD3D12PoolAllocator:ReadOnly4K(4MB Heap)、ReadOnly(64MB Heap)、RT、UAV(后两者默认关闭) - 算法
- Best-fit · 仅 ReadOnly 池整理
- 服务
- 非 Transient、非 Reserved 的 DEFAULT Heap 纹理
FD3D12UploadHeapAllocator
- 归属
- Adapter,每个 GPU 一个
- 后备
- Committed UPLOAD Buffer,常驻 Map
- 算法
- SmallBlockAllocator(MultiBuddy · ≤64KB)· BigBlockAllocator(PoolAllocator · ≤64MB)· FastConstantPageAllocator(MultiBuddy · 64KB Page)
- 服务
AllocUploadResource:Dynamic Buffer、MultiFrame UB、静态 Buffer Lock 写、工作线程上的 Buffer 初始数据、FastAllocator 超过 4MB 的请求;AllocFastConstantAllocationPage:只给 FD3D12FastConstantAllocator 供页
FD3D12FastAllocator(DefaultFastAllocator)
- 归属
- 每个 Device 一个
- 后备
- 4MB Committed UPLOAD Buffer 作为 Page,常驻 Map
- 算法
- Bump 指针 · 页随 fence 复用
- 服务
- 渲染 / RHI 线程的 Buffer 初始数据;Texture 初始数据、Texture Lock 写、UpdateTexture2D / 3D(不看线程);光追 SBT 数据
FD3D12FastConstantAllocator
- 归属
- 线程局部
FTransientUniformBufferAllocator+ 每个FD3D12CommandContext的 ConstantsAllocator;不属于 UploadHeapAllocator - 后备
- 向 UploadHeapAllocator 的 FastConstantPageAllocator 要 64KB Page(Buddy 块)
- 算法
- Bump · 256B 对齐
- 服务
- SingleFrame / SingleDraw UB、松散着色器常量
FD3D12TransientResourceHeapAllocator
- 归属
- RDG 每次图执行创建;Heap 缓存挂在 Adapter
- 后备
- ≥128MB 的 DEFAULT
ID3D12Heap - 算法
- 首次适配 + 生命周期别名 · Placed Resource 缓存
- 服务
- RDG 图内 Transient 纹理与 Buffer
Reserved 资源(CommitReservedResource)
- 归属
- 资源自身(
FD3D12Resource) - 后备
- 16MB Backing Heap,按 64KB tile 映射
- 算法
- 从尾部增长 / 从尾部收缩,先用完 slack
- 服务
- GPUScene、VSM 物理页池、SVT、Hair 曲线、Nanite 流送(实验)
Committed 回退路径
- 归属
- 各分配器内部的兜底分支
- 后备
- 每个资源一个隐式 Heap
- 算法
- 无,直接
CreateCommittedResource - 服务
- 超出池上限、Shared、RT / DS / UAV / MSAA 纹理;FRHIStagingBuffer 小块以外的 READBACK 资源
2.2统一句柄:FD3D12ResourceLocation
不管落点是什么,FD3D12Buffer、FD3D12Texture、FD3D12UniformBuffer 都只持有一个 FD3D12ResourceLocation。它记录底层的 FD3D12Resource、相对资源起点的 OffsetFromBaseOfResource、GPUVirtualAddress(= 资源基址 + 偏移)、CPU 映射地址和请求大小。绑定视图、做拷贝都只读这几个字段,所以 Buffer 被整理搬到别的池、或者 Dynamic Buffer 换了一块新地址(Rename),上层代码都不受影响。它的类型决定了释放时的行为:
| 类型 | 何时产生 | 释放时做什么 |
|---|---|---|
| eStandAlone | Committed、StandAlone Placed(开启分配追踪时)、Reserved、READBACK Buffer(Lock 读、CPUReadback 纹理等) | 资源进入 RHI 延迟删除队列(DeferDelete) |
| eSubAllocation | 从池或 Buddy 分配器切出的块 | 块交还分配器并挂上 fence,GPU 执行完该帧后才真正并入空闲 |
| eFastAllocation | Bump Page 里的切片(FD3D12FastAllocator、FD3D12FastConstantAllocator) | 什么都不做,切片随整个 Page 复用而失效 |
| eHeapAliased | Transient Heap 上的 Placed Resource | Placed Resource 对象延迟删除;Heap 内区间由 Transient 分配器回收 |
| eAliased · eNodeReference | XR 纹理别名 · 多 GPU 节点引用 | 引用计数递减,最后一个引用释放时才删除 |
2.3尺寸阈值总图
§3Buffer
Buffer 的路由最复杂:同一个 CreateBuffer 可能落到 UploadHeapAllocator、共享 Buffer 池、Placed 池、Reserved 或 Committed。决定因素是 Usage、由 Stride 推出的对齐、状态模式和尺寸。
3.1描述与对齐怎么算
路由之前,FD3D12Buffer::GetResourceDescAndAlignment 先把请求翻译成 D3D12_RESOURCE_DESC 和一个 Alignment:
- Width 向上对齐到 16(
RHI_RAW_VIEW_ALIGNMENT)。ByteAddress 视图按 4 字节一个元素计数,对齐到 16 能保证末尾数据不会在整除时丢掉 源码注释。 - Flags:UAV →
ALLOW_UNORDERED_ACCESS;既不要 SRV 也不是 AS →DENY_SHADER_RESOURCE(进池前会被抹掉,好让更多 Buffer 共用一个池);Shared →ALLOW_SIMULTANEOUS_ACCESS;AS →RAYTRACING_ACCELERATION_STRUCTURE。 - Alignment:Reserved 取 64KB(tile 大小);否则当 Stride > 0,且是 StructuredBuffer 或者不是 ByteAddress / DrawIndirect 时,取
LCM(Stride, 16);其余取 16。
结构化 Buffer 要按 Stride 对齐,是因为它的 SRV/UAV 用元素序号(FirstElement)而不是字节数描述起点,子分配偏移必须是 Stride 的整数倍,视图才指得准 源码注释。这个 Alignment 会直接改变路由和实际占用:
| Buffer | 请求大小 | Alignment | 落点 | 实际占用 |
|---|---|---|---|---|
| ByteAddress · IndexBuffer · Stride 为 0 | 1024B | 16 | 共享 Buffer 池 | 1024B |
| Structured,Stride 32 | 32 × 32 = 1024B | 32 | 共享 Buffer 池 | 1024B |
| Structured,Stride 24 | 24 × 42 = 1008B | 48 | 共享 Buffer 池 | 1280B256 不是 48 的倍数:先加 48,再按 256 取整 |
| Structured,Stride 36 | 36 × 28 = 1008B | 144 | 共享 Buffer 池 | 1280B1008 + 144 = 1152,再按 256 取整 |
| Structured,Stride 100 | 100 × 10 = 1000B | 400 | Placed 池Alignment > 256 | 65536B |
| 任意 Buffer + UAV | 1024B | — | Placed 池MultiState | 65536B |
实践结构化 Buffer 的 Stride 取 16 的倍数且不超过 256,小 Buffer 就能留在 256B 粒度的共享池;LCM(Stride, 16) 一旦超过 256,哪怕只有 1KB 也会按 64KB 放置。
3.2路由阶梯
第一级在 FD3D12Adapter::AllocateBuffer(前两条)与 FD3D12DefaultBufferAllocator::AllocDefaultResource(后三条)里决定去哪个分配器,自上而下判定,先命中者生效:
- Usage 含 Dynamic / Volatile?AllocateBuffer是UploadHeapAllocator AllocUploadResource≤64KB SmallBlockAllocator · ≤64MB BigBlockAllocator · 更大 Committed(§5.1)
- 调用方传入了 ResourceAllocator?RDG Transient 分配器的适配器是Transient Heap 上的 Placed Resource§6
- DrawIndirect,且 d3d12.AllowPoolAllocateIndirectArgBuffers = 0?该 CVar 代码默认值是 1,帮助文本里的 0 已过时,默认不会命中是Committed为规避 UE-115982 驱动崩溃保留的开关
- Usage 含 ReservedResource?是CreateReservedResource之后按需 CommitReservedResource(§7)
- 以上都不是抹掉 DENY_SHADER_RESOURCE,按池键查找或创建池→默认 Buffer 池,进入第二级
第二级决定池的策略(GetResourceAllocationStrategy):
- Alignment > 256?kD3D12ManualSubAllocationAlignment是Placed 池Placed Resource 偏移恒为 0,任何对齐都满足
- 状态模式为 MultiState,或 Default 模式下带 UAV?是Placed 池每个资源需要独立状态
- 以上都不是SingleState→共享 Buffer 池32MB Committed Buffer · 256B 粒度 · 偏移子分配
进池以后还有最后一道闸(FD3D12PoolAllocator::AllocateResource):请求大小超过池的单次上限(DEFAULT Heap 16MB、READBACK 64KB),或者带 ALLOW_SIMULTANEOUS_ACCESS,就不进池,直接 CreateCommittedResource,并把 Buffer 的 Alignment 改回 64KB。
3.3池如何划分
FD3D12DefaultBufferAllocator 并没有预建好的池,只有一个按需增长的 FD3D12PoolAllocator 数组。每个池由 FD3D12ResourceInitConfig(Heap 类型、Heap Flags、资源标志、初始状态)加上分配策略确定,SupportsAllocation 负责匹配,而两种策略的匹配规则不一样:
- 共享 Buffer 池要求配置完全一致。池里所有逻辑 Buffer 共用一个 Committed
ID3D12Resource(FD3D12MemoryPool::Init用CreateBuffer创建),资源标志和初始状态必须相同。进池前抹掉DENY_SHADER_RESOURCE,就是为了让不需要 SRV 的 VB/IB 和需要 SRV 的结构化 Buffer 落进同一个池。 - Placed 池只比较 Heap 类型和 Heap Flags。每个 Placed Resource 有自己的 desc 和状态,Heap(
FD3D12MemoryPool::Init用CreateHeap创建)只关心能不能放 Buffer,所以资源标志各不相同的可写 Buffer 共用同一组 Heap。
| 池 | Heap · 策略 | 池大小 | 池对齐 | 单次上限 | 整理 | 新池创建 |
|---|---|---|---|---|---|---|
| 只读 Buffer | DEFAULT · 共享 | 32MB | 256B | 16MB | 开 | Async,常备 1 个预分配池 |
| 可写 / 高对齐 Buffer | DEFAULT · Placed | 32MB | 64KB | 16MB | 开 | Async,常备 1 个预分配池 |
| Readback PoolFRHIStagingBuffer | READBACK · 共享 | 4MB | 256B | 64KB | 关(非 DEFAULT Heap) | Async,常备 1 个预分配池 |
| 光追 AS | DEFAULT · 共享 | 32MB | 256B | 16MB | 关 | Immediate,无预分配 |
DEFAULT Heap Buffer 池的单次上限(16MB)是池大小(32MB)的一半,任何进池的请求都保证能被一个新池装下;更大的请求本来就少,直接 Committed,CPU 开销可以忽略 源码注释。上限大于池大小的分配器(例如 UploadHeapAllocator 的 BigBlockAllocator:32MB 池、64MB 上限)遇到超过池大小的请求时,会按请求向上取 2 的幂(不超过上限)新建一个池。
3.4共享 Buffer 池内部:链表 + Best-fit
每个 FRHIMemoryPool 用一条按偏移排序的双向链表记录所有块(已分配或空闲),再用一个按大小升序排列的 FreeBlocks 数组做 Best-fit 查找。Best-fit 优先用最贴合的空闲块,把大块尽量留整。
UnlockPoolData)之前保持锁定,整理器不会搬一个还没写内容的块;释放后的块在 GPU 执行完之前保持锁定,既不参与合并,也不会被重新分配。子分配的对齐在池内这样处理(FRHIMemoryPool::GetAlignedSize / GetAlignedOffset):空闲块的偏移永远是池对齐(256B)的倍数;若请求的 Alignment 整除 256,块大小直接按 256 取整;若不整除,先多留一个 Alignment 的余量,再把起点推到 Alignment 的倍数:
FirstElement = 288 / 24 = 12 成为整数;尾部 240B 是按 256B 取整的余量。3.5Placed 池
Placed 池和共享池用的是同一套 FRHIMemoryPool 簿记,区别只有两点:池对齐是 64KB;分配成功后要在 AlignDown(offset, 64KB) 处调用 CreatePlacedResource,得到一个独立的 ID3D12Resource。于是每个 UAV Buffer 都有自己的状态,可以被跟踪、单独转换,代价是每块至少 64KB。
对 Buffer 而言,Placed 和 Committed 的取整浪费其实一样(都是 64KB 的倍数)。所以池化 Placed Resource 省下的不是粒度,而是创建成本和驻留对象数量:32MB 的 Heap 只建一次,之后 Placed Resource 只是在已存在的物理内存上创建资源对象 推断。
3.6初始数据、Lock 与 Dynamic Buffer
| 操作 | Staging 从哪里来 | 如何进入目标 | 要点 |
|---|---|---|---|
| 创建时带初始数据在渲染线程或 RHI 线程 | FD3D12FastAllocator4MB Bump Page;>4MB 转 AllocUploadResource | 排队 CopyBufferRegion。目标在共享池时,要把整个大 Buffer 临时转到 COPY_DEST 再转回 | 源码注释自认「不够优化,应当批处理」 |
| 创建时带初始数据在其他线程(如异步加载) | AllocUploadResource | 同上 | Staging 可能要跨帧存活,不适合按帧回收的 Bump Page 推断 |
| 可跟踪状态的 Placed Buffer 带初始数据 | 同上 | 直接以 COPY_DEST 创建,省掉一次转换 | 共享池里的 Buffer 做不到 |
| Dynamic:首次 Lock,或 WriteOnly_NoOverwrite | 无,本身就在 UPLOAD Heap | 直接写映射地址 | — |
| Dynamic:再次 WriteOnly Lock | AllocUploadResource 新分配一块大小 = 整个 Buffer | 在 RHI 时间线上 Rename 到新地址,旧块走 fence 延迟释放 | 效果等同 D3D11 的 MAP_WRITE_DISCARD |
| 静态 Buffer:Lock 写 | AllocUploadResource512B 对齐 | Unlock 时排队 CopyBufferRegion | 不看线程,一律走 UploadHeapAllocator |
| 静态 Buffer:Lock 读 | Committed READBACK | 排队拷贝后 SubmitAndBlockUntilGPUIdle | 同步等待 GPU,代价很高 |
新分配的池块默认处于锁定状态:带初始数据的 Buffer 在拷贝完成后才 UnlockPoolData,没有初始数据的 Buffer 创建后立即解锁。只有解锁的块才会被整理器搬动。
对比Buffer 只有初始数据按线程选 Staging(渲染 / RHI 线程用 FastAllocator,其他线程用 AllocUploadResource),Lock 写一律走 AllocUploadResource。Texture 的 Initializer 与 Lock 写都不看线程,一律先走 FastAllocator(RHIAsyncCreateTexture2D 例外),见 §4.6。
3.7光追 Buffer
- 加速结构(AS):
CreateRayTracingBuffer以RAYTRACING_ACCELERATION_STRUCTURE标志、256B 对齐、SingleState、BVHRead | BVHWrite 创建,于是走共享 Buffer 策略,进入 AS 专用池(32MB、单次 ≤16MB、Immediate、不整理),整个池的驻留优先级设为 HIGH;超过 16MB 的 Committed AS 单独设 HIGH。 - AS 能共享大 Buffer 的原因:AS 始终处于 RAYTRACING_ACCELERATION_STRUCTURE 状态,构建与引用都通过「GPU 虚拟地址 + 偏移」,完全符合约束 ③。
- AS 池不整理的原因:AS Buffer 可能创建后还没构建,对它执行
CopyRaytracingAccelerationStructure会让 GPU 崩溃;不能整理意味着浪费更多,所以单独配了一组池参数 源码注释。 - Scratch:UAV + Default 状态模式 → MultiState → Placed 池,并单独提升驻留优先级。源码注释的解释是:剩下的 scratch 分配已经不多,为它们专开单状态 Heap 反而浪费。
- 着色器绑定表(SBT):MultiState、64B 对齐(
D3D12_RAYTRACING_SHADER_TABLE_BYTE_ALIGNMENT)→ Placed 池;数据先写进 FastAllocator,再走拷贝队列上传。
§4Texture
纹理的实际大小由驱动决定,必须用 GetResourceAllocationInfo 查询。路由的关键只有两个问题:能不能 4KB 对齐?是不是只读?
4.1对齐怎么定
FD3D12TextureAllocatorPool::AllocateTexture 先设置 Desc.Alignment,再向驱动查询真实的 SizeInBytes 与 Alignment(纹理大小因架构而异,可能比像素数据本身大)官方文档:
- 能 4KB 对齐 → 4KB(
D3D12_SMALL_RESOURCE_PLACEMENT_ALIGNMENT); - 否则 MSAA → 4MB,其余 → 64KB。
FD3D12Texture::CanBe4KAligned 的条件:非 Reserved;不是 NV12 / P010 视频格式;没有 RT / DS 标志;单采样;按「一块 4KB」的 tile 形状估算,最高 mip 需要的 tile 数 ≤ 16。这与官方规则一致:最详细 mip 的估算大小不超过 64KB(16 × 4KB)时才允许 4KB 对齐 官方文档。
Get4KTileShape 按格式的每像素位数算出 4KB 能装多少 texel(32bpp 为 32×32,BC7 为 16×16 个块,BC1 / BC4 宽度再翻倍),再数最高 mip 需要几块。UE 按 Width × Height × DepthOrArraySize 计数,纹理数组的长度也会计入。4.2路由阶梯
第一级在 CreateTextureInternal / SafeCreateTexture2D:
- 调用方传入了 ResourceAllocator?RDG Transient是Transient Heap 上的 Placed Resource§6
- TexCreate_CPUReadback?是Committed READBACK Buffer大小由 GetCopyableFootprints 计算
- TexCreate_ReservedResource?仅 2D / 3D是CreateReservedResource64KB_UNDEFINED_SWIZZLE 布局;带 ImmediateCommit 时立即提交全部(§7)
- 以上都不是→TextureAllocatorPool,进入第二级
第二级选池:
- 带 RT 或 DS 标志?是RenderTarget 池 → 默认关闭 → Committed池大小 d3d12.PoolAllocator.RTUAVTextureVRAMPoolSize = 0
- 带 UAV 标志,或需要 BCn UAV 别名?是UAV 池 → 默认关闭 → Committed
- 能 4KB 对齐?是ReadOnly4K 池4MB Heap · 4KB 粒度 · 单次 ≤4MB
- 以上都不是→ReadOnly 池64MB Heap · 64KB 粒度 · 单次 ≤64MB
最后一道闸与 Buffer 相同:请求超过池的单次上限、带 Shared,或者 Alignment 大于池对齐(MSAA 的 4MB),就改走 Committed。开启分配追踪时,这条回退路径改为「单独 CreateHeap + CreatePlacedResource」,见 4.5。
4.3四个纹理池
| 池 | Heap Flags | Heap 大小 | 池对齐 | 单次上限 | 状态跟踪 | 整理 |
|---|---|---|---|---|---|---|
| ReadOnly4K | ALLOW_ONLY_NON_RT_DS_TEXTURES | 4MB | 4KB | 4MB | 不跟踪(只读) | 关块都是 4KB 的倍数且很小,本来就不易碎片化 源码注释 |
| ReadOnly | ALLOW_ONLY_NON_RT_DS_TEXTURES | 64MB | 64KB | 64MB | 不跟踪(只读) | 开 |
| RenderTarget默认关闭 | ALLOW_ONLY_RT_DS_TEXTURES | 0 | 64KB | 0 | MultiState | 关重建 Placed Resource 时取不到 ClearValue 源码注释 |
| UAV默认关闭 | ALLOW_ONLY_NON_RT_DS_TEXTURES | 0 | 64KB | 0 | MultiState | 关处理不了 BCn / UINT 的 UAV 别名 源码注释 |
纹理池全部是 Placed 策略,因为纹理不存在「在一个大资源里按偏移切」的用法:每张纹理必须是独立的 ID3D12Resource。
4.4为什么 RT / DS / UAV 纹理默认不池化
源码只写了这两个池不能整理的原因,没有写默认关闭池化的原因。以下是结合机制的分析 推断:
- 尺寸大,粒度收益小。屏幕尺寸的颜色、深度、UAV 纹理动辄数 MB 到数十 MB,64KB 取整的浪费占比可以忽略。
- 即使池化也不能整理。两个池都关闭了整理,释放后留下的空洞只能等整个 Heap 清空,长时间运行碎片会累积。
- 高频的 Transient RT 已有更合适的去处。RDG 图内的 Transient RT 走 Transient Heap(§6);长寿命的池化 RT 创建频率低,Committed 的创建成本可以接受。
- 驻留粒度更细。Committed 资源各自是驻留单位,不会因为用到一张小 RT 就把整个 Heap 拉进显存(§9)。
确有需要时,可在启动配置里把 d3d12.PoolAllocator.RTUAVTextureVRAMPoolSize 与 RTUAVTextureMaxAllocationSize 设为非零来启用(两者都是只读 CVar)。
4.5其他细节
- 尺寸查询有缓存与不缓存两种。纹理池分配时调用
GetResourceAllocationInfoUncached;RHICalcTexturePlatformSize与 Transient 路径用按 desc 哈希缓存的版本。 - Shared 纹理(
ALLOW_SIMULTANEOUS_ACCESS)一律 Committed,并加上D3D12_HEAP_FLAG_SHARED。 - MSAA 纹理的 Alignment 为 4MB,大于任何纹理池的池对齐,必然 Committed 源码注释。
- 分配追踪会改变分配形态。当
D3D12.TrackAllAllocations、GPU 崩溃调试或内存 Trace 开启(且 Resource Heap Tier 2)时,StandAlone 纹理不再 Committed,而是「单独 CreateHeap + CreatePlacedResource」,以便拿到 GPU 虚拟地址来定位崩溃;池 Heap 的 Flags 也改为ALLOW_ALL_BUFFERS_AND_TEXTURES。用 Insights 分析显存时要意识到这一点。 - XR 纹理别名(eAliased)让多个 ResourceLocation 引用同一资源,靠引用计数管理,源码注释明确建议尽量避免。
4.6初始数据、Lock 与 Update
Texture 选 Staging 的方式与 Buffer 不同:Initializer、Lock 写、UpdateTexture2D / 3D 都不看调用线程,一律先向 FD3D12FastAllocator 要(超过 4MB 由它转交 AllocUploadResource);只有 RHIAsyncCreateTexture2D 直接走 UploadHeapAllocator。
| 操作 | Staging 从哪里来 | 如何进入目标 | 要点 |
|---|---|---|---|
| RHICreateTextureInitializerBulkData · Initializer | FD3D12FastAllocator整个 mip 链一块 · 512B 对齐 | 纹理以 COPY_DEST 创建;UploadInitialData 逐 subresource 排队 CopyTextureRegion,再转到目标状态并 UnlockPoolData | 没有线程判断,与 Buffer 的 AllocateUploadMemory 不同 |
| RHIAsyncCreateTexture2D | AllocUploadResource总大小 >4MB 且多 mip 时,mip0 与其余 mip 各分一块 | 在 Copy Queue 上 UpdateSubresources,完成后由任务 UnlockPoolData | 注释称 4MB 以上新建 Page 的耗时会陡增,拆出 mip0 以免落入 StandAlone 回退而卡顿 源码注释 |
| Lock 写RLM_WriteOnly | FD3D12FastAllocatorSubresource 大小 · 512B 对齐 | Unlock 时排队 CopyTextureRegion | Dynamic 纹理:Staging 若是 StandAlone 或来自 BigBlockAllocator(AT_Pool),Unlock 后经延迟删除回收给该纹理,下次 Lock 直接复用 源码注释 |
| UpdateTexture2D | 整个 mip 且在 Immediate 命令列表上 → 走 Lock / Unlock;否则 FD3D12FastAllocator | 排队 CopyTextureRegion | — |
| BeginUpdateTexture3D | FD3D12FastAllocator开启对应 CVar 且非块压缩格式时改走 Compute Shader | EndUpdateTexture3D 排队拷贝 | — |
| Lock 读RLM_ReadOnly | CommittedREADBACK Buffer · Subresource 大小 · 每次新建 | 排队 CopyTextureRegion 后 SubmitAndBlockUntilGPUIdle,再 Map | 同步等待 GPU;多 GPU 未支持(源码 ensure) |
§5UPLOAD 与 READBACK Heap:UploadHeapAllocator、FastAllocator、FastConstantAllocator 与回读资源
CPU 可写内存(UPLOAD Heap)的分配特征和显存正好相反:数量极多、尺寸小、寿命短,而且状态恒为 GENERIC_READ,天然适合共享 Buffer 子分配,难点在于速度和按帧回收。CPU 可读内存(READBACK Heap)状态恒为 COPY_DEST,同样可以共享,但回读资源的创建入口分散在多处,见 5.5。
5.1FD3D12UploadHeapAllocator 的三个子分配器
FD3D12UploadHeapAllocator 每个 GPU 一个,内部按用途拆成三个子分配器,底层都是 CreateCommittedResource 建出、常驻 Map 的 UPLOAD Buffer。对外只有两个入口:AllocUploadResource 按大小在前两个之间路由,超过 64MB 时由 BigBlockAllocator 回退到 Committed;AllocFastConstantAllocationPage 只从第三个取 64KB Page。FD3D12FastConstantAllocator 本身不在其中,它只是这些 Page 的使用者(5.4)。
| 分配器 | 服务 | 实现 | 池 | 粒度 | 单次上限 | 为什么这样选 |
|---|---|---|---|---|---|---|
| SmallBlockAllocator | AllocUploadResource ≤64KB | MultiBuddy | 4MB | 512B 起,取整到 2 的幂 | 64KB | 快,但会取整到 2 的幂 源码注释 |
| BigBlockAllocator | AllocUploadResource >64KB | PoolAllocator按偏移的首次适配 · 不整理 | 32MB | 512B | 64MB,超出 Committed | 慢一些,但取整浪费少 源码注释 |
| FastConstantPageAllocator | AllocFastConstantAllocationPage:FD3D12FastConstantAllocator 的 64KB Page | MultiBuddy | 4MB | 64KB 起 | 4MB | 大小固定、通常同帧释放,单独成池以免把其他池切碎 源码注释 |
- 512B 的由来。它是
D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT,官方要求线性子资源拷贝按它对齐 官方文档;以它作最小粒度,Upload Staging 可以直接服务纹理数据拷贝 推断。 - BigBlockAllocator 用首次适配、不整理。空闲数组按偏移排序,取第一个放得下的块。Staging 寿命短、不值得搬动;首次适配让分配集中在低地址端,高地址端保持连续 推断。
- 新池异步预建。
d3d12.UploadHeap.PoolAllocationStrategy = 1(Async):每用掉一个预建池,就在后台线程再建一个(OverFlowPoolsCount = 1)源码注释,避免在渲染线程上同步创建 32MB 资源造成卡顿 推断。DEFAULT Heap Buffer 池采用同样的策略。 - 谁在调用 AllocUploadResource。Dynamic Buffer(创建与再次 Lock)、MultiFrame UB、静态 Buffer 的 Lock 写、工作线程上的 Buffer 初始数据、RHIAsyncCreateTexture2D、FastAllocator 超过 4MB 的请求,以及光追与 ResourceCollection 的若干上传。
- MultiFrame UB 为什么只落在 SmallBlockAllocator。它与 Dynamic Buffer 走同一个入口,但创建时 check 常量缓冲 ≤ 4096 × 16B = 64KB,而 SmallBlockAllocator 的判定是 ≤64KB,所以永远到不了 BigBlockAllocator。
5.2Buddy 分配
5.3FD3D12FastAllocator:4MB Bump Page
5.4FD3D12FastConstantAllocator 与 Uniform Buffer
Uniform Buffer 按生命周期分两条路(D3D12UniformBuffer.cpp):UniformBuffer_MultiFrame 走 UploadHeapAllocator::AllocUploadResource(256B 对齐),因 UB ≤64KB 必然落在 SmallBlockAllocator,可以逐个释放;SingleFrame / SingleDraw 走线程局部的 FTransientUniformBufferAllocator(FD3D12FastConstantAllocator 的子类,每个线程懒创建一个)。命令上下文里的松散着色器常量用各 FD3D12CommandContext 自己的 ConstantsAllocator,同样是 FD3D12FastConstantAllocator。更新 Uniform Buffer 同样是「分配新位置再替换」,不会原地改写 GPU 可能正在读的数据。
D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT)。第二行按比例绘制。小结UPLOAD 侧的共同点是常驻 Map、状态恒定、按帧回收:SmallBlockAllocator(Buddy)管小而碎的 Staging,BigBlockAllocator 管大块,FastAllocator / FastConstantAllocator(Bump Page)管只活一帧的数据。
5.5READBACK:回读资源的创建途径
READBACK Heap 上的资源必须以 COPY_DEST 创建、且不能转换状态 官方文档,所以理论上都能共享一个 Buffer。但 D3D12RHI 里回读资源的入口很分散:只有 FRHIStagingBuffer 经过 FD3D12DefaultBufferAllocator 的 READBACK 池,其余入口都直接调用 FD3D12Adapter::CreateBuffer(内部就是 CreateCommittedResource)或 CreateCommittedResource。
| 入口 | 典型调用方 | 落点 | 大小 · 生命周期 | 如何读取 |
|---|---|---|---|---|
| RHICopyToStagingBufferFD3D12StagingBuffer | FRHIGPUBufferReadback | Readback Pool≤64KB:4MB Committed READBACK Buffer 子分配 · 256B 粒度 · 不整理Committed>64KB | 容量不足时才重新分配,否则复用 | RHILockStagingBuffer 直接返回映射地址 + Offset |
| TexCreate_CPUReadbackCreateTextureInternal · SafeCreateTexture2D | FRHIGPUTextureReadback | CommittedREADBACK Buffer · 大小 = GetCopyableFootprints | 随纹理对象,反复作为拷贝目标 | RHIMapStagingSurface:Fence 未完成则 Wait,再 Map |
| LockBufferRLM_ReadOnly · 静态 Buffer | CPU 同步读 Buffer | Committed大小 = Offset + Size | 每次 Lock 新建,Unlock 后释放 | 排队拷贝后 SubmitAndBlockUntilGPUIdle |
| FD3D12Texture::LockRLM_ReadOnly | CPU 同步读纹理 | Committed大小 = Subresource | 每次 Lock 新建 | 同上 |
| ReadSurfaceData 系列RHIReadSurfaceData · ReadSurfaceFloatData · Read3DSurfaceFloatData · MSAA 版本 | CPU 同步读像素 | Committed临时 READBACK Buffer;MSAA 版本另建一张临时 Committed RT 做逐采样 resolve | 每次调用新建 | 同步读回;非 MSAA 的 RHIReadSurfaceData 若源纹理已在 READBACK Heap 则直接复用 |
| FD3D12QueryHeap | Timestamp · Occlusion · PipelineStatistics 查询 | CommittedCreateCommittedResource(READBACK · COPY_DEST) | 随 Query Heap 常驻 | 创建后 Map 一次,常驻映射 |
结论回读资源真正进池的只有 ≤64KB 的 FRHIStagingBuffer。Lock 读、ReadSurfaceData 这类同步路径每次都新建 Committed 并阻塞等待 GPU,适合调试与工具场景;需要逐帧回读时,用 FRHIGPUBufferReadback / FRHIGPUTextureReadback 这类按 Fence 查询的封装更合适 推断。
§6RDG Transient Resource:Transient Heap 与内存别名
RDG 编译图时知道每个 Transient Resource 在哪个 pass 首次使用、在哪个 pass 最后使用。Transient 分配器正是利用这一点,让生命周期不重叠的资源共用同一段物理内存。
6.1Heap 从哪里来
- Heap 缓存
FD3D12TransientHeapCache挂在 Adapter 上;RDG 每次执行图时创建一个FD3D12TransientResourceHeapAllocator向它借 Heap,执行完归还。 - 新 Heap 大小 =
max(RoundUpToPowerOfTwo(首个分配), 128MB)(RHI.TransientAllocator.MinimumHeapSize)。 - Heap 属性:DEFAULT;Resource Heap Tier 2 时 Buffer、纹理、RT 混放(AllowAll),否则按类型分 Heap;
Desc.Alignment = 4MB,MSAA 纹理也能放;带CREATE_NOT_ZEROED;驻留优先级 HIGH。 - 归还的 Heap 进入空闲列表,闲置超过 16 个回收周期(
RHI.TransientAllocator.GarbageCollectLatency)才销毁。
6.2分配:首次适配 + 生命周期别名
- 对齐与取整:纹理对齐取
max(64KB, GetResourceAllocationInfo 返回的对齐);Buffer 统一按 64KB 对齐并取整。 - Placed Resource 缓存:每个 Heap 按
hash(CreateInfo, Heap 内偏移)缓存 Placed Resource(各 64 个纹理、64 个 Buffer)。下一帧同样的资源落在同样的偏移,就直接复用已有的ID3D12Resource,不再调用CreatePlacedResource。 - 句柄:ResourceLocation 类型为 eHeapAliased 并标记为 Transient,不计入常规 Buffer 统计,内存 Trace 也不单独记录这些 Placed Resource(Heap 本身已记录)。
- 开关:
r.RDG.TransientAllocator为 0 关闭,1 开启(默认),2 仅对带 FastVRAM 标志的资源开启。
§7Reserved Resource(Tiled)
Reserved Resource 先只占 GPU 虚拟地址,物理内存以 64KB tile 为单位按需映射。它解决两个问题:资源要能增长或收缩;不想占用一整块连续的大显存。
7.1机制要点
- 创建:
CreateReservedResource;纹理必须使用D3D12_TEXTURE_LAYOUT_64KB_UNDEFINED_SWIZZLE布局,Alignment 为 0 或 64KB;Buffer 的对齐取 tile 大小,Stride 不得超过 64KB。 - 提交:
CommitReservedResource(RequiredCommitSizeInBytes)把目标大小按 64KB 向上取整,用GetResourceTiling求出各 mip 的 tile 布局,再分段UpdateTileMappings。带TexCreate_ImmediateCommit的纹理在创建时就提交全部。 - 驻留:资源对象本身不注册驻留,驻留跟踪落在每个 Backing Heap 上;命令列表引用该资源时,会把所有 Backing Heap 的驻留句柄加入驻留集合。
- 限制:不能带初始数据、不能是 Dynamic、不能使用自定义分配器(源码 checkf);Reserved Buffer 不能超过创建时的大小增长(UnifiedBuffer 中的 ensure)。
7.2引擎里谁在用
| 系统 | CVar | 默认 | 为什么用 Reserved Resource |
|---|---|---|---|
| GPUScene 实例数据 | r.GPUScene.UseReservedResources | true | 一次预留最大容量,增长时只提交更多 tile,免去重建再拷贝 推断 |
| 虚拟阴影贴图物理页池 | r.Shadow.Virtual.AllocatePagePoolAsReservedResource | 1 | 配合 ImmediateCommit,用 N 个小块代替一整块连续显存,让 WDDM 换入换出更高效 源码注释 |
| 稀疏体积纹理 tile 数据 | r.SparseVolumeTexture.Streaming.UseReservedResources | 1 | 由多个小块物理内存支撑以减少碎片,并突破单资源 2GB 限制 源码注释 |
| Nanite 流送 | r.Nanite.Streaming.ReservedResources | 0(实验) | 更好的显存利用率、更高效的扩缩 源码注释 |
| Hair 曲线簇数据 | — | 必需 | 要求平台支持 Reserved Resource(源码 check) |
§8延迟释放、整理与回收
分配只是开始。块什么时候真正可复用、碎片怎么收拢、空池什么时候还给系统,全都挂在帧节奏上。
8.1Frame fence 与每帧节奏
FD3D12ManualFence 是一个手动推进的帧计数器:渲染线程在 RHIEndFrame_RenderThread 调用 AdvanceTOP,RHI 线程在 RHIEndFrame 末尾调用 AdvanceBOP,GetCompletedFenceValue 返回 GPU 已经完成的值。释放请求记下 GetNextFenceToSignal(),也就是「下一个将要发出的 fence」;只有 GPU 执行完这一帧的工作,IsFenceComplete 才返回 true,块才真正回到空闲状态。
| 对象 | 何时可以复用 | 空闲后多久还给系统 |
|---|---|---|
| 池内块Buffer 池、纹理池、BigBlockAllocator | 记录的 fence 完成后,下一次 CleanUpAllocations 时 | 整个池为空,且最后使用的 fence + 20 ≤ 已完成 fence 时销毁Buffer 池处的代码注释写的是 2 帧,实际参数为 20 |
| Buddy 块SmallBlockAllocator、FastConstantPageAllocator | 同上(DeferredDeletionQueue) | 整个 Buddy 池(4MB Buffer)为空且闲置 ≥20 帧时销毁 |
| Bump PageFD3D12FastAllocator | Page 的 FrameFence 已完成且无人引用时,可被重新取出 | 池内超过 5 个 Page 且闲置 ≥10 帧,每帧最多释放 1 个 |
| Committed / StandAlone | 进入 RHI 延迟删除队列,GPU 用完后 Release | Release 即归还 |
| Transient Heap | 图执行结束,归还 Heap 缓存 | 闲置 ≥16 个回收周期 |
| Reserved Backing Heap | Decommit 时 DeferDelete | 走延迟删除后归还 |
8.2碎片整理(Defrag)
- 哪些池会整理:只有 DEFAULT Heap 上开启整理的池,即只读 / 可写 Buffer 池与 ReadOnly 纹理池。其余池不整理的原因见 §3.3、§4.3。
- 哪些块会被搬:已分配、未锁定、有 owner 的块。刚创建还没写入初始数据的块、正在被整理的块都是锁定的。
- 分配顺序配合整理:开启整理的分配器每次 CleanUp 后按已用量给池排序,新分配优先进最满的池 源码注释,空的池于是越来越空,最终被 20 帧回收规则销毁。不整理的分配器则优先小池,并按池序号稳定排序。
- 代价:搬一个 Placed 块要重建 Placed Resource、重建视图,再做一次 GPU 拷贝。每帧拷贝预算为 32MB,Buffer 与纹理各一份(
d3d12.VRAMBufferPoolDefrag.MaxCopySizePerFrame、d3d12.VRAMTexturePoolDefrag.MaxCopySizePerFrame);每次最多处理 1 个池(RHIPoolAllocator.DefragMaxPoolsToClear),在池之间轮转。
8.3池的增长
新池有两种来源:Immediate 在需要时于当前线程同步创建;Async 先取用预建的备用池,同时在后台线程再补建一个。DEFAULT Heap Buffer 池和 UploadHeapAllocator 使用 Async,AS 池和纹理池使用 Immediate。备用池创建时还不知道自己的池序号,被取用时才初始化空闲链表(InitFreeBlocks)。
§9驻留与 Heap Flags
分配决定资源放在哪里,驻留(Residency)决定它此刻是否真的占着显存。池化会让驻留粒度变粗,这是池化不太显眼的代价。
9.1默认不驻留,按需拉入
- 开关:
D3D12.ResidencyManagement = 1、D3D12.ResourcesStartResident = 0。Heap 与 Committed 资源以D3D12_HEAP_FLAG_CREATE_NOT_RESIDENT创建,创建时不占显存预算。该标志需要ID3D12Device8,系统不支持时强制改为创建即驻留。 - 驻留对象:每个 Committed 资源、每个 Heap(池 Heap、Transient Heap、Reserved Resource 的 Backing Heap)各持有一个
FD3D12ResidencyHandle,注册到 D3DX12Residency 管理器。池里的 Placed Resource 没有自己的句柄,查询时返回所在 Heap 的句柄。 - 何时拉入:命令列表提交前,上下文把本批命令用到的资源句柄(
UpdateResidency)放进驻留集合,由管理器确保它们驻留;超出预算时,按最近最少使用的顺序驱逐其他对象。 - 多 GPU:只跟踪 GPU 独占的 Heap,CPU 可访问的 Heap 不参与驻留管理。
9.2Heap Flags 速查
| 标志 / 设置 | 用在哪里 | 作用 |
|---|---|---|
| HEAP_FLAG_CREATE_NOT_RESIDENT | 池 Heap、Transient Heap、Committed 资源(不以驻留状态启动时) | 创建时不占显存预算,首次被引用才驻留 |
| HEAP_FLAG_CREATE_NOT_ZEROED | 池 Heap、Transient Heap;Committed 资源中非 RT / DS 的 | 跳过清零,加快创建;RT / DS 的 Committed 资源不加此标志 |
| HEAP_FLAG_ALLOW_ONLY_BUFFERS | Buffer 池、Buffer 的 Reserved Backing Heap | Heap 内只放 Buffer |
| ALLOW_ONLY_NON_RT_DS_TEXTURES ALLOW_ONLY_RT_DS_TEXTURES | 纹理池、纹理的 Reserved Backing Heap | 按是否 RT / DS 分 Heap |
| ALLOW_ALL_BUFFERS_AND_TEXTURES | Transient Heap(Tier 2)、开启分配追踪时的池 Heap | 混放;追踪时需要在 Heap 上放一个临时 Buffer 来取得 Heap 的 GPU 虚拟地址 |
| HEAP_FLAG_SHARED | Shared 资源(一律 Committed) | 跨进程 / 跨设备共享 |
| RESIDENCY_PRIORITY_HIGH | AS 池与大 AS、Scratch、Transient Heap、RT / DS / UAV 的 Reserved Backing Heap | 降低被驱逐的概率 |
§10数值与 CVar 速查
表中 d3d12.* 池参数与 RHI.TransientAllocator.* 多为只读 CVar,需要在启动前通过配置文件设置;RHIPoolAllocator.* 与 d3d12.FastAllocator.MinPagesToRetain 可在运行时修改。
| 数值 | 含义 | 宏 / CVar | 位置 |
|---|---|---|---|
| 对齐 | |||
| 16B | Buffer Width 对齐、默认 Buffer Alignment | RHI_RAW_VIEW_ALIGNMENT | RHIDefinitions.h |
| 64B | 光追着色器表对齐 | D3D12_RAYTRACING_SHADER_TABLE_BYTE_ALIGNMENT | d3d12.h |
| 256B | 共享 Buffer 池粒度,超过即 Placed | kD3D12ManualSubAllocationAlignment | D3D12Allocation.h |
| 256B | 常量缓冲放置对齐、AS 字节对齐 | D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BYTE_ALIGNMENT | d3d12.h |
| 512B | SmallBlockAllocator 最小块、BigBlockAllocator 对齐、静态 Buffer Lock 写与 Texture Staging 对齐 | D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT | d3d12.h |
| 4KB | 小纹理放置对齐、ReadOnly4K 池 | D3D12_SMALL_RESOURCE_PLACEMENT_ALIGNMENT | d3d12.h |
| 64KB | Buffer 与默认纹理放置对齐、Placed Buffer 池、Transient 最小对齐、Reserved tile | D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT MIN_PLACED_RESOURCE_SIZE | d3d12.h · D3D12Allocation.h |
| 4MB | MSAA 纹理放置对齐、Transient Heap 的 Desc.Alignment | D3D12_DEFAULT_MSAA_RESOURCE_PLACEMENT_ALIGNMENT | d3d12.h |
| Buffer 池 | |||
| 32MB / 16MB | DEFAULT Heap Buffer 池大小 / 单次上限 | BUFFER_POOL_DEFAULT_POOL_SIZE BUFFER_POOL_DEFAULT_POOL_MAX_ALLOC_SIZE | D3D12Allocation.cpp |
| 4MB / 64KB | Readback Pool 大小 / 单次上限 | READBACK_BUFFER_POOL_DEFAULT_POOL_SIZE READBACK_BUFFER_POOL_MAX_ALLOC_SIZE | D3D12RHIDefinitions.h |
| 32MB / 16MB | AS 池大小 / 单次上限 | BUFFER_POOL_RT_ACCELERATION_STRUCTURE_POOL_SIZE BUFFER_POOL_RT_ACCELERATION_STRUCTURE_MAX_ALLOC_SIZE | D3D12Allocation.cpp |
| 1 · 32MB | Buffer 池整理开关 · 每帧拷贝预算 | d3d12.VRAMBufferPoolDefrag d3d12.VRAMBufferPoolDefrag.MaxCopySizePerFrame | D3D12Allocation.cpp |
| 1 · 1 | 新池 Async · 预建池数 | d3d12.DefaultBuffer.PoolAllocationStrategy d3d12.DefaultBuffer.OverFlowPoolsCount | D3D12Allocation.cpp |
| 1 | 间接参数 Buffer 允许进池 | d3d12.AllowPoolAllocateIndirectArgBuffers | D3D12Allocation.cpp |
| 纹理池 | |||
| 64MB / 64MB | ReadOnly 池 Heap 大小 / 单次上限 | d3d12.PoolAllocator.ReadOnlyTextureVRAMPoolSize d3d12.PoolAllocator.ReadOnlyTextureMaxAllocationSize | D3D12Allocation.cpp |
| 0 / 0 | RT、UAV 纹理池(默认关闭) | d3d12.PoolAllocator.RTUAVTextureVRAMPoolSize d3d12.PoolAllocator.RTUAVTextureMaxAllocationSize | D3D12Allocation.cpp |
| 4MB / 4MB | ReadOnly4K 池(硬编码) | — | D3D12Allocation.cpp |
| 1 · 32MB | 纹理池整理开关 · 每帧拷贝预算 | d3d12.VRAMTexturePoolDefrag d3d12.VRAMTexturePoolDefrag.MaxCopySizePerFrame | D3D12Allocation.cpp |
| 整理(通用) | |||
| 0.9 | 使用率不低于该值的池不整理 | RHIPoolAllocator.DefragSizeFraction | RHIPoolAllocator.cpp |
| 1 | 每次整理最多处理的池数(负数表示全部、不分帧) | RHIPoolAllocator.DefragMaxPoolsToClear | RHIPoolAllocator.cpp |
| UPLOAD 与常量 | |||
| 64KB / 4MB | SmallBlockAllocator 上限 / 池大小 | d3d12.UploadHeap.SmallBlock.MaxAllocationSize d3d12.UploadHeap.SmallBlock.PoolSize | D3D12Allocation.cpp |
| 64MB / 32MB | BigBlockAllocator 上限 / 池大小 | d3d12.UploadHeap.BigBlock.MaxAllocationSize d3d12.UploadHeap.BigBlock.PoolSize | D3D12Allocation.cpp |
| 1 · 1 | UploadHeapAllocator 新池 Async · 预建池数 | d3d12.UploadHeap.PoolAllocationStrategy d3d12.UploadHeap.OverFlowPoolsCount | D3D12Allocation.cpp |
| 64KB | FastConstantAllocator Page 大小(也是 FastConstantPageAllocator 的最小块) | d3d12.FastConstantAllocatorPageSize | D3D12Allocation.cpp |
| 4MB | FastAllocator Page 大小(硬编码) | FD3D12Device 构造函数 | D3D12Device.cpp |
| 5 | FastAllocator 至少保留的 Page 数 | d3d12.FastAllocator.MinPagesToRetain | D3D12Allocation.cpp |
| Transient 与 Reserved | |||
| 1 | RDG 使用 Transient 分配器 | r.RDG.TransientAllocator | RenderGraphPrivate.cpp |
| 128MB | Transient Heap 最小尺寸 | RHI.TransientAllocator.MinimumHeapSize | RHICoreTransientResourceAllocator.cpp |
| 64 · 64 | 每个 Transient Heap 缓存的 Placed 纹理 / Buffer 数 | RHI.TransientAllocator.TextureCacheSize RHI.TransientAllocator.BufferCacheSize | RHICoreTransientResourceAllocator.cpp |
| 16 | Transient Heap 回收延迟(周期) | RHI.TransientAllocator.GarbageCollectLatency | RHICoreTransientResourceAllocator.cpp |
| 16MB | Reserved Resource 的 Backing Heap 大小 | d3d12.ReservedResourceHeapSizeMB | D3D12Resources.cpp |
| 回收与驻留 | |||
| 20 · 20 · 20 · 10 | UploadHeapAllocator · 纹理池 · Buffer 池 · FastAllocator Page 的清理帧延迟 | 硬编码 | D3D12Adapter.cpp · D3D12Allocation.cpp · D3D12RHI.cpp |
| 1 · 0 | 驻留管理开关 · 创建即驻留 | D3D12.ResidencyManagement D3D12.ResourcesStartResident | D3D12Adapter.cpp |
| 0 | 追踪全部分配(开启后分配形态会改变,见 §4.5) | D3D12.TrackAllAllocations | D3D12Adapter.cpp |
展望本版本 D3D12RHI 没有使用 D3D12_RESOURCE_FLAG_USE_TIGHT_ALIGNMENT(源码中无引用)。按新规范,紧凑对齐的 Buffer 放置对齐可以低到 256B 以内 官方文档;一旦采用,Placed Buffer 的 64KB 取整不再成立,共享 Buffer 池存在的主要理由(约束 ②)会被削弱,但单状态约束 ③ 依然存在 推断。
§11源码索引与参考
路径相对引擎根目录,行号对应本文依据的 UE 5.8.1 源码。
- D3D12RHI/Private/D3D12Allocation.h
分配器声明;kD3D12ManualSubAllocationAlignment(25);MIN_PLACED_RESOURCE_SIZE(73) - D3D12RHI/Private/D3D12Allocation.cpp
CVar 默认值(19–190)· Buddy 后备资源 Initialize(278–374)· Buddy(381–771)· MultiBuddy(777–1032)· UploadHeapAllocator(1041–1187:三个子分配器 1041–1087、AllocUploadResource 1103–1137、AllocFastConstantAllocationPage 1140–1148)· DEFAULT Buffer 池(1418–1747)· 纹理池(1754–1934)· FastAllocator(2094–2305)· FastConstantAllocator(2307–2349) - D3D12RHI/Private/D3D12PoolAllocator.h / .cpp
策略枚举(h 7–19)· 池初始化:Placed 用 CreateHeap、共享用 CreateBuffer(cpp 45–164)· 策略判定(231–247)· AllocateResource(345–564)· 释放(595–667)· 整理(690–777)· CleanUp(780–863)· 拷贝(979–1075) - RHICore/Private/RHIPoolAllocator.cpp
对齐计算(252–277)· TryAllocate 与 Best-fit(363–407、475–515)· 合并(531–579)· 池排序(932–958)· Defrag(961–1073) - D3D12RHI/Private/D3D12Resources.h / .cpp
FD3D12ResourceLocation(h 613–791)· CommitReservedResource(cpp 223)· Create*Resource(784–1150,CreateBuffer 即 CreateCommittedResource 1123–1150)· ReleaseResource(1297–1400)· OnAllocationMoved(1498–1624) - D3D12RHI/Private/D3D12Buffer.cpp
AllocateUploadMemory 线程分流(78–95)· AllocateBuffer(168–233)· GetResourceDescAndAlignment(377–424)· CreateBufferInternal(474–519)· CreateBufferInitializerForWriting(521–561)· Lock / Unlock(637–834) - D3D12RHI/Private/D3D12Texture.cpp
CanBe4KAligned(181–211)· GetResourceDesc(224–361)· SafeCreateTexture2D(481–587)· CreateTextureInternal(621 起,CPUReadback 695–708)· CreateTextureInitializerForWriting(1029–1067)· RHIAsyncCreateTexture2D(1154 起)· Lock / UploadInitialData / Unlock(1958–2298)· UpdateTexture2D(2300–2399)· BeginUpdateTexture3D(2693–2736) - D3D12RHI/Private/D3D12UniformBuffer.cpp
UB 上限 check(61)· MultiFrame / SingleFrame 分流(73–90、191–206) - D3D12RHI/Private/D3D12TransientResourceAllocator.cpp
Transient Heap 创建(9–87)· 纹理与 Buffer 的 Placed Resource(137–242) - RHICore/Private/RHICoreTransientResourceAllocator.cpp
首次适配与别名(132–296)· 默认参数(521–530)· Heap 缓存(542–627) - D3D12RHI/Private/D3D12RayTracing.cpp
CreateRayTracingBuffer(3688–3749)· 着色器绑定表(1757–1784) - D3D12RHI/Private/D3D12RHI.cpp · D3D12Adapter.cpp
帧节奏(RHI 547–612)· EndFrame(Adapter 1837–1875)· 驻留 CVar(Adapter 29–46)· NOT_ZEROED / NOT_RESIDENT 支持检测(Adapter 1101–1145) - D3D12RHI/Private/D3D12Adapter.h · D3D12CommandContext.h · D3D12Device.cpp
FTransientUniformBufferAllocator(Adapter.h 133)· GetTransientUniformBufferAllocator(Adapter.cpp 1877–1892)· ConstantsAllocator(CommandContext.h 570)· DefaultFastAllocator 4MB UPLOAD(Device.cpp 118) - D3D12RHI/Private/D3D12Commands.cpp · D3D12DirectCommandListManager.cpp
RHICopyToStagingBuffer(Commands 214–289)· FD3D12StagingBuffer::Lock(DirectCommandListManager 114–127) - D3D12RHI/Private/D3D12RenderTarget.cpp
GetStagingTexture(367 起)· ReadSurfaceDataMSAARaw(670 起)· RHIMapStagingSurface(850–888)· RHIReadSurfaceFloatData(907 起)· RHIRead3DSurfaceFloatData(1038 起) - D3D12RHI/Private/D3D12Query.cpp
Query Heap 结果 Buffer(99–124) - RHI/Private/RHIGPUReadback.cpp
FRHIGPUBufferReadback 使用 Staging Buffer(45–55)· FRHIGPUTextureReadback 创建 CPUReadback 纹理(150–180)· MapStagingSurface(223) - D3D12RHI/Private/D3D12RHIDefinitions.h · Windows/WindowsD3D12RHIDefinitions.h
平台开关(USE_BUFFER_POOL_ALLOCATOR 等)与池尺寸宏 - D3D12RHI/Private/D3D12Util.h
GetTilesNeeded · Get4KTileShape(399 起)
11.1官方文档
- D3D12_RESOURCE_DESC:Alignment 的取值规则、小资源的 4KB / 64KB 条件
- ID3D12Device::GetResourceAllocationInfo:纹理大小因适配器而异;非紧凑对齐的 Buffer 按 64KB 取整
- Residency:Placed Resource 的偏移对齐、Heap 与驻留
- D3D12_HEAP_TYPE:UPLOAD 资源须为 GENERIC_READ,READBACK 资源须为 COPY_DEST 且不能转换状态
- UMA Optimizations: CPU Accessible Textures and Standard Swizzle:512B、256B 等数据放置对齐常量
- Direct3D 12 Tight Placed Resource Alignment:紧凑对齐规范