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}