Slang 编译器入口点合法化测试束解析:coverage/legalize 的白盒表征测试体系
2026/9/19 12:16:09 网站建设 项目流程

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)用例:

函数所在文件探针用例
invokePathConstantFuncInHullShaderslang-ir-glsl-legalize.cpp:1229hull-patch-constant-func-spirv.slang
getOrCreateBuiltinParamForHullShaderslang-ir-glsl-legalize.cpp:1121同上(合成路径)
createPatchConstantFuncResultTypeLayoutslang-ir-glsl-legalize.cpp:1167同上
legalizeMeshOutputParamslang-ir-glsl-legalize.cpp:3502mesh-shader-output-block-glsl.slang
getOrCreatePerVertexInputArrayslang-ir-glsl-legalize.cpp:3759pervertex-input-array-glsl.slang
consolidateParametersslang-ir-glsl-legalize.cpp:3040raytracing-multi-payload-consolidation-spirv.slang
assignRayPayloadHitObjectAttributeLocationsslang-ir-glsl-legalize.cpp:4563raytracing-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_Positiondocs/design/pipeline/05-ir-passes.md#entry-point-and-parameter-handlingvarying-struct-flatten-glsl.slang
SPIR-V 变长参数合法化器把 struct 输入/输出拆成逐字段的 OpVariable 全局变量,带顺序 Location 装饰,SV_Position路由到 BuiltIn Position同上varying-struct-flatten-spirv.slang
四个 compute thread-id 系统值映射到 GLSLgl_*builtindocs/design/target-pipelines/spirv.md#legalizeentrypointsforglsl-despite-the-namecompute-sysval-builtins-glsl.slang
fragment 的SV_Position输入映射到gl_FragCoordSV_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 参数并物化 OutputPatchdocs/design/pipeline/05-ir-passes.md#entry-point-and-parameter-handlinghull-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_PrimitiveIDSV_GSInstanceID变为gl_InvocationID,layer/viewport 收窄为 intdocs/design/target-pipelines/spirv.md#legalizeentrypointsforglsl-despite-the-namegeometry-shader-sysvals-glsl.slang
mesh 着色器的输出只重声明gl_MeshPerVertexEXT/gl_MeshPerPrimitiveEXT中被使用的成员,索引放入独立的gl_PrimitiveTriangleIndicesEXT数组docs/design/pipeline/05-ir-passes.md#entry-point-and-parameter-handlingmesh-shader-output-block-glsl.slang
含两个及以上 inout varying 参数的 ray-tracing 入口点,参数合并为一个 Location 0 的IncomingRayPayloadKHRstruct 变量docs/design/target-pipelines/spirv.md#legalizeentrypointsforglsl-despite-the-nameraytracing-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_FragFullyCoveredNVSV_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_BaryCoordEXTgl_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#legalizeentrypointvaryingparamsforcudaoptix-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-handlingmetal-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-handlingwgsl-fragment-sysvals.slang
WGSL 表标记为不支持的语义以 E55202 报告,诊断信息在声明处给出小写化的语义名,而不是静默丢弃同上wgsl-unsupported-system-value-diag.slang

源码级纵深:三个最具代表性的重写细节

hull 阶段的 patch-constant 拼接(最大的结构重写)

Vulkan 没有独立的 "patch constant" 函数:细分控制着色器必须自己、且每个 patch 只算一次细分因子。因此合法化器把入口点的return重写为跳转到新的尾块,该尾块:

  1. 发射OpControlBarrier(控制屏障);
  2. 把调用 id 与 0 比较(OpIEqual ... %int_0),形成OpSelectionMerge守卫;
  3. true分支内调用[patchconstantfunc],传入入口点自己的InputPatch参数和从逐控制点输出数组物化出的OutputPatch

测试 hull-patch-constant-func-spirv.slang 通过 SPIR-V 断言精确固定了这一形态:OpControlBarrierOpIEqual守卫、TessLevelOuter/TessLevelInnerPatch装饰,以及SV_OutputControlPointID复用为InvocationIdbuiltin 并兼任"是否控制点 0"的谓词。

OptiX payload 的 32 位寄存器打包

OptiX 没有 payload 内存,只有最多 32 个不透明的 32 位寄存器。因此inoutpayload struct 会在入口/出口被逐字段拆解为optixGetPayload_N/optixSetPayload_N对,optix-payload-register-packing-cuda.slang 在一个 payload 中同时压测了所有字段路径:

  • booluint8_thalfint16_t是子字字段,多个共享一个寄存器,读侧用 shift +0xFF/0xFFFF掩码提取,写侧做 read-modify-write 以保护相邻字段;
  • uint直接占一个寄存器,float需要位转换(bit-cast);
  • float3float2x2float[2]逐元素递归,寄存器连续且按声明顺序;
  • doubleint64_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 断言E55202system 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 无法驱动或不应固化的代码路径,这是表征测试方法论中"知道什么不测"同样重要的部分:

  1. invokePathConstantFuncInHullShaderSV_PrimitiveID/SV_OutputControlPointID分支slang-ir-glsl-legalize.cpp:1305-1315):对 CLI 输入是死代码。patch-constant 函数的参数不带IRVarLayout:1299if (!layout)守卫先行触发,每个标量系统值参数在命名分支被查阅前就以 E57002 拒绝。已记为 findingglsl-hull-patch-constant-system-value-param-rejected,只有修复该问题后分支才可达。
  2. getOrCreateBuiltinParamForHullShader的"参数已存在"路径:1128-1146):原则可达,但仅与上一条联合出现——它按系统值语义查找已有入口点参数,而它被调用的两个语义恰是 patch-constant 路径拒绝的那两个。合成路径(:1148-1165)已由 hull-patch-constant-func-spirv.slang 覆盖。
  3. legalizeDynamicResourcesForGLSL:5295,92 行):可达,但已被手写测试 tests/slang-extension/dynamic-resource-gl.slang 完全驱动,重复添加无新增价值;唯一遗漏的AmbiguousReferenceIr分支(动态资源参数在重写后仍有存活使用)无法构造出前端保留的、无.as<T>()的使用形态。
  4. getGLSLSystemValueInfonv_x_right/nv_viewport_mask:879-924):NVX 多视图每视图属性路径。源码注释将其记录为已知不完整的 hack("This doesn't seem to work correctly on its own between hlsl/glsl"),固化当前输出会固化编译器自己都不认为正确(正确与否未获证实)的行为。
  5. SV_StencilRef(GLSL)与SV_CullDistance(GLSL):可达且易驱动,但发射的#extension指令格式错误,按方法论记为 finding(glsl-legalize-extension-name-missing-gl-prefix)而非测试。同理包括:mesh 图元输出中的SV_ShadingRateglsl-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 分支或省略该字段。
  6. 基类兜底diagnoseUnsupportedSystemVal/diagnoseUnsupportedUserValslang-ir-legalize-varying-params.cpp:1042/:1053):仍为 0%。每个具体目标上下文(Metal、WGSL、CUDA、CPU)都用自带的诊断覆盖了不支持路径(wgsl-unsupported-system-value-diag.slang 固定的正是 WGSL 的覆盖版本);基类版本只会在某目标上下文不覆盖时触发,而目前 CLI 无法触达任何这种目标。
  7. CUDA 上下文超尺寸 hit-attribute 分支:2383):可达(hit-attribute struct 超过 32 字节),但它报的是E99999内部错误而非用户诊断,因此记为 findingoptix-oversized-hit-attributes-internal-error而不固化。
  8. handleSpecialSystemValue基类版本(:2833)与assign(IRBuilder&, LegalizedVaryingVal, IRInst*)重载(:3186:防御性的基类体(断言或空操作),每个有特殊系统值的目标都覆盖了它们。
  9. 上一轮遗留的slang-ir-legalize-types.cpp缺口UseOfUninitializedOpaqueHandleCooperativeMatrixUnsupportedCapture)已不在本批范围:测试束的coverage_targets仅含slang-ir-glsl-legalize.cppslang-ir-legalize-varying-params.cpp

文档缺口(doc gaps):测试驱动文档补全

测试束还反向指出了三处官方设计文档的不足,每条都附带了具体的补充建议,是读者理解合法化器行为的重要旁证:

  1. HLSL 到 Vulkan 的索引重定基未被文档化:文档提到 GLSL 合法化器"尽管名字如此"也跑在 SPIR-V 分支上,但未说明它执行索引重定基——SV_VertexID变成gl_VertexIndex - gl_BaseVertexSV_InstanceID同理),从而让用户代码保持 HLSL 的按 draw 从零开始的语义。因此发射值不是裸的gl_VertexIndex
  2. SV_到 gl_映射表缺失**:文档未枚举系统值语义到 gl_* builtin 的映射(SV_DispatchThreadIDgl_GlobalInvocationID、fragmentSV_Positiongl_FragCoordSV_IsFrontFacegl_FrontFacing等),也未提及未知SV_*语义会以错误 49999("unknown system-value semantic")拒绝。对应的行为断言见 unknown-system-value-semantic.slang。
  3. 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;其余行号引用(:518sv_clipdistance:543sv_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询