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

  1. Restructure the hull shader entry point so the output patch / HS_OUTPUT is referenced by exactly one out parameter.
  2. Merge multiple output parameters into a single structure parameter so only one emission point is needed.
  3. If this comes from generated/processed signatures, adjust the pre-processing so hull outputs are coalesced before GenerateWrapper runs.
  4. 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

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


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)