Skip to content

feat(recording): capture a window by itself, on request, on macOS and Windows - #3452

Merged
richiemcilroy merged 30 commits into
mainfrom
feat/recording-window-only
Oct 27, 2026
Merged

richiemcilroy merged 30 commits into
mainfrom
feat/recording-window-only

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Stacked on #3450.

This brings in #2469 by @MinitJain (macOS) and #2301 by @Stephen-Psaradellis (Windows) unchanged, merged with their history. On top of them is one commit that makes window-only capture opt-in.

Problem

A window recording today is a crop of its display, taken where the window was when recording started. Anything dragged over the window is recorded, and if the window moves the recording keeps showing the old rectangle. #2469 and #2301 fix this by capturing the window itself, through a ScreenCaptureKit desktop-independent window filter and a window GraphicsCaptureItem. Both do it for every window recording, which changes what a window recording means for everyone.

Change

  • RecordingBaseInputs::window_only and ActorBuilder::with_window_only on both the studio and instant actors. The default is false, so every existing caller records exactly as before.
  • The two PRs each added their own window field and init parameter, gated on macOS and Windows respectively. They are now one #[cfg(any(target_os = "macos", windows))] field, filled through window_only_target(target, window_only).
  • Cursor: on Windows (from fix(recording): capture the window itself on Windows, not a crop of the display #2301) and now macOS, Studio's cursor follows the window, but only for window-only captures. The crop path places it as before. On macOS the window is read with one CGWindowListCopyWindowInfo call for that window per tick (Window::from_id_direct), not a full window list. Linux keeps its existing X11 window source.
  • Window gone: if the window is gone when a macOS window-only capture starts or restarts, the capture fails with WindowGone instead of falling back to the display crop. A crop would record whatever now covers the window's old rectangle.
  • Windows trimmed frames: a frame trimmed down from its surface takes its texture from a small pool and hands it back when dropped. A frame still queued for the encoder is never overwritten by a later one.
  • Instant mode on macOS and Windows has no camera compositing: the camera bubble reaches the video only because the screen capture records it. fix(recording): capture the selected window itself on macOS #2469 already kept macOS on the display crop while a camera is attached; instant_capture_window now applies the same rule to both platforms.

One change from #2301 reaches the default path. On Windows, a cropped capture now skips a frame whose crop box no longer fits the source instead of copying out of range and vending a stale surface.

The app wiring (the Focus toggle and the window capture style setting) lands separately.

Test

RetriggerView in GreptileConfidence Score: 5/5

No new blocking finding is reported; the texture-lifetime concern belongs to an existing thread.

Summary

Adds opt-in window-only recording on macOS and Windows while keeping display-crop recording as the default.

  • A selected window can be recorded by itself on macOS and Windows.
  • Windows capture frames keep their size and contents as windows resize.
  • Cursor markers follow a moving window during recording.

Reviews (5) · Last reviewed commit: "Merge feat/gpui-area-aspect-picker into ..." · Reviewed by Greptile

MinitJain and others added 14 commits October 12, 2026 01:44
Window targets on macOS were recorded as a display capture cropped to the
window's bounds at start time, so moving the window or overlapping it with
other apps changed what was recorded (#1754).

Carry the window id into the macOS capture config and build a
desktop-independent window SCContentFilter for it, skipping the display
crop. The same filter is used when the capturer is rebuilt after a system
stop. Display, Area and Camera Only targets are unchanged.

Instant mode keeps the display crop when a camera is attached, because
macOS Instant recordings have no camera compositing and the camera preview
only appears through the display capture.

Known limitations:
- Studio custom cursor positions are still normalised against the window's
  start bounds, so the cursor overlay drifts after the window moves.
- Output size is fixed at the window's size at start; resize behaviour is
  untested.
- With system audio on, ScreenCaptureKit likely limits audio to the
  window's owning app.
- If the window is missing from shareable content (initially or on
  restart), capture falls back to the previous display crop.

Window-filter approach also explored in #2368.
The window-only SCContentFilter ignored the "Hide windows from recordings"
exclusions, so selecting an excluded window in Window mode recorded it.

When the selected window is in the exclusion list, skip the window filter
and use the display filter with exclusions, which hides it as before. The
check runs both when capture starts and when the capturer is rebuilt.
…capture item

Window targets capture the window's own GraphicsCaptureItem instead of a crop
of the display. The output is sized from that item floored to even, and odd
items are trimmed on the GPU (scap-direct3d Settings::even_dimensions), so a
window recording's frames match the encoder size and never take the CPU
scaler. The Studio cursor is placed against the window's current bounds, so it
follows a moved window. Frame pool is recreated when the item resizes.
Recreating the frame pool on a size change, and skipping frames a crop box no longer fits inside, now sit behind Settings::follow_item_size. windows.rs sets it alongside even_dimensions for window targets only, so display and area recordings behave exactly as on main.
…m test

The test resizes the window twice; without follow_item_size the frame pool keeps its first surfaces while the trim copy uses the new ContentSize.
…tSize

ContentSize is the item's size at capture time, and it runs ahead of the
frame's surface whenever the item grows before the pool is recreated. That
includes frames the old pool had already queued when Recreate ran: they
carry the new ContentSize on a surface still at the old size, so the
even-dimension trim copied a source box past the surface's edge. Hardware
drivers ignore that; WARP removes the device, and Windows.Graphics.Capture
then fails fast (0xc0000409) on its next use of it.

- skip frames whose surface does not match their ContentSize when
  following the item's size (queued before the pool was recreated)
- clamp the even-dimension trim and the crop check to the surface size
- report a removed device instead of recreating the pool on it

Tests:
- can run on WARP (CAP_SCAP_TEST_WARP=1 for all, always for the two new
  ones) and fail if the capture device was removed
- new: frames queued behind a slow consumer are not copied on recreate
- new: the trim stays inside the surface without follow_item_size
- size and place the fixture windows from the primary work area so the
  stacked-window crop fits on a 1024x768 display
…r than the frame

The plain pass-through branch labelled the frame with ContentSize but handed over the whole pool surface. On Server 2022 a window item's surface includes the invisible resize borders, so as_buffer's CopyResource into a ContentSize staging texture was a copy between different sizes, which D3D11 drops, and the readback was all zeros. Every branch now bounds the frame by both ContentSize and the surface and copies that region when it differs from the surface. The even-dimension tests now expect the frame's ContentSize instead of item.Size(), and a new test shrinks the window under a fixed pool to reach the same path on any machine.
@vercel

vercel Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cap-status Ready Ready Preview Oct 27, 2026 9:46pm UTC

@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

Comment thread crates/recording/src/sources/screen_capture/macos.rs
Comment thread crates/recording/src/studio_recording.rs
Comment thread crates/scap-direct3d/src/lib.rs Outdated
Comment thread crates/recording/src/cursor.rs Outdated
…the cursor follows the window on macOS, and trimmed D3D frames stop sharing a texture

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Capy found 1 potential issue (1 high).

Open in Capy

);
display
.raw_handle()
.as_content_filter_excluding_windows(content.retained(), left_out)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

macOS window-only capture no longer follows the window it records

High Severity

Recording a window with window-only capture on macOS and moving or resizing it mid-recording silently corrupts the output. The video crop is fixed once at setup: ScreenCaptureConfig::init (crates/recording/src/sources/screen_capture/mod.rs:516) trims the window's start-time bounds into config.crop_bounds, and the call anchored here stamps those bounds as sourceRect onto a display content filter that never tracks the window — this replaced ContentFilter::with_desktop_independent_window, which followed the window wherever it went. rebuild_capturer reuses the same stale params.config.crop_bounds, so not even a macOS-forced restart re-aims the crop. Meanwhile the cursor recorder recomputes macos_window_crop from the window's live bounds every tick (crates/recording/src/cursor.rs:795), so after the window moves the cursor is normalized against the new position while the video shows the original screen region: the cursor overlay is displaced by exactly the window's movement, and the frame shows whatever now occupies the start location. That contradicts the feature's own contract: RecordingBaseInputs.window_only and the with_window_only builders say record only that window, following it, on macOS and Windows, and instead of the region of the screen it starts in, and cursor.rs states A window recording follows the window. The same one-shot snapshot also fails the window-by-itself premise in a second way: the windows_in_front exclusion list is captured at filter-build time, so any window that arrives or drags over the target later is recorded on top of it. Fix by re-aiming sourceRect and refreshing the in-front exclusion list from the window's current bounds whenever it moves or resizes, so the video and cursor stay on the same trimmed frame. (Also: prior finding 01M5Z3YHMANHZWDB1KX01WD7P0 is resolved at this head — source_rect and its unconditional 8pt origin are deleted and both set_src_rect call sites pass crop_bounds position and size verbatim, matching the cursor trim for every window size.)

Open in Capy

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Capy found no issues.

Open in Capy

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Capy found no issues.

Open in Capy

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Capy found no issues.

Open in Capy

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Capy found no issues.

Open in Capy

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Capy found no issues.

Open in Capy

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Capy found no issues.

Open in Capy

@richiemcilroy
richiemcilroy changed the base branch from feat/gpui-area-aspect-picker to main October 27, 2026 22:44
@richiemcilroy
richiemcilroy merged commit d0aa6cb into main Oct 27, 2026
62 checks passed

This branch was successfully deployed

2 active deployments
Preview – cap-web — 7ddbc0fc Deployed Oct 27, 2026 by vercel[bot]
Preview – cap-status — 7ddbc0fc Deployed Oct 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants