8.5 KiB
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 编译”分开钉死:
main.xcvolartifact 载入已经压到亚秒级。cloud.nvdb对应 payload 的 CPU 读入和 GPU 上传不再是主瓶颈。- 当前剩余大头是 D3D12 图形管线创建时的运行时 shader 编译。
最新 editor 实测:
VolumeFieldLoader Artifact total_ms = 674AsyncVolumeLoadCompleted elapsed_ms = 909UploadVolumeField total_ms = 736D3D12 shader compile MainPS total_ms = 6205SceneReady elapsed_ms = 8500
这说明当前主线问题已经从“体积资源路径错误”切换成“D3D12 shader 运行时现场编译没有被缓存机制覆盖”。
2. 根本问题
当前工程里虽然有 shader artifact,也预留了 compiledBinary 字段,但整条链路没有真正闭合。
2.1 已有但未闭环的部分
ShaderStageVariant有compiledBinary字段。- shader artifact 文件格式会序列化/反序列化
compiledBinary。 - shader runtime build 也会统计
compiledBinary的内存占用。
2.2 真正缺失的部分
- 没有任何正式步骤把 D3D12 编译结果写入
variant.compiledBinary。 - 没有任何 D3D12 运行时路径优先消费
compiledBinary。 D3D12Device::CreatePipelineState(...)仍然直接对ShaderCompileDesc走D3DCompile(...)。- shader artifact 现在缓存的是源码展开结果和变体描述,不是可直接用于 D3D12 的 DXBC/DXIL。
因此当前的真实状态不是“缓存偶尔失效”,而是:
D3D12 shader 预编译缓存机制整体没有真正落地。
NanoVDB 只是把这个系统性缺口最先炸出来,因为它的 MainPS 足够重。
3. 目标
3.1 一级目标
让 D3D12 路径对同一 shader variant 不再反复运行时现场编译。
要求:
- 首次编译后能够落盘保存 D3D12 可复用字节码。
- 再次打开 editor / scene / project 时优先命中缓存。
CreatePipelineState(...)在缓存命中时不再进入D3DCompile(...)。
3.2 二级目标
把这套机制做成通用能力,而不是只给 volumetric.shader 打补丁。
要求:
- 面向所有 D3D12 shader variant。
- 与现有 shader artifact/variant 系统兼容。
- 缓存失效规则明确,可随源码、profile、macro、backend、entry point 正确失效。
3.3 三级目标
为后续 Vulkan / OpenGL / DXC 路径保留统一设计空间。
本轮不要求一次做完多后端,但数据模型和接口命名不能把后续扩展堵死。
4. 非目标
本轮不做以下内容:
- 不重写整个 shader authoring 系统。
- 不直接切 DXC 全量替换 FXC。
- 不先做跨后端统一离线编译工具链。
- 不先做完整 PSO cache blob 体系。
- 不先解决所有 shader 首次编译耗时,只先解决“重复现场编译”这个系统性缺口。
5. 正式实施方向
5.1 方向一:明确 D3D12 预编译产物的数据模型
要先统一“什么叫缓存命中”。
建议把 D3D12 预编译缓存键固定为以下信息的稳定组合:
- shader 资源路径
- pass name
- stage
- backend
- source language
- entry point
- profile
- macro 集合
- 变体关键字集合
- 源码内容哈希
产物:
- 对应 stage 的已编译 D3D12 字节码
- 可选的编译日志/调试信息版本标识
- 可选的 shader reflection 派生数据版本号
这一步的目的不是立即提速,而是先避免后面做出“缓存看起来有,实际上命不中或误命中”的半成品。
5.2 方向二:把 compiledBinary 从占位字段变成真实产物
当前 compiledBinary 只是格式字段,不是真正能力。
本阶段要做:
- D3D12 编译成功后把字节码写回
ShaderStageVariant::compiledBinary - shader artifact 写出时携带该字节码
- 重新载入 shader artifact 时恢复该字节码
要求:
- 同一 variant 的 artifact 可以直接携带 D3D12 字节码
- 非 D3D12 后端不会被这套数据污染
- 缓存不存在时依然允许 fallback 到现场编译
5.3 方向三:D3D12 runtime 优先消费已编译字节码
这是运行时闭环的关键。
需要调整的不是 asset 层,而是 D3D12 runtime 创建 shader / pipeline 的入口:
CreateShader(...)CreatePipelineState(...)- 任何内部
CompileD3D12Shader(...)的调用链
优先级顺序应为:
- 命中
compiledBinary,直接创建 shader bytecode - 未命中时才走
D3DCompile(...) - 首次现场编译成功后可回写缓存
验收标准:
- 对同一项目二次启动时,
MainPS不再重新编译 - 日志里能明确区分
cache_hit/cache_miss/runtime_compile
5.4 方向四:把缓存失效规则正式化
没有失效规则,缓存就会变成隐患。
必须纳入失效判定的至少包括:
- shader 源文件内容变化
#include展开结果变化- entry point 变化
- profile 变化
- macro 变化
- backend 变化
- variant keyword 变化
- shader artifact schema version 变化
这一阶段必须输出一套明确规则,避免后面出现“改了 shader 却继续吃旧字节码”的错误。
5.5 方向五:引入可验证的日志与测试
这次 NanoVDB 能钉死问题,靠的是可计时日志,不是猜。
shader 预编译缓存正式化之后,也必须保留最小必要日志:
- cache key
- cache hit/miss
- runtime compile elapsed
- binary load elapsed
- fallback reason
测试层至少要覆盖:
- 首次冷启动:miss + compile + write
- 第二次热启动:hit + no compile
- 改 shader 源码后:缓存失效 + 重新编译
- 切换 profile / macro / keyword 后:缓存失效
6. 分阶段执行计划
Phase 0:基线与接口盘点
任务:
- 梳理
ShaderStageVariant::compiledBinary的现状用途 - 盘点 D3D12 shader / pipeline 创建的全部编译入口
- 定义统一缓存键和版本策略
- 确认 artifact 与 runtime 的责任边界
交付:
- 一份固定缓存键规则
- 一份 D3D12 编译调用链清单
Phase 1:让 artifact 真正带上 D3D12 编译产物
任务:
- 为 D3D12 variant 生成并保存编译字节码
- artifact 写入与读取完整覆盖
compiledBinary - 为编译产物附加必要版本信息
交付:
- D3D12 shader artifact 中存在真实二进制 payload
- 可验证 artifact 前后字节码一致
Phase 2:让 D3D12 runtime 优先吃缓存
任务:
- 调整
CompileD3D12Shader(...)调用链 - 优先从
compiledBinary构造 shader bytecode - 未命中时 fallback 到
D3DCompile(...) - 命中/失效/回写日志打通
交付:
- 二次打开 editor 时不再重新编译体积云
MainPS - 其他 D3D12 shader 也具备相同缓存能力
Phase 3:引入自动验证与冷启动对比
任务:
- 加入 shader cache 命中测试
- 加入变体失效测试
- 对 editor 冷启动做两轮连续对比
目标数字:
- 二次启动
MainPS compile total_ms应接近0 SceneReady应继续从当前~8.5s下探
7. 当前建议的提交边界
在新计划开始实施前,建议把已经完成的修复与后续 shader cache 工作拆成两批提交:
提交一:NanoVDB 路径修复与编译热点降压
应包含:
- volume artifact / payload 路径修复
ResizeUninitialized与大 payload 默认构造开销修复volumetric.shader去除高风险[unroll]- HLSL profile 对齐到
5_1 - 当前保留的计时日志
提交二:Shader 预编译缓存正式化
暂不在本提交里混入,避免把“已验证修复”和“下一阶段系统改造”搅在一起。
8. 验收标准
本计划完成时,至少满足:
- D3D12 对同一 shader variant 的二次启动不再现场编译。
- 日志能明确证明缓存命中。
- 修改 shader 后缓存会正确失效。
NanoVDB体积云场景的二次启动SceneReady不再被MainPS编译拖慢。