From 00102ad1630a5dc89e0ec98774822de9e80fce25 Mon Sep 17 00:00:00 2001 From: Aatman Jain Date: Thu, 29 Oct 2026 23:26:41 +0000 Subject: [PATCH 01/10] perf(rendering): cheaper blur sampling and overlay capture Motion-blur taps use a single texture read instead of sharpened downscale sampling; overlay backdrops refresh less often when crop and display size are stable; cropped D3D capture frames reuse a texture pool so queued frames are not overwritten. --- apps/desktop-gpui/src/overlay_backdrop.rs | 3 +- apps/desktop-gpui/src/target_overlay.rs | 20 +++++++-- .../src/shaders/composite-video-frame.wgsl | 42 +++++++++++++------ crates/scap-direct3d/src/lib.rs | 29 +++++++------ 4 files changed, 64 insertions(+), 30 deletions(-) diff --git a/apps/desktop-gpui/src/overlay_backdrop.rs b/apps/desktop-gpui/src/overlay_backdrop.rs index b896b3981bf..59b8ddc1ce9 100644 --- a/apps/desktop-gpui/src/overlay_backdrop.rs +++ b/apps/desktop-gpui/src/overlay_backdrop.rs @@ -22,7 +22,8 @@ use gpui::{AsyncApp, RenderImage, Window}; use scap_targets::DisplayId; /// A capture older than this is retaken the next time the overlay draws. -pub const STALE_AFTER: Duration = Duration::from_secs(3); +pub const STALE_AFTER: Duration = Duration::from_secs(8); +pub const STALE_AFTER_MIN: Duration = Duration::from_secs(2); /// How long the panels must hold still before a stale capture is retaken. pub const SETTLE: Duration = Duration::from_millis(400); diff --git a/apps/desktop-gpui/src/target_overlay.rs b/apps/desktop-gpui/src/target_overlay.rs index 28cc9b5b91f..21b4dbe611c 100644 --- a/apps/desktop-gpui/src/target_overlay.rs +++ b/apps/desktop-gpui/src/target_overlay.rs @@ -1224,6 +1224,8 @@ pub struct OverlayWindow { /// The area as the last render drew it, and when it last changed: a /// capture waits for the panels to stop moving. backdrop_crop: Option, + backdrop_capture_crop: Option, + backdrop_display_size: Option<(f32, f32)>, crop_moved_at: Option, backdrop_wake_pending: bool, inline_camera: Option>, @@ -1398,6 +1400,8 @@ impl OverlayWindow { backdrop_at: None, backdrop_capturing: false, backdrop_crop: None, + backdrop_capture_crop: None, + backdrop_display_size: None, crop_moved_at: None, backdrop_wake_pending: false, inline_camera: None, @@ -1570,6 +1574,8 @@ impl OverlayWindow { if self.backdrop.has_image() { self.backdrop.set_image(None, gpui::Bounds::default()); self.backdrop_at = None; + self.backdrop_capture_crop = None; + self.backdrop_display_size = None; } return; } @@ -1577,17 +1583,23 @@ impl OverlayWindow { self.backdrop_crop = self.crop; self.crop_moved_at = Some(Instant::now()); } + let bounds_unchanged = self.backdrop.has_image() + && self.backdrop_capture_crop == self.crop + && self.backdrop_display_size == Some(self.display_size); // A drawn area moves the camera into it, so a capture from before the // area last changed is stale however recent it is. let moved_since = self .crop_moved_at .zip(self.backdrop_at) .is_some_and(|(moved, captured)| moved > captured); + let stale_after = if bounds_unchanged { + crate::overlay_backdrop::STALE_AFTER + } else { + crate::overlay_backdrop::STALE_AFTER_MIN + }; if self.backdrop_capturing || (!moved_since - && self - .backdrop_at - .is_some_and(|at| at.elapsed() < crate::overlay_backdrop::STALE_AFTER)) + && self.backdrop_at.is_some_and(|at| at.elapsed() < stale_after)) { return; } @@ -1644,6 +1656,8 @@ impl OverlayWindow { size, ) }); + this.backdrop_capture_crop = this.crop; + this.backdrop_display_size = image.as_ref().map(|_| size); this.backdrop.set_image( image, gpui::Bounds::new( diff --git a/crates/rendering/src/shaders/composite-video-frame.wgsl b/crates/rendering/src/shaders/composite-video-frame.wgsl index ec2e57b5a59..1118ac1481d 100644 --- a/crates/rendering/src/shaders/composite-video-frame.wgsl +++ b/crates/rendering/src/shaders/composite-video-frame.wgsl @@ -359,7 +359,7 @@ fn fs_main(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { for (var i = 0; i <= 20; i = i + 1) { let sample_uv = target_uv + velocity_uv * (f32(i) / k); - var tap = sample_texture(sample_uv, crop_bounds_uv); + var tap = sample_texture_blur(sample_uv, crop_bounds_uv); tap = apply_rounded_corners(tap, sample_uv); accum += tap.rgb * tap.a; alpha_sum += tap.a; @@ -399,7 +399,7 @@ fn fs_main(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { let weight = 4.0 * (percent - percent * percent); let sample_uv = target_uv + scaled_dir * percent; - var tap = sample_texture(sample_uv, crop_bounds_uv); + var tap = sample_texture_blur(sample_uv, crop_bounds_uv); tap = apply_rounded_corners(tap, sample_uv); accum += tap.rgb * tap.a * weight; alpha_sum += tap.a * weight; @@ -419,21 +419,37 @@ fn fs_main(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { ); } -fn sample_texture(uv: vec2, crop_bounds_uv: vec4) -> vec4 { +fn cropped_sample_uv(uv: vec2, crop_bounds_uv: vec4) -> vec2 { + var sample_uv = uv; + if uniforms.mirror_x != 0.0 { + sample_uv.x = 1.0 - sample_uv.x; + } + + let crop_size = crop_bounds_uv.zw - crop_bounds_uv.xy; + var cropped_uv = sample_uv * crop_size + crop_bounds_uv.xy; + + let texel_offset = 1.0 / uniforms.frame_size; + let safe_min = crop_bounds_uv.xy + texel_offset; + let safe_max = crop_bounds_uv.zw - texel_offset; + return clamp(cropped_uv, safe_min, safe_max); +} + +fn sample_texture_blur(uv: vec2, crop_bounds_uv: vec4) -> vec4 { if uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0 { - var sample_uv = uv; - if uniforms.mirror_x != 0.0 { - sample_uv.x = 1.0 - sample_uv.x; - } + let cropped_uv = cropped_sample_uv(uv, crop_bounds_uv); + let center_sample = textureSample(frame_texture, frame_sampler, cropped_uv); + let out_alpha = select(1.0, center_sample.a, uniforms.preserve_source_alpha > 0.5); + return vec4(center_sample.rgb, out_alpha); + } - let crop_size = crop_bounds_uv.zw - crop_bounds_uv.xy; - var cropped_uv = sample_uv * crop_size + crop_bounds_uv.xy; + return vec4(0.0); +} - let texel_offset = 1.0 / uniforms.frame_size; - let safe_min = crop_bounds_uv.xy + texel_offset; - let safe_max = crop_bounds_uv.zw - texel_offset; - cropped_uv = clamp(cropped_uv, safe_min, safe_max); +fn sample_texture(uv: vec2, crop_bounds_uv: vec4) -> vec4 { + if uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0 { + let cropped_uv = cropped_sample_uv(uv, crop_bounds_uv); + let crop_size = crop_bounds_uv.zw - crop_bounds_uv.xy; let source_size = uniforms.frame_size * crop_size; let target_size = uniforms.target_size; let scale_ratio = source_size / target_size; diff --git a/crates/scap-direct3d/src/lib.rs b/crates/scap-direct3d/src/lib.rs index 92e37f9d342..dc3a1517bff 100644 --- a/crates/scap-direct3d/src/lib.rs +++ b/crates/scap-direct3d/src/lib.rs @@ -622,15 +622,9 @@ impl Capturer { let crop_data = settings .crop .map(|crop| { - let texture = create_copy_texture( - &d3d_device, - crop.right - crop.left, - crop.bottom - crop.top, - settings.pixel_format, - ) - .map_err(NewCapturerError::CropTexture)?; - - Ok::<_, NewCapturerError>((texture, crop)) + let width = crop.right - crop.left; + let height = crop.bottom - crop.top; + Ok::<_, NewCapturerError>((Arc::new(TrimPool::default()), crop, width, height)) }) .transpose()?; @@ -737,7 +731,9 @@ impl Capturer { return Ok(()); } - let frame = if let Some((cropped_texture, crop)) = crop_data.clone() { + let frame = if let Some((crop_pool, crop, crop_width, crop_height)) = + crop_data.clone() + { // A shrunk item leaves the crop box hanging off the // edge of the source texture, which copies nothing // and would vend a stale surface as if it were live. @@ -750,6 +746,13 @@ impl Capturer { return Ok(()); } + let cropped_texture = crop_pool.take( + &d3d_device, + crop_width, + crop_height, + pixel_format, + )?; + unsafe { d3d_context.CopySubresourceRegion( &cropped_texture, @@ -764,15 +767,15 @@ impl Capturer { } Frame { - width: crop.right - crop.left, - height: crop.bottom - crop.top, + width: crop_width, + height: crop_height, pixel_format: settings.pixel_format, inner: frame, texture: cropped_texture, d3d_context: d3d_context.clone(), d3d_device: d3d_device.clone(), staging_pool: staging_pool.clone(), - recycle: None, + recycle: Some(crop_pool), } } else { // The frame is the content bounded by its surface, From 02c1dae2dbfd875b763b03cd99d11e6d25d8082d Mon Sep 17 00:00:00 2001 From: Aatman Jain Date: Thu, 29 Oct 2026 23:38:30 +0000 Subject: [PATCH 02/10] fix(rendering): restore safe UV bounds in sample_texture Refactoring for blur sampling moved crop clamping into cropped_sample_uv but left sample_texture using safe_min/safe_max out of scope. --- crates/rendering/src/shaders/composite-video-frame.wgsl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/rendering/src/shaders/composite-video-frame.wgsl b/crates/rendering/src/shaders/composite-video-frame.wgsl index 1118ac1481d..92eaffe70dc 100644 --- a/crates/rendering/src/shaders/composite-video-frame.wgsl +++ b/crates/rendering/src/shaders/composite-video-frame.wgsl @@ -448,6 +448,9 @@ fn sample_texture_blur(uv: vec2, crop_bounds_uv: vec4) -> vec4 { fn sample_texture(uv: vec2, crop_bounds_uv: vec4) -> vec4 { if uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0 { let cropped_uv = cropped_sample_uv(uv, crop_bounds_uv); + let texel_offset = 1.0 / uniforms.frame_size; + let safe_min = crop_bounds_uv.xy + texel_offset; + let safe_max = crop_bounds_uv.zw - texel_offset; let crop_size = crop_bounds_uv.zw - crop_bounds_uv.xy; let source_size = uniforms.frame_size * crop_size; From a9a4198206ab07045ddb1a18ebff409316555eb4 Mon Sep 17 00:00:00 2001 From: Aatman Jain Date: Thu, 29 Oct 2026 23:56:11 +0000 Subject: [PATCH 03/10] fix(recording): keep cropped capture frames alive through MF encode Hold the scap Direct3D frame until the encoder finishes processing the texture so pooled crop surfaces are not recycled early. --- crates/recording/src/output_pipeline/win.rs | 13 ++++++++++++- .../recording/src/output_pipeline/win_segmented.rs | 14 +++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/crates/recording/src/output_pipeline/win.rs b/crates/recording/src/output_pipeline/win.rs index faab33d6e76..e221030f05e 100644 --- a/crates/recording/src/output_pipeline/win.rs +++ b/crates/recording/src/output_pipeline/win.rs @@ -346,13 +346,24 @@ impl Muxer for WindowsMuxer { let mut first_timestamp: Option = None; let mut frame_count: u64 = 0; let mut frames_reused: u64 = 0; + let mut captured_keepalive: Option = None; let result = encoder.run( Arc::new(AtomicBool::default()), || loop { let (texture, timestamp) = match pacer.next(&video_rx, &pause_flag) { Paced::Frame(frame, timestamp) => { - let texture = frame.texture().clone(); + let texture = match frame { + screen_capture::ScreenFrame::Captured(captured) => { + let texture = captured.texture().clone(); + captured_keepalive = Some(captured); + texture + } + screen_capture::ScreenFrame::Scaled(scaled) => { + captured_keepalive = None; + scaled.texture.clone() + } + }; last_texture = Some(texture.clone()); (texture, timestamp) } diff --git a/crates/recording/src/output_pipeline/win_segmented.rs b/crates/recording/src/output_pipeline/win_segmented.rs index 1edaf97daf1..509a01c7032 100644 --- a/crates/recording/src/output_pipeline/win_segmented.rs +++ b/crates/recording/src/output_pipeline/win_segmented.rs @@ -531,6 +531,7 @@ impl WindowsSegmentedMuxer { either::Left((mut encoder, mut muxer)) => { trace!("Running native encoder for segment"); let mut first_timestamp: Option = None; + let mut captured_keepalive: Option = None; let result = encoder.run( Arc::new(AtomicBool::default()), || { @@ -547,7 +548,18 @@ impl WindowsSegmentedMuxer { }; let frame_time = duration_to_timespan(relative); - Ok(Some((frame.texture().clone(), frame_time))) + let texture = match frame { + screen_capture::ScreenFrame::Captured(captured) => { + let texture = captured.texture().clone(); + captured_keepalive = Some(captured); + texture + } + screen_capture::ScreenFrame::Scaled(scaled) => { + captured_keepalive = None; + scaled.texture.clone() + } + }; + Ok(Some((texture, frame_time))) }, |output_sample| { let mut output = match output_clone.lock() { From 9f68edea78c082722ccd4ff132accf14a98694b4 Mon Sep 17 00:00:00 2001 From: Aatman Jain Date: Fri, 30 Oct 2026 09:29:47 +0000 Subject: [PATCH 04/10] test(rendering): validate every WGSL shader with naga --- Cargo.lock | 1 + crates/rendering/Cargo.toml | 1 + .../src/shaders/composite-video-frame.wgsl | 2 +- crates/rendering/tests/wgsl_shaders.rs | 51 +++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 crates/rendering/tests/wgsl_shaders.rs diff --git a/Cargo.lock b/Cargo.lock index 9b5243fb43e..aabdd55b32b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1714,6 +1714,7 @@ dependencies = [ "libloading 0.8.8", "log", "metal 0.31.0", + "naga", "objc2 0.6.2", "pollster", "pretty_assertions", diff --git a/crates/rendering/Cargo.toml b/crates/rendering/Cargo.toml index de9690088a2..b0f6b8a066c 100644 --- a/crates/rendering/Cargo.toml +++ b/crates/rendering/Cargo.toml @@ -74,6 +74,7 @@ cap-video-decode = { path = "../video-decode", features = ["scripted"] } cap-verify-expect = { path = "../verify-expect" } tempfile = "3" pollster = "0.4.0" +naga = { version = "25", features = ["wgsl-in"] } pretty_assertions = "1.4.1" tracing-subscriber = "0.3.19" diff --git a/crates/rendering/src/shaders/composite-video-frame.wgsl b/crates/rendering/src/shaders/composite-video-frame.wgsl index 92eaffe70dc..d9a5ddc7efa 100644 --- a/crates/rendering/src/shaders/composite-video-frame.wgsl +++ b/crates/rendering/src/shaders/composite-video-frame.wgsl @@ -426,7 +426,7 @@ fn cropped_sample_uv(uv: vec2, crop_bounds_uv: vec4) -> vec2 { } let crop_size = crop_bounds_uv.zw - crop_bounds_uv.xy; - var cropped_uv = sample_uv * crop_size + crop_bounds_uv.xy; + let cropped_uv = sample_uv * crop_size + crop_bounds_uv.xy; let texel_offset = 1.0 / uniforms.frame_size; let safe_min = crop_bounds_uv.xy + texel_offset; diff --git a/crates/rendering/tests/wgsl_shaders.rs b/crates/rendering/tests/wgsl_shaders.rs new file mode 100644 index 00000000000..08740aa5640 --- /dev/null +++ b/crates/rendering/tests/wgsl_shaders.rs @@ -0,0 +1,51 @@ +use naga::{ + front::wgsl, + valid::{Capabilities, ValidationFlags, Validator}, +}; +use std::path::PathBuf; + +fn shader_paths() -> Vec { + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/shaders"); + let mut paths: Vec = std::fs::read_dir(&dir) + .unwrap_or_else(|error| panic!("read {}: {error}", dir.display())) + .map(|entry| entry.unwrap().path()) + .filter(|path| { + path.extension() + .is_some_and(|extension| extension == "wgsl") + }) + .collect(); + paths.sort(); + paths +} + +fn validate(source: &str) -> Result<(), String> { + let module = wgsl::parse_str(source).map_err(|error| error.emit_to_string(source))?; + Validator::new(ValidationFlags::all(), Capabilities::all()) + .validate(&module) + .map(|_| ()) + .map_err(|error| error.emit_to_string(source)) +} + +#[test] +fn every_wgsl_shader_parses_and_validates() { + let paths = shader_paths(); + assert!(!paths.is_empty(), "no shaders found"); + + let failures: Vec = paths + .iter() + .filter_map(|path| { + let source = std::fs::read_to_string(path).unwrap(); + validate(&source) + .err() + .map(|error| format!("{}:\n{error}", path.file_name().unwrap().to_string_lossy())) + }) + .collect(); + + assert!(failures.is_empty(), "{}", failures.join("\n\n")); +} + +#[test] +fn validation_rejects_an_undefined_identifier() { + let source = "@fragment fn main() -> @location(0) vec4 { return vec4(missing); }"; + assert!(validate(source).is_err()); +} From 2abcf37b61c23f51972442513cb862b4cf04a972 Mon Sep 17 00:00:00 2001 From: Aatman Jain Date: Fri, 30 Oct 2026 09:29:47 +0000 Subject: [PATCH 05/10] fix(recording): keep only pooled capture frames alive through encode and stop reading a private field --- crates/recording/src/output_pipeline/win.rs | 15 +++------------ .../src/output_pipeline/win_segmented.rs | 15 +++------------ .../src/sources/screen_capture/windows.rs | 11 +++++++++++ crates/scap-direct3d/src/lib.rs | 12 +++++++++++- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/crates/recording/src/output_pipeline/win.rs b/crates/recording/src/output_pipeline/win.rs index e221030f05e..c99081c0231 100644 --- a/crates/recording/src/output_pipeline/win.rs +++ b/crates/recording/src/output_pipeline/win.rs @@ -346,24 +346,15 @@ impl Muxer for WindowsMuxer { let mut first_timestamp: Option = None; let mut frame_count: u64 = 0; let mut frames_reused: u64 = 0; - let mut captured_keepalive: Option = None; + let mut pooled_frame: Option = None; let result = encoder.run( Arc::new(AtomicBool::default()), || loop { let (texture, timestamp) = match pacer.next(&video_rx, &pause_flag) { Paced::Frame(frame, timestamp) => { - let texture = match frame { - screen_capture::ScreenFrame::Captured(captured) => { - let texture = captured.texture().clone(); - captured_keepalive = Some(captured); - texture - } - screen_capture::ScreenFrame::Scaled(scaled) => { - captured_keepalive = None; - scaled.texture.clone() - } - }; + let texture = frame.texture().clone(); + pooled_frame = frame.into_pooled(); last_texture = Some(texture.clone()); (texture, timestamp) } diff --git a/crates/recording/src/output_pipeline/win_segmented.rs b/crates/recording/src/output_pipeline/win_segmented.rs index 509a01c7032..412a26a31af 100644 --- a/crates/recording/src/output_pipeline/win_segmented.rs +++ b/crates/recording/src/output_pipeline/win_segmented.rs @@ -531,7 +531,7 @@ impl WindowsSegmentedMuxer { either::Left((mut encoder, mut muxer)) => { trace!("Running native encoder for segment"); let mut first_timestamp: Option = None; - let mut captured_keepalive: Option = None; + let mut pooled_frame: Option = None; let result = encoder.run( Arc::new(AtomicBool::default()), || { @@ -548,17 +548,8 @@ impl WindowsSegmentedMuxer { }; let frame_time = duration_to_timespan(relative); - let texture = match frame { - screen_capture::ScreenFrame::Captured(captured) => { - let texture = captured.texture().clone(); - captured_keepalive = Some(captured); - texture - } - screen_capture::ScreenFrame::Scaled(scaled) => { - captured_keepalive = None; - scaled.texture.clone() - } - }; + let texture = frame.texture().clone(); + pooled_frame = frame.into_pooled(); Ok(Some((texture, frame_time))) }, |output_sample| { diff --git a/crates/recording/src/sources/screen_capture/windows.rs b/crates/recording/src/sources/screen_capture/windows.rs index ef8a9dbd343..972e9542499 100644 --- a/crates/recording/src/sources/screen_capture/windows.rs +++ b/crates/recording/src/sources/screen_capture/windows.rs @@ -118,6 +118,17 @@ impl ScreenFrame { } } + /// The frame, when its texture goes back to the capturer's pool once the + /// frame is dropped. A caller that passes such a texture on must keep the + /// frame until the texture has been consumed, or the next capture can + /// overwrite it. + pub fn into_pooled(self) -> Option { + match self { + ScreenFrame::Captured(frame) if frame.is_pooled() => Some(frame), + ScreenFrame::Captured(_) | ScreenFrame::Scaled(_) => None, + } + } + pub fn width(&self) -> u32 { match self { ScreenFrame::Captured(frame) => frame.width(), diff --git a/crates/scap-direct3d/src/lib.rs b/crates/scap-direct3d/src/lib.rs index dc3a1517bff..b6967529747 100644 --- a/crates/scap-direct3d/src/lib.rs +++ b/crates/scap-direct3d/src/lib.rs @@ -624,7 +624,13 @@ impl Capturer { .map(|crop| { let width = crop.right - crop.left; let height = crop.bottom - crop.top; - Ok::<_, NewCapturerError>((Arc::new(TrimPool::default()), crop, width, height)) + let texture = + create_copy_texture(&d3d_device, width, height, settings.pixel_format) + .map_err(NewCapturerError::CropTexture)?; + let crop_pool = Arc::new(TrimPool::default()); + crop_pool.give(texture, width, height); + + Ok::<_, NewCapturerError>((crop_pool, crop, width, height)) }) .transpose()?; @@ -984,6 +990,10 @@ impl Frame { &self.texture } + pub fn is_pooled(&self) -> bool { + self.recycle.is_some() + } + pub fn d3d_device(&self) -> &ID3D11Device { &self.d3d_device } From bab1fb3b7d597b5ebdb30f1ef970e0838a6c7a90 Mon Sep 17 00:00:00 2001 From: Aatman Jain Date: Fri, 30 Oct 2026 09:29:47 +0000 Subject: [PATCH 06/10] fix(desktop): document the shorter backdrop staleness window --- apps/desktop-gpui/src/overlay_backdrop.rs | 4 +++- apps/desktop-gpui/src/target_overlay.rs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/desktop-gpui/src/overlay_backdrop.rs b/apps/desktop-gpui/src/overlay_backdrop.rs index 59b8ddc1ce9..81b644c49cb 100644 --- a/apps/desktop-gpui/src/overlay_backdrop.rs +++ b/apps/desktop-gpui/src/overlay_backdrop.rs @@ -21,7 +21,9 @@ use std::{ use gpui::{AsyncApp, RenderImage, Window}; use scap_targets::DisplayId; -/// A capture older than this is retaken the next time the overlay draws. +/// A capture older than this is retaken the next time the overlay draws, or +/// older than `STALE_AFTER_MIN` once the area or display size it was taken for +/// has changed. pub const STALE_AFTER: Duration = Duration::from_secs(8); pub const STALE_AFTER_MIN: Duration = Duration::from_secs(2); diff --git a/apps/desktop-gpui/src/target_overlay.rs b/apps/desktop-gpui/src/target_overlay.rs index 21b4dbe611c..b4b16d0e00e 100644 --- a/apps/desktop-gpui/src/target_overlay.rs +++ b/apps/desktop-gpui/src/target_overlay.rs @@ -1599,7 +1599,9 @@ impl OverlayWindow { }; if self.backdrop_capturing || (!moved_since - && self.backdrop_at.is_some_and(|at| at.elapsed() < stale_after)) + && self + .backdrop_at + .is_some_and(|at| at.elapsed() < stale_after)) { return; } From 4397279b52e3fdbb0aedae5f525a20685489cc4d Mon Sep 17 00:00:00 2001 From: Aatman Jain Date: Fri, 30 Oct 2026 10:28:08 +0000 Subject: [PATCH 07/10] fix(desktop-gpui): expect no picker backdrop where the overlay skips it --- apps/desktop-gpui/src/picker_probe.rs | 10 +++++++++- apps/desktop-gpui/src/target_overlay.rs | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/desktop-gpui/src/picker_probe.rs b/apps/desktop-gpui/src/picker_probe.rs index fcc7e77285f..5dbcaeb5d23 100644 --- a/apps/desktop-gpui/src/picker_probe.rs +++ b/apps/desktop-gpui/src/picker_probe.rs @@ -106,7 +106,7 @@ pub fn run(main: WindowHandle, cx: &mut App) { let mut mode = cx.update(|cx| probe_mode(label, cx)); mode["overlayInactiveBeforeActivation"] = inactive_before.into(); let screen = screenshot(&output, label, cx).await; - mode["backdrop"] = probe_backdrop(&output, label, screen.as_ref(), cx).await; + mode["backdrop"] = probe_backdrop(&output, label, kind, screen.as_ref(), cx).await; modes.push(mode); if kind == TargetType::Window { window_arrangement = Some(probe_window_arrangement(&output, cx).await); @@ -372,6 +372,7 @@ fn mean_difference( async fn probe_backdrop( output: &std::ffi::OsStr, label: &str, + kind: TargetType, screen: Option<&image::DynamicImage>, cx: &mut AsyncApp, ) -> serde_json::Value { @@ -393,6 +394,13 @@ async fn probe_backdrop( .ok() .flatten() }); + if !crate::target_overlay::backdrop_suits(Some(kind)) { + return serde_json::json!({ + "skipped": "the picker takes no backdrop in this mode here", + "present": image.is_some(), + "passed": image.is_none(), + }); + } let Some(image) = image else { return serde_json::json!({ "present": false, "passed": false }); }; diff --git a/apps/desktop-gpui/src/target_overlay.rs b/apps/desktop-gpui/src/target_overlay.rs index b4b16d0e00e..231289aef65 100644 --- a/apps/desktop-gpui/src/target_overlay.rs +++ b/apps/desktop-gpui/src/target_overlay.rs @@ -830,7 +830,7 @@ fn more_area_ratios() -> Vec<(u32, u32)> { /// so a capture under their panes showed through it as hard-edged blocks; /// there the panes sit on the fallback fill, as they do where no capture /// is possible. -fn backdrop_suits(mode: Option) -> bool { +pub(crate) fn backdrop_suits(mode: Option) -> bool { match mode { None => false, Some(TargetType::Display | TargetType::Window) => !cfg!(windows), From 95490569d9fcfc278596ae6d558cc693932e8800 Mon Sep 17 00:00:00 2001 From: Aatman Jain Date: Fri, 30 Oct 2026 10:23:07 +0000 Subject: [PATCH 08/10] fix(ci): configure Hyprland 0.57 through hyprland.lua in the Linux harness 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. --- scripts/linux-desktop-harness/arch-up.sh | 17 ++++++++++------- .../scenario-screenshot.sh | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/scripts/linux-desktop-harness/arch-up.sh b/scripts/linux-desktop-harness/arch-up.sh index 6a1a8028862..f3a883510d6 100755 --- a/scripts/linux-desktop-harness/arch-up.sh +++ b/scripts/linux-desktop-harness/arch-up.sh @@ -23,13 +23,16 @@ common=(LIBSEAT_BACKEND=seatd XDG_SESSION_TYPE=wayland) case "$desktop" in hyprland) - cat >"$HOME/.config/hypr/hyprland.conf" <"$HOME/.config/hypr/hyprland.lua" <"$HOME/.config/hypr/xdph.conf" < /^(Cap|so\.cap\.Cap)$/.test(w.get_wm_class() || '') && w.showing_on_its_workspace()).forEach(w => w.activate(global.display.get_current_time_roundtrip()))" \ >/dev/null 2>&1 elif [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]]; then - hyprctl dispatch focuswindow class:Cap >/dev/null 2>&1 + hyprctl dispatch "hl.dsp.focus({ window = 'class:Cap' })" >/dev/null 2>&1 fi } for _ in $(seq 60); do From 7fbeb8c03199310fe6a9dc4592ec558030aa7a3f Mon Sep 17 00:00:00 2001 From: Aatman Jain Date: Fri, 30 Oct 2026 10:38:26 +0000 Subject: [PATCH 09/10] fix(verify): encode the 320x180 compositing codec case at the Web preset 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. --- crates/verify/tests/codec.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/verify/tests/codec.rs b/crates/verify/tests/codec.rs index d2efa9de025..bbff4a6afe8 100644 --- a/crates/verify/tests/codec.rs +++ b/crates/verify/tests/codec.rs @@ -215,7 +215,11 @@ fn video_code_survives_compositing_scaling_rounding_bubble_and_blur() { let dir = tempfile::tempdir().unwrap(); // 640x360 is a typical verification output; 320x180 puts the screen at // a quarter of its source size and the camera code cells at ~6 px. - for (out_w, out_h, bitrate) in [(640, 360, 150_000), (320, 180, 80_000)] { + // 320x180 is encoded at the Web export preset (0.08 bpp): at 80 kbps the + // paravirtualised VideoToolbox HEVC encoder on the CI macOS VMs + // occasionally left a blurred camera cell at margin 0.17, below the + // reader's 0.18, while this host's hardware encoder read it at 0.31. + for (out_w, out_h, bitrate) in [(640, 360, 150_000), (320, 180, 138_240)] { for blur in [false, true] { let pairs: Vec<(u32, u32)> = (0..40).map(|i| (i * 3 + 100, i / 2 + 7)).collect(); let frames: Vec = pairs From 0c28d46b4ecec99900cb871b80b0edaef3e4f633 Mon Sep 17 00:00:00 2001 From: Aatman Jain Date: Fri, 30 Oct 2026 11:02:49 +0000 Subject: [PATCH 10/10] fix(ci): walk GIF blocks when measuring the exported GIF's length --- .../linux-desktop-harness/scenario-export.sh | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/scripts/linux-desktop-harness/scenario-export.sh b/scripts/linux-desktop-harness/scenario-export.sh index d73efcf77af..ec76b57331e 100755 --- a/scripts/linux-desktop-harness/scenario-export.sh +++ b/scripts/linux-desktop-harness/scenario-export.sh @@ -60,12 +60,32 @@ gif_frames_and_seconds() { import sys data = open(sys.argv[1], "rb").read() + + +def skip_sub_blocks(at): + while data[at]: + at += data[at] + 1 + return at + 1 + + if data[:6] in (b"GIF87a", b"GIF89a"): - frames, centiseconds, at = 0, 0, data.find(b"\x21\xf9\x04") - while at != -1 and at + 6 <= len(data): - frames += 1 - centiseconds += data[at + 4] | data[at + 5] << 8 - at = data.find(b"\x21\xf9\x04", at + 8) + frames, centiseconds, delay = 0, 0, 0 + flags = data[10] + at = 13 + (3 << ((flags & 7) + 1) if flags & 0x80 else 0) + while at < len(data) and data[at] != 0x3B: + if data[at] == 0x21: + if data[at + 1] == 0xF9: + delay = data[at + 4] | data[at + 5] << 8 + at = skip_sub_blocks(at + 2) + elif data[at] == 0x2C: + frames += 1 + centiseconds += delay + delay = 0 + local = data[at + 9] + at += 10 + (3 << ((local & 7) + 1) if local & 0x80 else 0) + at = skip_sub_blocks(at + 1) + else: + break print(frames, f"{centiseconds / 100:.2f}") PY }