Skip to main content

Chapter 26: Dynamic Shader Generation and Hot Reloading

动态 Shader 生成(Dynamic Shader Generation)解决的是实时渲染工程中的一个具体问题:同一套材质、光照和调试需求会在不同平台、不同 pass、不同资源布局和不同功能开关下产生多份 shader 代码。读者读完本章后,应能定位这些变体来自哪里,判断哪些条件应进入编译期,哪些条件应留给运行时参数,并追踪一次 shader 热更新从源码变化到画面刷新的完整路径。

本章的贯穿材料是一套材质预览器。它渲染一个球体和一个平面,材质包含 base color、normal map、clearcoat、emissive、shadow receiver 和 debug normal 几个功能。编辑器中有人打开法线调试模式,修改 pbr_lit.frag 的一行代码,并期望下一帧或下几个可控帧内看到结果。这个场景足够小,却覆盖了实际引擎中的关键对象:shader source、宏开关、material layout、resource binding、pipeline state、编译缓存和回退策略。

本章讨论的动态生成不限定在“运行时拼字符串”。在现代引擎里,它更常见地表现为:根据材质图、feature toggle、平台 profile 和 render pass 配置生成源代码、编译参数或中间表示,再把结果缓存成 shader module 或 pipeline object。这里的核心能力是把变体边界设计清楚,让 shader 迭代保持可控,让帧时间不会被临时编译和 pipeline 创建拖入不可预测状态。

后续五节按同一条链路展开:先确定动态生成服务哪些目标,再定义 variant key 和缓存边界,然后追踪热更新路径,接着评估性能成本和变体爆炸,最后用材质变体与 debug shader 的案例把生成、缓存、热更新和性能边界收束到一套可复用的检查顺序。

26.1 Dynamic Shader Generation Targets

动态 Shader 生成的第一个判断点是目标。生成系统接受的输入通常包括材质描述、功能开关、平台能力、render pass 信息、资源布局和编译器 profile;输出通常是 shader 源码、编译后的中间表示、shader module、reflection metadata 或完整 pipeline object。目标不同,输入边界和缓存粒度也会不同。

在材质预览器中,USE_NORMAL_MAPUSE_CLEARCOATDEBUG_NORMALSUSE_EMISSIVE 是最直观的生成目标。它们控制 fragment shader 是否读取额外纹理、是否计算额外 BRDF 分支、是否把最终颜色改成法线可视化。用户看到的是材质效果切换,工程里变化的是 shader 编译条件、绑定资源集合和 pipeline 兼容性。

动态生成主要服务四类目标。第一类是材质组合:材质图里接入 normal map、clearcoat、emissive、alpha clip 等节点时,引擎生成对应代码路径。第二类是 feature toggle:调试视图、阴影开关、fog、skinning、instancing 等功能会改变 shader 输入和执行路径。第三类是平台变体:Vulkan、Direct3D、Metal、OpenGL/WebGL 的 shader 语言、资源绑定模型、坐标约定和编译链不同,同一个材质语义需要落到不同后端。第四类是调试迭代:编辑 shader 源码或材质节点后,系统重新编译、校验并替换可用版本。

这些目标要放在 frame 和 pass 中理解。材质预览器的一帧可能包含 shadow pass、depth pre-pass、opaque lighting pass 和 debug overlay pass。shadow pass 只需要 position、skinning 和 alpha clip,opaque lighting pass 需要 PBR 参数、纹理采样和光照输入,debug overlay pass 可能只输出法线或材质 ID。把同一个材质功能无差别带进所有 pass,会扩大变体数量,也会让某些 pass 绑定多余资源。

动态生成还要区分编译期条件和运行时参数。编译期条件改变代码结构、资源声明、入口函数、线程组大小或 pipeline state 兼容性;运行时参数只改变 uniform、constant buffer、push constant 或 texture 内容。材质是否拥有 normal map 通常会影响资源声明和采样代码,适合成为编译期条件。normal strength 的数值只改变计算系数,适合留在运行时 buffer 中。这个区分直接决定 shader 是否需要重编译。

