stride3d/stride · error · InvalidOperationException

outputcontrolpoints not found on hull shader

Error message

outputcontrolpoints not found on hull shader {entryPoint.Id.Name}

What it means

When generating a stream wrapper for a hull shader, InterfaceProcessor.FindOutputPatchSize scans the hull entry point for an outputcontrolpoints attribute to determine the output patch array size. If no such attribute is found on the hull entry point it throws, because HS output signatures require the control-point count.

Solutions

  1. Add the outputcontrolpoints attribute to the hull shader entry point class (e.g. [outputcontrolpoints(3)] or the SDSL equivalent HullDomainControlPoints)
  2. Ensure the hull entry point is the class actually carrying the attribute (check composition overrides)
  3. Verify the SDSL syntax — the attribute must be on the entry-point method/class, not a helper function

Example fix

// before
[WaveSize(64)]
public override void HSMain() { ... } // no outputcontrolpoints
// after
[outputcontrolpoints(3)]
public override void HSMain() { ... }
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the hull entry point class declares the control point count
var hsClass = composition.Classes.First(c => c.GetMethods().Any(m => m.Name == "HSMain"));
if (!hsClass.GetCustomAttributesData().Any(a => a.AttributeType.Name.Contains("outputcontrolpoints"))
    && !hsClass.GetMethods().Any(m => m.CustomAttributes.Any(a => a.AttributeType.Name.Contains("outputcontrolpoints"))))
    throw new InvalidOperationException("Hull shader must declare [outputcontrolpoints(N)].");

Try / catch

try
{
    mixer.MergeSDSL(shaderSource, options, log, out var bytecode, out _, out _, out _);
}
catch (InvalidOperationException ex) when (ex.Message.StartsWith("outputcontrolpoints not found"))
{
    log.Error("Hull shader missing outputcontrolpoints attribute: " + ex.Message);
}

Prevention

When it happens

Trigger: MergeSDSL with a hull shader (HSMain) whose entry point class does not declare an outputcontrolpoints (patch constant/control point count) attribute, so the scan over the entry point's execution-mode/decoration instructions finds nothing.

Common situations: Porting an HLSL hull shader and omitting the outputcontrolpoints property; a hull mixin that inherits from a base which normally provides the attribute but the override lost it; composing a domain shader's hull counterpart from a class missing stage attributes.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


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

Appendix: source

Thrown at sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/InterfaceProcessor.cs:472

        static int FindOutputPatchSize(SpirvContext context, Symbol entryPoint, List<int> overrideChain)
        {
            for (var index = overrideChain.Count - 1; index >= 0; index--)
            {
                foreach (var i in context)
                {
                    if (i.Op == Op.OpExecutionMode && (OpExecutionMode)i is
                        {
                            EntryPoint: var target,
                            Mode: ExecutionMode.OutputVertices,
                            ModeParameters: { } m,
                        } && target == overrideChain[index])
                    {
                        return m.Span[0];
                    }
                }
            }

            throw new InvalidOperationException($"outputcontrolpoints not found on hull shader {entryPoint.Id.Name}");
        }

        private EntryPointInfo GenerateStreamWrapper(SymbolTable table, SpirvBuffer buffer, SpirvContext context, ExecutionModel executionModel, Symbol entryPoint, AnalysisResult analysisResult, LiveAnalysis liveAnalysis, bool isFirstActiveShader)
        {
            var streams = analysisResult.Streams;

            var stage = ExecutionModelToStageId(executionModel);

            var entryPointFunctionType = (FunctionType)entryPoint.Type;
            var overrideChain = ResolveOverrideChain(table, entryPoint);
            // TODO: check all parameters instead of hardcoded 0
            int? arrayInputSize = executionModel switch
            {
                ExecutionModel.Geometry => ((ArrayType)((PointerType)entryPointFunctionType.ParameterTypes[0].Type).BaseType).Size,
                ExecutionModel.TessellationControl or ExecutionModel.TessellationEvaluation => ((PatchType)((PointerType)entryPointFunctionType.ParameterTypes[0].Type).BaseType).Size,
                _ => null,
            };
            int? arrayOutputSize = executionModel switch

View on GitHub (pinned to 96fad776d2)