stride3d/stride · error · InvalidOperationException

Cannot PopLayer when no balancing PushLayer was called

Error message

Cannot PopLayer when no balancing PushLayer was called

What it means

MaterialGeneratorContext.PushLayer/PopLayer must be balanced. PopLayer throws InvalidOperationException when there is no open layer (currentLayerContext == null), meaning a custom visitor called PopLayer without a matching PushLayer.

Solutions

  1. Make every PopLayer in your Visit matched by a preceding PushLayer on all code paths
  2. Check Step guards: pushes and pops must both run only during MaterialGeneratorStep.GenerateShader
  3. Track layer depth yourself (e.g. a bool/counter) and only pop when a push actually happened
  4. Simplify nested-layer code so each branch pushes and pops symmetrically

Example fix

// before: pop without push guard
public override void Visit(MaterialGeneratorContext context)
{
    if (context.Step == MaterialGeneratorStep.GenerateShader && enabled)
        context.PushLayer(...);
    context.PopLayer(); // throws when !enabled
}
// after: pop only when pushed
public override void Visit(MaterialGeneratorContext context)
{
    if (context.Step != MaterialGeneratorStep.GenerateShader || !enabled) return;
    context.PushLayer(...);
    context.PopLayer();
}
Defensive patterns

Strategy: validation

Validate before calling

bool layerOpen = context.CurrentLayer != null;
if (layerOpen) context.PopLayer();

Try / catch

try { context.PopLayer(); }
catch (InvalidOperationException ex)
{
    logger.LogError(ex, "Unbalanced PopLayer in {Visitor}", GetType().Name);
}

Prevention

When it happens

Trigger: Calling PopLayer when no layer is currently open — a custom IMaterialDescriptor.Visit calls PopLayer unconditionally, or pushes were skipped due to the Step != GenerateShader early-return while pops still execute.

Common situations: Custom material assemblies with unbalanced layer code; asymmetric early returns inside Visit after PushLayer but before PopLayer (or vice versa); exceptions interrupting the push/pop sequence.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at sources/engine/Stride.Rendering/Rendering/Materials/MaterialGeneratorContext.cs:236

            var newLayer = new MaterialBlendLayerContext(this, currentLayerContext, blendMap);
            if (currentLayerContext != null)
            {
                currentLayerContext.Children.Add(newLayer);
            }
            currentLayerContext = newLayer;
        }

        /// <summary>
        /// Pops the current layer.
        /// </summary>
        public void PopLayer()
        {
            if (Step != MaterialGeneratorStep.GenerateShader)
                return;

            if (currentLayerContext == null)
            {
                throw new InvalidOperationException("Cannot PopLayer when no balancing PushLayer was called");
            }

            // If we are poping the last layer, so we can process all layers
            if (currentLayerContext.Parent == null)
            {
                ProcessLayer(currentLayerContext, true);
            }
            else
            {
                currentLayerContext = currentLayerContext.Parent;
            }
        }

        public void AddShaderSource(MaterialShaderStage stage, ShaderSource shaderSource)
        {
            EnsureStep(MaterialGeneratorStep.GenerateShader);
            if (shaderSource == null) throw new ArgumentNullException(nameof(shaderSource));
            currentLayerContext.GetContextPerStage(stage).ShaderSources.Add(shaderSource);

View on GitHub (pinned to 96fad776d2)