Skip to main content

Chapter 90: Multi-Threaded Rendering

多线程渲染讨论的是一帧图像如何在 CPU 多核、Direct3D 命令对象、GPU 队列和资源状态之间稳定交接。读完本章后,读者应能把一次帧时间抖动或资源竞争问题拆成几个可定位对象:哪个线程拥有 command recording,哪个资源正在被写入,哪个队列等待哪个 fence,哪个阶段让 GPU 或 CPU 出现空转。

本章的贯穿材料是一帧城市夜景渲染。这个 frame 包含场景更新、可见性剔除、阴影 pass、G-buffer pass、后处理 pass、贴图上传和最终 present。单线程写法会把这些工作串在一个 render thread 上,多线程写法会把其中的 CPU 准备工作分配给 worker thread,再把提交点收束到明确的 queue submit 合约中。

Direct3D 的多线程模型需要区分 D3D11 和 D3D12。D3D11 把 device 和 device context 分开,Microsoft Learn 的 Direct3D 11 multithreading introduction 明确说明 ID3D11Device 可用于多线程资源创建,ID3D11DeviceContext 需要单线程访问约束。D3D12 则取消旧式 immediate context,应用先录制 command list,再把 command list 提交到 command queue;Direct3D 12 work submission 把这个变化解释为提高 CPU 效率和多核扩展性的基础。

本章的核心结论是:多线程渲染的收益来自“可并行录制”和“可控提交”,风险来自“所有权模糊”和“同步过量”。稳定的工程方案先划分 frame 内的任务边界,再规定资源写入者、command allocator 使用者、queue submit 线程、fence 等待位置和诊断证据。

90.1 多线程渲染的必要性与目标

多线程渲染要解决的主问题是 CPU 侧准备工作过于集中。一个 frame 进入 GPU 之前,CPU 需要完成动画更新、场景遍历、剔除、材质分组、descriptor 更新、上传缓冲写入、command list 录制和最终提交。GPU 只接收已经形成的命令流,CPU 在这些工作上阻塞时,GPU 会等待新任务;GPU 被资源状态或 fence 卡住时,CPU 侧提前录制出的命令也无法形成稳定吞吐。

以城市夜景 frame 为例,单线程路径通常是 UpdateScene → Cull → Upload → RecordShadow → RecordGBuffer → RecordPost → Submit → Present。这个路径的好处是所有权清晰,代价是所有 CPU 工作排队。多线程路径会把 update、culling、上传准备和多个 pass 的 command recording 拆给 worker thread,但最终仍需要一个清晰的提交顺序。多线程渲染追求的是缩短 CPU 准备时间,并让 GPU 持续获得可执行命令;它的工程目标包含帧时间稳定、GPU 队列连续、资源状态可验证和调试路径可复盘。

下面的图只描述本章贯穿 frame 的 CPU 侧任务分解和 GPU 提交边界。图中 worker 负责生成可提交材料,render thread 负责收束依赖并提交。

这条路径里有两个并行层级。第一层是 CPU 任务并行,例如 culling、upload prepare、pass build 可以在不同 worker 上执行。第二层是 GPU 队列并行,例如 copy queue 可以提前上传贴图块,graphics queue 在等待必要 fence 后读取资源。并行层级清楚后,诊断也有了入口:CPU 准备慢看 task duration,GPU 等待长看 queue gap 和 fence wait,资源错误看 barrier 与 ownership 记录。

多线程渲染的目标可以压缩成四个判断点。第一,CPU worker 输出的是“命令材料”,包括排序后的 draw item、descriptor 更新请求、上传段和 command list;worker 直接改全局渲染状态会放大竞态。第二,每个 command list 的录制需要独占 command allocator 和上下文对象;共享 allocator 会把线程安全问题变成随机 crash 或 debug layer 错误。第三,资源写入需要唯一所有者;同一纹理在上传、渲染目标写入、shader 读取之间必须有状态变化证据。第四,提交线程需要掌握跨队列依赖;worker 完成录制只表示 CPU 工作完成,GPU 执行完成还要看 fence。

一个可迁移的多线程帧构建顺序如下:先把一帧拆成 pass graph,再给每个 pass 分配输入资源、输出资源和 command list 目标,然后把 CPU 任务投递给 job system,接着在 render thread 汇总 command list,最后按资源状态和 fence 依赖提交到 queue。这个顺序把“并行”限制在有明确输入输出的区域,降低了共享状态带来的不确定性。

