Skip to content

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

Open
richiemcilroy wants to merge 21 commits into
feat/gpui-area-aspect-pickerfrom
feat/recording-window-only
Open

richiemcilroy wants to merge 21 commits into
feat/gpui-area-aspect-pickerfrom
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:03am 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
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@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 medium).

View 1 other finding in Capy.

Open in Capy

impl Drop for Frame {
fn drop(&mut self) {
if let Some(pool) = self.recycle.take() {
pool.give(self.texture.clone(), self.width, self.height);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Trimmed textures are recycled while native encoders still use them

Medium Severity

For an odd-sized window-only capture, native WindowsMuxer clones frame.texture() and drops ScreenFrame inside its input callback (output_pipeline/win.rs:353–358); WindowsSegmentedMuxer does the same at win_segmented.rs:550. This new Drop implementation immediately returns that texture to TrimPool, although H264Encoder::run only copies it in video_processor.process_texture after the callback returns. A concurrent capture can therefore take and overwrite the texture before that copy, corrupting the recorded frame; WindowsMuxer's last_texture also retains a recyclable texture for repeated frames. Keep a frame/lease alive through the encoder copy and frame-pacing retention, or recycle only once all texture users release ownership.

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

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

This branch was successfully deployed

2 active deployments
Preview – cap-web — 76c89067 Deployed Oct 27, 2026 by vercel[bot]
Preview – cap-status — 76c89067 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