Skip to main content

drmkit_cursor/
pingpong.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Which cursor buffer is on screen, and which one is safe to draw into.
5//!
6//! A cursor is redrawn while the display is scanning it out — every animation
7//! frame, every shape change. Rewriting the buffer the hardware is reading
8//! tears for about a vblank between the draw and the commit taking effect.
9//! i915 happens to mask that in its cursor pipeline; nothing says it has to.
10//!
11//! So there are two buffers and this decides which is which. It holds no
12//! pixels and no descriptors — just the indices and one flag — which is what
13//! makes the rule testable on its own.
14
15/// Which of two cursor buffers is live.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct PingPong {
18    /// The buffer scanout is reading.
19    active: usize,
20    /// Whether the other one holds pixels that have not reached the screen.
21    drawn: bool,
22    /// Whether there is only one buffer after all.
23    single: bool,
24}
25
26impl PingPong {
27    /// A fresh pair, buffer 0 live.
28    #[must_use]
29    pub const fn new() -> Self {
30        Self {
31            active: 0,
32            drawn: false,
33            single: false,
34        }
35    }
36
37    /// A single buffer, for the legacy path.
38    ///
39    /// `drmModeSetCursor` uploads the pixels when the cursor is installed and
40    /// would need reinstalling for a content change anyway, so a second buffer
41    /// buys nothing there.
42    #[must_use]
43    pub const fn single() -> Self {
44        Self {
45            active: 0,
46            drawn: false,
47            single: true,
48        }
49    }
50
51    /// The buffer scanout is reading.
52    #[must_use]
53    pub const fn active(self) -> usize {
54        self.active
55    }
56
57    /// The buffer to draw the next frame into.
58    ///
59    /// The one that is not on screen, so drawing cannot tear. With a single
60    /// buffer there is no choice, and the tear is accepted — the legacy path
61    /// rejects rotation and reinstalls on content change, so the window is
62    /// narrow.
63    #[must_use]
64    pub const fn draw_target(self) -> usize {
65        if self.single {
66            self.active
67        } else {
68            self.active ^ 1
69        }
70    }
71
72    /// The buffer the next commit should publish.
73    ///
74    /// Whatever was just drawn, if anything was. A commit that only moves the
75    /// cursor re-publishes the buffer already on screen rather than flipping
76    /// to a stale one.
77    #[must_use]
78    pub const fn commit_target(self) -> usize {
79        if self.drawn && !self.single {
80            self.active ^ 1
81        } else {
82            self.active
83        }
84    }
85
86    /// Note that a frame has been drawn into [`PingPong::draw_target`].
87    pub const fn mark_drawn(&mut self) {
88        if !self.single {
89            self.drawn = true;
90        }
91    }
92
93    /// Whether a draw is waiting to reach the screen.
94    #[must_use]
95    pub const fn has_undrawn(self) -> bool {
96        self.drawn
97    }
98
99    /// Note that a commit published [`PingPong::commit_target`].
100    ///
101    /// The buffer just published becomes the live one, so the next draw goes
102    /// to the one now safely off screen.
103    ///
104    /// Call this on a commit that *landed*. Where the caller owns the commit
105    /// there is no way to know, and calling it optimistically is the right
106    /// trade: if their commit failed, scanout is still reading the buffer this
107    /// just marked as the draw target, and the next draw there can tear —
108    /// which is exactly what a single buffer does all the time. A successful
109    /// commit, the overwhelmingly common case, is tear-free.
110    pub const fn published(&mut self) {
111        if self.drawn {
112            self.active ^= 1;
113            self.drawn = false;
114        }
115    }
116}
117
118impl Default for PingPong {
119    fn default() -> Self {
120        Self::new()
121    }
122}