Schedule DRM frames against per-output presentation deadlines
This commit is contained in:
1 parent
9666e4ff6c
commit
a40b899b8c
10 files changed
+1168
-129
No files matched your search
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ mod daemon;
|
||||
mod dimming;
|
||||
mod effects;
|
||||
mod floating;
|
||||
mod frame_scheduler;
|
||||
mod gestures;
|
||||
mod grabs;
|
||||
mod handlers;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user