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    /// 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}