Skip to content

perf(desktop-gpui): convert the camera preview at bubble size, newest frame only, while shown - #2806

Draft
richiemcilroy wants to merge 5 commits into
perf/camera-preview-convert-off-mainfrom
perf/gpui-camera-preview-downscale
Draft

richiemcilroy wants to merge 5 commits into
perf/camera-preview-convert-off-mainfrom
perf/gpui-camera-preview-downscale

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Stacked on #2761 (perf/camera-preview-convert-off-main), so this diff shows only the work on top of it.

What

Before this change, every camera frame was converted at the camera's full size, even though the bubble shows it at about 460 px tall by default. At 4K that is 33 MB written per frame (99 MB when mirrored) and a 127 to 222 MB IOSurface ring. Queued frames were converted even after newer ones had arrived. Frames were also converted while the bubble was covered, the display was asleep, or the window was ordered out.

Change

  1. Convert to the bubble's size (all platforms). CameraWindow::preview_target takes the bubble's size (the larger of the requested window size and the current viewport, minus the toolbar) and multiplies it by the scale factor. It rounds each axis up to a fixed step: 270, 360, 480, 540, 720, 960, 1080, 1440, 1920 or 2160. cap_gpui_kernels::camera_preview::cover_dimensions then picks the smallest size that has the source's aspect ratio and still covers that step.
    • It never upscales. Both sides are even, because swscale is 2 to 4x slower on odd output widths (measured below).
    • It keeps the full size when the smaller frame would still have more than half the source's pixels. 1080p to 1706x960 measured slower than no scaling in both swscale and VideoToolbox.
    • Above 2160 px on either axis (a large full-shape bubble on Retina) the frame stays at full size, as it does when the target overlays draw the preview inline.
    • The window still lays itself out using the camera's own size (Converted::layout_dims, and CameraPreviewFrame::dims on Windows and Linux). The rounding never moves the window's aspect, so the Linux recording snapshot check sees exactly what it sees today.
    • Cover fit, crop, radius and mirroring are unchanged. The painted frame is just smaller.
    • Blur keeps its existing 720p (macOS) and 640x360 (Windows and Linux) fit, with the same scaler.
    • On Linux, frames go out at full size while a processed recording lease holds the worker, because those frames are recorded.
    • Resizing the bubble rebuilds the ring only when a step boundary is crossed. A frame converted for the previous step is dropped through the existing settings check.
  2. Convert only the newest frame (macOS).
    • The preview pump drains its bounded(4) channel and delivers only the newest frame. That releases stale AVFoundation buffers instead of holding up to four.
    • queue_conversion evicts a frame still waiting in the conversion thread's single-slot queue, so the thread always picks up the newest one. Before, the old frame was kept and the new one was dropped.
    • The bridge keeps a clone of the receiver only when the thread spawned. Otherwise the inline fallback still sees Disconnected.
    • The Windows and Linux worker already drained to the newest frame.
  3. Skip conversion while the bubble is not shown (macOS). platform::window_is_presenting checks occlusionState for NSWindowOcclusionStateVisible. That is the same test gpui's display link (and the frame pacer patch's FrameRequests::Once) starts and stops on, with the macOS 26 shim applied, so a window that fails it draws nothing anyway.
    • Blur jobs are skipped too.
    • The first frame after the window becomes visible again converts as usual, so the preview resumes within one camera frame interval.
    • The check is bypassed while the preview is drawn inline by the target overlays.
    • The encoder's camera feed is a separate sender and is not touched.

Hona-authored lines (snapshot_bgra, the copy_rows call in the preview image builder) are unchanged.

Measurements

Synthetic frames on CI (no cameras there), from scratch branch ci/camera-preview-downscale-ab (not for merge). Bubbles: small = 230 pt round (default), medium = 400 pt round (large preset), large = 600 pt full shape. All at 2x scale. "main" is the conversion main and #2761 run every frame with blur off (full size). "branch" is this PR.

macOS (Blacksmith macOS 26.3, Apple M5 Max virtual machine)

