Skip to content

Texture.Sample() in a vertex-stage material surface emits invalid SPIR-V (D3D11 compile fails with a bare InternalError) #3313

Description

@delustra

Release Type: GitHub (built from source)

Version: master (reproduced against a 4.4.1 fork). New in 4.4 — the D3D11 path now round-trips SPIR-V → HLSL via SPIRV-Cross; 4.3 went straight to HLSL and never hit this.

Platform(s): Windows — Direct3D11 only. (Vulkan/D3D12 never call LegalizeForHlsl, so they don't hit the failure, though the module is still invalid.)

Describe the bug

A Texture.Sample(...) call in a vertex-stage material surface fails to compile on Direct3D11. The SDSL→SPIR-V frontend emits OpImageSampleImplicitLod for .Sample() regardless of stage, but SPIR-V forbids implicit-LOD instructions outside the Fragment / GLCompute / Mesh / Task execution models (no screen-space derivatives exist in the vertex stage). EffectCompiler then hands the invalid module to LegalizeForHlsl, which fails with a bare spvOptimizerRun InternalError — nothing validates the module first, so the real cause is hidden.

The built-in MaterialDisplacementMapFeature does not hit this: it sets context.IsNotPixelStage = true, which routes its compute-color texture graph through the ComputeColorTextureLod* shaders (SampleLevel). That workaround only covers the compute-color path (ComputeColors/ComputeTextureBase.cs); a custom SDSL shader that calls .Sample() directly is lowered by TextureMethodsImplementations.CompileSample, which is stage-unaware and always emits implicit LOD.

To Reproduce

Minimal SDSL surface that triggers it (the texture fetch is legal HLSL in a vertex shader):

shader VertexTextureFetchSurface : IMaterialSurface, PositionStream, Texturing
{
    override void Compute()
    {
        float h = Texture0.Sample(PointSampler, float2(0.5, 0.5)).r;
        streams.Position = float4(streams.Position.x, h, streams.Position.z, streams.Position.w);
    }
};

…injected into a material's vertex stage via a MaterialFeature using the stock SetStreamFinalModifier API (the same hook MaterialDisplacementMapFeature uses):

context.SetStreamFinalModifier<MyFeature>(MaterialShaderStage.Vertex, new ShaderClassSource("VertexTextureFetchSurface"));

Apply that material to any mesh and run. No texture needs to be bound — the crash is in shader compilation, not rendering. (Full GameStudio repro: shader + feature + steps available if useful.)

Repo with reproduce project: https://github.com/delustra/stride-vtf-spirv-repro

Expected behavior

HLSL/FXC/DXC treat Texture.Sample(...) in a vertex shader as legal and lower it to LOD 0 (no gradients available). Stride should match that — compile and emit OpImageSampleExplicitLod with Lod = 0.0 for implicit-LOD samples reachable from a Vertex / Hull / Domain / Geometry entry point (Fragment / Compute / Mesh / Task left untouched).

Screenshots

N/A.

Log and callstacks

spirv-val on the emitted module (dumped pre-legalization):

OpEntryPoint Entry Point <id> '...VSMain_Wrapper's callgraph contains function <id> '...VertexTextureFetchSurface_Compute', which cannot be used with the current execution model:
ImplicitLod instructions require Fragment, GLCompute, MeshEXT or TaskEXT execution model: ImageSampleImplicitLod

What the engine reports today (LegalizeForHlsl):

InvalidOperationException: spvOptimizerRun failed: InternalError
   at Stride.Shaders.Spirv.Tools.SpirvTools.RunAndCopy(...)
   at Stride.Shades.Spirv.Tools.SpirvTools.LegalizeForHlsl(...)
   at Stride.Shaders.Compiler.EffectCompiler.Compile(...)
   at ...RootEffectRenderFeature.PrepareEffectPermutations

Additional context

Root cause: TextureMethodsImplementations.CompileSample (sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/TextureMethodsImplementations.cs) unconditionally emits OpImageSampleImplicitLod; there is no stage awareness on that path. Compare CompileSampleLevel (~line 139), which correctly emits OpImageSampleExplicitLod.

Sibling of #3303 — different invalid instruction (OpImageSampleImplicitLod vs OpExecutionMode LocalSize), different stage, different root cause, but the same failure mode: the 4.4 SDSL→SPIR-V frontend emits spec-invalid SPIR-V that surfaces as a bare InternalError because the module isn't validated before spirv-opt. #3304 fixes the [numthreads] variant; this one needs its own fix.

Suggested fix: a post-merge pass that walks the call-graph from Vertex / Hull / Domain / Geometry entry points and rewrites any reachable OpImageSampleImplicitLod (+ Dref/Proj variants) to the matching …ExplicitLod opcode with Lod = 0.0 — mirroring the HLSL lowering, and orthogonal to the existing IsNotPixelStage compute-color workaround. (A broader hardening worth considering for this whole family: run spirv-val and surface its message before spirv-opt, so variants like #3303 and this one self-diagnose instead of throwing a bare InternalError.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions