From f291f1180361b9ef096cb8cbbe74a3b44ac87bef Mon Sep 17 00:00:00 2001 From: Abir Deol Date: Fri, 30 Oct 2026 12:17:12 -0700 Subject: [PATCH 1/4] [SPIR-V] Replace pixel shader SV_CullPrimitive inputs with 0 DXIL replaces SV_CullPrimitive pixel shader inputs with 0. The SPIR-V backend mapped them to CullPrimitiveEXT, which needs MeshShadingEXT and isn't allowed in fragment shaders, so the output was invalid. Fixes #9084. --- docs/ReleaseNotes.md | 2 ++ tools/clang/lib/SPIRV/DeclResultIdMapper.cpp | 12 ++++++++---- .../meshshading.ext.cullprimitive.ps.hlsl | 10 ++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b48054bf209..aa21a8b7a3e 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -25,6 +25,8 @@ The included licenses apply to the following files: buffer-like resources (`StructuredBuffer`, `ByteAddressBuffer`, `ConstantBuffer`, `TextureBuffer`, and RW variants) via `SPV_KHR_untyped_pointers`. Requires `-fspv-target-env=vulkan1.3`. +- `SV_CullPrimitive` as a pixel shader input is now replaced with 0, matching + DXIL, instead of producing invalid SPIR-V. Place release notes for the upcoming release below this line and remove this line upon naming the release. Refer to previous for appropriate section names. diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp index 9aaa36c97ba..37478a9db75 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp @@ -3868,6 +3868,14 @@ bool DeclResultIdMapper::createStageVars(StageVarDataBundle &stageVarData, return false; } + // DXIL replaces a pixel shader SV_CullPrimitive input with 0, and + // CullPrimitiveEXT can't be used in fragment shaders. + if (semanticKind == hlsl::Semantic::Kind::CullPrimitive && + sigPointKind == hlsl::SigPoint::Kind::PSIn) { + *value = spvBuilder.getConstantNull(stageVarData.type); + return true; + } + // Special handling of certain mappings between HLSL semantics and // SPIR-V builtins: // * SV_CullDistance/SV_ClipDistance are outsourced to GlPerVertex. @@ -4773,10 +4781,6 @@ SpirvVariable *DeclResultIdMapper::createSpirvStageVar( // be used as MSOut. case hlsl::Semantic::Kind::CullPrimitive: { switch (sigPointKind) { - case hlsl::SigPoint::Kind::PSIn: - stageVar->setIsSpirvBuiltin(); - return spvBuilder.addStageBuiltinVar(type, sc, BuiltIn::CullPrimitiveEXT, - isPrecise, srcLoc); case hlsl::SigPoint::Kind::MSPOut: stageVar->setIsSpirvBuiltin(); return spvBuilder.addStageBuiltinVar(type, sc, BuiltIn::CullPrimitiveEXT, diff --git a/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl b/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl new file mode 100644 index 00000000000..3ae49557617 --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl @@ -0,0 +1,10 @@ +// RUN: %dxc -T ps_6_5 -E main %s -spirv -DCULL_TYPE=bool | FileCheck %s +// RUN: %dxc -T ps_6_5 -E main %s -spirv -DCULL_TYPE=uint | FileCheck %s + +// SV_CullPrimitive as a pixel shader input is replaced with 0, like DXIL. + +// CHECK-NOT: CullPrimitiveEXT +// CHECK: OpEntryPoint Fragment %main "main" %out_var_SV_Target +// CHECK-NOT: CullPrimitiveEXT + +float4 main(CULL_TYPE cull : SV_CullPrimitive) : SV_Target { return cull; } From 0d8977c9a8b936a1f3718efc6335cdbf00bcb7dd Mon Sep 17 00:00:00 2001 From: Abir Deol Date: Mon, 2 Nov 2026 11:05:48 -0700 Subject: [PATCH 2/4] [SPIR-V] Test the value and layout of replaced SV_CullPrimitive inputs Check with -fcgl that the input becomes a null constant of the declared type, for bool, uint, bool2 and bool[2]. Add a struct shared between a mesh shader primitive output and a pixel shader input, with SV_CullPrimitive first and not first: the other members keep the mesh shader's locations and the 0 lands in the right member. --- .../meshshading.ext.cullprimitive.ps.hlsl | 26 +++++++- ...ng.ext.cullprimitive.ps.shared-struct.hlsl | 66 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.shared-struct.hlsl diff --git a/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl b/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl index 3ae49557617..1c55ac25f15 100644 --- a/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl +++ b/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl @@ -1,10 +1,34 @@ // RUN: %dxc -T ps_6_5 -E main %s -spirv -DCULL_TYPE=bool | FileCheck %s // RUN: %dxc -T ps_6_5 -E main %s -spirv -DCULL_TYPE=uint | FileCheck %s +// RUN: %dxc -T ps_6_5 -E main %s -spirv -DCULL_TYPE=bool2 | FileCheck %s +// RUN: %dxc -T ps_6_5 -E main %s -spirv -DCULL_ARRAY | FileCheck %s + +// RUN: %dxc -T ps_6_5 -E main %s -spirv -fcgl -DCULL_TYPE=bool | FileCheck %s --check-prefix=BOOL +// RUN: %dxc -T ps_6_5 -E main %s -spirv -fcgl -DCULL_TYPE=uint | FileCheck %s --check-prefix=UINT +// RUN: %dxc -T ps_6_5 -E main %s -spirv -fcgl -DCULL_TYPE=bool2 | FileCheck %s --check-prefix=BOOL2 +// RUN: %dxc -T ps_6_5 -E main %s -spirv -fcgl -DCULL_ARRAY | FileCheck %s --check-prefix=ARRAY // SV_CullPrimitive as a pixel shader input is replaced with 0, like DXIL. +// DXIL doesn't check the type here, so aggregates are replaced too. // CHECK-NOT: CullPrimitiveEXT // CHECK: OpEntryPoint Fragment %main "main" %out_var_SV_Target // CHECK-NOT: CullPrimitiveEXT -float4 main(CULL_TYPE cull : SV_CullPrimitive) : SV_Target { return cull; } +// BOOL: [[null:%[0-9]+]] = OpConstantNull %bool +// BOOL: OpStore %param_var_cull [[null]] + +// UINT: [[null:%[0-9]+]] = OpConstantNull %uint +// UINT: OpStore %param_var_cull [[null]] + +// BOOL2: [[null:%[0-9]+]] = OpConstantNull %v2bool +// BOOL2: OpStore %param_var_cull [[null]] + +// ARRAY: [[null:%[0-9]+]] = OpConstantNull %_arr_bool_uint_2 +// ARRAY: OpStore %param_var_cull [[null]] + +#ifdef CULL_ARRAY +float4 main(bool cull[2] : SV_CullPrimitive) : SV_Target { return cull[1]; } +#else +float4 main(CULL_TYPE cull : SV_CullPrimitive) : SV_Target { return cull.x; } +#endif diff --git a/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.shared-struct.hlsl b/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.shared-struct.hlsl new file mode 100644 index 00000000000..44af1267d4b --- /dev/null +++ b/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.shared-struct.hlsl @@ -0,0 +1,66 @@ +// RUN: %dxc -T ms_6_5 -E ms_main %s -spirv -fspv-target-env=vulkan1.3 -DCULL_FIRST | FileCheck %s --check-prefix=MS +// RUN: %dxc -T ms_6_5 -E ms_main %s -spirv -fspv-target-env=vulkan1.3 | FileCheck %s --check-prefix=MS +// RUN: %dxc -T ps_6_5 -E ps_main %s -spirv -fspv-target-env=vulkan1.3 -DCULL_FIRST | FileCheck %s --check-prefix=PS +// RUN: %dxc -T ps_6_5 -E ps_main %s -spirv -fspv-target-env=vulkan1.3 | FileCheck %s --check-prefix=PS +// RUN: %dxc -T ps_6_5 -E ps_main %s -spirv -fspv-target-env=vulkan1.3 -fcgl -DCULL_FIRST | FileCheck %s --check-prefix=FIRST +// RUN: %dxc -T ps_6_5 -E ps_main %s -spirv -fspv-target-env=vulkan1.3 -fcgl | FileCheck %s --check-prefix=LATER + +// One struct used as a mesh shader primitive output and a pixel shader input, +// with SV_CullPrimitive as the first member and as a later one. The pixel +// shader replaces it with 0, so the other members must keep the locations the +// mesh shader gave them, and the 0 must land in the right struct member. + +// MS: OpDecorate {{%[0-9]+}} BuiltIn CullPrimitiveEXT +// MS-DAG: OpDecorate %out_var_PRIMID Location 0 +// MS-DAG: OpDecorate %out_var_COLOR0 Location 1 + +// PS-NOT: CullPrimitiveEXT +// PS-DAG: OpDecorate %in_var_PRIMID Location 0 +// PS-DAG: OpDecorate %in_var_COLOR0 Location 1 +// PS-NOT: CullPrimitiveEXT + +// FIRST: [[null:%[0-9]+]] = OpConstantNull %bool +// FIRST: [[id:%[0-9]+]] = OpLoad %uint %in_var_PRIMID +// FIRST: [[col:%[0-9]+]] = OpLoad %v4float %in_var_COLOR0 +// FIRST: OpCompositeConstruct %Prim [[null]] [[id]] [[col]] + +// LATER: [[null:%[0-9]+]] = OpConstantNull %bool +// LATER: [[id:%[0-9]+]] = OpLoad %uint %in_var_PRIMID +// LATER: [[col:%[0-9]+]] = OpLoad %v4float %in_var_COLOR0 +// LATER: OpCompositeConstruct %Prim [[id]] [[null]] [[col]] + +#ifdef CULL_FIRST +struct Prim { + bool cull : SV_CullPrimitive; + uint id : PRIMID; + float4 col : COLOR0; +}; +#else +struct Prim { + uint id : PRIMID; + bool cull : SV_CullPrimitive; + float4 col : COLOR0; +}; +#endif + +struct Vert { + float4 pos : SV_Position; +}; + +[outputtopology("triangle")] +[numthreads(1, 1, 1)] +void ms_main(out vertices Vert verts[3], out indices uint3 tris[1], + out primitives Prim prims[1]) { + SetMeshOutputCounts(3, 1); + verts[0].pos = 0; + verts[1].pos = 0; + verts[2].pos = 0; + tris[0] = uint3(0, 1, 2); + prims[0].cull = false; + prims[0].id = 7; + prims[0].col = 1; +} + +float4 ps_main(Vert v, Prim p) : SV_Target { + return p.cull ? 0 : p.col * p.id; +} From 7681dc46efc35f5e3f13bccc51693d572020845d Mon Sep 17 00:00:00 2001 From: Abir Deol Date: Mon, 2 Nov 2026 11:12:10 -0700 Subject: [PATCH 3/4] [SPIR-V] Drop the unneeded target env from the PS test runs --- ...shading.ext.cullprimitive.ps.shared-struct.hlsl | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.shared-struct.hlsl b/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.shared-struct.hlsl index 44af1267d4b..46f2217806a 100644 --- a/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.shared-struct.hlsl +++ b/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.shared-struct.hlsl @@ -1,14 +1,12 @@ // RUN: %dxc -T ms_6_5 -E ms_main %s -spirv -fspv-target-env=vulkan1.3 -DCULL_FIRST | FileCheck %s --check-prefix=MS // RUN: %dxc -T ms_6_5 -E ms_main %s -spirv -fspv-target-env=vulkan1.3 | FileCheck %s --check-prefix=MS -// RUN: %dxc -T ps_6_5 -E ps_main %s -spirv -fspv-target-env=vulkan1.3 -DCULL_FIRST | FileCheck %s --check-prefix=PS -// RUN: %dxc -T ps_6_5 -E ps_main %s -spirv -fspv-target-env=vulkan1.3 | FileCheck %s --check-prefix=PS -// RUN: %dxc -T ps_6_5 -E ps_main %s -spirv -fspv-target-env=vulkan1.3 -fcgl -DCULL_FIRST | FileCheck %s --check-prefix=FIRST -// RUN: %dxc -T ps_6_5 -E ps_main %s -spirv -fspv-target-env=vulkan1.3 -fcgl | FileCheck %s --check-prefix=LATER +// RUN: %dxc -T ps_6_5 -E ps_main %s -spirv -DCULL_FIRST | FileCheck %s --check-prefix=PS +// RUN: %dxc -T ps_6_5 -E ps_main %s -spirv | FileCheck %s --check-prefix=PS +// RUN: %dxc -T ps_6_5 -E ps_main %s -spirv -fcgl -DCULL_FIRST | FileCheck %s --check-prefix=FIRST +// RUN: %dxc -T ps_6_5 -E ps_main %s -spirv -fcgl | FileCheck %s --check-prefix=LATER -// One struct used as a mesh shader primitive output and a pixel shader input, -// with SV_CullPrimitive as the first member and as a later one. The pixel -// shader replaces it with 0, so the other members must keep the locations the -// mesh shader gave them, and the 0 must land in the right struct member. +// The other members keep the mesh shader's locations, and the 0 lands in the +// right member, whether SV_CullPrimitive is first or not. // MS: OpDecorate {{%[0-9]+}} BuiltIn CullPrimitiveEXT // MS-DAG: OpDecorate %out_var_PRIMID Location 0 From 9382ff88a0ce19dfc323322162c4891f567868d4 Mon Sep 17 00:00:00 2001 From: Abir Deol Date: Mon, 2 Nov 2026 12:05:01 -0700 Subject: [PATCH 4/4] [SPIR-V] Keep vk::builtin on pixel shader SV_CullPrimitive inputs An explicit vk::builtin took precedence over SV_CullPrimitive before the PS input was replaced with 0. Skip the replacement when the attribute is present. --- tools/clang/lib/SPIRV/DeclResultIdMapper.cpp | 6 ++++-- .../CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp index 37478a9db75..6f080b2fe06 100644 --- a/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp +++ b/tools/clang/lib/SPIRV/DeclResultIdMapper.cpp @@ -3869,9 +3869,11 @@ bool DeclResultIdMapper::createStageVars(StageVarDataBundle &stageVarData, } // DXIL replaces a pixel shader SV_CullPrimitive input with 0, and - // CullPrimitiveEXT can't be used in fragment shaders. + // CullPrimitiveEXT can't be used in fragment shaders. An explicit + // vk::builtin still takes precedence. if (semanticKind == hlsl::Semantic::Kind::CullPrimitive && - sigPointKind == hlsl::SigPoint::Kind::PSIn) { + sigPointKind == hlsl::SigPoint::Kind::PSIn && + !stageVarData.decl->hasAttr()) { *value = spvBuilder.getConstantNull(stageVarData.type); return true; } diff --git a/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl b/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl index 1c55ac25f15..5e2cbefd0e5 100644 --- a/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl +++ b/tools/clang/test/CodeGenSPIRV/meshshading.ext.cullprimitive.ps.hlsl @@ -7,6 +7,7 @@ // RUN: %dxc -T ps_6_5 -E main %s -spirv -fcgl -DCULL_TYPE=uint | FileCheck %s --check-prefix=UINT // RUN: %dxc -T ps_6_5 -E main %s -spirv -fcgl -DCULL_TYPE=bool2 | FileCheck %s --check-prefix=BOOL2 // RUN: %dxc -T ps_6_5 -E main %s -spirv -fcgl -DCULL_ARRAY | FileCheck %s --check-prefix=ARRAY +// RUN: %dxc -T ps_6_5 -E main %s -spirv -fcgl -DCULL_BUILTIN | FileCheck %s --check-prefix=BUILTIN // SV_CullPrimitive as a pixel shader input is replaced with 0, like DXIL. // DXIL doesn't check the type here, so aggregates are replaced too. @@ -27,8 +28,15 @@ // ARRAY: [[null:%[0-9]+]] = OpConstantNull %_arr_bool_uint_2 // ARRAY: OpStore %param_var_cull [[null]] +// An explicit vk::builtin is not replaced. +// BUILTIN-NOT: OpConstantNull +// BUILTIN: OpLoad %bool %HelperInvocation + #ifdef CULL_ARRAY float4 main(bool cull[2] : SV_CullPrimitive) : SV_Target { return cull[1]; } +#elif defined(CULL_BUILTIN) +float4 main([[vk::builtin("HelperInvocation")]] bool cull : SV_CullPrimitive) + : SV_Target { return cull; } #else float4 main(CULL_TYPE cull : SV_CullPrimitive) : SV_Target { return cull.x; } #endif