90.2 Direct3D 中的命令队列与线程模型

Direct3D 的线程模型由 API 版本决定。D3D11 的核心对象是 device 与 context:device 负责创建资源,context 负责记录或提交渲染命令。D3D12 的核心对象是 device、command allocator、command list、command queue 和 fence:device 创建对象,allocator 提供命令内存,command list 记录 GPU 命令,queue 执行提交,fence 表示 GPU 进度。多线程渲染设计先要把这些对象的职责分清。

D3D11 的 immediate context 是提交入口,也是 replay command list 的位置。Microsoft Learn 的 Immediate and Deferred Rendering 说明,deferred context 可以在附加线程上记录 command list,immediate context 用来播放这些 command list。这个模型适合把复杂场景拆成多个记录任务,但最终播放仍收束到 immediate context。DXGI 的 Present 也应与 immediate context 放在同一线程语境中处理,减少呈现和 immediate context 并发访问带来的不确定性。

D3D12 的模型更接近显式工作提交。Microsoft Learn 的 Executing and Synchronizing Command Lists 说明,应用可以从多个线程向 command queue 提交 command list,runtime 会按提交顺序序列化这些请求。它也列出 direct、compute、copy 三类常见 queue 类型:direct queue 可承载图形、计算和复制命令,compute queue 承载计算与复制相关命令,copy queue 承载复制命令。工程上通常把图形 pass 提交到 direct queue,把贴图上传和 buffer copy 放到 copy queue,把可独立执行的 compute 工作放到 compute queue。

D3D12 的多线程录制需要同时管理 command list 与 command allocator。Microsoft Learn 的 Creating and recording command lists and bundles 说明,一个 command allocator 同一时刻最多关联一个正在录制的 command list,allocator 的 Reset 需要等 GPU 完成使用它关联的 command list 后再执行,并且同一个 allocator 的 Reset 也需要单线程访问约束。这些规则直接决定了常见的 allocator 池设计:每个 recording worker 持有自己的 allocator,allocator 按 frame latency 分组回收,回收条件由 fence 证明。

下面是一个简化的 D3D12 frame recording 伪代码。它展示对象所有权分配,省略真实错误处理和资源创建细节。

// 简化伪代码:每个 worker 拿到独占 allocator 与 command list。
struct RecordingJob {
PassId pass;
CommandAllocator* allocator;
GraphicsCommandList* list;
PassInputs inputs;
};

void RecordPass(RecordingJob job) {
job.allocator->Reset();
job.list->Reset(job.allocator, PipelineStateFor(job.pass));
BindPassResources(job.list, job.inputs);
EmitResourceBarriers(job.list, job.pass);
EmitDraws(job.list, job.pass);
job.list->Close();
}

void SubmitFrame(FramePlan frame) {
WaitForRecordingJobs(frame.jobs);
ExecuteCopyQueue(frame.copyLists);
SignalCopyFence(frame.uploadFenceValue);
WaitGraphicsQueue(frame.uploadFenceValue);
ExecuteGraphicsQueue(frame.graphicsLists);
SignalFrameFence(frame.frameFenceValue);
}

这段伪代码对应三个合约。RecordPass 内部的 allocator 和 command list 由单个 worker 独占,减少录制阶段的锁。SubmitFrame 集中处理 copy queue 与 graphics queue 的依赖,保证上传结果在图形队列读取前完成。frame fence 用来回收 allocator、upload ring buffer 段和 per-frame descriptor 空间;CPU job 完成状态与 GPU fence 完成状态分属两个层级。

D3D11 与 D3D12 的差异可以用同一组维度比较。D3D11 多线程偏向“多个 deferred context 录制,immediate context 统一播放”,runtime 和 driver 承担更多隐式状态处理。D3D12 多线程偏向“多个 command list 显式录制,queue 显式提交,barrier 和 fence 显式管理”。前者降低了应用层同步对象数量,后者把资源状态、allocator 生命周期和队列依赖暴露给应用,从而让复杂引擎获得更高的控制力。

90.3 Thread-Safe Rendering Ownership and Synchronization Contract

线程安全渲染合约的核心是 ownership。ownership 指某个时间范围内,哪个线程或队列拥有某个对象的读写权。它是一组可执行工程约束,直接映射到 command allocator、command list、descriptor 空间、upload buffer 段、render target、UAV、swap chain back buffer 和 queue submit 顺序。合约写清后,锁的数量通常会减少,因为很多对象通过“每线程独占”或“每帧独占”来管理。

