stride3d/stride · error · InvalidOperationException

Can't figure array output size for tessellation shader

Error message

Can't figure array output size for tessellation shader

What it means

For tessellation shaders, the SPIR-V entry-point wrapper needs the output (TessellationControl) or input (TessellationEvaluation) array size to declare arrayed function/Output variables. The stream layout's ArrayOutputSize (hull shader patch output count) was not provided, so the generator cannot compute the array length and throws InvalidOperationException.

Solutions

  1. Set streamLayout.ArrayOutputSize explicitly (the patch constant count, e.g. [outputcontrolpoints(N)] equivalent) for the tessellation-control entry point.
  2. Ensure the hull shader declares a constant OutputPatch/array size in the source so the layout generator can populate ArrayOutputSize.
  3. Verify the layout construction code that builds streamLayout for tessellation pipelines and populate ArrayInputSize/ArrayOutputSize there.
  4. Clear shader caches / recompile shaders after fixing the declaration, so stale layouts without the size are not reused.

Example fix

// before (programmatic layout)
var streamLayout = new StreamLayout { OutputType = outputStruct }; // ArrayOutputSize left null

// after
var streamLayout = new StreamLayout { OutputType = outputStruct, ArrayOutputSize = 4 }; // matches outputcontrolpoints(4)
Defensive patterns

Strategy: validation

Validate before calling

// Before invoking GenerateWrapper for tessellation-control stages
if (executionModel == ExecutionModel.TessellationControl && streamLayout.ArrayOutputSize is null)
    throw new InvalidOperationException("Hull shader layout requires ArrayOutputSize (patch output control point count)");
if (executionModel == ExecutionModel.TessellationEvaluation && streamLayout.ArrayInputSize is null)
    throw new InvalidOperationException("Domain shader layout requires ArrayInputSize");

Type guard

static bool HasTessellationArraySizes(ExecutionModel model, StreamLayout l) =>
    model == ExecutionModel.TessellationControl ? l.ArrayOutputSize.HasValue
    : model == ExecutionModel.TessellationEvaluation ? l.ArrayInputSize.HasValue
    : true;

Try / catch

try
{
    var wrapper = EntryPointWrapperGenerator.GenerateWrapper(...);
}
catch (InvalidOperationException ex) when (ex.Message == "Can't figure array output size for tessellation shader")
{
    log.Error("Stream layout missing tessellation array size; regenerate layout with explicit patch counts");
    throw new ShaderCompilationException("Tessellation layout lacks ArrayOutputSize/ArrayInputSize", ex);
}

Prevention

When it happens

Trigger: GenerateWrapper runs with executionModel == TessellationControl and streamLayout.ArrayOutputSize is null — i.e. the interface/streams layout for a hull shader was generated without an explicit patch output count.

Common situations: Hull shaders whose output patch count is not inferable from the SDSL/HLSL source (e.g. declared via custom streams or OutputPatch without a constant size); pipeline definitions built programmatically where ArrayOutputSize was left unset; older shader caches produced before array sizes were tracked.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


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

Appendix: source

Thrown at sources/shaders/Stride.Shaders.Parsers/Spirv/Processing/Interfaces/Generation/EntryPointWrapperGenerator.cs:160

                        inputFieldValues[inputIndex] = BuiltinProcessor.ConvertInterfaceVariable(buffer, context, stream.InterfaceType, stream.Info.Type, inputFieldValues[inputIndex]);
                    }

                    inputValues[arrayIndex] = buffer.Add(new OpCompositeConstruct(context.GetOrRegister(streamLayout.InputType), context.Bound++, [.. inputFieldValues])).ResultId;
                }

                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;

View on GitHub (pinned to 96fad776d2)