{"record":{"id":"cf8887661053a1c7","repo":"stride3d/stride","slug":"hull-outputpatch-can-only-be-used-in-once-place-constant","errorCode":null,"errorMessage":"Hull OutputPatch can only be used in once place (constant patch)","messagePattern":"Hull OutputPatch can only be used in once place \\(constant patch\\)","errorType":"validation","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Generation/EntryPointWrapperGenerator.cs","lineNumber":166,"sourceCode":"                var inputsData1 = buffer.Add(new OpCompositeConstruct(context.GetOrRegister(new ArrayType(streamLayout.InputType, streamLayout.ArrayInputSize.Value)), context.Bound++, [.. inputValues])).ResultId;\n                return inputsData1;\n            }\n\n            var inputsData = ConvertInputsArray();\n\n            buffer.Add(new OpStore(inputsVariable, inputsData, null, []));\n\n            var entryPointTypeId = context.GetOrRegister(entryPoint.Type);\n            if (executionModel == ExecutionModel.TessellationControl || executionModel == ExecutionModel.TessellationEvaluation)\n            {\n                var arraySize = executionModel == ExecutionModel.TessellationControl\n                    ? streamLayout.ArrayOutputSize ?? throw new InvalidOperationException(\"Can't figure array output size for tessellation shader\")\n                    : streamLayout.ArrayInputSize.Value;\n                bool hullTessellationOutputsGenerated = false;\n                int GenerateHullTessellationOutputs()\n                {\n                    if (hullTessellationOutputsGenerated)\n                        throw new InvalidOperationException(\"Hull OutputPatch can only be used in once place (constant patch)\");\n                    hullTessellationOutputsGenerated = true;\n                    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;\n                    context.AddName(outputsVariable, \"outputs\");\n\n                    for (int arrayIndex = 0; arrayIndex < arraySize; ++arrayIndex)\n                    {\n                        for (var outputIndex = 0; outputIndex < streamLayout.OutputStreams.Count; outputIndex++)\n                        {\n                            var stream = streamLayout.OutputStreams[outputIndex];\n                            var outputsVariablePtr = buffer.Add(new OpAccessChain(context.GetOrRegister(new PointerType(stream.Info.Type, Specification.StorageClass.Function)),\n                                context.Bound++, outputsVariable,\n                                [context.CompileConstant(arrayIndex).Id, context.CompileConstant(outputIndex).Id])).ResultId;\n                            var outputSourcePtr = buffer.Add(new OpAccessChain(context.GetOrRegister(new PointerType(stream.Info.Type, Specification.StorageClass.Output)),\n                                context.Bound++, stream.Id,\n                                [context.CompileConstant(arrayIndex).Id])).ResultId;\n                            var outputsSourceValue = buffer.Add(new OpLoad(context.GetOrRegister(stream.Info.Type), context.Bound++, outputSourcePtr, null, [])).ResultId;\n                            outputsSourceValue = BuiltinProcessor.ConvertInterfaceVariable(buffer, context, stream.Info.Type, stream.InterfaceType, outputsSourceValue);\n                            buffer.Add(new OpStore(outputsVariablePtr, outputsSourceValue, null, []));","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/stride3d/stride/blob/96fad776d210c221682aac1ccdf4c79dc046fc38/sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Generation/EntryPointWrapperGenerator.cs#L148-L184","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nvoid HSMain(InputPatch<VS_OUTPUT, 3> input, out HS_OUTPUT output, out HS_PATCH patch) // two output-patch usage points\n\n// after\nvoid HSMain(InputPatch<VS_OUTPUT, 3> input, out HS_RESULT result) // single out containing both payload and patch constants","handlingStrategy":"validation","validationCode":"// Hull entry points must have at most one out parameter that consumes the output patch\nint patchUsages = entryPoint.Parameters.Count(p => p.IsOut && IsOutputPatchKind(p.TypeKind));\nbool ok = executionModel != ExecutionModel.TessellationControl || patchUsages <= 1;","typeGuard":"static bool HullHasSingleOutputPatchUsage(EntryPointSignature sig) =>\n    sig.OutParameters.Count(p => p.Kind == StreamsKindSDSL.Output || p.Kind == StreamsKindSDSL.Constants) <= 2\n    && sig.OutParameters.Count(p => p.Kind == StreamsKindSDSL.Output) <= 1;","tryCatchPattern":"try\n{\n    var wrapper = EntryPointWrapperGenerator.GenerateWrapper(...);\n}\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"OutputPatch can only be used in once place\"))\n{\n    log.Error(\"Hull shader references output patch from multiple out parameters; restructure signature\");\n    throw new ShaderCompilationException(\"Multiple output-patch usage points in hull entry point\", ex);\n}","preventionTips":["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."],"tags":["spirv","tessellation","shader-compilation","invariant-violation"],"backgroundTag":"internal-invariant-violation","analyzedSha":"96fad776d210c221682aac1ccdf4c79dc046fc38","analyzedAt":"2026-09-14T02:59:31.279Z","contentChangedAt":"2026-09-14T02:59:31.279Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}