stride3d/stride · error · InvalidOperationException

Can't write value on input or output struct

Error message

Can't write value on input or output struct

What it means

ReadWriteAnalyzer validates stream (input/output interface) access chains during SPIR-V post-processing. Writing through an access chain whose base is an Input or Output streams struct is illegal at this stage (stage outputs are assembled elsewhere), so the analyzer throws InvalidOperationException.

Solutions

  1. Remove the store into the input/output streams struct in the shader code
  2. Write outputs only through the stage's declared output mechanism (e.g. the SDSL stage output assignment, not a raw access-chain store)
  3. Check that the variable's StreamsKindSDSL decoration (Input vs Output) matches the intended usage
  4. If this is a compiler transformation emitting the store, fix the transformation to route writes to the correct target

Example fix

// before (SDSL)
streams.Position = position; // writing to input stream
// after
var p = streams.Position; // read only from input streams
Defensive patterns

Strategy: validation

Validate before calling

if (accessChain.IsStore && streams.TryGetValue(accessChain.Base, out var si) && si.Kind is StreamsKindSDSL.Input or StreamsKindSDSL.Output)
    throw new InvalidOperationException("Store to input/output stream is not allowed");

Type guard

bool IsStreamReadable(StreamInfo s) => s.Kind is StreamsKindSDSL.Input or StreamsKindSDSL.Output;

Try / catch

try { analyzer.AnalyzeStreamReadWrites(module); } catch (InvalidOperationException ex) { logger.LogError(ex, "Illegal stream write"); throw new ShaderCompilationException("Input/output streams are read-only at this stage", ex); }

Prevention

When it happens

Trigger: A shader writes to a variable in the `streams` input or output struct via an OpAccessChain + OpStore inside AnalyzeStreamReadWrites — e.g. assigning to an input stream member or storing directly into the output stream in an unexpected place.

Common situations: Shader code doing `streams.Normal = ...` on an input stream, or a pass/transformation writing the output stream where the analyzer doesn't expect a store; mislabeled StreamsKindSDSL decoration on a variable.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/433b67e10d4a5556. Report an issue: GitHub.

Appendix: source

Thrown at sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Analysis/ReadWriteAnalyzer.cs:138

                if (variables.TryGetValue(accessChain.Base, out var variableInfo))
                    variableInfo.UsedThisStage = true;
                if (analysisResult.Resources.TryGetValue(accessChain.Base, out var resourceInfo))
                    resourceInfo.UsedThisStage = true;
                if (analysisResult.CBuffers.TryGetValue(accessChain.Base, out var cbufferInfo))
                    cbufferInfo.UsedThisStage = true;
            }
            else if (i.Op is Op.OpStore && new OpStore(ref i) is { } store)
            {
                // Check for indirect access chains
                if (!accessChainBases.TryGetValue(store.Pointer, out var accessChain))
                    accessChain.Base = store.Pointer;

                if (streams.TryGetValue(accessChain.Base, out var streamInfo))
                {
                    var streamKind = accessChain.StreamKind!.Value;
                    // Write on input/output stream are not allowed
                    if (streamKind is StreamsKindSDSL.Input or StreamsKindSDSL.Output)
                        throw new InvalidOperationException("Can't write value on input or output struct");

                    streamInfo.Write = true;
                }
                if (variables.TryGetValue(accessChain.Base, out var variableInfo))
                    variableInfo.UsedThisStage = true;
                if (analysisResult.Resources.TryGetValue(accessChain.Base, out var resourceInfo))
                    resourceInfo.UsedThisStage = true;
                if (analysisResult.CBuffers.TryGetValue(accessChain.Base, out var cbufferInfo))
                    cbufferInfo.UsedThisStage = true;
            }
            else if (i.Op is Op.OpAtomicIAdd or Op.OpAtomicISub or Op.OpAtomicUMin or Op.OpAtomicUMax
                or Op.OpAtomicSMin or Op.OpAtomicSMax or Op.OpAtomicAnd or Op.OpAtomicOr or Op.OpAtomicXor
                or Op.OpAtomicExchange or Op.OpAtomicCompareExchange or Op.OpAtomicLoad or Op.OpAtomicStore
                or Op.OpAtomicIIncrement or Op.OpAtomicIDecrement)
            {
                // Atomic operations reference a pointer (word 3 after opcode, i.e. first operand after ResultType+ResultId or just after opcode for OpAtomicStore)
                // Extract the pointer and mark the underlying resource as used
                var pointer = i.Op == Op.OpAtomicStore

View on GitHub (pinned to 96fad776d2)