stride3d/stride · error · InvalidOperationException

Cannot pop more than push

Error message

Cannot pop more than push

What it means

RenderDrawContext keeps a stack of render-target states: PushRenderTargets saves a state and PopRenderTargets restores one. Popping without a matching push (currentStateIndex < 0) leaves nothing to restore, so the engine throws InvalidOperationException to keep the push/pop pairs balanced.

Solutions

  1. Ensure every PopRenderTargets call is paired with exactly one prior PushRenderTargets call in the same scope
  2. Move push/pop into try/finally only around code that cannot throw before the push
  3. Audit custom render features/pass ordering so a feature never pops a push owned by another feature

Example fix

// before
commandList.SetRenderTarget(...);
context.PopRenderTargets(); // never pushed
// after
context.PushRenderTargets();
commandList.SetRenderTarget(...);
context.PopRenderTargets(); // balanced pair
Defensive patterns

Strategy: validation

Validate before calling

if (context.currentStateIndex < 0)
    throw new InvalidOperationException("PopRenderTargets called without matching PushRenderTargets");

Try / catch

try { context.PopRenderTargets(); }
catch (InvalidOperationException ex) when (ex.Message == "Cannot pop more than push") {
    logger.Error("Unbalanced Push/PopRenderTargets in render feature");
}

Prevention

When it happens

Trigger: Calling PopRenderTargets more times than PushRenderTargets was called during a render frame, or calling Pop before any Push in the current frame context.

Common situations: Unbalanced error/early-exit paths in custom render features or renderers that push but skip the pop (or pop in a finally when push failed); calling pop from outside a RenderFeature's Execute scope.

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/1f62fef98170f01c. Report an issue: GitHub.

Appendix: source

Thrown at sources/engine/Stride.Rendering/Rendering/RenderDrawContext.cs:98

                allocatedStates.Add(newState);
            }
            else
            {
                newState = allocatedStates[currentStateIndex];
            }
            newState.Capture(CommandList);

            return new RenderTargetRestore(this);
        }

        /// <summary>
        /// Restores render targets and viewport state.
        /// </summary>
        public void PopRenderTargets()
        {
            if (currentStateIndex < 0)
            {
                throw new InvalidOperationException("Cannot pop more than push");
            }

            var oldState = allocatedStates[currentStateIndex--];
            oldState.Restore(CommandList);
        }

        /// <summary>
        /// Gets or creates a shared effect.
        /// </summary>
        /// <typeparam name="T">Type of the shared effect (mush have a constructor taking a <see cref="Rendering.RenderContext"/></typeparam>
        /// <returns>A singleton instance of <typeparamref name="T"/></returns>
        public T GetSharedEffect<T>() where T : DrawEffect, new()
        {
            // TODO: Add a way to support custom constructor
            lock (sharedEffects)
            {
                DrawEffect effect;
                if (!sharedEffects.TryGetValue(typeof(T), out effect))

View on GitHub (pinned to 96fad776d2)