对 CPU 对象,ownership 首先落在 command recording 上。每个 worker 录制自己的 command list,使用自己的 allocator,写入自己的 transient descriptor 区间和 upload suballocation 记录。全局渲染状态在 frame begin 时冻结成 FramePlan,worker 读取 FramePlan,输出 RecordedPass。这样做的效果是把动态共享状态转成不可变输入与局部输出;竞态主要集中到 job completion 队列和 submit 汇总表,而这些位置可以用更小的同步范围处理。

对 GPU 对象,ownership 落在 resource state 和 queue ownership 上。一个 shadow map 在 shadow pass 中作为 depth write,后续 lighting pass 中作为 shader resource。一个 HDR color buffer 在 post pass 中作为 render target,后续 tonemap pass 中作为 shader resource。一个上传目标 texture 先由 copy queue 写入,再由 graphics queue 采样。每次角色变化都需要状态证据:D3D12 使用 transition barrier、UAV barrier、aliasing barrier 表达资源内存访问的顺序要求;Microsoft Learn 的 resource barriers documentation 说明,D3D12 把大部分 per-resource state 管理交给应用,并用 ResourceBarrier 把访问变化告知 driver。

一个实用的 ownership 表应覆盖“谁写、谁读、何时交接、用什么证据证明完成”。下面的表以城市夜景 frame 的资源为例。

对象CPU 所有者GPU 队列所有者交接证据失败症状
Shadow command listShadow workerDirect queuejob done + Close 成功阴影 pass 缺失、debug layer 报 list 未关闭
Upload ring segmentUpload workerCopy queuecopy fence value随机材质错图、常量 buffer 被覆盖
Shadow map textureShadow pass builderDirect queuedepth write 到 shader read barrier阴影闪烁、采样读到旧内容
Particle UAVCompute pass builderCompute 或 Direct queueUAV barrier + fence dependency粒子数量跳变、后处理读取未完成写入
Back bufferPresent threadDirect queue + DXGIpresent 前后 state transitionpresent 错误、frame pacing 抖动

表中的“交接证据”是调试入口。CPU job done 只能证明录制或准备完成;Close 成功只能证明 command list 结束录制;queue fence 才能证明 GPU 执行已经推进到某个点;barrier 只能表达同一 queue 内或命令序列内的资源状态变化;跨 queue 读取还需要 fence 或 queue wait 连接两个执行流。把这些证据混在一起,会导致诊断时把 CPU 竞态误判成 GPU 同步问题,或者把 GPU 等待误判成 worker 负载过高。

同步合约还要规定等待位置。CPU 侧可用 mutex、job counter、condition variable 或 lock-free queue 管理 worker 输出;GPU 侧用 fence 管理队列进度和资源生命周期。等待位置应集中在两个地方:recording 汇总点等待 CPU job 完成,资源回收点等待 GPU fence 达到目标值。把 fence wait 放进每个 worker 会把 GPU 进度和 CPU 任务系统耦合,容易形成线程池阻塞;把所有资源回收都放到 frame begin 则可能造成单帧尖峰。

下面的伪代码展示资源回收与 allocator 回收的判断顺序。

// 简化伪代码:使用 fence 证明 GPU 已经完成上一批命令。
void ReclaimFrameResources(uint64_t completedFence) {
for (FrameBucket& bucket : retiredFrameBuckets) {
if (bucket.fenceValue <= completedFence) {
allocatorPool.Return(bucket.commandAllocators);
descriptorPool.Return(bucket.transientDescriptors);
uploadRing.FreeUntil(bucket.uploadEndOffset);
}
}
}

这个回收函数不关心 worker 是否完成录制,它只关心 GPU 是否完成使用资源。recording ownership 和 lifetime ownership 是两个层级:前者保护 CPU 写命令的过程,后者保护 GPU 读写资源的过程。多线程渲染中的死锁经常来自层级交叉,例如 worker 持有任务锁等待 fence,submit thread 需要同一个任务锁才能提交 signal 该 fence 的 command list。稳定做法是让 submit thread 拥有 queue signal 权限,worker 只产生提交材料。

90.4 资源共享与同步机制

