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 /// The legacy ioctls take one buffer handle and have no flip to alternate
40 /// on, so a second buffer buys nothing there.
41 #[must_use]
42 pub const fn single() -> Self {
43 Self {
44 active: 0,
45 drawn: false,
46 single: true,
47 }
48 }
49
50 /// The buffer scanout is reading.
51 #[must_use]
52 pub const fn active(self) -> usize {
53 self.active
54 }
55
56 /// The buffer to draw the next frame into.
57 ///
58 /// The one that is not on screen, so drawing cannot tear. With a single
59 /// buffer there is no choice, and the tear is accepted: the legacy path
60 /// redraws the buffer the CRTC is showing, which only a new shape or an
61 /// animation frame does.
62 #[must_use]
63 pub const fn draw_target(self) -> usize {
64 if self.single {
65 self.active
66 } else {
67 self.active ^ 1
68 }
69 }
70
71 /// The buffer the next commit should publish.
72 ///
73 /// Whatever was just drawn, if anything was. A commit that only moves the
74 /// cursor re-publishes the buffer already on screen rather than flipping
75 /// to a stale one.
76 #[must_use]
77 pub const fn commit_target(self) -> usize {
78 if self.drawn && !self.single {
79 self.active ^ 1
80 } else {
81 self.active
82 }
83 }
84
85 /// Note that a frame has been drawn into [`PingPong::draw_target`].
86 pub const fn mark_drawn(&mut self) {
87 if !self.single {
88 self.drawn = true;
89 }
90 }
91
92 /// Whether a draw is waiting to reach the screen.
93 #[must_use]
94 pub const fn has_undrawn(self) -> bool {
95 self.drawn
96 }
97
98 /// Note that a commit published [`PingPong::commit_target`].
99 ///
100 /// The buffer just published becomes the live one, so the next draw goes
101 /// to the one now safely off screen.
102 ///
103 /// Call this on a commit that *landed*. Where the caller owns the commit
104 /// there is no way to know, and calling it optimistically is the right
105 /// trade: if their commit failed, scanout is still reading the buffer this
106 /// just marked as the draw target, and the next draw there can tear —
107 /// which is exactly what a single buffer does all the time. A successful
108 /// commit, the overwhelmingly common case, is tear-free.
109 pub const fn published(&mut self) {
110 if self.drawn {
111 self.active ^= 1;
112 self.drawn = false;
113 }
114 }
115}
116
117impl Default for PingPong {
118 fn default() -> Self {
119 Self::new()
120 }
121}