From 6b2974d0faae3119105b2a4b8055205e2a213a10 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Fri, 2 Oct 2026 07:57:15 +0100 Subject: [PATCH] Track displayed surface visibility for idle inhibition --- compositor/src/backends/direct.rs | 19 ++++ compositor/src/display_presentation.rs | 142 +++++++++++++++++++++++++ compositor/src/handlers/compositor.rs | 19 ++++ compositor/src/handlers/mod.rs | 2 +- compositor/src/main.rs | 1 + compositor/src/portal_session.rs | 8 +- compositor/src/session_lock.rs | 4 + compositor/src/state.rs | 2 + compositor/src/state/layout.rs | 5 + compositor/src/state/outputs.rs | 3 + compositor/src/winit.rs | 4 + 11 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 compositor/src/display_presentation.rs diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 29d2b20..982d648 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -289,6 +289,9 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() }); } + state.display_presentation.clear(); + state.refresh_idle_inhibition(); + libinput_context.suspend(); tracing::info!("direct session paused"); } @@ -594,6 +597,18 @@ fn open_primary_device( } } + if retired { + if let Some(output) = state + .direct_backend + .as_ref() + .and_then(|backend| backend.devices.get(&node)) + .and_then(|device| device.outputs.get(&crtc)) + { + state.display_presentation.presented(&output.output); + } + state.refresh_idle_inhibition(); + } + state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence); tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip"); let active = state.direct_backend.as_ref().is_some_and(|backend| backend.active); @@ -661,6 +676,7 @@ pub(crate) fn sleep_locked_outputs(state: &mut Ferese) { Ok(()) => { cancel_output_timers(&state.loop_handle, output); output.power_off = true; + state.display_presentation.remove_output(&output.output); } Err(error) => { @@ -669,6 +685,7 @@ pub(crate) fn sleep_locked_outputs(state: &mut Ferese) { } } } + state.refresh_idle_inhibition(); } pub(crate) fn wake_locked_outputs(state: &mut Ferese) { @@ -983,8 +1000,10 @@ fn render_output( &result.states, Kind::Vsync | Kind::HwClock | Kind::HwCompletion, ); + let visibility = result.states.clone(); drop(result); output.surface.queue_frame(presentation)?; + state.display_presentation.queued(&output.output, &visibility); output.primary_commit = primary_commit; output .render_metrics diff --git a/compositor/src/display_presentation.rs b/compositor/src/display_presentation.rs new file mode 100644 index 0000000..d9b5ea9 --- /dev/null +++ b/compositor/src/display_presentation.rs @@ -0,0 +1,142 @@ +//! Visibility of client content on displayed outputs, independent of capture. +use std::collections::{HashMap, HashSet}; + +use smithay::backend::renderer::element::{Id, RenderElementPresentationState, RenderElementStates}; +use smithay::output::Output; + +#[derive(Default)] +struct OutputPresentation { + displayed: HashSet, + queued: Option>, +} + +#[derive(Default)] +pub(crate) struct DisplayPresentation { + outputs: HashMap, +} + +fn visible(states: &RenderElementStates) -> HashSet { + states + .states + .iter() + .filter_map(|(id, state)| { + (state.visible_area > 0 && state.presentation_state != RenderElementPresentationState::Skipped) + .then(|| id.clone()) + }) + .collect() +} + +impl DisplayPresentation { + // Call only after a successful submission, never from an offscreen capture. + pub fn queued(&mut self, output: &Output, states: &RenderElementStates) { + self.outputs.entry(output.clone()).or_default().queued = Some(visible(states)); + } + + pub fn presented(&mut self, output: &Output) { + if let Some(record) = self.outputs.get_mut(output) + && let Some(queued) = record.queued.take() + { + record.displayed = queued; + } + } + + pub fn visible(&self, surface: &Id) -> bool { + self.outputs.values().any(|record| record.displayed.contains(surface)) + } + + pub fn remove_surface(&mut self, surface: &Id) { + for record in self.outputs.values_mut() { + record.displayed.remove(surface); + if let Some(queued) = &mut record.queued { + queued.remove(surface); + } + } + } + + pub fn remove_output(&mut self, output: &Output) { + self.outputs.remove(output); + } + + pub fn clear(&mut self) { + self.outputs.clear(); + } +} + +#[cfg(test)] +mod tests { + use smithay::backend::renderer::element::RenderElementState; + use smithay::output::{PhysicalProperties, Subpixel}; + + use super::*; + + fn output(name: &str) -> Output { + Output::new( + name.into(), + PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "test".into(), + model: "test".into(), + }, + ) + } + + fn scene(id: &Id) -> RenderElementStates { + let mut states = RenderElementStates::default(); + states.states.insert( + id.clone(), + RenderElementState { + visible_area: 20, + presentation_state: RenderElementPresentationState::ZeroCopy, + }, + ); + states + } + + #[test] + fn visibility_changes_only_when_a_submission_is_presented() { + let mut record = DisplayPresentation::default(); + let output = output("a"); + let id = Id::new(); + record.queued(&output, &scene(&id)); + assert!(!record.visible(&id)); + record.presented(&output); + assert!(record.visible(&id)); + + record.queued(&output, &RenderElementStates::default()); + assert!(record.visible(&id)); + record.presented(&output); + assert!(!record.visible(&id)); + } + + #[test] + fn outputs_are_independent_and_removal_cannot_resurrect_a_surface() { + 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); + } + record.remove_output(&a); + assert!(record.visible(&id)); + record.queued(&b, &scene(&id)); + record.remove_surface(&id); + record.presented(&b); + assert!(!record.visible(&id)); + record.clear(); + assert!(!record.visible(&id)); + } + + #[test] + fn occluded_and_skipped_content_do_not_count_as_visible() { + let id = Id::new(); + let mut states = scene(&id); + states.states.get_mut(&id).unwrap().visible_area = 0; + assert!(visible(&states).is_empty()); + states.states.get_mut(&id).unwrap().visible_area = 20; + states.states.get_mut(&id).unwrap().presentation_state = RenderElementPresentationState::Skipped; + assert!(visible(&states).is_empty()); + } +} diff --git a/compositor/src/handlers/compositor.rs b/compositor/src/handlers/compositor.rs index 861ba01..757204b 100644 --- a/compositor/src/handlers/compositor.rs +++ b/compositor/src/handlers/compositor.rs @@ -71,12 +71,31 @@ impl CompositorHandler for Ferese { self.record_client_commit(&window); } } + if !is_sync_subsurface(surface) { + let mut surfaces = Vec::new(); + smithay::desktop::utils::with_surfaces_surface_tree(surface, |surface, _| surfaces.push(surface.clone())); + for surface in surfaces { + let mapped = smithay::backend::renderer::utils::with_renderer_surface_state(&surface, |state| { + state.buffer().is_some() + }) + .unwrap_or(false); + if !mapped { + smithay::desktop::utils::with_surfaces_surface_tree(&surface, |child, _| { + self.display_presentation.remove_surface(&child.into()); + }); + } + } + self.refresh_idle_inhibition(); + } + layer_shell::handle_commit(self, surface); xdg_shell::handle_commit(&mut self.popups, &mut self.space, surface); crate::backends::direct::render_surface(self, surface); } fn destroyed(&mut self, surface: &WlSurface) { + self.display_presentation.remove_surface(&surface.into()); + if self.session_lock.active { self.session_lock .surfaces diff --git a/compositor/src/handlers/mod.rs b/compositor/src/handlers/mod.rs index b803407..2903401 100644 --- a/compositor/src/handlers/mod.rs +++ b/compositor/src/handlers/mod.rs @@ -96,7 +96,7 @@ impl IdleInhibitHandler for Ferese { fn inhibit(&mut self, surface: WlSurface) { let count = self.idle_inhibitors.entry(surface).or_default(); *count = count.saturating_add(1); - self.idle_notifier_state.set_is_inhibited(true); + self.refresh_idle_inhibition(); } fn uninhibit(&mut self, surface: WlSurface) { diff --git a/compositor/src/main.rs b/compositor/src/main.rs index 04cde41..e56b3a1 100644 --- a/compositor/src/main.rs +++ b/compositor/src/main.rs @@ -5,6 +5,7 @@ mod config; mod cursor; mod daemon; mod dimming; +mod display_presentation; mod effects; mod floating; mod frame_scheduler; diff --git a/compositor/src/portal_session.rs b/compositor/src/portal_session.rs index 020d0f6..dd317e7 100644 --- a/compositor/src/portal_session.rs +++ b/compositor/src/portal_session.rs @@ -235,8 +235,12 @@ impl PortalSession { impl crate::Ferese { pub(crate) fn refresh_idle_inhibition(&mut self) { - self.idle_notifier_state - .set_is_inhibited(!self.idle_inhibitors.is_empty() || self.portal_session.idle_inhibited()); + self.idle_notifier_state.set_is_inhibited( + self.idle_inhibitors + .keys() + .any(|surface| self.display_presentation.visible(&surface.into())) + || self.portal_session.idle_inhibited(), + ); } pub(crate) fn end_portal_session(&mut self) { diff --git a/compositor/src/session_lock.rs b/compositor/src/session_lock.rs index 0435c03..3ea715f 100644 --- a/compositor/src/session_lock.rs +++ b/compositor/src/session_lock.rs @@ -236,6 +236,8 @@ impl SessionLockHandler for Ferese { } self.cancel_logout_confirmation(); self.session_lock.active = true; + self.display_presentation.clear(); + self.refresh_idle_inhibition(); self.session_lock.idle_since = Some(Instant::now()); match self .loop_handle @@ -302,6 +304,8 @@ impl SessionLockHandler for Ferese { } let sleeping = self.session_lock.sleeping; self.session_lock = Lock::default(); + self.display_presentation.clear(); + self.refresh_idle_inhibition(); if sleeping { crate::backends::direct::wake_locked_outputs(self); } diff --git a/compositor/src/state.rs b/compositor/src/state.rs index cf8a4d8..29a4321 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -334,6 +334,7 @@ pub struct Ferese { output_identity_ids: HashMap, pub(crate) windows: WindowRegistry, pub(crate) render: crate::render::RenderResources, + pub(crate) display_presentation: crate::display_presentation::DisplayPresentation, pub(crate) nested_backend: Option, pub(crate) wallpaper: WallpaperState, window_stack: WindowStack, @@ -561,6 +562,7 @@ impl Ferese { output_identity_ids: HashMap::new(), windows: WindowRegistry::default(), render: Default::default(), + display_presentation: Default::default(), nested_backend: None, wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())), window_stack: WindowStack::default(), diff --git a/compositor/src/state/layout.rs b/compositor/src/state/layout.rs index d7869e5..c95f92f 100644 --- a/compositor/src/state/layout.rs +++ b/compositor/src/state/layout.rs @@ -44,8 +44,13 @@ impl Ferese { geometry.settle_presentation(); } + window.with_surfaces(|surface, _| { + self.display_presentation.remove_surface(&surface.into()); + }); self.space.unmap_elem(&window); } + self.refresh_idle_inhibition(); + changed } diff --git a/compositor/src/state/outputs.rs b/compositor/src/state/outputs.rs index f9f0052..f8f7272 100644 --- a/compositor/src/state/outputs.rs +++ b/compositor/src/state/outputs.rs @@ -53,6 +53,9 @@ impl Ferese { } pub fn unregister_output(&mut self, output: &Output) { + self.display_presentation.remove_output(output); + self.refresh_idle_inhibition(); + let Some(output_id) = self.output_ids.remove(output) else { return; }; diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 869a7ee..f9763a5 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -183,6 +183,10 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() state.loop_signal.stop(); return; } + state.display_presentation.queued(&output, &rendered_states); + state.display_presentation.presented(&output); + state.refresh_idle_inhibition(); + if state.session_lock.active { state.lock_frame_presented(&output); }