资源共享的工程问题可以按 CPU 可见资源、GPU 资源状态和 descriptor 可见性三层处理。CPU 可见资源包括 upload heap、readback heap、staging 数据和 transient allocation;GPU 资源状态包括 render target、depth、shader resource、copy source、copy dest、unordered access;descriptor 可见性包括 CBV、SRV、UAV、sampler 和 descriptor heap 中的 slot 生命周期。每一层的共享机制不同,混用会产生难定位的视觉错误。

CPU 可见资源最常见的是 upload ring buffer。多个 worker 可以并行申请上传段,但每个段必须在一帧内拥有唯一写入者,并在 GPU 完成读取前保持有效。Microsoft Learn 的 Fence-Based Resource Management 使用 upload heap ring buffer 示例说明,应用可以通过 fence 跟踪 GPU 进度,再决定哪些内存段可复用。这个例子对应真实引擎中的 per-frame constant buffer、skinning buffer、instance buffer 和小纹理更新。

GPU 资源状态共享要靠 barrier 与 queue fence 分工。transition barrier 表达一个 subresource 从一种用途切到另一种用途,例如 render target 到 shader resource。UAV barrier 表达 unordered access view 的读写顺序,例如 compute pass 写粒子 buffer 后,后续 pass 读取或继续写入前需要顺序保证。aliasing barrier 表达两个 placed resource 或 tiled resource 共享同一 heap 区域时的内存使用切换。barrier 解决资源访问顺序,fence 解决队列执行进度;跨 copy queue 和 graphics queue 的资源交接通常同时需要资源状态计划和 fence 依赖。

descriptor 共享更容易被忽视。worker 为材质或对象写 descriptor 时,descriptor slot 的生命周期必须覆盖 GPU 使用时间。一个常见错误是 CPU 在 frame N+1 重写 frame N 的 descriptor slot,GPU 仍在执行 frame N 的 draw call,最终表现为材质贴图偶发错乱。稳定方案是把 descriptor heap 的 transient 区域按 frame latency 分块,或为长期资源使用 persistent descriptor,frame 结束时记录 fence value,等 fence 完成后再回收 transient 区域。

下面的表把资源共享机制和证据对应起来。

共享对象共享方式同步证据观察入口
Upload ring buffer多线程分配、单段单写frame fence 或 copy fence上传偏移、fence value、资源错图
Render targetpass 间角色切换transition barrierPIX / RenderDoc 中的 resource state
UAV buffercompute 或 pixel 写入后再读写UAV barrier粒子、tile list、光照列表结果
Descriptor slotframe 分块或 persistent slotdescriptor 区间 fencedraw call 绑定资源与 heap offset
Command allocator每线程录制、按 fence 回收allocator 对应 fencecommand list close、allocator reset 错误

资源共享的判断顺序是:先定位资源在当前 frame 内的写入者,再列出后续读取者,然后标出同一 queue 内的 barrier,再标出跨 queue 的 fence,最后检查 CPU descriptor 与 upload memory 生命周期。这个顺序从“谁改了数据”开始,能直接连接到视觉结果。比如某个贴图偶发黑块,先查上传段是否被复用,再查 copy queue 是否 signal,接着查 graphics queue 是否 wait,最后查 shader 看到的 descriptor 是否指向正确 subresource。

多线程资源访问还要控制锁粒度。粗锁把整个 renderer 保护起来,race 会减少,但 CPU 并行收益会消失;细锁给每个资源加锁,代码会快速变成无法推理的状态机。更稳定的做法是让共享资源通过分配器和 frame graph 进行所有权转移:worker 申请局部资源句柄,frame graph 记录读写关系,submit 阶段集中生成 barrier 和 queue wait。锁保护的是分配器元数据,资源内容的读写顺序由 pass graph 和 fence 管理。

D3D11 工程也有类似边界。device 可以用于多线程创建资源,context 需要单线程访问约束;deferred context 可以并行录制 command list,immediate context 负责播放。资源更新、map、query 和 playback 的限制要按 D3D11 文档处理,尤其是 deferred context 的 Map 与 query data 读取边界。D3D11 的隐式管理减少了显式 barrier 数量,但应用仍然需要规划资源更新与 command list playback 的线程所有权。

90.5 Threaded Rendering Race and Frame-Time Diagnosis

多线程渲染的问题通常表现为两类症状:画面随机错误和帧时间尖峰。画面随机错误多来自资源生命周期、descriptor 覆盖、command list 未正确关闭、barrier 缺失或跨 queue 依赖缺失。帧时间尖峰多来自 worker 负载不均、submit thread 等待 job、CPU 等待 fence、queue 间等待过长、present pacing 被前面提交节奏拖动。诊断时要先把症状分到 CPU recording、resource ownership、queue submission、fence wait 和 frame pacing 五个位置。

