Skip to content

perf(recording): reduce Windows CPU copies and fix studio fps cap - #3581

Open
AatmanAJ wants to merge 8 commits into
CapSoftware:mainfrom
AatmanAJ:perf/win-recording-cpu-path
Open

AatmanAJ wants to merge 8 commits into
CapSoftware:mainfrom
AatmanAJ:perf/win-recording-cpu-path

Conversation

@AatmanAJ

@AatmanAJ AatmanAJ commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Use the configured screen capture frame rate for non-fragmented Windows studio muxing instead of a hardcoded 30 fps cap.
  • Reuse NV12 CPU readback buffers in Media Foundation decode, deepen the D3D11 capture frame pool, and always downscale Win/Linux camera preview to bubble-sized dims.
  • Drop per-frame UYVY→YUYV conversion on Windows camera upload/FFmpeg paths when the encoder already accepts UYVY.

Test plan

  • Windows studio recording (non-fragmented): verify output fps matches capture setting (e.g. 60 Hz display).
  • Windows camera bubble preview: confirm preview stays sharp enough and CPU use drops vs full-res scale.
  • UYVY webcam studio/instant path: colors correct without extra conversion.
  • cargo check -p cap-recording -p cap-video-decode (Windows CI).

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; all previous findings are addressed and no new blocking issue was found.

Summary

This PR reduces copying in Windows recording paths and removes the fixed 30 fps limit from regular Studio output.

  • Windows studio recordings use the configured screen frame rate.
  • Windows camera encoders keep UYVY frames in their original format.
  • Windows screen capture keeps more frames in its Direct3D pool.
  • Media Foundation builds NV12 readback data directly from mapped bytes.
  • The Linux desktop harness starts and focuses Cap with current Hyprland commands.
  • The Linux export harness counts GIF frames by parsing the file structure.
  • The low-resolution codec check uses a higher bitrate.

Reviews (5) · Last reviewed commit: "fix(ci): walk GIF blocks when measuring ..." · Reviewed by Greptile

Use capture fps for non-fragmented studio muxing, reuse NV12 readback buffers, downscale camera preview on Win/Linux, deepen D3D capture pools, and feed UYVY camera frames through without swizzle copies.
@vercel

vercel Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Someone is attempting to deploy a commit to the Cap Software Inc Team on Vercel.

A member of the Team first needs to authorize it.

