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
- Add the outputcontrolpoints attribute to the hull shader entry point class (e.g. [outputcontrolpoints(3)] or the SDSL equivalent HullDomainControlPoints)
- Ensure the hull entry point is the class actually carrying the attribute (check composition overrides)
- 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
- Always pair hull shaders with [outputcontrolpoints(N)] on the entry point
- When porting HLSL hull shaders, carry over all patch attributes, not just patchconstantfunc
- Add a reflection-based test that compiles all hull shaders in the project
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
- Hull shader patch constant function
- Can't cast from to (larger matrix)
- Can't cast from to
- Can't cast from to
- Can't figure array output size for tessellation shader
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 switchView on GitHub (pinned to 96fad776d2)