From cadef4a9fb9e616402e969b8e40f52fca67daf90 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:08:18 +0800 Subject: [PATCH 1/5] perf(desktop-gpui): convert the camera preview at bubble size, newest frame only, while shown --- .../kernels/src/camera_preview.rs | 89 +++++++++ apps/desktop-gpui/kernels/src/lib.rs | 1 + apps/desktop-gpui/src/camera_window.rs | 182 +++++++++++++----- apps/desktop-gpui/src/feeds.rs | 144 +++++++++++++- apps/desktop-gpui/src/platform.rs | 13 +- 5 files changed, 381 insertions(+), 48 deletions(-) create mode 100644 apps/desktop-gpui/kernels/src/camera_preview.rs diff --git a/apps/desktop-gpui/kernels/src/camera_preview.rs b/apps/desktop-gpui/kernels/src/camera_preview.rs new file mode 100644 index 00000000000..05c8eaf5997 --- /dev/null +++ b/apps/desktop-gpui/kernels/src/camera_preview.rs @@ -0,0 +1,89 @@ +const STEPS: [f32; 10] = [ + 270., 360., 480., 540., 720., 960., 1080., 1440., 1920., 2160., +]; + +pub fn quantized_target((width, height): (f32, f32)) -> Option<(usize, usize)> { + let step = |extent: f32| { + STEPS + .iter() + .find(|step| extent <= **step) + .map(|step| *step as usize) + }; + Some((step(width)?, step(height)?)) +} + +pub fn cover_dimensions(source: (usize, usize), target: (usize, usize)) -> (usize, usize) { + let (width, height) = source; + if width == 0 || height == 0 { + return source; + } + let scale = (target.0 as f64 / width as f64).max(target.1 as f64 / height as f64); + if scale >= 1.0 { + return source; + } + ( + ((width as f64 * scale).round() as usize).clamp(1, width), + ((height as f64 * scale).round() as usize).clamp(1, height), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn targets_round_up_to_a_fixed_step() { + assert_eq!(quantized_target((460., 460.)), Some((480, 480))); + assert_eq!(quantized_target((480., 480.)), Some((480, 480))); + assert_eq!(quantized_target((481., 300.)), Some((540, 360))); + assert_eq!(quantized_target((0., -56.)), Some((270, 270))); + assert_eq!(quantized_target((2133.3, 1200.)), Some((2160, 1440))); + } + + #[test] + fn targets_beyond_the_largest_step_or_not_finite_are_uncapped() { + assert_eq!(quantized_target((2161., 400.)), None); + assert_eq!(quantized_target((400., f32::NAN)), None); + assert_eq!(quantized_target((f32::INFINITY, 400.)), None); + } + + #[test] + fn nearby_sizes_share_a_target() { + let targets = (461..=480) + .map(|extent| quantized_target((extent as f32, extent as f32))) + .collect::>(); + assert!(targets.iter().all(|target| *target == Some((480, 480)))); + } + + #[test] + fn cover_dimensions_fill_the_target_and_keep_the_source_aspect() { + assert_eq!(cover_dimensions((1920, 1080), (480, 480)), (853, 480)); + assert_eq!(cover_dimensions((3840, 2160), (480, 480)), (853, 480)); + assert_eq!(cover_dimensions((1920, 1080), (960, 540)), (960, 540)); + assert_eq!(cover_dimensions((1440, 1080), (960, 540)), (960, 720)); + assert_eq!(cover_dimensions((1080, 1920), (480, 480)), (480, 853)); + for source in [ + (1280, 720), + (1920, 1080), + (3840, 2160), + (1440, 1080), + (1080, 1920), + ] { + for target in [(270, 270), (480, 480), (960, 540), (1080, 1080)] { + let (width, height) = cover_dimensions(source, target); + assert!(width >= target.0.min(source.0)); + assert!(height >= target.1.min(source.1)); + let aspect = source.0 as f64 / source.1 as f64; + assert!((width as f64 / height as f64 - aspect).abs() / aspect < 0.005); + } + } + } + + #[test] + fn cover_dimensions_never_upscale() { + assert_eq!(cover_dimensions((1280, 720), (1440, 1440)), (1280, 720)); + assert_eq!(cover_dimensions((1920, 1080), (1920, 1080)), (1920, 1080)); + assert_eq!(cover_dimensions((1920, 1080), (2160, 1080)), (1920, 1080)); + assert_eq!(cover_dimensions((0, 1080), (480, 480)), (0, 1080)); + } +} diff --git a/apps/desktop-gpui/kernels/src/lib.rs b/apps/desktop-gpui/kernels/src/lib.rs index 3971ea2da35..cd3bd826500 100644 --- a/apps/desktop-gpui/kernels/src/lib.rs +++ b/apps/desktop-gpui/kernels/src/lib.rs @@ -1,3 +1,4 @@ +pub mod camera_preview; pub mod codec; pub mod frame; pub mod mask_cache; diff --git a/apps/desktop-gpui/src/camera_window.rs b/apps/desktop-gpui/src/camera_window.rs index 3325cce996d..89799a82ae5 100644 --- a/apps/desktop-gpui/src/camera_window.rs +++ b/apps/desktop-gpui/src/camera_window.rs @@ -384,10 +384,21 @@ mod frame { pub struct Converted { pub buffer: arc::R, pub dims: (usize, usize), + /// What the window lays itself out for: the camera's own size when + /// the frame was only shrunk to the bubble, so the rounding of a + /// cover target never nudges the window's aspect. + pub layout_dims: (usize, usize), pub generation: u64, pub mirrored: bool, } + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum Target { + Source, + Fit((usize, usize)), + Cover((usize, usize)), + } + // Handed from the conversion thread to the main thread once written; the // buffer is only read after that, and CF refcounts are atomic. unsafe impl Send for Converted {} @@ -518,21 +529,32 @@ mod frame { } /// The frame as a BGRA IOSurface-backed pixel buffer, downscaled to - /// fit `max_dims` when given and horizontally flipped when - /// `mirrored`. Recreates the rings when the camera's frame size, the - /// target size or the mirror toggle changes, or when `force_rebuild` - /// asks for a fresh rotation session (keeping the generation count - /// rising so no earlier ring can alias a new one). + /// fit inside a maximum size or to just cover the bubble per + /// `target`, and horizontally flipped when `mirrored`. Recreates the + /// rings when the camera's frame size, the target size or the mirror + /// toggle changes, or when `force_rebuild` asks for a fresh rotation + /// session (keeping the generation count rising so no earlier ring + /// can alias a new one). pub fn convert( this: &mut Option, frame: &cap_recording::NativeCameraFrame, - max_dims: Option<(usize, usize)>, + target: Target, mirrored: bool, force_rebuild: bool, ) -> Option { let src = frame.sample_buf.image_buf()?; let src_dims = (src.width(), src.height()); - let dst_dims = max_dims.map_or(src_dims, |max| fit_within(src_dims, max)); + let dst_dims = match target { + Target::Source => src_dims, + Target::Fit(max) => fit_within(src_dims, max), + Target::Cover(visible) => { + cap_gpui_kernels::camera_preview::cover_dimensions(src_dims, visible) + } + }; + let layout_dims = match target { + Target::Cover(_) => src_dims, + Target::Source | Target::Fit(_) => dst_dims, + }; if force_rebuild || this.as_ref().is_none_or(|converter| { converter.src_dims != src_dims @@ -566,6 +588,7 @@ mod frame { Some(Converted { buffer: out, dims: converter.dst_dims, + layout_dims, generation: converter.generation, mirrored: converter.flip_session.is_some(), }) @@ -1460,16 +1483,32 @@ struct ConvertJob { struct ConvertSettings { epoch: u64, blur_mode: Option, + preview_target: Option<(usize, usize)>, mirrored: bool, rebuilds: u64, } +#[cfg(target_os = "macos")] +impl ConvertSettings { + fn target(&self) -> frame::Target { + match (self.blur_mode, self.preview_target) { + (Some(_), _) => frame::Target::Fit(camera_blur::BLUR_MAX_DIMS), + (None, Some(visible)) => frame::Target::Cover(visible), + (None, None) => frame::Target::Source, + } + } +} + /// The `camera-preview-convert` thread. The VideoToolbox transfer (and the /// mirror pass) blocks until the hardware finishes, ~4-5 ms per frame, which /// stalled every main-thread frame while it ran on the main thread. #[cfg(target_os = "macos")] struct ConvertBridge { tx: flume::Sender, + /// The thread's own queue, so a newer frame can evict one still waiting + /// there. `None` when the thread could not be spawned: holding a receiver + /// would keep the channel connected and hide the inline fallback. + queued: Option>, _pump: gpui::Task<()>, } @@ -1488,11 +1527,10 @@ fn run_preview_conversion( let settings = job.settings; let force_rebuild = rebuilds.is_some_and(|rebuilds| rebuilds != settings.rebuilds); rebuilds = Some(settings.rebuilds); - let max_dims = settings.blur_mode.map(|_| camera_blur::BLUR_MAX_DIMS); let result = frame::FrameConverter::convert( &mut converter, &job.frame, - max_dims, + settings.target(), settings.mirrored, force_rebuild, ); @@ -1852,34 +1890,18 @@ impl CameraWindow { if failures_before != self.effect_failures { self.sync_effect_feedback(cx); } - let job = ConvertJob { - frame, - settings: self.convert_settings(epoch), - }; - match self.ensure_convert_bridge(window, cx).tx.try_send(job) { - // A full queue means the thread is mid-conversion; it picks up - // the next frame. - Ok(()) | Err(flume::TrySendError::Full(_)) => {} - Err(flume::TrySendError::Disconnected(job)) => { - let settings = job.settings; - let max_dims = settings.blur_mode.map(|_| camera_blur::BLUR_MAX_DIMS); - let force_rebuild = self - .fallback_rebuilds - .is_some_and(|rebuilds| rebuilds != settings.rebuilds); - self.fallback_rebuilds = Some(settings.rebuilds); - let converted = frame::FrameConverter::convert( - &mut self.fallback_converter, - &job.frame, - max_dims, - settings.mirrored, - force_rebuild, - ); - self.converted_arrived(converted, settings, window, cx); - } + // The same occlusion test gpui's display link runs on: a window + // failing it presents nothing, so converting (or blurring) for it + // is wasted. The next frame after it is shown converts again. + if self.inline || platform::window_is_presenting(window) { + self.queue_conversion(frame, epoch, window, cx); } } #[cfg(not(target_os = "macos"))] { + Feeds::global(cx) + .read(cx) + .set_camera_preview_target(self.preview_target(window)); let first_frame = self.frame_dims.is_none(); let dims_changed = self.frame_dims != Some(frame.dims); self.frame_dims = Some(frame.dims); @@ -1945,10 +1967,57 @@ impl CameraWindow { } #[cfg(target_os = "macos")] - fn convert_settings(&self, epoch: u64) -> ConvertSettings { + fn queue_conversion( + &mut self, + frame: cap_recording::NativeCameraFrame, + epoch: u64, + window: &mut Window, + cx: &mut Context, + ) { + let job = ConvertJob { + frame, + settings: self.convert_settings(epoch, window), + }; + let bridge = self.ensure_convert_bridge(window, cx); + let sent = match bridge.tx.try_send(job) { + Err(flume::TrySendError::Full(job)) => { + if let Some(queued) = &bridge.queued { + let _ = queued.try_recv(); + } + bridge.tx.try_send(job) + } + sent => sent, + }; + match sent { + Ok(()) | Err(flume::TrySendError::Full(_)) => {} + Err(flume::TrySendError::Disconnected(job)) => { + let settings = job.settings; + let force_rebuild = self + .fallback_rebuilds + .is_some_and(|rebuilds| rebuilds != settings.rebuilds); + self.fallback_rebuilds = Some(settings.rebuilds); + let converted = frame::FrameConverter::convert( + &mut self.fallback_converter, + &job.frame, + settings.target(), + settings.mirrored, + force_rebuild, + ); + self.converted_arrived(converted, settings, window, cx); + } + } + } + + #[cfg(target_os = "macos")] + fn convert_settings(&self, epoch: u64, window: &Window) -> ConvertSettings { + let blur_mode = self.active_blur_mode(); ConvertSettings { epoch, - blur_mode: self.active_blur_mode(), + blur_mode, + preview_target: blur_mode + .is_none() + .then(|| self.preview_target(window)) + .flatten(), mirrored: self.state.mirrored, rebuilds: self.convert_rebuilds, } @@ -1965,7 +2034,7 @@ impl CameraWindow { let Some(epoch) = Feeds::global(cx).read(cx).camera_preview_epoch() else { return; }; - if settings != self.convert_settings(epoch) { + if settings != self.convert_settings(epoch, window) { return; } let blur_mode = settings.blur_mode; @@ -1977,8 +2046,8 @@ impl CameraWindow { self.effect_failures.mirror = self.state.mirrored && !converted.mirrored; let first_frame = self.frame_dims.is_none(); let dims = converted.dims; - let dims_changed = self.frame_dims != Some(dims); - self.frame_dims = Some(dims); + let dims_changed = self.frame_dims != Some(converted.layout_dims); + self.frame_dims = Some(converted.layout_dims); let mut paint_raw = blur_mode.is_none(); if let Some(mode) = blur_mode { @@ -2036,16 +2105,21 @@ impl CameraWindow { fn ensure_convert_bridge(&mut self, window: &Window, cx: &mut Context) -> &ConvertBridge { if self.convert.is_none() { let (job_tx, job_rx) = flume::bounded::(1); + let queued = job_rx.clone(); let (out_tx, out_rx) = flume::bounded(1); - if let Err(error) = std::thread::Builder::new() + let queued = match std::thread::Builder::new() .name("camera-preview-convert".into()) .stack_size(crate::MEDIA_THREAD_STACK_SIZE) .spawn(move || run_preview_conversion(job_rx, out_tx)) { - // job_rx is gone; the caller's try_send sees Disconnected and - // converts inline. - tracing::warn!("camera preview conversion thread failed to spawn: {error}"); - } + Ok(_) => Some(queued), + Err(error) => { + // job_rx is gone; the caller's try_send sees Disconnected + // and converts inline. + tracing::warn!("camera preview conversion thread failed to spawn: {error}"); + None + } + }; let pump = cx.spawn_in(window, async move |this, cx| { while let Ok((converted, settings)) = out_rx.recv_async().await { if this @@ -2060,6 +2134,7 @@ impl CameraWindow { }); self.convert = Some(ConvertBridge { tx: job_tx, + queued, _pump: pump, }); } @@ -2156,6 +2231,27 @@ impl CameraWindow { first } + /// The bubble in physical pixels, rounded up to a fixed step so resizing + /// rebuilds the conversion rings only at step boundaries: the size camera + /// frames are shrunk to cover. The larger of the requested and the + /// current window size, so a frame converted mid-resize still covers + /// either. `None` (full size) while the target overlays draw the preview + /// inline, possibly on displays of another scale. + fn preview_target(&self, window: &Window) -> Option<(usize, usize)> { + if self.inline { + return None; + } + let (width, height) = self + .picker_size + .unwrap_or_else(|| window_size(&self.state, self.frame_aspect())); + let viewport = window.viewport_size(); + let scale = window.scale_factor().max(1.); + cap_gpui_kernels::camera_preview::quantized_target(( + width.max(f32::from(viewport.width)) * scale, + (height.max(f32::from(viewport.height)) - CAMERA_TOOLBAR_HEIGHT) * scale, + )) + } + fn frame_aspect(&self) -> Option { self.frame_dims .map(|(width, height)| width as f32 / height.max(1) as f32) diff --git a/apps/desktop-gpui/src/feeds.rs b/apps/desktop-gpui/src/feeds.rs index 17f195422ef..5028d9ffbfb 100644 --- a/apps/desktop-gpui/src/feeds.rs +++ b/apps/desktop-gpui/src/feeds.rs @@ -417,6 +417,22 @@ struct CameraPreviewWorkerConfig { mirrored: Arc, active: Arc, blur: Arc, + target: Arc, +} + +#[cfg(not(target_os = "macos"))] +fn pack_preview_target(target: Option<(usize, usize)>) -> u64 { + target.map_or(0, |(width, height)| ((width as u64) << 32) | height as u64) +} + +#[cfg(not(target_os = "macos"))] +fn unpack_preview_target(packed: u64) -> Option<(usize, usize)> { + (packed != 0).then(|| { + ( + (packed >> 32) as usize, + (packed & u64::from(u32::MAX)) as usize, + ) + }) } #[cfg(not(target_os = "macos"))] @@ -428,6 +444,7 @@ fn run_camera_preview_worker(config: CameraPreviewWorkerConfig) { mirrored, active, blur, + target, } = config; let mut scaler = None; let mut processor = None; @@ -550,7 +567,21 @@ fn run_camera_preview_worker(config: CameraPreviewWorkerConfig) { .publisher .fail("Requested camera blur is unavailable".to_string()); } - let max_dims = blur_mode.map(|_| crate::camera_blur_portable::MAX_DIMS); + let source_dims = (frame.inner.width() as usize, frame.inner.height() as usize); + let preview_target = unpack_preview_target(target.load(Ordering::Relaxed)); + // A processed recording lease records these frames; only the preview + // alone may be shrunk to the bubble. + #[cfg(target_os = "linux")] + let preview_target = preview_target.filter(|_| recording.is_none()); + let max_dims = match blur_mode { + Some(_) => Some(crate::camera_blur_portable::MAX_DIMS), + None => preview_target.map(|visible| { + let (width, height) = + cap_gpui_kernels::camera_preview::cover_dimensions(source_dims, visible); + (width as u32, height as u32) + }), + }; + let layout_dims = blur_mode.is_none().then_some(source_dims); let Some((mut image, dims)) = camera_preview_image(&frame.inner, &mut scaler, requested_mirror, max_dims) else { @@ -651,7 +682,7 @@ fn run_camera_preview_worker(config: CameraPreviewWorkerConfig) { if active.load(Ordering::Acquire) { match preview_tx.try_send(crate::camera_window::CameraPreviewFrame { image, - dims, + dims: layout_dims.unwrap_or(dims), timestamp: frame.timestamp, }) { Ok(()) | Err(flume::TrySendError::Full(_)) => {} @@ -704,6 +735,8 @@ pub struct Feeds { #[cfg(not(target_os = "macos"))] camera_preview_blur: Arc, #[cfg(not(target_os = "macos"))] + camera_preview_target: Arc, + #[cfg(not(target_os = "macos"))] camera_preview_reset: Option>, camera_input_epoch: Arc, #[cfg(target_os = "linux")] @@ -767,6 +800,8 @@ impl Feeds { #[cfg(not(target_os = "macos"))] camera_preview_blur: Arc::new(AtomicU8::new(0)), #[cfg(not(target_os = "macos"))] + camera_preview_target: Arc::new(AtomicU64::new(0)), + #[cfg(not(target_os = "macos"))] camera_preview_reset: None, camera_input_epoch: Arc::new(AtomicU64::new(0)), #[cfg(target_os = "linux")] @@ -865,6 +900,12 @@ impl Feeds { ); } + #[cfg(not(target_os = "macos"))] + pub fn set_camera_preview_target(&self, target: Option<(usize, usize)>) { + self.camera_preview_target + .store(pack_preview_target(target), Ordering::Relaxed); + } + /// Select (or deselect) the camera. Opens/closes the preview window and /// points the app-scoped feed at the device. pub fn set_camera(&mut self, selection: Option, cx: &mut Context) { @@ -1268,8 +1309,9 @@ impl Feeds { }); // The preview channel: bounded(4) so a stalled UI drops frames instead - // of ballooning; the pump drains on the main thread and hands each - // frame straight to the camera window. + // of ballooning; the pump drains on the main thread and hands only the + // newest frame to the camera window, releasing the stale capture + // buffers rather than converting them. #[cfg(target_os = "macos")] let pump = { let (frame_tx, frame_rx) = flume::bounded::(4); @@ -1277,6 +1319,7 @@ impl Feeds { cx.spawn(async move |_this, cx| { while let Ok(frame) = frame_rx.recv_async().await { + let frame = frame_rx.drain().last().unwrap_or(frame); cx.update(|cx| app_windows::deliver_camera_frame(frame, cx)); } }) @@ -1292,6 +1335,7 @@ impl Feeds { let mirrored = self.camera_preview_mirrored.clone(); let active = self.camera_preview_active.clone(); let blur = self.camera_preview_blur.clone(); + let target = self.camera_preview_target.clone(); if let Err(error) = std::thread::Builder::new() .name("camera-preview".into()) .stack_size(crate::MEDIA_THREAD_STACK_SIZE) @@ -1303,6 +1347,7 @@ impl Feeds { mirrored, active, blur, + target, }); }) { @@ -2152,6 +2197,97 @@ mod tests { camera_preview_image(&frame, &mut scaler, false, Some((16, 16))).unwrap(); assert_eq!(recapped, (16, 8)); } + + #[cfg(not(target_os = "macos"))] + #[test] + fn camera_preview_target_round_trips_through_its_atomic() { + for target in [None, Some((270, 270)), Some((2160, 1440)), Some((854, 480))] { + assert_eq!(unpack_preview_target(pack_preview_target(target)), target); + } + } + + fn synthetic_camera_frame( + format: ffmpeg::format::Pixel, + width: u32, + height: u32, + seed: usize, + ) -> ffmpeg::frame::Video { + let mut frame = ffmpeg::frame::Video::new(format, width, height); + for plane in 0..frame.planes() { + for (index, byte) in frame.data_mut(plane).iter_mut().enumerate() { + *byte = (16 + (index * 7 + seed * 31) % 220) as u8; + } + } + frame + } + + #[test] + #[ignore = "benchmark: cargo test --bin cap-gpui camera_preview_scale_benchmark -- --ignored --nocapture"] + fn camera_preview_scale_benchmark() { + const WARMUP: usize = 10; + const ITERATIONS: usize = 90; + let bubbles = [ + ("small 230pt round", (460.0f32, 460.0f32)), + ("medium 400pt round", (800.0, 800.0)), + ("large 600pt full", (600.0 * 16.0 / 9.0 * 2.0, 1200.0)), + ]; + println!( + "| format | source | mirrored | path | bubble | output | mean ms | p50 ms | p95 ms | MB/frame |" + ); + println!("|---|---|---|---|---|---|---|---|---|---|"); + for format in [ffmpeg::format::Pixel::NV12, ffmpeg::format::Pixel::YUYV422] { + for (width, height) in [(1280u32, 720u32), (1920, 1080), (3840, 2160)] { + let frames = (0..4) + .map(|seed| synthetic_camera_frame(format, width, height, seed)) + .collect::>(); + let source = (width as usize, height as usize); + let mut cases = vec![("main (full size)", "any", None)]; + for (label, physical) in bubbles { + let target = cap_gpui_kernels::camera_preview::quantized_target(physical).map( + |target| { + let (width, height) = + cap_gpui_kernels::camera_preview::cover_dimensions(source, target); + (width as u32, height as u32) + }, + ); + cases.push(("branch (bubble)", label, target)); + } + for mirrored in [false, true] { + for (path, bubble, max_dims) in &cases { + let mut scaler = None; + let mut samples = Vec::with_capacity(ITERATIONS); + let mut dims = (0, 0); + for iteration in 0..WARMUP + ITERATIONS { + let started = Instant::now(); + let (image, output) = camera_preview_image( + &frames[iteration % frames.len()], + &mut scaler, + mirrored, + *max_dims, + ) + .expect("synthetic frame converts"); + let elapsed = started.elapsed().as_secs_f64() * 1e3; + drop(image); + dims = output; + if iteration >= WARMUP { + samples.push(elapsed); + } + } + samples.sort_by(f64::total_cmp); + let mean = samples.iter().sum::() / samples.len() as f64; + let p50 = samples[samples.len() / 2]; + let p95 = samples[samples.len() * 95 / 100]; + println!( + "| {format:?} | {width}x{height} | {mirrored} | {path} | {bubble} | {}x{} | {mean:.2} | {p50:.2} | {p95:.2} | {:.2} |", + dims.0, + dims.1, + (dims.0 * dims.1 * 4) as f64 / 1_048_576.0, + ); + } + } + } + } + } } #[cfg(test)] diff --git a/apps/desktop-gpui/src/platform.rs b/apps/desktop-gpui/src/platform.rs index 6189355ea70..059d32a34c5 100644 --- a/apps/desktop-gpui/src/platform.rs +++ b/apps/desktop-gpui/src/platform.rs @@ -643,7 +643,7 @@ mod mac { use gpui::Window; use objc2::rc::Id; use objc2::runtime::{AnyObject, Sel}; - use objc2_app_kit::{NSView, NSWindow, NSWindowCollectionBehavior}; + use objc2_app_kit::{NSView, NSWindow, NSWindowCollectionBehavior, NSWindowOcclusionState}; use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use super::{ForcedAppearance, MaterialKind, PanelBehavior}; @@ -992,6 +992,17 @@ mod mac { } } + /// Whether AppKit reports the window visible: the `occlusionState` test + /// gpui's display link starts and stops on (with the shim above), so a + /// window failing it draws nothing until it passes again. + pub fn window_is_presenting(window: &Window) -> bool { + ns_window(window).is_none_or(|ns_window| { + ns_window + .occlusionState() + .contains(NSWindowOcclusionState::Visible) + }) + } + fn rerun_occlusion_handler(ns_window: &NSWindow) { unsafe { let _: () = objc2::msg_send![ns_window, windowDidChangeOcclusionState: std::ptr::null_mut::()]; From 4394baf074744197d017cd37bc3e125c6b32ef04 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:08:18 +0800 Subject: [PATCH 2/5] chore(desktop-gpui): benchmark bubble-sized camera preview conversion --- .../camera-preview-convert-benchmark.rs | 294 +++++++++++++++++- 1 file changed, 292 insertions(+), 2 deletions(-) diff --git a/apps/desktop-gpui/examples/camera-preview-convert-benchmark.rs b/apps/desktop-gpui/examples/camera-preview-convert-benchmark.rs index cff351041ee..ff971583eb2 100644 --- a/apps/desktop-gpui/examples/camera-preview-convert-benchmark.rs +++ b/apps/desktop-gpui/examples/camera-preview-convert-benchmark.rs @@ -11,7 +11,13 @@ //! Metal-compatible IOSurface-backed BGRA buffer from a fixed ring, ready for //! `paint_surface` with zero further CPU touches. //! -//! Run: cargo run -p cap-desktop-gpui --example camera-preview-convert-benchmark --release +//! `--bubble` instead measures the preview's conversion at full camera size +//! (what `main` converts every frame to) against the bubble-sized cover +//! target, for small/medium/large bubbles on a 2x display, mirrored and not: +//! per-frame time, bytes written per frame, the ring's resident memory, and +//! how close each looks once gpui's linear sampler draws it at bubble size. +//! +//! Run: cargo run -p cap-desktop-gpui --example camera-preview-convert-benchmark --release [-- --bubble] #[cfg(not(target_os = "macos"))] fn main() { @@ -20,7 +26,11 @@ fn main() { #[cfg(target_os = "macos")] fn main() { - macos::run(); + if std::env::args().any(|arg| arg == "--bubble") { + macos::bubble(); + } else { + macos::run(); + } } #[cfg(target_os = "macos")] @@ -249,4 +259,284 @@ mod macos { ); } } + + type CreateRotationSession = unsafe extern "C-unwind" fn( + Option<&cf::Allocator>, + *mut Option>, + ) -> cidre::os::Status; + type RotateImage = unsafe extern "C-unwind" fn( + &vt::PixelRotationSession, + &cv::PixelBuf, + &mut cv::PixelBuf, + ) -> cidre::os::Status; + + struct FlipSession { + session: arc::R, + rotate_image: RotateImage, + } + + impl FlipSession { + fn new() -> Self { + let create = unsafe { + libc::dlsym(libc::RTLD_DEFAULT, c"VTPixelRotationSessionCreate".as_ptr()) + }; + let rotate = unsafe { + libc::dlsym( + libc::RTLD_DEFAULT, + c"VTPixelRotationSessionRotateImage".as_ptr(), + ) + }; + let key = unsafe { + libc::dlsym( + libc::RTLD_DEFAULT, + c"kVTPixelRotationPropertyKey_FlipHorizontalOrientation".as_ptr(), + ) + }; + assert!(!create.is_null() && !rotate.is_null() && !key.is_null()); + let create = + unsafe { std::mem::transmute::<*mut libc::c_void, CreateRotationSession>(create) }; + let rotate_image = + unsafe { std::mem::transmute::<*mut libc::c_void, RotateImage>(rotate) }; + let key = unsafe { key.cast::<*const cf::String>().read().as_ref() } + .expect("flip property key"); + let mut session = None; + unsafe { create(None, &mut session).result() }.expect("rotation session"); + let mut session = session.expect("rotation session"); + session + .set_prop(key, Some(cf::Boolean::value_true().as_ref())) + .expect("flip property"); + Self { + session, + rotate_image, + } + } + + fn rotate(&self, source: &cv::PixelBuf, destination: &mut cv::PixelBuf) { + unsafe { (self.rotate_image)(&self.session, source, destination).result() } + .expect("flip"); + } + } + + const MAIN_RING: usize = 4; + const PREVIEW_RING: usize = 6; + const QUALITY_SEED: u8 = 200; + + struct Case { + dims: (usize, usize), + stats: (f64, f64, f64), + buffer_bytes: usize, + output: Vec, + } + + fn bgra_ring(count: usize, (width, height): (usize, usize)) -> Vec> { + let attrs = iosurface_bgra_attrs(); + (0..count) + .map(|_| { + let buf = cv::PixelBuf::new(width, height, cv::PixelFormat::_32_BGRA, Some(&attrs)) + .expect("IOSurface BGRA destination"); + assert!(buf.io_surf().is_some()); + buf + }) + .collect() + } + + fn run_case( + sources: &[arc::R], + quality_source: &cv::PixelBuf, + dims: (usize, usize), + mirrored: bool, + ) -> Case { + let mut session = vt::PixelTransferSession::new().expect("VT session"); + session.set_realtime(true).expect("VT realtime"); + let flip = mirrored.then(FlipSession::new); + let mut ring = bgra_ring(PREVIEW_RING, dims); + let scratch = flip.as_ref().map(|_| bgra_ring(1, dims).remove(0)); + let mut stats = Stats::new(); + let mut next = 0; + for iteration in 0..(WARMUP + ITERATIONS) { + let source = &sources[iteration % sources.len()]; + let out = &mut ring[next]; + let started = Instant::now(); + match (&flip, &scratch) { + (Some(flip), Some(scratch)) => { + session.transfer(source, scratch).expect("transfer"); + flip.rotate(scratch, out); + } + _ => session.transfer(source, out).expect("transfer"), + } + let elapsed = started.elapsed().as_secs_f64() * 1e6; + next = (next + 1) % PREVIEW_RING; + if iteration >= WARMUP { + stats.push(elapsed); + } + } + session + .transfer(quality_source, &ring[0]) + .expect("quality transfer"); + let buffer_bytes = { + let ptr = ring[0].as_ref() as *const cv::PixelBuf; + unsafe { CVPixelBufferGetBytesPerRow(ptr) * dims.1 } + }; + let output = read_bgra(&mut ring[0], dims.0, dims.1); + Case { + dims, + stats: stats.report(), + buffer_bytes, + output, + } + } + + fn make_quality_source(width: usize, height: usize) -> arc::R { + let mut buf = make_420v_source(width, height, QUALITY_SEED); + unsafe { + buf.lock_base_addr(cv::pixel_buffer::LockFlags::DEFAULT) + .result() + .expect("lock 420v source"); + let y_stride = buf.plane_bytes_per_row(0); + let y_base = buf.plane_base_address(0).cast_mut(); + for row in 0..height { + for col in 0..width { + let x = col as f64 / width as f64; + let y = row as f64 / height as f64; + let smooth = 0.5 + 0.25 * (x * 23.0).sin() * (y * 17.0).cos(); + let detail = 0.15 * ((x * 640.0).sin() * (y * 360.0).sin()); + let edge = if ((col * 8 / width) + (row * 6 / height)) % 2 == 0 { + 0.1 + } else { + -0.1 + }; + let value = (smooth + detail + edge).clamp(0.0, 1.0); + *y_base.add(row * y_stride + col) = (16.0 + value * 219.0) as u8; + } + } + buf.unlock_lock_base_addr(cv::pixel_buffer::LockFlags::DEFAULT); + } + buf + } + + fn area_average(pixels: &[u8], src: (usize, usize), dst: (usize, usize)) -> Vec { + let mut out = vec![0.0; dst.0 * dst.1 * 4]; + for y in 0..dst.1 { + let y0 = y * src.1 / dst.1; + let y1 = ((y + 1) * src.1 / dst.1).max(y0 + 1); + for x in 0..dst.0 { + let x0 = x * src.0 / dst.0; + let x1 = ((x + 1) * src.0 / dst.0).max(x0 + 1); + let mut sum = [0.0; 4]; + for sy in y0..y1 { + for sx in x0..x1 { + let offset = (sy * src.0 + sx) * 4; + for (channel, total) in sum.iter_mut().enumerate() { + *total += pixels[offset + channel] as f64; + } + } + } + let count = ((y1 - y0) * (x1 - x0)) as f64; + for (channel, total) in sum.iter().enumerate() { + out[(y * dst.0 + x) * 4 + channel] = total / count; + } + } + } + out + } + + fn linear_sample(pixels: &[u8], src: (usize, usize), dst: (usize, usize)) -> Vec { + let mut out = vec![0.0; dst.0 * dst.1 * 4]; + let texel = + |x: usize, y: usize, channel: usize| pixels[(y * src.0 + x) * 4 + channel] as f64; + for y in 0..dst.1 { + let v = ((y as f64 + 0.5) * src.1 as f64 / dst.1 as f64 - 0.5) + .clamp(0.0, (src.1 - 1) as f64); + let (y0, fy) = (v.floor() as usize, v.fract()); + let y1 = (y0 + 1).min(src.1 - 1); + for x in 0..dst.0 { + let u = ((x as f64 + 0.5) * src.0 as f64 / dst.0 as f64 - 0.5) + .clamp(0.0, (src.0 - 1) as f64); + let (x0, fx) = (u.floor() as usize, u.fract()); + let x1 = (x0 + 1).min(src.0 - 1); + for channel in 0..4 { + let top = texel(x0, y0, channel) * (1.0 - fx) + texel(x1, y0, channel) * fx; + let bottom = texel(x0, y1, channel) * (1.0 - fx) + texel(x1, y1, channel) * fx; + out[(y * dst.0 + x) * 4 + channel] = top * (1.0 - fy) + bottom * fy; + } + } + } + out + } + + fn psnr(reference: &[f64], candidate: &[f64]) -> f64 { + let mut squared = 0.0; + let mut count = 0usize; + for (pixel, other) in reference.chunks_exact(4).zip(candidate.chunks_exact(4)) { + for (a, b) in pixel[..3].iter().zip(&other[..3]) { + squared += (a - b).powi(2); + count += 1; + } + } + let mse = squared / count as f64; + if mse == 0.0 { + return f64::INFINITY; + } + 10.0 * (255.0f64.powi(2) / mse).log10() + } + + fn mb(bytes: usize) -> f64 { + bytes as f64 / 1_048_576.0 + } + + pub fn bubble() { + let bubbles = [ + ("small 230pt round", (460.0f32, 460.0f32)), + ("medium 400pt round", (800.0, 800.0)), + ("large 600pt full", (600.0 * 16.0 / 9.0 * 2.0, 1200.0)), + ]; + println!( + "| source | bubble | mirrored | path | output | mean us | p50 us | p95 us | MB written/frame | ring MB | PSNR at bubble size dB |" + ); + println!("|---|---|---|---|---|---|---|---|---|---|---|"); + for (width, height) in [(1280usize, 720usize), (1920, 1080), (3840, 2160)] { + let sources: Vec<_> = (0..SOURCE_RING) + .map(|seed| make_420v_source(width, height, seed as u8)) + .collect(); + let quality_source = make_quality_source(width, height); + for mirrored in [false, true] { + let passes = if mirrored { 2 } else { 1 }; + let scratch = usize::from(mirrored); + let full = run_case(&sources, &quality_source, (width, height), mirrored); + for (label, physical) in bubbles { + let displayed = cap_gpui_kernels::camera_preview::cover_dimensions( + (width, height), + (physical.0.ceil() as usize, physical.1.ceil() as usize), + ); + let target = cap_gpui_kernels::camera_preview::quantized_target(physical) + .map_or((width, height), |target| { + cap_gpui_kernels::camera_preview::cover_dimensions( + (width, height), + target, + ) + }); + let bubble = run_case(&sources, &quality_source, target, mirrored); + let reference = area_average(&full.output, full.dims, displayed); + for (path, case, slots) in [ + ("main (full size)", &full, MAIN_RING), + ("branch (bubble)", &bubble, PREVIEW_RING), + ] { + let (mean, p50, p95) = case.stats; + let shown = linear_sample(&case.output, case.dims, displayed); + println!( + "| {width}x{height} | {label} | {mirrored} | {path} | {}x{} | {mean:.1} | {p50:.1} | {p95:.1} | {:.2} | {:.1} | {:.2} |", + case.dims.0, + case.dims.1, + mb(case.buffer_bytes * passes), + mb(case.buffer_bytes * (slots + scratch)), + psnr(&reference, &shown), + ); + } + } + } + } + println!( + "ring MB: main keeps {MAIN_RING} slots (#2761 and this branch {PREVIEW_RING}), plus one scratch buffer when mirrored; PSNR compares gpui's linear sampling of each output at the displayed cover size against an area average of the full-size conversion." + ); + } } From 5a318b4085b38741c6da0a29bbbe59bfd46bf51e Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:24:51 +0800 Subject: [PATCH 3/5] perf(desktop-gpui): keep bubble-sized camera preview widths even for swscale --- .../kernels/src/camera_preview.rs | 20 ++++++---- apps/desktop-gpui/src/feeds.rs | 38 ++++++++++++++----- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/apps/desktop-gpui/kernels/src/camera_preview.rs b/apps/desktop-gpui/kernels/src/camera_preview.rs index 05c8eaf5997..652c012565e 100644 --- a/apps/desktop-gpui/kernels/src/camera_preview.rs +++ b/apps/desktop-gpui/kernels/src/camera_preview.rs @@ -21,10 +21,12 @@ pub fn cover_dimensions(source: (usize, usize), target: (usize, usize)) -> (usiz if scale >= 1.0 { return source; } - ( - ((width as f64 * scale).round() as usize).clamp(1, width), - ((height as f64 * scale).round() as usize).clamp(1, height), - ) + // Even sizes: swscale forces full-resolution chroma interpolation for an + // odd output width, which measured 2-4x slower than the even width beside + // it (camera_preview_scale_benchmark, Windows and Linux CI). + let even = + |extent: usize| ((extent as f64 * scale / 2.0).round() as usize * 2).clamp(2, extent); + (even(width), even(height)) } #[cfg(test)] @@ -57,11 +59,12 @@ mod tests { #[test] fn cover_dimensions_fill_the_target_and_keep_the_source_aspect() { - assert_eq!(cover_dimensions((1920, 1080), (480, 480)), (853, 480)); - assert_eq!(cover_dimensions((3840, 2160), (480, 480)), (853, 480)); + assert_eq!(cover_dimensions((1920, 1080), (480, 480)), (854, 480)); + assert_eq!(cover_dimensions((3840, 2160), (480, 480)), (854, 480)); + assert_eq!(cover_dimensions((1920, 1080), (960, 960)), (1706, 960)); assert_eq!(cover_dimensions((1920, 1080), (960, 540)), (960, 540)); assert_eq!(cover_dimensions((1440, 1080), (960, 540)), (960, 720)); - assert_eq!(cover_dimensions((1080, 1920), (480, 480)), (480, 853)); + assert_eq!(cover_dimensions((1080, 1920), (480, 480)), (480, 854)); for source in [ (1280, 720), (1920, 1080), @@ -71,6 +74,9 @@ mod tests { ] { for target in [(270, 270), (480, 480), (960, 540), (1080, 1080)] { let (width, height) = cover_dimensions(source, target); + if (width, height) != source { + assert_eq!((width % 2, height % 2), (0, 0)); + } assert!(width >= target.0.min(source.0)); assert!(height >= target.1.min(source.1)); let aspect = source.0 as f64 / source.1 as f64; diff --git a/apps/desktop-gpui/src/feeds.rs b/apps/desktop-gpui/src/feeds.rs index 5028d9ffbfb..2f1114cbb04 100644 --- a/apps/desktop-gpui/src/feeds.rs +++ b/apps/desktop-gpui/src/feeds.rs @@ -573,8 +573,12 @@ fn run_camera_preview_worker(config: CameraPreviewWorkerConfig) { // alone may be shrunk to the bubble. #[cfg(target_os = "linux")] let preview_target = preview_target.filter(|_| recording.is_none()); - let max_dims = match blur_mode { - Some(_) => Some(crate::camera_blur_portable::MAX_DIMS), + let output = match blur_mode { + Some(_) => crate::camera_blur_portable::fitted_dimensions( + frame.inner.width(), + frame.inner.height(), + crate::camera_blur_portable::MAX_DIMS, + ), None => preview_target.map(|visible| { let (width, height) = cap_gpui_kernels::camera_preview::cover_dimensions(source_dims, visible); @@ -583,7 +587,7 @@ fn run_camera_preview_worker(config: CameraPreviewWorkerConfig) { }; let layout_dims = blur_mode.is_none().then_some(source_dims); let Some((mut image, dims)) = - camera_preview_image(&frame.inner, &mut scaler, requested_mirror, max_dims) + camera_preview_image_at(&frame.inner, &mut scaler, requested_mirror, output) else { #[cfg(target_os = "linux")] if let Some(recording) = &recording { @@ -1438,19 +1442,35 @@ impl Feeds { } } -#[cfg(any(not(target_os = "macos"), test))] +#[cfg(test)] pub(crate) fn camera_preview_image( frame: &ffmpeg::frame::Video, scaler: &mut Option, mirrored: bool, max_dims: Option<(u32, u32)>, +) -> Option<(Arc, (usize, usize))> { + let output = match max_dims { + Some(max) => Some(crate::camera_blur_portable::fitted_dimensions( + frame.width(), + frame.height(), + max, + )?), + None => None, + }; + camera_preview_image_at(frame, scaler, mirrored, output) +} + +#[cfg(any(not(target_os = "macos"), test))] +pub(crate) fn camera_preview_image_at( + frame: &ffmpeg::frame::Video, + scaler: &mut Option, + mirrored: bool, + output: Option<(u32, u32)>, ) -> Option<(Arc, (usize, usize))> { let source_width = frame.width(); let source_height = frame.height(); - let (width, height) = match max_dims { - Some(max) => { - crate::camera_blur_portable::fitted_dimensions(source_width, source_height, max)? - } + let (width, height) = match output { + Some(output) => output, None if source_width > 0 && source_height > 0 => (source_width, source_height), None => return None, }; @@ -2259,7 +2279,7 @@ mod tests { let mut dims = (0, 0); for iteration in 0..WARMUP + ITERATIONS { let started = Instant::now(); - let (image, output) = camera_preview_image( + let (image, output) = camera_preview_image_at( &frames[iteration % frames.len()], &mut scaler, mirrored, From 5a026710a00fa78429119e4badfdeab53411834c Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:38:05 +0800 Subject: [PATCH 4/5] chore(desktop-gpui): report process CPU time in the camera preview bubble benchmark --- .../camera-preview-convert-benchmark.rs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/desktop-gpui/examples/camera-preview-convert-benchmark.rs b/apps/desktop-gpui/examples/camera-preview-convert-benchmark.rs index ff971583eb2..989b7f38299 100644 --- a/apps/desktop-gpui/examples/camera-preview-convert-benchmark.rs +++ b/apps/desktop-gpui/examples/camera-preview-convert-benchmark.rs @@ -324,6 +324,7 @@ mod macos { struct Case { dims: (usize, usize), stats: (f64, f64, f64), + cpu_us: f64, buffer_bytes: usize, output: Vec, } @@ -340,6 +341,13 @@ mod macos { .collect() } + fn process_cpu_us() -> f64 { + let mut usage = unsafe { std::mem::zeroed::() }; + unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) }; + let micros = |time: libc::timeval| time.tv_sec as f64 * 1e6 + time.tv_usec as f64; + micros(usage.ru_utime) + micros(usage.ru_stime) + } + fn run_case( sources: &[arc::R], quality_source: &cv::PixelBuf, @@ -353,7 +361,11 @@ mod macos { let scratch = flip.as_ref().map(|_| bgra_ring(1, dims).remove(0)); let mut stats = Stats::new(); let mut next = 0; + let mut cpu_started = 0.0; for iteration in 0..(WARMUP + ITERATIONS) { + if iteration == WARMUP { + cpu_started = process_cpu_us(); + } let source = &sources[iteration % sources.len()]; let out = &mut ring[next]; let started = Instant::now(); @@ -370,6 +382,7 @@ mod macos { stats.push(elapsed); } } + let cpu_us = (process_cpu_us() - cpu_started) / ITERATIONS as f64; session .transfer(quality_source, &ring[0]) .expect("quality transfer"); @@ -381,6 +394,7 @@ mod macos { Case { dims, stats: stats.report(), + cpu_us, buffer_bytes, output, } @@ -491,9 +505,9 @@ mod macos { ("large 600pt full", (600.0 * 16.0 / 9.0 * 2.0, 1200.0)), ]; println!( - "| source | bubble | mirrored | path | output | mean us | p50 us | p95 us | MB written/frame | ring MB | PSNR at bubble size dB |" + "| source | bubble | mirrored | path | output | mean us | p50 us | p95 us | process CPU us | MB written/frame | ring MB | PSNR at bubble size dB |" ); - println!("|---|---|---|---|---|---|---|---|---|---|---|"); + println!("|---|---|---|---|---|---|---|---|---|---|---|---|"); for (width, height) in [(1280usize, 720usize), (1920, 1080), (3840, 2160)] { let sources: Vec<_> = (0..SOURCE_RING) .map(|seed| make_420v_source(width, height, seed as u8)) @@ -524,9 +538,10 @@ mod macos { let (mean, p50, p95) = case.stats; let shown = linear_sample(&case.output, case.dims, displayed); println!( - "| {width}x{height} | {label} | {mirrored} | {path} | {}x{} | {mean:.1} | {p50:.1} | {p95:.1} | {:.2} | {:.1} | {:.2} |", + "| {width}x{height} | {label} | {mirrored} | {path} | {}x{} | {mean:.1} | {p50:.1} | {p95:.1} | {:.1} | {:.2} | {:.1} | {:.2} |", case.dims.0, case.dims.1, + case.cpu_us, mb(case.buffer_bytes * passes), mb(case.buffer_bytes * (slots + scratch)), psnr(&reference, &shown), From a79a7457df519d9a93e03442e91f4d3bdcb04384 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:45:03 +0800 Subject: [PATCH 5/5] perf(desktop-gpui): keep the camera preview at full size when shrinking it saves little --- apps/desktop-gpui/kernels/src/camera_preview.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/desktop-gpui/kernels/src/camera_preview.rs b/apps/desktop-gpui/kernels/src/camera_preview.rs index 652c012565e..70fa434d8ee 100644 --- a/apps/desktop-gpui/kernels/src/camera_preview.rs +++ b/apps/desktop-gpui/kernels/src/camera_preview.rs @@ -26,7 +26,14 @@ pub fn cover_dimensions(source: (usize, usize), target: (usize, usize)) -> (usiz // it (camera_preview_scale_benchmark, Windows and Linux CI). let even = |extent: usize| ((extent as f64 * scale / 2.0).round() as usize * 2).clamp(2, extent); - (even(width), even(height)) + let scaled = (even(width), even(height)); + // A scaled conversion leaves the converters' unscaled fast paths, so a + // frame that keeps most of its pixels costs more to shrink than it saves + // (1080p to 1706x960 measured slower in swscale and VideoToolbox alike). + if scaled.0 * scaled.1 * 2 > width * height { + return source; + } + scaled } #[cfg(test)] @@ -61,7 +68,9 @@ mod tests { fn cover_dimensions_fill_the_target_and_keep_the_source_aspect() { assert_eq!(cover_dimensions((1920, 1080), (480, 480)), (854, 480)); assert_eq!(cover_dimensions((3840, 2160), (480, 480)), (854, 480)); - assert_eq!(cover_dimensions((1920, 1080), (960, 960)), (1706, 960)); + assert_eq!(cover_dimensions((1920, 1080), (960, 960)), (1920, 1080)); + assert_eq!(cover_dimensions((3840, 2160), (960, 960)), (1706, 960)); + assert_eq!(cover_dimensions((3840, 2160), (2160, 1440)), (2560, 1440)); assert_eq!(cover_dimensions((1920, 1080), (960, 540)), (960, 540)); assert_eq!(cover_dimensions((1440, 1080), (960, 540)), (960, 720)); assert_eq!(cover_dimensions((1080, 1920), (480, 480)), (480, 854));