定位 command recording 问题时,先看每个 worker 的任务时间和 command list 状态。若某个 pass worker 长期慢于其他 worker,说明任务切分粒度或 pass 内 draw 排序成本需要调整。若 debug layer 报 command list 未关闭、allocator reset 时机错误、bundle 使用限制错误,应回到 allocator ownership 表。D3D12 文档列出的 ExecuteCommandLists 限制包含 command list 必须 Close、allocator 重置后不可提交旧列表、上一轮执行未完成时不可重复提交同一列表等条件;这些错误都属于 command recording 或生命周期合约失败。

定位 resource ownership 问题时,先查“最后写入者”和“第一次读取者”。如果 shadow map 闪烁,检查 shadow pass 是否写入了当前 frame 的 texture,lighting pass 读取前是否存在 depth write 到 shader resource 的 transition。若粒子 buffer 数量跳变,检查 compute 写入后是否有 UAV barrier,后续 graphics 或 compute pass 是否在正确 queue dependency 后读取。若材质偶发错图,检查 transient descriptor 和 upload segment 的 fence 回收规则。

定位 queue submission 问题时,先看 queue timeline。copy queue 提前执行上传本来应减少 graphics queue 等待;若 graphics queue 长时间等待 copy fence,说明上传提交太晚、上传量过大,或资源被拆成太多小 copy。compute queue 的异步执行也要看真实重叠区域:如果 compute pass 写出的 buffer 被紧接着的 graphics pass 使用,queue overlap 可能被 wait 抵消。此时把 compute pass 并入 graphics queue 或调整 pass 顺序,常常比盲目增加异步队列更稳定。

定位 fence wait 问题时,要区分 CPU 等 fence 和 GPU queue 等 fence。CPU 等 fence 常见于 upload ring buffer 空间不足、allocator 池耗尽或 frame latency 过低;GPU queue 等 fence 常见于 copy/compute 结果被 graphics 过早消费。CPU 等待会在线程 profiler 中表现为 blocked 或 wait event,GPU 等待会在 PIX、RenderDoc 或厂商工具的 queue timeline 中表现为空隙。二者的修复方向不同:CPU 等待通常扩充分配池、延迟回收或减少瞬时上传;GPU 等待通常调整 pass 顺序、合批 copy、提前 dispatch 或减少跨 queue 依赖。

定位 frame pacing 问题时,把 present 放回整帧提交节奏。present 抖动有时来自 GPU 末尾 workload 过重,有时来自 CPU submit 不稳定,有时来自等待 swap chain back buffer。对于 D3D11,DXGI present 与 immediate context 的线程关系要保持一致;对于 D3D12,back buffer 的 state transition、frame fence、swap chain buffer index 和最大帧延迟共同决定 pacing。诊断时先固定测试场景和垂直同步条件,再观察 CPU frame time、GPU frame time、queue idle、present interval 和 fence wait。

一个可复用的诊断顺序如下。先打开 debug layer 或等价 validation,修复明确的 API 使用错误。再记录 CPU job timeline,确认 worker 是否产生负载不均或 submit thread 等待。接着看 GPU queue timeline,确认 graphics、copy、compute 队列是否真的重叠。然后检查资源状态与 descriptor 绑定,确认出错 draw call 读到的资源、subresource、descriptor slot 和 barrier 符合 ownership 表。最后检查 fence 回收日志,把 allocator、descriptor、upload segment 的回收点与 GPU completed fence 对齐。

下面是一段诊断记录模板。它的作用是把一次随机错图或尖峰帧转成可比较证据。

frame: 18420
symptom: "G-buffer pass texture slot 17 flickers"
cpu:
slow_job: "UploadPrepare"
submit_wait_ms: 0.3
gpu:
graphics_queue_gap_ms: 1.8
copy_queue_done_fence: 9201
graphics_wait_fence: 9202
resource:
name: "MaterialAtlasPage_42"
last_writer: "CopyQueue Upload"
first_reader: "GBuffer Pixel Shader"
descriptor_frame_block: 2
hypothesis: "graphics queue waits for a fence value that the copy queue signals after the G-buffer submit batch"
next_check: "move upload submit before G-buffer list execution and verify descriptor slot lifetime"

