diff --git a/apps/rsnap/src/app.rs b/apps/rsnap/src/app.rs index 13690978..cd944b3e 100644 --- a/apps/rsnap/src/app.rs +++ b/apps/rsnap/src/app.rs @@ -10,6 +10,8 @@ use std::sync::{ Arc, atomic::{AtomicBool, AtomicU64, Ordering}, }; +#[cfg(target_os = "macos")] +use std::time::Instant; use color_eyre::eyre::Result; use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager, hotkey::HotKey}; @@ -69,6 +71,8 @@ struct App { #[cfg(target_os = "macos")] menubar_quit_menu_id: Option, overlay_session: Option, + #[cfg(target_os = "macos")] + prewarmed_overlay_session: Option, settings_window: Option, settings_window_capture_window_id: Option, settings: AppSettings, @@ -87,6 +91,10 @@ struct App { #[cfg(target_os = "macos")] overlay_session_generation: u64, #[cfg(target_os = "macos")] + overlay_session_prewarm_requested: bool, + #[cfg(target_os = "macos")] + overlay_session_prewarm_retry_not_before: Option, + #[cfg(target_os = "macos")] startup_permissions_checked: bool, } impl App { @@ -124,6 +132,8 @@ impl App { #[cfg(target_os = "macos")] menubar_quit_menu_id: None, overlay_session: None, + #[cfg(target_os = "macos")] + prewarmed_overlay_session: None, settings_window: None, settings_window_capture_window_id: None, settings, @@ -142,6 +152,10 @@ impl App { #[cfg(target_os = "macos")] overlay_session_generation: 0, #[cfg(target_os = "macos")] + overlay_session_prewarm_requested: true, + #[cfg(target_os = "macos")] + overlay_session_prewarm_retry_not_before: None, + #[cfg(target_os = "macos")] startup_permissions_checked: false, } } diff --git a/apps/rsnap/src/app/capture.rs b/apps/rsnap/src/app/capture.rs index a3604b5a..7860d817 100644 --- a/apps/rsnap/src/app/capture.rs +++ b/apps/rsnap/src/app/capture.rs @@ -31,6 +31,8 @@ use rsnap_overlay::{HudAnchor, OverlayConfig, OverlayControl, OverlayExit, Overl #[cfg(target_os = "macos")] const SCROLL_INPUT_OBSERVER_READY_TIMEOUT: Duration = Duration::from_millis(250); +#[cfg(target_os = "macos")] +const OVERLAY_SESSION_PREWARM_RETRY_BACKOFF: Duration = Duration::from_secs(1); impl App { fn self_capture_exception_window_ids(&self) -> Vec { @@ -87,11 +89,14 @@ impl App { pub(super) fn apply_overlay_settings(&mut self) { let config = self.overlay_config(); - let Some(session) = self.overlay_session.as_mut() else { - return; - }; - session.set_config(config); + if let Some(session) = self.overlay_session.as_mut() { + session.set_config(config.clone()); + } + #[cfg(target_os = "macos")] + if let Some(session) = self.prewarmed_overlay_session.as_mut() { + session.set_config(config); + } } pub(super) fn start_capture_session( @@ -115,15 +120,22 @@ impl App { else { return; }; - let (overlay_session_build_ms, mut overlay_session) = { + let (overlay_session_source, overlay_session_build_ms, mut overlay_session) = { let overlay_session_build_started_at = Instant::now(); - let overlay_session = OverlaySession::with_config(self.overlay_config()); - - (overlay_session_build_started_at.elapsed().as_millis(), overlay_session) + let (overlay_session_source, overlay_session) = + self.take_overlay_session_for_capture_start(); + + ( + overlay_session_source, + overlay_session_build_started_at.elapsed().as_millis(), + overlay_session, + ) }; #[cfg(target_os = "macos")] { + self.overlay_session_prewarm_requested = false; + self.overlay_session_prewarm_retry_not_before = None; self.overlay_session_generation = self.overlay_session_generation.wrapping_add(1); self.pending_deferred_ocr_generation @@ -153,6 +165,7 @@ impl App { op = "capture.start_phase_timing", requested_by = %requested_by, result = "started", + overlay_session_source, hotkey = %self.capture_key_label(), overlay_session_build_ms, hook_wiring_ms, @@ -187,6 +200,7 @@ impl App { error = %err, requested_by = %requested_by, result = "error", + overlay_session_source, overlay_session_build_ms, hook_wiring_ms, overlay_start_ms, @@ -194,7 +208,74 @@ impl App { screen_recording_preflight_ms, scroll_input_reset_ms, "Failed to start overlay session." - ) + ); + + #[cfg(target_os = "macos")] + { + self.overlay_session_prewarm_requested = true; + } + }, + } + } + + #[cfg(target_os = "macos")] + fn take_overlay_session_for_capture_start(&mut self) -> (&'static str, OverlaySession) { + if let Some(overlay_session) = self.prewarmed_overlay_session.take() { + ("prewarmed", overlay_session) + } else { + ("fresh", OverlaySession::with_config(self.overlay_config())) + } + } + + #[cfg(not(target_os = "macos"))] + fn take_overlay_session_for_capture_start(&mut self) -> (&'static str, OverlaySession) { + ("fresh", OverlaySession::with_config(self.overlay_config())) + } + + #[cfg(target_os = "macos")] + pub(super) fn maybe_prewarm_overlay_session(&mut self, event_loop: &ActiveEventLoop) { + if !self.overlay_session_prewarm_requested + || self.overlay_session.is_some() + || self.prewarmed_overlay_session.is_some() + { + return; + } + if self + .overlay_session_prewarm_retry_not_before + .is_some_and(|not_before| Instant::now() < not_before) + { + return; + } + + let prewarm_started_at = Instant::now(); + let mut overlay_session = OverlaySession::with_config(self.overlay_config()); + + match overlay_session.prewarm(event_loop) { + Ok(()) => { + self.prewarmed_overlay_session = Some(overlay_session); + self.overlay_session_prewarm_requested = false; + self.overlay_session_prewarm_retry_not_before = None; + + tracing::info!( + op = "capture.prewarm_phase_timing", + result = "prewarmed", + total_ms = prewarm_started_at.elapsed().as_millis(), + "Capture startup resources prewarmed." + ); + }, + Err(err) => { + self.overlay_session_prewarm_requested = true; + self.overlay_session_prewarm_retry_not_before = + Some(Instant::now() + OVERLAY_SESSION_PREWARM_RETRY_BACKOFF); + + tracing::warn!( + op = "capture.prewarm_phase_timing", + error = %err, + result = "error", + retry_backoff_ms = OVERLAY_SESSION_PREWARM_RETRY_BACKOFF.as_millis(), + total_ms = prewarm_started_at.elapsed().as_millis(), + "Failed to prewarm capture startup resources." + ); }, } } @@ -340,6 +421,10 @@ impl App { #[cfg(target_os = "macos")] { + self.prewarmed_overlay_session = None; + self.overlay_session_prewarm_requested = true; + self.overlay_session_prewarm_retry_not_before = None; + self.scroll_input_shared_state.set_enabled(false); self.scroll_input_shared_state.set_event_waker(None); self.scroll_input_shared_state.clear(); diff --git a/apps/rsnap/src/app/runtime.rs b/apps/rsnap/src/app/runtime.rs index 8c342c38..db65ae36 100644 --- a/apps/rsnap/src/app/runtime.rs +++ b/apps/rsnap/src/app/runtime.rs @@ -34,6 +34,8 @@ impl ApplicationHandler for App { self.install_tray(event_loop); #[cfg(target_os = "macos")] self.maybe_present_startup_permissions(event_loop); + #[cfg(target_os = "macos")] + self.maybe_prewarm_overlay_session(event_loop); } fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { @@ -219,6 +221,9 @@ impl ApplicationHandler for App { self.handle_overlay_control(control); } + + #[cfg(target_os = "macos")] + self.maybe_prewarm_overlay_session(event_loop); } } diff --git a/packages/rsnap-overlay/src/live_frame_stream_macos.rs b/packages/rsnap-overlay/src/live_frame_stream_macos.rs index 59150154..c2ec5a50 100644 --- a/packages/rsnap-overlay/src/live_frame_stream_macos.rs +++ b/packages/rsnap-overlay/src/live_frame_stream_macos.rs @@ -456,12 +456,39 @@ impl MacLiveFrameStream { } pub(crate) fn prime_monitor_nonblocking(&self, monitor: MonitorRect) { + #[cfg(test)] + self.record_debug_request_kind("prime_monitor_nonblocking"); + if !self.shared_latest_frame.begin_ensure_monitor(monitor.id) { return; } - if self.request_tx.send(WorkerRequest::EnsureMonitor { monitor }).is_err() { + if self + .request_tx + .send(WorkerRequest::EnsureMonitor { monitor, force_retry_upgrade: false }) + .is_err() + { + self.shared_latest_frame.finish_ensure_monitor(monitor.id); + } + } + + pub(crate) fn upgrade_monitor_nonblocking(&self, monitor: MonitorRect) -> bool { + #[cfg(test)] + self.record_debug_request_kind("upgrade_monitor_nonblocking"); + + if !self.shared_latest_frame.begin_ensure_monitor(monitor.id) { + return false; + } + if self + .request_tx + .send(WorkerRequest::EnsureMonitor { monitor, force_retry_upgrade: true }) + .is_err() + { self.shared_latest_frame.finish_ensure_monitor(monitor.id); + + return false; } + + true } pub(crate) fn refresh_monitor_nonblocking_if_stale( @@ -1025,6 +1052,7 @@ struct StartedStreamArtifacts { enum WorkerRequest { EnsureMonitor { monitor: MonitorRect, + force_retry_upgrade: bool, }, RefreshMonitor { monitor: MonitorRect, @@ -1199,16 +1227,19 @@ fn handle_stream_worker_request( shared_latest_frame: Arc, ) -> bool { match request { - WorkerRequest::EnsureMonitor { monitor } => handle_ensure_monitor_request( - state, - last_setup_attempt_at, - monitor, - filter, - capture_target, - frame_waker, - frame_seq_counter, - shared_latest_frame, - ), + WorkerRequest::EnsureMonitor { monitor, force_retry_upgrade } => { + handle_ensure_monitor_request( + state, + last_setup_attempt_at, + monitor, + force_retry_upgrade, + filter, + capture_target, + frame_waker, + frame_seq_counter, + shared_latest_frame, + ) + }, WorkerRequest::RefreshMonitor { monitor } => handle_refresh_monitor_request( state, last_setup_attempt_at, @@ -1310,6 +1341,7 @@ fn handle_ensure_monitor_request( state: &mut Option, last_setup_attempt_at: &mut Option, monitor: MonitorRect, + force_retry_upgrade: bool, filter: &StreamFilterConfig, capture_target: StreamCaptureTarget, frame_waker: Option>, @@ -1327,6 +1359,7 @@ fn handle_ensure_monitor_request( state, last_setup_attempt_at, STREAM_SETUP_BACKOFF, + force_retry_upgrade, monitor, filter, capture_target, @@ -1411,6 +1444,7 @@ fn reply_with_sample_cursor( state, last_setup_attempt_at, STREAM_SETUP_BACKOFF, + false, monitor, filter, capture_target, @@ -1449,6 +1483,7 @@ fn reply_with_latest_rgba_snapshot( state, last_setup_attempt_at, STREAM_SETUP_BACKOFF, + false, monitor, filter, capture_target, @@ -1581,6 +1616,7 @@ fn refresh_stream_nonblocking( state, last_setup_attempt_at, STREAM_SETUP_BACKOFF, + false, monitor, filter, capture_target, @@ -1607,6 +1643,7 @@ fn ensure_stream( state: &mut Option, last_setup_attempt_at: &mut Option, setup_backoff: Duration, + force_retry_upgrade: bool, monitor: MonitorRect, filter: &StreamFilterConfig, capture_target: StreamCaptureTarget, @@ -1619,7 +1656,7 @@ fn ensure_stream( state.as_ref().is_some_and(|current| current.self_capture_filter_complete), monitor.id, ); - let setup_backoff = stream_setup_backoff(reuse_decision, setup_backoff); + let setup_backoff = stream_setup_backoff(reuse_decision, setup_backoff, force_retry_upgrade); if reuse_decision == StreamReuseDecision::ReuseCurrent { return StreamRequestProgress::Settled; @@ -1733,6 +1770,7 @@ fn latest_fresh_rgba_region( state, last_setup_attempt_at, STREAM_SETUP_BACKOFF, + false, monitor, filter, capture_target, @@ -1797,6 +1835,7 @@ fn ordered_queued_rgba_regions_after_seq_nonblocking( state, last_setup_attempt_at, STREAM_SETUP_BACKOFF, + false, monitor, filter, capture_target, @@ -1829,6 +1868,7 @@ fn ordered_fresh_rgba_regions_after_seq( state, last_setup_attempt_at, STREAM_SETUP_BACKOFF, + false, monitor, filter, capture_target, @@ -2384,8 +2424,10 @@ fn stream_reuse_decision( fn stream_setup_backoff( reuse_decision: StreamReuseDecision, default_setup_backoff: Duration, + force_retry_upgrade: bool, ) -> Duration { match reuse_decision { + StreamReuseDecision::RetryUpgradeUsingCurrent if force_retry_upgrade => Duration::ZERO, StreamReuseDecision::RetryUpgradeUsingCurrent => { STREAM_INCOMPLETE_EXCEPTION_UPGRADE_BACKOFF }, @@ -2889,17 +2931,27 @@ mod tests { assert_eq!( live_frame_stream_macos::stream_setup_backoff( live_frame_stream_macos::StreamReuseDecision::SetupFresh, - Duration::from_millis(300) + Duration::from_millis(300), + false, ), Duration::from_millis(300) ); assert_eq!( live_frame_stream_macos::stream_setup_backoff( live_frame_stream_macos::StreamReuseDecision::RetryUpgradeUsingCurrent, - Duration::from_millis(300) + Duration::from_millis(300), + false, ), Duration::from_secs(3) ); + assert_eq!( + live_frame_stream_macos::stream_setup_backoff( + live_frame_stream_macos::StreamReuseDecision::RetryUpgradeUsingCurrent, + Duration::from_millis(300), + true, + ), + Duration::ZERO + ); } #[test] diff --git a/packages/rsnap-overlay/src/overlay.rs b/packages/rsnap-overlay/src/overlay.rs index 539de770..b37dd990 100644 --- a/packages/rsnap-overlay/src/overlay.rs +++ b/packages/rsnap-overlay/src/overlay.rs @@ -735,6 +735,7 @@ pub struct OverlaySession { #[cfg(not(target_os = "macos"))] cursor_device: Option, state: OverlayState, + session_active: bool, cursor_monitor: Option, egui_repaint_deadline: Arc>>, windows: HashMap, @@ -841,6 +842,8 @@ pub struct OverlaySession { startup_aux_window_creation_pending: bool, #[cfg(target_os = "macos")] startup_aux_window_creation_scheduled: bool, + #[cfg(target_os = "macos")] + pending_startup_aux_live_stream_filter_upgrade: bool, response_waker: Option>, } impl OverlaySession { @@ -950,6 +953,7 @@ impl OverlaySession { #[cfg(not(target_os = "macos"))] cursor_device: None, state: OverlayState::new(), + session_active: false, cursor_monitor: None, windows: HashMap::new(), focused_window_ids: HashSet::new(), @@ -1036,6 +1040,8 @@ impl OverlaySession { startup_aux_window_creation_pending: false, #[cfg(target_os = "macos")] startup_aux_window_creation_scheduled: false, + #[cfg(target_os = "macos")] + pending_startup_aux_live_stream_filter_upgrade: false, response_waker: None, } } @@ -1159,7 +1165,14 @@ impl OverlaySession { #[must_use] pub(crate) fn is_active(&self) -> bool { - !self.windows.is_empty() + self.session_active + } + + fn has_prewarmed_startup_resources(&self) -> bool { + !self.session_active + && self.gpu.is_some() + && !self.windows.is_empty() + && self.hud_window.is_some() } fn use_fake_hud_blur(&self) -> bool { @@ -2816,6 +2829,9 @@ impl OverlaySession { let Some(monitor) = monitor else { return; }; + + self.maybe_apply_pending_startup_aux_live_stream_filter_upgrade(monitor); + let visible = self.update_loupe_window_position(monitor); let was_visible = self.loupe_window_visible; @@ -4481,6 +4497,7 @@ impl OverlaySession { self.update_scroll_toolbar_default_position(monitor); self.set_scroll_overlay_mouse_passthrough_persistent(true, "scroll_capture_started"); self.focus_scroll_keyboard_window(); + self.maybe_apply_pending_startup_aux_live_stream_filter_upgrade(monitor); if let Some(preview) = self.scroll_preview_window.as_ref() { preview.window.set_visible(true); @@ -5307,6 +5324,9 @@ impl OverlaySession { fn reset_runtime_for_exit(&mut self) { #[cfg(target_os = "macos")] self.set_scroll_overlay_mouse_passthrough(false); + + self.session_active = false; + self.windows.clear(); self.hud_window = None; diff --git a/packages/rsnap-overlay/src/overlay/config_runtime.rs b/packages/rsnap-overlay/src/overlay/config_runtime.rs index 8dbd4d68..037c18a6 100644 --- a/packages/rsnap-overlay/src/overlay/config_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/config_runtime.rs @@ -32,6 +32,10 @@ impl OverlaySession { self.state.loupe = None; } if !self.is_active() { + if self.has_prewarmed_startup_resources() { + self.configure_hud_windows_for_config(); + } + return; } @@ -149,7 +153,7 @@ impl OverlaySession { || self.png_encode_inflight } - fn configure_hud_windows_for_config(&mut self) { + pub(super) fn configure_hud_windows_for_config(&mut self) { if let Some(hud_window) = self.hud_window.as_ref() { let window = Arc::clone(&hud_window.window); diff --git a/packages/rsnap-overlay/src/overlay/cursor_context_runtime.rs b/packages/rsnap-overlay/src/overlay/cursor_context_runtime.rs index 8d90aefa..3926deb3 100644 --- a/packages/rsnap-overlay/src/overlay/cursor_context_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/cursor_context_runtime.rs @@ -64,6 +64,33 @@ impl OverlaySession { StartupLiveRgbPlan { focus_window: true, seed_monitor: startup_monitor } } + #[cfg(target_os = "macos")] + pub(super) fn prime_startup_live_stream_nonblocking( + &self, + startup_monitor: Option, + ) { + if !matches!(self.state.mode, OverlayMode::Live) { + return; + } + + let Some(monitor) = startup_monitor else { + return; + }; + let Some(stream) = self.live_sample_stream.as_ref() else { + return; + }; + + stream.prime_monitor_nonblocking(monitor); + } + + #[cfg(not(target_os = "macos"))] + pub(super) fn prime_startup_live_stream_nonblocking( + &self, + startup_monitor: Option, + ) { + let _ = startup_monitor; + } + #[cfg(target_os = "macos")] pub(super) fn seed_startup_live_cursor_rgb( &mut self, @@ -73,6 +100,9 @@ impl OverlaySession { if !matches!(self.state.mode, OverlayMode::Live) || self.state.rgb.is_some() { return; } + if self.startup_aux_window_creation_pending { + return; + } let Some(stream) = self.live_sample_stream.as_ref() else { return; @@ -89,6 +119,26 @@ impl OverlaySession { } } + #[cfg(target_os = "macos")] + pub(super) fn kick_startup_live_sampling(&mut self) { + if !matches!(self.state.mode, OverlayMode::Live) { + return; + } + + let Some(cursor) = self.state.cursor else { + return; + }; + let Some(monitor) = self.active_cursor_monitor() else { + return; + }; + + if self.use_fake_hud_blur() { + self.maybe_request_live_bg(monitor); + } + + let _ = self.request_live_samples_for_cursor(monitor, cursor); + } + pub(super) fn maybe_request_live_bg(&mut self, monitor: MonitorRect) { if !matches!(self.state.mode, OverlayMode::Live) || !self.use_fake_hud_blur() { return; diff --git a/packages/rsnap-overlay/src/overlay/tests.rs b/packages/rsnap-overlay/src/overlay/tests.rs index 5bc25eee..f5cf2e54 100644 --- a/packages/rsnap-overlay/src/overlay/tests.rs +++ b/packages/rsnap-overlay/src/overlay/tests.rs @@ -525,14 +525,14 @@ fn duplicate_live_frames_schedule_forced_refresh_when_downward_backlog_is_fresh( session.maybe_schedule_duplicate_stream_refresh(frame.frame_seq, observed_at); - assert_eq!( + assert!(matches!( session .scroll_capture .live_stream .as_ref() .and_then(MacLiveFrameStream::debug_last_request_kind), - Some("refresh_monitor_nonblocking_if_stale") - ); + Some("refresh_monitor_nonblocking_if_stale") | Some("prime_monitor_nonblocking") + )); assert_eq!(session.scroll_capture.pending_post_stall_burst_after_seq, Some(frame.frame_seq)); assert_eq!(session.scroll_capture.last_duplicate_stream_refresh_at, Some(observed_at)); } @@ -747,10 +747,12 @@ fn reset_for_start_preserves_external_scroll_input_drain_reader() { #[test] fn reset_for_start_clears_reused_session_transient_flags() { let mut session = OverlaySession { + session_active: true, window_list_refresh_inflight: true, drop_next_window_list_refresh_snapshot: true, png_encode_inflight: true, pending_self_capture_exception_window_ids_worker_refresh: true, + pending_startup_aux_live_stream_filter_upgrade: true, authoritative_frozen_capture_ready: true, capture_windows_hidden: true, loupe_activation_key_down: true, @@ -766,10 +768,12 @@ fn reset_for_start_clears_reused_session_transient_flags() { session.reset_for_start(); + assert!(!session.is_active()); assert!(!session.window_list_refresh_inflight); assert!(!session.drop_next_window_list_refresh_snapshot); assert!(!session.png_encode_inflight); assert!(!session.pending_self_capture_exception_window_ids_worker_refresh); + assert!(!session.pending_startup_aux_live_stream_filter_upgrade); assert!(!session.authoritative_frozen_capture_ready); assert!(!session.capture_windows_hidden); assert!(!session.loupe_activation_key_down); @@ -782,6 +786,15 @@ fn reset_for_start_clears_reused_session_transient_flags() { assert_eq!(session.toolbar_window_warmup_redraws_remaining, 0); } +#[test] +fn is_active_tracks_explicit_session_state() { + let inactive = OverlaySession::default(); + let active = OverlaySession { session_active: true, ..OverlaySession::default() }; + + assert!(!inactive.is_active()); + assert!(active.is_active()); +} + #[cfg(target_os = "macos")] #[test] fn drain_external_scroll_input_events_through_advances_last_seen_seq() { diff --git a/packages/rsnap-overlay/src/overlay/tests/live_runtime.rs b/packages/rsnap-overlay/src/overlay/tests/live_runtime.rs index 32931a99..b6e03915 100644 --- a/packages/rsnap-overlay/src/overlay/tests/live_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/tests/live_runtime.rs @@ -1,5 +1,7 @@ use image::RgbaImage; +#[cfg(target_os = "macos")] +use crate::live_frame_stream_macos::MacLiveFrameStream; #[allow(unused_imports)] use crate::overlay::tests::{ self, Duration, GlobalPoint, HudRedrawSummary, LoupeSample, MonitorRect, MonitorRectPoints, @@ -429,6 +431,41 @@ fn sync_live_sample_attempt_does_not_leave_pending_request() { assert_eq!(session.applied_live_cursor_sample_request_id, Some(7)); } +#[cfg(target_os = "macos")] +#[test] +fn request_live_samples_for_cursor_primes_stream_setup_while_startup_aux_windows_pending() { + let monitor = tests::test_monitor(); + let cursor = GlobalPoint::new(120, 180); + let mut session = OverlaySession::new(); + + session.live_sample_stream = Some(MacLiveFrameStream::new()); + session.startup_aux_window_creation_pending = true; + + assert!(!session.request_live_samples_for_cursor(monitor, cursor)); + assert_eq!(session.latest_live_cursor_sample_request_id, Some(1)); + assert_eq!(session.applied_live_cursor_sample_request_id, Some(1)); + assert_eq!( + session.live_sample_stream.as_ref().and_then(MacLiveFrameStream::debug_last_request_kind), + Some("prime_monitor_nonblocking") + ); +} + +#[cfg(target_os = "macos")] +#[test] +fn prime_startup_live_stream_nonblocking_primes_stream_for_live_mode() { + let monitor = tests::test_monitor(); + let mut session = OverlaySession::new(); + + session.live_sample_stream = Some(MacLiveFrameStream::new()); + + session.prime_startup_live_stream_nonblocking(Some(monitor)); + + assert_eq!( + session.live_sample_stream.as_ref().and_then(MacLiveFrameStream::debug_last_request_kind), + Some("prime_monitor_nonblocking") + ); +} + #[test] fn monitor_for_cursor_in_rects_finds_matching_monitor_without_windows() { let monitor_a = MonitorRect { diff --git a/packages/rsnap-overlay/src/overlay/tests/self_capture_runtime.rs b/packages/rsnap-overlay/src/overlay/tests/self_capture_runtime.rs index 4a627821..e12198e7 100644 --- a/packages/rsnap-overlay/src/overlay/tests/self_capture_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/tests/self_capture_runtime.rs @@ -1,3 +1,6 @@ +#[cfg(target_os = "macos")] +use std::ptr; + #[cfg(target_os = "macos")] #[allow(unused_imports)] use crate::overlay::tests::{ @@ -55,10 +58,14 @@ fn apply_self_capture_exception_window_ids_to_active_streams_updates_live_stream #[cfg(target_os = "macos")] #[test] -fn complete_startup_aux_window_creation_refreshes_live_stream_filters() { +fn complete_startup_aux_window_creation_kicks_first_live_sample_before_refresh_is_needed() { + let monitor = tests::test_monitor(); + let cursor = GlobalPoint::new(120, 180); let (mut session, original_worker_debug_id) = tests::configured_session_with_macos_worker(); session.startup_aux_window_creation_pending = true; + session.cursor_monitor = Some(monitor); + session.state.cursor = Some(cursor); session.window_list_snapshot = Some(Arc::new(WindowListSnapshot { captured_at: Instant::now(), windows: Arc::new(vec![WindowRect { @@ -73,25 +80,84 @@ fn complete_startup_aux_window_creation_refreshes_live_stream_filters() { session.complete_startup_aux_window_creation(true); assert!(!session.startup_aux_window_creation_pending); + assert_eq!(session.latest_live_cursor_sample_request_id, Some(1)); + assert_eq!(session.applied_live_cursor_sample_request_id, Some(1)); + assert_eq!(session.worker.as_ref().unwrap().debug_id(), original_worker_debug_id); + assert!(session.window_list_snapshot.is_some()); +} + +#[cfg(target_os = "macos")] +#[test] +fn complete_startup_aux_window_creation_defers_live_stream_upgrade_until_aux_window_use() { + let monitor = tests::test_monitor(); + let (mut session, original_worker_debug_id) = tests::configured_session_with_macos_worker(); + let original_live_sample_stream = ptr::from_ref(session.live_sample_stream.as_ref().unwrap()); + let original_scroll_live_stream = + ptr::from_ref(session.scroll_capture.live_stream.as_ref().unwrap()); + + session.startup_aux_window_creation_pending = true; + session.cursor_monitor = Some(monitor); + + session.note_live_cursor_sample_request_started(7); + session.finish_sync_live_cursor_sample_attempt(7); + + session.window_list_snapshot = Some(Arc::new(WindowListSnapshot { + captured_at: Instant::now(), + windows: Arc::new(vec![WindowRect { + window_id: Some(9), + x: 10, + y: 12, + width: 30, + height: 40, + }]), + })); + + session.complete_startup_aux_window_creation(true); + + assert!(!session.startup_aux_window_creation_pending); + assert!(session.pending_startup_aux_live_stream_filter_upgrade); + assert_eq!(session.latest_live_cursor_sample_request_id, Some(7)); + assert_eq!(session.applied_live_cursor_sample_request_id, Some(7)); assert_eq!( - session.live_sample_stream.as_ref().unwrap().debug_self_capture_exception_window_ids(), - &[17] + ptr::from_ref(session.live_sample_stream.as_ref().unwrap()), + original_live_sample_stream ); assert_eq!( - session - .scroll_capture - .live_stream - .as_ref() - .unwrap() - .debug_self_capture_exception_window_ids(), - &[17] + ptr::from_ref(session.scroll_capture.live_stream.as_ref().unwrap()), + original_scroll_live_stream ); - assert_ne!(session.worker.as_ref().unwrap().debug_id(), original_worker_debug_id); - assert!(session.window_list_snapshot.is_none()); - assert!( - session.last_window_list_refresh_request_at.elapsed() - >= session.window_list_refresh_interval + assert_eq!(session.live_sample_stream.as_ref().unwrap().debug_last_request_kind(), None); + assert_eq!(session.worker.as_ref().unwrap().debug_id(), original_worker_debug_id); + assert!(session.window_list_snapshot.is_some()); +} + +#[cfg(target_os = "macos")] +#[test] +fn showing_loupe_window_applies_pending_startup_live_stream_upgrade() { + let monitor = tests::test_monitor(); + let (mut session, original_worker_debug_id) = tests::configured_session_with_macos_worker(); + let original_live_sample_stream = ptr::from_ref(session.live_sample_stream.as_ref().unwrap()); + let original_scroll_live_stream = + ptr::from_ref(session.scroll_capture.live_stream.as_ref().unwrap()); + + session.pending_startup_aux_live_stream_filter_upgrade = true; + + session.set_alt_loupe_window_visible(Some(monitor), true); + + assert!(!session.pending_startup_aux_live_stream_filter_upgrade); + assert_eq!( + ptr::from_ref(session.live_sample_stream.as_ref().unwrap()), + original_live_sample_stream + ); + assert_eq!( + ptr::from_ref(session.scroll_capture.live_stream.as_ref().unwrap()), + original_scroll_live_stream ); + assert_eq!( + session.live_sample_stream.as_ref().unwrap().debug_last_request_kind(), + Some("upgrade_monitor_nonblocking") + ); + assert_eq!(session.worker.as_ref().unwrap().debug_id(), original_worker_debug_id); } #[cfg(target_os = "macos")] diff --git a/packages/rsnap-overlay/src/overlay/tests/stream_refresh_runtime.rs b/packages/rsnap-overlay/src/overlay/tests/stream_refresh_runtime.rs index 6bd74e27..a2796b2f 100644 --- a/packages/rsnap-overlay/src/overlay/tests/stream_refresh_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/tests/stream_refresh_runtime.rs @@ -52,6 +52,7 @@ fn handle_scroll_input_ready_drains_input_and_polls_stream_fallback() { session.scroll_capture.live_stream.as_ref().unwrap().debug_last_request_kind(), Some("ordered_rgba_regions_after_seq_nonblocking") | Some("refresh_monitor_nonblocking_if_stale") + | Some("prime_monitor_nonblocking") )); } diff --git a/packages/rsnap-overlay/src/overlay/tests/worker_tick_runtime.rs b/packages/rsnap-overlay/src/overlay/tests/worker_tick_runtime.rs index 014ac6d8..9005b386 100644 --- a/packages/rsnap-overlay/src/overlay/tests/worker_tick_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/tests/worker_tick_runtime.rs @@ -33,6 +33,7 @@ fn maybe_tick_scroll_capture_stays_on_stream_path_without_worker_fallback() { session.scroll_capture.live_stream.as_ref().unwrap().debug_last_request_kind(), Some("ordered_rgba_regions_after_seq_nonblocking") | Some("refresh_monitor_nonblocking_if_stale") + | Some("prime_monitor_nonblocking") )); } diff --git a/packages/rsnap-overlay/src/overlay/toolbar_runtime.rs b/packages/rsnap-overlay/src/overlay/toolbar_runtime.rs index cf023543..d2aa57bf 100644 --- a/packages/rsnap-overlay/src/overlay/toolbar_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/toolbar_runtime.rs @@ -205,6 +205,11 @@ impl OverlaySession { let should_focus_frozen_keyboard = !self.toolbar_window_visible && matches!(self.state.mode, OverlayMode::Frozen) && !self.scroll_capture.active; + + if !self.toolbar_window_visible { + self.maybe_apply_pending_startup_aux_live_stream_filter_upgrade(monitor); + } + let Some(gpu) = self.gpu.as_ref() else { return Ok(()); }; diff --git a/packages/rsnap-overlay/src/overlay/window_runtime.rs b/packages/rsnap-overlay/src/overlay/window_runtime.rs index 79cce157..3da37d76 100644 --- a/packages/rsnap-overlay/src/overlay/window_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/window_runtime.rs @@ -1,14 +1,18 @@ -use std::sync::Arc; +use std::collections::HashMap; +use std::mem; +use std::sync::{Arc, Mutex}; #[cfg(not(target_os = "macos"))] use std::time::Duration; use std::time::Instant; #[cfg(target_os = "macos")] use objc2_foundation::NSArray; -use winit::window::Window; +use winit::window::{Window, WindowId}; use crate::backend; #[cfg(target_os = "macos")] +use crate::overlay::MacOSHudWindowConfigState; +#[cfg(target_os = "macos")] use crate::overlay::{self, MacLiveFrameStream, MainThreadMarker, NSScreen}; use crate::overlay::{ ActiveEventLoop, GlobalPoint, GpuContext, HUD_PILL_CORNER_RADIUS_POINTS, HudOverlayWindow, @@ -43,9 +47,16 @@ impl OverlaySession { let startup_cursor = self.sample_mouse_location(); let startup_monitor = Self::monitor_for_cursor_in_rects(&monitors, startup_cursor); + let startup_stream_prime_started_at = Instant::now(); + + self.prime_startup_live_stream_nonblocking(startup_monitor); + + let startup_stream_prime_ms = startup_stream_prime_started_at.elapsed().as_millis(); let gpu_init_started_at = Instant::now(); - self.gpu = Some(GpuContext::new().map_err(|err| format!("{err:#}"))?); + if self.gpu.is_none() { + self.gpu = Some(GpuContext::new().map_err(|err| format!("{err:#}"))?); + } let gpu_init_ms = gpu_init_started_at.elapsed().as_millis(); let window_creation = self.create_startup_windows(event_loop, &monitors)?; @@ -62,6 +73,8 @@ impl OverlaySession { let initialize_cursor_ms = initialize_cursor_started_at.elapsed().as_millis(); let request_redraw_started_at = Instant::now(); + self.session_active = true; + self.request_redraw_all(); let request_redraw_ms = request_redraw_started_at.elapsed().as_millis(); @@ -75,12 +88,14 @@ impl OverlaySession { reset_ms, worker_setup_ms, monitor_enum_ms, + startup_stream_prime_ms, gpu_init_ms, overlay_windows_ms = window_creation.overlay_windows_ms, hud_window_ms = window_creation.hud_window_ms, loupe_window_ms = window_creation.loupe_window_ms, toolbar_window_ms = window_creation.toolbar_window_ms, scroll_preview_window_ms = window_creation.scroll_preview_window_ms, + startup_windows_source = window_creation.startup_windows_source, startup_aux_windows_deferred = cfg!(target_os = "macos"), prime_cursor_ms, startup_seed_ms, @@ -93,6 +108,60 @@ impl OverlaySession { Ok(()) } + /// Pre-creates the GPU context plus hidden overlay/HUD windows so the first capture can + /// reuse them instead of paying the full cold-start cost on demand. + pub fn prewarm(&mut self, event_loop: &ActiveEventLoop) -> Result<(), String> { + if self.is_active() || self.has_prewarmed_startup_resources() { + return Ok(()); + } + + let prewarm_started_at = Instant::now(); + let monitor_enum_started_at = Instant::now(); + let monitors = self.available_overlay_monitors()?; + let monitor_enum_ms = monitor_enum_started_at.elapsed().as_millis(); + + if monitors.is_empty() { + return Err(String::from("No monitors detected")); + } + + let gpu_init_started_at = Instant::now(); + + if self.gpu.is_none() { + self.gpu = Some(GpuContext::new().map_err(|err| format!("{err:#}"))?); + } + + let gpu_init_ms = gpu_init_started_at.elapsed().as_millis(); + + if !self.windows.is_empty() || self.hud_window.is_some() { + self.discard_prewarmed_startup_resources(); + } + + let overlay_windows_started_at = Instant::now(); + + self.create_overlay_windows(event_loop, &monitors, false)?; + + let overlay_windows_ms = overlay_windows_started_at.elapsed().as_millis(); + let hud_window_started_at = Instant::now(); + + self.create_hud_window(event_loop)?; + + let hud_window_ms = hud_window_started_at.elapsed().as_millis(); + + tracing::info!( + op = "overlay.prewarm_phase_timing", + monitor_count = monitors.len(), + window_count = self.windows.len(), + monitor_enum_ms, + gpu_init_ms, + overlay_windows_ms, + hud_window_ms, + total_ms = prewarm_started_at.elapsed().as_millis(), + "Overlay startup resources prewarmed." + ); + + Ok(()) + } + fn setup_startup_worker(&mut self) -> u128 { let worker_setup_started_at = Instant::now(); @@ -119,15 +188,26 @@ impl OverlaySession { monitors: &[MonitorRect], ) -> Result { let overlay_windows_started_at = Instant::now(); + let reused_prewarmed_windows = self.has_matching_prewarmed_startup_resources(monitors); - self.create_overlay_windows(event_loop, monitors)?; + if reused_prewarmed_windows { + self.activate_prewarmed_overlay_windows(); + } else { + self.discard_prewarmed_startup_resources(); + self.create_overlay_windows(event_loop, monitors, true)?; + } let overlay_windows_ms = overlay_windows_started_at.elapsed().as_millis(); let hud_window_started_at = Instant::now(); - self.create_hud_window(event_loop)?; + if reused_prewarmed_windows { + self.configure_hud_windows_for_config(); + } else { + self.create_hud_window(event_loop)?; + } let hud_window_ms = hud_window_started_at.elapsed().as_millis(); + let startup_windows_source = if reused_prewarmed_windows { "prewarmed" } else { "fresh" }; #[cfg(target_os = "macos")] { @@ -140,6 +220,7 @@ impl OverlaySession { loupe_window_ms: 0, toolbar_window_ms: 0, scroll_preview_window_ms: 0, + startup_windows_source, }) } #[cfg(not(target_os = "macos"))] @@ -166,6 +247,7 @@ impl OverlaySession { loupe_window_ms, toolbar_window_ms, scroll_preview_window_ms, + startup_windows_source, }) } } @@ -249,18 +331,51 @@ impl OverlaySession { self.startup_aux_window_creation_pending = false; if created_aux_windows { - // When ScreenCaptureKit falls back to excluding only currently shareable - // rsnap windows, deferred aux windows must exist before we rebuild the - // active filters or they can remain visible in the live stream. - self.apply_self_capture_exception_window_ids_to_active_streams(); + if self.latest_live_cursor_sample_request_id.is_some() { + // If startup already primed live sampling, keep the existing stream alive + // and defer the narrow ScreenCaptureKit upgrade until an auxiliary window + // is actually shown. + self.pending_startup_aux_live_stream_filter_upgrade = true; + } else { + // Delay the first live-stream ensure until after the aux windows exist so + // startup can begin with the full self-capture exclusion set. + self.kick_startup_live_sampling(); + } } } + #[cfg(target_os = "macos")] + pub(super) fn maybe_apply_pending_startup_aux_live_stream_filter_upgrade( + &mut self, + monitor: MonitorRect, + ) { + if !self.pending_startup_aux_live_stream_filter_upgrade { + return; + } + + let Some(stream) = self.live_sample_stream.as_ref() else { + return; + }; + + if stream.upgrade_monitor_nonblocking(monitor) { + self.pending_startup_aux_live_stream_filter_upgrade = false; + } + } + + #[cfg(not(target_os = "macos"))] + pub(super) fn maybe_apply_pending_startup_aux_live_stream_filter_upgrade( + &mut self, + monitor: MonitorRect, + ) { + let _ = monitor; + } + pub(super) fn reset_for_start(&mut self) { #[cfg(target_os = "macos")] self.set_scroll_overlay_mouse_passthrough(false); let config = self.config.clone(); + let prewarmed_startup_resources = self.take_prewarmed_startup_resources(); let response_waker = self.response_waker.clone(); #[cfg(target_os = "macos")] let scroll_frame_waker = self.scroll_frame_waker.clone(); @@ -277,6 +392,9 @@ impl OverlaySession { self.scroll_capture.external_scroll_input_drain_reader.clone(); *self = Self::with_config(config); + + self.restore_prewarmed_startup_resources(prewarmed_startup_resources); + self.response_waker = response_waker; #[cfg(target_os = "macos")] { @@ -285,6 +403,7 @@ impl OverlaySession { self.scroll_capture_starting_hook = scroll_capture_starting_hook; self.scroll_capture_started_hook = scroll_capture_started_hook; self.startup_aux_window_waker = startup_aux_window_waker; + self.pending_startup_aux_live_stream_filter_upgrade = false; self.scroll_capture.external_scroll_input_drain_reader = external_scroll_input_drain_reader; } @@ -430,10 +549,11 @@ impl OverlaySession { &mut self, event_loop: &ActiveEventLoop, monitors: &[MonitorRect], + visible: bool, ) -> Result<(), String> { for monitor in monitors { let monitor_rect = *monitor; - let attrs = Window::default_attributes() + let mut attrs = Window::default_attributes() .with_title("rsnap-overlay") .with_decorations(false) .with_resizable(false) @@ -450,6 +570,11 @@ impl OverlaySession { monitor_rect.origin.x as f64, monitor_rect.origin.y as f64, )); + + if !visible { + attrs = attrs.with_visible(false); + } + let window = event_loop .create_window(attrs) .map_err(|err| format!("Unable to create overlay window: {err}"))?; @@ -476,8 +601,10 @@ impl OverlaySession { let refresh_rate_millihertz = window.current_monitor().and_then(|monitor| monitor.refresh_rate_millihertz()); - window.request_redraw(); - window.focus_window(); + if visible { + window.request_redraw(); + window.focus_window(); + } let gpu = self.gpu.as_ref().ok_or_else(|| String::from("Missing GPU context"))?; let renderer = WindowRenderer::new( @@ -496,6 +623,65 @@ impl OverlaySession { Ok(()) } + fn activate_prewarmed_overlay_windows(&self) { + for window in self.windows.values() { + window.window.set_visible(true); + } + } + + fn has_matching_prewarmed_startup_resources(&self, monitors: &[MonitorRect]) -> bool { + self.has_prewarmed_startup_resources() + && self.windows.len() == monitors.len() + && monitors + .iter() + .all(|monitor| self.windows.values().any(|window| window.monitor == *monitor)) + } + + fn discard_prewarmed_startup_resources(&mut self) { + self.windows.clear(); + + self.hud_window = None; + self.hud_inner_size_points = None; + self.hud_outer_pos = None; + self.pending_hud_outer_pos = None; + + #[cfg(target_os = "macos")] + self.macos_hud_window_config_cache.clear(); + } + + fn take_prewarmed_startup_resources(&mut self) -> Option { + if !self.has_prewarmed_startup_resources() { + return None; + } + + Some(PrewarmedStartupResources { + egui_repaint_deadline: Arc::clone(&self.egui_repaint_deadline), + gpu: self.gpu.take(), + windows: mem::take(&mut self.windows), + hud_window: self.hud_window.take(), + #[cfg(target_os = "macos")] + macos_hud_window_config_cache: mem::take(&mut self.macos_hud_window_config_cache), + }) + } + + fn restore_prewarmed_startup_resources( + &mut self, + resources: Option, + ) { + let Some(resources) = resources else { + return; + }; + + self.egui_repaint_deadline = resources.egui_repaint_deadline; + self.gpu = resources.gpu; + self.windows = resources.windows; + self.hud_window = resources.hud_window; + #[cfg(target_os = "macos")] + { + self.macos_hud_window_config_cache = resources.macos_hud_window_config_cache; + } + } + fn create_hud_window(&mut self, event_loop: &ActiveEventLoop) -> Result<(), String> { let attrs = Window::default_attributes() .with_title("rsnap-hud") @@ -715,6 +901,16 @@ struct StartupWindowCreationMetrics { loupe_window_ms: u128, toolbar_window_ms: u128, scroll_preview_window_ms: u128, + startup_windows_source: &'static str, +} + +struct PrewarmedStartupResources { + egui_repaint_deadline: Arc>>, + gpu: Option, + windows: HashMap, + hud_window: Option, + #[cfg(target_os = "macos")] + macos_hud_window_config_cache: HashMap, } #[cfg(test)]