Skip to content

fix: invalid SPIR-V that fails HLSL legalization ([numthreads] on non-entry methods, implicit-LOD sampling outside pixel/compute) - #3304

Open
sasvdw wants to merge 4 commits into
stride3d:masterfrom
LazyWorksZA:fix/sdsl-numthreads-non-entry
Open

fix: invalid SPIR-V that fails HLSL legalization ([numthreads] on non-entry methods, implicit-LOD sampling outside pixel/compute)#3304
sasvdw wants to merge 4 commits into
stride3d:masterfrom
LazyWorksZA:fix/sdsl-numthreads-non-entry

Conversation

@sasvdw

@sasvdw sasvdw commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

PR Details

Summary

Two unrelated SDSL constructs emit invalid SPIR-V. On Direct3D11 both fail the same way.
LegalizeForHlsl throws a bare spvOptimizerRun InternalError, because nothing validates the module
first. This PR fixes both causes and reports the shared failure clearly.

1. [numthreads] on a non-entry compute method (#3303)

The compiler emitted OpExecutionMode LocalSize on the method that carries the attribute. The compute
entry point is a generated CSMain_Wrapper, so the mode landed on a non-entry function. The result was
invalid SPIR-V.

fxc and dxc accept [numthreads] on a non-entry function and then ignore it. The thread group size
comes only from the entry point. Stride now does the same.

  • MethodOrMember.cs: emit LocalSize only when the attribute is on the compute entry
    (EntryPoint == ComputeShader). Otherwise skip it and log a warning that points to
    ThreadNumberX/Y/Z.
  • ShaderMixer.cs: drain SymbolTable.Warnings to log.Warning in MergeSDSL. Before this change the
    code drained only the errors, so the warning never reached the user.
  • ComputeEffectShader.cs: clarify the XML documentation. ThreadNumbers sets the thread group size of
    the compute shader, which is its [numthreads]. ThreadGroupCounts is the dispatch count.

This is not a regression. The classic 4.3 path also dropped a misplaced [numthreads].

2. Implicit-LOD texture sampling outside the pixel and compute stages (#3313)

A Texture.Sample(...) call reachable from a vertex, hull, domain or geometry entry point emitted
OpImageSampleImplicitLod. SPIR-V allows that instruction only in Fragment, GLCompute, MeshEXT
and TaskEXT. Implicit LOD needs screen-space derivatives, and those stages do not have them.

This change reports an error. It does not lower the call to SampleLevel(..., 0), because the shader
was never valid.
I checked both Microsoft compilers directly:

Texture0.Sample(s, uv) in a vertex shader
fxc /T vs_5_0 error X4532: cannot map expression to vs_5_0 instruction set
dxc -T vs_6_0 error: Opcode Sample not valid in shader model vs_6_0"Function uses features incompatible with the shader stage (vs)"
dxc -T vs_6_0 -spirv error: sampling with implicit lod is only allowed in fragment and compute shaders
SPIR-V spec ImplicitLod instructions require Fragment, GLCompute, MeshEXT or TaskEXT execution model

The Microsoft documentation for Sample agrees: "This method can be invoked within a pixel shader, but
it is not supported in a vertex shader or a geometry shader."
The "Vertex-shader stage" page does list
Sample as legal in a vertex shader. That entry is a long-standing documentation error and should read
SampleLevel. SampleGrad and SampleCmpLevelZero do compile for vs_5_0.

So this is not a functional regression, because such a shader never compiled. It is a diagnostics
regression. Version 4.3 reported X4532. Version 4.4 reported spvOptimizerRun InternalError.

The engine has always treated this as illegal. That is why MaterialDisplacementMapFeature sets
IsNotPixelStage and routes texture reads through ComputeColorTextureLod, which uses SampleLevel.

  • ShaderMixer.cs: after interface processing, walk the call graph from each non-fragment entry point.
    Report every implicit-LOD sample that the walk reaches, with its source location and the explicit-LOD
    alternatives:

    VSTextureSample.sdsl(14,25): A texture sample with an implicit level of detail needs a pixel or
    compute shader. A Vertex shader reaches this code. Use SampleLevel, SampleGrad or SampleCmpLevelZero
    to give an explicit level of detail.
    

A silent lowering to LOD 0 would make Stride accept a shader that fxc, dxc and the SPIR-V validator all
reject. It would also change the sampling behavior without telling the author. I am happy to change this
if you prefer the permissive behavior.

3. Shared diagnostics

Related Issue

Fixes #3303.
Fixes #3313.

Repro for #3303: https://github.com/sasvdw/stride-spirv-legalize-repro
Repro for #3313: https://github.com/delustra/stride-vtf-spirv-repro

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have built and run the editor to try this change out.

Validation status

  • Tests: StrideShaderTests gains TextureSampleOutsideFragmentStageIsReported, which uses
    CompilerTests/VSTextureSample.sdsl. It also gains TextureSampleInFragmentStageCompiles, which uses
    CompilerTests/PSTextureSample.sdsl. The second test is the control. It proves that pixel-stage
    sampling still compiles to valid SPIR-V. NumThreadsOnNonEntryMethodIsIgnoredWithWarning still passes.
    I checked each test red before the fix and green after it. The test for Texture.Sample() in a vertex-stage material surface emits invalid SPIR-V (D3D11 compile fails with a bare InternalError) #3313 is a separate commit that
    comes before its fix, so the failure is on record.
  • Suite: 518 tests pass. 10 failures are pre-existing on this branch and unrelated. The test harness
    parses floats from .sdsl test headers with the current culture, so they fail on any comma-decimal
    locale. The same tests fail when I revert these changes. fix: parse and format shader numbers with the invariant culture #3325 fixes them separately.
  • Editor: outstanding. This is the mandatory personal-testing step.
  • Docs: the XML documentation for ComputeEffectShader.ThreadNumbers and ThreadGroupCounts is now
    clearer, and it flows to the API reference. The manual has no compute-shader guide. I track that as a
    separate stride-docs follow-up.

sasvdw and others added 2 commits July 29, 2026 10:54
[numthreads] on a method other than the compute entry point (CSMain) was
emitted as an OpExecutionMode LocalSize on that method's own function id, but
the SPIR-V entry point is a generated CSMain_Wrapper. The mode was left stranded
on a non-entry function, producing invalid SPIR-V that failed SPIRV-Cross HLSL
legalization with a bare spvOptimizerRun InternalError (Direct3D11 path).

HLSL/FXC/DXC treat [numthreads] on a non-entry function as legal and silently
ignored; the thread group size comes only from the entry point. Match that: skip
the LocalSize on a non-entry compute method and warn, pointing to
ThreadNumberX/Y/Z (C#-overridable via ComputeEffectShader.ThreadNumbers, which
also drives dispatch). Drain SymbolTable.Warnings to the log in MergeSDSL so the
warning reaches the user. Also validate the module when LegalizeForHlsl throws,
surfacing the real invalid instruction instead of a bare InternalError.

Adds a regression test (compiles + valid SPIR-V + warning; verified red/green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ThreadNumbers sets the compute shader's thread group size (its [numthreads],
via the ThreadNumberX/Y/Z macros on CSMain); ThreadGroupCounts is the dispatch
count. Documenting this points users at the supported mechanism rather than a
literal [numthreads] in the shader body (which is ignored on non-entry methods).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Draft PR — automatic CI is skipped to save runner minutes.

  • Mark the PR ready for review to run the full automatic CI — or add a ci-run-on-draft label to run it now without leaving draft.
  • Or arm a specific opt-in suite: ci-enduser, ci-editor, ci-ios, ci-android.

sasvdw and others added 2 commits August 4, 2026 21:10
Fails today: the sample lowers to OpImageSampleImplicitLod, which SPIR-V allows
only in Fragment/GLCompute/Mesh/Task, so spirv-val rejects the module and the
D3D11 legalization path dies with a bare InternalError.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
stride3d#3313)

Texture.Sample() reachable from a vertex, hull, domain or geometry entry point
emitted OpImageSampleImplicitLod, which SPIR-V allows only in Fragment, GLCompute,
MeshEXT and TaskEXT. The module then failed validation, and the Direct3D11 path
surfaced it as a bare spvOptimizerRun InternalError.

fxc rejects the equivalent HLSL with X4532 and dxc with "Opcode Sample not valid in
shader model vs_6_0", so such a shader was never valid; the pre-SPIR-V compiler
reported it clearly. Restore that diagnostic by walking the call graph from each
non-fragment entry point and reporting any implicit-LOD sample, with its source
location and the explicit-LOD alternatives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sasvdw sasvdw changed the title fix: ignore [numthreads] on non-entry compute methods (match HLSL) fix: invalid SPIR-V that fails HLSL legalization ([numthreads] on non-entry methods, implicit-LOD sampling outside pixel/compute) Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants