Chapter 4: Command Processing and Scheduling
CPU 调用图形 API 时,真正提交给 GPU 的对象通常是一段整理后的命令流。读完本章后,读者应能追踪一帧从 CPU 生成命令、API 运行时检查状态、命令进入队列、GPU 按依赖执行,到 fence 或 present 回报完成的路径,并能判断卡顿来自 CPU 提交、GPU 执行、同步等待还是命令组织方式。
本章使用一个贯穿帧作为材料:一个实时渲染器每帧执行 shadow pass、main color pass、post-process pass 和 UI pass,同时在后台上传下一帧要用的纹理。这个帧足够小,可以看清 command buffer、queue、barrier、fence、resource lifetime 和多线程 recording 的关系;它也足够接近真实引擎,因为每个 pass 都对应明确的资源读写和状态切换。
本章的 API 边界主要参考 Vulkan Command Buffers、Direct3D 12 Work Submission、Direct3D 12 Multi-engine Synchronization、OpenGL Synchronization 和 Apple 的 Metal Command Structure。这些资料只提供事实边界,正文会把它们整理成可迁移的命令流判断顺序。
本章的核心结论是:命令处理性能由两条路径共同决定。第一条是 CPU 侧的命令生成路径,它关心验证、状态整理、command buffer recording、资源生命周期和提交粒度。第二条是 GPU 侧的执行路径,它关心 queue 依赖、barrier、资源读写顺序、pass 之间的数据流和完成回报。调度问题只有放到这两条路径的交界处观察,才能区分“CPU 没把工作及时提交出去”和“GPU 已经排队但受依赖阻塞”。
4.1 GPU 命令队列与调度模型
GPU 命令队列解决的问题,是把 CPU 生成的大量渲染意图变成 GPU 可以顺序接收、并按依赖推进的工作流。这里的“命令”指 API 层记录的绘制、派发、拷贝、绑定状态、资源转换和同步操作,粒度高于单个 shader 指令。命令队列是这些操作进入 GPU 执行系统的入口,应用通过它表达“这些工作可以开始了”和“这些工作之间有怎样的完成关系”。
在贯穿帧中,CPU 先准备 shadow pass 的深度图写入,再准备 main color pass 对 shadow map 和场景材质的读取,之后准备 post-process 对 color buffer 的读取和写入,最后准备 UI pass 与 present。后台纹理上传通常走 copy queue 或同一 graphics queue 中的 copy 命令。这个帧的命令关系可以压缩成一条主线:upload resources → shadow depth → main color → post process → UI → present。其中 upload 可以与前一帧 GPU 工作重叠,shadow 和 main color 之间存在资源读写依赖,post-process 和 UI 依赖 color target 的前序输出。
下面的图只描述应用可观察的逻辑路径,不假设某个厂商 GPU 的内部微架构。真实硬件会有命令处理器、前端调度、多个执行单元、cache 和内存系统;API 队列给应用暴露的是提交顺序、同步点和完成回报。
这条路径中有三个容易混淆的时间点。第一个时间点是 CPU 录制命令完成,它只说明应用已经把工作描述写进 command buffer 或 driver 内部命令流。第二个时间点是 queue submit 完成,它只说明命令已经进入 API 认可的提交路径。第三个时间点是 GPU 完成信号到达,它才说明命令流中的某个位置已经执行完毕,并且应用可以根据同步对象回收资源或复用 command allocator。
OpenGL 把很多命令组织细节隐藏在 driver 内部。应用调用 glDraw* 后,命令可能先进入 driver 内部缓冲,再由 driver 挑选时机送到 GPU 队列。OpenGL 规范给应用呈现出按顺序生效的模型,但实现可以异步执行;glFlush、glFinish 和 sync object 用来让应用观察或强制推进这条异步路径。这个模型降低了显式管理成本,同时让 CPU 侧提交成本、隐式同步和 driver 决策更难直接定位。
Vulkan、Metal 和 Direct3D 12 把命令缓冲、队列和同步对象显式暴露出来。应用需要先把绘制、拷贝、dispatch、pipeline bind、descriptor bind 和 barrier 录入 command buffer,再提交到 queue。这个模型把一部分 driver 自动工作转移给应用,换来更可控的多线程构建、更少的隐式状态检查和更清楚的资源依赖边界。
调度模型的关键对象可以按角色拆开。command buffer 保存将来要执行的命令序列;queue 接收可执行命令并形成应用可见的完成时间线;barrier 描述同一资源在不同用途之间的访问顺序和可见性;semaphore 或 event 描述队列之间的等待关系;fence 把 GPU 进度回报给 CPU。把这些对象混在一起会导致错误判断,例如把 fence 当成资源状态转换,或者把 barrier 当成 CPU 等待。
在贯穿帧中,shadow pass 写 depth texture,main pass 读取这张 texture。这里需要的是 GPU 内部的资源访问依赖:shadow 写入完成后,depth texture 进入 shader-readable 的访问状态,main pass 的 fragment shader 才读取稳定数据。CPU 只有在复用 depth texture、释放相关内存或重置承载它的 command buffer 内存时,才需要根据 fence 判断 GPU 是否已经越过相应位置。
队列调度还受 queue type 影响。graphics queue 通常能处理 draw、部分 copy 和 compute;compute queue 面向 dispatch;copy queue 面向资源传输。Direct3D 12 把 3D、compute 和 copy 队列区分为不同类型,并通过 fence 等同步对象协调多引擎进度。Vulkan 通过 queue family 暴露队列能力,command pool 与 queue family 绑定。Metal 则通过 command queue 和 command buffer 组织提交,具体并行程度由设备、驱动和命令依赖共同决定。
判断命令队列问题时,先看 CPU 是否持续提交可执行工作,再看 GPU 队列是否存在等待点,最后看资源依赖是否让某个 pass 被迫串行。一个稳定的检查顺序是:确认 frame 是否有命令提交,确认提交是否过细或过少,确认 GPU marker 中 pass 是否连续,确认 fence 等待发生在 CPU 侧还是 GPU queue 侧,确认 barrier 是否覆盖了实际读写资源,最后判断多队列是否真的带来重叠执行。
4.2 图形 API 中的命令缓冲设计模式
命令缓冲的设计模式,本质上是在“driver 自动整理状态”和“应用显式描述工作”之间分配责任。OpenGL 偏向隐式状态机,应用通过当前 context 的绑定状态发出 draw;Vulkan、Metal 和 Direct3D 12 偏向显式命令对象,应用先录制,再提交,再用同步对象管理生命周期。比较这些 API 时,应使用同一组维度:状态来源、录制时机、提交对象、资源状态、同步方式和多线程边界。
| API | 状态来源 | 提交对象 | 资源状态表达 | 多线程 recording 边界 | 典型风险 |
|---|---|---|---|---|---|
| OpenGL | 当前 context 的隐式状态 | driver 内部命令流 | 多数状态由 driver 隐式处理 | 多 context 共享需要小心管理 | 隐式同步、状态污染、driver 开销难定位 |
| Vulkan | command buffer 内显式录制的状态和命令 | VkCommandBuffer 提交到 queue | image layout、access mask、pipeline stage 等显式同步 | command pool 需要外部同步,常按线程分配 | barrier 缺失、command buffer 生命周期错误、queue family 边界错误 |
| Metal | command buffer 内由 encoder 记录 pass 命令 | MTLCommandBuffer commit 到 command queue | resource usage、encoder 边界、fence/event 等表达依赖 | 可并行构建数据,具体 command encoder 使用按对象规则管理 | encoder 边界混乱、资源写后读顺序表达不足 |
| Direct3D 12 | command list、PSO、root signature、descriptor heap 等显式对象 | command list 执行到 command queue | resource barrier 和 fence 显式表达 | command allocator/list 复用必须等待 GPU 完成 | allocator 过早 reset、barrier 不完整、descriptor 生命周期错误 |
OpenGL 的命令设计让应用更像是在修改一个当前状态机。绑定 program、VAO、texture、framebuffer 后调用 draw,driver 根据当前状态生成内部命令。优点是入门路径短,缺点是错误常表现为“某个 draw 结果不对”或“某个调用突然阻塞”。当你在工具里看到 OpenGL frame capture,通常需要从 draw call 反推当时 context 上的 program、uniform、texture、framebuffer 和 blend/depth state。
Vulkan 的 command buffer 具有明确生命周期。典型状态从 initial 到 recording,再到 executable,提交后进入 pending,GPU 完成后再回到可复用状态或变成 invalid。这个生命周期迫使应用明确回答几个问题:命令从哪个 command pool 分配,在哪个线程录制,提交到哪个 queue family,对象在 GPU pending 阶段是否仍然存活,何时可以 reset 或释放。Vulkan 规范还强调 command pool 的 host 访问需要外部同步,因此多线程 recording 的常见工程做法是每个 worker thread 使用自己的 command pool。
Direct3D 12 的 command list 与 command allocator 分离。command list 记录命令,command allocator 保存记录命令所需的内存。应用可以关闭 command list 后提交给 queue,之后等 fence 表示 GPU 已经执行到安全位置,再 reset allocator 和 command list。这个设计的检查点非常明确:allocator 复用必须跟 GPU 完成信号绑定,descriptor heap 中被 GPU 读取的 descriptor 也必须在对应 fence 完成前保持有效。
Metal 的 command buffer 通常从 command queue 创建,然后通过 render、compute 或 blit command encoder 录制不同类型的命令。encoder 表达一段 pass 内的状态和操作,command buffer 表达一次可提交工作。它的使用方式和 Vulkan/D3D12 在思想上接近:应用构建清楚的 pass 边界,提交 command buffer,然后通过 completion handler、shared event 或 fence 类对象观察进度。平台细节需要按 Apple 文档和设备能力处理,跨平台引擎不应把 Metal 的对象生命周期直接套到其他 API。
下面的简化伪代码表达贯穿帧在显式 API 中的组织方式。它是一段跨 API 的说明性代码,用于展示 command buffer 会记录“资源状态、pipeline state、binding、draw/dispatch、pass 边界和同步点”的组合。
// simplified explicit graphics API pseudocode
FrameCommands build_frame_commands(FrameGraph& graph, FrameResources& resources) {
FrameCommands frame;
CommandBuffer upload = begin_command_buffer(CommandType::Copy);
upload_texture_tiles(upload, resources.pendingTextures);
signal_after_submit(upload, resources.uploadFinished);
frame.copyCommands.push(upload);
CommandBuffer graphics = begin_command_buffer(CommandType::Graphics);
wait_on_queue(graphics, resources.uploadFinished);
transition(graphics, resources.shadowMap, Access::DepthWrite);
begin_pass(graphics, graph.shadowPass);
bind_pipeline(graphics, resources.shadowPipeline);
draw_shadow_casters(graphics, graph.visibleObjects);
end_pass(graphics);
transition(graphics, resources.shadowMap, Access::ShaderRead);
transition(graphics, resources.colorTarget, Access::ColorWrite);
begin_pass(graphics, graph.mainPass);
bind_pipeline(graphics, resources.mainPipeline);
bind_scene_resources(graphics, resources.materials, resources.shadowMap);
draw_visible_objects(graphics, graph.visibleObjects);
end_pass(graphics);
transition(graphics, resources.colorTarget, Access::ShaderRead);
begin_pass(graphics, graph.postProcessPass);
dispatch_or_draw_fullscreen(graphics, resources.colorTarget);
end_pass(graphics);
begin_pass(graphics, graph.uiPass);
draw_ui(graphics, graph.uiDrawData);
end_pass(graphics);
signal_after_submit(graphics, resources.frameFinished);
frame.graphicsCommands.push(graphics);
return frame;
}
这段伪代码里最有价值的是顺序关系。上传命令可以提前进入 copy 队列,graphics 命令在使用上传结果之前等待 upload signal。shadow map 从 depth write 转成 shader read,color target 从 color write 转成 shader read。每个 pass 内部绑定 pipeline 和资源,然后发出 draw 或 dispatch。最后的 fence 让 CPU 知道何时可以复用本帧资源。
显式 command buffer 的常见设计模式可以分成三层。第一层是 per-frame command buffer,用于组织一帧的主 graphics 工作,生命周期跟 frame-in-flight 绑定。第二层是 per-pass 或 secondary command buffer,用于把 shadow、main、post-process、UI 等局部命令拆给 worker thread 录制,再合并到主提交路径。第三层是 reusable bundle 或预录制命令,用于静态场景、重复状态组合或平台支持的 bundle/secondary command buffer,但这类复用必须重新检查资源绑定、descriptor、dynamic state 和可见集是否仍然有效。
命令缓冲设计的失败情况通常出现在生命周期和状态边界。第一类是 CPU 过早复用 allocator、command pool 或 per-frame resource,GPU 仍在读取旧命令或旧 descriptor。第二类是 barrier 描述缺失,pass 之间的读写顺序在某些平台上表现为闪烁、黑块或偶发错误。第三类是提交粒度过碎,大量小 command buffer 让 CPU 侧 queue submit 和 driver 调度成本上升。第四类是预录制过度,表面上减少了 recording,实际却因为动态资源变多而产生更多 descriptor 更新和状态修补。
4.3 多线程命令提交与调度协调
多线程命令提交解决的是 CPU 侧构建渲染命令的扩展性问题。现代 frame 往往包含场景遍历、可见性裁剪、材质排序、资源更新、pass 构建、command recording 和主队列提交。单线程把这些工作串起来时,GPU 可能在等待 CPU 交付下一批命令;多线程的目标是把可并行的 CPU 工作拆开,同时保持最终 queue submit 的依赖清楚。
贯穿帧可以拆成四类 CPU 任务。第一类是 scene traversal,读取场景层级、相机、灯光和对象包围体,生成可见对象列表。第二类是 resource preparation,整理本帧要上传的 texture tile、uniform buffer、storage buffer、descriptor 或 argument buffer。第三类是 pass building,把 shadow、main、post-process、UI 的输入输出资源和 pipeline state 固定下来。第四类是 command recording,把每个 pass 的 draw/dispatch 写入 command buffer,最后由 render thread 或 submission thread 执行 queue submit。
下面的调度图展示 CPU 多线程构建和 GPU 队列执行之间的交界。图中 worker thread 不直接制造最终 present 顺序;它们产出可提交的 pass command 或 pass data,主提交点负责把这些结果按依赖组织成一个 frame timeline。
这套拆分的前提是数据所有权清楚。worker thread 可以并行读取稳定的 scene snapshot,可以写入自己负责的 command buffer,可以使用自己独占的 transient allocator、command pool 或 linear upload allocator。共享对象需要集中处理,例如 descriptor heap ring、material cache、pipeline cache、resource state tracker 和 frame graph registry。只要共享对象在 recording 阶段被多个线程写入,就需要锁、分片、批量合并或单线程提交阶段。
Vulkan 的 command pool 外部同步规则直接影响多线程设计。常见方案是每个 worker thread 拥有自己的 command pool,从中分配当前 frame 的 command buffer;frame 结束后根据 fence 批量 reset 对应 frame slot 的 pool。Direct3D 12 中也常给每个 worker 和每个 frame-in-flight 分配 allocator,等对应 fence 完成后再 reset。这样做的目的,是让 recording 阶段很少争用共享内存,同时让生命周期和 GPU 完成信号一一对应。
多线程提交并不等同于多线程同时调用 queue submit。大多数引擎会让多个线程并行构建命令,但把最终 submit 收敛到一个提交点。原因是 submit 需要维护全局顺序:copy queue 的上传 signal 要被 graphics queue wait,shadow pass 要先于 main pass 的 shadow read,post-process 要先于 UI 合成和 present。多个线程直接提交到同一 queue,容易把全局 frame timeline 分散到不可维护的调用点。
多队列协调需要额外判断是否值得。copy queue 适合较大资源上传、纹理 streaming 和读回;compute queue 适合能与 graphics pass 重叠的 simulation、culling、particle 或后处理任务。收益成立的条件是:任务之间有可重叠的时间窗口,资源读写通过 fence/semaphore/event 表达清楚,额外 queue 同步成本低于重叠带来的收益。如果 compute 结果马上被同一帧 main pass 使用,compute queue 可能只增加一次等待,实际帧时间没有下降。
在贯穿帧中,后台纹理上传适合先尝试 copy queue,因为上传结果可能到下一帧或数帧后才被使用。shadow pass 和 main pass 通常留在 graphics queue 上顺序执行,因为 main pass 直接读取 shadow map,拆到不同 queue 反而需要额外同步。post-process 如果是 compute shader,并且读取 main color 后立即写下一阶段输入,它多数情况下仍受 main color 完成约束;只有当它与下一帧或其他独立工作形成流水,才可能稳定获得重叠收益。
调试多线程命令构建时,先不要看线程数量,先看 timeline。CPU profiler 应显示 scene、resource、recording 是否并行展开;GPU capture 应显示 pass marker 是否按预期排列;queue timeline 应显示是否存在长时间空洞;fence wait 应指出等待发生在 CPU frame start、resource recycle、present 前还是跨队列依赖处。线程越多,越需要用 marker 和 frame id 把 command buffer、资源版本和 fence value 绑定起来。
多线程命令系统的稳定设计通常包含 frame-in-flight 环形资源。假设有 3 个 frame slot,每个 slot 拥有自己的 transient buffer、descriptor segment、command allocator/pool 和 fence value。CPU 构建 frame N 时使用 slot N % 3,在覆盖该 slot 前检查对应 fence 已完成。这个规则把“资源还能不能复用”转换成一个可检查条件,降低偶发 GPU 读旧数据的概率。
4.4 高效构建渲染命令流
高效命令流的目标,是让 CPU 少做重复状态整理,让 GPU 少遇到无意义等待,并让资源转换与 pass 边界对应真实数据流。构建顺序应从 frame graph 开始,先建立资源读写图,再进入 draw call 局部排序。frame graph 先描述每个 pass 读写哪些资源,再由这个资源图推导 pass 顺序、barrier、queue 依赖和资源生命周期;draw call 排序只是 main pass 或某个局部 pass 内部的优化步骤。
贯穿帧的资源图可以这样描述:shadow pass 写 shadow map;main pass 读 shadow map、材质纹理和场景 buffer,写 color target 和 depth target;post-process 读 color target,写 post target;UI pass 读 post target 或 backbuffer,写最终呈现目标;copy pass 写未来要用的 texture。这个描述比“先画阴影、再画模型、再做后处理”更有价值,因为它直接告诉你哪里需要 barrier、哪里可以并行、哪里可以复用 memory aliasing、哪里需要保留到 present 完成。
命令流组织的第一条规则是按 pass 固定资源边界。每个 pass 应有明确输入、输出、load/store 行为、pipeline 类型和可见 draw 集合。这样做能让工具 capture 中的 marker 与资源状态对应起来,也能让 barrier 自动化。shadow pass 的输出是 depth texture,main pass 的输入是 shader-readable shadow map;如果这两个 pass 的边界清楚,状态转换就可以由 frame graph 或 resource state tracker 自动生成。
第二条规则是按 pipeline state 和 material 减少状态切换。draw call 的顺序会影响 pipeline bind、descriptor bind、texture bind、vertex/index buffer bind 和 dynamic state 更新次数。常见排序顺序是先按 pass,再按 pipeline,再按 material 或 descriptor set,再按 mesh 或 instance batch。透明物体需要额外考虑深度排序,因此它通常形成独立 pass 或独立 draw bucket。排序不能破坏资源依赖和视觉正确性,尤其是透明、stencil、decal、order-dependent effect 和屏幕空间效果。
第三条规则是控制提交粒度。一个极端是每个 draw 都 submit,这会让 CPU 和 driver 承担大量提交开销。另一个极端是整帧只有一个巨大 command buffer,这会降低并行 recording 和局部复用空间。常见平衡是每个 frame 形成少量主 command buffer,内部按 pass 划分 marker;大型 pass 可以由多个 secondary command buffer、bundle 或 parallel encoder 分段录制。判断粒度是否合理,应看 CPU submit 时间、worker recording 时间、GPU bubble 和 capture 中 marker 可读性。
第四条规则是把 barrier 当成资源依赖证据。barrier 数量少不自动代表高效,barrier 数量多也不自动代表低效。真正要检查的是 barrier 是否与实际读写路径一致,是否把多个无关资源绑到同一个全局等待,是否在 pass 内制造过强同步,是否让 copy/compute/graphics 之间的重叠窗口缩短。对于 tile-based GPU,还要关注 render pass 的 load/store、memoryless attachment、resolve 和 framebuffer fetch 等平台特性,资源边界设计会直接影响片外内存流量。
第五条规则是让资源生命周期跟 frame slot 对齐。uniform buffer、storage buffer、descriptor、argument buffer、temporary render target、upload staging buffer 都可能被 GPU 延迟读取。一个安全做法是每个 frame slot 持有自己的 transient allocation,并用 fence 回收。对长期资源,例如材质纹理和 pipeline object,应把创建、上传、可见版本切换和销毁拆成不同阶段,销毁延后到最后一次使用的 fence 完成之后。
下面是一套可复用的命令流检查顺序。它适合用于 RenderDoc、Nsight、Xcode GPU tools 或自研 marker 日志,也适合在没有大型工具时检查引擎内部 frame graph 输出。
- 第一步,列出 pass 输入输出:写出每个 pass 读写的 texture、buffer 和 attachment,确认资源图能解释最终画面。
- 第二步,标出 queue 和同步对象:确认 copy、compute、graphics、present 分别在哪条 timeline 上推进,确认 signal/wait/fence value 对应哪一帧。
- 第三步,检查 barrier 来源:确认每个 barrier 都能对应一个资源从前一用途转到后一用途,检查是否存在过宽的全局等待。
- 第四步,检查 command buffer 生命周期:确认 recording、submit、pending、reset 或 reuse 都受 fence 保护。
- 第五步,检查状态切换热区:在 draw 密集 pass 内统计 pipeline、descriptor、texture 和 buffer 绑定变化,判断排序是否服务主要瓶颈。
- 第六步,检查 CPU 与 GPU 空洞:CPU timeline 中 recording 结束后 GPU 仍空闲,通常指向提交过晚;GPU pass 之间空洞明显,通常指向跨队列等待、present 节流或资源依赖过强。
这套顺序能把“帧率低”拆成可定位问题。CPU submit 时间高,优先看 command buffer 过碎、状态检查过多、descriptor 更新过频和多线程 recording 争用。GPU pass 时间高,优先看 shader、带宽、overdraw、采样和 render target 格式。GPU timeline 有 bubble,优先看 queue wait、barrier、present 和资源上传时机。CPU 在 frame start 等 fence,优先看 frame-in-flight 数量、资源回收策略和是否把读回或 upload buffer 复用放得过早。
命令流优化还要保持可调试性。把全部 draw 合并成巨大的无标记命令流,会让 capture 难以定位;把每个材质都拆成独立 pass,会增加 barrier 和 render target 切换。工程上更稳定的做法是使用清晰 marker:Frame 42 / Shadow / Directional Light 0、Frame 42 / Main / Opaque、Frame 42 / Post / Bloom Downsample、Frame 42 / UI。marker 名称应反映 pass、资源和目的,不应只写“draw”或“pass”。
最终,一个高效渲染命令流应满足四个条件。CPU 侧可以并行准备并及时提交;GPU 侧能看到连续、依赖清楚的工作;资源状态转换覆盖真实读写路径;生命周期回收由 fence 或等价完成信号保护。达成这四点后,后续优化才有稳定入口,例如 batching、instancing、descriptor strategy、async compute、render pass 合并或 tile memory 调整。
最小自检任务
给定一个简化帧:copy queue 上传一张新纹理,graphics queue 依次执行 shadow pass、opaque main pass、bloom compute pass、UI pass,最后 present。opaque main pass 需要读取 shadow map 和新纹理;bloom compute pass 读取 opaque color target 并写 bloom target;UI pass 读取 bloom target 并写 backbuffer。请写出这帧的命令流检查顺序,并指出哪些位置需要 queue wait、barrier 或 fence。
答案要点
先把资源读写列出来:copy pass 写新纹理;shadow pass 写 shadow map;opaque main pass 读 shadow map 和新纹理,写 color target;bloom compute pass 读 color target,写 bloom target;UI pass 读 bloom target,写 backbuffer;present 使用 backbuffer。这个资源图决定了命令顺序和同步需求。
copy queue 上传完成后需要 signal,一个 GPU 可见的等待关系要连接到 graphics queue 中首次读取新纹理的位置。CPU 不需要为这次上传立即等待,除非要复用 upload staging 内存或销毁上传相关资源。新纹理从 copy destination 进入 shader read 状态,具体表达方式取决于 API;在 D3D12/Vulkan 中通常由 resource barrier 或 image layout/access transition 表达。
shadow pass 写完 shadow map 后,opaque main pass 读取 shadow map,因此需要从 depth write 转到 shader read 的资源访问依赖。opaque main pass 写完 color target 后,bloom compute pass 读取它,因此需要从 color attachment write 转到 shader read 或 compute shader read。bloom compute pass 写 bloom target 后,UI pass 读取它,因此需要从 storage write 或 unordered access write 转到 shader read。UI pass 写 backbuffer 后,present 前需要 backbuffer 处于平台要求的 present 状态。
fence 的作用是把 GPU 进度回报给 CPU。frame fence 应在 graphics queue 提交后 signal,CPU 在复用本 frame slot 的 command allocator、transient buffer、descriptor segment、upload staging 或 render target 前检查对应 fence。跨 queue 的顺序由 GPU wait/signal 表达,CPU fence 等待只用于资源生命周期和帧节流,不应替代 pass 之间的 GPU 资源依赖。
排查顺序是:先看 pass marker 是否按 copy upload → shadow → opaque → bloom → UI → present 的资源依赖排列;再看 copy signal 是否被 graphics queue 在读取新纹理前等待;再看 shadow map、color target、bloom target、backbuffer 的 barrier 是否覆盖读写转换;最后看 CPU 是否在 frame start 或资源回收点等待 fence。如果 GPU timeline 中 bloom 前有空洞,优先检查 opaque color target 的 barrier 和 compute queue 依赖;如果 CPU timeline 中 submit 很晚,优先检查 recording、descriptor 更新和多线程任务合并。
本章知识点总结
- 命令入口:GPU 队列是应用把已整理渲染工作交给 GPU 执行系统的入口。
- 提交边界:CPU 录制完成、queue submit 完成和 GPU 执行完成是三个不同时间点。
- 显式模型:Vulkan、Metal 和 Direct3D 12 通过 command buffer、queue、barrier 和 fence 暴露更多调度责任。
- 隐式模型:OpenGL 把命令缓冲和部分同步隐藏在 driver 内部,调试时需要从 draw call 反推状态。
- 资源依赖:pass 顺序应由资源读写关系决定,barrier 是资源用途转换的证据。
- 同步分工:barrier 管资源访问顺序,queue wait 管队列之间顺序,fence 管 CPU 观察 GPU 进度。
- 生命周期:command allocator、command pool、descriptor 和 transient buffer 的复用必须受 GPU 完成信号保护。
- 线程边界:多线程 recording 应让 worker 独占临时分配器和 command pool,再由提交点组织全局顺序。
- 多队列收益:copy 或 compute queue 只有在存在可重叠窗口且同步成本较低时才会降低帧时间。
- 提交粒度:command buffer 过碎会增加 CPU 提交成本,过大又会削弱并行 recording 和局部复用。
- 状态排序:draw 密集 pass 内按 pipeline、material、descriptor 和 mesh 组织命令,可以减少重复绑定。
- 工具证据:marker、queue timeline、fence wait 和 resource state 是定位命令调度问题的主要证据。