From 2f63cae54cd2d544f533a734963ff803985c6514 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Fri, 2 Oct 2026 07:59:41 +0100 Subject: [PATCH] Gate display frame callbacks on shared presentation visibility --- compositor/src/backends/direct.rs | 8 ++++---- compositor/src/display_presentation.rs | 27 ++++++++++++++++++++++++++ compositor/src/session_lock.rs | 4 ++-- compositor/src/state/hit_testing.rs | 10 +++------- compositor/src/winit.rs | 14 ++++++++----- 5 files changed, 45 insertions(+), 18 deletions(-) diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 982d648..7be7e1c 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -1552,14 +1552,14 @@ fn send_frame_callbacks(state: &mut Ferese, output: &Output) { .is_some_and(|id| state.window_belongs_to_output(*id, output)) }) .for_each(|window| { - window.send_frame(output, state.start_time.elapsed(), Some(Duration::ZERO), |_, _| { - Some(output.clone()) + window.send_frame(output, state.start_time.elapsed(), None, |surface, _| { + state.display_presentation.callback_output(&surface.into()) }); }); let layers = layer_map_for_output(output).layers().cloned().collect::>(); layers.iter().for_each(|layer| { - layer.send_frame(output, state.start_time.elapsed(), Some(Duration::ZERO), |_, _| { - Some(output.clone()) + layer.send_frame(output, state.start_time.elapsed(), None, |surface, _| { + state.display_presentation.callback_output(&surface.into()) }); }); state.send_cursor_frame(output); diff --git a/compositor/src/display_presentation.rs b/compositor/src/display_presentation.rs index d9b5ea9..3e80d7b 100644 --- a/compositor/src/display_presentation.rs +++ b/compositor/src/display_presentation.rs @@ -44,6 +44,17 @@ impl DisplayPresentation { self.outputs.values().any(|record| record.displayed.contains(surface)) } + // Membership describes every output; callback ownership is a separate choice. + // A stable tie-break avoids alternating owners when refresh rates match. + pub fn callback_output(&self, surface: &Id) -> Option { + self.outputs + .iter() + .filter(|(_, record)| record.displayed.contains(surface)) + .map(|(output, _)| output) + .max_by_key(|output| (output.current_mode().map_or(0, |mode| mode.refresh), output.name())) + .cloned() + } + pub fn remove_surface(&mut self, surface: &Id) { for record in self.outputs.values_mut() { record.displayed.remove(surface); @@ -139,4 +150,20 @@ mod tests { states.states.get_mut(&id).unwrap().presentation_state = RenderElementPresentationState::Skipped; assert!(visible(&states).is_empty()); } + #[test] + fn callback_ownership_is_stable_and_moves_when_an_output_disappears() { + let mut record = DisplayPresentation::default(); + let a = output("a"); + let b = output("b"); + let id = Id::new(); + for output in [&a, &b] { + record.queued(output, &scene(&id)); + record.presented(output); + } + assert_eq!(record.callback_output(&id), Some(b.clone())); + record.remove_output(&b); + assert_eq!(record.callback_output(&id), Some(a.clone())); + record.remove_surface(&id); + assert_eq!(record.callback_output(&id), None); + } } diff --git a/compositor/src/session_lock.rs b/compositor/src/session_lock.rs index 3ea715f..cbcd927 100644 --- a/compositor/src/session_lock.rs +++ b/compositor/src/session_lock.rs @@ -218,8 +218,8 @@ impl Ferese { surface.wl_surface(), output, self.start_time.elapsed(), - Some(std::time::Duration::ZERO), - |_, _| Some(output.clone()), + None, + |surface, _| self.display_presentation.callback_output(&surface.into()), ); } } diff --git a/compositor/src/state/hit_testing.rs b/compositor/src/state/hit_testing.rs index 3b3e881..a278f3b 100644 --- a/compositor/src/state/hit_testing.rs +++ b/compositor/src/state/hit_testing.rs @@ -119,13 +119,9 @@ impl Ferese { return; } - send_frames_surface_tree( - surface, - output, - self.start_time.elapsed(), - Some(Duration::ZERO), - |_, _| Some(output.clone()), - ); + send_frames_surface_tree(surface, output, self.start_time.elapsed(), None, |surface, _| { + self.display_presentation.callback_output(&surface.into()) + }); } pub fn window_under_visual(&self, position: Point) -> Option { diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index f9763a5..fc10920 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -163,7 +163,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() // A callback means permission to draw the next client // frame, not proof of a new compositor presentation. // No-damage frames must still unblock layer clients. - send_nested_frame_callbacks(state, &output, refresh.get()); + send_nested_frame_callbacks(state, &output); state.space.refresh(); state.popups.cleanup(); layer_map_for_output(&output).cleanup(); @@ -209,7 +209,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() sequence, PresentationKind::Vsync, ); - send_nested_frame_callbacks(state, &output, refresh.get()); + send_nested_frame_callbacks(state, &output); state.space.refresh(); state.popups.cleanup(); layer_map_for_output(&output).cleanup(); @@ -224,7 +224,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() Ok(()) } -fn send_nested_frame_callbacks(state: &mut Ferese, output: &Output, refresh: Duration) { +fn send_nested_frame_callbacks(state: &mut Ferese, output: &Output) { if state.session_lock.active { state.lock_frame_callbacks(output); return; @@ -237,10 +237,14 @@ fn send_nested_frame_callbacks(state: &mut Ferese, output: &Output, refresh: Dur .get(*window) .is_some_and(|id| state.window_belongs_to_output(*id, output)) }) { - window.send_frame(output, time, Some(refresh), |_, _| Some(output.clone())); + window.send_frame(output, time, None, |surface, _| { + state.display_presentation.callback_output(&surface.into()) + }); } for layer in layer_surfaces(output) { - layer.send_frame(output, time, Some(refresh), |_, _| Some(output.clone())); + layer.send_frame(output, time, None, |surface, _| { + state.display_presentation.callback_output(&surface.into()) + }); } state.send_cursor_frame(output); }