Slang 编译器入口点合法化测试束解析:coverage/legalize 的白盒表征测试体系
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
导读
本文围绕 Slang 着色语言编译器仓库中的docs/generated/tests/coverage/legalize/README.md及其 23 个测试用例,系统讲解编译器 IR 层面两个核心合法化器——GLSL/SPIR-V 入口点合法化器(source/slang/slang-ir-glsl-legalize.cpp)与变长参数(varying parameter)合法化器(source/slang/slang-ir-legalize-varying-params.cpp)——的实际行为。读者将理解:从一条普通slangc命令行到最终着色器输出的过程中,入口点参数如何被展平、系统值语义(system-value semantic)如何映射为各目标平台的 builtin、hull/mesh/ray-tracing 等特殊阶段经历了怎样的结构重写,以及测试束如何通过 "characterization"(表征)方法把这些当前观察到的行为固化为可回归的断言。
测试束定位:白盒表征测试而非规范
coverage/legalize测试束的定位非常明确:它是针对入口点/变长参数合法化器的白盒表征测试(white-box characterization tests),固定的是"当前观察到的合法化行为",而非一份规范(spec)。所谓表征测试,意味着这些测试不是为了定义"正确的行为应该是什么",而是把某个特定提交(source_commit: 5634a0ea1b)下、由 CLI 可达的入口点参数形态所产生的实际输出逐字记录(pin)下来;一旦合法化器行为改变导致输出变化,测试就会失败,从而暴露回归。
测试束的关键元数据约束(见 README.md)包括:
- 不验证、只固定:映射遵循已文档化的 GLSL/Vulkan、Metal、WGSL builtin 命名,因此全部视为已验证,测试束内没有任何用例携带
characterization-unverified标记; - 下游可接受性:对于文本 GLSL 目标,生成的着色器还会通过 glslang 反向验证(
-target spirv -emit-spirv-via-glsl);对于 SPIR-V 目标,用SLANG_RUN_SPIRV_VALIDATION=1做校验,确保固定的输出不会被下游消费方拒绝; - 不可达缺口单独处理:凡是会产生下游消费者拒绝的输出形态,都被从测试中剔除并记录为 findings(详见"不可达缺口"一节)。
三路并进的覆盖策略
README 记录了测试束第二轮(本批次)补齐的三类缺口,这也是理解 23 个测试文件组织逻辑的主线:
1. 没有其他用例驱动的阶段(0% 行覆盖率)
以下函数此前行覆盖率全部为 0,只有 hull、domain、geometry、mesh、per-vertex fragment 和 ray-tracing 入口点才能从一条普通slangc调用抵达它们,因此每个函数配一个探针(probe)用例:
| 函数 | 所在文件 | 探针用例 |
|---|---|---|
invokePathConstantFuncInHullShader | slang-ir-glsl-legalize.cpp:1229 | hull-patch-constant-func-spirv.slang |
getOrCreateBuiltinParamForHullShader | slang-ir-glsl-legalize.cpp:1121 | 同上(合成路径) |
createPatchConstantFuncResultTypeLayout | slang-ir-glsl-legalize.cpp:1167 | 同上 |
legalizeMeshOutputParam | slang-ir-glsl-legalize.cpp:3502 | mesh-shader-output-block-glsl.slang |
getOrCreatePerVertexInputArray | slang-ir-glsl-legalize.cpp:3759 | pervertex-input-array-glsl.slang |
consolidateParameters | slang-ir-glsl-legalize.cpp:3040 | raytracing-multi-payload-consolidation-spirv.slang |
assignRayPayloadHitObjectAttributeLocations | slang-ir-glsl-legalize.cpp:4563 | raytracing-payload-location-assignment-spirv.slang |
2. GLSL 系统值表的长尾(231 行未覆盖)
getGLSLSystemValueInfo是整个文件中最大的单点缺口(231 行未覆盖)。此前从未被触达的语义,恰好是那些 GLSL 形态不是简单改名的条目:
- 数组值的
SV_Coverage; SV_DepthGreaterEqual的 layout 限定符;- 对
noperspective敏感的SV_Barycentrics分支; - int 类型的
gl_BaseVertex/gl_BaseInstance; SV_PrimitiveID的输入/输出分离。
这些语义按阶段归入三个探针用例:fragment-coverage-depth-glsl.slang、fragment-barycentrics-layer-viewport-glsl.slang、geometry-shader-sysvals-glsl.slang。
3. 非 Vulkan 的变长参数合法化器(322 行未覆盖)
emitOptiXPayloadRead/emitOptiXPayloadWrite(两者合计 322 行未覆盖)、Metal 和 WGSL 各自的getSystemValueInfo表、Metal 的 amplification/mesh 重写。这些从纯文本发射(-target cuda/metal/wgsl)即可触达,不依赖下游工具链,因此所有用例都不做工具门控(tool-gated)。
功能覆盖表:21 条行为声明的完整清单
README 的核心价值在于其 21 条 "Claim → 意图 → 文档锚点 → 测试" 的映射表。以下完整继承并整理(意图均为 characterization):
GLSL/SPIR-V 入口点合法化(slang-ir-glsl-legalize.cpp)
| 行为声明 | 文档锚点 | 测试 |
|---|---|---|
顶点入口点的 struct 型 varying 输入/输出展平为逐字段的 GLSLin/out全局变量,SV_Position路由到gl_Position | docs/design/pipeline/05-ir-passes.md#entry-point-and-parameter-handling | varying-struct-flatten-glsl.slang |
SPIR-V 变长参数合法化器把 struct 输入/输出拆成逐字段的 OpVariable 全局变量,带顺序 Location 装饰,SV_Position路由到 BuiltIn Position | 同上 | varying-struct-flatten-spirv.slang |
四个 compute thread-id 系统值映射到 GLSLgl_*builtin | docs/design/target-pipelines/spirv.md#legalizeentrypointsforglsl-despite-the-name | compute-sysval-builtins-glsl.slang |
fragment 的SV_Position输入映射到gl_FragCoord,SV_IsFrontFace映射到gl_FrontFacing | 同上 | fragment-position-frontface-glsl.slang |
无法识别的SV_*语义被 GLSL 入口点合法化器以 E49999 拒绝 | 同上 | unknown-system-value-semantic.slang |
SV_VertexID/SV_InstanceID在 GLSL 中降级为gl_VertexIndex/gl_InstanceIndex并减去 base offset | 同上 | vertex-id-base-offset-glsl.slang |
hull 着色器的 patch-constant 函数被拼接进入口点,置于控制屏障之后、invocation id == 0守卫之内,接收 InputPatch 参数并物化 OutputPatch | docs/design/pipeline/05-ir-passes.md#entry-point-and-parameter-handling | hull-patch-constant-func-spirv.slang |
domain 入口点把 OutputPatch 路由到 Location 输入,SV_DomainLocation路由到 TessCoord,patch-constant struct 的SV_InsideTessFactor路由到 Patch 装饰的 TessLevelInner | 同上 | domain-shader-patch-input-spirv.slang |
geometry 着色器中SV_PrimitiveID分裂为输入的gl_PrimitiveIDIn与输出的gl_PrimitiveID,SV_GSInstanceID变为gl_InvocationID,layer/viewport 收窄为 int | docs/design/target-pipelines/spirv.md#legalizeentrypointsforglsl-despite-the-name | geometry-shader-sysvals-glsl.slang |
mesh 着色器的输出只重声明gl_MeshPerVertexEXT/gl_MeshPerPrimitiveEXT中被使用的成员,索引放入独立的gl_PrimitiveTriangleIndicesEXT数组 | docs/design/pipeline/05-ir-passes.md#entry-point-and-parameter-handling | mesh-shader-output-block-glsl.slang |
含两个及以上 inout varying 参数的 ray-tracing 入口点,参数合并为一个 Location 0 的IncomingRayPayloadKHRstruct 变量 | docs/design/target-pipelines/spirv.md#legalizeentrypointsforglsl-despite-the-name | raytracing-multi-payload-consolidation-spirv.slang |
| 未显式声明 location 的 ray payload、callable payload 与 hit-object attributes,从各自类型独立计数器中取最低空闲编号(跳过被显式占用的位置) | 同上 | raytracing-payload-location-assignment-spirv.slang |
同一 nointerpolation fragment 输入上重复的GetAttributeAtVertex共享一个记忆化的pervertexEXT三元素数组,声明时不带插值限定符 | 同上 | pervertex-input-array-glsl.slang |
SV_Coverage成为gl_SampleMaskIn/gl_SampleMask的 0 号元素并做 uint/int 转换,SV_InnerCoverage变为 bool 的gl_FragFullyCoveredNV,SV_DepthGreaterEqual变为layout(depth_greater) | 同上 | fragment-coverage-depth-glsl.slang |
SV_StartVertexLocation/SV_StartInstanceLocation映射到 int 的gl_BaseVertex/gl_BaseInstance并强制#version 460;带索引的SV_ClipDistanceN共享同一个gl_ClipDistance数组 | 同上 | vertex-base-location-clip-distance-glsl.slang |
SV_Barycentrics依据字段的noperspective修饰符选择gl_BaryCoordEXT或gl_BaryCoordNoPerspEXT,连同 fragment 的 layer/viewport/sample/shading-rate 映射 | 同上 | fragment-barycentrics-layer-viewport-glsl.slang |
CUDA/OptiX 变长参数合法化(slang-ir-legalize-varying-params.cpp)
| 行为声明 | 文档锚点 | 测试 |
|---|---|---|
| 混合位宽的 OptiX ray payload 被打包进 32 位 payload 寄存器:子字(sub-word)字段通过 shift/mask 的 read-modify-write 共享一个寄存器,聚合类型逐元素递归,64 位字段占用一对寄存器 | docs/design/target-pipelines/cuda.md#legalizeentrypointvaryingparamsforcuda | optix-payload-register-packing-cuda.slang |
聚合 hit-attribute struct 按声明顺序每次取一个 32 位optixGetAttribute槽,float 槽带__int_as_float,整型槽不加转换 | 同上 | optix-hit-attribute-fetch-cuda.slang |
Metal 变长参数合法化(slang-ir-legalize-varying-params.cpp)
| 行为声明 | 文档锚点 | 测试 |
|---|---|---|
Metal fragment 系统值表把SV_ViewID映射到[[amplification_id]]、SV_DepthGreaterEqual映射到[[depth(greater)]],输入 struct 展平为每字段一个带属性的参数 | docs/design/pipeline/05-ir-passes.md#entry-point-and-parameter-handling | metal-fragment-sysvals.slang |
Metal 上SV_GroupIndex没有属性,由[[thread_position_in_threadgroup]]与[numthreads]范围合成,其余 vertex/compute id 直接映射 | 同上 | metal-vertex-compute-sysvals.slang |
Metal amplification 获得合成的_slang_mesh_payload/_slang_mgp参数,DispatchMesh变为set_threadgroups_per_grid;mesh 阶段把输出收敛进一个metal::mesh对象 | 同上 | metal-mesh-amplification.slang |
WGSL 变长参数合法化(slang-ir-legalize-varying-params.cpp)
| 行为声明 | 文档锚点 | 测试 |
|---|---|---|
WGSL fragment 表把SV_Depth映射到@builtin(frag_depth)、SV_Coverage映射到标量@builtin(sample_mask),SV_Target变为@location(0) | docs/design/pipeline/05-ir-passes.md#entry-point-and-parameter-handling | wgsl-fragment-sysvals.slang |
| WGSL 表标记为不支持的语义以 E55202 报告,诊断信息在声明处给出小写化的语义名,而不是静默丢弃 | 同上 | wgsl-unsupported-system-value-diag.slang |
源码级纵深:三个最具代表性的重写细节
hull 阶段的 patch-constant 拼接(最大的结构重写)
Vulkan 没有独立的 "patch constant" 函数:细分控制着色器必须自己、且每个 patch 只算一次细分因子。因此合法化器把入口点的return重写为跳转到新的尾块,该尾块:
- 发射
OpControlBarrier(控制屏障); - 把调用 id 与 0 比较(
OpIEqual ... %int_0),形成OpSelectionMerge守卫; - 在
true分支内调用[patchconstantfunc],传入入口点自己的InputPatch参数和从逐控制点输出数组物化出的OutputPatch。
测试 hull-patch-constant-func-spirv.slang 通过 SPIR-V 断言精确固定了这一形态:OpControlBarrier、OpIEqual守卫、TessLevelOuter/TessLevelInner的Patch装饰,以及SV_OutputControlPointID复用为InvocationIdbuiltin 并兼任"是否控制点 0"的谓词。
OptiX payload 的 32 位寄存器打包
OptiX 没有 payload 内存,只有最多 32 个不透明的 32 位寄存器。因此inoutpayload struct 会在入口/出口被逐字段拆解为optixGetPayload_N/optixSetPayload_N对,optix-payload-register-packing-cuda.slang 在一个 payload 中同时压测了所有字段路径:
bool、uint8_t、half、int16_t是子字字段,多个共享一个寄存器,读侧用 shift +0xFF/0xFFFF掩码提取,写侧做 read-modify-write 以保护相邻字段;uint直接占一个寄存器,float需要位转换(bit-cast);float3、float2x2、float[2]逐元素递归,寄存器连续且按声明顺序;double、int64_t各占两个寄存器,以(high << 32) | low重组。
测试断言每个字段落位的寄存器索引与精确的提取/重组表达式,从而固定打包器执行的字节偏移算术——字段对齐或顺序的任何变化都会移动索引并使测试失败。
Metal amplification/mesh 的重写
Metal 的 mesh 管线与 HLSL 完全不同:amplification 着色器是[[object]]函数,通过object_data指针写 payload,通过mesh_grid_properties对象启动 mesh 网格——而这两个参数在用户源码中都不存在。metal-mesh-amplification.slang 固定了合法化器定位DispatchMesh调用后合成_slang_mesh_payload、_slang_mgp两个参数并把调用改写为set_threadgroups_per_grid;消费侧的 mesh 合法化器则把payload参数重定型为object_data指针,把三个输出数组收敛为一个metal::mesh<Vertex_0, Prim_0, 3U, 1U, metal::topology::triangle>对象,并驱动set_vertex/set_index/set_primitive调用。
测试驱动方式
每个测试文件的头部//TEST:指令给出了精确的 CLI 驱动方式,全部经由slangc直接可达:
- GLSL 文本输出:
//TEST:SIMPLE(filecheck=GLSL):-target glsl -entry main -stage vertex(如 varying-struct-flatten-glsl.slang),配合//GLSL-DAG:///GLSL:断言; - SPIR-V 汇编:
//TEST:SIMPLE(filecheck=SPIRV):-target spirv-asm -entry chit -stage closesthit,配合//SPIRV:///SPIRV-DAG:断言(如 raytracing-multi-payload-consolidation-spirv.slang); - CUDA 文本:
//TEST:SIMPLE(filecheck=CUDA):-target cuda -entry chit -stage closesthit; - Metal / WGSL 文本:
-target metal/-target wgsl,Metal 用例用两段 filecheck 分别检查 amplification(AMP)与 mesh(MESH)两个入口点; - 诊断测试:
//DIAGNOSTIC_TEST:SIMPLE(diag=CHECK):-target wgsl ...,断言错误码与消息文本(如 wgsl-unsupported-system-value-diag.slang 断言E55202与system value semantic 'sv_clipdistance' is not supported for the current target.)。
文本 GLSL 目标的输出还会经 glslang 往返校验(-target spirv -emit-spirv-via-glsl),SPIR-V 目标用SLANG_RUN_SPIRV_VALIDATION=1验证,从而保证"测试固定的输出不会被下游消费方拒绝"。
不可达(unreachable)缺口清单
README 详尽列出了那些从 CLI 无法驱动或不应固化的代码路径,这是表征测试方法论中"知道什么不测"同样重要的部分:
invokePathConstantFuncInHullShader的SV_PrimitiveID/SV_OutputControlPointID分支(slang-ir-glsl-legalize.cpp:1305-1315):对 CLI 输入是死代码。patch-constant 函数的参数不带IRVarLayout,:1299的if (!layout)守卫先行触发,每个标量系统值参数在命名分支被查阅前就以 E57002 拒绝。已记为 findingglsl-hull-patch-constant-system-value-param-rejected,只有修复该问题后分支才可达。getOrCreateBuiltinParamForHullShader的"参数已存在"路径(:1128-1146):原则可达,但仅与上一条联合出现——它按系统值语义查找已有入口点参数,而它被调用的两个语义恰是 patch-constant 路径拒绝的那两个。合成路径(:1148-1165)已由 hull-patch-constant-func-spirv.slang 覆盖。legalizeDynamicResourcesForGLSL(:5295,92 行):可达,但已被手写测试 tests/slang-extension/dynamic-resource-gl.slang 完全驱动,重复添加无新增价值;唯一遗漏的AmbiguousReferenceIr分支(动态资源参数在重写后仍有存活使用)无法构造出前端保留的、无.as<T>()的使用形态。getGLSLSystemValueInfo的nv_x_right/nv_viewport_mask(:879-924):NVX 多视图每视图属性路径。源码注释将其记录为已知不完整的 hack("This doesn't seem to work correctly on its own between hlsl/glsl"),固化当前输出会固化编译器自己都不认为正确(正确与否未获证实)的行为。SV_StencilRef(GLSL)与SV_CullDistance(GLSL):可达且易驱动,但发射的#extension指令格式错误,按方法论记为 finding(glsl-legalize-extension-name-missing-gl-prefix)而非测试。同理包括:mesh 图元输出中的SV_ShadingRate(glsl-mesh-shading-rate-block-member-type)、用户语义的 patch-constant 输出(hull-patch-constant-user-output-location-collides)、hull 的整段文本 GLSL 形态(glsl-hull-patch-constant-func-void-return)、domain 的文本 GLSL 形态(glsl-domain-shader-patch-input-array-size)、带两个inout参数的 callable 着色器(callable-shader-multi-inout-uses-ray-payload-storage)。这就是为什么对应测试改用 SPIR-V 分支或省略该字段。- 基类兜底
diagnoseUnsupportedSystemVal/diagnoseUnsupportedUserVal(slang-ir-legalize-varying-params.cpp:1042/:1053):仍为 0%。每个具体目标上下文(Metal、WGSL、CUDA、CPU)都用自带的诊断覆盖了不支持路径(wgsl-unsupported-system-value-diag.slang 固定的正是 WGSL 的覆盖版本);基类版本只会在某目标上下文不覆盖时触发,而目前 CLI 无法触达任何这种目标。 - CUDA 上下文超尺寸 hit-attribute 分支(
:2383):可达(hit-attribute struct 超过 32 字节),但它报的是E99999内部错误而非用户诊断,因此记为 findingoptix-oversized-hit-attributes-internal-error而不固化。 handleSpecialSystemValue基类版本(:2833)与assign(IRBuilder&, LegalizedVaryingVal, IRInst*)重载(:3186):防御性的基类体(断言或空操作),每个有特殊系统值的目标都覆盖了它们。- 上一轮遗留的
slang-ir-legalize-types.cpp缺口(UseOfUninitializedOpaqueHandle、CooperativeMatrixUnsupportedCapture)已不在本批范围:测试束的coverage_targets仅含slang-ir-glsl-legalize.cpp与slang-ir-legalize-varying-params.cpp。
文档缺口(doc gaps):测试驱动文档补全
测试束还反向指出了三处官方设计文档的不足,每条都附带了具体的补充建议,是读者理解合法化器行为的重要旁证:
- HLSL 到 Vulkan 的索引重定基未被文档化:文档提到 GLSL 合法化器"尽管名字如此"也跑在 SPIR-V 分支上,但未说明它执行索引重定基——
SV_VertexID变成gl_VertexIndex - gl_BaseVertex(SV_InstanceID同理),从而让用户代码保持 HLSL 的按 draw 从零开始的语义。因此发射值不是裸的gl_VertexIndex。 - SV_到 gl_映射表缺失**:文档未枚举系统值语义到 gl_* builtin 的映射(
SV_DispatchThreadID→gl_GlobalInvocationID、fragmentSV_Position→gl_FragCoord、SV_IsFrontFace→gl_FrontFacing等),也未提及未知SV_*语义会以错误 49999("unknown system-value semantic")拒绝。对应的行为断言见 unknown-system-value-semantic.slang。 - hull 与 ray-tracing 的结构重写未记载:
docs/design/pipeline/05-ir-passes.md的入口点参数处理一节未描述 hull 入口点被重写为内联调用[patchconstantfunc](含屏障与单调用守卫、OutputPatch 从控制点输出数组物化)这一"全 pass 最大的结构重写",也未描述consolidateRayTracingParameters/assignRayPayloadHitObjectAttributeLocations这对函数(何时合并、ray/callable/hit-attribute 位置如何独立编号)。
漂移审查(drift review)机制
测试束自带周期性的漂移审查纪律,README 记录了本批次审查结果(在d592afa9b9对照生成基准ef1068b548):
- 期间有 16 个提交触及被监视路径(watched paths),全部 23 个既有测试仍然通过;
- 基准提交后新增了一个来自
coverage_targets的诊断 E55214(经不可内联调用抵达的着色器终止内建函数),它已有手写测试 tests/cuda/optix-terminate-nested-recursion.slang,故未新增测试; - 一处引用漂移被修正:
nv_x_right/nv_viewport_mask分支的行号从:876-905更正为:879-924;其余行号引用(:518的sv_clipdistance、:543的sv_coverage)复查后仍精确; - 窗口内落入的
5ed83a468c(#12182,CUDA/OptiX callable 着色器支持)对 ray-tracing 入口点参数诊断的影响,已在docs/design/pipeline/04c-layout-ir中由文档声明及其测试重定向处理。
结语:这套测试束的工程价值
从coverage/legalize这套测试可以提炼出 Slang 编译器测试基础设施的三种方法论:表征而非规范(固定当前行为、暴露回归)、下游可接受性校验(GLSL 过 glslang、SPIR-V 过验证器)、可测性边界纪律(无法触达或会固化缺陷行为的路径以 findings 形式记录而非强行断言)。对于希望深入 Slang 入口点合法化逻辑的读者,推荐按以下路径继续探索:docs/design/pipeline/05-ir-passes.md(IR pass 总览与入口点参数处理)、docs/design/target-pipelines/spirv.md("despite the name" 的 GLSL 合法化说明)、docs/design/target-pipelines/cuda.md(CUDA 变长参数合法化),以及两个核心实现文件 source/slang/slang-ir-glsl-legalize.cpp 与 source/slang/slang-ir-legalize-varying-params.cpp。
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考