stride3d/stride · error · NotImplementedException

Unsupported OpSpecConstantOp inner op

Error message

Unsupported OpSpecConstantOp inner op: {op}

What it means

ConstantExpression.ParseInstruction throws this NotImplementedException when an OpSpecConstantOp instruction uses an inner opcode that the expression builder does not implement. Only a fixed set of unary, binary, and OpSelect operations is supported; anything else (e.g. OpUMulExtended, OpSMulExtended, matrix ops) cannot be represented as a ConstantExpression tree.

Solutions

  1. Identify the inner op from the message and restructure the shader so the spec constant uses only supported arithmetic/bitwise/logical/select ops.
  2. Pre-fold the expression to a plain OpConstant before handing the buffer to ParseFromBuffer (e.g. constant-fold in the producing tool).
  3. If the op is legitimately needed, add a case to the switch in ConstantExpression.cs and a matching representation in the ConstantExpression hierarchy.
  4. Check whether a simple conversion (OpSConvert/OpUConvert) was intended and use one of the supported convert ops instead.

Example fix

// before: spec constant uses an unimplemented inner op (e.g. OpBitFieldUExtract)
// after: fold it or use a supported op
var supported = ConstantExpression.ParseFromBuffer(idOfIAddSpecConstant, buffer, context); // OpIAdd is in the supported set
Defensive patterns

Strategy: try-catch

Validate before calling

static readonly HashSet<Op> SupportedSpecOps = new()
{
    Op.OpConvertFToS, Op.OpConvertFToU, Op.OpConvertSToF, Op.OpConvertUToF,
    Op.OpSNegate, Op.OpFNegate, Op.OpNot, Op.OpLogicalNot,
    Op.OpIAdd, Op.OpISub, Op.OpIMul, Op.OpUDiv, Op.OpSDiv,
    Op.OpFAdd, Op.OpFSub, Op.OpFMul, Op.OpFDiv,
    Op.OpShiftRightLogical, Op.OpShiftRightArithmetic, Op.OpShiftLeftLogical,
    Op.OpBitwiseOr, Op.OpBitwiseXor, Op.OpBitwiseAnd,
    Op.OpLogicalOr, Op.OpLogicalAnd, Op.OpLogicalEqual, Op.OpLogicalNotEqual,
    Op.OpIEqual, Op.OpINotEqual,
    Op.OpULessThan, Op.OpSLessThan, Op.OpUGreaterThan, Op.OpSGreaterThan,
    Op.OpULessThanEqual, Op.OpSLessThanEqual, Op.OpUGreaterThanEqual, Op.OpSGreaterThanEqual,
    Op.OpSelect
};
static bool IsSupportedSpecConstantOp(SpirvBuffer buffer, int id)
    => buffer.TryGetInstructionById(id, out var inst)
       && inst.Op == Op.OpSpecConstantOp
       && SupportedSpecOps.Contains((Op)inst.Data.Memory.Span[3]);

Try / catch

try
{
    var expr = ConstantExpression.ParseFromBuffer(id, buffer, context);
}
catch (NotImplementedException ex) when (ex.Message.StartsWith("Unsupported OpSpecConstantOp"))
{
    // fall back to leaving the raw spec constant unresolved, or pre-fold upstream
}

Prevention

When it happens

Trigger: Parsing a SPIR-V buffer containing a specialization-constant operation outside the supported list in ConstantExpression.cs:161-219 — e.g. a spec constant computed with OpBitFieldInsert, OpUMulExtended, or any op not enumerated in the switch.

Common situations: Shaders compiled with aggressive constant propagation producing exotic spec-constant ops; SPIR-V from other compilers (glslang, DXC) using spec-constant operations Stride's SDSL pipeline never emits; using OpSpecConstantOp to encode casts like OpUConvert/OpSConvert that are missing from the conversion list.

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/d80821d7c702710f. Report an issue: GitHub.

Appendix: source

Thrown at sources/shaders/Stride.Shaders.Parsers/Core/ConstantExpression.cs:219

                    case Op.OpSGreaterThan:
                    case Op.OpULessThanEqual:
                    case Op.OpSLessThanEqual:
                    case Op.OpUGreaterThanEqual:
                    case Op.OpSGreaterThanEqual:
                    {
                        var left = ParseFromBuffer(inst.Data.Memory.Span[4], buffer, context);
                        var right = ParseFromBuffer(inst.Data.Memory.Span[5], buffer, context);
                        return new BinaryOpExpr(op, left, right);
                    }
                    case Op.OpSelect:
                    {
                        var cond = ParseFromBuffer(inst.Data.Memory.Span[4], buffer, context);
                        var trueVal = ParseFromBuffer(inst.Data.Memory.Span[5], buffer, context);
                        var falseVal = ParseFromBuffer(inst.Data.Memory.Span[6], buffer, context);
                        return new SelectExpr(cond, trueVal, falseVal);
                    }
                    default:
                        throw new NotImplementedException($"Unsupported OpSpecConstantOp inner op: {op}");
                }
            }

            default:
                throw new NotImplementedException($"Cannot parse constant expression from {inst.Op}");
        }
    }
}

/// <summary>
/// Integer constant. Covers int, uint, long, ulong — signedness determined at emission by SPIR-V type context.
/// </summary>
public sealed record IntConstExpr(long Value) : ConstantExpression
{
    public override int Emit(SpirvContext context)
    {
        // For values that fit in int, use int (signed) — matches the common case for array sizes
        if (Value is >= int.MinValue and <= int.MaxValue)

View on GitHub (pinned to 96fad776d2)