Skip to content

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

Open
KamilDev wants to merge 3 commits into
CoplayDev:betafrom
KamilDev:fix/composited-screenshot-wait-for-end-of-frame
Open

fix(screenshot): wait for end-of-frame before composited capture#1132
KamilDev wants to merge 3 commits into
CoplayDev:betafrom
KamilDev:fix/composited-screenshot-wait-for-end-of-frame

Conversation

@KamilDev
Copy link
Copy Markdown

@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
    • Improved screenshot capture in Editor Play mode so UI overlays are included and blank captures are prevented.
    • Editor edit-mode and non-editor builds keep their prior capture behavior; existing camera-based fallback and optional downscaling remain unchanged.

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5eb795eb-675e-4f59-9880-1ff36161e77e

📥 Commits

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

📒 Files selected for processing (1)
  • MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs

📝 Walkthrough

Walkthrough

Adds an editor-only deferred capture helper that waits until end-of-frame to call ScreenCapture.CaptureScreenshotAsTexture and updates CaptureComposited to use this helper during Play Mode in the Unity Editor so UI Toolkit overlays are included.

Changes

Editor composited-frame screenshot capture

Layer / File(s) Summary
Editor capture infrastructure and hidden MonoBehaviour
MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs
Editor-only static pending-state and CompositedFrameCapturer MonoBehaviour schedule ScreenCapture.CaptureScreenshotAsTexture after WaitForEndOfFrame. CaptureCompositedAfterFrame creates the capturer, spins the editor (EditorApplication.Step) until completion or timeout, then returns the captured Texture2D and clears shared state.
CaptureComposited editor play mode routing
MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs
CaptureComposited now invokes CaptureCompositedAfterFrame(result.SuperSize) when running in the Unity Editor and Application.isPlaying is true; edit-mode editor and non-editor builds keep the direct ScreenCapture.CaptureScreenshotAsTexture call.

Possibly related PRs

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I waited till the frame said "done",
Hid a MonoBehaviour, silent as sun,
End-of-frame secrets captured true,
UI overlays now show through,
A tiny hop for a perfect view.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 accurately summarizes the main change: adding a wait for end-of-frame before composited capture to fix the screenshot issue.
Description check ✅ Passed The description covers all template sections with detailed context: bug description, type of change, specific code changes, testing methodology, and justification for documentation decisions.
Linked Issues check ✅ Passed The PR directly addresses issue #1039 by implementing composited frame capture via WaitForEndOfFrame in Play Mode, ensuring UI Toolkit overlays are included in inline screenshot images.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the composited screenshot capture path in Play Mode. Edit-mode overlay capture and other unrelated functionality are explicitly noted as out-of-scope and unchanged.

✏️ 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.
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

1 participant