Skip to content

feat(screenshot): Windows.Graphics.Capture path — transparent/GPU-composited windows capture correctly#48

Merged
runyourempire merged 2 commits into
mainfrom
feat/wgc-capture
Jul 4, 2026
Merged

feat(screenshot): Windows.Graphics.Capture path — transparent/GPU-composited windows capture correctly#48
runyourempire merged 2 commits into
mainfrom
feat/wgc-capture

Conversation

@runyourempire

Copy link
Copy Markdown
Collaborator

What

Windows screenshots now try Windows.Graphics.Capture (WGC) first, falling back to the existing GDI (PrintWindow/BitBlt) path. WGC reads the DWM-composited surface, so transparent (WS_EX_LAYERED) and GPU-composited windows — which GDI copies as blank and which previously errored loudly — now capture with full content. This is the fix behind the 4DA notification-window blank-capture class, and the prerequisite for any future offscreen-webview capture.

How

  • capture_window_raw (Windows) dispatches: WGC → blank-frame check → GDI fallback; the GDI loud blank-frame error is preserved and now annotated with the WGC failure reason, so a window neither engine can see still fails clearly instead of returning silent wrong pixels.
  • WGC path: CoInitializeEx (MTA) → IsSupported gate → D3D11 device (hardware → WARP fallback for RDP/headless) → IGraphicsCaptureItemInterop::CreateForWindowfree-threaded Direct3D11CaptureFramePool (no DispatcherQueue; we're on a tokio blocking thread) → bounded 2s first-frame wait (minimized windows time out instead of hanging) → CPU staging texture → RowPitch-aware BGRA→RGBA with un-premultiply (same math as the macOS path) and a client-area crop so output dimensions stay identical to the GDI path (PW_CLIENTONLY) for filmstrip/trace/scrub consumers.
  • Best-effort SetIsBorderRequired(false) / SetIsCursorCaptureEnabled(false) — ignored on older builds (a yellow border is acceptable; a failed capture is not).
  • Public signatures unchanged; macOS/Linux untouched; no new crates (feature additions to the existing windows dep).

Verification

  • 6 new pure unit tests (BGRA swap, RowPitch padding, crop + clamping, undersized buffer, un-premultiply incl. div-by-zero) + 1 #[ignore]d live test (wgc_captures_live_foreground_window).
  • Live end-to-end on a real desktop session: demo-app built from this branch, driven over REST — screenshot returned a 900×600 PNG with 25 distinct colors / luma variance 1454 from a window carrying WS_EX_LAYERED (exstyle 0xC0110), WGC path confirmed taken.
  • Gates: build, clippy --all-targets/--no-default-features/--release all -D warnings, fmt, cargo test -p victauri-plugin (878 passed). Independently re-run by a second reviewer session.

Known limitations (documented in code)

  • Yellow capture border on Windows 10 < ~21H1 (setter unavailable → ignored).
  • Per-capture D3D11 device + session (~tens of ms); a cached session is a possible follow-up for trace.
  • Minimized windows: WGC times out (2s) → GDI fallback; hidden windows remain blocked upstream by the visibility guard.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CsePmfHDMBKRNDLNb3TLD5

…posited windows capture correctly

Windows capture now tries WGC (DXGI) first: D3D11 device (hardware, WARP
fallback) -> GraphicsCaptureItem from the HWND via IGraphicsCaptureItemInterop
-> free-threaded Direct3D11CaptureFramePool (we run on a tokio blocking thread,
no DispatcherQueue) -> one frame within a bounded 2s wait -> CPU staging
texture -> mapped BGRA rows (RowPitch-aware) -> straight RGBA, un-premultiplied
and cropped to the client area so output dimensions match the legacy GDI path.

WGC reads the DWM-composited surface, so WS_EX_LAYERED / transparent Tauri
windows — which PrintWindow/BitBlt copy as uniform blank and which previously
failed loudly — now return real pixels. GDI remains the fallback (older
Windows builds without WGC, minimized windows where DWM stops composing,
capture-session failures) and keeps its loud blank-frame error, now annotated
with why the WGC attempt failed. Border/cursor suppression is best-effort
(SetIsBorderRequired/SetIsCursorCaptureEnabled can fail on older builds and
are ignored). Public entry points (capture_window / capture_window_raw) keep
their signatures and client-area semantics for the filmstrip and trace
consumers.

Tests: 6 pure unit tests for the BGRA->RGBA conversion (RowPitch padding,
client-area crop + clamping, undersized-buffer rejection, un-premultiply,
zero-area/out-of-frame errors) plus an #[ignore]d live-desktop WGC test
(foreground window; WGC rejects shell/desktop windows by design).

Verified live on Windows 10: the ignored test passes, and a demo-app built
from this branch served POST /api/tools/screenshot -> 900x600 PNG with real
pixel variance via the WGC path for a window carrying WS_EX_LAYERED
(exstyle 0xC0110).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsePmfHDMBKRNDLNb3TLD5
@runyourempire runyourempire merged commit 1b7b1a6 into main Jul 4, 2026
21 checks passed
@runyourempire runyourempire deleted the feat/wgc-capture branch July 4, 2026 06:17
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.

1 participant