diff --git a/niri-config/src/window_rule.rs b/niri-config/src/window_rule.rs index 0465d28fa..85fdf526c 100644 --- a/niri-config/src/window_rule.rs +++ b/niri-config/src/window_rule.rs @@ -72,6 +72,10 @@ pub struct WindowRule { pub scroll_factor: Option>, #[knuffel(child, unwrap(argument))] pub tiled_state: Option, + #[knuffel(child, unwrap(argument))] + pub force_render: Option, + #[knuffel(child, unwrap(argument))] + pub force_render_fps: Option, } #[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)] diff --git a/src/niri.rs b/src/niri.rs index 9c59977f4..4a6ec1790 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -420,6 +420,7 @@ pub struct Niri { /// Window ID for the "dynamic cast" special window for the xdp-gnome picker. #[cfg(feature = "xdp-gnome-screencast")] pub dynamic_cast_id_for_portal: MappedId, + force_render_state: RefCell>, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -572,6 +573,20 @@ struct SurfaceFrameThrottlingState { last_sent_at: RefCell>, } +struct ForceRenderState { + last_time: Duration, + is_delay: bool, +} + +impl Default for ForceRenderState { + fn default() -> Self { + Self { + last_time: Duration::ZERO, + is_delay: false, + } + } +} + pub enum CenterCoords { Separately, Both, @@ -2822,6 +2837,7 @@ impl Niri { #[cfg(feature = "xdp-gnome-screencast")] dynamic_cast_id_for_portal: MappedId::next(), + force_render_state: RefCell::new(HashMap::new()), }; niri.reset_pointer_inactivity_timer(); @@ -4956,6 +4972,46 @@ impl Niri { } } + pub fn delay_to_send_frame_callbacks( + &mut self, + surface: WlSurface, + output: Output, + interval: Duration, + ) { + let timer = Timer::from_duration(interval); + self.event_loop + .insert_source(timer, move |_, _, state| { + state + .niri + .send_frame_callback_for_surface(surface.clone(), &output); + TimeoutAction::Drop + }) + .unwrap(); + } + + fn send_frame_callback_for_surface(&mut self, surface: WlSurface, output: &Output) { + let frame_callback_time = get_monotonic_time(); + let mut force_render_state_borrow = self.force_render_state.borrow_mut(); + + let force_render_state = force_render_state_borrow + .entry(surface.id().protocol_id()) + .or_default(); + + // reset the delay state + force_render_state.last_time = frame_callback_time; + force_render_state.is_delay = false; + + debug!("Sending frame callback for surface"); + + send_frames_surface_tree( + &surface, + output, + frame_callback_time, + FRAME_CALLBACK_THROTTLE, + |_, _| Some(output.clone()), + ); + } + pub fn send_frame_callbacks(&mut self, output: &Output) { let _span = tracy_client::span!("Niri::send_frame_callbacks"); @@ -4997,13 +5053,97 @@ impl Niri { let frame_callback_time = get_monotonic_time(); + // Collect delayed surfaces + let delayed_surfaces = RefCell::new(HashMap::new()); + for mapped in self.layout.windows_for_output_mut(output) { - mapped.send_frame( - output, - frame_callback_time, - FRAME_CALLBACK_THROTTLE, - should_send, - ); + // Check if the surface should be forced to render + if mapped.rules().force_render == Some(true) || mapped.is_window_cast_target() { + // Calculate delay time + let interval = if let Some(force_render_fps) = mapped.rules().force_render_fps { + Duration::from_secs_f64(1.0 / force_render_fps as f64) + } else { + Duration::ZERO + }; + + // Check if the surface should be forced to render + let should_force_render = |surface: &WlSurface, states: &SurfaceData| { + // Check if the surface is on the primary output. + let mut on_primary = true; + let current_primary_output = surface_primary_scanout_output(surface, states); + if current_primary_output.as_ref() != Some(output) { + on_primary = false; + } + + let mut force_render_state_borrow = self.force_render_state.borrow_mut(); + + if !on_primary && !interval.is_zero() { + //fps limited branch + let force_render_state = force_render_state_borrow + .entry(surface.id().protocol_id()) + .or_default(); + + if !force_render_state.is_delay { + // Time diff since last frame callback + let time_diff: Duration = + frame_callback_time.saturating_sub(force_render_state.last_time); + + // Calculate next frame callback time and push needed data to delayed_surfaces + let delay_time = interval.saturating_sub(time_diff); + let mut delayed = delayed_surfaces.borrow_mut(); + delayed + .entry(surface.id()) + .or_insert((surface.clone(), delay_time)); + + // Set force render state + force_render_state.is_delay = true; + } + return None; + } else { + // Remove force render state if not on primary output + if on_primary { + force_render_state_borrow.remove(&surface.id().protocol_id()); + } + //fps unlimited branch + let frame_throttling_state = states + .data_map + .get_or_insert(SurfaceFrameThrottlingState::default); + + // Next, check the throttling status. + let mut last_sent_at = frame_throttling_state.last_sent_at.borrow_mut(); + let mut send = true; + + // If we already sent a frame callback to this surface this output refresh + // cycle, don't send one again to prevent empty-damage commit busy loops. + if let Some((last_output, last_sequence)) = &*last_sent_at { + if last_output == output && *last_sequence == sequence { + send = false; + } + } + + if send { + *last_sent_at = Some((output.clone(), sequence)); + Some(output.clone()) + } else { + None + } + } + }; + + mapped.send_frame( + output, + frame_callback_time, + FRAME_CALLBACK_THROTTLE, + should_force_render, + ); + } else { + mapped.send_frame( + output, + frame_callback_time, + FRAME_CALLBACK_THROTTLE, + should_send, + ); + } } for surface in layer_map_for_output(output).layers() { @@ -5044,6 +5184,12 @@ impl Niri { should_send, ); } + + // Call delay timer + let delayed_map = delayed_surfaces.into_inner(); + for (_, (surface, interval)) in delayed_map { + self.delay_to_send_frame_callbacks(surface, output.clone(), interval); + } } pub fn send_frame_callbacks_on_fallback_timer(&mut self) { diff --git a/src/window/mod.rs b/src/window/mod.rs index c8c358f15..77b281e7c 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -119,6 +119,12 @@ pub struct ResolvedWindowRules { /// Override whether to set the Tiled xdg-toplevel state on the window. pub tiled_state: Option, + + /// Whether to force a render on this window. + pub force_render: Option, + + /// force render fps limit for this window. + pub force_render_fps: Option, } impl<'a> WindowRef<'a> { @@ -296,6 +302,14 @@ impl ResolvedWindowRules { if let Some(x) = rule.tiled_state { resolved.tiled_state = Some(x); } + + if let Some(x) = rule.force_render { + resolved.force_render = Some(x); + } + + if let Some(x) = rule.force_render_fps { + resolved.force_render_fps = Some(x); + } } resolved.open_on_output = open_on_output.map(|x| x.to_owned());