stride3d/stride · error · InvalidOperationException

Cannot PopMaterial more than PushMaterial

Error message

Cannot PopMaterial more than PushMaterial

What it means

MaterialGeneratorContext maintains a stack of material descriptors via PushMaterial/PopMaterial while visiting a material hierarchy. Popping when the stack is empty means unbalanced push/pop calls, so it throws InvalidOperationException.

Solutions

  1. Audit your IMaterialDescriptor.Visit implementation so every PopMaterial call is matched 1:1 with an earlier PushMaterial
  2. Only call PopMaterial when the visitor actually pushed (e.g. guard with a bool returned by the push path)
  3. Remove PopMaterial calls from finally blocks that can run after an earlier pop
  4. Update custom material plugins to the current visitor API contract

Example fix

// before: unconditional pop
public override void Visit(MaterialGeneratorContext context)
{
    context.PushMaterial(descriptor);
    if (condition) return; // early exit skips nothing, but finally pops again later
    context.PopMaterial();
}
// after: balanced push/pop
public override void Visit(MaterialGeneratorContext context)
{
    context.PushMaterial(descriptor);
    if (condition) { context.PopMaterial(); return; }
    context.PopMaterial();
}
Defensive patterns

Strategy: validation

Validate before calling

// track push depth yourself before popping
int pushed = 0;
context.PushMaterial(descriptor); pushed++;
if (pushed > 0) context.PopMaterial(); pushed--;

Try / catch

try { context.PopMaterial(); }
catch (InvalidOperationException ex)
{
    logger.LogError(ex, "Unbalanced PopMaterial in custom descriptor {Name}", GetType().Name);
}

Prevention

When it happens

Trigger: Calling PopMaterial more times than PushMaterial during material generation — typically a bug in a custom IMaterialDescriptor.Visit implementation that pops unconditionally on every branch or in a finally block.

Common situations: Custom material descriptors/assemblies whose Visit calls PopMaterial in code paths not matched by a PushMaterial; exception thrown mid-visit skipping a PopMaterial then a finally popping again.

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/4c5e0f086fe3e48d. Report an issue: GitHub.

Appendix: source

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

                {
                    Log.Error($"The material [{materialName}] cannot be used recursively.");
                    hasErrors = true;
                }
            }

            if (!hasErrors)
            {
                materialStack.Push(materialDescriptor);
            }

            return !hasErrors;
        }

        public IMaterialDescriptor PopMaterial()
        {
            if (materialStack.Count == 0)
            {
                throw new InvalidOperationException("Cannot PopMaterial more than PushMaterial");
            }
            return materialStack.Pop();
        }

        /// <summary>
        /// Pushes a new layer with the specified blend map.
        /// </summary>
        /// <param name="blendMap">The blend map used by this layer.</param>
        public void PushLayer(IComputeScalar blendMap)
        {
            if (Step != MaterialGeneratorStep.GenerateShader)
                return;

            // We require a blend layer expect for the top level one.
            if (currentLayerContext != null && blendMap == null)
            {
                throw new ArgumentNullException(nameof(blendMap), "Blendmap parameter cannot be null for a child layer");
            }

View on GitHub (pinned to 96fad776d2)