smithay/wayland/commit_timing/
mod.rs

1//! Utilities for handling the `wp-commit-timing` protocol
2//!
3//! ## How to use it
4//!
5//! ### Initialization
6//!
7//! To initialize this implementation create the [`CommitTimingManagerState`] and store it inside your `State` struct.
8//!
9//! ```
10//! use smithay::delegate_commit_timing;
11//! use smithay::wayland::compositor;
12//! use smithay::wayland::commit_timing::CommitTimingManagerState;
13//!
14//! # struct State { commit_timing_manager_state: CommitTimingManagerState }
15//! # let mut display = wayland_server::Display::<State>::new().unwrap();
16//! // Create the compositor state
17//! let commit_timing_manager_state = CommitTimingManagerState::new::<State>(
18//!     &display.handle(),
19//! );
20//!
21//! // insert the CommitTimingManagerState into your state
22//! // ..
23//!
24//! delegate_commit_timing!(State);
25//!
26//! // You're now ready to go!
27//! ```
28//!
29//! ### Use the commit timer state
30//!
31//! Whenever the client commits a surface content update containing a commit timer timestamp set
32//! through [`wp_commit_timer_v1::Request::SetTimestamp`] the implementation will place a [`Blocker`](crate::wayland::compositor::Blocker)
33//! on the surface and register it in the [`CommitTimerBarrierState`]. It is your responsibility to query for pending commit timers
34//! and signal them to allow client to make forward progress.
35//!
36//! You can query the pending commit timers and signal them as shown in the following example:
37//!
38//! ```no_run
39//! # use wayland_server::{backend::ObjectId, protocol::wl_surface, Resource};
40//! use smithay::wayland::compositor;
41//! use smithay::wayland::commit_timing::CommitTimerBarrierStateUserData;
42//! # use smithay::wayland::commit_timing::Timestamp;
43//! # struct State;
44//! # let mut display = wayland_server::Display::<State>::new().unwrap();
45//! # let dh = display.handle();
46//! # let surface = wl_surface::WlSurface::from_id(&dh, ObjectId::null()).unwrap();
47//! # let frame_target: Timestamp = todo!();
48//! compositor::with_states(&surface, |states| {
49//!     if let Some(mut commit_timer_state) = states
50//!         .data_map
51//!         .get::<CommitTimerBarrierStateUserData>()
52//!         .map(|commit_timer| commit_timer.lock().unwrap())
53//!         {
54//!             if commit_timer_state.signal_until(frame_target) {
55//!                 // ..signal blocker cleared
56//!             }
57//!         }
58//! });
59//! ```
60//!
61//! ### Unmanaged mode
62//!
63//! If for some reason the integrated solution for commit timers does not suit your needs
64//! you can create an unmanaged version with [`CommitTimingManagerState::unmanaged`].
65//! In this case it is your responsibility to listen for surface commits and place blockers
66//! accordingly to the spec.
67//!
68//! The commit timer timestamp can be retrieved like shown in the following example:
69//!
70//! ```no_run
71//! # let surface: wayland_server::protocol::wl_surface::WlSurface = todo!();
72//! use smithay::wayland::compositor;
73//! use smithay::wayland::commit_timing::CommitTimerStateUserData;
74//!
75//! let timestamp = compositor::with_states(&surface, |states| {
76//!     states
77//!         .data_map
78//!         .get::<CommitTimerStateUserData>()
79//!         .and_then(|state| state.borrow_mut().timestamp.take())
80//! });
81//! ```
82use std::{cell::RefCell, collections::BinaryHeap, sync::Mutex};
83
84use rustix::fs::Timespec;
85use wayland_protocols::wp::commit_timing::v1::server::{
86    wp_commit_timer_v1::{self, WpCommitTimerV1},
87    wp_commit_timing_manager_v1::{self, WpCommitTimingManagerV1},
88};
89use wayland_server::{
90    backend::GlobalId, protocol::wl_surface::WlSurface, DataInit, Dispatch, DisplayHandle, GlobalDispatch,
91    New, Resource, Weak,
92};
93
94use crate::{
95    utils::Time,
96    wayland::compositor::{add_blocker, add_pre_commit_hook},
97};
98
99use super::compositor::{with_states, Barrier};
100
101/// State for the [`WpCommitTimingManagerV1`] global
102#[derive(Debug)]
103pub struct CommitTimingManagerState {
104    global: GlobalId,
105    is_managed: bool,
106}
107
108impl CommitTimingManagerState {
109    /// Create a new [`WpCommitTimingManagerV1`] global
110    //
111    /// The id provided by [`CommitTimingManagerState::global`] may be used to
112    /// remove or disable this global in the future.
113    pub fn new<D>(display: &DisplayHandle) -> Self
114    where
115        D: GlobalDispatch<WpCommitTimingManagerV1, bool>,
116        D: Dispatch<WpCommitTimingManagerV1, bool>,
117        D: 'static,
118    {
119        Self::new_internal::<D>(display, true)
120    }
121
122    /// Create a new unmanaged [`WpCommitTimingManagerV1`] global
123    //
124    /// The id provided by [`CommitTimingManagerState::global`] may be used to
125    /// remove or disable this global in the future.
126    pub fn unmanaged<D>(display: &DisplayHandle) -> Self
127    where
128        D: GlobalDispatch<WpCommitTimingManagerV1, bool>,
129        D: Dispatch<WpCommitTimingManagerV1, bool>,
130        D: 'static,
131    {
132        Self::new_internal::<D>(display, false)
133    }
134
135    fn new_internal<D>(display: &DisplayHandle, is_managed: bool) -> Self
136    where
137        D: GlobalDispatch<WpCommitTimingManagerV1, bool>,
138        D: Dispatch<WpCommitTimingManagerV1, bool>,
139        D: 'static,
140    {
141        let global = display.create_global::<D, WpCommitTimingManagerV1, _>(1, is_managed);
142
143        Self { global, is_managed }
144    }
145
146    /// Returns the id of the [`WpCommitTimingManagerV1`] global.
147    pub fn global(&self) -> GlobalId {
148        self.global.clone()
149    }
150
151    /// Returns if this [`CommitTimingManagerState`] operates in managed mode.
152    pub fn is_managed(&self) -> bool {
153        self.is_managed
154    }
155}
156
157impl<D> GlobalDispatch<WpCommitTimingManagerV1, bool, D> for CommitTimingManagerState
158where
159    D: GlobalDispatch<WpCommitTimingManagerV1, bool>,
160    D: Dispatch<WpCommitTimingManagerV1, bool>,
161    D: 'static,
162{
163    fn bind(
164        _state: &mut D,
165        _dh: &DisplayHandle,
166        _client: &wayland_server::Client,
167        resource: New<WpCommitTimingManagerV1>,
168        global_data: &bool,
169        data_init: &mut DataInit<'_, D>,
170    ) {
171        data_init.init(resource, *global_data);
172    }
173}
174
175impl<D> Dispatch<WpCommitTimingManagerV1, bool, D> for CommitTimingManagerState
176where
177    D: Dispatch<WpCommitTimingManagerV1, bool>,
178    D: Dispatch<WpCommitTimerV1, Weak<WlSurface>>,
179    D: 'static,
180{
181    fn request(
182        _state: &mut D,
183        _client: &wayland_server::Client,
184        _resource: &WpCommitTimingManagerV1,
185        request: wp_commit_timing_manager_v1::Request,
186        data: &bool,
187        _dhandle: &DisplayHandle,
188        data_init: &mut DataInit<'_, D>,
189    ) {
190        let is_managed = *data;
191
192        match request {
193            wp_commit_timing_manager_v1::Request::GetTimer { id, surface } => {
194                let (is_initial, has_active_commit_timer) = with_states(&surface, |states| {
195                    let marker = states.data_map.get::<RefCell<CommitTimerMarker>>();
196                    (
197                        marker.is_none(),
198                        marker.map(|m| m.borrow().0.is_some()).unwrap_or(false),
199                    )
200                });
201
202                // The protocol mandates that only a single commit timer object is associated with a surface at all times
203                if has_active_commit_timer {
204                    surface.post_error(
205                        wp_commit_timing_manager_v1::Error::CommitTimerExists,
206                        "the surface has already a commit timer object associated",
207                    );
208                    return;
209                }
210
211                // Make sure we do not install the hook more then once in case the surface is being reused
212                if is_managed && is_initial {
213                    add_pre_commit_hook::<D, _>(&surface, |_, _, surface| {
214                        let timestamp = with_states(surface, |states| {
215                            states
216                                .data_map
217                                .get::<CommitTimerStateUserData>()
218                                .and_then(|state| state.borrow_mut().timestamp.take())
219                        });
220
221                        if let Some(timestamp) = timestamp {
222                            let barrier = with_states(surface, |states| {
223                                let barrier_state = states
224                                    .data_map
225                                    .get_or_insert(CommitTimerBarrierStateUserData::default);
226                                barrier_state.lock().unwrap().register(timestamp)
227                            });
228
229                            add_blocker(surface, barrier);
230                        }
231                    });
232                }
233
234                let commit_timer: WpCommitTimerV1 = data_init.init(id, surface.downgrade());
235
236                with_states(&surface, |states| {
237                    states
238                        .data_map
239                        .get_or_insert(|| RefCell::new(CommitTimerMarker(None)))
240                        .borrow_mut()
241                        .0 = Some(commit_timer);
242                });
243            }
244            wp_commit_timing_manager_v1::Request::Destroy => (),
245            _ => unreachable!(),
246        }
247    }
248}
249
250// Internal marker to track if a commit timer object is currently associated with a surface.
251// Used to realize the `AlreadyExists` protocol check.
252struct CommitTimerMarker(Option<WpCommitTimerV1>);
253
254impl<D> Dispatch<WpCommitTimerV1, Weak<WlSurface>, D> for CommitTimingManagerState
255where
256    D: Dispatch<WpCommitTimerV1, Weak<WlSurface>>,
257    D: 'static,
258{
259    fn request(
260        _state: &mut D,
261        _client: &wayland_server::Client,
262        resource: &WpCommitTimerV1,
263        request: wp_commit_timer_v1::Request,
264        data: &Weak<WlSurface>,
265        _dhandle: &DisplayHandle,
266        _data_init: &mut DataInit<'_, D>,
267    ) {
268        match request {
269            wp_commit_timer_v1::Request::SetTimestamp {
270                tv_sec_hi,
271                tv_sec_lo,
272                tv_nsec,
273            } => {
274                let Ok(surface) = data.upgrade() else {
275                    resource.post_error(
276                        wp_commit_timer_v1::Error::SurfaceDestroyed as u32,
277                        "the surface associated with this commit timer object has been destroyed".to_string(),
278                    );
279                    return;
280                };
281
282                let tv_sec = ((tv_sec_hi as u64) << 32) | tv_sec_lo as u64;
283
284                let timestamp = Timestamp(Timespec {
285                    tv_sec: tv_sec as rustix::time::Secs,
286                    tv_nsec: tv_nsec as rustix::time::Nsecs,
287                });
288
289                let already_has_timestamp = with_states(&surface, move |states| {
290                    let mut commit_timer_state = states
291                        .data_map
292                        .get_or_insert(CommitTimerStateUserData::default)
293                        .borrow_mut();
294
295                    if commit_timer_state.timestamp.is_some() {
296                        return true;
297                    }
298
299                    commit_timer_state.timestamp = Some(timestamp);
300                    false
301                });
302
303                if already_has_timestamp {
304                    resource.post_error(
305                        wp_commit_timer_v1::Error::TimestampExists as u32,
306                        "the surface already has a timestamp associated for this commit".to_string(),
307                    );
308                }
309            }
310            wp_commit_timer_v1::Request::Destroy => {
311                if let Ok(surface) = data.upgrade() {
312                    with_states(&surface, |states| {
313                        states
314                            .data_map
315                            .get::<RefCell<CommitTimerMarker>>()
316                            .unwrap()
317                            .borrow_mut()
318                            .0 = None;
319                    });
320                }
321            }
322            _ => unreachable!(),
323        }
324    }
325}
326
327/// Timestamp set through [`wp_commit_timer_v1::Request::SetTimestamp`] for a surface
328#[derive(Debug, PartialEq, Eq, Copy, Clone)]
329pub struct Timestamp(Timespec);
330
331impl Ord for Timestamp {
332    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
333        match self.0.tv_sec.cmp(&other.0.tv_sec) {
334            std::cmp::Ordering::Equal => self.0.tv_nsec.cmp(&other.0.tv_nsec),
335            other => other,
336        }
337    }
338}
339
340impl PartialOrd for Timestamp {
341    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
342        Some(self.cmp(other))
343    }
344}
345
346/// Per surface [`WpCommitTimerV1`] state stored in the surface user data
347pub type CommitTimerStateUserData = RefCell<CommitTimerState>;
348
349/// Per surface state for [`WpCommitTimerV1`]
350#[derive(Debug, Default)]
351pub struct CommitTimerState {
352    /// The timestamp set through [`wp_commit_timer_v1::Request::SetTimestamp`] for this surface
353    pub timestamp: Option<Timestamp>,
354}
355
356/// Per surface barrier state stored in the surface user data
357pub type CommitTimerBarrierStateUserData = Mutex<CommitTimerBarrierState>;
358
359/// Per surface barrier state using the managed mode
360#[derive(Debug, Default)]
361pub struct CommitTimerBarrierState {
362    barriers: BinaryHeap<CommitTimerBarrier>,
363}
364
365impl<Kind> From<Time<Kind>> for Timestamp {
366    fn from(value: Time<Kind>) -> Self {
367        Self(value.into())
368    }
369}
370
371impl<Kind> From<Timestamp> for Time<Kind> {
372    fn from(value: Timestamp) -> Self {
373        Time::from(value.0)
374    }
375}
376
377impl CommitTimerBarrierState {
378    /// Signal all tracked barriers matching the specified deadline
379    ///
380    /// A barrier is considered to lie within the deadline if the
381    /// timestamp of the barrier is past the specified deadline
382    ///
383    /// Returns `true` if a barrier has been signaled, false otherwise
384    pub fn signal_until(&mut self, deadline: impl Into<Timestamp>) -> bool {
385        let deadline = deadline.into();
386
387        let num_barriers = self.barriers.len();
388        loop {
389            let Some(barrier) = self.barriers.peek() else {
390                break;
391            };
392
393            if barrier.timestamp > deadline {
394                break;
395            }
396
397            barrier.barrier.signal();
398            let _ = self.barriers.pop();
399        }
400
401        num_barriers != self.barriers.len()
402    }
403
404    /// Register a new barrier with the provided timestamp
405    pub fn register(&mut self, timestamp: Timestamp) -> Barrier {
406        let barrier = Barrier::new(false);
407        self.barriers.push(CommitTimerBarrier {
408            timestamp,
409            barrier: barrier.clone(),
410        });
411        barrier
412    }
413
414    /// Retrieve the next deadline when available
415    pub fn next_deadline(&self) -> Option<Timestamp> {
416        self.barriers.peek().map(|barrier| barrier.timestamp)
417    }
418}
419
420#[derive(Debug, Clone, PartialEq, Eq)]
421struct CommitTimerBarrier {
422    timestamp: Timestamp,
423    barrier: Barrier,
424}
425
426impl std::cmp::Ord for CommitTimerBarrier {
427    #[inline]
428    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
429        // earlier values have priority
430        self.timestamp.cmp(&other.timestamp).reverse()
431    }
432}
433
434impl std::cmp::PartialOrd for CommitTimerBarrier {
435    #[inline]
436    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
437        Some(self.cmp(other))
438    }
439}
440
441/// Macro used to delegate [`WpCommitTimingManagerV1`] events
442#[macro_export]
443macro_rules! delegate_commit_timing {
444    ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
445        $crate::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
446            $crate::reexports::wayland_protocols::wp::commit_timing::v1::server::wp_commit_timing_manager_v1::WpCommitTimingManagerV1: bool
447        ] => $crate::wayland::commit_timing::CommitTimingManagerState);
448
449        $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
450            $crate::reexports::wayland_protocols::wp::commit_timing::v1::server::wp_commit_timing_manager_v1::WpCommitTimingManagerV1: bool
451        ] => $crate::wayland::commit_timing::CommitTimingManagerState);
452        $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
453            $crate::reexports::wayland_protocols::wp::commit_timing::v1::server::wp_commit_timer_v1::WpCommitTimerV1: $crate::reexports::wayland_server::Weak<$crate::reexports::wayland_server::protocol::wl_surface::WlSurface>
454        ] => $crate::wayland::commit_timing::CommitTimingManagerState);
455    };
456}