不同 API 的边界需要写进生成系统。OpenGL 的 GLSL 通常在驱动中编译和链接 program,运行时状态隐式程度较高;Vulkan 更强调离线或加载时生成 SPIR-V,再用 shader stage、render pass 或 dynamic rendering、descriptor set layout 等对象创建 pipeline;Direct3D 12 常见路径是 HLSL 经 DXC 编译成 DXIL,再进入 pipeline state object;Metal 可以通过 MSL library、function constant 和 pipeline state 组织变体。具体工程可以封装统一的 ShaderBackend,但封装层要保留各后端对资源布局和 pipeline 创建的差异。这里的 API 边界可回溯到 Vulkan Pipelines specificationDirect3D 12 pipeline state documentationMetal function specialization documentation,它们分别支撑 pipeline object、PSO 和 function constant 这三类工程判断。

一个可执行的目标设计应回答三个问题:这一项变化是否改变 shader 二进制;是否改变资源绑定或 pipeline state;是否需要在当前 frame 内立即刷新。材质预览器中切换 DEBUG_NORMALS 会改变输出路径,但资源布局可以保持不变;启用 clearcoat 纹理会新增纹理输入,resource layout 和 reflection metadata 都要更新;修改 blend mode 会改变 pipeline state,shader module 命中缓存也可能仍需创建新 pipeline。

26.2 Shader Variant Keys and Cache Boundaries

Shader variant key 是一段可序列化的身份信息,用来描述“这份 shader 和 pipeline 由哪些条件决定”。它的作用是让缓存命中和失效都可解释。key 缺少影响编译结果的条件,会复用错误 shader;key 放入过多运行时参数,会制造大量低价值变体。

材质预览器的最小 key 至少包含 shader 源码依赖 hash、entry point、stage、宏定义、平台后端、编译 profile、material feature set、resource layout version、vertex layout 和 render pass 兼容信息。对于 Vulkan 或 Direct3D 12 这类显式 API,pipeline state 还会受 color attachment format、depth format、blend state、raster state、sample count 和 layout 影响。对于 Metal,function constant 与 pipeline descriptor 共同决定 pipeline state。对于 OpenGL,program link 结果还会受 shader stage 组合和部分驱动状态约束影响。

一个常见边界是 shader module cache 与 pipeline cache 的分层。shader module cache 关心源代码、宏、entry point、stage 和编译 profile,输出是可被 API 接收的模块或字节码。pipeline cache 关心 shader module 加上 vertex input、resource layout、render target 格式、depth/stencil、blend、rasterization、multisample 等状态,输出是可绑定执行的 pipeline object。reflection cache 关心 shader 声明出的资源、常量、varying 和 push constant,用来校验材质数据和绑定表。

下面的简化结构展示 key 的组成。它是跨 API 的边界示意,重点是把“影响 shader 二进制”和“影响 pipeline 兼容性”的字段分开存放。

struct ShaderModuleKey {
uint64_t sourceDependencyHash;
std::string backend; // vulkan, d3d12, metal, opengl
std::string stage; // vertex, fragment, compute
std::string entryPoint;
std::string compilerProfile;
std::vector<std::string> defines;
};

struct PipelineKey {
ShaderModuleKey vertex;
ShaderModuleKey fragment;
uint64_t resourceLayoutHash;
uint64_t vertexLayoutHash;
uint64_t renderTargetFormatHash;
uint64_t depthStencilStateHash;
uint64_t blendStateHash;
uint64_t rasterStateHash;
uint32_t sampleCount;
};

这段代码表达的工程判断是:normalStrength = 0.8 这类数值不进入 ShaderModuleKey,因为它应来自 uniform 或 constant buffer;USE_NORMAL_MAP 进入 defines,因为它可能改变纹理声明和采样路径;rgba16frgba8 这类 render target 格式进入 PipelineKey,因为它们影响 pipeline 兼容性和输出格式。读者检查自己的引擎时,应把每个字段都追问到“它改变的是字节码、绑定布局、pipeline state 还是普通数据”。

