Commit a1f375a8b4be
tarball8 files changed+258-0
@@ -3,6 +3,7 @@
33
44
use crate::drm::Drm;55
use crate::nested::Host;6
+use crate::timing::Timeline;67
use monolith_layout::{OutputId, Rect};78
use smithay::backend::drm::DrmNode;89
use smithay::output::Output;@@ -54,6 +55,15 @@ impl Backend {
5455
}5556
}5657
58
+ /// When `output` presents, if that is known yet.59
+ pub fn timeline(&self, output: &Output) -> Option<Timeline> {60
+ match self {61
+ Backend::Drm(d) => d.timeline(output),62
+ Backend::Nested(h) => (h.output == *output).then(|| h.timeline()).flatten(),63
+ Backend::Starting => None,64
+ }65
+ }66
+5767
/// The node clients allocate dmabufs on, known on bare metal.5868
pub fn render_node(&self) -> Option<DrmNode> {5969
self.drm().map(|d| d.render_node)@@ -26,6 +26,7 @@ impl CompositorHandler for State {
2626
2727
fn new_surface(&mut self, surface: &WlSurface) {2828
add_pre_commit_hook::<Self, _>(surface, |state, _dh, surface| {29
+ crate::timing::watch(state, surface);2930
hold_until_readable(state, surface);3031
});3132
}@@ -678,6 +678,15 @@ impl Drm {
678678
.find(|s| s.id == id)679679
.map(|s| s.output.clone())680680
}681
+682
+ pub fn timeline(&self, output: &Output) -> Option<crate::timing::Timeline> {683
+ let s = self.surfaces.values().find(|s| s.output == *output)?;684
+ Some(crate::timing::Timeline {685
+ last: s.last_vblank_ns?,686
+ refresh: if s.vrr_on { 0 } else { s.refresh_ns },687
+ budget: s.budget.ns(),688
+ })689
+ }681690
}682691
683692
fn on_vblank(state: &mut State, crtc: crtc::Handle, meta: &mut Option<DrmEventMetadata>) {@@ -10,6 +10,7 @@ use smithay::backend::renderer::ImportDma;
1010
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::WmCapabilities;1111
use smithay::reexports::wayland_server::DisplayHandle;1212
use smithay::utils::{ClockSource, Monotonic};13
+use smithay::wayland::commit_timing::CommitTimingManagerState;1314
use smithay::wayland::compositor::CompositorState;1415
use smithay::wayland::cursor_shape::CursorShapeManagerState;1516
use smithay::wayland::dmabuf::{DmabufFeedbackBuilder, DmabufGlobal, DmabufState};@@ -78,6 +79,7 @@ pub struct Protocols {
7879
_relative_pointer: RelativePointerManagerState,7980
_pointer_constraints: PointerConstraintsState,8081
_fifo: FifoManagerState,82
+ _commit_timing: CommitTimingManagerState,8183
}8284
8385
impl Protocols {@@ -146,6 +148,7 @@ impl Protocols {
146148
_relative_pointer: RelativePointerManagerState::new::<State>(dh),147149
_pointer_constraints: PointerConstraintsState::new::<State>(dh),148150
_fifo: FifoManagerState::new::<State>(dh),151
+ _commit_timing: CommitTimingManagerState::new::<State>(dh),149152
}150153
}151154
}@@ -32,6 +32,7 @@ mod seat;
3232
mod signals;3333
mod state;3434
mod stats;35
+mod timing;3536
mod toplevels;3637
mod windows;3738
mod wm;@@ -59,6 +59,7 @@ pub struct Host {
5959
/// The host clock is CLOCK_MONOTONIC, so its timestamps share our timeline.6060
monotonic: bool,6161
last_present_ns: Option<u64>,62
+ last_frame_ns: Option<u64>,6263
refresh_ns: u64,6364
feedback: Vec<(u64, OutputPresentationFeedback)>,6465
next_feedback: u64,@@ -76,6 +77,17 @@ impl Host {
7677
None => now,7778
}7879
}80
+81
+ pub fn timeline(&self) -> Option<crate::timing::Timeline> {82
+ Some(crate::timing::Timeline {83
+ last: self84
+ .last_present_ns85
+ .filter(|_| self.monotonic)86
+ .or(self.last_frame_ns)?,87
+ refresh: self.refresh_ns,88
+ budget: 0,89
+ })90
+ }7991
}8092
8193
/// Destroys the host-side buffer when its swapchain slot goes away.@@ -163,6 +175,7 @@ pub fn init(
163175
presentation,164176
monotonic: false,165177
last_present_ns: None,178
+ last_frame_ns: None,166179
refresh_ns: 16_666_667,167180
feedback: Vec::new(),168181
next_feedback: 0,@@ -556,6 +569,7 @@ impl Dispatch<wl_callback::WlCallback, ()> for State {
556569
Kind::empty(),557570
);558571
}572
+ host.last_frame_ns = Some(crate::state::now_ns());559573
let output = host.output.clone();560574
crate::fifo::presented(state, &output);561575
}@@ -101,6 +101,7 @@ pub struct Services {
101101
pub power: crate::power::Power,102102
pub commits: crate::stats::CommitStats,103103
pub fifo: crate::fifo::Fifo,104
+ pub timing: crate::timing::Timing,104105
}105106
106107
/// The config and what is derived from it, swapped whole on reload.@@ -222,6 +223,7 @@ impl State {
222223
power: Default::default(),223224
commits: Default::default(),224225
fifo: Default::default(),226
+ timing: Default::default(),225227
},226228
})227229
}@@ -0,0 +1,218 @@
1
+//! wp_commit_timing_v1 timestamps. A commit holds until the first frame2
+//! that can still be rendered and presents at or after its timestamp.3
+4
+use crate::pacing;5
+use crate::state::{State, now_ns};6
+use smithay::output::Output;7
+use smithay::reexports::calloop::RegistrationToken;8
+use smithay::reexports::calloop::timer::{TimeoutAction, Timer};9
+use smithay::reexports::rustix::time::Timespec;10
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;11
+use smithay::reexports::wayland_server::{Resource, Weak};12
+use smithay::utils::{Monotonic, Time};13
+use smithay::wayland::commit_timing::{14
+ CommitTimerBarrierStateUserData, CommitTimerStateUserData, Timestamp,15
+};16
+use smithay::wayland::compositor::with_states;17
+use std::time::Duration;18
+19
+/// A timestamp may lie this fraction of a refresh past a vblank and still20
+/// count for it. Clients aim at the vblank they computed, which our own21
+/// estimate misses by microseconds.22
+const SLACK_DIVISOR: u64 = 8;23
+/// Past the start of the previous frame's render, so the wake never beats24
+/// that frame's own timer to the commit.25
+const AFTER_START_NS: u64 = 1_000_000;26
+/// Timers for timestamps further out than this run again to recompute.27
+const LONGEST_WAIT: Duration = Duration::from_secs(10);28
+29
+/// What a backend knows of one output's vblank timeline.30
+#[derive(Clone, Copy)]31
+pub struct Timeline {32
+ pub last: u64,33
+ /// Zero when vblanks are not on a fixed grid, as under VRR.34
+ pub refresh: u64,35
+ pub budget: u64,36
+}37
+38
+#[derive(Default)]39
+pub struct Timing {40
+ waiting: Vec<Weak<WlSurface>>,41
+ timer: Option<RegistrationToken>,42
+}43
+44
+/// Notes a surface about to commit a timestamp, ahead of the smithay hook45
+/// that turns it into a blocker.46
+pub fn watch(state: &mut State, surface: &WlSurface) {47
+ let stamped = with_states(surface, |s| {48
+ s.data_map49
+ .get::<CommitTimerStateUserData>()50
+ .is_some_and(|t| t.borrow().timestamp.is_some())51
+ });52
+ if !stamped {53
+ return;54
+ }55
+ let waiting = &mut state.services.timing.waiting;56
+ if !waiting.iter().any(|w| w == surface) {57
+ waiting.push(surface.downgrade());58
+ }59
+ wake(state, Duration::ZERO);60
+}61
+62
+fn wake(state: &mut State, after: Duration) {63
+ if let Some(token) = state.services.timing.timer.take() {64
+ state.core.handle.remove(token);65
+ }66
+ let token = state67
+ .core68
+ .handle69
+ .insert_source(Timer::from_duration(after), |_, _, state| {70
+ state.services.timing.timer = None;71
+ run(state);72
+ TimeoutAction::Drop73
+ });74
+ match token {75
+ Ok(token) => state.services.timing.timer = Some(token),76
+ Err(e) => tracing::warn!("could not arm the commit timing timer: {e}"),77
+ }78
+}79
+80
+fn nanos(t: Timestamp) -> u64 {81
+ let t = Timespec::from(Time::<Monotonic>::from(t));82
+ let secs = u64::try_from(t.tv_sec).unwrap_or(0);83
+ let sub = u64::try_from(t.tv_nsec).unwrap_or(0).min(999_999_999);84
+ secs.saturating_mul(1_000_000_000).saturating_add(sub)85
+}86
+87
+fn stamp(ns: u64) -> Timestamp {88
+ Time::<Monotonic>::from(Timespec {89
+ tv_sec: (ns / 1_000_000_000) as _,90
+ tv_nsec: (ns % 1_000_000_000) as _,91
+ })92
+ .into()93
+}94
+95
+fn output_of(state: &State, surface: &WlSurface) -> Option<Output> {96
+ let root = crate::idle::root_surface(surface);97
+ match state.wm.id_of(&root).and_then(|id| state.wm.window(id)) {98
+ Some(window) => state99
+ .wm100
+ .space101
+ .outputs_for_element(window)102
+ .into_iter()103
+ .next(),104
+ None => state.wm.space.outputs().next().cloned(),105
+ }106
+}107
+108
+fn run(state: &mut State) {109
+ let now = now_ns();110
+ let mut clients = Vec::new();111
+ let mut next: Option<u64> = None;112
+ let mut still = Vec::new();113
+ for weak in std::mem::take(&mut state.services.timing.waiting) {114
+ let Ok(surface) = weak.upgrade() else {115
+ continue;116
+ };117
+ let timeline = output_of(state, &surface).and_then(|o| state.backend.timeline(&o));118
+ let pending = with_states(&surface, |s| {119
+ let mut barriers = s120
+ .data_map121
+ .get::<CommitTimerBarrierStateUserData>()?122
+ .lock()123
+ .unwrap();124
+ let mut signaled = false;125
+ let mut wake = None;126
+ while let Some(t) = barriers.next_deadline() {127
+ match release(timeline, nanos(t), now) {128
+ Release::Now(covers) => signaled |= barriers.signal_until(stamp(covers)),129
+ Release::At(at) => {130
+ wake = Some(at);131
+ break;132
+ }133
+ }134
+ }135
+ if signaled {136
+ clients.extend(surface.client());137
+ }138
+ Some(wake)139
+ });140
+ if let Some(Some(at)) = pending {141
+ next = Some(next.map_or(at, |n| n.min(at)));142
+ still.push(weak);143
+ }144
+ }145
+ state.services.timing.waiting.extend(still);146
+ crate::fifo::unblock(state, clients);147
+ if let Some(at) = next {148
+ let after = Duration::from_nanos(at.saturating_sub(now_ns()).max(1_000_000));149
+ wake(state, after.min(LONGEST_WAIT));150
+ }151
+}152
+153
+enum Release {154
+ /// The commit may apply now, and so may every one stamped up to this time.155
+ Now(u64),156
+ /// The commit must wait until this time.157
+ At(u64),158
+}159
+160
+fn release(timeline: Option<Timeline>, target: u64, now: u64) -> Release {161
+ let Some(tl) = timeline else {162
+ return Release::Now(u64::MAX);163
+ };164
+ if tl.refresh == 0 {165
+ let at = target.saturating_sub(tl.budget);166
+ return if at <= now {167
+ Release::Now(target)168
+ } else {169
+ Release::At(at)170
+ };171
+ }172
+ let slack = tl.refresh / SLACK_DIVISOR;173
+ let Some((reach, _)) = pacing::deadline(tl.last, tl.refresh, tl.budget, now) else {174
+ return Release::Now(u64::MAX);175
+ };176
+ let aimed = target.saturating_sub(slack);177
+ if aimed <= reach {178
+ return Release::Now(reach + slack);179
+ }180
+ let first = (aimed - tl.last).div_ceil(tl.refresh);181
+ let before = tl182
+ .last183
+ .saturating_add((first - 1).saturating_mul(tl.refresh));184
+ Release::At(before.saturating_sub(tl.budget) + AFTER_START_NS)185
+}186
+187
+#[cfg(test)]188
+mod tests {189
+ use super::*;190
+191
+ const TL: Timeline = Timeline {192
+ last: 1_000_000_000,193
+ refresh: 10_000_000,194
+ budget: 2_000_000,195
+ };196
+197
+ #[test]198
+ fn a_timestamp_waits_for_the_frame_after_the_one_being_drawn() {199
+ let now = TL.last + 3_000_000;200
+ let target = TL.last + 30_000_000;201
+ let Release::At(at) = release(Some(TL), target, now) else {202
+ panic!("a timestamp three frames out must wait");203
+ };204
+ assert_eq!(at, TL.last + 20_000_000 - TL.budget + AFTER_START_NS);205
+ let Release::Now(covers) = release(Some(TL), target, at) else {206
+ panic!("the wake must release it");207
+ };208
+ assert!(covers >= target);209
+ }210
+211
+ #[test]212
+ fn a_timestamp_a_hair_past_a_vblank_still_takes_it() {213
+ let now = TL.last + 9_000_000;214
+ let target = TL.last + 20_000_000 + TL.refresh / SLACK_DIVISOR;215
+ assert!(matches!(release(Some(TL), target, now), Release::Now(_)));216
+ assert!(matches!(release(Some(TL), target + 1, now), Release::At(_)));217
+ }218
+}