Source: cargo run --example camera-preview-convert-benchmark -- --bubble. The benchmark runs the same VideoToolbox transfer, plus the rotation pass when mirrored, that FrameConverter runs.

  • p50 is the wall time of the synchronous transfer.
  • Process CPU is getrusage user plus system time per frame.
  • Ring memory uses main's 4 slots, or the 6 slots of perf(desktop-gpui): convert camera preview frames off the main thread #2761 and this PR, plus one scratch buffer when mirrored.
  • PSNR compares two things against an area average of the full-size conversion: gpui's linear sampling of each output at the displayed cover size. Higher is closer to an ideal downscale.
source mirrored bubble branch output p50 us main / branch process CPU us main / branch MB written per frame main / branch ring MB main (4 slots) / #2761 (6) / branch (6) PSNR dB main / branch
1280x720 no small 230pt round 854x480 483.3 / 553.0 122.3 / 104.3 3.52 / 1.58 14.1 / 21.1 / 9.5 26.02 / 26.24
1280x720 no medium 400pt round 1280x720 483.3 / 336.9 122.3 / 73.7 3.52 / 3.52 14.1 / 21.1 / 21.1 inf / inf
1280x720 no large 600pt full 1280x720 483.3 / 431.1 122.3 / 104.2 3.52 / 3.52 14.1 / 21.1 / 21.1 inf / inf
1280x720 yes small 230pt round 854x480 852.9 / 1036.2 183.7 / 215.6 7.03 / 3.16 17.6 / 24.6 / 11.1 26.02 / 26.24
1280x720 yes medium 400pt round 1280x720 852.9 / 978.0 183.7 / 254.5 7.03 / 7.03 17.6 / 24.6 / 24.6 inf / inf
1280x720 yes large 600pt full 1280x720 852.9 / 982.5 183.7 / 250.3 7.03 / 7.03 17.6 / 24.6 / 24.6 inf / inf
1920x1080 no small 230pt round 854x480 442.4 / 789.4 101.0 / 124.9 7.91 / 1.58 31.6 / 47.5 / 9.5 26.89 / 27.21
1920x1080 no medium 400pt round 1920x1080 442.4 / 540.9 101.0 / 124.2 7.91 / 7.91 31.6 / 47.5 / 47.5 inf / inf
1920x1080 no large 600pt full 1920x1080 442.4 / 521.4 101.0 / 115.5 7.91 / 7.91 31.6 / 47.5 / 47.5 inf / inf
1920x1080 yes small 230pt round 854x480 1062.0 / 1433.3 266.9 / 327.6 15.82 / 3.16 39.6 / 55.4 / 11.1 26.89 / 27.21
1920x1080 yes medium 400pt round 1920x1080 1062.0 / 982.4 266.9 / 239.5 15.82 / 15.82 39.6 / 55.4 / 55.4 inf / inf
1920x1080 yes large 600pt full 1920x1080 1062.0 / 960.7 266.9 / 219.5 15.82 / 15.82 39.6 / 55.4 / 55.4 inf / inf
3840x2160 no small 230pt round 854x480 672.9 / 1851.6 126.8 / 170.7 31.64 / 1.58 126.6 / 189.8 / 9.5 26.24 / 28.00
3840x2160 no medium 400pt round 1706x960 672.9 / 1921.2 126.8 / 151.0 31.64 / 6.27 126.6 / 189.8 / 37.6 27.52 / 28.29
3840x2160 no large 600pt full 2560x1440 672.9 / 2209.1 126.8 / 220.9 31.64 / 14.06 126.6 / 189.8 / 84.4 27.54 / 27.91
3840x2160 yes small 230pt round 854x480 1540.1 / 2519.8 333.4 / 361.0 63.28 / 3.16 158.2 / 221.5 / 11.1 26.24 / 28.00
3840x2160 yes medium 400pt round 1706x960 1540.1 / 2796.2 333.4 / 433.1 63.28 / 12.54 158.2 / 221.5 / 43.9 27.52 / 28.29
3840x2160 yes large 600pt full 2560x1440 1540.1 / 3020.2 333.4 / 437.2 63.28 / 28.12 158.2 / 221.5 / 98.4 27.54 / 27.91