缓存边界还需要处理依赖图。pbr_lit.frag 可能包含公共文件 lighting.glslbrdf.glsldebug_views.glsl。修改公共文件时,只根据主文件 hash 失效会留下旧结果。稳定做法是把直接源码和间接 include 的内容 hash 合并成 sourceDependencyHash,并记录依赖到 variant 的反向索引。这样一次公共 BRDF 修改能定位所有受影响的材质变体。

材质参数和宏之间的边界要保持一致。材质系统可以把 hasNormalMap 作为 feature bit,把 normalTextureIndex 作为资源绑定数据,把 normalStrength 作为 uniform。这样 key 只跟 feature bit 和 layout version 相关,材质实例变化主要落在 buffer 和 descriptor 更新上。若每个材质实例都把颜色、粗糙度、贴图索引写进 variant key,预览器里的几十个材质会产生几十份功能等价 shader,缓存命中率会下降。

Reflection metadata 是缓存安全的证据。编译完成后,系统应读取 shader 声明出的 uniform buffer、sampler、texture、storage buffer、push constant、vertex attribute 和 fragment output。它回答的问题是:材质系统准备的资源能否绑定到当前 shader。热更新时,旧 shader 的 binding 3 可能是 normal map,新 shader 的 binding 3 可能被删除或改成 clearcoat map。没有 reflection 校验,画面错误会表现为黑块、随机颜色、validation error 或驱动层崩溃。

variant key 的设计结论可以收束成一条检查顺序:先列出所有会改变 shader 二进制的条件,再列出所有会改变 pipeline state 的条件,然后把普通材质数值留给运行时 buffer,最后用 reflection 验证资源布局。这个顺序比直接扩展宏列表更稳定,因为它让每一类变化都有明确归属。

26.3 Hot Reloading and Pipeline Refresh Path

热更新(Hot Reloading)是把 shader 源码或材质图变化转成可见画面变化的工程路径。它的输入是文件变化、编辑器节点变化或编译选项变化;处理过程包括依赖定位、编译、reflection 校验、pipeline 创建、资源重绑和安全替换;输出是新 shader 在后续 draw call 中生效,或者系统保留上一份可用版本并报告错误。

材质预览器的热更新从 pbr_lit.frag 保存开始。文件监听器记录变化,依赖图找到所有使用该文件的 variant key,编译队列生成新的 shader module。编译成功后,系统读取 reflection,检查当前 material layout、descriptor set layout 或 argument buffer layout 是否兼容。兼容时创建或查找 pipeline object,并在 frame 安全点替换材质实例持有的 pipeline handle。编译失败或 layout 失配时,旧 pipeline 继续服务当前画面,错误信息进入编辑器 overlay 和日志。

下图描述的是热更新的安全路径。它的边界是单个材质预览器,不覆盖完整多人协作 asset pipeline,也不讨论平台驱动内部的编译调度。

图中的关键路径是“先生成新对象,再替换旧对象”。热更新不应在当前 draw call 正在使用某个 pipeline 时原地修改它。显式 API 通常要求应用自己管理 in-flight frame 资源,旧 pipeline 需要保留到相关 command buffer 执行完成。OpenGL 这类接口的对象生命周期由驱动隐藏更多细节,但工程层仍应把 program 替换放在可控的 render thread 阶段,并让材质系统在下一次绑定时使用新 handle。

资源重绑是热更新最容易暴露问题的环节。若新 shader 删除了 normal map,材质仍然保留该纹理绑定通常不会影响结果;若新 shader 新增了 clearcoat map,材质实例缺少对应 texture slot,就需要绑定默认纹理或拒绝切换到新 pipeline。若 uniform buffer 的布局变化,CPU 端写入结构也要同步更新。这里的核心判断是:源码变化是否改变了 shader 期望的资源形状。