Comment on lines +1749 to +1750
} else {
original_data

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Camera colors are corrupted

upload_mf_buffer_to_texture now uploads raw UYVY422 bytes, but NativeCameraFrame::dxgi_format() still labels them DXGI_FORMAT_YUY2. These formats put brightness and color bytes in different positions. A Windows recording using a UYVY webcam and the Media Foundation encoder will save corrupted brightness and colors. Keep the byte conversion for this texture path, even when the FFmpeg path accepts UYVY directly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/output_pipeline/win.rs
Line: 1749-1750

Comment:
**Camera colors are corrupted**

`upload_mf_buffer_to_texture` now uploads raw `UYVY422` bytes, but `NativeCameraFrame::dxgi_format()` still labels them `DXGI_FORMAT_YUY2`. These formats put brightness and color bytes in different positions. A Windows recording using a UYVY webcam and the Media Foundation encoder will save corrupted brightness and colors. Keep the byte conversion for this texture path, even when the FFmpeg path accepts UYVY directly.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

let stride = ffmpeg_frame.stride(0);
copy_plane(
final_data,
data,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Padded camera rows are corrupted

camera_frame_to_ffmpeg now copies the original UYVY buffer, but its UYVY branch still uses width * 2 as the source stride. That was correct only after the removed conversion packed the rows. If a webcam adds padding between rows, each row after the first starts at the wrong byte, corrupting the recorded image. Use layout.primary_stride for the source stride and size check.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/output_pipeline/win.rs
Line: 1371

Comment:
**Padded camera rows are corrupted**

`camera_frame_to_ffmpeg` now copies the original UYVY buffer, but its UYVY branch still uses `width * 2` as the source stride. That was correct only after the removed conversion packed the rows. If a webcam adds padding between rows, each row after the first starts at the wrong byte, corrupting the recorded image. Use `layout.primary_stride` for the source stride and size check.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.


let mut ffmpeg_frame = ffmpeg::frame::Video::new(final_format, frame.width, frame.height);
let flip = frame.is_bottom_up && frame.pixel_format != cap_camera_windows::PixelFormat::UYVY422;
let mut ffmpeg_frame = ffmpeg::frame::Video::new(ffmpeg_format, frame.width, frame.height);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Camera frames fail conversion

camera_frame_to_ffmpeg now returns UYVY422 frames, but WindowsFragmentedM4SCameraMuxer::normalize_video_config_for_frame still configures UYVY cameras as YUYV422. The encoder builds its scaler from that configured format. The AVX2 converter hides the mismatch for same-size frames, but resizing or running without AVX2 falls through to a scaler that expects the wrong input, causing conversion to fail. Update the fragmented camera configuration to match the returned frame format.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/output_pipeline/win.rs
Line: 1299

Comment:
**Camera frames fail conversion**

`camera_frame_to_ffmpeg` now returns `UYVY422` frames, but `WindowsFragmentedM4SCameraMuxer::normalize_video_config_for_frame` still configures UYVY cameras as `YUYV422`. The encoder builds its scaler from that configured format. The AVX2 converter hides the mismatch for same-size frames, but resizing or running without AVX2 falls through to a scaler that expects the wrong input, causing conversion to fail. Update the fragmented camera configuration to match the returned frame format.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +780 to +783
let mut data = self
.nv12_byte_pool
.pop()
.unwrap_or_else(|| Vec::with_capacity(copy_len));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Decoder buffers are not reused

nv12_byte_pool starts empty and is never replenished. Its only uses are construction and pop(), while each resulting buffer leaves through Arc::new(data). Every frame therefore still allocates a fresh byte buffer. Add a return path that waits until consumers release the buffer, or remove the unused pool.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/video-decode/src/media_foundation.rs
Line: 780-783

Comment:
**Decoder buffers are not reused**

`nv12_byte_pool` starts empty and is never replenished. Its only uses are construction and `pop()`, while each resulting buffer leaves through `Arc::new(data)`. Every frame therefore still allocates a fresh byte buffer. Add a return path that waits until consumers release the buffer, or remove the unused pool.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread apps/desktop-gpui/src/feeds.rs Outdated
.fail("Requested camera blur is unavailable".to_string());
}
let max_dims = blur_mode.map(|_| crate::camera_blur_portable::MAX_DIMS);
let max_dims = Some(crate::camera_blur_portable::MAX_DIMS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Recorded camera image loses detail

This cap also reduces the camera image saved in Linux Instant recordings, not just the preview. The worker publishes the same capped image through recording.publish. A 1080p webcam now supplies at most 640×360 even with blur off. If the recording uses a larger camera overlay, it must enlarge that reduced image and loses detail. Keep the preview cap separate from recording, or size recording frames for the actual overlay.

Knowledge Base Used: GPUI desktop client

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/feeds.rs
Line: 565

Comment:
**Recorded camera image loses detail**

This cap also reduces the camera image saved in Linux Instant recordings, not just the preview. The worker publishes the same capped image through `recording.publish`. A 1080p webcam now supplies at most 640×360 even with blur off. If the recording uses a larger camera overlay, it must enlarge that reduced image and loses detail. Keep the preview cap separate from recording, or size recording frames for the actual overlay.

**Knowledge Base Used:** [GPUI desktop client](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/gpui-desktop-client.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Aatman Jain added 2 commits October 29, 2026 23:53
Re-swizzle UYVY to YUY2 for Media Foundation texture uploads while keeping FFmpeg on native UYVY with padded stride handling.
Cap preview downscale to blur bubble only, match fragmented camera muxer
to UYVY422 frames, and drop the non-functional NV12 byte pool.
@AatmanAJ

Copy link
Copy Markdown
Contributor Author

@greptile-apps please re-review. Addressed the open P1/P2 items in the latest push on this branch.

Aatman Jain added 2 commits October 30, 2026 00:19
clippy::uninit_vec (deny) rejects with_capacity + set_len in the Media
Foundation NV12 readback, which failed the Windows clippy job in
cap-video-decode before cap-recording was reached. Copy the mapped bytes
with to_vec (allocation plus memcpy, no zero fill) and take map_staging
back to &self now that the byte pool is gone.
@AatmanAJ
AatmanAJ force-pushed the perf/win-recording-cpu-path branch from 31f68df to 7e6b3c3 Compare October 30, 2026 00:25
@AatmanAJ

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

Aatman Jain added 2 commits October 30, 2026 10:28
…rness

Hyprland 0.57 dropped hyprland.conf and only reads hyprland.lua, so the
harness's monitor mode, session export and waybar were ignored and the
output came up at vkms's preferred 1024x768. The screenshot scenario then
reported screenshot_size=1024x768 instead of yes. Write the same settings
as Lua and use the Lua form of the focus dispatcher.
…set bitrate

At 80 kbps the paravirtualised VideoToolbox HEVC encoder on the CI macOS
VMs occasionally left a blurred camera code cell at margin 0.17, below the
reader's 0.18 threshold. 138,240 bps is the Web export preset (0.08 bpp),
still below the Social preset the verify exports use.
@AatmanAJ

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

@AatmanAJ

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

This branch has not been deployed

No deployments
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