stride3d/stride · error · NotSupportedException

Unsupported interlocked operation

Error message

Unsupported interlocked operation: {op}

What it means

CompileInterlockedCall maps an InterlockedOp enum value to its SPIR-V atomic opcode (OpAtomicIAdd, OpAtomicAnd, OpAtomicUMax, etc.). The switch covers Add, And, Or, Xor, Max, Min, Exchange, CompareExchange, CompareStore; any other op value falls to the default and throws NotSupportedException.

Solutions

  1. Add a case in the switch in CompileInterlockedCall mapping the new InterlockedOp to its SPIR-V atomic opcode.
  2. Check which Interlocked intrinsic your shader code calls and confirm it is in the supported set (Add, And, Or, Xor, Min, Max, Exchange, CompareExchange, CompareStore).
  3. If the op is unsupported on SPIR-V, rewrite the shader using an emulated CAS (compare-exchange) loop.
  4. Report/patch upstream in Stride's IntrinsicImplementations if a standard op is missing.

Example fix

// before (C#)
InterlockedOp.Xchg => /* missing */
_ => throw new NotSupportedException($"Unsupported interlocked operation: {op}"),
// after
InterlockedOp.Exchange => Specification.Op.OpAtomicExchange,
InterlockedOp.Xchg => Specification.Op.OpAtomicExchange, // add the missing mapping
Defensive patterns

Strategy: type-guard

Validate before calling

static readonly HashSet<InterlockedOp> Supported = new() {
    InterlockedOp.Add, InterlockedOp.And, InterlockedOp.Or, InterlockedOp.Xor,
    InterlockedOp.Min, InterlockedOp.Max, InterlockedOp.Exchange,
    InterlockedOp.CompareExchange, InterlockedOp.CompareStore };
bool IsSupportedInterlockedOp(InterlockedOp op) => Supported.Contains(op);

Try / catch

try { result = CompileInterlockedCall(table, ctx, builder, op, dest, value); }
catch (NotSupportedException ex) { /* map op to SPIR-V opcode or reject at compile time */ }

Prevention

When it happens

Trigger: An InterlockedOp enum value outside the implemented set reaching CompileInterlockedCall — typically from a new intrinsic wired to the dispatcher without adding its opcode mapping, or internal dispatch confusion.

Common situations: Adding a new interlocked intrinsic in SDSL but forgetting to extend the opcode switch; a frontend feature (e.g. InterlockedAdd on floats via a special op flag) not yet mapped.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/IntrinsicImplementations.cs:665

            originalValue = new SpirvValue(instruction.ResultId, instruction.ResultType);
        }
        else
        {
            var instruction = builder.Insert(new OpAtomicIAdd(context.GetOrRegister(resultType), context.Bound++, dest.Id,
                context.CompileConstant((int)Specification.Scope.Device).Id,
                context.CompileConstant((int)Specification.MemorySemanticsMask.Relaxed).Id,
                value.Id));
            // Update instruction type (they all share same memory layout)
            instruction.InstructionMemory.Span[0] = (int)(instruction.InstructionMemory.Span[0] & 0xFFFF0000) | (int)(op switch
            {
                InterlockedOp.Add => Specification.Op.OpAtomicIAdd,
                InterlockedOp.And => Specification.Op.OpAtomicAnd,
                InterlockedOp.Or => Specification.Op.OpAtomicOr,
                InterlockedOp.Xor => Specification.Op.OpAtomicXor,
                InterlockedOp.Max => s.IsSigned() ? Specification.Op.OpAtomicSMax : Specification.Op.OpAtomicUMax,
                InterlockedOp.Min => s.IsSigned() ? Specification.Op.OpAtomicSMin : Specification.Op.OpAtomicUMin,
                InterlockedOp.Exchange => Specification.Op.OpAtomicExchange,
                _ => throw new NotSupportedException($"Unsupported interlocked operation: {op}"),
            });
            originalValue = new SpirvValue(instruction.ResultId, instruction.ResultType);
        }

        // Out parameter?
        if (originalLocation is { } originalLocationValue)
        {
            var originalLocationType = context.ReverseTypes[originalLocationValue.TypeId];
            if (originalLocationType is not PointerType originalLocationPointerType)
                throw new InvalidOperationException($"out parameter is not a l-value, got {originalLocationType} instead");

            originalValue = builder.Convert(context, originalValue, originalLocationPointerType.BaseType);
            builder.Insert(new OpStore(originalLocationValue.Id, originalValue.Id, null, []));
        }

        return new();
    }

View on GitHub (pinned to 96fad776d2)