[Bug]: Play-mode screenshot re-enters the PlayerLoop (EditorApplication.Step from ExecuteTasks) - unbounded Editor.log and Editor crash
Summary
manage_camera(action="screenshot") taken while the Editor is in play mode drives the PlayerLoop from a context that is already inside it. Unity reports the re-entrancy, and from that point the Editor emits an internal assertion continuously. In our case it wrote 20.4 GB to Editor.log and the Editor crashed unattended overnight.
Still present on main (v10.1.0, c14de1e6dc01) — verified 2026-07-28.
Cause
ScreenshotUtility.CaptureCompositedAfterFrame waits for the capture by stepping the Editor:
// Runtime/Helpers/ScreenshotUtility.cs:194 (main)
for (int i = 0; i < timeoutSteps && !done; i++)
{
UnityEditor.EditorApplication.Step();
}EditorApplication.Step() drives the PlayerLoop. But this is reached from TransportCommandDispatcher.RequestMainThreadPump → UnitySynchronizationContext.ExecuteTasks, which Unity already executes from inside the PlayerLoop. So the loop is driven from within itself.
Unity's message, with the package stack (verbatim from Editor.log):
An abnormal situation has occurred: the PlayerLoop internal function has been called recursively.
UnityEngine.StackTraceUtility:ExtractStackTrace ()
MCPForUnity.Runtime.Helpers.ScreenshotUtility:CaptureCompositedAfterFrame (int,int) (at .../ScreenshotUtility.cs:191)
MCPForUnity.Runtime.Helpers.ScreenshotUtility:CaptureComposited (string,int,bool,bool,int,string) (at .../ScreenshotUtility.cs:221)
MCPForUnity.Editor.Tools.ManageScene:CaptureScreenshot (...) (at .../ManageScene.cs:618)
MCPForUnity.Editor.Tools.Cameras.ManageCamera:HandleCommand (...) (at .../ManageCamera.cs:74)
MCPForUnity.Editor.Tools.CommandRegistry:ExecuteCommand (...) (at .../CommandRegistry.cs:305)
MCPForUnity.Editor.Services.Transport.TransportCommandDispatcher:ProcessCommand (...) (at .../TransportCommandDispatcher.cs:366)
MCPForUnity.Editor.Services.Transport.TransportCommandDispatcher:ProcessQueue () (at .../TransportCommandDispatcher.cs:262)
MCPForUnity.Editor.Services.Transport.TransportCommandDispatcher:<RequestMainThreadPump>g__Pump|11_0 () (at .../TransportCommandDispatcher.cs:184)
UnityEngine.UnitySynchronizationContext/WorkRequest:Invoke ()
UnityEngine.UnitySynchronizationContext:Exec ()
UnityEngine.UnitySynchronizationContext:ExecuteTasks ()Note the last three frames: the call arrives through Unity's own main-thread pump, which runs inside the PlayerLoop.
What follows
After the re-entrant step, the log fills with one line and never stops:
Access version should be odd when acquiring lockMeasured in the crash-time log:
| Flood begins at byte offset | ~1,364,796 |
| Total log size | 20,444,189,922 bytes (20.4 GB) |
| Share of the log that is this one line | >99.99% |
Sampled at 25%, 50% and 75% of the file — every window is that line back to back. Disk space was not the trigger; 1,227 GB remained free.
It accumulates over a session — a leaked capturer per timed-out capture
Symptomatically this bites hardest in long-running sessions; the scrolling message has appeared several times and restarting the Editor clears it. There is a concrete mechanism.
ScreenshotCapturer.Begin spawns a hidden GameObject that is destroyed only after WaitForEndOfFrame resumes:
private System.Collections.IEnumerator Start()
{
yield return new WaitForEndOfFrame(); // never resumes if no frame is rendered
...
_onComplete?.Invoke(tex);
Destroy(gameObject); // so this is never reached
}WaitForEndOfFrame does not fire when the frame is not rendered — paused play mode, unfocused Editor, minimized player. The __MCP_ScreenshotCapturer__ object is then never destroyed and its coroutine stays suspended for the life of the session, while the synchronous caller has already given up after timeoutSteps.
The two defects compound, and #1230 supplies the link:
Step()pauses play mode (per #1230's own description).- On any version without #1230, that pause is never restored.
- A paused Editor renders no frames, so every subsequent capture's
WaitForEndOfFramecan never fire — each one times out and leaks its capturer.
So the first screenshot of a session can put the Editor into a state where every later screenshot is guaranteed to leak.
Suggested diagnostic: take several play-mode screenshots, then count objects named __MCP_ScreenshotCapturer__. They should be zero.
Secondary symptom: captures silently return the wrong thing
The for loop exits on timeoutSteps whether or not done was set, so callers can receive a null or half-composited texture with no error. Three identical calls in one session returned a correct 640x419 image, then a blank 512x512, then a 448x448 frame with all Screen Space - Overlay UI missing.
Steps to reproduce
- Open a project and enter play mode. (Set
Application.runInBackground = trueif the Editor will be unfocused; otherwise frames don't tick.) - Call
manage_camera(action="screenshot", include_image=true). - Observe
An abnormal situation has occurred: the PlayerLoop internal function has been called recursively. - Exit play mode, leave the Editor idle.
Access version should be odd when acquiring lockrepeats without limit;Editor.loggrows without bound.
Environment
- MCP for Unity
10.0.0(4ee62b097584), installed fromgit?path=/MCPForUnity#main - Verified still present on
main=c14de1e6dc01(v10.1.0) - Unity 6000.4.3f1 (39d1a88d4dd1)
- Windows 11 Pro 10.0.26200
Prior work in this area
- #1132 (2026-05-26) introduced
CaptureCompositedAfterFrameand theStep()pump. - #1230 (2026-07-06) fixed a side effect of the pump —
Step()pausing play mode.
Both left the re-entrancy in place.
Suggested fixes
- Don't pump the Editor from a context already inside the PlayerLoop. If the request arrives during
ExecuteTasks, defer the capture to a later editor tick instead of callingEditorApplication.Step(). - Better, make the play-mode path fully async: return the pending MCP command's result from the
WaitForEndOfFramecallback rather than blocking the dispatcher untildone. - Give
ScreenshotCapturera timeout/lifetime so it destroys itself if the end-of-frame never arrives. - At minimum, treat
!doneaftertimeoutStepsas an error returned to the caller, rather than returning a partial texture as success.
A crash.dmp (3 MB) and log extracts can be supplied if useful.
Source: CoplayDev/unity-mcp