diff --git a/packages/rsnap-overlay/src/overlay.rs b/packages/rsnap-overlay/src/overlay.rs index b0ae6093..e5dfc2c0 100644 --- a/packages/rsnap-overlay/src/overlay.rs +++ b/packages/rsnap-overlay/src/overlay.rs @@ -865,6 +865,7 @@ pub struct OverlaySession { window_list_refresh_interval: Duration, last_live_bg_request_at: Instant, live_bg_request_interval: Duration, + freeze_capture_send_full_count: u64, hit_test_send_full_count: u64, hit_test_send_disconnected_count: u64, hit_test_request_id: u64, @@ -1088,6 +1089,7 @@ impl OverlaySession { window_list_refresh_interval: Duration::ZERO, last_live_bg_request_at: Instant::now(), live_bg_request_interval: Duration::ZERO, + freeze_capture_send_full_count: 0, hit_test_send_full_count: 0, hit_test_send_disconnected_count: 0, hit_test_request_id: 0, @@ -1490,20 +1492,7 @@ impl OverlaySession { } #[cfg(target_os = "macos")] - fn try_latest_live_freeze_preview(&mut self, monitor: MonitorRect) -> Option { - if self.state.live_bg_monitor == Some(monitor) - && let Some(image) = self.state.live_bg_image.take() - { - return Some(image); - } - - self.live_sample_stream - .as_ref() - .and_then(|stream| stream.peek_latest_rgba_snapshot(monitor)) - .map(|snapshot| snapshot.image.as_ref().clone()) - } - - #[cfg(target_os = "macos")] + #[allow(dead_code)] fn commit_frozen_preview( &mut self, monitor: MonitorRect, @@ -1665,6 +1654,7 @@ impl OverlaySession { self.pending_freeze_capture_armed = false; self.inflight_freeze_capture = None; self.authoritative_frozen_capture_ready = false; + self.freeze_capture_send_full_count = 0; self.pending_window_freeze_capture = window_target; self.inflight_window_freeze_capture = None; self.frozen_window_image = None; @@ -1678,17 +1668,14 @@ impl OverlaySession { #[cfg(target_os = "macos")] { - if let Some(image) = self.try_latest_live_freeze_preview(monitor) { - self.state.live_bg_monitor = None; - self.state.live_bg_image = None; + let _ = cursor; - self.commit_frozen_preview(monitor, image, cursor); - self.force_apply_pending_toolbar_window_move(); - } else { - self.state.live_bg_monitor = None; - self.state.live_bg_image = None; - self.capture_windows_hidden = true; - } + self.state.live_bg_monitor = None; + self.state.live_bg_image = None; + self.capture_windows_hidden = true; + self.pending_freeze_capture_armed = true; + + self.hide_capture_windows(); } #[cfg(not(target_os = "macos"))] { @@ -4728,7 +4715,11 @@ impl OverlaySession { } self.state.finish_freeze(monitor, frozen_preview_image); + #[cfg(target_os = "macos")] + self.destroy_live_only_aux_windows(); self.restore_capture_windows_visibility(); + #[cfg(target_os = "macos")] + self.request_aux_window_creation_if_needed(); self.toolbar_state.needs_redraw = true; @@ -5242,6 +5233,13 @@ impl OverlaySession { return; }; + #[cfg(target_os = "macos")] + if self.loupe_window.is_none() { + self.request_aux_window_creation_if_needed(); + + return; + } + self.maybe_apply_pending_startup_aux_live_stream_filter_upgrade(monitor); let visible = self.update_loupe_window_position(monitor); @@ -6747,6 +6745,11 @@ impl OverlaySession { { self.toolbar_state.visible = !self.toolbar_state.visible; + #[cfg(target_os = "macos")] + if self.toolbar_state.visible { + self.request_aux_window_creation_if_needed(); + } + self.request_redraw_all(); OverlayControl::Continue @@ -7265,6 +7268,7 @@ impl OverlaySession { "Entered scroll-capture mode." ); + self.request_aux_window_creation_if_needed(); self.sync_frozen_toolbar_state(); self.refresh_scroll_preview_committed_image(); self.refresh_scroll_preview_display_image(); @@ -7983,9 +7987,7 @@ impl OverlaySession { overlay_monitor: MonitorRect, draw_toolbar: bool, ) -> OverlayControl { - if self.should_dispatch_pending_freeze_capture(overlay_monitor) - && let Some(worker) = &self.worker - { + if self.should_dispatch_pending_freeze_capture(overlay_monitor) { let pending_window_target = self .pending_window_freeze_capture .filter(|target| target.monitor == overlay_monitor); @@ -7993,33 +7995,32 @@ impl OverlaySession { .map_or(FreezeCaptureTarget::Monitor, |target| FreezeCaptureTarget::Window { window_id: target.window_id, }); - #[cfg(target_os = "macos")] - { - if worker.request_freeze_capture(overlay_monitor, freeze_target) { - self.pending_freeze_capture = None; - self.pending_freeze_capture_armed = false; - self.inflight_freeze_capture = Some(overlay_monitor); - self.inflight_window_freeze_capture = pending_window_target; - self.pending_window_freeze_capture = None; - } else { - self.request_redraw_for_monitor(overlay_monitor); - } - } + let _ = (&freeze_target, &pending_window_target, &overlay_monitor); + #[cfg(not(target_os = "macos"))] { // Capture must happen on a post-hide redraw so the HUD/loupe are not included. if self.pending_freeze_capture_armed { - if worker.request_freeze_capture(overlay_monitor, freeze_target) { - self.pending_freeze_capture = None; - self.pending_freeze_capture_armed = false; - self.inflight_freeze_capture = Some(overlay_monitor); - self.inflight_window_freeze_capture = pending_window_target; - self.pending_window_freeze_capture = None; - } else { - self.request_redraw_for_monitor(overlay_monitor); + let Some(worker) = &self.worker else { + self.abort_pending_freeze_capture("Capture worker is unavailable."); + + return OverlayControl::Continue; + }; + + match worker.request_freeze_capture(overlay_monitor, freeze_target) { + Ok(()) => { + self.note_freeze_capture_request_started( + overlay_monitor, + pending_window_target, + ); + }, + Err(err) => { + self.handle_freeze_capture_request_send_error(overlay_monitor, err); + }, } } else { + self.freeze_capture_send_full_count = 0; self.pending_freeze_capture_armed = true; #[cfg(not(target_os = "macos"))] @@ -8231,6 +8232,10 @@ impl OverlaySession { self.event_loop_last_progress_monitor_id = None; self.event_loop_last_progress_detail = None; self.event_loop_last_stall_warn_at = None; + + #[cfg(target_os = "macos")] + self.macos_hud_window_config_cache.clear(); + self.toolbar_left_button_down = false; self.toolbar_left_button_went_down = false; self.toolbar_left_button_went_up = false; @@ -9071,8 +9076,6 @@ fn macos_configure_overlay_window_mouse_moved_events(window: &Window) { let _: () = objc::msg_send![ns_window, setOpaque: false]; let _: () = objc::msg_send![ns_window, setHasShadow: false]; - let sharing_type_none = 0_u64; - let _: () = objc::msg_send![ns_window, setSharingType: sharing_type_none]; let clear: *mut Object = objc::msg_send![objc::class!(NSColor), clearColor]; let _: () = objc::msg_send![ns_window, setBackgroundColor: clear]; let _: () = objc::msg_send![ns_window, setLevel: MACOS_OVERLAY_WINDOW_LEVEL]; @@ -9134,8 +9137,6 @@ fn macos_configure_hud_window( let _: () = objc::msg_send![ns_window, setHasShadow: false]; let _: () = objc::msg_send![ns_window, setAcceptsMouseMovedEvents: YES]; let _: () = objc::msg_send![ns_window, setLevel: MACOS_HUD_WINDOW_LEVEL]; - let sharing_type_none = 0_u64; - let _: () = objc::msg_send![ns_window, setSharingType: sharing_type_none]; let clear: *mut Object = objc::msg_send![objc::class!(NSColor), clearColor]; let _: () = objc::msg_send![ns_window, setBackgroundColor: clear]; let content_view: *mut Object = objc::msg_send![ns_window, contentView]; diff --git a/packages/rsnap-overlay/src/overlay/aux_window_runtime.rs b/packages/rsnap-overlay/src/overlay/aux_window_runtime.rs index 0d8c7869..28c8549f 100644 --- a/packages/rsnap-overlay/src/overlay/aux_window_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/aux_window_runtime.rs @@ -64,6 +64,8 @@ impl OverlaySession { self.maybe_log_event_loop_stall(now); self.mark_progress(OverlayEventLoopPhase::AboutToWait); self.maybe_clear_loupe_activation_after_focus_loss(); + #[cfg(target_os = "macos")] + self.maybe_dispatch_armed_freeze_capture(); self.maybe_request_keepalive_redraw(); self.maybe_keep_selection_flow_repaint(); self.maybe_keep_frozen_text_caret_repaint(); diff --git a/packages/rsnap-overlay/src/overlay/capture_window_runtime.rs b/packages/rsnap-overlay/src/overlay/capture_window_runtime.rs index f180c292..3b3373e0 100644 --- a/packages/rsnap-overlay/src/overlay/capture_window_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/capture_window_runtime.rs @@ -1,5 +1,5 @@ #[allow(unused_imports)] -use crate::overlay::{GlobalPoint, MonitorRect, OverlayMode, OverlaySession}; +use crate::overlay::{GlobalPoint, Instant, MonitorRect, OverlayMode, OverlaySession}; impl OverlaySession { pub(super) fn update_cursor_state(&mut self, monitor: MonitorRect, cursor: GlobalPoint) { @@ -7,6 +7,42 @@ impl OverlaySession { self.state.cursor = Some(cursor); } + #[cfg(target_os = "macos")] + pub(super) fn hide_capture_windows(&mut self) { + self.capture_windows_hidden = true; + + for overlay_window in self.windows.values() { + overlay_window.window.set_visible(false); + } + + if let Some(hud_window) = &self.hud_window { + hud_window.window.set_visible(false); + } + + self.hud_window_visible = false; + + if let Some(loupe_window) = &self.loupe_window { + loupe_window.window.set_visible(false); + } + + self.loupe_window_visible = false; + + self.reset_loupe_window_warmup_redraws(); + + if let Some(toolbar_window) = &self.toolbar_window { + toolbar_window.window.set_visible(false); + } + + self.toolbar_window_visible = false; + self.toolbar_window_warmup_redraws_remaining = 0; + + if let Some(preview_window) = &self.scroll_preview_window { + preview_window.window.set_visible(false); + } + + self.last_present_at = Instant::now(); + } + #[cfg(not(target_os = "macos"))] pub(super) fn hide_capture_windows(&mut self) { self.capture_windows_hidden = true; @@ -28,6 +64,58 @@ impl OverlaySession { } self.capture_windows_hidden = false; + #[cfg(target_os = "macos")] + { + for overlay_window in self.windows.values() { + overlay_window.window.set_visible(true); + overlay_window.window.request_redraw(); + } + + if matches!(self.state.mode, OverlayMode::Live) { + if let Some(hud_window) = &self.hud_window { + hud_window.window.set_visible(true); + hud_window.window.request_redraw(); + } + + self.hud_window_visible = self.hud_window.is_some(); + + if let Some(loupe_window) = &self.loupe_window { + loupe_window.window.set_visible(self.state.alt_held); + loupe_window.window.request_redraw(); + } + + self.loupe_window_visible = self.state.alt_held && self.loupe_window.is_some(); + + return; + } + + self.hud_window_visible = false; + self.loupe_window_visible = false; + + if let Some(toolbar_window) = &self.toolbar_window { + let show_toolbar = matches!(self.state.mode, OverlayMode::Frozen) + && self.toolbar_state.visible + && self.authoritative_frozen_capture_ready + && self.state.frozen_image.is_some(); + + toolbar_window.window.set_visible(show_toolbar); + + if show_toolbar { + toolbar_window.window.request_redraw(); + } + + self.toolbar_window_visible = show_toolbar; + } else { + self.toolbar_window_visible = false; + } + if let Some(preview_window) = &self.scroll_preview_window { + preview_window.window.set_visible(self.scroll_capture.active); + + if self.scroll_capture.active { + preview_window.window.request_redraw(); + } + } + } #[cfg(not(target_os = "macos"))] { if matches!(self.state.mode, OverlayMode::Live) { @@ -59,4 +147,18 @@ impl OverlaySession { loupe_window.window.focus_window(); } } + + #[cfg(target_os = "macos")] + pub(super) fn destroy_live_only_aux_windows(&mut self) { + if let Some(loupe_window) = self.loupe_window.take() { + self.remove_macos_hud_window_config_cache_entry(loupe_window.window.id()); + } + + self.loupe_inner_size_points = None; + self.loupe_outer_pos = None; + self.pending_loupe_outer_pos = None; + self.loupe_window_visible = false; + + self.reset_loupe_window_warmup_redraws(); + } } diff --git a/packages/rsnap-overlay/src/overlay/config_runtime.rs b/packages/rsnap-overlay/src/overlay/config_runtime.rs index 037c18a6..126f0347 100644 --- a/packages/rsnap-overlay/src/overlay/config_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/config_runtime.rs @@ -1,4 +1,6 @@ use winit::window::Window; +#[cfg(target_os = "macos")] +use winit::window::WindowId; #[cfg(target_os = "macos")] use crate::backend; @@ -65,45 +67,49 @@ impl OverlaySession { self.config.self_capture_exception_window_ids.clone(), )); - if self.scroll_capture.active { - self.scroll_capture.live_stream = if self.should_use_scroll_capture_worker_sampling() { - None - } else { - match ( - self.scroll_capture.capture_rect_points, - self.scroll_capture.capture_rect_pixels, - ) { - (Some(capture_rect_points), Some(capture_rect_pixels)) => { - Some(MacLiveFrameStream::with_scroll_capture_region_and_waker( - self.config.self_capture_exception_window_ids.clone(), - capture_rect_points, - capture_rect_pixels, - self.scroll_frame_waker.clone(), - )) - }, - _ => { - Some(MacLiveFrameStream::with_self_capture_exception_window_ids_and_waker( - self.config.self_capture_exception_window_ids.clone(), - self.scroll_frame_waker.clone(), - )) - }, - } - }; - - self.scroll_capture.live_stream_backlog.clear(); - - self.scroll_capture.last_stream_frame_seq = 0; - self.scroll_capture.last_stream_frame_fingerprint = None; - self.scroll_capture.consecutive_identical_stream_frames = 0; - self.scroll_capture.last_consumed_stream_frame_captured_at = None; - self.scroll_capture.last_stream_event_at = None; - self.scroll_capture.last_stream_poll_at = None; - self.scroll_capture.pending_post_stall_burst_after_seq = None; - self.scroll_capture.live_stream_stale_grace = None; - self.scroll_capture.last_duplicate_stream_refresh_at = None; + self.rebuild_active_scroll_capture_live_stream(); + self.refresh_active_worker_for_self_capture_exception_window_ids_if_safe(); + } + + #[cfg(target_os = "macos")] + pub(super) fn rebuild_active_scroll_capture_live_stream(&mut self) -> bool { + if !self.scroll_capture.active { + return false; } - self.refresh_active_worker_for_self_capture_exception_window_ids_if_safe(); + self.scroll_capture.live_stream = if self.should_use_scroll_capture_worker_sampling() { + None + } else { + match (self.scroll_capture.capture_rect_points, self.scroll_capture.capture_rect_pixels) + { + (Some(capture_rect_points), Some(capture_rect_pixels)) => { + Some(MacLiveFrameStream::with_scroll_capture_region_and_waker( + self.config.self_capture_exception_window_ids.clone(), + capture_rect_points, + capture_rect_pixels, + self.scroll_frame_waker.clone(), + )) + }, + _ => Some(MacLiveFrameStream::with_self_capture_exception_window_ids_and_waker( + self.config.self_capture_exception_window_ids.clone(), + self.scroll_frame_waker.clone(), + )), + } + }; + + self.scroll_capture.live_stream_backlog.clear(); + + self.scroll_capture.last_stream_frame_seq = 0; + self.scroll_capture.last_stream_frame_fingerprint = None; + self.scroll_capture.consecutive_identical_stream_frames = 0; + self.scroll_capture.last_consumed_stream_frame_captured_at = None; + self.scroll_capture.last_stream_event_at = None; + self.scroll_capture.last_stream_poll_at = None; + self.scroll_capture.pending_post_stall_burst_after_seq = None; + self.scroll_capture.live_stream_stale_grace = None; + self.scroll_capture.last_duplicate_stream_refresh_at = None; + + self.scroll_capture.live_stream.is_some() } #[cfg(target_os = "macos")] @@ -251,6 +257,11 @@ impl OverlaySession { let _ = self.macos_hud_window_config_cache.insert(window.id(), desired); } + #[cfg(target_os = "macos")] + pub(super) fn remove_macos_hud_window_config_cache_entry(&mut self, window_id: WindowId) { + let _ = self.macos_hud_window_config_cache.remove(&window_id); + } + fn handle_fake_hud_blur_toggle(&mut self, prev_fake_blur: bool, new_fake_blur: bool) { if prev_fake_blur == new_fake_blur { return; diff --git a/packages/rsnap-overlay/src/overlay/cursor_context_runtime.rs b/packages/rsnap-overlay/src/overlay/cursor_context_runtime.rs index 3570696c..f06d89f8 100644 --- a/packages/rsnap-overlay/src/overlay/cursor_context_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/cursor_context_runtime.rs @@ -168,7 +168,7 @@ impl OverlaySession { return; }; - if worker.request_freeze_capture(monitor, FreezeCaptureTarget::Monitor) { + if worker.request_freeze_capture(monitor, FreezeCaptureTarget::Monitor).is_ok() { self.last_live_bg_request_at = Instant::now(); } } diff --git a/packages/rsnap-overlay/src/overlay/tests.rs b/packages/rsnap-overlay/src/overlay/tests.rs index f7887dfb..dc5da3b6 100644 --- a/packages/rsnap-overlay/src/overlay/tests.rs +++ b/packages/rsnap-overlay/src/overlay/tests.rs @@ -86,7 +86,7 @@ use crate::state::{WindowListSnapshot, WindowRect}; #[cfg(target_os = "macos")] use crate::worker::OverlayWorker; #[cfg(target_os = "macos")] -use crate::worker::{WorkerErrorSource, WorkerResponse}; +use crate::worker::{WorkerErrorSource, WorkerRequestSendError, WorkerResponse}; #[cfg(target_os = "macos")] struct SequenceScrollCaptureBackend { 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 e12198e7..628125f6 100644 --- a/packages/rsnap-overlay/src/overlay/tests/self_capture_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/tests/self_capture_runtime.rs @@ -1,6 +1,11 @@ #[cfg(target_os = "macos")] use std::ptr; +#[cfg(target_os = "macos")] +use crate::overlay::RectPoints; +#[cfg(target_os = "macos")] +#[allow(unused_imports)] +use crate::overlay::tests::WorkerRequestSendError; #[cfg(target_os = "macos")] #[allow(unused_imports)] use crate::overlay::tests::{ @@ -13,6 +18,8 @@ use crate::overlay::tests::{ GlobalPoint, Instant, OverlaySession, ScrollDirection, WorkerErrorSource, WorkerResponse, overlay, }; +#[cfg(target_os = "macos")] +use crate::overlay::worker_runtime::FREEZE_CAPTURE_SEND_FULL_RETRY_LIMIT; #[cfg(target_os = "macos")] #[test] @@ -133,7 +140,130 @@ fn complete_startup_aux_window_creation_defers_live_stream_upgrade_until_aux_win #[cfg(target_os = "macos")] #[test] -fn showing_loupe_window_applies_pending_startup_live_stream_upgrade() { +fn refresh_startup_live_stream_after_window_creation_rebuilds_and_reprimes_stream() { + let monitor = tests::test_monitor(); + let (mut session, _original_worker_debug_id) = tests::configured_session_with_macos_worker(); + + assert!( + session + .live_sample_stream + .as_ref() + .unwrap() + .debug_self_capture_exception_window_ids() + .is_empty() + ); + + session.refresh_startup_live_stream_after_window_creation(Some(monitor)); + + assert_eq!( + session.live_sample_stream.as_ref().unwrap().debug_self_capture_exception_window_ids(), + &[17] + ); + assert_eq!( + session.live_sample_stream.as_ref().unwrap().debug_last_request_kind(), + Some("prime_monitor_nonblocking") + ); +} + +#[cfg(target_os = "macos")] +#[test] +fn rebuild_active_scroll_capture_live_stream_rebuilds_and_reprimes_after_aux_window_creation() { + let monitor = tests::test_monitor(); + let (mut session, _original_worker_debug_id) = tests::configured_session_with_macos_worker(); + + session.scroll_capture.live_stream.as_ref().unwrap().prime_monitor_nonblocking(monitor); + + session.scroll_capture.monitor = Some(monitor); + session.scroll_capture.capture_rect_points = Some(RectPoints::new(1, 2, 30, 40)); + session.scroll_capture.capture_rect_pixels = Some(RectPoints::new(2, 4, 60, 80)); + + session.scroll_capture.live_stream_backlog.push_back(ScrollCaptureLiveFrame { + frame_seq: 3, + captured_at: Instant::now(), + image: tests::test_frozen_image(), + }); + + session.scroll_capture.last_stream_frame_seq = 3; + session.scroll_capture.last_stream_event_at = Some(Instant::now()); + session.scroll_capture.last_stream_poll_at = Some(Instant::now()); + + assert!(session.rebuild_active_scroll_capture_live_stream()); + + let rebuilt_scroll_live_stream = session.scroll_capture.live_stream.as_ref().unwrap(); + + assert_eq!(rebuilt_scroll_live_stream.debug_self_capture_exception_window_ids(), &[17]); + assert_eq!(rebuilt_scroll_live_stream.debug_last_request_kind(), None); + assert!(session.scroll_capture.live_stream_backlog.is_empty()); + assert_eq!(session.scroll_capture.last_stream_frame_seq, 0); + assert!(session.scroll_capture.last_stream_event_at.is_none()); + assert!(session.scroll_capture.last_stream_poll_at.is_none()); + + rebuilt_scroll_live_stream.prime_monitor_nonblocking(monitor); + + assert_eq!( + session.scroll_capture.live_stream.as_ref().unwrap().debug_last_request_kind(), + Some("prime_monitor_nonblocking") + ); +} + +#[cfg(target_os = "macos")] +#[test] +fn armed_freeze_capture_without_worker_restores_visibility_and_surfaces_error() { + let monitor = tests::test_monitor(); + let mut session = OverlaySession::new(); + + session.state.begin_freeze(monitor); + + session.pending_freeze_capture = Some(monitor); + session.pending_freeze_capture_armed = true; + session.capture_windows_hidden = true; + + session.maybe_dispatch_armed_freeze_capture(); + + assert!(session.pending_freeze_capture.is_none()); + assert!(session.inflight_freeze_capture.is_none()); + assert!(!session.pending_freeze_capture_armed); + assert!(!session.capture_windows_hidden); + assert_eq!(session.state.error_message.as_deref(), Some("Capture worker is unavailable.")); +} + +#[cfg(target_os = "macos")] +#[test] +fn repeated_freeze_capture_send_full_aborts_and_restores_hidden_windows() { + let monitor = tests::test_monitor(); + let mut session = OverlaySession::new(); + + session.state.begin_freeze(monitor); + + session.pending_freeze_capture = Some(monitor); + session.pending_freeze_capture_armed = true; + session.capture_windows_hidden = true; + + for _ in 0..FREEZE_CAPTURE_SEND_FULL_RETRY_LIMIT.saturating_sub(1) { + session.handle_freeze_capture_request_send_error(monitor, WorkerRequestSendError::Full); + + assert_eq!(session.pending_freeze_capture, Some(monitor)); + assert!(session.pending_freeze_capture_armed); + assert!(session.capture_windows_hidden); + assert!(session.state.error_message.is_none()); + } + + session.handle_freeze_capture_request_send_error(monitor, WorkerRequestSendError::Full); + + assert!(session.pending_freeze_capture.is_none()); + assert!(session.inflight_freeze_capture.is_none()); + assert!(!session.pending_freeze_capture_armed); + assert!(!session.capture_windows_hidden); + assert_eq!(session.freeze_capture_send_full_count, 0); + assert_eq!( + session.state.error_message.as_deref(), + Some("Capture worker is busy. Please try again.") + ); +} + +#[cfg(target_os = "macos")] +#[test] +fn showing_loupe_window_requests_lazy_creation_before_applying_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()); @@ -144,7 +274,8 @@ fn showing_loupe_window_applies_pending_startup_live_stream_upgrade() { session.set_alt_loupe_window_visible(Some(monitor), true); - assert!(!session.pending_startup_aux_live_stream_filter_upgrade); + assert!(session.pending_startup_aux_live_stream_filter_upgrade); + assert!(session.startup_aux_window_creation_pending); assert_eq!( ptr::from_ref(session.live_sample_stream.as_ref().unwrap()), original_live_sample_stream @@ -153,10 +284,7 @@ fn showing_loupe_window_applies_pending_startup_live_stream_upgrade() { 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.live_sample_stream.as_ref().unwrap().debug_last_request_kind(), None); assert_eq!(session.worker.as_ref().unwrap().debug_id(), original_worker_debug_id); } @@ -273,6 +401,26 @@ fn captured_freeze_response_applies_deferred_worker_refresh() { assert!(!session.pending_self_capture_exception_window_ids_worker_refresh); } +#[cfg(target_os = "macos")] +#[test] +fn freeze_error_response_applies_deferred_worker_refresh() { + let monitor = tests::test_monitor(); + let (mut session, original_worker_debug_id) = tests::configured_session_with_macos_worker(); + + session.inflight_freeze_capture = Some(monitor); + session.pending_self_capture_exception_window_ids_worker_refresh = true; + + let control = session.maybe_tick_worker_response_limiter(WorkerResponse::Error { + source: WorkerErrorSource::FreezeCapture, + message: String::from("freeze failed"), + }); + + assert!(matches!(control, super::OverlayControl::Continue)); + assert_ne!(session.worker.as_ref().unwrap().debug_id(), original_worker_debug_id); + assert!(!session.pending_self_capture_exception_window_ids_worker_refresh); + assert_eq!(session.state.error_message.as_deref(), Some("freeze failed")); +} + #[cfg(target_os = "macos")] #[test] fn hit_test_response_applies_deferred_worker_refresh() { diff --git a/packages/rsnap-overlay/src/overlay/window_runtime.rs b/packages/rsnap-overlay/src/overlay/window_runtime.rs index 600949cc..2c6da24f 100644 --- a/packages/rsnap-overlay/src/overlay/window_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/window_runtime.rs @@ -58,7 +58,15 @@ impl OverlaySession { } let gpu_init_ms = gpu_init_started_at.elapsed().as_millis(); + #[cfg(target_os = "macos")] + let reused_prewarmed_windows = self.has_matching_prewarmed_startup_resources(&monitors); let window_creation = self.create_startup_windows(event_loop, &monitors)?; + + #[cfg(target_os = "macos")] + if !reused_prewarmed_windows { + self.refresh_startup_live_stream_after_window_creation(startup_monitor); + } + let prime_cursor_started_at = Instant::now(); self.prime_startup_cursor_context(startup_cursor, startup_monitor); @@ -210,7 +218,7 @@ impl OverlaySession { #[cfg(target_os = "macos")] { - self.startup_aux_window_creation_pending = true; + self.startup_aux_window_creation_pending = false; self.startup_aux_window_creation_scheduled = false; Ok(StartupWindowCreationMetrics { @@ -280,13 +288,25 @@ impl OverlaySession { } } + #[cfg(target_os = "macos")] + pub(super) fn refresh_startup_live_stream_after_window_creation( + &mut self, + startup_monitor: Option, + ) { + self.live_sample_stream = Some(MacLiveFrameStream::with_self_capture_exception_window_ids( + self.config.self_capture_exception_window_ids.clone(), + )); + + self.prime_startup_live_stream_nonblocking(startup_monitor); + } + #[cfg(target_os = "macos")] /// Completes creation of non-critical auxiliary windows after the first overlay frame. pub fn finish_startup_aux_window_creation( &mut self, event_loop: &ActiveEventLoop, ) -> Result<(), String> { - if !self.startup_aux_window_creation_pending { + if !self.startup_aux_window_creation_pending && !self.aux_window_creation_needed() { return Ok(()); } @@ -294,17 +314,17 @@ impl OverlaySession { let mut created_aux_windows = false; - if self.loupe_window.is_none() { + if self.loupe_window.is_none() && self.loupe_window_needed() { self.create_loupe_window(event_loop)?; created_aux_windows = true; } - if self.toolbar_window.is_none() { + if self.toolbar_window.is_none() && self.toolbar_window_needed() { self.create_toolbar_window(event_loop)?; created_aux_windows = true; } - if self.scroll_preview_window.is_none() { + if self.scroll_preview_window.is_none() && self.scroll_preview_window_needed() { self.create_scroll_preview_window(event_loop)?; created_aux_windows = true; @@ -312,19 +332,68 @@ impl OverlaySession { self.complete_startup_aux_window_creation(created_aux_windows); - if self.state.alt_held && matches!(self.state.mode, OverlayMode::Live) { + if created_aux_windows + && let Some(monitor) = self.scroll_capture.monitor + && self.rebuild_active_scroll_capture_live_stream() + && let Some(live_stream) = self.scroll_capture.live_stream.as_ref() + { + live_stream.prime_monitor_nonblocking(monitor); + } + if self.loupe_window_needed() { self.set_alt_loupe_window_visible(self.active_cursor_monitor(), true); } - if self.toolbar_state.visible { + if self.toolbar_window_needed() { self.request_redraw_toolbar_window(); } - if self.scroll_capture.active { + if self.scroll_preview_window_needed() { + if let Some(monitor) = self.scroll_capture.monitor { + self.position_scroll_preview_window(monitor); + } + self.request_redraw_scroll_preview_window(); } Ok(()) } + #[cfg(target_os = "macos")] + fn loupe_window_needed(&self) -> bool { + matches!(self.state.mode, OverlayMode::Live) + && self.state.alt_held + && !self.live_loupe_uses_hud_window() + } + + #[cfg(target_os = "macos")] + fn toolbar_window_needed(&self) -> bool { + matches!(self.state.mode, OverlayMode::Frozen) + && self.toolbar_state.visible + && self.authoritative_frozen_capture_ready + && self.state.frozen_image.is_some() + } + + #[cfg(target_os = "macos")] + fn scroll_preview_window_needed(&self) -> bool { + self.scroll_capture.active + } + + #[cfg(target_os = "macos")] + fn aux_window_creation_needed(&self) -> bool { + (self.loupe_window.is_none() && self.loupe_window_needed()) + || (self.toolbar_window.is_none() && self.toolbar_window_needed()) + || (self.scroll_preview_window.is_none() && self.scroll_preview_window_needed()) + } + + #[cfg(target_os = "macos")] + pub(super) fn request_aux_window_creation_if_needed(&mut self) { + if !self.aux_window_creation_needed() { + return; + } + + self.startup_aux_window_creation_pending = true; + + self.maybe_schedule_startup_aux_window_creation(); + } + #[cfg(target_os = "macos")] pub(super) fn complete_startup_aux_window_creation(&mut self, created_aux_windows: bool) { self.startup_aux_window_creation_pending = false; diff --git a/packages/rsnap-overlay/src/overlay/worker_runtime.rs b/packages/rsnap-overlay/src/overlay/worker_runtime.rs index 84cbe660..3cc317c6 100644 --- a/packages/rsnap-overlay/src/overlay/worker_runtime.rs +++ b/packages/rsnap-overlay/src/overlay/worker_runtime.rs @@ -3,13 +3,120 @@ use crate::overlay::CursorSampleRequest; #[allow(unused_imports)] use crate::overlay::{ - Arc, CURSOR_POLL_INTERVAL_MIN, CapturedMonitorRegionResult, Duration, GlobalPoint, Instant, - LiveCursorSample, LiveSampleApplyResult, MonitorRect, MonitorRectPoints, OverlayControl, - OverlayMode, OverlaySession, WindowFreezeCaptureTarget, WindowHit, WindowListSnapshot, - WorkerErrorSource, WorkerRequestSendError, WorkerResponse, mem, + Arc, CURSOR_POLL_INTERVAL_MIN, CapturedMonitorRegionResult, Duration, FreezeCaptureTarget, + GlobalPoint, Instant, LiveCursorSample, LiveSampleApplyResult, MonitorRect, MonitorRectPoints, + OverlayControl, OverlayMode, OverlaySession, WindowFreezeCaptureTarget, WindowHit, + WindowListSnapshot, WorkerErrorSource, WorkerRequestSendError, WorkerResponse, mem, }; +pub(super) const FREEZE_CAPTURE_SEND_FULL_RETRY_LIMIT: u64 = 8; + impl OverlaySession { + fn clear_freeze_capture_tracking(&mut self) { + self.pending_freeze_capture = None; + self.inflight_freeze_capture = None; + self.pending_freeze_capture_armed = false; + self.pending_window_freeze_capture = None; + self.inflight_window_freeze_capture = None; + self.freeze_capture_send_full_count = 0; + } + + pub(super) fn abort_pending_freeze_capture(&mut self, message: impl Into) { + let message = message.into(); + + self.clear_freeze_capture_tracking(); + self.restore_capture_windows_visibility(); + self.state.set_error(message); + self.request_redraw_all(); + } + + pub(super) fn note_freeze_capture_request_started( + &mut self, + overlay_monitor: MonitorRect, + pending_window_target: Option, + ) { + self.pending_freeze_capture = None; + self.pending_freeze_capture_armed = false; + self.inflight_freeze_capture = Some(overlay_monitor); + self.inflight_window_freeze_capture = pending_window_target; + self.pending_window_freeze_capture = None; + self.freeze_capture_send_full_count = 0; + } + + pub(super) fn handle_freeze_capture_request_send_error( + &mut self, + overlay_monitor: MonitorRect, + err: WorkerRequestSendError, + ) { + match err { + WorkerRequestSendError::Full => { + self.freeze_capture_send_full_count = + self.freeze_capture_send_full_count.saturating_add(1); + + tracing::debug!( + monitor_id = overlay_monitor.id, + full_count = self.freeze_capture_send_full_count, + "Freeze capture request dropped: worker queue full." + ); + + if self.freeze_capture_send_full_count >= FREEZE_CAPTURE_SEND_FULL_RETRY_LIMIT { + self.abort_pending_freeze_capture("Capture worker is busy. Please try again."); + } else { + self.schedule_egui_repaint_after( + self.repaint_interval_for_monitor(Some(overlay_monitor)), + ); + } + }, + WorkerRequestSendError::Disconnected => { + tracing::warn!( + monitor_id = overlay_monitor.id, + "Freeze capture request failed: worker disconnected before capture could start." + ); + + self.abort_pending_freeze_capture("Capture worker is unavailable."); + }, + } + } + + #[cfg(target_os = "macos")] + pub(super) fn maybe_dispatch_armed_freeze_capture(&mut self) { + if !self.pending_freeze_capture_armed { + return; + } + + let Some(overlay_monitor) = self.pending_freeze_capture else { + self.pending_freeze_capture_armed = false; + self.freeze_capture_send_full_count = 0; + + return; + }; + + if !self.pending_freeze_capture_matches(overlay_monitor) { + self.pending_freeze_capture_armed = false; + self.freeze_capture_send_full_count = 0; + + return; + } + + let Some(worker) = &self.worker else { + self.abort_pending_freeze_capture("Capture worker is unavailable."); + + return; + }; + let pending_window_target = + self.pending_window_freeze_capture.filter(|target| target.monitor == overlay_monitor); + let freeze_target = pending_window_target.map_or(FreezeCaptureTarget::Monitor, |target| { + FreezeCaptureTarget::Window { window_id: target.window_id } + }); + + match worker.request_freeze_capture(overlay_monitor, freeze_target) { + Ok(()) => { + self.note_freeze_capture_request_started(overlay_monitor, pending_window_target); + }, + Err(err) => self.handle_freeze_capture_request_send_error(overlay_monitor, err), + } + } + pub(super) fn drain_worker_responses(&mut self) -> OverlayControl { #[cfg(target_os = "macos")] if self.worker.is_none() && self.live_sample_worker.is_none() { @@ -515,15 +622,13 @@ impl OverlaySession { OverlayControl::Continue }, WorkerResponse::Error { source, message } => { + let mut error_already_handled = false; + match source { WorkerErrorSource::FreezeCapture => { - self.pending_freeze_capture = None; - self.inflight_freeze_capture = None; - self.pending_freeze_capture_armed = false; - self.pending_window_freeze_capture = None; - self.inflight_window_freeze_capture = None; + self.abort_pending_freeze_capture(message.as_str()); - self.restore_capture_windows_visibility(); + error_already_handled = true; }, WorkerErrorSource::RefreshWindowList => { #[cfg(target_os = "macos")] @@ -546,8 +651,10 @@ impl OverlaySession { }, } - self.state.set_error(message); - self.request_redraw_all(); + if !error_already_handled { + self.state.set_error(message); + self.request_redraw_all(); + } OverlayControl::Continue }, diff --git a/packages/rsnap-overlay/src/worker.rs b/packages/rsnap-overlay/src/worker.rs index 5274dc89..fd8e2d4b 100644 --- a/packages/rsnap-overlay/src/worker.rs +++ b/packages/rsnap-overlay/src/worker.rs @@ -400,8 +400,10 @@ impl OverlayWorker { &self, monitor: MonitorRect, target: FreezeCaptureTarget, - ) -> bool { - self.req_tx.try_send(WorkerRequest::FreezeCapture { monitor, target }).is_ok() + ) -> Result<(), WorkerRequestSendError> { + let request = WorkerRequest::FreezeCapture { monitor, target }; + + self.req_tx.try_send(request).map_err(Self::map_try_send_error) } pub(crate) fn request_hit_test_window(