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
- Audit your IMaterialDescriptor.Visit implementation so every PopMaterial call is matched 1:1 with an earlier PushMaterial
- Only call PopMaterial when the visitor actually pushed (e.g. guard with a bool returned by the push path)
- Remove PopMaterial calls from finally blocks that can run after an earlier pop
- 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
- Keep PushMaterial/PopMaterial in the same scope in Visit implementations
- Never pop in finally blocks after an unconditional earlier pop
- Write a visitor unit test that visits a full material tree twice to catch imbalance
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
- Cannot PopLayer when no balancing PushLayer was called
- A MaterialPass can only belong to a single Material
- [Material] Unknown node type:
- Blendmap parameter cannot be null for a child layer
- [ ] cannot be null in
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)