这个模板的价值在于把“感觉像多线程 race”的问题压缩到几个可证伪字段。若 copy queue 已经 signal 目标 fence,诊断要转向 descriptor slot 或 resource barrier。若 descriptor slot 生命周期覆盖当前 frame,诊断要转向 subresource state 和 shader resource view。若所有资源证据都正确,诊断再回到 shader 输入、材质排序和采样逻辑。多线程问题的排查效率取决于证据层级是否清楚。

最小自检任务

给定一个 D3D12 frame:CPU 有 4 个 worker。Worker A 录制 shadow pass,Worker B 录制 G-buffer pass,Worker C 准备贴图上传并录制 copy command list,Worker D 录制 post pass。贴图上传写入 AlbedoAtlas,G-buffer pass 会采样它。当前实现中,Worker C 完成录制后把 copy list 放入提交队列,render thread 先提交 shadow 和 G-buffer,再提交 copy list,最后提交 post。运行时偶发出现物体 albedo 读取旧贴图,偶尔还出现 upload ring buffer 空间不足导致 CPU 等待。请给出 ownership 表、正确提交顺序和诊断顺序。

答案要点

AlbedoAtlas 的 CPU 写入者是 Worker C 对应的 upload staging 或 upload ring segment,GPU 写入者是 copy queue,第一次 GPU 读取者是 graphics queue 中的 G-buffer pass。它的交接证据应包含 copy list Close 成功、copy queue 执行并 signal 指定 fence、graphics queue 在 G-buffer pass 前等待该 fence,并且 AlbedoAtlas 处于可被 shader resource view 读取的状态。

正确提交顺序应先让 render thread 收集 Worker C 的 copy list,提交到 copy queue 并 signal upload fence;随后 graphics queue 在执行 G-buffer 相关 command list 前 wait 该 fence。shadow pass 如果不依赖 AlbedoAtlas,可以先提交。G-buffer pass 依赖上传结果,需要放在 queue wait 之后。post pass 依赖 G-buffer 输出,需要跟在 G-buffer 的资源状态转换之后。upload ring buffer 的段回收要根据 GPU completed fence,不能根据 Worker C 的录制完成状态回收。

诊断顺序先查 debug layer 是否报告 command list、allocator 或 resource state 错误;再查 copy queue timeline,确认 upload fence 是否早于 G-buffer 执行;接着查 G-buffer draw call 的 descriptor slot 是否指向 AlbedoAtlas 的当前页;然后查 upload ring buffer 的分配和回收日志,确认被复用段的 fence 已完成;最后比较修正前后的 graphics queue gap 和 CPU fence wait,判断问题来自提交顺序、上传容量,还是 descriptor 生命周期。

本章知识点总结

  • 主线目标:多线程渲染把场景更新、剔除、上传准备和命令录制拆到 CPU 多核,并把 GPU 提交收束到明确 queue 合约。
  • 贯穿材料:一帧城市夜景可以拆成 update、culling、upload、shadow、G-buffer、post 和 present,每个阶段都有输入、输出和同步证据。
  • D3D11 模型:D3D11 通过 device、immediate context 和 deferred context 组织多线程资源创建、命令录制和统一播放。
  • D3D12 模型:D3D12 通过 command allocator、command list、command queue、barrier 和 fence 让应用显式管理录制、提交和进度。
  • Allocator 合约:command allocator 适合按 worker 和 frame latency 分池,回收条件由 GPU fence 证明。
  • Ownership 定义:线程安全渲染的关键是规定某个时间范围内谁写对象、谁读对象、何时交接以及用什么证据证明完成。
  • Barrier 作用:transition、UAV 和 aliasing barrier 负责表达资源访问顺序和状态变化,跨 queue 依赖还需要 fence 连接。
  • Descriptor 生命周期:transient descriptor slot 必须覆盖 GPU 使用时间,回收要绑定 frame fence 或更精确的资源 fence。
  • Upload 管理:upload ring buffer 的复用依据是 GPU completed fence,CPU 录制完成状态不足以证明内存可回收。
  • 提交诊断:queue timeline 能区分 copy、compute 和 graphics 是否真正重叠,也能显示 graphics queue 是否被 fence wait 拉开空隙。
  • Race 排查:随机错图先查最后写入者、第一次读取者、descriptor slot、barrier 和 fence,再回到 shader 输入逻辑。
  • 帧时间诊断:CPU 等 fence 多指向分配池和上传容量,GPU queue 等 fence 多指向跨队列依赖和 pass 顺序。