stride3d/stride · error · InvalidOperationException
l-value int or uint expected but got
Error message
l-value int or uint expected but got {destType} What it means
CompileInterlockedCall implements InterlockedAdd/Exchange/CompareExchange and friends as SPIR-V atomic opcodes. SPIR-V atomics require the destination to be a pointer to an int or uint scalar; if the dest argument is not a PointerType or points to a non-integer scalar (e.g. float), InvalidOperationException is thrown.
Solutions
- Make the dest an int/uint storage location: use RWStructuredBuffer<int/uint> or groupshared int/uint and pass the element reference.
- For float atomics, emulate with InterlockedCompareExchange on uint bits or use asuint-based CAS loops.
- Ensure you pass the l-value itself (e.g. buffer[index]) not a previously copied value.
- Verify the pointed-to scalar type resolves to int or uint; cast the buffer element type if needed.
Example fix
// before (SDSL/HLSL) float counter; InterlockedAdd(counter, 1); // throws // after uint counter; InterlockedAdd(counter, 1u); // dest must be l-value int/uint
Defensive patterns
Strategy: validation
Validate before calling
bool IsInterlockedDest(SpirvContext ctx, SpirvValue dest) =>
ctx.ReverseTypes[dest.TypeId] is PointerType { BaseType: ScalarType { Type: Scalar.UInt or Scalar.Int } }; Type guard
bool IsIntPointer(TypeBase t) => t is PointerType { BaseType: ScalarType { Type: Scalar.Int or Scalar.UInt } }; Try / catch
try { result = CompileInterlockedCall(table, ctx, builder, op, dest, value); }
catch (InvalidOperationException ex) { /* report dest must be l-value int/uint */ } Prevention
- Only perform atomics on int/uint storage (buffers, groupshared)
- Emulate float atomics with asuint CAS loops
- Pass the l-value itself, not a copied value
When it happens
Trigger: Passing a regular (non-l-value) value, a float-typed variable, or a struct member of unsupported type as the dest of an InterlockedAdd/InterlockedExchange/InterlockedCompareExchange call.
Common situations: Doing atomic adds on float buffers (not supported by this path); passing a local variable instead of a RWByteAddressBuffer/RWStructuredBuffer element or groupshared variable; HLSL code ported from float atomics on other APIs (e.g. CUDA) that Vulkan doesn't expose here.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Unsupported interlocked operation
- out parameter is not a l-value, got
- Unsupported element type for clamp
- Unsupported mul operand types
- Unsupported element type for saturate
AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14).
Data as JSON: /api/errors/9858aca9edfeb8d0.
Report an issue: GitHub.
Appendix: source
Thrown at sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/IntrinsicImplementations.cs:633
public static SpirvValue CompileGLSLFloatBinaryCall(SymbolTable table, SpirvContext context, SpirvBuilder builder, SymbolType resultType, Specification.GLSLOp op, SpirvValue x, SpirvValue y)
{
var instruction = builder.Insert(new GLSLPow(context.GetOrRegister(resultType), context.Bound++, context.GetGLSL(), x.Id, y.Id));
// Adjust OpCode only since Pow/Atan2/etc. share the same operands
instruction.InstructionMemory.Span[4] = (int)op;
return new(instruction.ResultId, instruction.ResultType);
}
public static SpirvValue CompileBitcastCall(SymbolTable table, SpirvContext context, SpirvBuilder builder, SymbolType resultType, SpirvValue x)
{
var instruction = builder.Insert(new OpBitcast(context.GetOrRegister(resultType), context.Bound++, x.Id));
return new(instruction.ResultId, instruction.ResultType);
}
public static SpirvValue CompileInterlockedCall(SymbolTable table, SpirvContext context, SpirvBuilder builder, InterlockedOp op, SpirvValue dest, SpirvValue value, SpirvValue? originalLocation = null, SpirvValue? compare = null)
{
var destType = context.ReverseTypes[dest.TypeId];
if (destType is not PointerType pointerType || pointerType.BaseType is not ScalarType { Type: Scalar.UInt or Scalar.Int } s)
throw new InvalidOperationException($"l-value int or uint expected but got {destType}");
var resultType = s;
// If there is an out parameter to save original value
SpirvValue originalValue;
if (op == InterlockedOp.CompareStore || op == InterlockedOp.CompareExchange)
{
var instruction = builder.Insert(new OpAtomicCompareExchange(context.GetOrRegister(resultType), context.Bound++, dest.Id,
context.CompileConstant((int)Specification.Scope.Device).Id,
context.CompileConstant((int)Specification.MemorySemanticsMask.Relaxed).Id,
context.CompileConstant((int)Specification.MemorySemanticsMask.Relaxed).Id,
compare!.Value.Id,
value.Id));
originalValue = new SpirvValue(instruction.ResultId, instruction.ResultType);
}
else
{
var instruction = builder.Insert(new OpAtomicIAdd(context.GetOrRegister(resultType), context.Bound++, dest.Id,View on GitHub (pinned to 96fad776d2)