stride3d/stride · error · InvalidOperationException
Hull OutputPatch can only be used in once place (constant pa
Error message
Hull OutputPatch can only be used in once place (constant patch)
What it means
The wrapper generator emits the hull (TessellationControl) output-patch code exactly once via a lazily-invoked local function guarded by the hullTessellationOutputsGenerated flag. If GenerateHullTessellationOutputs is invoked a second time (i.e. another OutputPatch-style use point was reached after code was already emitted), it throws InvalidOperationException, since the constant patch initialization can only be emitted at a single place.
Solutions
- Restructure the hull shader entry point so the output patch / HS_OUTPUT is referenced by exactly one out parameter.
- Merge multiple output parameters into a single structure parameter so only one emission point is needed.
- If this comes from generated/processed signatures, adjust the pre-processing so hull outputs are coalesced before GenerateWrapper runs.
- Inspect the call sites of GenerateHullTessellationOutputs in EntryPointWrapperGenerator.cs to identify which signature pattern triggers the second invocation.
Example fix
// before void HSMain(InputPatch<VS_OUTPUT, 3> input, out HS_OUTPUT output, out HS_PATCH patch) // two output-patch usage points // after void HSMain(InputPatch<VS_OUTPUT, 3> input, out HS_RESULT result) // single out containing both payload and patch constants
Defensive patterns
Strategy: validation
Validate before calling
// Hull entry points must have at most one out parameter that consumes the output patch int patchUsages = entryPoint.Parameters.Count(p => p.IsOut && IsOutputPatchKind(p.TypeKind)); bool ok = executionModel != ExecutionModel.TessellationControl || patchUsages <= 1;
Type guard
static bool HullHasSingleOutputPatchUsage(EntryPointSignature sig) =>
sig.OutParameters.Count(p => p.Kind == StreamsKindSDSL.Output || p.Kind == StreamsKindSDSL.Constants) <= 2
&& sig.OutParameters.Count(p => p.Kind == StreamsKindSDSL.Output) <= 1; Try / catch
try
{
var wrapper = EntryPointWrapperGenerator.GenerateWrapper(...);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("OutputPatch can only be used in once place"))
{
log.Error("Hull shader references output patch from multiple out parameters; restructure signature");
throw new ShaderCompilationException("Multiple output-patch usage points in hull entry point", ex);
} Prevention
- Use exactly one out parameter for the hull payload (combine payload and patch constants into one struct).
- Avoid duplicating OutputPatch-style out parameters in tessellation-control entry points.
- If your pre-processor rewrites signatures, assert it never splits hull outputs into two usage points.
- Add a signature-shape unit test for tessellation-control entry points covering single vs multiple out parameters.
When it happens
Trigger: During TessellationControl wrapper generation, the generated code path calls GenerateHullTessellationOutputs more than once — typically two or more out-parameters/blocks in the entry-point signature that each reference the hull outputs (OutputPatch/HS_OUTPUT style usage).
Common situations: Hull shaders with multiple 'out' parameters both needing the output patch (e.g. separate HS_OUTPUT and patch outputs in unusual signatures); entry points where generated wrapper logic encounters the outputs section twice due to signature shape; hand-modified SDSL that duplicates OutputPatch usage.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Can't figure array output size for tessellation shader
- Can't process argument {i + 1} of type {parameterType} in me
- spirv_to_dxil_pipeline failed; SPIR-V dumped to {dumpPath} {
- Can't OpLoad with cbuffer
- Unsupported composite type {Type} during regrouping
AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14).
Data as JSON: /api/errors/cf8887661053a1c7.
Report an issue: GitHub.
Appendix: source
Thrown at sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Generation/EntryPointWrapperGenerator.cs:166
var inputsData1 = buffer.Add(new OpCompositeConstruct(context.GetOrRegister(new ArrayType(streamLayout.InputType, streamLayout.ArrayInputSize.Value)), context.Bound++, [.. inputValues])).ResultId;
return inputsData1;
}
var inputsData = ConvertInputsArray();
buffer.Add(new OpStore(inputsVariable, inputsData, null, []));
var entryPointTypeId = context.GetOrRegister(entryPoint.Type);
if (executionModel == ExecutionModel.TessellationControl || executionModel == ExecutionModel.TessellationEvaluation)
{
var arraySize = executionModel == ExecutionModel.TessellationControl
? streamLayout.ArrayOutputSize ?? throw new InvalidOperationException("Can't figure array output size for tessellation shader")
: streamLayout.ArrayInputSize.Value;
bool hullTessellationOutputsGenerated = false;
int GenerateHullTessellationOutputs()
{
if (hullTessellationOutputsGenerated)
throw new InvalidOperationException("Hull OutputPatch can only be used in once place (constant patch)");
hullTessellationOutputsGenerated = true;
var outputsVariable = buffer.Insert(variableInsertIndex++, new OpVariable(context.GetOrRegister(new PointerType(new ArrayType(streamLayout.OutputType, arraySize), Specification.StorageClass.Function)), context.Bound++, Specification.StorageClass.Function, null)).ResultId;
context.AddName(outputsVariable, "outputs");
for (int arrayIndex = 0; arrayIndex < arraySize; ++arrayIndex)
{
for (var outputIndex = 0; outputIndex < streamLayout.OutputStreams.Count; outputIndex++)
{
var stream = streamLayout.OutputStreams[outputIndex];
var outputsVariablePtr = buffer.Add(new OpAccessChain(context.GetOrRegister(new PointerType(stream.Info.Type, Specification.StorageClass.Function)),
context.Bound++, outputsVariable,
[context.CompileConstant(arrayIndex).Id, context.CompileConstant(outputIndex).Id])).ResultId;
var outputSourcePtr = buffer.Add(new OpAccessChain(context.GetOrRegister(new PointerType(stream.Info.Type, Specification.StorageClass.Output)),
context.Bound++, stream.Id,
[context.CompileConstant(arrayIndex).Id])).ResultId;
var outputsSourceValue = buffer.Add(new OpLoad(context.GetOrRegister(stream.Info.Type), context.Bound++, outputSourcePtr, null, [])).ResultId;
outputsSourceValue = BuiltinProcessor.ConvertInterfaceVariable(buffer, context, stream.Info.Type, stream.InterfaceType, outputsSourceValue);
buffer.Add(new OpStore(outputsVariablePtr, outputsSourceValue, null, []));View on GitHub (pinned to 96fad776d2)