编译错误应作为可观察状态进入编辑器。错误信息至少包含文件路径、行列、stage、entry point、variant key 摘要和编译器输出。材质预览器可以在视口角落显示“fragment shader compile failed”,同时保留上一份有效画面。这样用户能继续观察旧结果并修正代码,系统也不会因为半成品 shader 破坏整帧渲染。

下面的简化伪代码展示热更新的替换边界。重点是所有失败路径都返回上一份可用 pipeline,线程模型由具体引擎决定。

ReloadResult reloadShaderVariant(const PipelineKey& key) {
CompileResult module = compileShaderModules(key);
if (!module.ok) {
return ReloadResult::keepPrevious(module.diagnostics);
}

ReflectionInfo reflection = reflectShader(module.bytecode);
LayoutCheck layout = checkMaterialLayout(reflection, key.resourceLayoutHash);
if (!layout.compatible) {
LayoutBuildResult rebuilt = rebuildBindingLayout(reflection);
if (!rebuilt.ok) {
return ReloadResult::keepPrevious(rebuilt.diagnostics);
}
}

PipelineHandle next = createOrFindPipeline(key, module.bytecode, reflection);
if (!next.valid()) {
return ReloadResult::keepPrevious({"pipeline creation failed"});
}

enqueueFrameBoundarySwap(key, next);
return ReloadResult::ready(next);
}

这段代码对应三个判断点。compileShaderModules 只回答 shader 代码能否被目标后端接受;checkMaterialLayout 回答资源声明是否匹配材质系统;createOrFindPipeline 回答当前 shader 和 pipeline state 是否能组成可执行对象。把这三个判断合并成一个“reload failed”会降低排查效率,因为编译错误、资源布局错误和 pipeline state 错误的修复位置完全不同。

热更新还要处理跨 stage 接口。vertex shader 输出的 varying 必须被 fragment shader 以兼容方式接收,fragment shader 输出的 render target location 必须匹配当前 pass。修改 fragment shader 的输出数量,可能要求 pipeline color attachment 设置同步变化。修改 vertex shader 的 attribute 输入,可能要求 mesh vertex layout 和 pipeline vertex input 同步变化。热更新系统应把这些接口校验放在创建 pipeline 之前。

工具证据可以帮助复查热更新结果。RenderDoc 可检查 draw call 使用的 pipeline、shader source 或反编译结果、resource binding 和 render target;Nsight 或 Xcode GPU tools 可观察 pipeline 创建、shader 编译相关的卡顿位置和 GPU pass;API validation layer 可提示 descriptor layout、resource state 或 shader interface 错误。工具只是观察入口,正文中的判断仍来自源码依赖、reflection、pipeline key 和 frame boundary swap 这条链路。

26.4 Performance Costs and Variant Explosion

动态生成的性能成本来自四个位置:shader 编译、pipeline 创建、缓存查找失效和变体数量膨胀。材质预览器规模小时,这些成本可能只表现为一次短暂停顿;扩展到完整场景后,同一套 feature bit 在多平台、多 pass、多材质和多 render target 格式下相乘,会形成大量 pipeline。

变体爆炸可以用一个简单关系描述:总变体数接近各个独立编译期开关取值数量的乘积。若一个材质有 normal map、clearcoat、emissive、alpha clip、receive shadow、debug view 六个二值条件,理论组合就是 26=642^6 = 64。如果再乘以三个 pass、两种 sample count、三种 render target 格式和两个平台 profile,pipeline 数量会继续扩大。实际工程通常不会启用全部组合,但 key 设计若放任每个开关独立相乘,缓存和预编译规模会快速失控。

控制顺序应从变体维度开始。先把确实改变资源声明或大段代码结构的条件保留在编译期,再把数值、颜色、强度、索引和调试显示参数放入运行时 buffer。然后按 pass 裁剪功能:shadow pass 保留 alpha clip 和 skinning,opaque lighting pass 保留 PBR 与纹理采样,debug pass 保留输出模式。最后再决定哪些组合进入启动前预热,哪些组合允许编辑器按需编译。

