Schedule DRM frames against per-output presentation deadlines

This commit is contained in:
Samuel Onoja committed 2026-10-01 17:22:34 +01:00
1 parent 9666e4ff6c
commit a40b899b8c
10 files changed
+1168 -129

No files matched your search

+230 -97
View File
@@ -4,6 +4,8 @@ use std::io;
use std::path::Path;
use std::time::{Duration, Instant};
mod scheduling;
use scheduling::*;
use smithay::backend::allocator::Fourcc;
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType};
@@ -163,8 +165,9 @@ struct DirectOutput {
damage_tracker: OutputDamageTracker,
render_metrics: RenderMetrics,
frame_pending: bool,
scheduler: crate::frame_scheduler::FrameScheduler,
render_timer: Option<RegistrationToken>,
power_off: bool,
callback_clock: crate::presentation::CallbackClock,
callback_timer: Option<RegistrationToken>,
lock_frame_pending: bool,
}
@@ -211,6 +214,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
animation_timer: None,
animation_active: false,
});
open_primary_device(event_loop, state, &primary_path)?;
event_loop
.handle()
@@ -228,17 +232,20 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
backend.input_devices.push(device);
}
}
InputEvent::DeviceRemoved { device } => {
if let Some(backend) = state.direct_backend.as_mut() {
backend.input_devices.retain(|candidate| candidate != &device);
}
}
event => state.process_input_event(event),
})?;
event_loop
.handle()
.insert_source(notifier, move |event, _, state| match event {
SessionEvent::PauseSession => {
let handle = state.loop_handle.clone();
if let Some(backend) = state.direct_backend.as_mut() {
backend.active = false;
backend
@@ -248,6 +255,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
backend.devices.values_mut().for_each(|device| {
device.drm.pause();
device.outputs.values_mut().for_each(|output| {
cancel_output_timers(&handle, output);
output.surface.reset_buffers();
output.damage_tracker = OutputDamageTracker::from_output(&output.output);
output.frame_pending = false;
@@ -255,13 +263,16 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
});
});
}
libinput_context.suspend();
tracing::info!("direct session paused");
}
SessionEvent::ActivateSession => {
if libinput_context.resume().is_err() {
tracing::error!("failed to resume libinput");
}
if let Some(backend) = state.direct_backend.as_mut() {
backend.active = true;
for device in backend.devices.values_mut() {
@@ -270,6 +281,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
}
}
}
update_power_policy(state);
state.reset_animation_clock();
tracing::info!("direct session activated");
@@ -287,11 +299,13 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
tracing::info!(?device_id, ?path, "additional DRM device discovered");
}
}
UdevEvent::Changed { device_id } => {
if let Some(node) = direct_node_for_device(state, device_id) {
rescan_device(state, node);
}
}
UdevEvent::Removed { device_id } => {
if let Some(node) = direct_node_for_device(state, device_id) {
remove_device(state, node);
@@ -447,6 +461,7 @@ fn open_primary_device(
)
.into());
}
state
.direct_backend
.as_mut()
@@ -484,11 +499,13 @@ fn open_primary_device(
retired = true;
feedback
}
Err(error) => {
tracing::error!(?node, ?crtc, %error, "failed to retire DRM frame");
None
}
});
if let (Some(mut feedback), DrmEventTime::Monotonic(time)) = (feedback, metadata.time) {
feedback.presented(
Time::<Monotonic>::from(time),
@@ -497,6 +514,7 @@ fn open_primary_device(
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
);
}
let lock_output = state
.direct_backend
.as_mut()
@@ -509,34 +527,59 @@ fn open_primary_device(
None
}
});
if let Some(output) = lock_output {
state.lock_frame_presented(&output);
}
if let Some(output) = state
.direct_backend
.as_mut()
.and_then(|backend| backend.devices.get_mut(&node))
.and_then(|device| device.outputs.get_mut(&crtc))
&& retired
{
output.frame_pending = false;
if let DrmEventTime::Monotonic(timestamp) = metadata.time {
let now = monotonic_now();
if timestamp <= now + refresh_duration_from_mode(output.mode) {
if let Some(plan) = output.scheduler.presented(timestamp)
&& std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some()
{
tracing::debug!(target: "ferese::render", output = %output.output.name(),
target_us = plan.present_at.as_micros(), actual_us = timestamp.as_micros(),
request_to_present_us = timestamp.saturating_sub(plan.requested_at).as_micros(),
missed_deadlines = output.scheduler.missed_deadlines,
"DRM presentation schedule");
}
} else {
output.scheduler.retire_unknown();
}
} else {
output.scheduler.retire_unknown();
}
}
state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence);
tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip");
let active = state.direct_backend.as_ref().is_some_and(|backend| backend.active);
if active {
let was_animating = state
if active && retired {
schedule_frame_callbacks(state, node, crtc, monotonic_now());
let identity = state
.direct_backend
.as_ref()
.is_some_and(|backend| backend.animation_active);
let animating = state.advance_animations(Instant::now());
if animating || was_animating {
render_outputs(state, animating);
.and_then(|backend| backend.devices.get(&node))
.and_then(|device| device.outputs.get(&crtc))
.map(|output| output.output.clone());
if identity.is_some_and(|output| state.output_has_animations(&output)) {
request_frame(state, node, crtc);
} else {
render_output(state, node, crtc);
arm_frame(state, node, crtc);
}
}
}
}
DrmEvent::Error(error) => {
tracing::error!(?node, %error, "DRM event error");
}
@@ -566,16 +609,23 @@ pub(crate) fn sleep_locked_outputs(state: &mut Ferese) {
if !state.session_lock.active || !state.session_lock.sleeping {
return;
}
let Some(backend) = state.direct_backend.as_mut() else {
return;
};
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) {
for output in device.outputs.values_mut() {
if output.power_off || output.frame_pending {
continue;
}
match output.surface.surface().clear() {
Ok(()) => output.power_off = true,
Ok(()) => {
cancel_output_timers(&state.loop_handle, output);
output.power_off = true;
}
Err(error) => {
tracing::warn!(%error, output = %output.output.name(), "could not sleep locked display")
}
@@ -609,6 +659,7 @@ 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
@@ -623,7 +674,7 @@ fn render_outputs(state: &mut Ferese, animating: bool) {
.unwrap_or_default();
for (node, crtc) in outputs {
render_output(state, node, crtc);
request_frame(state, node, crtc);
}
}
@@ -697,8 +748,7 @@ fn lid_hides_panel(closed: bool, external_available: bool) -> bool {
closed && external_available
}
fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
schedule_frame_callbacks(state, node, crtc);
fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: crate::frame_scheduler::FramePlan) {
if state.session_lock.active && state.session_lock.sleeping {
sleep_locked_outputs(state);
let asleep = state
@@ -711,11 +761,13 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
return;
}
}
let missed_deadlines = state
.direct_backend
.as_ref()
.and_then(|backend| backend.presentation.get(&(node, crtc)))
.map_or(0, |clock| clock.missed_deadlines);
.and_then(|backend| backend.devices.get(&node))
.and_then(|device| device.outputs.get(&crtc))
.map_or(0, |output| output.scheduler.missed_deadlines);
let Some(mut device) = state
.direct_backend
.as_mut()
@@ -723,10 +775,12 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
else {
return;
};
let Some(mut output) = device.outputs.remove(&crtc) else {
restore_device(state, node, device);
return;
};
if !device.drm.is_active() || output.frame_pending {
device.outputs.insert(crtc, output);
restore_device(state, node, device);
@@ -734,73 +788,92 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
}
let render_started = Instant::now();
let rendered = (|| -> Result<bool, Box<dyn Error>> {
state.process_dmabuf_imports(&mut device.renderer);
let (mut buffer, age) = output.surface.next_buffer()?;
let elements = animated_window_elements(state, &mut device.renderer, &output.output);
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
let mut framebuffer = device.renderer.bind(&mut buffer)?;
let result = output.damage_tracker.render_output(
&mut device.renderer,
&mut framebuffer,
usize::from(age),
&elements,
[0.035, 0.04, 0.055, 1.0],
)?;
let cursorless_capture = state.has_pending_screencopy(&output.output, false);
let captured_with_cursor = state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true);
if cursorless_capture {
let cursorless_elements = cursorless_window_elements(state, &mut device.renderer, &output.output);
redraw_output(
let now = monotonic_now();
let horizon = output
.scheduler
.forecast_time(now, plan)
.map_or(Duration::ZERO, |target| target.saturating_sub(now));
let mut forecast = state.forecast_render(&output.output, horizon);
let rendered = {
let state = &mut *forecast;
(|| -> Result<bool, Box<dyn Error>> {
state.process_dmabuf_imports(&mut device.renderer);
let (mut buffer, age) = output.surface.next_buffer()?;
let elements = animated_window_elements(state, &mut device.renderer, &output.output);
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
let mut framebuffer = device.renderer.bind(&mut buffer)?;
let result = output.damage_tracker.render_output(
&mut device.renderer,
&mut framebuffer,
&output.output,
&cursorless_elements,
usize::from(age),
&elements,
[0.035, 0.04, 0.055, 1.0],
)?;
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, false);
redraw_output(&mut device.renderer, &mut framebuffer, &output.output, &elements)?;
} else if captured_with_cursor {
let _ = device
.renderer
.render(
let cursorless_capture = state.has_pending_screencopy(&output.output, false);
let captured_with_cursor =
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true);
if cursorless_capture {
let cursorless_elements = cursorless_window_elements(state, &mut device.renderer, &output.output);
redraw_output(
&mut device.renderer,
&mut framebuffer,
output.output.current_mode().expect("output has a mode").size,
output.output.current_transform(),
)?
.finish()?;
}
let Some(damage) = result.damage.cloned() else {
&output.output,
&cursorless_elements,
)?;
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, false);
redraw_output(&mut device.renderer, &mut framebuffer, &output.output, &elements)?;
} else if captured_with_cursor {
let _ = device
.renderer
.render(
&mut framebuffer,
output.output.current_mode().expect("output has a mode").size,
output.output.current_transform(),
)?
.finish()?;
}
let Some(damage) = result.damage.cloned() else {
output
.render_metrics
.record_no_damage(render_started.elapsed(), missed_deadlines);
return Ok(false);
};
let presentation = crate::presentation::take_output_feedback(
state,
&output.output,
&result.states,
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
);
output
.surface
.queue_buffer(Some(result.sync), Some(damage.clone()), presentation)?;
output
.render_metrics
.record_no_damage(render_started.elapsed(), missed_deadlines);
return Ok(false);
};
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
output.frame_pending = true;
output.lock_frame_pending = state.session_lock.active;
Ok(true)
})()
};
let presentation = crate::presentation::take_output_feedback(
state,
&output.output,
&result.states,
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
);
output
.surface
.queue_buffer(Some(result.sync), Some(damage.clone()), presentation)?;
output
.render_metrics
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
output.frame_pending = true;
output.lock_frame_pending = state.session_lock.active;
Ok(true)
})();
let submitted = rendered.as_ref().is_ok_and(|submitted| *submitted);
record_frame_schedule(&mut output, plan, render_started.elapsed(), submitted);
match rendered {
Ok(true) => tracing::trace!(?node, ?crtc, "queued DRM frame"),
Ok(false) => (),
Err(error) => tracing::error!(?node, ?crtc, %error, "failed to render DRM frame"),
}
device.outputs.insert(crtc, output);
restore_device(state, node, device);
restore_device(&mut forecast, node, device);
drop(forecast);
if !submitted {
schedule_frame_callbacks(state, node, crtc, plan.present_at);
}
}
fn restore_device(state: &mut Ferese, node: DrmNode, device: DirectDevice) {
@@ -840,11 +913,13 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
return;
}
};
let lid_closed = state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed);
apply_lid_policy(&mut scan, lid_closed);
if let Some(backend) = state.direct_backend.as_mut() {
backend.connected_outputs = scan.connected_outputs;
}
let mut selections = scan
.selections
.into_iter()
@@ -866,10 +941,12 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
} else {
Ok(())
};
match result {
Ok(()) => {
let old_geometry = state.space.output_geometry(&output.output);
output.mode = selection.mode;
cancel_output_timers(&state.loop_handle, output);
output.settings = selection.settings.clone();
let position = selection.settings.position.unwrap_or_else(|| {
state
@@ -878,6 +955,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
.map(|g| [g.loc.x, g.loc.y])
.unwrap_or([0, 0])
});
output.output.change_current_state(
Some(OutputMode::from(output.mode)),
Some(output_transform(output.settings.transform)),
@@ -910,6 +988,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
);
}
}
output.damage_tracker = OutputDamageTracker::from_output(&output.output);
if let Some(backend) = state.direct_backend.as_mut() {
let clock = backend.presentation.entry((node, crtc)).or_default();
@@ -917,13 +996,16 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
clock.reset_timing();
}
}
Err(error) => {
tracing::warn!(?crtc, %error, "display change failed; retaining current output")
}
}
selections.remove(&crtc);
continue;
}
let unchanged = device.outputs.get(&crtc).is_some_and(|output| {
selections.get(&crtc).is_some_and(|selection| {
selection.connector.handle() == output.connector
@@ -931,6 +1013,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
&& selection.settings == output.settings
})
});
if unchanged {
selections.remove(&crtc);
continue;
@@ -944,12 +1027,14 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
continue;
}
if let Some(output) = device.outputs.remove(&crtc) {
if let Some(mut output) = device.outputs.remove(&crtc) {
cancel_output_timers(&state.loop_handle, &mut output);
state.display_handle.disable_global::<Ferese>(output.global);
state.unregister_output(&output.output);
if let Some(backend) = state.direct_backend.as_mut() {
backend.presentation.remove(&(node, crtc));
}
tracing::info!(?node, ?crtc, "removed DRM output");
}
}
@@ -969,6 +1054,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
.set_refresh(OutputMode::from(mode).refresh);
device.outputs.insert(crtc, output);
}
Err(error) => tracing::error!(?node, ?crtc, %error, "failed to add DRM output"),
}
}
@@ -997,9 +1083,12 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
info.enabled = true;
info.current_mode = Some(connected_mode_info(output.mode));
}
continue;
}
if let Some(output) = device.outputs.remove(&crtc) {
if let Some(mut output) = device.outputs.remove(&crtc) {
cancel_output_timers(&state.loop_handle, &mut output);
state.display_handle.disable_global::<Ferese>(output.global);
state.unregister_output(&output.output);
if let Some(backend) = state.direct_backend.as_mut() {
@@ -1007,6 +1096,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
}
}
}
if let Some(backend) = state.direct_backend.as_mut() {
for output in device.outputs.values() {
if let Some(info) = backend
@@ -1023,6 +1113,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
}
}
}
restore_device(state, node, device);
state.restore_output_focus();
state.relayout();
@@ -1041,16 +1132,19 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
let context = device.renderer.context_id().erased();
state.wallpaper.forget_context(&context);
state.resize_snapshots.retain(|_, snapshot| snapshot.context != context);
for (crtc, output) in device.outputs {
for (crtc, mut output) in device.outputs {
cancel_output_timers(&state.loop_handle, &mut output);
state.display_handle.disable_global::<Ferese>(output.global);
state.unregister_output(&output.output);
if let Some(backend) = state.direct_backend.as_mut() {
backend.presentation.remove(&(node, crtc));
}
}
if let Some(backend) = state.direct_backend.as_mut() {
backend.connected_outputs.clear();
}
tracing::info!(?node, "removed DRM device");
}
@@ -1066,6 +1160,10 @@ fn refresh_duration(state: &Ferese, node: DrmNode, crtc: crtc::Handle) -> Durati
.unwrap_or_else(|| Duration::from_millis(16))
}
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
.direct_backend
@@ -1088,6 +1186,7 @@ fn arm_animation_timer(state: &mut Ferese) {
if !needed {
return;
}
let deadline = state.animation_fallback_deadline(animation_interval(state));
match state
.loop_handle
@@ -1101,7 +1200,30 @@ fn arm_animation_timer(state: &mut Ferese) {
if Instant::now() < deadline {
return TimeoutAction::ToInstant(deadline);
}
render_all(state);
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()
@@ -1110,9 +1232,11 @@ fn arm_animation_timer(state: &mut Ferese) {
return TimeoutAction::ToInstant(state.animation_fallback_deadline(animation_interval(state)));
}
}
if let Some(backend) = state.direct_backend.as_mut() {
backend.animation_timer = None;
}
TimeoutAction::Drop
}) {
Ok(token) => state.direct_backend.as_mut().unwrap().animation_timer = Some(token),
@@ -1120,12 +1244,13 @@ fn arm_animation_timer(state: &mut Ferese) {
}
}
fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, target: Duration) {
if state.session_lock.sleeping {
return;
}
let now = Instant::now();
let interval = refresh_duration(state, node, crtc);
let timestamp = monotonic_now();
let Some(output) = state
.direct_backend
.as_ref()
@@ -1136,15 +1261,19 @@ fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl
else {
return;
};
if output.callback_timer.is_some() {
return;
}
let identity = output.output.clone();
let deadline = output.callback_clock.deadline(now, interval);
if deadline <= now {
let callback_at = output.scheduler.callback_deadline(timestamp, target);
if callback_at <= timestamp {
deliver_frame_callbacks(state, node, crtc, &identity);
return;
}
let deadline = now + callback_at.saturating_sub(timestamp);
match state
.loop_handle
.insert_source(Timer::from_deadline(deadline), move |_, _, state| {
@@ -1161,6 +1290,7 @@ fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl
output.callback_timer = Some(token);
}
}
Err(error) => tracing::warn!(%error, "could not schedule DRM frame callbacks"),
}
}
@@ -1169,9 +1299,11 @@ fn deliver_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle
let Some(backend) = state.direct_backend.as_mut() else {
return;
};
let Some(device) = backend.devices.get_mut(&node) else {
return;
};
let Some(output) = device
.outputs
.get_mut(&crtc)
@@ -1179,11 +1311,20 @@ fn deliver_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle
else {
return;
};
output.callback_timer = None;
if !backend.active || !device.drm.is_active() || state.session_lock.sleeping {
return;
}
output.callback_clock.sent(Instant::now());
let timestamp = monotonic_now();
let next = output.scheduler.callback_deadline(timestamp, timestamp);
if next > timestamp {
schedule_frame_callbacks(state, node, crtc, next);
return;
}
output.scheduler.callback_sent(timestamp);
send_frame_callbacks(state, identity);
}
@@ -1438,7 +1579,8 @@ fn create_direct_output(
render_metrics,
frame_pending: false,
power_off: false,
callback_clock: Default::default(),
scheduler: crate::frame_scheduler::FrameScheduler::new(refresh_duration_from_mode(mode)),
render_timer: None,
callback_timer: None,
lock_frame_pending: false,
})
@@ -1540,9 +1682,15 @@ impl DirectBackendState {
time: DrmEventTime,
sequence: u32,
) -> Option<Duration> {
self.presentation
.get_mut(&(node, crtc))
.and_then(|clock| clock.record(time, sequence))
let missed = self
.devices
.get(&node)
.and_then(|device| device.outputs.get(&crtc))
.map_or(0, |output| output.scheduler.missed_deadlines);
self.presentation.get_mut(&(node, crtc)).and_then(|clock| {
clock.missed_deadlines = missed;
clock.record(time, sequence)
})
}
}
@@ -1566,21 +1714,6 @@ impl PresentationClock {
self.last_presentation = Some(time);
self.presented_frames = self.presented_frames.saturating_add(1);
if let (Some(delta), Some(refresh)) = (delta, self.refresh_interval)
&& delta > refresh + refresh / 2
{
let elapsed_frames = (delta.as_nanos() / refresh.as_nanos()).max(1) as u64;
let missed = elapsed_frames.saturating_sub(1);
self.missed_deadlines = self.missed_deadlines.saturating_add(missed);
tracing::warn!(
sequence,
?delta,
?refresh,
missed,
total = self.missed_deadlines,
"missed DRM presentation deadline"
);
}
delta
}
}
@@ -1639,14 +1772,14 @@ mod tests {
}
#[test]
fn presentation_clock_counts_missed_deadlines() {
fn presentation_clock_does_not_count_idle_gaps_as_missed_deadlines() {
let mut clock = PresentationClock::default();
clock.set_refresh(60_000);
clock.record(DrmEventTime::Monotonic(Duration::ZERO), 1);
clock.record(DrmEventTime::Monotonic(Duration::from_millis(50)), 2);
assert_eq!(clock.missed_deadlines, 2);
assert_eq!(clock.missed_deadlines, 0);
}
#[test]
@@ -0,0 +1,141 @@
use super::*;
use crate::frame_scheduler::FramePlan;
pub(super) fn monotonic_now() -> Duration {
smithay::utils::Clock::<Monotonic>::new().now().into()
}
pub(super) fn cancel_output_timers(
handle: &smithay::reexports::calloop::LoopHandle<'static, Ferese>,
output: &mut DirectOutput,
) {
for token in [output.render_timer.take(), output.callback_timer.take()]
.into_iter()
.flatten()
{
handle.remove(token);
}
output.scheduler.reset(Duration::from_nanos(
1_000_000_000_000 / OutputMode::from(output.mode).refresh.max(1) as u64,
));
}
pub(super) fn request_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
if state.session_lock.sleeping {
sleep_locked_outputs(state);
return;
}
let now = monotonic_now();
let Some(output) = state
.direct_backend
.as_mut()
.filter(|backend| backend.active)
.and_then(|backend| backend.devices.get_mut(&node))
.filter(|device| device.drm.is_active())
.and_then(|device| device.outputs.get_mut(&crtc))
else {
return;
};
if output.power_off {
return;
}
output.scheduler.request(now);
arm_frame(state, node, crtc);
}
pub(super) fn arm_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
let now = monotonic_now();
let Some(output) = state
.direct_backend
.as_mut()
.filter(|backend| backend.active)
.and_then(|backend| backend.devices.get_mut(&node))
.filter(|device| device.drm.is_active())
.and_then(|device| device.outputs.get_mut(&crtc))
else {
return;
};
if output.render_timer.is_some() || output.frame_pending || output.power_off || state.session_lock.sleeping {
return;
}
let Some(plan) = output.scheduler.plan(now) else {
return;
};
let identity = output.output.clone();
// Even an immediate repaint is queued. Bursts of input and surface commits
// in the same event-loop dispatch are folded into one scene build.
let deadline = Instant::now() + plan.render_at.saturating_sub(now);
match state
.loop_handle
.insert_source(Timer::from_deadline(deadline), move |_, _, state| {
dispatch_frame(state, node, crtc, &identity);
TimeoutAction::Drop
}) {
Ok(token) => {
if let Some(output) = state
.direct_backend
.as_mut()
.and_then(|backend| backend.devices.get_mut(&node))
.and_then(|device| device.outputs.get_mut(&crtc))
{
output.render_timer = Some(token);
}
}
Err(error) => tracing::warn!(%error, "could not schedule DRM frame"),
}
}
fn dispatch_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identity: &Output) {
let Some(output) = state
.direct_backend
.as_mut()
.filter(|backend| backend.active)
.and_then(|backend| backend.devices.get_mut(&node))
.filter(|device| device.drm.is_active())
.and_then(|device| device.outputs.get_mut(&crtc))
.filter(|output| &output.output == identity)
else {
return;
};
output.render_timer = None;
if output.frame_pending || output.power_off || state.session_lock.sleeping {
return;
}
let Some(plan) = output.scheduler.begin() else {
return;
};
let animating = state.advance_animations(Instant::now());
if let Some(backend) = state.direct_backend.as_mut() {
backend.animation_active = animating;
}
render_output(state, node, crtc, plan);
if state.output_has_animations(identity) {
request_frame(state, node, crtc);
}
arm_animation_timer(state);
}
pub(super) fn record_frame_schedule(output: &mut DirectOutput, plan: FramePlan, cost: Duration, submitted: bool) {
output.scheduler.rendered(plan, monotonic_now(), cost, submitted);
if std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some() {
let started = monotonic_now().saturating_sub(cost);
tracing::debug!(target: "ferese::render", output = %output.output.name(),
render_target_us = plan.render_at.as_micros(), presentation_target_us = plan.present_at.as_micros(),
target_phase_known = plan.phase_known,
render_started_us = started.as_micros(), timer_lateness_us = started.saturating_sub(plan.render_at).as_micros(),
render_us = cost.as_micros(), submitted, "DRM frame schedule");
}
}
+12 -1
View File
@@ -18,7 +18,7 @@ pub(crate) fn target(
}
/// A bounded, interruption-safe opacity transition; it never overshoots.
#[derive(Debug)]
#[derive(Clone, Debug)]
pub(crate) struct DimAnimation {
pub current: f64,
start: f64,
@@ -27,6 +27,14 @@ pub(crate) struct DimAnimation {
}
impl DimAnimation {
pub(crate) fn is_animating(&self) -> bool {
self.current != self.target
}
pub(crate) fn predict(&mut self, delta: Duration, duration_ms: f64) {
self.advance(self.target, delta, duration_ms);
}
pub fn new(value: f64) -> Self {
Self {
current: value,
@@ -42,16 +50,19 @@ impl DimAnimation {
self.target = target;
self.elapsed = 0.0;
}
if duration_ms == 0.0 {
self.current = target;
return false;
}
self.elapsed += delta.as_secs_f64() * 1000.0;
let progress = (self.elapsed / duration_ms).min(1.0);
if progress == 1.0 {
self.current = target;
return false;
}
let eased = progress * progress * (3.0 - 2.0 * progress);
self.current = self.start + (target - self.start) * eased;
self.current != target
+368
View File
@@ -0,0 +1,368 @@
//! Fixed-refresh scheduling policy, independent of DRM and the event loop.
use std::time::Duration;
const MIN_RENDER: Duration = Duration::from_millis(2);
const TIMER_MARGIN: Duration = Duration::from_millis(1);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct FramePlan {
pub render_at: Duration,
pub present_at: Duration,
pub requested_at: Duration,
pub phase_known: bool,
}
#[derive(Debug)]
pub(crate) struct FrameScheduler {
interval: Duration,
last_presented: Option<Duration>,
last_target: Option<Duration>,
requested_at: Option<Duration>,
scheduled: Option<FramePlan>,
submitted: Option<FramePlan>,
last_callback: Option<Duration>,
render_estimate: Duration,
render_variance: Duration,
last_sample: Option<Duration>,
extra_margin: Duration,
pub missed_deadlines: u64,
}
impl FrameScheduler {
pub fn new(interval: Duration) -> Self {
Self {
interval: interval.max(Duration::from_micros(1)),
last_presented: None,
last_target: None,
requested_at: None,
scheduled: None,
submitted: None,
last_callback: None,
render_estimate: MIN_RENDER,
render_variance: Duration::ZERO,
last_sample: None,
extra_margin: Duration::ZERO,
missed_deadlines: 0,
}
}
pub fn reset(&mut self, interval: Duration) {
let missed = self.missed_deadlines;
*self = Self::new(interval);
self.missed_deadlines = missed;
}
pub fn request(&mut self, now: Duration) {
self.requested_at.get_or_insert(now);
}
fn budget(&self, now: Duration) -> Duration {
let idle = self
.last_presented
.is_none_or(|last| now.saturating_sub(last) >= self.interval * 100);
if idle {
// A cold GPU is less predictable. Start immediately on idle wakeup.
self.interval
} else {
(self.render_estimate + self.render_variance * 2 + TIMER_MARGIN + self.extra_margin).min(self.interval)
}
}
fn phase_at_or_after(&self, time: Duration) -> Duration {
let Some(last) = self.last_presented else {
return time;
};
let count = time
.saturating_sub(last)
.as_nanos()
.div_ceil(self.interval.as_nanos())
.max(1);
last + self.interval.mul_f64(count as f64)
}
pub fn plan(&mut self, now: Duration) -> Option<FramePlan> {
if self.submitted.is_some() {
return None;
}
if let Some(plan) = self.scheduled {
return Some(plan);
}
let requested_at = self.requested_at?;
if self.last_presented.is_some_and(|last| last > now + self.interval) {
self.last_presented = None;
self.last_target = None;
}
let budget = self.budget(now);
let idle = self
.last_presented
.is_none_or(|last| now.saturating_sub(last) >= self.interval * 100);
// With one in-flight GBM frame, submitting immediately can hit the
// nearest vblank even when the conservative budget is larger than
// the remaining time. Do not deliberately skip that presentation.
let next = if self.last_presented.is_some() {
now + Duration::from_nanos(1)
} else {
now + self.interval
};
let earliest = next.max(self.last_target.map_or(next, |last| last + self.interval));
let present_at = self.phase_at_or_after(earliest);
let plan = FramePlan {
render_at: if idle {
now
} else {
present_at.saturating_sub(budget).max(now)
},
present_at,
requested_at,
phase_known: self.last_presented.is_some(),
};
self.scheduled = Some(plan);
Some(plan)
}
pub fn begin(&mut self) -> Option<FramePlan> {
let plan = self.scheduled.take()?;
self.requested_at = None;
self.last_target = Some(plan.present_at);
Some(plan)
}
pub fn rendered(&mut self, plan: FramePlan, now: Duration, cost: Duration, submitted: bool) {
if submitted {
self.submitted = Some(plan);
let elapsed = self
.last_sample
.map_or(Duration::from_millis(500), |last| now.saturating_sub(last));
self.last_sample = Some(now);
let sample = cost.max(MIN_RENDER);
let ratio = (elapsed.as_secs_f64() / 0.5).clamp(0.01, 1.0);
let deviation = sample.saturating_sub(self.render_estimate);
let variance_ratio = (elapsed.as_secs_f64() / 6.0).clamp(0.001, 0.1);
self.render_variance = self.render_variance.mul_f64(1.0 - variance_ratio).max(deviation);
self.render_estimate = self.render_estimate.mul_f64(1.0 - ratio) + sample.mul_f64(ratio);
}
}
pub fn presented(&mut self, timestamp: Duration) -> Option<FramePlan> {
// Regressive or implausibly early events must not shift the phase backward.
if self.last_presented.is_some_and(|last| timestamp < last) {
self.last_presented = None;
self.last_target = None;
return self.submitted.take();
}
self.last_presented = Some(timestamp);
let plan = self.submitted.take()?;
// The first submission has only an estimated phase. Retiring it on
// the actual refresh must not carry that estimate into the next frame.
self.last_target = Some(timestamp);
let late = timestamp.saturating_sub(plan.present_at);
if plan.phase_known && late > self.interval / 2 {
let missed = (late.as_nanos() + self.interval.as_nanos() / 2) / self.interval.as_nanos();
self.missed_deadlines = self.missed_deadlines.saturating_add(missed as u64);
// CPU submission timings may omit asynchronous GPU work. Increase
// the margin promptly after a real miss; relax it slowly afterward.
self.extra_margin = (self.extra_margin + TIMER_MARGIN).min(self.interval / 2);
} else {
self.extra_margin = self.extra_margin.mul_f64(0.98);
}
Some(plan)
}
pub fn retire_unknown(&mut self) {
self.submitted = None;
self.last_presented = None;
self.last_target = None;
}
pub fn forecast_time(&self, now: Duration, plan: FramePlan) -> Option<Duration> {
// Before the first real page flip there is no reliable refresh phase.
// A late timer still samples the next physical presentation, while
// deadline metrics retain the original scheduled target.
plan.phase_known.then(|| {
plan.present_at
.max(self.phase_at_or_after(now + Duration::from_nanos(1)))
})
}
pub fn callback_deadline(&self, now: Duration, target: Duration) -> Duration {
let tolerance = Duration::from_micros(500).min(self.interval / 8);
if target <= now
&& self
.last_callback
.is_none_or(|last| last + self.interval <= now + tolerance)
{
return now;
}
let earliest = self
.last_callback
.map_or(now, |last| last + self.interval)
.max(target)
.max(now);
self.phase_at_or_after(earliest)
}
pub fn callback_sent(&mut self, now: Duration) {
// Record the refresh slot, not delayed event-loop dispatch time.
// Otherwise a callback delivered a few microseconds late can make
// the next refresh look too early and halve a client's frame rate.
self.last_callback = Some(self.last_presented.map_or(now, |last| {
let tolerance = Duration::from_micros(500).min(self.interval / 8);
let slots = (now.saturating_sub(last) + tolerance).as_nanos() / self.interval.as_nanos();
last + self.interval.mul_f64(slots as f64)
}));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ms(value: u64) -> Duration {
Duration::from_millis(value)
}
#[test]
fn requests_coalesce_without_postponing_the_frame_and_wait_for_retirement() {
let mut scheduler = FrameScheduler::new(ms(10));
scheduler.presented(ms(100));
scheduler.request(ms(101));
let plan = scheduler.plan(ms(101)).unwrap();
assert_eq!((plan.render_at, plan.present_at), (ms(107), ms(110)));
scheduler.request(ms(102));
assert_eq!(scheduler.plan(ms(102)), Some(plan));
assert_eq!(scheduler.begin(), Some(plan));
scheduler.rendered(plan, ms(108), ms(1), true);
scheduler.request(ms(109));
assert_eq!(scheduler.plan(ms(109)), None);
scheduler.presented(ms(110));
assert_eq!(scheduler.plan(ms(110)).unwrap().present_at, ms(120));
}
#[test]
fn idle_wakeup_starts_early_and_is_not_a_missed_deadline() {
let mut scheduler = FrameScheduler::new(ms(10));
scheduler.presented(ms(100));
scheduler.request(ms(10_100));
let plan = scheduler.plan(ms(10_100)).unwrap();
assert_eq!(plan.render_at, ms(10_100));
scheduler.begin();
scheduler.rendered(plan, ms(10_101), ms(1), true);
scheduler.presented(ms(10_110));
assert_eq!(scheduler.missed_deadlines, 0);
}
#[test]
fn misses_measure_the_submitted_target_and_expand_the_budget() {
let mut scheduler = FrameScheduler::new(ms(10));
scheduler.presented(ms(100));
scheduler.request(ms(101));
let plan = scheduler.plan(ms(101)).unwrap();
scheduler.begin();
scheduler.rendered(plan, ms(108), ms(1), true);
assert_eq!(scheduler.presented(ms(130)), Some(plan));
assert_eq!(scheduler.missed_deadlines, 2);
assert_eq!(scheduler.budget(ms(131)), ms(4));
assert_eq!(scheduler.presented(ms(129)), None);
}
#[test]
fn first_retirement_aligns_the_next_frame_to_the_observed_refresh() {
let mut scheduler = FrameScheduler::new(ms(10));
scheduler.request(ms(100));
let plan = scheduler.plan(ms(100)).unwrap();
scheduler.begin();
scheduler.rendered(plan, ms(102), ms(2), true);
scheduler.presented(ms(103));
scheduler.request(ms(104));
assert_eq!(scheduler.plan(ms(104)).unwrap().present_at, ms(113));
}
#[test]
fn no_damage_attempts_and_callbacks_do_not_spin_within_a_refresh() {
let mut scheduler = FrameScheduler::new(ms(10));
scheduler.presented(ms(100));
scheduler.request(ms(101));
let plan = scheduler.plan(ms(101)).unwrap();
scheduler.begin();
scheduler.rendered(plan, ms(108), ms(1), false);
scheduler.callback_sent(ms(110));
scheduler.request(ms(108));
assert_eq!(scheduler.plan(ms(108)).unwrap().present_at, ms(120));
assert_eq!(scheduler.callback_deadline(ms(111), ms(110)), ms(120));
}
#[test]
fn outputs_keep_independent_phases_and_reset_cancels_obsolete_work() {
let mut fast = FrameScheduler::new(ms(4));
let mut slow = FrameScheduler::new(ms(16));
for scheduler in [&mut fast, &mut slow] {
scheduler.presented(ms(100));
scheduler.request(ms(101));
}
assert_eq!(fast.plan(ms(101)).unwrap().present_at, ms(104));
assert_eq!(slow.plan(ms(101)).unwrap().present_at, ms(116));
fast.reset(ms(8));
assert_eq!(fast.plan(ms(102)), None);
}
#[test]
fn callback_dispatch_jitter_does_not_skip_a_refresh_or_duplicate_a_callback() {
let mut scheduler = FrameScheduler::new(ms(10));
scheduler.presented(ms(100));
scheduler.callback_sent(ms(100) + Duration::from_micros(80));
scheduler.presented(ms(110));
let now = ms(110) + Duration::from_micros(10);
assert_eq!(scheduler.callback_deadline(now, now), now);
scheduler.callback_sent(now);
assert_eq!(scheduler.callback_deadline(now, now), ms(120));
}
#[test]
fn unknown_retirement_and_regressive_timestamps_do_not_strand_pending_work() {
let mut scheduler = FrameScheduler::new(ms(10));
scheduler.presented(ms(100));
scheduler.request(ms(101));
let plan = scheduler.plan(ms(101)).unwrap();
scheduler.begin();
scheduler.rendered(plan, ms(108), ms(1), true);
scheduler.request(ms(109));
assert_eq!(scheduler.presented(ms(90)), Some(plan));
assert!(scheduler.plan(ms(110)).is_some());
scheduler.retire_unknown();
assert!(scheduler.plan(ms(111)).is_some());
}
#[test]
fn cold_gpu_and_late_dispatch_do_not_predict_a_past_or_deliberately_skipped_frame() {
let mut scheduler = FrameScheduler::new(ms(10));
scheduler.presented(ms(100));
scheduler.request(ms(10_101));
let plan = scheduler.plan(ms(10_101)).unwrap();
assert_eq!(plan.render_at, ms(10_101));
assert_eq!(plan.present_at, ms(10_110));
assert_eq!(scheduler.forecast_time(ms(10_111), plan), Some(ms(10_120)));
let mut unknown = FrameScheduler::new(ms(10));
unknown.request(ms(100));
let plan = unknown.plan(ms(100)).unwrap();
assert_eq!(unknown.forecast_time(ms(101), plan), None);
unknown.begin();
unknown.rendered(plan, ms(102), ms(2), true);
unknown.presented(ms(130));
assert_eq!(
unknown.missed_deadlines, 0,
"unknown phase is not a measurable missed deadline"
);
}
}
+1
View File
@@ -7,6 +7,7 @@ mod daemon;
mod dimming;
mod effects;
mod floating;
mod frame_scheduler;
mod gestures;
mod grabs;
mod handlers;
+30
View File
@@ -102,6 +102,12 @@ pub(crate) struct OverviewState {
labels: OverviewStateLabel,
}
pub(crate) struct OverviewAnimationSnapshot {
presentations: HashMap<WindowId, AnimatedRect>,
opacity: AnimatedValue,
exit_transition: Option<(f64, f64)>,
}
impl Default for OverviewState {
fn default() -> Self {
Self {
@@ -160,6 +166,30 @@ fn centered_row_start(strip: Rect, count: usize, card_width: f64) -> f64 {
}
impl OverviewState {
pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool {
self.exit_transition.is_some()
|| (self.opacity.current - self.opacity.target).abs() > 0.001
|| self.opacity.velocity.abs() > 0.005
|| self.presentations.values().any(|rect| !rect.is_settled(spring))
}
pub(crate) fn predict(&mut self, delta: Duration, spring: SpringConfig) -> OverviewAnimationSnapshot {
let snapshot = OverviewAnimationSnapshot {
presentations: self.presentations.clone(),
opacity: self.opacity,
exit_transition: self.exit_transition,
};
self.advance(delta, spring, true);
snapshot
}
pub(crate) fn restore_prediction(&mut self, snapshot: OverviewAnimationSnapshot) {
self.presentations = snapshot.presentations;
self.opacity = snapshot.opacity;
self.exit_transition = snapshot.exit_transition;
}
pub(crate) fn set_font_family(&mut self, family: String) {
if self.font_family != family {
self.font_family = family;
+17 -31
View File
@@ -59,21 +59,6 @@ fn rendered_feedback_output(output: &Output, id: Id, rendered: &RenderElementSta
.then(|| output.clone())
}
#[derive(Default)]
pub(crate) struct CallbackClock {
last_sent: Option<Instant>,
}
impl CallbackClock {
pub(crate) fn deadline(&self, now: Instant, interval: Duration) -> Instant {
self.last_sent.map_or(now, |last| (last + interval).max(now))
}
pub(crate) fn sent(&mut self, now: Instant) {
self.last_sent = Some(now);
}
}
pub(crate) fn frame_delta(last: &mut Instant, now: Instant) -> Duration {
let delta = now.saturating_duration_since(*last);
*last = (*last).max(now);
@@ -442,37 +427,38 @@ mod tests {
#[test]
fn no_damage_requests_are_paced_without_buffer_submission() {
let start = Instant::now();
let start = Duration::ZERO;
let refresh = Duration::from_millis(16);
let mut clock = CallbackClock::default();
assert_eq!(clock.deadline(start, refresh), start);
clock.sent(start);
let mut clock = crate::frame_scheduler::FrameScheduler::new(refresh);
assert_eq!(clock.callback_deadline(start, start), start);
clock.callback_sent(start);
for millisecond in 1..16 {
assert_eq!(
clock.deadline(start + Duration::from_millis(millisecond), refresh),
clock.callback_deadline(start + Duration::from_millis(millisecond), start),
start + refresh
);
}
let late = start + Duration::from_millis(50);
assert_eq!(clock.deadline(late, refresh), late);
clock.sent(late);
assert_eq!(clock.deadline(late, refresh), late + refresh);
assert_eq!(clock.callback_deadline(late, late), late);
clock.callback_sent(late);
assert_eq!(clock.callback_deadline(late, late), late + refresh);
}
#[test]
fn independent_output_callback_clocks_do_not_sum_their_rates() {
let start = Instant::now();
let mut slow = CallbackClock::default();
let mut fast = CallbackClock::default();
slow.sent(start);
fast.sent(start);
fast.sent(start + Duration::from_millis(8));
let start = Duration::ZERO;
let mut slow = crate::frame_scheduler::FrameScheduler::new(Duration::from_millis(16));
let mut fast = crate::frame_scheduler::FrameScheduler::new(Duration::from_millis(8));
slow.callback_sent(start);
fast.callback_sent(start);
fast.callback_sent(start + Duration::from_millis(8));
assert_eq!(
slow.deadline(start + Duration::from_millis(8), Duration::from_millis(16)),
slow.callback_deadline(start + Duration::from_millis(8), start),
start + Duration::from_millis(16)
);
assert_eq!(
fast.deadline(start + Duration::from_millis(8), Duration::from_millis(8)),
fast.callback_deadline(start + Duration::from_millis(8), start),
start + Duration::from_millis(16)
);
}
+3
View File
@@ -5,6 +5,7 @@ mod hit_testing;
mod layout;
mod navigation;
mod outputs;
mod prediction;
mod windows;
use std::collections::hash_map::Entry;
@@ -169,12 +170,14 @@ impl FocusSwipe {
}
}
#[derive(Clone)]
struct WorkspaceSlideItem {
workspace: WorkspaceId,
start: SlideOffset,
target: SlideOffset,
}
#[derive(Clone)]
struct WorkspaceSlide {
items: Vec<WorkspaceSlideItem>,
elapsed: Duration,
+345
View File
@@ -0,0 +1,345 @@
use super::*;
/// Rendering samples an isolated forecast. Restore authoritative state before
/// returning to input, client commits, or another monitor's render deadline.
pub(crate) struct RenderForecast<'a> {
state: &'a mut Ferese,
geometry: Vec<(WindowId, WindowGeometry)>,
slides: HashMap<OutputId, WorkspaceSlide>,
closing: HashMap<WindowId, ClosingAnimation>,
focus: HashMap<WindowId, crate::dimming::DimAnimation>,
dimming: HashMap<WindowId, crate::dimming::DimAnimation>,
overview: Option<crate::overview::OverviewAnimationSnapshot>,
}
impl std::ops::Deref for RenderForecast<'_> {
type Target = Ferese;
fn deref(&self) -> &Ferese {
self.state
}
}
impl std::ops::DerefMut for RenderForecast<'_> {
fn deref_mut(&mut self) -> &mut Ferese {
self.state
}
}
impl Drop for RenderForecast<'_> {
fn drop(&mut self) {
if self.overview.is_none() {
return;
}
for (id, geometry) in self.geometry.drain(..) {
self.state.window_geometry.insert(id, geometry);
}
self.state.workspace_slides = std::mem::take(&mut self.slides);
self.state.closing_windows = std::mem::take(&mut self.closing);
self.state.window_focus = std::mem::take(&mut self.focus);
self.state.window_dimming = std::mem::take(&mut self.dimming);
if let Some(overview) = self.overview.take() {
self.state.overview.restore_prediction(overview);
}
}
}
impl Ferese {
pub(crate) fn output_has_animations(&self, output: &Output) -> bool {
if self.overview.is_animating(self.spring_config) || !self.dismissing_popups.is_empty() {
return true;
}
if self
.output_id(output)
.and_then(|id| self.workspace_slides.get(&id))
.is_some_and(|slide| slide.held_progress.is_none() && slide.elapsed < WORKSPACE_SLIDE_DURATION)
{
return true;
}
self.window_geometry.iter().any(|(id, geometry)| {
if !self.window_belongs_to_output(*id, output) {
return false;
}
if self.resize_transactions.contains_key(id)
|| self.resize_snapshots.contains_key(id)
|| self.closing_windows.get(id).is_some_and(|close| !close.close_sent)
|| self.window_focus.get(id).is_some_and(|focus| focus.is_animating())
|| self.window_dimming.get(id).is_some_and(|dim| dim.is_animating())
{
return true;
}
let mut geometry = *geometry;
if geometry.advance(Duration::ZERO, self.spring_config, self.animations_enabled) {
return true;
}
let Some(workspace) = self.workspaces.workspace_for_window(*id) else {
return false;
};
self.viewport_animations.get(&workspace).is_some_and(|viewport| {
let mut viewport = *viewport;
let held = self
.focus_swipe
.as_ref()
.is_some_and(|swipe| swipe.workspace == workspace);
!held && viewport.advance(Duration::ZERO, self.viewport_spring_config)
}) || self.viewport_coupled_widths.get(id).is_some_and(|(_, width)| {
let mut width = *width;
width.advance(Duration::ZERO, self.viewport_spring_config)
})
})
}
pub(crate) fn forecast_render(&mut self, output: &Output, horizon: Duration) -> RenderForecast<'_> {
if horizon.is_zero() || !self.animations_enabled || !self.output_has_animations(output) {
return RenderForecast {
state: self,
geometry: Vec::new(),
slides: HashMap::new(),
closing: HashMap::new(),
focus: HashMap::new(),
dimming: HashMap::new(),
overview: None,
};
}
let delta = if self.animations_enabled {
horizon.min(Duration::from_millis(100)).mul_f64(self.animation_speed)
} else {
Duration::ZERO
};
let geometry = self
.window_geometry
.iter()
.filter(|(id, _)| self.window_belongs_to_output(**id, output))
.map(|(id, geometry)| (*id, *geometry))
.collect::<Vec<_>>();
let slides = self.workspace_slides.clone();
let closing = self.closing_windows.clone();
let focus = self.window_focus.clone();
let dimming = self.window_dimming.clone();
let overview = self.overview.predict(delta, self.spring_config);
let blocked = self
.resize_transactions
.keys()
.filter_map(|id| self.workspaces.workspace_for_window(*id))
.collect::<HashSet<_>>();
for (id, original) in &geometry {
let workspace = self.workspaces.workspace_for_window(*id);
if workspace.is_some_and(|workspace| blocked.contains(&workspace)) {
continue;
}
let world = self.scrolling_world_x.get(id).and_then(|(workspace, world)| {
let viewport = self.viewport_animations.get(workspace)?;
let held = self
.focus_swipe
.as_ref()
.is_some_and(|swipe| swipe.workspace == *workspace);
Some((*world, *viewport, held))
});
let width = self.viewport_coupled_widths.get(id).map(|(_, width)| *width);
let predicted = predict_geometry(
*original,
delta,
self.spring_config,
self.viewport_spring_config,
world,
width,
);
self.window_geometry.insert(*id, predicted);
}
for slide in self
.workspace_slides
.values_mut()
.filter(|slide| slide.held_progress.is_none())
{
slide.advance(delta);
}
for close in self.closing_windows.values_mut() {
close.advance(delta, self.animations_enabled);
}
for focus in self.window_focus.values_mut() {
focus.predict(delta, self.inactive_dim.duration_ms);
}
for dim in self.window_dimming.values_mut() {
dim.predict(delta, self.inactive_dim.duration_ms);
}
RenderForecast {
state: self,
geometry,
slides,
closing,
focus,
dimming,
overview: Some(overview),
}
}
}
fn predict_geometry(
original: WindowGeometry,
delta: Duration,
spring: SpringConfig,
viewport_spring: SpringConfig,
world: Option<(AnimatedValue, AnimatedValue, bool)>,
width: Option<AnimatedValue>,
) -> WindowGeometry {
let mut predicted = original;
let zooming = predicted.is_zooming();
if width.is_some() {
predicted.visual.target.width = predicted.visual.current.width;
predicted.visual.velocity.width = 0.0;
}
predicted.advance(delta, spring, true);
predicted.visual.target.width = original.visual.target.width;
if let Some((mut world, mut viewport, held)) = world
&& !zooming
{
world.advance(delta, spring);
if !held {
viewport.advance(delta, viewport_spring);
}
predicted.visual.current.x = world.current - viewport.current;
predicted.visual.velocity.x = world.velocity - viewport.velocity;
}
if let Some(mut width) = width {
width.advance(delta, viewport_spring);
predicted.visual.current.width = width.current;
predicted.visual.velocity.width = width.velocity;
}
predicted
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
fn render_forecast_restores_compositor_state_between_output_deadlines() {
let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap());
assert!(
runtime.starts_with(std::env::temp_dir()),
"use a disposable runtime directory"
);
let mut event_loop = EventLoop::try_new().unwrap();
let display = Display::new().unwrap();
let config = crate::config::Config::default().runtime_config().unwrap();
let mut state = Ferese::new(&mut event_loop, display, config).unwrap();
let output = Output::new(
"forecast-test".into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(smithay::output::Mode {
size: (1920, 1080).into(),
refresh: 60_000,
}),
Some(smithay::utils::Transform::Normal),
None,
Some((0, 0).into()),
);
state.space.map_output(&output, (0, 0));
state.register_output(&output, "forecast-test".into());
let id = WindowId(1);
state.workspaces.insert_window(id, Axis::Horizontal, 0.5).unwrap();
let mut geometry = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None);
geometry.visual.set_target(Rect::new(600., 0., 400., 300.));
state.window_geometry.insert(id, geometry);
let tick = state.last_animation_tick;
for horizon in [
Duration::from_millis(16),
Duration::from_millis(4),
Duration::from_millis(8),
] {
{
let forecast = state.forecast_render(&output, horizon);
assert!(forecast.window_geometry[&id].visual.current.x > geometry.visual.current.x);
}
assert_eq!(state.window_geometry[&id], geometry);
assert_eq!(state.last_animation_tick, tick);
}
state.animations_enabled = false;
let forecast = state.forecast_render(&output, Duration::from_millis(16));
assert_eq!(forecast.window_geometry[&id], geometry);
}
#[test]
fn mixed_refresh_forecasts_do_not_change_authoritative_geometry_or_client_state() {
let mut geometry = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None);
geometry.visual.set_target(Rect::new(600., 0., 500., 300.));
let original = geometry;
let sample = |delta| {
predict_geometry(
geometry,
delta,
SpringConfig::default(),
SpringConfig::default(),
None,
None,
)
};
let fast = sample(Duration::from_nanos(1_000_000_000 / 240));
let slow = sample(Duration::from_nanos(1_000_000_000 / 60));
assert!(fast.visual.current.x > 0.);
assert!(slow.visual.current.x > fast.visual.current.x);
assert_eq!(sample(Duration::from_nanos(1_000_000_000 / 240)), fast);
assert_eq!(geometry, original);
assert_eq!(slow.client, original.client);
assert_eq!(slow.logical, original.logical);
}
#[test]
fn scrolling_coordinates_and_coupled_width_use_the_same_forecast_time() {
let geometry = WindowGeometry::new(Rect::new(500., 0., 400., 300.), None);
let mut world = AnimatedValue::new(500.);
world.set_target(600.);
let mut viewport = AnimatedValue::new(0.);
viewport.set_target(200.);
let mut width = AnimatedValue::new(400.);
width.set_target(500.);
let spring = SpringConfig::default();
let delta = Duration::from_millis(10);
let predicted = predict_geometry(
geometry,
delta,
spring,
spring,
Some((world, viewport, false)),
Some(width),
);
world.advance(delta, spring);
viewport.advance(delta, spring);
width.advance(delta, spring);
assert_eq!(predicted.visual.current.x, world.current - viewport.current);
assert_eq!(predicted.visual.current.width, width.current);
assert_eq!(predicted.visual.target, geometry.visual.target);
}
}