stride3d/stride · error · NotSupportedException

Can't OpLoad with cbuffer

Error message

Can't OpLoad with cbuffer

What it means

While merging cbuffer declarations across mixed SDSL shader classes into a single SPIR-V struct, the mixer scans the SPIR-V for direct OpLoad instructions targeting the cbuffer variable pointer. Only OpAccessChain-based access is supported; a direct OpLoad on the cbuffer variable means the compiler cannot rewrite the access to point at the merged struct, so it throws NotSupportedException to fail loudly rather than corrupt the module.

Solutions

  1. Find the shader/access performing the direct load: disassemble the SPIR-V (spirv-dis) and look for OpLoad whose result type is the cbuffer struct pointer.
  2. Rewrite the shader to only access cbuffer members through member access (HLSL field access), never the cbuffer object itself.
  3. If the SPIR-V comes from a front-end (DXC/glslang), check its version and flags — update or adjust options so it doesn't emit OpLoad on the cbuffer.
  4. If it's a legitimate pattern, extend the mixer to handle OpLoad on cbuffers by rewriting the result type, following the OpAccessChain rewrite logic.

Example fix

// before: passing whole cbuffer (front-end may emit OpLoad on it)
float4 DoThing(ConstantBuffer<MyCB> cb) { return cb.color; }

// after: access members only
float4 DoThing() { return MyCBuffer.color; }
Defensive patterns

Strategy: try-catch

Validate before calling

// After SPIR-V generation, before mixing: reject direct loads on cbuffer variables
foreach (var inst in module.Instructions)
    if (inst.Op == Op.OpLoad && cbufferVariableIds.Contains(((OpLoad)inst).Pointer))
        throw new InvalidOperationException("Shader performs OpLoad directly on a cbuffer; only member access (OpAccessChain) is supported.");

Type guard

static bool LoadsCbuffer(Instruction i, HashSet<uint> cbufferVarIds) => i.Op == Op.OpLoad && ((OpLoad)i).Pointer != null && cbufferVarIds.Contains(((OpLoad)i).Pointer);

Try / catch

try { mixer.MergeSDSL(...); }
catch (NotSupportedException ex) { log.Error($"Cbuffer merge failed: {ex.Message}. Check that the shader only accesses cbuffer members."); throw; }

Prevention

When it happens

Trigger: MergeCBuffers (called from MergeSDSL) encounters an OpLoad whose pointer operand resolves, via the collected `variables` map, to a cbuffer variable — i.e. shader code (or generated SPIR-V from a toolchain) loads the cbuffer's flat pointer instead of accessing it through an access chain.

Common situations: HLSL->SPIR-V front-ends (DXC/glslang) emitting a load of the constant buffer itself (e.g. binding a whole cbuffer to a UBO-typed function parameter or copying it); hand-written or tool-generated SPIR-V that copies a cbuffer; edge-case shader patterns that pass entire constant buffers around.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at sources/shaders/Stride.Shaders.Compilers/SDSL/ShaderMixer.CBuffers.cs:323

                        if (i.Op == Op.OpAccessChain && (OpAccessChain)i is { } accessChain)
                        {
                            if (variables.TryGetValue(accessChain.BaseId, out var cbuffer) && cbuffer.MemberIndexOffset > 0)
                            {
                                // According to spec, this must be a OpConstant (and we only create them with int)
                                var indexes = accessChain.Indexes.Elements.Span;
                                var constantId = indexes[0];
                                var index = cbuffer.MemberIndexOffset + (int)context.GetConstantValue(constantId);
                                indexes[0] = context.CompileConstant(index).Id;

                                // Regenerate buffer (since we modify accessChain.Indexes, it doesn't get rebuilt automatically)
                                accessChain.UpdateInstructionMemory();
                            }
                        }
                        // Out of safety, check for any OpLoad/OpStore on the variables (forbidden, only OpAccessChain)
                        else if (i.Op == Op.OpLoad && (OpLoad)i is { } load)
                        {
                            if (variables.TryGetValue(load.Pointer, out var cbuffer))
                                throw new NotSupportedException("Can't OpLoad with cbuffer");
                        }
                        else if (i.Op == Op.OpStore && (OpStore)i is { } store)
                        {
                            if (variables.TryGetValue(store.Pointer, out var cbuffer))
                                throw new NotSupportedException("Can't OpLoad with cbuffer");
                        }
                    }

                    // Update first variable to use new type
                    cbuffersSpan[0].Variable.Data.IdResultType = mergedCbufferPtrStructId;
                    cbufferMemberMetadata[cbuffersSpan[0].VariableId] = GenerateCBufferLinks(cbuffersSpan[0].VariableId, cbuffersSpan, mergedCbufferStruct);

                    foreach (var i in buffer)
                    {
                        if (i.Op == Op.OpName && (OpName)i is { } name)
                        {
                            // Ensure cbuffer variable name is correct (it might still have a pending number such as Test.0 if there was multiple buffers with same name)
                            if (cbuffersSpan[0].VariableId == name.Target)

View on GitHub (pinned to 96fad776d2)