静态分支和动态分支的取舍要结合执行粒度。编译期宏能删除代码和资源声明,适合影响大、跨像素一致、组合数量受控的功能。运行时 uniform 分支能减少变体数量,适合开关频繁、成本可接受、资源布局稳定的功能。fragment shader 中每像素变化的分支会影响 SIMD/SIMT 执行效率;由 draw call 或 material uniform 控制的统一分支通常更可控。这个判断要回到 Chapter 2 中的线程束发散理解,但当前章只使用它来决定 variant 维度。

pipeline 创建成本在显式 API 中尤其需要管理。Vulkan 和 Direct3D 12 的 pipeline object 通常把 shader 和大量固定状态绑定成一个可执行对象,创建成本可能进入毫秒级或更高,具体数值受驱动、硬件、编译器和状态复杂度影响。Metal 的 pipeline state 创建也应视为重成本路径。OpenGL 运行时编译和 program link 可能把成本隐藏在首次使用或链接阶段。工程层应把这些行为都看作潜在 frame hitch 来源,并通过预编译、磁盘缓存、启动预热、编辑器编译队列和进度提示控制用户可感知停顿。

缓存命中率是评估系统是否健康的直接指标。材质预览器可以记录每次 draw call 绑定 pipeline 时的 cache hit、cache miss、compile time、pipeline creation time、variant key 摘要和使用次数。若大量变体只使用一次,说明 key 里混入了实例级数据,或 pass 维度拆分过细。若热更新频繁触发全量重编译,说明依赖图粒度太粗。若 pipeline 创建集中发生在相机第一次看到某个区域,说明预热集合没有覆盖首屏可见材质。

编译缓存也需要版本边界。缓存文件应至少记录引擎版本、shader compiler 版本、目标后端、平台 profile、sourceDependencyHash、编译参数和 pipeline state 摘要。驱动更新或编译器更新后,旧缓存可能无法保证稳定复用。工程上可以把缓存当作加速路径,不把它当成唯一来源;缓存失效时系统能从源码和 key 重新生成结果。

调试 shader 也会参与变体控制。debug normal、debug roughness、debug UV、overdraw visualization 等视图可以做成独立 debug shader,也可以作为主 shader 的调试分支。独立 debug shader 让主材质变体更清晰,资源绑定更少;主 shader 分支能复用已有材质输入,切换成本更低。选择依据是调试视图是否需要原材质完整资源,以及是否会污染主 shader 的 variant key。

性能排查的可复用顺序是:先看每帧是否有 shader compile 或 pipeline create,再看 cache miss 来自哪些 key 字段,然后统计每个 variant 的使用次数,接着检查 pass 和平台维度是否重复生成,最后把高频低收益的 feature 从编译期移到运行时参数。这个顺序从可观察成本出发,不依赖某个 API 或厂商工具的特定 counter。

26.5 Runtime Shader Case Study

本节用材质预览器完成一次 runtime shader 案例。目标是在 PBR 材质中支持 normal map、clearcoat 和 debug normal,并让 pbr_lit.frag 修改后热更新到画面。这个案例的价值在于把前四节的对象放到同一条路径中:生成目标决定 feature set,variant key 决定缓存,reflection 决定资源绑定,pipeline refresh 决定替换时机,性能统计决定是否继续拆分变体。

案例中的 fragment shader 可以从一份模板生成。模板中保留稳定接口,把条件代码放在宏控制块内。下面的 GLSL 片段只展示关键路径,省略完整 BRDF、颜色空间转换和阴影计算。

#version 450

layout(location = 0) in vec2 vUv;
layout(location = 1) in vec3 vWorldNormal;
layout(location = 2) in vec3 vWorldPosition;

layout(set = 0, binding = 0) uniform MaterialParams {
vec4 baseColor;
float roughness;
float metallic;
float normalStrength;
float clearcoatStrength;
} material;

