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:
Samuel Onoja committed 2026-10-02 01:21:11 +01:00
1 parent 28e03a2624
commit 6f2951c6b0
27 files changed
+965 -139

No files matched your search

+207 -78
View File
@@ -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,
+398
View File
@@ -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]));
}
}
+4 -4
View File
@@ -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);