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
- 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.
- Rewrite the shader to only access cbuffer members through member access (HLSL field access), never the cbuffer object itself.
- 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.
- 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
- Never reference the cbuffer object itself in shader code — access members only.
- Avoid passing entire constant buffers as function parameters in SDSL/HLSL.
- Keep the HLSL->SPIR-V front-end (DXC/glslang) updated; older versions may emit OpLoad on the cbuffer.
- Add a spirv-dis check in CI to detect OpLoad/OpStore on cbuffer variables before mixing.
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
- spirv_to_dxil_pipeline failed; SPIR-V dumped to {dumpPath} {
- Unsupported composite type {Type} during regrouping
- Symbol {symbol} has not been imported or created properly
- Exception of type 'System.NotImplementedException' was throw
- Unsupported AlignmentRules value: {alignmentRules}
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)