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
- Make every PopLayer in your Visit matched by a preceding PushLayer on all code paths
- Check Step guards: pushes and pops must both run only during MaterialGeneratorStep.GenerateShader
- Track layer depth yourself (e.g. a bool/counter) and only pop when a push actually happened
- 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
- Mirror every PushLayer with exactly one PopLayer in the same Visit branch
- Apply the same Step == GenerateShader guard to pushes and pops
- Add a debug assertion/logging of layer depth in custom descriptors
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
- Cannot PopMaterial more than PushMaterial
- 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/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)