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

  1. Make the dest an int/uint storage location: use RWStructuredBuffer<int/uint> or groupshared int/uint and pass the element reference.
  2. For float atomics, emulate with InterlockedCompareExchange on uint bits or use asuint-based CAS loops.
  3. Ensure you pass the l-value itself (e.g. buffer[index]) not a previously copied value.
  4. 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

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


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)