{"record":{"id":"4c5e0f086fe3e48d","repo":"stride3d/stride","slug":"cannot-popmaterial-more-than-pushmaterial","errorCode":null,"errorMessage":"Cannot PopMaterial more than PushMaterial","messagePattern":"Cannot PopMaterial more than PushMaterial","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"sources/engine/Stride.Rendering/Rendering/Materials/MaterialGeneratorContext.cs","lineNumber":198,"sourceCode":"                {\n                    Log.Error($\"The material [{materialName}] cannot be used recursively.\");\n                    hasErrors = true;\n                }\n            }\n\n            if (!hasErrors)\n            {\n                materialStack.Push(materialDescriptor);\n            }\n\n            return !hasErrors;\n        }\n\n        public IMaterialDescriptor PopMaterial()\n        {\n            if (materialStack.Count == 0)\n            {\n                throw new InvalidOperationException(\"Cannot PopMaterial more than PushMaterial\");\n            }\n            return materialStack.Pop();\n        }\n\n        /// <summary>\n        /// Pushes a new layer with the specified blend map.\n        /// </summary>\n        /// <param name=\"blendMap\">The blend map used by this layer.</param>\n        public void PushLayer(IComputeScalar blendMap)\n        {\n            if (Step != MaterialGeneratorStep.GenerateShader)\n                return;\n\n            // We require a blend layer expect for the top level one.\n            if (currentLayerContext != null && blendMap == null)\n            {\n                throw new ArgumentNullException(nameof(blendMap), \"Blendmap parameter cannot be null for a child layer\");\n            }","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/stride3d/stride/blob/96fad776d210c221682aac1ccdf4c79dc046fc38/sources/engine/Stride.Rendering/Rendering/Materials/MaterialGeneratorContext.cs#L180-L216","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: unconditional pop\npublic override void Visit(MaterialGeneratorContext context)\n{\n    context.PushMaterial(descriptor);\n    if (condition) return; // early exit skips nothing, but finally pops again later\n    context.PopMaterial();\n}\n// after: balanced push/pop\npublic override void Visit(MaterialGeneratorContext context)\n{\n    context.PushMaterial(descriptor);\n    if (condition) { context.PopMaterial(); return; }\n    context.PopMaterial();\n}","handlingStrategy":"validation","validationCode":"// track push depth yourself before popping\nint pushed = 0;\ncontext.PushMaterial(descriptor); pushed++;\nif (pushed > 0) context.PopMaterial(); pushed--;","typeGuard":null,"tryCatchPattern":"try { context.PopMaterial(); }\ncatch (InvalidOperationException ex)\n{\n    logger.LogError(ex, \"Unbalanced PopMaterial in custom descriptor {Name}\", GetType().Name);\n}","preventionTips":["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"],"tags":["material","generator","stack-imbalance","csharp"],"backgroundTag":"invalid-state-transition","analyzedSha":"96fad776d210c221682aac1ccdf4c79dc046fc38","analyzedAt":"2026-09-14T02:59:31.279Z","contentChangedAt":"2026-09-14T02:59:31.279Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}