Reading it:

  • Memory and bandwidth fall 5 to 20x. A 4K camera's ring drops from 127 MB (main) and 190 MB (perf(desktop-gpui): convert camera preview frames off the main thread #2761) to 9.5 MB with the default bubble, and each frame writes 1.6 MB instead of 31.6 MB.
  • The bubble-size image matches the full-size sampling or comes slightly closer to an ideal downscale, on every row.
  • On this virtual machine, though, a VideoToolbox transfer that scales costs more than one that does not. The per-frame wait goes up by about 0.1 to 0.4 ms at 720p and 1080p, and by about 1.2 to 1.5 ms at 4K. CPU goes up by about 20 to 100 us at 4K.
  • Since perf(desktop-gpui): convert camera preview frames off the main thread #2761, this wait happens on the camera-preview-convert thread, not the main thread. It stays far below the 33 ms frame interval.
  • Whether real hardware behaves like this virtual machine is the open question for an on-device check (see below).

Windows (Blacksmith Windows 2025)

Source: cargo test --bin cap-gpui camera_preview_scale_benchmark -- --ignored --nocapture. It times the real camera_preview_image_at (swscale, row copy, RenderImage) at full size against the bubble target. MB per frame is the RenderImage, which is what the main thread uploads to the atlas each frame.

format source mirrored bubble branch output p50 ms main / branch MB per frame main / branch
NV12 1280x720 no small 230pt round 854x480 1.23 / 0.76 3.52 / 1.56
NV12 1280x720 no medium 400pt round 1280x720 1.23 / 1.26 3.52 / 3.52
NV12 1280x720 no large 600pt full 1280x720 1.23 / 1.29 3.52 / 3.52
NV12 1280x720 yes small 230pt round 854x480 1.73 / 1.04 3.52 / 1.56
NV12 1280x720 yes medium 400pt round 1280x720 1.73 / 1.72 3.52 / 3.52
NV12 1280x720 yes large 600pt full 1280x720 1.73 / 1.70 3.52 / 3.52
NV12 1920x1080 no small 230pt round 854x480 3.64 / 1.28 7.91 / 1.56
NV12 1920x1080 no medium 400pt round 1920x1080 3.64 / 3.79 7.91 / 7.91
NV12 1920x1080 no large 600pt full 1920x1080 3.64 / 3.83 7.91 / 7.91
NV12 1920x1080 yes small 230pt round 854x480 4.76 / 1.67 7.91 / 1.56
NV12 1920x1080 yes medium 400pt round 1920x1080 4.76 / 4.75 7.91 / 7.91
NV12 1920x1080 yes large 600pt full 1920x1080 4.76 / 4.78 7.91 / 7.91
NV12 3840x2160 no small 230pt round 854x480 14.47 / 3.16 31.64 / 1.56
NV12 3840x2160 no medium 400pt round 1706x960 14.47 / 6.42 31.64 / 6.25
NV12 3840x2160 no large 600pt full 2560x1440 14.47 / 8.83 31.64 / 14.06
NV12 3840x2160 yes small 230pt round 854x480 19.02 / 3.43 31.64 / 1.56
NV12 3840x2160 yes medium 400pt round 1706x960 19.02 / 7.14 31.64 / 6.25
NV12 3840x2160 yes large 600pt full 2560x1440 19.02 / 10.41 31.64 / 14.06
YUYV422 1280x720 no small 230pt round 854x480 1.45 / 0.95 3.52 / 1.56
YUYV422 1280x720 no medium 400pt round 1280x720 1.45 / 1.39 3.52 / 3.52
YUYV422 1280x720 no large 600pt full 1280x720 1.45 / 1.43 3.52 / 3.52
YUYV422 1280x720 yes small 230pt round 854x480 1.91 / 1.21 3.52 / 1.56
YUYV422 1280x720 yes medium 400pt round 1280x720 1.91 / 1.88 3.52 / 3.52
YUYV422 1280x720 yes large 600pt full 1280x720 1.91 / 1.87 3.52 / 3.52
YUYV422 1920x1080 no small 230pt round 854x480 4.19 / 1.76 7.91 / 1.56
YUYV422 1920x1080 no medium 400pt round 1920x1080 4.19 / 4.18 7.91 / 7.91
YUYV422 1920x1080 no large 600pt full 1920x1080 4.19 / 4.30 7.91 / 7.91
YUYV422 1920x1080 yes small 230pt round 854x480 5.07 / 2.04 7.91 / 1.56
YUYV422 1920x1080 yes medium 400pt round 1920x1080 5.07 / 5.18 7.91 / 7.91
YUYV422 1920x1080 yes large 600pt full 1920x1080 5.07 / 5.35 7.91 / 7.91
YUYV422 3840x2160 no small 230pt round 854x480 16.21 / 4.20 31.64 / 1.56
YUYV422 3840x2160 no medium 400pt round 1706x960 16.21 / 7.82 31.64 / 6.25
YUYV422 3840x2160 no large 600pt full 2560x1440 16.21 / 10.47 31.64 / 14.06
YUYV422 3840x2160 yes small 230pt round 854x480 19.79 / 4.45 31.64 / 1.56
YUYV422 3840x2160 yes medium 400pt round 1706x960 19.79 / 8.66 31.64 / 6.25
YUYV422 3840x2160 yes large 600pt full 2560x1440 19.79 / 11.75 31.64 / 14.06