#if USE_BASE_COLOR_MAP
layout(set = 1, binding = 0) uniform sampler2D baseColorMap;
#endif

#if USE_NORMAL_MAP
layout(set = 1, binding = 1) uniform sampler2D normalMap;
#endif

layout(location = 0) out vec4 outColor;

void main() {
vec3 normal = normalize(vWorldNormal);

#if USE_NORMAL_MAP
vec3 tangentNormal = texture(normalMap, vUv).xyz * 2.0 - 1.0;
normal = normalize(mix(normal, tangentNormal, material.normalStrength));
#endif

#if DEBUG_NORMALS
outColor = vec4(normal * 0.5 + 0.5, 1.0);
return;
#endif

vec3 baseColor = material.baseColor.rgb;
#if USE_BASE_COLOR_MAP
baseColor *= texture(baseColorMap, vUv).rgb;
#endif

vec3 litColor = baseColor;
#if USE_CLEARCOAT
litColor += vec3(material.clearcoatStrength * 0.04);
#endif

outColor = vec4(litColor, material.baseColor.a);
}

这段 shader 对应四个生成条件:USE_BASE_COLOR_MAPUSE_NORMAL_MAPUSE_CLEARCOATDEBUG_NORMALS。其中 material.normalStrengthmaterial.clearcoatStrength 保持为 uniform 数值,切换数值不会触发重编译。DEBUG_NORMALS 保持资源布局稳定,因为它复用已有输入并提前返回;USE_NORMAL_MAP 会声明 normalMap,所以它影响 reflection 和 descriptor layout。

材质实例进入渲染前,系统根据材质和 pass 生成 feature set。opaque lighting pass 可以启用 base color map、normal map 和 clearcoat;shadow pass 只读取 alpha clip 相关资源;debug normal pass 可以使用独立 debug pipeline,也可以复用上面模板中的 DEBUG_NORMALS 宏。案例选择复用模板,因为预览器需要快速切换视图,并且资源规模很小。

生成 variant key 时,系统把这四个宏、shader source dependency hash、fragment stage、Vulkan 后端、entry point、resource layout hash、color format 和 depth format 写入 key。然后先查 shader module cache。module 命中后继续查 pipeline cache;pipeline 命中则直接绑定;pipeline 未命中才创建 pipeline object。这样 normalStrength 从 0.5 改成 1.0 只更新 uniform buffer,DEBUG_NORMALS 切换会查找另一份 pipeline,pbr_lit.frag 修改会通过依赖 hash 失效相关 module。

热更新发生时,系统保留旧 pipeline 并编译新 module。若用户把 normalMap 的 binding 从 1 改到 5,reflection 会提示 layout 变化。预览器有两个可选动作:同步重建 descriptor layout 和材质绑定表,或者拒绝本次切换并提示 layout mismatch。对于小型预览器,重建绑定表可接受;对于大型引擎,频繁改 layout 会影响大量材质和 pipeline,通常需要编辑器显式确认或使用稳定的资源 slot 约定。

案例还需要一组可观察数据。每次 variant 请求记录 moduleCacheHitpipelineCacheHitcompileMspipelineCreateMsuseCount。当用户频繁切换 debug normal,理想结果是第一次 miss 后进入稳定 hit。若每次切换都 miss,说明 key 中存在帧号、材质实例 ID 或临时指针这类不稳定字段。若修改 normalStrength 触发编译,说明材质数值被错误写进 variant key。

最终的 runtime shader 路径可以按以下顺序复盘:材质实例产生 feature set;feature set 和 pass 信息生成 variant key;key 查询 module cache 和 pipeline cache;编译成功后 reflection 校验资源;pipeline 在 frame boundary 替换;画面更新后用统计数据确认缓存命中。这个顺序能迁移到 debug shader、post-process shader、compute particle update 和材质图生成等同类场景。

最小自检任务

