Files
XCEngine/docs/used/Shader预编译缓存与D3D12字节码正式化计划_完成归档_2026-04-10.md

8.5 KiB
Raw Blame History

Shader 预编译缓存与 D3D12 字节码正式化计划

文档日期2026-04-10

适用范围:XCEngine 当前 Resources / Asset / Rendering / RHI / Editor 主线,目标问题为编辑器与运行时虽然已经具备 shader artifact 与 variant 系统,但 D3D12 路径仍普遍在运行时现场 D3DCompile(...),缺少真正可复用的编译字节码缓存链路。

关联归档: NanoVDB体积云加载阻塞与Runtime上传修复计划_完成归档_2026-04-10.md


1. 当前结论

本轮 NanoVDB 卡顿排查已经把“资源读入/上传”与“shader 编译”分开钉死:

  1. main.xcvol artifact 载入已经压到亚秒级。
  2. cloud.nvdb 对应 payload 的 CPU 读入和 GPU 上传不再是主瓶颈。
  3. 当前剩余大头是 D3D12 图形管线创建时的运行时 shader 编译。

最新 editor 实测:

  • VolumeFieldLoader Artifact total_ms = 674
  • AsyncVolumeLoadCompleted elapsed_ms = 909
  • UploadVolumeField total_ms = 736
  • D3D12 shader compile MainPS total_ms = 6205
  • SceneReady elapsed_ms = 8500

这说明当前主线问题已经从“体积资源路径错误”切换成“D3D12 shader 运行时现场编译没有被缓存机制覆盖”。


2. 根本问题

当前工程里虽然有 shader artifact也预留了 compiledBinary 字段,但整条链路没有真正闭合。

2.1 已有但未闭环的部分

  1. ShaderStageVariantcompiledBinary 字段。
  2. shader artifact 文件格式会序列化/反序列化 compiledBinary
  3. shader runtime build 也会统计 compiledBinary 的内存占用。

2.2 真正缺失的部分

  1. 没有任何正式步骤把 D3D12 编译结果写入 variant.compiledBinary
  2. 没有任何 D3D12 运行时路径优先消费 compiledBinary
  3. D3D12Device::CreatePipelineState(...) 仍然直接对 ShaderCompileDescD3DCompile(...)
  4. shader artifact 现在缓存的是源码展开结果和变体描述,不是可直接用于 D3D12 的 DXBC/DXIL。

因此当前的真实状态不是“缓存偶尔失效”,而是:

D3D12 shader 预编译缓存机制整体没有真正落地。

NanoVDB 只是把这个系统性缺口最先炸出来,因为它的 MainPS 足够重。


3. 目标

3.1 一级目标

让 D3D12 路径对同一 shader variant 不再反复运行时现场编译。

要求:

  1. 首次编译后能够落盘保存 D3D12 可复用字节码。
  2. 再次打开 editor / scene / project 时优先命中缓存。
  3. CreatePipelineState(...) 在缓存命中时不再进入 D3DCompile(...)

3.2 二级目标

把这套机制做成通用能力,而不是只给 volumetric.shader 打补丁。

要求:

  1. 面向所有 D3D12 shader variant。
  2. 与现有 shader artifact/variant 系统兼容。
  3. 缓存失效规则明确可随源码、profile、macro、backend、entry point 正确失效。

3.3 三级目标

为后续 Vulkan / OpenGL / DXC 路径保留统一设计空间。

本轮不要求一次做完多后端,但数据模型和接口命名不能把后续扩展堵死。


4. 非目标

本轮不做以下内容:

  1. 不重写整个 shader authoring 系统。
  2. 不直接切 DXC 全量替换 FXC。
  3. 不先做跨后端统一离线编译工具链。
  4. 不先做完整 PSO cache blob 体系。
  5. 不先解决所有 shader 首次编译耗时,只先解决“重复现场编译”这个系统性缺口。

5. 正式实施方向

5.1 方向一:明确 D3D12 预编译产物的数据模型

要先统一“什么叫缓存命中”。

建议把 D3D12 预编译缓存键固定为以下信息的稳定组合:

  1. shader 资源路径
  2. pass name
  3. stage
  4. backend
  5. source language
  6. entry point
  7. profile
  8. macro 集合
  9. 变体关键字集合
  10. 源码内容哈希

产物:

  1. 对应 stage 的已编译 D3D12 字节码
  2. 可选的编译日志/调试信息版本标识
  3. 可选的 shader reflection 派生数据版本号

这一步的目的不是立即提速,而是先避免后面做出“缓存看起来有,实际上命不中或误命中”的半成品。

5.2 方向二:把 compiledBinary 从占位字段变成真实产物

当前 compiledBinary 只是格式字段,不是真正能力。

