Scope redraw requests and avoid redundant animation fallback wakeups
Instrument redraw sources, per-output request coalescing, and animation fallback scheduling. Route local visual changes to their affected outputs while preserving deferred grab and gesture updates. Schedule fallback recovery using each orphaned animation output's refresh interval, suppress it while a normal frame chain exists, and retain requested final frames after scheduling failures. Add mixed-refresh, cursor footprint, frame-chain, and disconnect regressions and document measurement limits.
This commit is contained in:
1 parent
28e03a2624
commit
6f2951c6b0
27 files changed
+965
-139
No files matched your search
@@ -7,7 +7,9 @@ use std::time::{Duration, Instant};
|
||||
|
||||
mod capture;
|
||||
mod planes;
|
||||
mod redraw;
|
||||
mod scheduling;
|
||||
use redraw::{AnimationFallback, SchedulingMetrics};
|
||||
use scheduling::*;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||
@@ -54,10 +56,15 @@ pub struct DirectBackendState {
|
||||
presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>,
|
||||
pub(crate) connected_outputs: Vec<ConnectedOutputInfo>,
|
||||
animation_timer: Option<RegistrationToken>,
|
||||
animation_active: bool,
|
||||
animation_deadline: Option<Instant>,
|
||||
scheduling_metrics: SchedulingMetrics,
|
||||
}
|
||||
|
||||
impl DirectBackendState {
|
||||
pub(crate) fn dump_scheduling_metrics(&self) {
|
||||
self.scheduling_metrics.dump();
|
||||
}
|
||||
|
||||
pub(crate) fn capture_resize_snapshot(
|
||||
&mut self,
|
||||
window: &smithay::desktop::Window,
|
||||
@@ -179,6 +186,8 @@ struct DirectOutput {
|
||||
capture_texture: Option<GlesTexture>,
|
||||
render_metrics: RenderMetrics,
|
||||
frame_pending: bool,
|
||||
animation_fallback: AnimationFallback,
|
||||
cursor_visible: bool,
|
||||
scheduler: crate::frame_scheduler::FrameScheduler,
|
||||
render_timer: Option<RegistrationToken>,
|
||||
power_off: bool,
|
||||
@@ -226,7 +235,8 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
presentation: HashMap::new(),
|
||||
connected_outputs: Vec::new(),
|
||||
animation_timer: None,
|
||||
animation_active: false,
|
||||
animation_deadline: None,
|
||||
scheduling_metrics: SchedulingMetrics::new(),
|
||||
});
|
||||
|
||||
open_primary_device(event_loop, state, &primary_path)?;
|
||||
@@ -600,6 +610,8 @@ fn open_primary_device(
|
||||
} else {
|
||||
arm_frame(state, node, crtc);
|
||||
}
|
||||
|
||||
arm_animation_timer(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -673,35 +685,110 @@ pub(crate) fn wake_locked_outputs(state: &mut Ferese) {
|
||||
render_all(state);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn render_all(state: &mut Ferese) {
|
||||
redraw(state, None, std::panic::Location::caller());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn render_on(state: &mut Ferese, outputs: &[Output]) {
|
||||
redraw(state, Some(outputs), std::panic::Location::caller());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn render_cursor(state: &mut Ferese) {
|
||||
let outputs = cursor_affected_outputs(state);
|
||||
redraw(state, Some(&outputs), std::panic::Location::caller());
|
||||
}
|
||||
|
||||
fn cursor_affected_outputs(state: &Ferese) -> Vec<Output> {
|
||||
state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|backend| backend.devices.values())
|
||||
.flat_map(|device| device.outputs.values())
|
||||
.filter(|output| output.cursor_visible || redraw::cursor_on_output(state, &output.output))
|
||||
.map(|output| output.output.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn render_window(state: &mut Ferese, window: ferese_layout::WindowId) {
|
||||
let outputs = state
|
||||
.space
|
||||
.outputs()
|
||||
.filter(|output| state.window_belongs_to_output(window, output))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
render_on(state, &outputs);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn render_surface(
|
||||
state: &mut Ferese,
|
||||
surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
) {
|
||||
use smithay::input::pointer::CursorImageStatus;
|
||||
use smithay::wayland::compositor::get_parent;
|
||||
let mut root = surface.clone();
|
||||
while let Some(parent) = get_parent(&root) {
|
||||
root = parent;
|
||||
}
|
||||
|
||||
if matches!(&state.cursor_status, CursorImageStatus::Surface(cursor) if cursor == &root) {
|
||||
render_cursor(state);
|
||||
return;
|
||||
}
|
||||
|
||||
let outputs = state.surface_outputs(&root);
|
||||
render_on(state, &outputs);
|
||||
}
|
||||
|
||||
fn redraw(state: &mut Ferese, selected: Option<&[Output]>, source: &'static std::panic::Location<'static>) {
|
||||
if state.direct_backend.as_ref().is_none_or(|backend| !backend.active) {
|
||||
return;
|
||||
}
|
||||
let animating = state.advance_animations(Instant::now());
|
||||
render_outputs(state, animating);
|
||||
}
|
||||
|
||||
fn render_outputs(state: &mut Ferese, animating: bool) {
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.animation_active = animating && !state.session_lock.sleeping;
|
||||
}
|
||||
|
||||
arm_animation_timer(state);
|
||||
let outputs = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.map(|backend| {
|
||||
backend
|
||||
.devices
|
||||
.unwrap()
|
||||
.devices
|
||||
.iter()
|
||||
.flat_map(|(node, device)| {
|
||||
device
|
||||
.outputs
|
||||
.iter()
|
||||
.flat_map(|(node, device)| device.outputs.keys().map(|crtc| (*node, *crtc)))
|
||||
.collect::<Vec<_>>()
|
||||
.map(|(crtc, output)| (*node, *crtc, output.output.clone()))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
for (node, crtc) in outputs {
|
||||
request_frame(state, node, crtc);
|
||||
.collect::<Vec<_>>();
|
||||
// Include animations before advancing, so their final settled frame is
|
||||
// drawn too. Advancing shared state must not leave another output stale.
|
||||
let affected = outputs
|
||||
.iter()
|
||||
.map(|(_, _, output)| {
|
||||
selected.is_none_or(|selected| selected.contains(output))
|
||||
|| state.output_has_animations(output)
|
||||
|| state.output_has_pending_visual_changes(output)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
state.advance_animations(Instant::now());
|
||||
let mut requested = 0;
|
||||
for ((node, crtc, output), affected) in outputs.iter().zip(affected) {
|
||||
if affected || state.output_has_animations(output) {
|
||||
request_frame(state, *node, *crtc);
|
||||
requested += 1;
|
||||
}
|
||||
}
|
||||
|
||||
state.direct_backend.as_mut().unwrap().scheduling_metrics.redraw(
|
||||
source,
|
||||
selected.is_none(),
|
||||
requested,
|
||||
outputs.len(),
|
||||
);
|
||||
arm_animation_timer(state);
|
||||
}
|
||||
|
||||
pub fn switch_vt(state: &mut Ferese, vt: i32) {
|
||||
@@ -908,6 +995,12 @@ fn render_output(
|
||||
})()
|
||||
};
|
||||
|
||||
// Keep the footprint of the last successful scene, including queued
|
||||
// cursor planes. Requests do not read the pointer while a grab holds it.
|
||||
if rendered.is_ok() {
|
||||
output.cursor_visible = redraw::cursor_on_output(state, &output.output);
|
||||
}
|
||||
|
||||
let submitted = rendered.as_ref().is_ok_and(|submitted| *submitted);
|
||||
record_frame_schedule(&mut output, plan, render_started.elapsed(), submitted);
|
||||
|
||||
@@ -1201,6 +1294,7 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
|
||||
backend.connected_outputs.clear();
|
||||
}
|
||||
|
||||
arm_animation_timer(state);
|
||||
tracing::info!(?node, "removed DRM device");
|
||||
}
|
||||
|
||||
@@ -1220,82 +1314,115 @@ fn refresh_duration_from_mode(mode: DrmMode) -> Duration {
|
||||
Duration::from_nanos(1_000_000_000_000 / OutputMode::from(mode).refresh.max(1) as u64)
|
||||
}
|
||||
|
||||
fn animation_interval(state: &Ferese) -> Duration {
|
||||
state
|
||||
fn animation_fallbacks(state: &mut Ferese) -> Vec<(DrmNode, crtc::Handle, Instant)> {
|
||||
let now = Instant::now();
|
||||
let outputs = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|backend| backend.devices.values())
|
||||
.flat_map(|device| device.outputs.values())
|
||||
.filter_map(|output| output.output.current_mode())
|
||||
.filter(|mode| mode.refresh > 0)
|
||||
.map(|mode| Duration::from_nanos(1_000_000_000_000 / mode.refresh as u64))
|
||||
.min()
|
||||
.unwrap_or(Duration::from_millis(16))
|
||||
.flat_map(|backend| backend.devices.iter())
|
||||
.flat_map(|(node, device)| {
|
||||
device
|
||||
.outputs
|
||||
.iter()
|
||||
.map(move |(crtc, output)| (*node, *crtc, output.output.clone(), device.drm.is_active()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut fallbacks = Vec::new();
|
||||
for (node, crtc, identity, device_active) in outputs {
|
||||
let enabled = device_active
|
||||
&& !state.session_lock.sleeping
|
||||
&& state.direct_backend.as_ref().is_some_and(|backend| backend.active);
|
||||
let animating = enabled && state.output_has_animations(&identity);
|
||||
let backend = state.direct_backend.as_mut().unwrap();
|
||||
let output = backend.devices.get_mut(&node).unwrap().outputs.get_mut(&crtc).unwrap();
|
||||
// Preserve recovery of a requested final frame if another output
|
||||
// advances this animation to rest before the watchdog is dispatched.
|
||||
let active = enabled
|
||||
&& (animating || (output.animation_fallback.waiting_for_frame() && output.scheduler.has_pending_request()));
|
||||
let chained = output.render_timer.is_some() || output.frame_pending;
|
||||
if active && chained {
|
||||
backend.scheduling_metrics.chained_animation_observations += 1;
|
||||
}
|
||||
|
||||
if let Some(deadline) = output.animation_fallback.deadline(
|
||||
now,
|
||||
refresh_duration_from_mode(output.mode),
|
||||
active && !output.power_off,
|
||||
chained,
|
||||
) {
|
||||
fallbacks.push((node, crtc, deadline));
|
||||
}
|
||||
}
|
||||
|
||||
fallbacks
|
||||
}
|
||||
|
||||
fn arm_animation_timer(state: &mut Ferese) {
|
||||
let needed = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.animation_active && backend.animation_timer.is_none());
|
||||
if !needed {
|
||||
let deadline = animation_fallbacks(state)
|
||||
.iter()
|
||||
.map(|(_, _, deadline)| *deadline)
|
||||
.min();
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if backend.animation_deadline == deadline {
|
||||
return;
|
||||
}
|
||||
|
||||
let deadline = state.animation_fallback_deadline(animation_interval(state));
|
||||
if let Some(token) = backend.animation_timer.take() {
|
||||
state.loop_handle.remove(token);
|
||||
backend.scheduling_metrics.fallback_cancels += 1;
|
||||
}
|
||||
|
||||
backend.animation_deadline = None;
|
||||
let Some(deadline) = deadline else {
|
||||
return;
|
||||
};
|
||||
match state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_deadline(deadline), |_, _, state| {
|
||||
let active = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.active && backend.animation_active && !state.session_lock.sleeping);
|
||||
if active {
|
||||
let deadline = state.animation_fallback_deadline(animation_interval(state));
|
||||
if Instant::now() < deadline {
|
||||
return TimeoutAction::ToInstant(deadline);
|
||||
}
|
||||
|
||||
let animating = state.advance_animations(Instant::now());
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.animation_active = animating;
|
||||
}
|
||||
|
||||
let outputs = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|backend| backend.devices.iter())
|
||||
.flat_map(|(node, device)| {
|
||||
device
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|(crtc, output)| (*node, *crtc, output.output.clone()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (node, crtc, output) in outputs {
|
||||
if state.output_has_animations(&output) {
|
||||
request_frame(state, node, crtc);
|
||||
}
|
||||
}
|
||||
|
||||
if state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.animation_active)
|
||||
{
|
||||
return TimeoutAction::ToInstant(state.animation_fallback_deadline(animation_interval(state)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.animation_timer = None;
|
||||
backend.animation_deadline = None;
|
||||
backend.scheduling_metrics.fallback_wakes += 1;
|
||||
}
|
||||
|
||||
// Revalidate identities, power state, activity and normal frame chains
|
||||
// at dispatch. Disconnected/replaced outputs never get a stale request.
|
||||
for (node, crtc, deadline) in animation_fallbacks(state) {
|
||||
if deadline <= Instant::now() {
|
||||
state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.scheduling_metrics
|
||||
.fallback_recoveries += 1;
|
||||
state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.devices
|
||||
.get_mut(&node)
|
||||
.unwrap()
|
||||
.outputs
|
||||
.get_mut(&crtc)
|
||||
.unwrap()
|
||||
.animation_fallback = AnimationFallback::default();
|
||||
request_frame(state, node, crtc);
|
||||
}
|
||||
}
|
||||
|
||||
arm_animation_timer(state);
|
||||
TimeoutAction::Drop
|
||||
}) {
|
||||
Ok(token) => state.direct_backend.as_mut().unwrap().animation_timer = Some(token),
|
||||
Ok(token) => {
|
||||
let backend = state.direct_backend.as_mut().unwrap();
|
||||
backend.animation_timer = Some(token);
|
||||
backend.animation_deadline = Some(deadline);
|
||||
backend.scheduling_metrics.fallback_arms += 1;
|
||||
}
|
||||
|
||||
Err(error) => tracing::warn!(%error, "could not schedule DRM animation fallback"),
|
||||
}
|
||||
}
|
||||
@@ -1656,6 +1783,8 @@ fn create_direct_output(
|
||||
capture_texture: None,
|
||||
render_metrics,
|
||||
frame_pending: false,
|
||||
animation_fallback: AnimationFallback::default(),
|
||||
cursor_visible: false,
|
||||
power_off: false,
|
||||
scheduler: crate::frame_scheduler::FrameScheduler::new(refresh_duration_from_mode(mode)),
|
||||
render_timer: None,
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
//! Redraw/fallback measurements and the orphaned-animation watchdog policy.
|
||||
use std::collections::HashMap;
|
||||
use std::panic::Location;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct AnimationFallback {
|
||||
orphaned_since: Option<Instant>,
|
||||
}
|
||||
|
||||
impl AnimationFallback {
|
||||
pub fn waiting_for_frame(&self) -> bool {
|
||||
self.orphaned_since.is_some()
|
||||
}
|
||||
|
||||
pub fn deadline(&mut self, now: Instant, interval: Duration, active: bool, chained: bool) -> Option<Instant> {
|
||||
if !active || chained {
|
||||
self.orphaned_since = None;
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(*self.orphaned_since.get_or_insert(now) + interval.saturating_mul(2))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct RedrawCount {
|
||||
calls: u64,
|
||||
requested_outputs: u64,
|
||||
connected_outputs: u64,
|
||||
}
|
||||
|
||||
pub(super) struct SchedulingMetrics {
|
||||
enabled: bool,
|
||||
started: Instant,
|
||||
redraws: HashMap<(&'static str, u32, bool), RedrawCount>,
|
||||
pub fallback_arms: u64,
|
||||
pub fallback_cancels: u64,
|
||||
pub fallback_wakes: u64,
|
||||
pub fallback_recoveries: u64,
|
||||
pub chained_animation_observations: u64,
|
||||
}
|
||||
|
||||
impl SchedulingMetrics {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
enabled: std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some(),
|
||||
started: Instant::now(),
|
||||
redraws: HashMap::new(),
|
||||
fallback_arms: 0,
|
||||
fallback_cancels: 0,
|
||||
fallback_wakes: 0,
|
||||
fallback_recoveries: 0,
|
||||
chained_animation_observations: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redraw(&mut self, source: &'static Location<'static>, global: bool, requested: usize, connected: usize) {
|
||||
if !self.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let count = self.redraws.entry((source.file(), source.line(), global)).or_default();
|
||||
count.calls += 1;
|
||||
count.requested_outputs += requested as u64;
|
||||
count.connected_outputs += connected as u64;
|
||||
if self.started.elapsed() >= Duration::from_secs(5) {
|
||||
self.dump();
|
||||
self.started = Instant::now();
|
||||
self.redraws.clear();
|
||||
self.fallback_arms = 0;
|
||||
self.fallback_cancels = 0;
|
||||
self.fallback_wakes = 0;
|
||||
self.fallback_recoveries = 0;
|
||||
self.chained_animation_observations = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump(&self) {
|
||||
if !self.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
for ((file, line, global), count) in &self.redraws {
|
||||
tracing::info!(target: "ferese::render", file, line, global,
|
||||
calls = count.calls, requested_outputs = count.requested_outputs,
|
||||
connected_outputs = count.connected_outputs, "redraw requests");
|
||||
}
|
||||
|
||||
tracing::info!(target: "ferese::render", fallback_arms = self.fallback_arms,
|
||||
fallback_cancels = self.fallback_cancels, fallback_wakes = self.fallback_wakes,
|
||||
fallback_recoveries = self.fallback_recoveries,
|
||||
chained_animation_observations = self.chained_animation_observations,
|
||||
"animation fallback scheduling");
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn cursor_on_output(state: &crate::Ferese, output: &smithay::output::Output) -> bool {
|
||||
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
|
||||
use smithay::utils::{Logical, Rectangle};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
if state.input_capture.active() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(pointer) = state.seat.get_pointer() else {
|
||||
return false;
|
||||
};
|
||||
let Some(bounds) = state.space.output_geometry(output) else {
|
||||
return false;
|
||||
};
|
||||
let location = pointer.current_location();
|
||||
let rect = match &state.cursor_status {
|
||||
CursorImageStatus::Surface(surface) => {
|
||||
let hotspot = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<CursorImageSurfaceData>()
|
||||
.map(|attributes| attributes.lock().unwrap().hotspot)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
smithay::desktop::utils::bbox_from_surface_tree(surface, (location - hotspot.to_f64()).to_i32_round())
|
||||
.to_f64()
|
||||
}
|
||||
|
||||
CursorImageStatus::Named(icon) => {
|
||||
let Some(cursor) = state.named_cursors.get(icon) else {
|
||||
return false;
|
||||
};
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
// Named buffers use buffer scale 1: Smithay scales their size
|
||||
// with the output, while the hotspot is subtracted physically.
|
||||
let size = cursor.size;
|
||||
Rectangle::<f64, Logical>::new(
|
||||
(
|
||||
location.x - f64::from(cursor.hotspot.x) / scale,
|
||||
location.y - f64::from(cursor.hotspot.y) / scale,
|
||||
)
|
||||
.into(),
|
||||
(f64::from(size.w), f64::from(size.h)).into(),
|
||||
)
|
||||
}
|
||||
|
||||
CursorImageStatus::Hidden => return false,
|
||||
};
|
||||
// One logical pixel covers physical rounding at fractional scales.
|
||||
let bounds = Rectangle::<f64, Logical>::new(
|
||||
(f64::from(bounds.loc.x) - 1.0, f64::from(bounds.loc.y) - 1.0).into(),
|
||||
(f64::from(bounds.size.w) + 2.0, f64::from(bounds.size.h) + 2.0).into(),
|
||||
);
|
||||
rect.overlaps(bounds)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mixed_refresh_only_orphaned_animations_need_a_watchdog() {
|
||||
let now = Instant::now();
|
||||
let fast = Duration::from_nanos(1_000_000_000 / 240);
|
||||
let slow = Duration::from_nanos(1_000_000_000 / 60);
|
||||
let mut idle_fast = AnimationFallback::default();
|
||||
let mut moving_slow = AnimationFallback::default();
|
||||
assert_eq!(idle_fast.deadline(now, fast, false, false), None);
|
||||
assert_eq!(moving_slow.deadline(now, slow, true, false), Some(now + slow * 2));
|
||||
assert_eq!(
|
||||
moving_slow.deadline(now + slow, slow, true, false),
|
||||
Some(now + slow * 2)
|
||||
);
|
||||
assert_eq!(moving_slow.deadline(now + slow, slow, true, true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disconnect_and_reconnect_do_not_keep_an_old_deadline() {
|
||||
let now = Instant::now();
|
||||
let interval = Duration::from_millis(16);
|
||||
let mut output = AnimationFallback::default();
|
||||
output.deadline(now, interval, true, false);
|
||||
assert_eq!(output.deadline(now + interval, interval, false, false), None);
|
||||
let reconnected = now + Duration::from_secs(1);
|
||||
assert_eq!(
|
||||
output.deadline(reconnected, interval, true, false),
|
||||
Some(reconnected + interval * 2)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measured_legacy_fast_monitor_watchdog_wakes_with_an_intact_slow_chain() {
|
||||
// The old watchdog wakes at last_animation_tick + 2*fast_interval,
|
||||
// then advances the shared clock, even with a slow render in flight.
|
||||
let mut last_tick_us = 0;
|
||||
let mut old_wakes = 0;
|
||||
let mut policy = AnimationFallback::default();
|
||||
let start = Instant::now();
|
||||
for now_us in 0..1_000_000 {
|
||||
if now_us % 16_667 == 0 {
|
||||
last_tick_us = now_us;
|
||||
}
|
||||
|
||||
if now_us >= last_tick_us + 8_334 {
|
||||
old_wakes += 1;
|
||||
last_tick_us = now_us;
|
||||
}
|
||||
|
||||
assert_eq!(policy.deadline(start, Duration::from_micros(16_667), true, true), None);
|
||||
}
|
||||
|
||||
assert_eq!(old_wakes, 60);
|
||||
eprintln!("watchdog replay: legacy={old_wakes} unnecessary wakes/s, intact-chain policy=0");
|
||||
}
|
||||
#[test]
|
||||
fn scheduled_and_in_flight_frames_suppress_fallback_until_the_chain_is_lost() {
|
||||
use crate::frame_scheduler::FrameScheduler;
|
||||
let start = Instant::now();
|
||||
let interval = Duration::from_millis(16);
|
||||
let mut scheduler = FrameScheduler::new(interval);
|
||||
let mut fallback = AnimationFallback::default();
|
||||
scheduler.request(Duration::ZERO);
|
||||
let scheduled = scheduler.plan(Duration::ZERO).unwrap();
|
||||
assert_eq!(fallback.deadline(start, interval, true, true), None);
|
||||
let plan = scheduler.begin().unwrap();
|
||||
assert_eq!(plan, scheduled);
|
||||
scheduler.rendered(plan, Duration::from_millis(2), Duration::from_millis(2), true);
|
||||
assert_eq!(scheduler.plan(interval), None);
|
||||
assert_eq!(fallback.deadline(start + interval, interval, true, true), None);
|
||||
scheduler.presented(interval);
|
||||
assert_eq!(
|
||||
fallback.deadline(start + interval, interval, true, false),
|
||||
Some(start + interval * 3)
|
||||
);
|
||||
scheduler.request(interval);
|
||||
assert!(scheduler.plan(interval).is_some());
|
||||
assert_eq!(fallback.deadline(start + interval, interval, true, true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settled_animation_still_recovers_its_requested_final_frame_after_scheduling_failure() {
|
||||
use crate::frame_scheduler::FrameScheduler;
|
||||
let start = Instant::now();
|
||||
let interval = Duration::from_millis(16);
|
||||
let mut scheduler = FrameScheduler::new(interval);
|
||||
let mut fallback = AnimationFallback::default();
|
||||
scheduler.request(Duration::ZERO);
|
||||
scheduler.plan(Duration::ZERO).unwrap();
|
||||
// Timer insertion failed: a scheduler plan without an event-loop
|
||||
// source is not a live chain. Another output can settle the spring.
|
||||
assert!(scheduler.has_pending_request());
|
||||
let deadline = fallback.deadline(start, interval, true, false).unwrap();
|
||||
let animation_active = false;
|
||||
let needs_final_frame = animation_active || (fallback.waiting_for_frame() && scheduler.has_pending_request());
|
||||
assert_eq!(
|
||||
fallback.deadline(deadline, interval, needs_final_frame, false),
|
||||
Some(deadline)
|
||||
);
|
||||
// A successful retry installs the normal timer and cancels fallback.
|
||||
assert_eq!(fallback.deadline(deadline, interval, needs_final_frame, true), None);
|
||||
scheduler.begin().unwrap();
|
||||
assert!(!scheduler.has_pending_request());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires private sockets; subprocess supplies its own runtime directory"]
|
||||
fn cursor_footprints_limit_output_local_redraws_and_clear_both_sides_of_a_crossing() {
|
||||
if std::env::var_os("FERESE_CURSOR_REDRAW_TEST_CHILD").is_none() {
|
||||
let runtime = tempfile::tempdir().unwrap();
|
||||
let result = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.args(["--exact", "backends::direct::redraw::tests::cursor_footprints_limit_output_local_redraws_and_clear_both_sides_of_a_crossing", "--include-ignored", "--nocapture"])
|
||||
.env("XDG_RUNTIME_DIR", runtime.path())
|
||||
.env("FERESE_CURSOR_REDRAW_TEST_CHILD", "1")
|
||||
.output().unwrap();
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&result.stdout),
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
use smithay::input::pointer::{CursorImageStatus, MotionEvent};
|
||||
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||
use smithay::utils::SERIAL_COUNTER;
|
||||
let mut event_loop = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
||||
let display = smithay::reexports::wayland_server::Display::new().unwrap();
|
||||
let config = crate::config::Config::default().runtime_config().unwrap();
|
||||
let mut state = crate::Ferese::new(&mut event_loop, display, config).unwrap();
|
||||
let mut outputs = Vec::new();
|
||||
for (index, refresh) in [60_000, 240_000].into_iter().enumerate() {
|
||||
let output = Output::new(
|
||||
format!("cursor-test-{index}"),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size: if index == 0 {
|
||||
(3840, 2160).into()
|
||||
} else {
|
||||
(1920, 1080).into()
|
||||
},
|
||||
refresh,
|
||||
}),
|
||||
None,
|
||||
Some(smithay::output::Scale::Fractional(if index == 0 { 2.0 } else { 1.0 })),
|
||||
Some(((index as i32) * 1920, 0).into()),
|
||||
);
|
||||
state.space.map_output(&output, ((index as i32) * 1920, 0));
|
||||
state.register_output(&output, output.name());
|
||||
outputs.push(output);
|
||||
}
|
||||
|
||||
let pointer = state.seat.get_pointer().unwrap();
|
||||
let mut old = [false; 2];
|
||||
let mut local_requests = 0;
|
||||
for x in [500.0, 501.0, 502.0] {
|
||||
pointer.motion(
|
||||
&mut state,
|
||||
None,
|
||||
&MotionEvent {
|
||||
location: (x, 500.0).into(),
|
||||
serial: SERIAL_COUNTER.next_serial(),
|
||||
time: 0,
|
||||
},
|
||||
);
|
||||
let current = [
|
||||
cursor_on_output(&state, &outputs[0]),
|
||||
cursor_on_output(&state, &outputs[1]),
|
||||
];
|
||||
assert_eq!(current, [true, false]);
|
||||
local_requests += old
|
||||
.into_iter()
|
||||
.zip(current)
|
||||
.filter(|(before, after)| *before || *after)
|
||||
.count();
|
||||
old = current;
|
||||
}
|
||||
|
||||
assert_eq!(local_requests, 3);
|
||||
eprintln!("cursor replay: legacy=6 output requests, footprint policy={local_requests}");
|
||||
pointer.motion(
|
||||
&mut state,
|
||||
None,
|
||||
&MotionEvent {
|
||||
location: (1919.0, 500.0).into(),
|
||||
serial: SERIAL_COUNTER.next_serial(),
|
||||
time: 0,
|
||||
},
|
||||
);
|
||||
assert!(cursor_on_output(&state, &outputs[0]));
|
||||
assert!(cursor_on_output(&state, &outputs[1]), "cursor image straddles the seam");
|
||||
if let CursorImageStatus::Named(icon) = &state.cursor_status {
|
||||
let cursor = state.named_cursors.get(icon).unwrap();
|
||||
let x = 1920.0 - f64::from(cursor.size.w) * 0.75 + f64::from(cursor.hotspot.x) / 2.0;
|
||||
pointer.motion(
|
||||
&mut state,
|
||||
None,
|
||||
&MotionEvent {
|
||||
location: (x, 500.0).into(),
|
||||
serial: SERIAL_COUNTER.next_serial(),
|
||||
time: 0,
|
||||
},
|
||||
);
|
||||
assert!(
|
||||
cursor_on_output(&state, &outputs[1]),
|
||||
"fractional-scale footprint matches rendered cursor size"
|
||||
);
|
||||
}
|
||||
|
||||
old = [true, true];
|
||||
state.cursor_status = CursorImageStatus::Hidden;
|
||||
let current = [
|
||||
cursor_on_output(&state, &outputs[0]),
|
||||
cursor_on_output(&state, &outputs[1]),
|
||||
];
|
||||
assert_eq!(current, [false, false]);
|
||||
assert_eq!(
|
||||
old.into_iter()
|
||||
.zip(current)
|
||||
.filter(|(before, after)| *before || *after)
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
// Grabs and held gestures defer window/output work independently of
|
||||
// the cursor, which may be on another display or hidden entirely.
|
||||
state.defer_output_redraw(outputs[0].clone());
|
||||
state.defer_output_redraw(outputs[1].clone());
|
||||
state.defer_output_redraw(outputs[1].clone());
|
||||
assert_eq!(state.output_redraw_pending, outputs);
|
||||
state.unregister_output(&outputs[1]);
|
||||
assert_eq!(state.output_redraw_pending, vec![outputs[0].clone()]);
|
||||
assert!(!cursor_on_output(&state, &outputs[1]));
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,9 @@ pub(super) fn request_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl
|
||||
return;
|
||||
}
|
||||
|
||||
output
|
||||
.render_metrics
|
||||
.record_request(output.render_timer.is_some(), output.frame_pending);
|
||||
output.scheduler.request(now);
|
||||
arm_frame(state, node, crtc);
|
||||
}
|
||||
@@ -115,10 +118,7 @@ fn dispatch_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identit
|
||||
return;
|
||||
};
|
||||
|
||||
let animating = state.advance_animations(Instant::now());
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.animation_active = animating;
|
||||
}
|
||||
state.advance_animations(Instant::now());
|
||||
|
||||
let output_animating = render_output(state, node, crtc, plan);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const DEFAULT_CURSOR_SIZE: u32 = 24;
|
||||
pub(crate) struct NamedCursor {
|
||||
pub buffer: MemoryRenderBuffer,
|
||||
pub hotspot: Point<i32, Buffer>,
|
||||
pub size: Size<i32, Buffer>,
|
||||
}
|
||||
|
||||
pub(crate) fn cursor_theme() -> CursorTheme {
|
||||
@@ -74,6 +75,7 @@ fn cursor_from_image(image: Image) -> NamedCursor {
|
||||
NamedCursor {
|
||||
buffer,
|
||||
hotspot: Point::from((image.xhot as i32, image.yhot as i32)),
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +105,7 @@ fn fallback_cursor() -> NamedCursor {
|
||||
None,
|
||||
),
|
||||
hotspot: Point::default(),
|
||||
size: (WIDTH as i32, HEIGHT as i32).into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ impl Dispatch<FereseMaterialManagerV1, ()> for Ferese {
|
||||
let material = data_init.init(id, SurfaceEffectsUserData::new(surface.clone()));
|
||||
set_surface_role(&surface, Some(SemanticRole::Modal));
|
||||
material.ready();
|
||||
crate::backends::direct::render_all(state);
|
||||
crate::backends::direct::render_surface(state, &surface);
|
||||
}
|
||||
ferese_material_manager_v1::Request::Destroy => {}
|
||||
_ => unreachable!(),
|
||||
@@ -167,7 +167,7 @@ impl Dispatch<FereseSurfaceMaterialV1, SurfaceEffectsUserData> for Ferese {
|
||||
&& let Some(surface) = data.surface()
|
||||
&& detach(&surface)
|
||||
{
|
||||
crate::backends::direct::render_all(state);
|
||||
crate::backends::direct::render_surface(state, &surface);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
|
||||
true
|
||||
});
|
||||
if changed {
|
||||
crate::backends::direct::render_all(state);
|
||||
crate::backends::direct::render_surface(state, &surface);
|
||||
}
|
||||
}
|
||||
ferese_surface_effects_v1::Request::SetRole { role } => {
|
||||
@@ -336,7 +336,7 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
|
||||
WEnum::Unknown(_) | WEnum::Value(_) => return,
|
||||
};
|
||||
if set_surface_role(&surface, Some(role)) {
|
||||
crate::backends::direct::render_all(state);
|
||||
crate::backends::direct::render_surface(state, &surface);
|
||||
}
|
||||
}
|
||||
ferese_surface_effects_v1::Request::SetRegions { regions } => {
|
||||
@@ -356,17 +356,17 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
|
||||
true
|
||||
});
|
||||
if changed {
|
||||
crate::backends::direct::render_all(state);
|
||||
crate::backends::direct::render_surface(state, &surface);
|
||||
}
|
||||
}
|
||||
ferese_surface_effects_v1::Request::ClearRole => {
|
||||
if set_surface_role(&surface, None) {
|
||||
crate::backends::direct::render_all(state);
|
||||
crate::backends::direct::render_surface(state, &surface);
|
||||
}
|
||||
}
|
||||
ferese_surface_effects_v1::Request::Destroy => {
|
||||
if detach(&surface) {
|
||||
crate::backends::direct::render_all(state);
|
||||
crate::backends::direct::render_surface(state, &surface);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
|
||||
@@ -56,6 +56,10 @@ impl FrameScheduler {
|
||||
self.requested_at.get_or_insert(now);
|
||||
}
|
||||
|
||||
pub fn has_pending_request(&self) -> bool {
|
||||
self.requested_at.is_some() || self.scheduled.is_some()
|
||||
}
|
||||
|
||||
fn budget(&self, now: Duration) -> Duration {
|
||||
let idle = self
|
||||
.last_presented
|
||||
|
||||
@@ -99,7 +99,7 @@ impl CompositorHandler for Ferese {
|
||||
}
|
||||
layer_shell::handle_commit(self, surface);
|
||||
xdg_shell::handle_commit(&mut self.popups, &mut self.space, surface);
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_surface(self, surface);
|
||||
}
|
||||
|
||||
fn destroyed(&mut self, surface: &WlSurface) {
|
||||
@@ -110,14 +110,14 @@ impl CompositorHandler for Ferese {
|
||||
self.focus_lock_surface();
|
||||
}
|
||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_surface(self, surface);
|
||||
if self.idle_inhibitors.remove(surface).is_some() {
|
||||
self.refresh_idle_inhibition();
|
||||
}
|
||||
if matches!(&self.cursor_status, smithay::input::pointer::CursorImageStatus::Surface(cursor) if cursor == surface)
|
||||
{
|
||||
self.cursor_status = smithay::input::pointer::CursorImageStatus::default_named();
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_cursor(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,17 @@ impl InputMethodHandler for Ferese {
|
||||
if let Err(error) = self.popups.track_popup(PopupKind::InputMethod(surface.clone())) {
|
||||
tracing::warn!(?error, "failed to track input method popup");
|
||||
}
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_surface(self, surface.wl_surface());
|
||||
}
|
||||
|
||||
fn dismiss_popup(&mut self, _surface: PopupSurface) {
|
||||
fn dismiss_popup(&mut self, surface: PopupSurface) {
|
||||
let outputs = self.surface_outputs(surface.wl_surface());
|
||||
self.popups.cleanup();
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_on(self, &outputs);
|
||||
}
|
||||
|
||||
fn popup_repositioned(&mut self, _surface: PopupSurface) {
|
||||
crate::backends::direct::render_all(self);
|
||||
fn popup_repositioned(&mut self, surface: PopupSurface) {
|
||||
crate::backends::direct::render_surface(self, surface.wl_surface());
|
||||
}
|
||||
|
||||
fn parent_geometry(&self, parent: &WlSurface) -> Rectangle<i32, Logical> {
|
||||
|
||||
@@ -32,8 +32,7 @@ impl WlrLayerShellHandler for Ferese {
|
||||
|
||||
tracing::debug!(%namespace, output = %output.name(), "mapped layer surface");
|
||||
super::set_surface_tree_output(layer.wl_surface(), &output);
|
||||
self.relayout();
|
||||
crate::backends::direct::render_all(self);
|
||||
self.relayout_on(&[output]);
|
||||
}
|
||||
|
||||
fn new_popup(&mut self, parent: LayerSurface, popup: PopupSurface) {
|
||||
@@ -59,17 +58,22 @@ impl WlrLayerShellHandler for Ferese {
|
||||
return;
|
||||
};
|
||||
let restore_focus = layer_has_keyboard_focus(self, &layer);
|
||||
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
||||
let outputs = self
|
||||
.space
|
||||
.outputs()
|
||||
.filter(|output| layer_map_for_output(output).layer_geometry(&layer).is_some())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for output in outputs {
|
||||
layer_map_for_output(&output).unmap_layer(&layer);
|
||||
for output in &outputs {
|
||||
layer_map_for_output(output).unmap_layer(&layer);
|
||||
}
|
||||
|
||||
self.relayout();
|
||||
self.relayout_on(&outputs);
|
||||
if restore_focus {
|
||||
self.restore_keyboard_focus();
|
||||
}
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_on(self, &outputs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,10 +81,15 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
let Some(layer) = state.space.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) else {
|
||||
return;
|
||||
};
|
||||
let outputs = state.space.outputs().cloned().collect::<Vec<_>>();
|
||||
let outputs = state
|
||||
.space
|
||||
.outputs()
|
||||
.filter(|output| layer_map_for_output(output).layer_geometry(&layer).is_some())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for output in outputs {
|
||||
let mut map = layer_map_for_output(&output);
|
||||
for output in &outputs {
|
||||
let mut map = layer_map_for_output(output);
|
||||
if map.layer_geometry(&layer).is_some() {
|
||||
map.arrange();
|
||||
layer.layer_surface().send_pending_configure();
|
||||
@@ -103,7 +112,7 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
state.restore_keyboard_focus();
|
||||
}
|
||||
|
||||
state.relayout();
|
||||
state.relayout_on(&outputs);
|
||||
if let Err(error) = state.display_handle.flush_clients() {
|
||||
tracing::debug!(%error, "failed to flush layer-surface configure");
|
||||
}
|
||||
|
||||
@@ -132,6 +132,38 @@ impl KeyboardShortcutsInhibitHandler for Ferese {
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
pub(crate) fn surface_outputs(&self, surface: &WlSurface) -> Vec<smithay::output::Output> {
|
||||
use smithay::desktop::{WindowSurfaceType, find_popup_root_surface};
|
||||
use smithay::wayland::compositor::get_parent;
|
||||
let mut root = surface.clone();
|
||||
while let Some(parent) = get_parent(&root) {
|
||||
root = parent;
|
||||
}
|
||||
|
||||
if let Some(popup) = self.popups.find_popup(&root)
|
||||
&& let Ok(parent) = find_popup_root_surface(&popup)
|
||||
{
|
||||
root = parent;
|
||||
}
|
||||
|
||||
let window = self.windows.id_for_surface(&root);
|
||||
self.space
|
||||
.outputs()
|
||||
.filter(|output| {
|
||||
window.is_some_and(|id| self.window_belongs_to_output(id, output))
|
||||
|| layer_map_for_output(output)
|
||||
.layer_for_surface(&root, WindowSurfaceType::ALL)
|
||||
.is_some()
|
||||
|| self
|
||||
.session_lock
|
||||
.surfaces
|
||||
.get(output)
|
||||
.is_some_and(|lock| lock.wl_surface() == &root)
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn update_surface_preferences(&self, surface: &WlSurface) {
|
||||
use smithay::desktop::{WindowSurfaceType, find_popup_root_surface};
|
||||
use smithay::wayland::compositor::get_parent;
|
||||
|
||||
@@ -282,12 +282,12 @@ impl Ferese {
|
||||
frame: frame.clone(),
|
||||
buffer,
|
||||
},
|
||||
output,
|
||||
output: output.clone(),
|
||||
region: data.region,
|
||||
overlay_cursor: data.overlay_cursor,
|
||||
with_damage,
|
||||
});
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_on(self, &[output]);
|
||||
}
|
||||
|
||||
pub(crate) fn process_screencopies<R>(
|
||||
|
||||
@@ -36,14 +36,18 @@ impl XdgShellHandler for Ferese {
|
||||
fn title_changed(&mut self, surface: ToplevelSurface) {
|
||||
if self.windows.refresh_metadata(surface.wl_surface()) {
|
||||
self.send_shell_snapshots();
|
||||
self.cursor_redraw_pending = true;
|
||||
for output in self.surface_outputs(surface.wl_surface()) {
|
||||
self.defer_output_redraw(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn app_id_changed(&mut self, surface: ToplevelSurface) {
|
||||
if self.windows.refresh_metadata(surface.wl_surface()) {
|
||||
self.send_shell_snapshots();
|
||||
self.cursor_redraw_pending = true;
|
||||
for output in self.surface_outputs(surface.wl_surface()) {
|
||||
self.defer_output_redraw(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-7
@@ -440,7 +440,7 @@ impl Ferese {
|
||||
pointer.frame(self);
|
||||
self.focus_window_under_pointer(&pointer, position);
|
||||
self.activate_focused_pointer_constraint(&pointer);
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_cursor(self);
|
||||
}
|
||||
InputEvent::PointerMotion { event, .. } => {
|
||||
self.last_pointer_time = event.time_msec();
|
||||
@@ -484,7 +484,7 @@ impl Ferese {
|
||||
pointer.frame(self);
|
||||
self.focus_window_under_pointer(&pointer, location);
|
||||
self.activate_focused_pointer_constraint(&pointer);
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_cursor(self);
|
||||
}
|
||||
InputEvent::PointerButton { event, .. } => {
|
||||
self.last_pointer_time = event.time_msec();
|
||||
@@ -511,11 +511,14 @@ impl Ferese {
|
||||
&& crate::effects::begin_surface_dismiss(&surface)
|
||||
{
|
||||
let opacity = crate::effects::surface_opacity(&surface);
|
||||
self.dismissing_popups
|
||||
.push((root, popup, crate::dimming::DimAnimation::new(f64::from(opacity))));
|
||||
self.dismissing_popups.push((
|
||||
root.clone(),
|
||||
popup,
|
||||
crate::dimming::DimAnimation::new(f64::from(opacity)),
|
||||
));
|
||||
pointer.unset_grab(self, serial, event.time_msec());
|
||||
self.focus_window_at(pointer.current_location(), serial, true);
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_surface(self, &root);
|
||||
}
|
||||
|
||||
if self.overview.is_active() {
|
||||
@@ -871,11 +874,36 @@ impl Ferese {
|
||||
}
|
||||
|
||||
if raise {
|
||||
self.relayout();
|
||||
let outputs = self
|
||||
.space
|
||||
.outputs()
|
||||
.filter(|output| {
|
||||
self.space
|
||||
.output_geometry(output)
|
||||
.is_some_and(|bounds| bounds.to_f64().contains(position))
|
||||
|| previous
|
||||
.into_iter()
|
||||
.chain(self.focused_window)
|
||||
.any(|id| self.window_belongs_to_output(id, output))
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
self.relayout_on(&outputs);
|
||||
} else {
|
||||
// Focus, dimming and the bar title update without moving the layout.
|
||||
self.send_shell_snapshots();
|
||||
crate::backends::direct::render_all(self);
|
||||
let outputs = self
|
||||
.space
|
||||
.outputs()
|
||||
.filter(|output| {
|
||||
previous
|
||||
.into_iter()
|
||||
.chain(self.focused_window)
|
||||
.any(|id| self.window_belongs_to_output(id, output))
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
crate::backends::direct::render_on(self, &outputs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -802,7 +802,11 @@ impl Ferese {
|
||||
|
||||
// The nested backend redraws on its refresh timer; this drives the
|
||||
// direct backend immediately and is a no-op otherwise.
|
||||
crate::backends::direct::render_all(self);
|
||||
let outputs = planned
|
||||
.iter()
|
||||
.map(|part| targets[part.index].0.clone())
|
||||
.collect::<Vec<_>>();
|
||||
crate::backends::direct::render_on(self, &outputs);
|
||||
}
|
||||
|
||||
fn start_window_screenshot(&mut self, request: Request, response: SyncSender<Response>) {
|
||||
|
||||
+15
-1
@@ -128,8 +128,18 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
state.apply_unlocked_pointer_hint();
|
||||
let wallpaper_retry = state.wallpaper.take_retry_wakeup();
|
||||
if std::mem::take(&mut state.cursor_redraw_pending) || wallpaper_changed || wallpaper_retry {
|
||||
let cursor_changed = std::mem::take(&mut state.cursor_redraw_pending);
|
||||
let output_redraws = std::mem::take(&mut state.output_redraw_pending);
|
||||
if wallpaper_changed || wallpaper_retry {
|
||||
backends::direct::render_all(state);
|
||||
} else {
|
||||
if cursor_changed {
|
||||
backends::direct::render_cursor(state);
|
||||
}
|
||||
|
||||
if !output_redraws.is_empty() {
|
||||
backends::direct::render_on(state, &output_redraws);
|
||||
}
|
||||
}
|
||||
if let Err(error) = state
|
||||
.wallpaper
|
||||
@@ -145,6 +155,10 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(backend) = state.direct_backend.as_ref() {
|
||||
backend.dump_scheduling_metrics();
|
||||
}
|
||||
|
||||
#[cfg(feature = "resize-metrics")]
|
||||
state.resize_metrics.dump();
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ pub(crate) struct RenderMetrics {
|
||||
output: String,
|
||||
interval_started: Instant,
|
||||
rendered_frames: u64,
|
||||
redraw_requests: u64,
|
||||
requests_scheduled: u64,
|
||||
requests_in_flight: u64,
|
||||
no_damage_frames: u64,
|
||||
no_damage_render_time: Duration,
|
||||
damaged_pixels: u64,
|
||||
@@ -37,6 +40,9 @@ impl RenderMetrics {
|
||||
output: output.into(),
|
||||
interval_started: now,
|
||||
rendered_frames: 0,
|
||||
redraw_requests: 0,
|
||||
requests_scheduled: 0,
|
||||
requests_in_flight: 0,
|
||||
no_damage_frames: 0,
|
||||
no_damage_render_time: Duration::ZERO,
|
||||
damaged_pixels: 0,
|
||||
@@ -46,6 +52,16 @@ impl RenderMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn record_request(&mut self, scheduled: bool, in_flight: bool) {
|
||||
if !self.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
self.redraw_requests += 1;
|
||||
self.requests_scheduled += u64::from(scheduled);
|
||||
self.requests_in_flight += u64::from(in_flight);
|
||||
}
|
||||
|
||||
pub(crate) fn record_frame(
|
||||
&mut self,
|
||||
render_time: Duration,
|
||||
@@ -100,6 +116,9 @@ impl RenderMetrics {
|
||||
output = %self.output,
|
||||
interval_ms = elapsed.as_millis(),
|
||||
frames = self.rendered_frames,
|
||||
redraw_requests = self.redraw_requests,
|
||||
requests_scheduled = self.requests_scheduled,
|
||||
requests_in_flight = self.requests_in_flight,
|
||||
no_damage_frames = self.no_damage_frames,
|
||||
no_damage_render_us = self.no_damage_render_time.as_micros(),
|
||||
damaged_pixels = self.damaged_pixels,
|
||||
@@ -113,6 +132,9 @@ impl RenderMetrics {
|
||||
|
||||
self.interval_started = now;
|
||||
self.rendered_frames = 0;
|
||||
self.redraw_requests = 0;
|
||||
self.requests_scheduled = 0;
|
||||
self.requests_in_flight = 0;
|
||||
self.no_damage_frames = 0;
|
||||
self.no_damage_render_time = Duration::ZERO;
|
||||
self.damaged_pixels = 0;
|
||||
@@ -148,6 +170,22 @@ mod tests {
|
||||
assert_eq!(metrics.no_damage_render_time, Duration::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redraw_requests_distinguish_scheduled_and_in_flight_coalescing() {
|
||||
let mut metrics = RenderMetrics::new("test", true, Instant::now());
|
||||
metrics.record_request(false, false);
|
||||
metrics.record_request(true, false);
|
||||
metrics.record_request(false, true);
|
||||
assert_eq!(metrics.redraw_requests, 3);
|
||||
assert_eq!(metrics.requests_scheduled, 1);
|
||||
assert_eq!(metrics.requests_in_flight, 1);
|
||||
assert_eq!(metrics.rendered_frames, 0);
|
||||
|
||||
let mut disabled = RenderMetrics::new("test", false, Instant::now());
|
||||
disabled.record_request(true, true);
|
||||
assert_eq!(disabled.redraw_requests, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_damage_attempts_are_separate_from_presented_frame_statistics() {
|
||||
let mut metrics = RenderMetrics::new("test", true, Instant::now());
|
||||
|
||||
@@ -310,6 +310,10 @@ impl OverviewState {
|
||||
.or_else(|| windows.iter().copied().filter(visible).min_by_key(|id| id.0));
|
||||
}
|
||||
|
||||
pub(crate) fn selection_state(&self) -> (Option<WindowId>, Option<WorkspaceId>) {
|
||||
(self.selected, self.selection_workspace)
|
||||
}
|
||||
|
||||
pub(crate) fn selected(&self) -> Option<WindowId> {
|
||||
self.selected
|
||||
}
|
||||
@@ -455,12 +459,34 @@ impl Ferese {
|
||||
.and_then(|w| self.windows.ids().get(&w).copied());
|
||||
|
||||
if selected.is_some() && self.overview.selected != selected {
|
||||
let previous = self.overview.selection_state();
|
||||
self.overview.selection_workspace = None;
|
||||
self.overview.selected = selected;
|
||||
crate::backends::direct::render_all(self);
|
||||
self.redraw_overview_selection(previous);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn redraw_overview_selection(&mut self, previous: (Option<WindowId>, Option<WorkspaceId>)) {
|
||||
let outputs = self
|
||||
.space
|
||||
.outputs()
|
||||
.filter(|output| {
|
||||
[previous.0, self.overview.selected]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(|id| self.window_belongs_to_output(id, output))
|
||||
|| [previous.1, self.overview.selection_workspace]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(|workspace| {
|
||||
self.output_workspaces.output_for_workspace(workspace) == self.output_id(output)
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
crate::backends::direct::render_on(self, &outputs);
|
||||
}
|
||||
|
||||
pub(crate) fn retain_overview_labels(&mut self) {
|
||||
self.overview
|
||||
.window_labels
|
||||
@@ -554,8 +580,9 @@ impl Ferese {
|
||||
return false;
|
||||
}
|
||||
|
||||
let previous = self.overview.selection_state();
|
||||
if self.overview.select_direction(direction) {
|
||||
crate::backends::direct::render_all(self);
|
||||
self.redraw_overview_selection(previous);
|
||||
}
|
||||
|
||||
true
|
||||
@@ -797,7 +824,7 @@ impl Ferese {
|
||||
self.overview
|
||||
.strip_offsets
|
||||
.insert(output_id, next.min(ordered.len().saturating_sub(capacity)));
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_on(self, &[output]);
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -375,6 +375,7 @@ pub struct Ferese {
|
||||
// Cursor callbacks may run with Smithay's pointer mutex held. Rendering
|
||||
// reads the pointer position, so defer it until event dispatch returns.
|
||||
pub(crate) cursor_redraw_pending: bool,
|
||||
pub(crate) output_redraw_pending: Vec<Output>,
|
||||
pub(crate) locked_pointer_hint: Option<LockedPointerHint>,
|
||||
pub(crate) last_pointer_time: u32,
|
||||
pub(crate) cursor_theme: xcursor::CursorTheme,
|
||||
@@ -602,6 +603,7 @@ impl Ferese {
|
||||
autostart: config.autostart,
|
||||
cursor_status: CursorImageStatus::default_named(),
|
||||
cursor_redraw_pending: false,
|
||||
output_redraw_pending: Vec::new(),
|
||||
locked_pointer_hint: None,
|
||||
last_pointer_time: 0,
|
||||
cursor_theme,
|
||||
|
||||
@@ -72,10 +72,6 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn animation_fallback_deadline(&self, interval: std::time::Duration) -> Instant {
|
||||
self.last_animation_tick + interval.saturating_mul(2)
|
||||
}
|
||||
|
||||
pub(crate) fn reset_animation_clock(&mut self) {
|
||||
self.last_animation_tick = Instant::now();
|
||||
#[cfg(feature = "resize-metrics")]
|
||||
|
||||
@@ -45,7 +45,7 @@ impl Ferese {
|
||||
return;
|
||||
}
|
||||
|
||||
self.relayout();
|
||||
self.relayout_window(window);
|
||||
}
|
||||
|
||||
pub fn toggle_focused_fullscreen(&mut self) {
|
||||
@@ -58,7 +58,7 @@ impl Ferese {
|
||||
return;
|
||||
}
|
||||
|
||||
self.relayout();
|
||||
self.relayout_window(window);
|
||||
}
|
||||
|
||||
pub fn toggle_focused_maximized(&mut self) {
|
||||
@@ -106,7 +106,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.relayout();
|
||||
self.relayout_window(window);
|
||||
}
|
||||
|
||||
pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool {
|
||||
@@ -124,7 +124,7 @@ impl Ferese {
|
||||
|
||||
pub fn set_window_fullscreen(&mut self, window: WindowId, enabled: bool) {
|
||||
match self.workspaces.set_fullscreen(window, enabled) {
|
||||
Ok(true) => self.relayout(),
|
||||
Ok(true) => self.relayout_window(window),
|
||||
Ok(false) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(%error, ?window, enabled, "failed to set fullscreen window")
|
||||
@@ -195,6 +195,16 @@ impl Ferese {
|
||||
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
|
||||
// Cursor rendering reads current_location(), which would lock it again.
|
||||
// The event-loop epilogue coalesces this redraw after the grab returns.
|
||||
let outputs = self
|
||||
.space
|
||||
.outputs()
|
||||
.filter(|output| self.window_belongs_to_output(id, output))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for output in outputs {
|
||||
self.defer_output_redraw(output);
|
||||
}
|
||||
|
||||
self.cursor_redraw_pending = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,8 +73,9 @@ impl Ferese {
|
||||
.as_mut()
|
||||
.and_then(|cycle| cycle.advance(reverse, &available))
|
||||
{
|
||||
let previous = self.overview.selection_state();
|
||||
self.overview.select_window(id);
|
||||
crate::backends::direct::render_all(self);
|
||||
self.redraw_overview_selection(previous);
|
||||
} else {
|
||||
self.cancel_focus_cycle();
|
||||
}
|
||||
|
||||
@@ -50,6 +50,35 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub fn relayout(&mut self) {
|
||||
self.relayout_outputs(None);
|
||||
}
|
||||
|
||||
pub(crate) fn relayout_window(&mut self, id: WindowId) {
|
||||
let outputs = self
|
||||
.space
|
||||
.outputs()
|
||||
.filter(|output| self.window_belongs_to_output(id, output))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
self.relayout_on(&outputs);
|
||||
}
|
||||
|
||||
pub(crate) fn relayout_on(&mut self, outputs: &[Output]) {
|
||||
self.relayout_outputs(Some(outputs.to_vec()));
|
||||
}
|
||||
|
||||
fn relayout_outputs(&mut self, mut redraw_outputs: Option<Vec<Output>>) {
|
||||
if let Some(outputs) = redraw_outputs.as_mut() {
|
||||
outputs.extend(
|
||||
self.space
|
||||
.outputs()
|
||||
.filter(|output| {
|
||||
self.output_has_animations(output) || self.output_has_pending_visual_changes(output)
|
||||
})
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
|
||||
self.refresh_input_capture_zones();
|
||||
let visible_workspaces = self.visible_workspace_ids();
|
||||
self.reconcile_workspaces(&visible_workspaces);
|
||||
@@ -384,7 +413,11 @@ impl Ferese {
|
||||
self.retarget_overview();
|
||||
self.send_shell_snapshots();
|
||||
|
||||
crate::backends::direct::render_all(self);
|
||||
if let Some(outputs) = redraw_outputs {
|
||||
crate::backends::direct::render_on(self, &outputs);
|
||||
} else {
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
|
||||
|
||||
@@ -72,7 +72,15 @@ impl Ferese {
|
||||
swipe.progress = progress;
|
||||
}
|
||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||
self.cursor_redraw_pending = true;
|
||||
if let Some(output) = self
|
||||
.focus_swipe
|
||||
.as_ref()
|
||||
.and_then(|swipe| self.output_workspaces.output_for_workspace(swipe.workspace))
|
||||
.and_then(|id| self.outputs_by_id.get(&id))
|
||||
.cloned()
|
||||
{
|
||||
self.defer_output_redraw(output);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish_focus_swipe(&mut self, direction: Option<SwipeDirection>) -> bool {
|
||||
@@ -433,7 +441,9 @@ impl Ferese {
|
||||
.held_progress = Some(progress);
|
||||
self.refresh_workspace_slide_offsets();
|
||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||
self.cursor_redraw_pending = true;
|
||||
if let Some(output) = self.outputs_by_id.get(&output).cloned() {
|
||||
self.defer_output_redraw(output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.swipe.preview_started() {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use super::*;
|
||||
|
||||
impl Ferese {
|
||||
pub(crate) fn defer_output_redraw(&mut self, output: Output) {
|
||||
if !self.output_redraw_pending.contains(&output) {
|
||||
self.output_redraw_pending.push(output);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_output(&mut self, output: &Output, identity: String) {
|
||||
if self.output_ids.is_empty() {
|
||||
self.reset_animation_clock();
|
||||
@@ -51,6 +57,7 @@ impl Ferese {
|
||||
return;
|
||||
};
|
||||
|
||||
self.output_redraw_pending.retain(|pending| pending != output);
|
||||
self.outputs_by_id.remove(&output_id);
|
||||
self.output_names.remove(&output_id);
|
||||
self.workspace_slides.remove(&output_id);
|
||||
|
||||
@@ -44,8 +44,36 @@ impl Ferese {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn output_has_pending_visual_changes(&self, output: &Output) -> bool {
|
||||
let selected = if self.overview.is_active() {
|
||||
self.overview.selected()
|
||||
} else {
|
||||
self.focused_window
|
||||
};
|
||||
self.output_window_records(output, false).any(|(id, record, _)| {
|
||||
self.window_belongs_to_output(id, output)
|
||||
&& (record
|
||||
.focus
|
||||
.as_ref()
|
||||
.is_some_and(|focus| focus.needs_update(if selected == Some(id) { 1.0 } else { 0.0 }))
|
||||
|| record.dimming.as_ref().is_some_and(|dim| {
|
||||
dim.needs_update(crate::dimming::target(
|
||||
self.inactive_dim,
|
||||
self.focused_window,
|
||||
id,
|
||||
self.overview.is_presenting(),
|
||||
))
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn output_has_animations(&self, output: &Output) -> bool {
|
||||
if self.overview.is_animating(self.spring_config) || !self.dismissing_popups.is_empty() {
|
||||
if self.overview.is_animating(self.spring_config)
|
||||
|| self
|
||||
.dismissing_popups
|
||||
.iter()
|
||||
.any(|(root, _, _)| self.surface_outputs(root).contains(output))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -421,6 +449,26 @@ mod tests {
|
||||
state.animations_enabled = true;
|
||||
assert!(!state.sample_frame(&output, Duration::ZERO).animating);
|
||||
assert!(state.sample_frame(&other, Duration::ZERO).animating);
|
||||
// Target changes with animations disabled still need their final frame
|
||||
// on both displays, even though no spring/fade remains active afterward.
|
||||
state.focused_window = Some(id);
|
||||
state
|
||||
.windows
|
||||
.update(id, |record| record.focus = Some(crate::dimming::DimAnimation::new(1.0)));
|
||||
state.focused_window = Some(other_id);
|
||||
assert!(state.output_has_pending_visual_changes(&output));
|
||||
state.animations_enabled = false;
|
||||
state.advance_animations(Instant::now());
|
||||
assert!(!state.output_has_pending_visual_changes(&output));
|
||||
state.animations_enabled = true;
|
||||
state.windows.set_geometry(id, geometry);
|
||||
assert!(state.output_has_animations(&output));
|
||||
state.unregister_output(&other);
|
||||
assert!(!state.output_has_animations(&other));
|
||||
assert!(
|
||||
state.output_has_animations(&output),
|
||||
"remaining output keeps its active animation"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -660,7 +660,7 @@ impl Ferese {
|
||||
self.windows.update(focused, |record| {
|
||||
record.closing.get_or_insert_default();
|
||||
});
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_window(self, focused);
|
||||
}
|
||||
|
||||
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
|
||||
@@ -672,7 +672,7 @@ impl Ferese {
|
||||
self.windows.update(id, |record| {
|
||||
record.closing.get_or_insert_default();
|
||||
});
|
||||
crate::backends::direct::render_all(self);
|
||||
crate::backends::direct::render_window(self, id);
|
||||
} else {
|
||||
self.send_window_close(id);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,34 @@ asynchronous GPU work is covered conservatively by feedback-driven margins.
|
||||
Check sustained animation, idle wakeup, callback-only clients, and mixed-refresh
|
||||
outputs separately. Nested sessions do not exercise the DRM scheduler.
|
||||
|
||||
## Redraw and animation fallback counters
|
||||
|
||||
With the same environment variables, direct sessions report `redraw requests`
|
||||
by source file and line, with call counts, requested outputs and connected outputs.
|
||||
`render performance` adds per-output redraw requests and counts requests made
|
||||
while a render timer was scheduled or a frame was in flight. Compare these with
|
||||
`frames`, `no_damage_frames`, render time and missed deadlines; requests can
|
||||
coalesce and are not themselves rendered frames.
|
||||
|
||||
`animation fallback scheduling` reports watchdog arms, cancellations, wakeups,
|
||||
recovery requests and observations of animations with an intact normal frame
|
||||
chain. Summaries are emitted during redraw activity every five seconds and on
|
||||
normal exit. The fallback counters reset with each summary. An orphaned animation
|
||||
gets a watchdog deadline two refresh intervals after its own output loses its
|
||||
frame chain. Recovery also retains a requested final frame if another output
|
||||
settles that animation while scheduling is unavailable. Idle, powered-off and
|
||||
disconnected outputs do not set that deadline.
|
||||
An already scheduled render or pending page flip suppresses the watchdog; this
|
||||
policy does not replace DRM error recovery or diagnose a permanently lost flip.
|
||||
|
||||
The regression replays use a 60 Hz animation beside an idle 240 Hz output.
|
||||
The old fallback rule produces 60 unnecessary wakes in one simulated second;
|
||||
the new rule produces zero while the normal chain remains intact. Three cursor
|
||||
moves within one output require three output requests rather than the old six.
|
||||
These are deterministic scheduling counts, not live DRM CPU, latency or battery
|
||||
measurements. Verify those separately with the same hardware workload before
|
||||
making claims about power savings.
|
||||
|
||||
## Historical results
|
||||
|
||||
On 2026-09-27 at `08a5866`, two nested runs used a 1422 × 1696 output at 2× scale,
|
||||
|
||||
Reference in new issue
Block a user