Linux (Blacksmith Ubuntu 24.04)

format source mirrored bubble branch output p50 ms main / branch MB per frame main / branch
NV12 1280x720 no small 230pt round 854x480 0.88 / 0.63 3.52 / 1.56
NV12 1280x720 no medium 400pt round 1280x720 0.88 / 0.88 3.52 / 3.52
NV12 1280x720 no large 600pt full 1280x720 0.88 / 0.88 3.52 / 3.52
NV12 1280x720 yes small 230pt round 854x480 1.50 / 1.00 3.52 / 1.56
NV12 1280x720 yes medium 400pt round 1280x720 1.50 / 1.49 3.52 / 3.52
NV12 1280x720 yes large 600pt full 1280x720 1.50 / 1.49 3.52 / 3.52
NV12 1920x1080 no small 230pt round 854x480 2.18 / 1.22 7.91 / 1.56
NV12 1920x1080 no medium 400pt round 1920x1080 2.18 / 2.25 7.91 / 7.91
NV12 1920x1080 no large 600pt full 1920x1080 2.18 / 2.21 7.91 / 7.91
NV12 1920x1080 yes small 230pt round 854x480 3.49 / 1.53 7.91 / 1.56
NV12 1920x1080 yes medium 400pt round 1920x1080 3.49 / 3.50 7.91 / 7.91
NV12 1920x1080 yes large 600pt full 1920x1080 3.49 / 3.46 7.91 / 7.91
NV12 3840x2160 no small 230pt round 854x480 16.04 / 3.39 31.64 / 1.56
NV12 3840x2160 no medium 400pt round 1706x960 16.04 / 5.58 31.64 / 6.25
NV12 3840x2160 no large 600pt full 2560x1440 16.04 / 6.85 31.64 / 14.06
NV12 3840x2160 yes small 230pt round 854x480 20.02 / 3.68 31.64 / 1.56
NV12 3840x2160 yes medium 400pt round 1706x960 20.02 / 6.54 31.64 / 6.25
NV12 3840x2160 yes large 600pt full 2560x1440 20.02 / 8.88 31.64 / 14.06
YUYV422 1280x720 no small 230pt round 854x480 1.00 / 0.78 3.52 / 1.56
YUYV422 1280x720 no medium 400pt round 1280x720 1.00 / 0.99 3.52 / 3.52
YUYV422 1280x720 no large 600pt full 1280x720 1.00 / 0.99 3.52 / 3.52
YUYV422 1280x720 yes small 230pt round 854x480 1.62 / 1.12 3.52 / 1.56
YUYV422 1280x720 yes medium 400pt round 1280x720 1.62 / 1.62 3.52 / 3.52
YUYV422 1280x720 yes large 600pt full 1280x720 1.62 / 1.61 3.52 / 3.52
YUYV422 1920x1080 no small 230pt round 854x480 2.53 / 1.58 7.91 / 1.56
YUYV422 1920x1080 no medium 400pt round 1920x1080 2.53 / 2.58 7.91 / 7.91
YUYV422 1920x1080 no large 600pt full 1920x1080 2.53 / 2.63 7.91 / 7.91
YUYV422 1920x1080 yes small 230pt round 854x480 3.78 / 1.90 7.91 / 1.56
YUYV422 1920x1080 yes medium 400pt round 1920x1080 3.78 / 3.78 7.91 / 7.91
YUYV422 1920x1080 yes large 600pt full 1920x1080 3.78 / 3.75 7.91 / 7.91
YUYV422 3840x2160 no small 230pt round 854x480 17.32 / 4.11 31.64 / 1.56
YUYV422 3840x2160 no medium 400pt round 1706x960 17.32 / 6.70 31.64 / 6.25
YUYV422 3840x2160 no large 600pt full 2560x1440 17.32 / 7.97 31.64 / 14.06
YUYV422 3840x2160 yes small 230pt round 854x480 21.93 / 4.36 31.64 / 1.56
YUYV422 3840x2160 yes medium 400pt round 1706x960 21.93 / 7.52 31.64 / 6.25
YUYV422 3840x2160 yes large 600pt full 2560x1440 21.93 / 9.84 31.64 / 14.06