本阶段要做:

  1. D3D12 编译成功后把字节码写回 ShaderStageVariant::compiledBinary
  2. shader artifact 写出时携带该字节码
  3. 重新载入 shader artifact 时恢复该字节码

要求:

  1. 同一 variant 的 artifact 可以直接携带 D3D12 字节码
  2. 非 D3D12 后端不会被这套数据污染
  3. 缓存不存在时依然允许 fallback 到现场编译

5.3 方向三D3D12 runtime 优先消费已编译字节码

这是运行时闭环的关键。

需要调整的不是 asset 层,而是 D3D12 runtime 创建 shader / pipeline 的入口:

  1. CreateShader(...)
  2. CreatePipelineState(...)
  3. 任何内部 CompileD3D12Shader(...) 的调用链

优先级顺序应为:

  1. 命中 compiledBinary,直接创建 shader bytecode
  2. 未命中时才走 D3DCompile(...)
  3. 首次现场编译成功后可回写缓存

验收标准:

  1. 对同一项目二次启动时,MainPS 不再重新编译
  2. 日志里能明确区分 cache_hit / cache_miss / runtime_compile

5.4 方向四:把缓存失效规则正式化

没有失效规则,缓存就会变成隐患。

必须纳入失效判定的至少包括:

  1. shader 源文件内容变化
  2. #include 展开结果变化
  3. entry point 变化
  4. profile 变化
  5. macro 变化
  6. backend 变化
  7. variant keyword 变化
  8. shader artifact schema version 变化

这一阶段必须输出一套明确规则,避免后面出现“改了 shader 却继续吃旧字节码”的错误。

5.5 方向五:引入可验证的日志与测试

这次 NanoVDB 能钉死问题,靠的是可计时日志,不是猜。

shader 预编译缓存正式化之后,也必须保留最小必要日志:

  1. cache key
  2. cache hit/miss
  3. runtime compile elapsed
  4. binary load elapsed
  5. fallback reason

测试层至少要覆盖:

  1. 首次冷启动miss + compile + write
  2. 第二次热启动hit + no compile
  3. 改 shader 源码后:缓存失效 + 重新编译
  4. 切换 profile / macro / keyword 后:缓存失效

6. 分阶段执行计划

Phase 0基线与接口盘点

任务:

  1. 梳理 ShaderStageVariant::compiledBinary 的现状用途
  2. 盘点 D3D12 shader / pipeline 创建的全部编译入口
  3. 定义统一缓存键和版本策略
  4. 确认 artifact 与 runtime 的责任边界

交付:

  1. 一份固定缓存键规则
  2. 一份 D3D12 编译调用链清单

Phase 1让 artifact 真正带上 D3D12 编译产物

任务:

  1. 为 D3D12 variant 生成并保存编译字节码
  2. artifact 写入与读取完整覆盖 compiledBinary
  3. 为编译产物附加必要版本信息

交付:

  1. D3D12 shader artifact 中存在真实二进制 payload
  2. 可验证 artifact 前后字节码一致

Phase 2让 D3D12 runtime 优先吃缓存

任务:

  1. 调整 CompileD3D12Shader(...) 调用链
  2. 优先从 compiledBinary 构造 shader bytecode
  3. 未命中时 fallback 到 D3DCompile(...)
  4. 命中/失效/回写日志打通

交付:

  1. 二次打开 editor 时不再重新编译体积云 MainPS
  2. 其他 D3D12 shader 也具备相同缓存能力

Phase 3引入自动验证与冷启动对比

任务:

  1. 加入 shader cache 命中测试
  2. 加入变体失效测试
  3. 对 editor 冷启动做两轮连续对比

目标数字:

  1. 二次启动 MainPS compile total_ms 应接近 0
  2. SceneReady 应继续从当前 ~8.5s 下探

7. 当前建议的提交边界

在新计划开始实施前,建议把已经完成的修复与后续 shader cache 工作拆成两批提交:

提交一NanoVDB 路径修复与编译热点降压

应包含:

  1. volume artifact / payload 路径修复
  2. ResizeUninitialized 与大 payload 默认构造开销修复
  3. volumetric.shader 去除高风险 [unroll]
  4. HLSL profile 对齐到 5_1
  5. 当前保留的计时日志

提交二Shader 预编译缓存正式化

暂不在本提交里混入,避免把“已验证修复”和“下一阶段系统改造”搅在一起。


8. 验收标准

本计划完成时,至少满足:

  1. D3D12 对同一 shader variant 的二次启动不再现场编译。
  2. 日志能明确证明缓存命中。
  3. 修改 shader 后缓存会正确失效。
  4. NanoVDB 体积云场景的二次启动 SceneReady 不再被 MainPS 编译拖慢。