Skip to content

fix(screenshot): wait for end-of-frame before composited capture#1132

Merged
Scriptwonder merged 9 commits into
CoplayDev:betafrom
KamilDev:fix/composited-screenshot-wait-for-end-of-frame
May 26, 2026
Merged

fix(screenshot): wait for end-of-frame before composited capture#1132
Scriptwonder merged 9 commits into
CoplayDev:betafrom
KamilDev:fix/composited-screenshot-wait-for-end-of-frame

Conversation

@KamilDev
Copy link
Copy Markdown
Contributor

@KamilDev KamilDev commented May 17, 2026

Description

manage_camera action=screenshot capture_source=game_view include_image=true (no explicit camera, play mode) returned fully-transparent PNGs (every pixel RGBA (0, 0, 0, 0)). The path was supposed to capture UI Toolkit overlays — #1040 wired it to ScreenCapture.CaptureScreenshotAsTexture for that reason — but called the API inline, before the next frame was rendered, so the captured backbuffer was unwritten and the result was always blank.

This change routes the call through a WaitForEndOfFrame coroutine and advances the player loop with EditorApplication.Step() so the captured texture actually contains the composited frame. Single-call API is preserved.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Changes Made

MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs:

  • Added CompositedFrameCapturer MonoBehaviour (nested, #if UNITY_EDITOR): yields WaitForEndOfFrame, then calls ScreenCapture.CaptureScreenshotAsTexture(SuperSize) and stores the result in static state.
  • Added CaptureCompositedAfterFrame(int superSize, int timeoutSteps = 5): spawns the capturer GameObject, advances the player loop with EditorApplication.Step() up to timeoutSteps times until the coroutine completes, returns the texture (or null, which falls through to existing camera fallback).
  • Routed CaptureComposited's editor + play-mode path through CaptureCompositedAfterFrame. Edit-mode editor path and non-editor builds retain the previous direct synchronous call.

The public API (ScreenshotUtility.CaptureComposited signature, manage_camera screenshot tool surface) is unchanged.

Testing/Screenshots/Recordings

Tested locally against beta HEAD (a2a5edf) with Unity 6000.3.14f1, URP, Windows 11.

Repro:

  1. Scene with a UIDocument using a PanelSettings asset.
  2. PanelSettings.clearColor = true, colorClearValue = magenta (unambiguous signal for overlay capture).
  3. Enter play mode.
  4. manage_camera action=screenshot capture_source=game_view include_image=true (no camera).
Path Before After
manage_camera screenshot (no camera, include_image=true, play mode) fully-transparent PNG composited frame including UITK overlay ✅
manage_camera screenshot camera="Main Camera" (explicit camera) camera render only, no overlay (by design) unchanged ✅
manage_ui render_ui (uses its own coroutine path) works unchanged ✅

No automated test added. The fix's correctness depends on play-mode end-of-frame behavior with a live UIDocument — out of scope for the existing edit-mode test fixtures in TestProjects/UnityMCPTests/Assets/Tests/EditMode/. Happy to add one if reviewers can point me at the right play-mode test harness pattern in this repo.

Documentation Updates

  • I have added/removed/modified tools or resources

This change fixes the behavior of manage_camera action=screenshot's composited path but does not change its parameters, response schema, or add/remove any tool or resource. No documentation updates needed.

Related Issues

Fixes #1039 (the underlying behavior described in the issue persisted after #1040 — the API choice in #1040 was correct, but the synchronous invocation produced empty captures).

Additional Notes

  • This complements fix: include UI Toolkit overlays in game_view screenshots with include_image #1040 rather than reverting any of it. fix: include UI Toolkit overlays in game_view screenshots with include_image #1040's diff is the right API call (ScreenCapture.CaptureScreenshotAsTexture instead of camera render), and that line is preserved — just moved inside the coroutine.
  • ManageUI.cs already contains an equivalent MCP_ScreenCapturer MonoBehaviour used by render_ui's two-call pending/ready protocol. A future refactor could share CompositedFrameCapturer between both call sites and offer a single-call shape for render_ui too. Left out to keep this PR focused.
  • CaptureCompositedAfterFrame resets stale in-flight state on entry rather than refusing, so a previous failed capture does not poison subsequent attempts.
  • EditorApplication.Step() is play-mode-only; this is fine because the fixed code path is already gated by Application.isPlaying.
  • Edit-mode overlay capture is intentionally not addressed. ScreenCapture.CaptureScreenshotAsTexture() returns null when called from a script in edit mode (confirmed empirically), and EditorApplication.Step() is a no-op outside play mode — so neither the API nor the spin primitive used by this fix is usable there. Edit-mode include_image=true continues to fall through to the existing camera-based path, which excludes UI Toolkit overlays by design. A real edit-mode overlay capture would need a different mechanism (camera RT + PanelSettings.targetTexture readback composited manually) — out of scope for this PR.

Summary by CodeRabbit

  • Bug Fixes

    • Editor Play-mode screenshots now reliably include UI overlays and avoid blank captures; previous editor edit-mode and non-editor capture behaviors and camera fallback remain unchanged.
  • Refactor

    • Play-mode screenshot flow streamlined: editor captures are coordinated to produce composited end-of-frame images more reliably and synchronously.

Review Change Stack

ScreenshotUtility.CaptureComposited called ScreenCapture.CaptureScreenshotAsTexture
inline, before the next frame had been rendered and presented. UI Toolkit overlays
are composited at end-of-frame, so the captured texture contained an unwritten
backbuffer and the saved PNG was blank.

Route the play-mode editor path through a transient MonoBehaviour that yields
WaitForEndOfFrame before calling ScreenCapture, then advance the player loop with
EditorApplication.Step() until the coroutine completes. The single-call API is
preserved; total cost is bounded (5 steps, ~80ms at 60fps).

Complements CoplayDev#1040, which switched to the correct API but kept the synchronous
invocation that caused the empty capture.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an editor-only after-frame capture helper that schedules CaptureScreenshotAsTexture at end-of-frame via a transient MonoBehaviour, and updates CaptureComposited and ManageUI to use it in Editor Play Mode so UI Toolkit overlays are included in screenshots.

Changes

Editor composited-frame screenshot capture

Layer / File(s) Summary
Editor capture infrastructure and hidden MonoBehaviour
MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs
Adds an editor-only synchronous helper that starts a hidden ScreenshotCapturer which yields WaitForEndOfFrame, calls ScreenCapture.CaptureScreenshotAsTexture(superSize), invokes a completion callback with the Texture2D (or null on error), and self-destructs; the helper spins the editor loop (EditorApplication.Step) until completion or timeout and returns the captured Texture2D.
CaptureComposited editor play mode routing & ManageUI integration
MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs, MCPForUnity/Editor/Tools/ManageUI.cs
CaptureComposited now uses the after-frame helper (CaptureCompositedAfterFrame) when in the Unity Editor and Application.isPlaying; ManageUI replaces its internal MCP_ScreenCapturer usage with ScreenshotCapturer.Begin(...) and a callback that assigns s_pendingCaptureTex, s_pendingCaptureDone, and resets s_pendingCaptureStarted.

Sequence Diagram(s)

sequenceDiagram
  participant ManageUI
  participant CaptureComposited
  participant CaptureCompositedAfterFrame
  participant ScreenshotCapturer
  participant WaitForEndOfFrame
  participant ScreenCapture
  participant EditorApplication

  ManageUI->>CaptureComposited: request composited screenshot (Play Mode)
  CaptureComposited->>CaptureCompositedAfterFrame: call with superSize
  CaptureCompositedAfterFrame->>ScreenshotCapturer: Begin(superSize, onComplete)
  ScreenshotCapturer->>WaitForEndOfFrame: coroutine yields end of frame
  WaitForEndOfFrame->>ScreenCapture: CaptureScreenshotAsTexture(superSize)
  ScreenCapture->>ScreenshotCapturer: return Texture2D or fail
  loop editor spin
    EditorApplication->>EditorApplication: Step() until onComplete invoked
  end
  CaptureCompositedAfterFrame->>CaptureComposited: return Texture2D
  CaptureComposited->>ManageUI: return/compress image and set pending fields
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hid a tiny capturer to wait,
Till frames finish dressing up the state,
End-of-frame pixels now sing true,
UI overlays peek into view,
A quiet hop — the screenshot’s great.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(screenshot): wait for end-of-frame before composited capture' clearly and concisely describes the main change: adding end-of-frame waiting to the composited screenshot capture mechanism.
Description check ✅ Passed The description is comprehensive and detailed, including problem statement, solution explanation, changes made, testing/repro steps, and all required sections per the template.
Linked Issues check ✅ Passed The PR fully addresses issue #1039 by implementing composited frame capture after end-of-frame via a coroutine in Play Mode, ensuring UI Toolkit overlays are included in inline screenshots.
Out of Scope Changes check ✅ Passed All changes are scoped to the composited screenshot capture flow in Play Mode. The refactoring of ManageUI.cs to use the new ScreenshotCapturer is a direct consequence of the primary fix and is in scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs (1)

261-266: 💤 Low value

Set s_pendingCompositedDone = true on exception to avoid unnecessary spinning.

When an exception occurs, the coroutine is effectively complete (the GameObject is destroyed at line 268), but s_pendingCompositedDone = false causes CaptureCompositedAfterFrame to continue spinning EditorApplication.Step() until timeout. The null texture already signals failure.

Suggested fix
                 catch (Exception ex)
                 {
                     Debug.LogError($"[MCP for Unity] CaptureScreenshotAsTexture failed: {ex.Message}");
                     s_pendingCompositedTex = null;
-                    s_pendingCompositedDone = false;
+                    s_pendingCompositedDone = true;
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs` around lines 261 - 266, The
catch block in CaptureScreenshotAsTexture leaves s_pendingCompositedDone = false
which causes CaptureCompositedAfterFrame to keep spinning; update the catch to
set s_pendingCompositedDone = true (while keeping
s_pendingCompositedCompositedTex = null and logging) so the coroutine loop in
CaptureCompositedAfterFrame will exit promptly when an exception occurs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs`:
- Around line 274-281: Reset the pending-composite state unconditionally at the
start of the capture routine to avoid returning a stale texture: set
s_pendingCompositedTex = null, s_pendingCompositedDone = false, and
s_pendingCompositedStarted = false immediately on entry to the ScreenshotUtility
capture method (the method that contains the existing s_pendingCompositedStarted
check), rather than only when s_pendingCompositedStarted is true, so any
background coroutine completion after a timeout cannot cause a stale
s_pendingCompositedTex to be returned.

---

Nitpick comments:
In `@MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs`:
- Around line 261-266: The catch block in CaptureScreenshotAsTexture leaves
s_pendingCompositedDone = false which causes CaptureCompositedAfterFrame to keep
spinning; update the catch to set s_pendingCompositedDone = true (while keeping
s_pendingCompositedCompositedTex = null and logging) so the coroutine loop in
CaptureCompositedAfterFrame will exit promptly when an exception occurs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bfeedb41-7689-49a0-93fb-c14e8e48959a

📥 Commits

Reviewing files that changed from the base of the PR and between b98193d and fb2c12f.

📒 Files selected for processing (1)
  • MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs

Comment thread MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs Outdated
KamilDev added 2 commits May 18, 2026 00:28
…ale state

Two robustness tweaks to CompositedFrameCapturer / CaptureCompositedAfterFrame
flagged by CodeRabbit on the parent commit:

1. On exception inside the WaitForEndOfFrame coroutine, mark
   s_pendingCompositedDone = true so CaptureCompositedAfterFrame's spin loop
   exits immediately. The null texture already signals failure to the caller,
   which falls through to the camera-based fallback. Previously the loop would
   spin for the full timeout despite the coroutine being effectively done.

2. Reset s_pendingCompositedTex / s_pendingCompositedDone unconditionally on
   entry. A coroutine from a previous call that timed out can complete
   asynchronously and leave a stale texture / done flag behind; clearing on
   entry prevents the next call from picking up that stale capture.
Removing the if (s_pendingCompositedStarted) guard in the previous commit
left the field with only assignments and no reads, which Unity surfaces as
CS0414 ("assigned but its value is never used"). Drop the field and its
three write sites.

No behavior change.
@Scriptwonder
Copy link
Copy Markdown
Collaborator

Hi thanks for the PR! I think its a little over-engineered and this is actually duplication of ManageUI.cs at line 819, so it would be great if you reuse the wheel or call into that wheel. Trim the comments also, keep it under 1-2 lines.

…nd CaptureComposited

Per review feedback: ManageUI.MCP_ScreenCapturer and the new
CompositedFrameCapturer added in this PR were near-duplicates. Extract a single
public ScreenshotCapturer MonoBehaviour to Runtime/Helpers that takes a callback,
and have both call sites use it:

- ManageUI.render_ui (two-call pending/ready protocol): callback writes the
  texture and flips its own static done/started flags.
- ScreenshotUtility.CaptureCompositedAfterFrame (single-call sync spin):
  callback writes a local; spin loop pumps EditorApplication.Step until set.

Trimmed verbose comments in the same pass.

No behaviour change. Both paths re-verified end-to-end with a magenta UITK
panel-clear repro.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs`:
- Around line 242-251: The CaptureCompositedAfterFrame method can return before
the ScreenshotCapturer.Begin callback runs, leaking the Texture2D the callback
creates; update the callback to detect whether the capture was still wanted and
destroy late textures. Concretely, introduce a local bool flag (e.g., accepted
or captured) captured by the callback and set it to true only when you assign
result and before returning; inside the Begin callback, if the flag shows the
caller already timed out, immediately dispose the incoming Texture2D (using
UnityEngine.Object.DestroyImmediate or Destroy appropriately) instead of
assigning it; keep references to ScreenshotCapturer.Begin,
CaptureCompositedAfterFrame, timeoutSteps and the EditorApplication.Step loop
when making this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: acfb4308-4880-4000-bae0-a0db70b78f52

📥 Commits

Reviewing files that changed from the base of the PR and between 090d1b4 and a697d09.

📒 Files selected for processing (2)
  • MCPForUnity/Editor/Tools/ManageUI.cs
  • MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs

Comment thread MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs
KamilDev added 2 commits May 24, 2026 21:07
Drop the "Shared by ManageUI.render_ui ... and ScreenshotUtility's editor
synchronous-spin path" line. The first sentence already describes the class on
its own; naming specific callers in the doc is brittle (goes stale if a call
site is added or renamed) and pushed the summary to 4 content lines, over the
1-2 line guidance.
…callback

If the editor spin loop times out before ScreenshotCapturer's coroutine fires
the callback, the callback would still later assign the captured Texture2D to
the (now-dead) local result variable. The texture itself would never reach the
consumer's finally-block DestroyTexture, leaking a Unity object until the next
domain reload.

Track caller-returned state; if the callback runs after timeout, destroy the
incoming texture immediately instead of assigning it.

Flagged by CodeRabbit on the prior commit.
@KamilDev
Copy link
Copy Markdown
Contributor Author

Done in a697d09. Extracted a shared ScreenshotCapturer MonoBehaviour to Runtime/Helpers/ScreenshotUtility.cs and rewired both call sites (ManageUI.render_ui's two-call protocol and CaptureComposited's sync-spin path). Net -44 lines across the two files. Trimmed comments in the same pass (0c277af). Re-tested both paths end-to-end against the magenta UITK overlay repro — no behavior change.

Follow-up nit from CodeRabbit on the refactor (late-callback texture leak after timeout) addressed in 48fbad4.

…nch to ternary

The 4-line inline comment in CaptureComposited duplicated CaptureCompositedAfterFrame's
own header and exceeded the 1-2 line guidance. Trim to 2 lines and collapse the
play-mode / edit-mode branch to a ternary (matches the file's existing style).
@Scriptwonder
Copy link
Copy Markdown
Collaborator

Hi, can you keep it updated to the beta so we could run the CI so it would not fail on other Unity versions

@KamilDev
Copy link
Copy Markdown
Contributor Author

KamilDev commented May 26, 2026

Merged latest beta into the branch (2a7ec426). Clean merge — none of the 90+ upstream commits touched ScreenshotUtility.cs or ManageUI.cs, so the diff vs current beta is still just the two files (+71 / -37). CI should re-run against the latest matrix.


Update (6e4302db): Spoke too soon. Between that comment and now, beta moved further forward and 4805bc12 landed a compat fix that did touch ScreenshotUtility.cs — it switched CaptureComposited to a reflective MethodInfo dispatch so the file compiles when the Screen Capture module is disabled. Re-merged with a real conflict resolution: extracted a single shared InvokeCaptureScreenshotAsTexture(int) helper on ScreenshotUtility that does the reflective dispatch, and routed both the existing call site and the new ScreenshotCapturer.Start() through it. Now consistent with the upstream compat fix. Verified locally — compiles clean, magenta UITK overlay still captured in play mode via manage_camera screenshot.

…enshot-wait-for-end-of-frame

# Conflicts:
#	MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs
@Scriptwonder Scriptwonder merged commit a5e207e into CoplayDev:beta May 26, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

game_view screenshots with include_image miss UI Toolkit overlays

2 participants