Windows and Linux are faster or equal on every row. For the default bubble they are 2 to 5x faster, and the main-thread atlas upload shrinks 2.3x at 720p, 5x at 1080p and 20x at 4K. Where the branch column is up to 0.3 ms higher, both sides run the identical full-size conversion. That difference is run-to-run noise.

cap-recording's camera-preview-benchmark needs a physical camera, which CI does not have. The ignored test above replaces it, using synthetic NV12 and YUYV frames.

Not in this PR

  • Windows and Linux do not skip conversion while the bubble is hidden. Linux already stops the worker through set_camera_preview_rendering during clean capture, and Windows has no equivalent hook yet.
  • The swscale output frame is still allocated per frame, copy_rows still zero-fills its Vec, and the atlas still re-uploads each frame on Windows and Linux. They are now about 5x smaller at 1080p. A follow-up can address them.

Needs an on-device check

  • Visual A/B of the bubble at 230, 400 and 600 pt (round, square, full), mirrored and not, on a Retina display and a 1x display, with a 1080p and ideally a 4K camera. It should look the same as main or slightly smoother.
  • Resize the bubble across the 480/540/720/960 px steps. Expect no flicker beyond the single dropped frame at each crossing, and no window aspect jumps.
  • Cover the bubble with another window, lock the screen, then uncover it. The camera preview cadence log should show painted fps dropping to 0 and resuming at once.
  • The macOS conversion time on real hardware: cargo run -p cap-desktop-gpui --example camera-preview-convert-benchmark -- --bubble. If scaled VideoToolbox transfers also measure slower there, this PR keeps the memory win and costs a little off-main time.

@vercel

vercel Bot commented Oct 21, 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 21, 2026 5:45am UTC

@blacksmith-sh

blacksmith-sh Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Found 2 test failures on Blacksmith runners:

Failures

Test View Logs
crash/recorder-kill/inproc/static-screen/static-screen View Logs
crash/recorder-kill/oop/static-screen/static-screen View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@richiemcilroy

Copy link
Copy Markdown
Member Author

@greptileai review

This branch was successfully deployed

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