@@ -384,10 +384,21 @@ mod frame {
384384 pub struct Converted {
385385 pub buffer : arc:: R < cv:: PixelBuf > ,
386386 pub dims : ( usize , usize ) ,
387+ /// What the window lays itself out for: the camera's own size when
388+ /// the frame was only shrunk to the bubble, so the rounding of a
389+ /// cover target never nudges the window's aspect.
390+ pub layout_dims : ( usize , usize ) ,
387391 pub generation : u64 ,
388392 pub mirrored : bool ,
389393 }
390394
395+ #[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
396+ pub enum Target {
397+ Source ,
398+ Fit ( ( usize , usize ) ) ,
399+ Cover ( ( usize , usize ) ) ,
400+ }
401+
391402 // Handed from the conversion thread to the main thread once written; the
392403 // buffer is only read after that, and CF refcounts are atomic.
393404 unsafe impl Send for Converted { }
@@ -518,21 +529,32 @@ mod frame {
518529 }
519530
520531 /// The frame as a BGRA IOSurface-backed pixel buffer, downscaled to
521- /// fit `max_dims` when given and horizontally flipped when
522- /// `mirrored`. Recreates the rings when the camera's frame size, the
523- /// target size or the mirror toggle changes, or when `force_rebuild`
524- /// asks for a fresh rotation session (keeping the generation count
525- /// rising so no earlier ring can alias a new one).
532+ /// fit inside a maximum size or to just cover the bubble per
533+ /// `target`, and horizontally flipped when `mirrored`. Recreates the
534+ /// rings when the camera's frame size, the target size or the mirror
535+ /// toggle changes, or when `force_rebuild` asks for a fresh rotation
536+ /// session (keeping the generation count rising so no earlier ring
537+ /// can alias a new one).
526538 pub fn convert (
527539 this : & mut Option < Self > ,
528540 frame : & cap_recording:: NativeCameraFrame ,
529- max_dims : Option < ( usize , usize ) > ,
541+ target : Target ,
530542 mirrored : bool ,
531543 force_rebuild : bool ,
532544 ) -> Option < Converted > {
533545 let src = frame. sample_buf . image_buf ( ) ?;
534546 let src_dims = ( src. width ( ) , src. height ( ) ) ;
535- let dst_dims = max_dims. map_or ( src_dims, |max| fit_within ( src_dims, max) ) ;
547+ let dst_dims = match target {
548+ Target :: Source => src_dims,
549+ Target :: Fit ( max) => fit_within ( src_dims, max) ,
550+ Target :: Cover ( visible) => {
551+ cap_gpui_kernels:: camera_preview:: cover_dimensions ( src_dims, visible)
552+ }
553+ } ;
554+ let layout_dims = match target {
555+ Target :: Cover ( _) => src_dims,
556+ Target :: Source | Target :: Fit ( _) => dst_dims,
557+ } ;
536558 if force_rebuild
537559 || this. as_ref ( ) . is_none_or ( |converter| {
538560 converter. src_dims != src_dims
@@ -566,6 +588,7 @@ mod frame {
566588 Some ( Converted {
567589 buffer : out,
568590 dims : converter. dst_dims ,
591+ layout_dims,
569592 generation : converter. generation ,
570593 mirrored : converter. flip_session . is_some ( ) ,
571594 } )
@@ -1460,16 +1483,32 @@ struct ConvertJob {
14601483struct ConvertSettings {
14611484 epoch : u64 ,
14621485 blur_mode : Option < cap_camera_effects:: BlurMode > ,
1486+ preview_target : Option < ( usize , usize ) > ,
14631487 mirrored : bool ,
14641488 rebuilds : u64 ,
14651489}
14661490
1491+ #[ cfg( target_os = "macos" ) ]
1492+ impl ConvertSettings {
1493+ fn target ( & self ) -> frame:: Target {
1494+ match ( self . blur_mode , self . preview_target ) {
1495+ ( Some ( _) , _) => frame:: Target :: Fit ( camera_blur:: BLUR_MAX_DIMS ) ,
1496+ ( None , Some ( visible) ) => frame:: Target :: Cover ( visible) ,
1497+ ( None , None ) => frame:: Target :: Source ,
1498+ }
1499+ }
1500+ }
1501+
14671502/// The `camera-preview-convert` thread. The VideoToolbox transfer (and the
14681503/// mirror pass) blocks until the hardware finishes, ~4-5 ms per frame, which
14691504/// stalled every main-thread frame while it ran on the main thread.
14701505#[ cfg( target_os = "macos" ) ]
14711506struct ConvertBridge {
14721507 tx : flume:: Sender < ConvertJob > ,
1508+ /// The thread's own queue, so a newer frame can evict one still waiting
1509+ /// there. `None` when the thread could not be spawned: holding a receiver
1510+ /// would keep the channel connected and hide the inline fallback.
1511+ queued : Option < flume:: Receiver < ConvertJob > > ,
14731512 _pump : gpui:: Task < ( ) > ,
14741513}
14751514
@@ -1488,11 +1527,10 @@ fn run_preview_conversion(
14881527 let settings = job. settings ;
14891528 let force_rebuild = rebuilds. is_some_and ( |rebuilds| rebuilds != settings. rebuilds ) ;
14901529 rebuilds = Some ( settings. rebuilds ) ;
1491- let max_dims = settings. blur_mode . map ( |_| camera_blur:: BLUR_MAX_DIMS ) ;
14921530 let result = frame:: FrameConverter :: convert (
14931531 & mut converter,
14941532 & job. frame ,
1495- max_dims ,
1533+ settings . target ( ) ,
14961534 settings. mirrored ,
14971535 force_rebuild,
14981536 ) ;
@@ -1852,34 +1890,18 @@ impl CameraWindow {
18521890 if failures_before != self . effect_failures {
18531891 self . sync_effect_feedback ( cx) ;
18541892 }
1855- let job = ConvertJob {
1856- frame,
1857- settings : self . convert_settings ( epoch) ,
1858- } ;
1859- match self . ensure_convert_bridge ( window, cx) . tx . try_send ( job) {
1860- // A full queue means the thread is mid-conversion; it picks up
1861- // the next frame.
1862- Ok ( ( ) ) | Err ( flume:: TrySendError :: Full ( _) ) => { }
1863- Err ( flume:: TrySendError :: Disconnected ( job) ) => {
1864- let settings = job. settings ;
1865- let max_dims = settings. blur_mode . map ( |_| camera_blur:: BLUR_MAX_DIMS ) ;
1866- let force_rebuild = self
1867- . fallback_rebuilds
1868- . is_some_and ( |rebuilds| rebuilds != settings. rebuilds ) ;
1869- self . fallback_rebuilds = Some ( settings. rebuilds ) ;
1870- let converted = frame:: FrameConverter :: convert (
1871- & mut self . fallback_converter ,
1872- & job. frame ,
1873- max_dims,
1874- settings. mirrored ,
1875- force_rebuild,
1876- ) ;
1877- self . converted_arrived ( converted, settings, window, cx) ;
1878- }
1893+ // The same occlusion test gpui's display link runs on: a window
1894+ // failing it presents nothing, so converting (or blurring) for it
1895+ // is wasted. The next frame after it is shown converts again.
1896+ if self . inline || platform:: window_is_presenting ( window) {
1897+ self . queue_conversion ( frame, epoch, window, cx) ;
18791898 }
18801899 }
18811900 #[ cfg( not( target_os = "macos" ) ) ]
18821901 {
1902+ Feeds :: global ( cx)
1903+ . read ( cx)
1904+ . set_camera_preview_target ( self . preview_target ( window) ) ;
18831905 let first_frame = self . frame_dims . is_none ( ) ;
18841906 let dims_changed = self . frame_dims != Some ( frame. dims ) ;
18851907 self . frame_dims = Some ( frame. dims ) ;
@@ -1945,10 +1967,57 @@ impl CameraWindow {
19451967 }
19461968
19471969 #[ cfg( target_os = "macos" ) ]
1948- fn convert_settings ( & self , epoch : u64 ) -> ConvertSettings {
1970+ fn queue_conversion (
1971+ & mut self ,
1972+ frame : cap_recording:: NativeCameraFrame ,
1973+ epoch : u64 ,
1974+ window : & mut Window ,
1975+ cx : & mut Context < Self > ,
1976+ ) {
1977+ let job = ConvertJob {
1978+ frame,
1979+ settings : self . convert_settings ( epoch, window) ,
1980+ } ;
1981+ let bridge = self . ensure_convert_bridge ( window, cx) ;
1982+ let sent = match bridge. tx . try_send ( job) {
1983+ Err ( flume:: TrySendError :: Full ( job) ) => {
1984+ if let Some ( queued) = & bridge. queued {
1985+ let _ = queued. try_recv ( ) ;
1986+ }
1987+ bridge. tx . try_send ( job)
1988+ }
1989+ sent => sent,
1990+ } ;
1991+ match sent {
1992+ Ok ( ( ) ) | Err ( flume:: TrySendError :: Full ( _) ) => { }
1993+ Err ( flume:: TrySendError :: Disconnected ( job) ) => {
1994+ let settings = job. settings ;
1995+ let force_rebuild = self
1996+ . fallback_rebuilds
1997+ . is_some_and ( |rebuilds| rebuilds != settings. rebuilds ) ;
1998+ self . fallback_rebuilds = Some ( settings. rebuilds ) ;
1999+ let converted = frame:: FrameConverter :: convert (
2000+ & mut self . fallback_converter ,
2001+ & job. frame ,
2002+ settings. target ( ) ,
2003+ settings. mirrored ,
2004+ force_rebuild,
2005+ ) ;
2006+ self . converted_arrived ( converted, settings, window, cx) ;
2007+ }
2008+ }
2009+ }
2010+
2011+ #[ cfg( target_os = "macos" ) ]
2012+ fn convert_settings ( & self , epoch : u64 , window : & Window ) -> ConvertSettings {
2013+ let blur_mode = self . active_blur_mode ( ) ;
19492014 ConvertSettings {
19502015 epoch,
1951- blur_mode : self . active_blur_mode ( ) ,
2016+ blur_mode,
2017+ preview_target : blur_mode
2018+ . is_none ( )
2019+ . then ( || self . preview_target ( window) )
2020+ . flatten ( ) ,
19522021 mirrored : self . state . mirrored ,
19532022 rebuilds : self . convert_rebuilds ,
19542023 }
@@ -1965,7 +2034,7 @@ impl CameraWindow {
19652034 let Some ( epoch) = Feeds :: global ( cx) . read ( cx) . camera_preview_epoch ( ) else {
19662035 return ;
19672036 } ;
1968- if settings != self . convert_settings ( epoch) {
2037+ if settings != self . convert_settings ( epoch, window ) {
19692038 return ;
19702039 }
19712040 let blur_mode = settings. blur_mode ;
@@ -1977,8 +2046,8 @@ impl CameraWindow {
19772046 self . effect_failures . mirror = self . state . mirrored && !converted. mirrored ;
19782047 let first_frame = self . frame_dims . is_none ( ) ;
19792048 let dims = converted. dims ;
1980- let dims_changed = self . frame_dims != Some ( dims ) ;
1981- self . frame_dims = Some ( dims ) ;
2049+ let dims_changed = self . frame_dims != Some ( converted . layout_dims ) ;
2050+ self . frame_dims = Some ( converted . layout_dims ) ;
19822051
19832052 let mut paint_raw = blur_mode. is_none ( ) ;
19842053 if let Some ( mode) = blur_mode {
@@ -2036,16 +2105,21 @@ impl CameraWindow {
20362105 fn ensure_convert_bridge ( & mut self , window : & Window , cx : & mut Context < Self > ) -> & ConvertBridge {
20372106 if self . convert . is_none ( ) {
20382107 let ( job_tx, job_rx) = flume:: bounded :: < ConvertJob > ( 1 ) ;
2108+ let queued = job_rx. clone ( ) ;
20392109 let ( out_tx, out_rx) = flume:: bounded ( 1 ) ;
2040- if let Err ( error ) = std:: thread:: Builder :: new ( )
2110+ let queued = match std:: thread:: Builder :: new ( )
20412111 . name ( "camera-preview-convert" . into ( ) )
20422112 . stack_size ( crate :: MEDIA_THREAD_STACK_SIZE )
20432113 . spawn ( move || run_preview_conversion ( job_rx, out_tx) )
20442114 {
2045- // job_rx is gone; the caller's try_send sees Disconnected and
2046- // converts inline.
2047- tracing:: warn!( "camera preview conversion thread failed to spawn: {error}" ) ;
2048- }
2115+ Ok ( _) => Some ( queued) ,
2116+ Err ( error) => {
2117+ // job_rx is gone; the caller's try_send sees Disconnected
2118+ // and converts inline.
2119+ tracing:: warn!( "camera preview conversion thread failed to spawn: {error}" ) ;
2120+ None
2121+ }
2122+ } ;
20492123 let pump = cx. spawn_in ( window, async move |this, cx| {
20502124 while let Ok ( ( converted, settings) ) = out_rx. recv_async ( ) . await {
20512125 if this
@@ -2060,6 +2134,7 @@ impl CameraWindow {
20602134 } ) ;
20612135 self . convert = Some ( ConvertBridge {
20622136 tx : job_tx,
2137+ queued,
20632138 _pump : pump,
20642139 } ) ;
20652140 }
@@ -2156,6 +2231,27 @@ impl CameraWindow {
21562231 first
21572232 }
21582233
2234+ /// The bubble in physical pixels, rounded up to a fixed step so resizing
2235+ /// rebuilds the conversion rings only at step boundaries: the size camera
2236+ /// frames are shrunk to cover. The larger of the requested and the
2237+ /// current window size, so a frame converted mid-resize still covers
2238+ /// either. `None` (full size) while the target overlays draw the preview
2239+ /// inline, possibly on displays of another scale.
2240+ fn preview_target ( & self , window : & Window ) -> Option < ( usize , usize ) > {
2241+ if self . inline {
2242+ return None ;
2243+ }
2244+ let ( width, height) = self
2245+ . picker_size
2246+ . unwrap_or_else ( || window_size ( & self . state , self . frame_aspect ( ) ) ) ;
2247+ let viewport = window. viewport_size ( ) ;
2248+ let scale = window. scale_factor ( ) . max ( 1. ) ;
2249+ cap_gpui_kernels:: camera_preview:: quantized_target ( (
2250+ width. max ( f32:: from ( viewport. width ) ) * scale,
2251+ ( height. max ( f32:: from ( viewport. height ) ) - CAMERA_TOOLBAR_HEIGHT ) * scale,
2252+ ) )
2253+ }
2254+
21592255 fn frame_aspect ( & self ) -> Option < f32 > {
21602256 self . frame_dims
21612257 . map ( |( width, height) | width as f32 / height. max ( 1 ) as f32 )
0 commit comments