给定一个材质预览器,它有三个材质实例:A 使用 base color map 与 normal map,B 只使用 base color 常量,C 使用 base color map、normal map 与 clearcoat。预览器支持 DEBUG_NORMALS 视图。用户修改了公共文件 brdf.glsl,然后把材质 A 的 normalStrength 从 0.5 调到 1.0。请设计一套最小 variant key 与热更新检查顺序,说明哪些操作会触发 shader 编译,哪些操作只更新运行时数据,并指出热更新失败时如何保持画面可用。

答案要点

最小 variant key 应包含 shader source dependency hash、stage、entry point、backend、compiler profile、宏集合、resource layout hash、vertex layout hash、render target/depth format、blend/raster/sample 等 pipeline state 摘要。A 和 C 都包含 USE_BASE_COLOR_MAPUSE_NORMAL_MAP,C 额外包含 USE_CLEARCOAT;B 不包含贴图相关宏。DEBUG_NORMALS 若作为宏实现,应进入 key;若作为独立 debug shader,也会形成独立 key。

修改 brdf.glsl 会改变 sourceDependencyHash,所有直接或间接依赖它的 variant 都应失效并重新编译。把 A 的 normalStrength 从 0.5 调到 1.0 只更新 material uniform buffer,不改变 shader module,也不改变 pipeline key。若这个数值变化触发编译,说明 key 混入了实例级运行时数据。

热更新检查顺序是:先从依赖图找到受影响 variant,再编译 shader module,然后读取 reflection 校验资源布局,接着创建或查找 pipeline,最后在 frame boundary 替换材质实例持有的 pipeline handle。编译失败、reflection 失配或 pipeline 创建失败时,系统保留上一份有效 pipeline,并把 stage、文件、行列、variant key 摘要和错误信息显示给编辑器。

画面可用性的关键边界是“新对象准备完成后再替换”。旧 pipeline 应保留到相关 in-flight frame 完成。资源 layout 变化时,小型预览器可以重建绑定表;大型引擎通常应先报告 layout mismatch,并让材质系统显式更新资源 slot 约定。

本章知识点总结

  • 生成目标:动态 Shader 生成服务材质组合、功能开关、平台变体和调试迭代,每类目标都应映射到 shader、资源或 pipeline 的具体变化。
  • 编译边界:改变代码结构、资源声明、入口函数或 pipeline 兼容性的条件进入编译期,普通材质数值应留在运行时 buffer。
  • Pass 裁剪:不同 render pass 只携带本 pass 需要的功能,shadow、opaque lighting 和 debug pass 应拥有不同的 feature set。
  • Variant Key:variant key 是 shader module 与 pipeline cache 的身份信息,必须覆盖源码依赖、宏、后端、资源布局和 pipeline state。
  • 缓存分层:shader module cache、pipeline cache 和 reflection cache回答不同问题,混成一层会降低失效和排查精度。
  • 依赖 Hash:公共 include 文件变化应通过依赖图影响所有相关 variant,单独 hash 主文件会留下旧编译结果。
  • Reflection:reflection metadata 用来校验 shader 声明的资源、常量、attribute 和输出是否匹配材质系统。
  • 热更新路径:热更新按依赖定位、编译、reflection 校验、pipeline 创建、frame boundary 替换的顺序执行。
  • 失败回退:编译错误、layout 失配或 pipeline 创建失败时,系统应保留上一份有效 pipeline 并暴露可定位诊断信息。
  • 性能成本:shader 编译、pipeline 创建、缓存 miss 和变体数量膨胀都会进入帧时间或编辑器交互延迟。
  • 变体控制:控制变体数量应先收缩编译期维度,再按 pass 裁剪功能,然后用预热和缓存覆盖高频组合。
  • 调试 Shader:debug view 可以作为独立 shader 或主 shader 分支,选择依据是资源复用需求、切换成本和 key 污染程度。
  • 案例链路:材质实例产生 feature set,feature set 生成 key,key 查询缓存,reflection 校验资源,pipeline 安全替换,统计数据验证结果。