Skip to main content

drmkit_present/
frame_economy.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Deciding whether this vblank is worth a commit at all.
5
6/// What the present loop should do with this frame.
7///
8/// One enum rather than the reference's `{ FrameAction action; bool full; }`,
9/// where `full` is meaningless unless `action` is `Commit` and is set to
10/// `false` on a skip — a value a caller can read and act on. Three variants
11/// make the nonsensical combination unrepresentable, and leave the caller one
12/// thing to match.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum FrameAction {
16    /// Nothing changed. Issue no commit: no page flip, no scanout reprogram.
17    Skip,
18    /// Commit the whole frame, omitting `FB_DAMAGE_CLIPS`.
19    CommitFull,
20    /// Commit with the producer's damage rectangles attached.
21    CommitDamaged,
22}
23
24impl FrameAction {
25    /// Whether this action puts a commit on the wire.
26    #[must_use]
27    pub const fn commits(self) -> bool {
28        !matches!(self, Self::Skip)
29    }
30}
31
32/// The per-frame present decision, and what it has decided so far.
33///
34/// # The point of it
35///
36/// The win is the skip. When nothing has changed since the last commit, the
37/// present loop issues no atomic commit at all — no flip, no reprogram. That
38/// is power saved on every panel, and on a panel capable of self-refresh it is
39/// what lets the panel stay there.
40///
41/// So the skip is **unconditional** and consults no PSR capability. Not
42/// flipping is a win whether or not the panel can self-refresh, and gating it
43/// on a capability would mean burning power on the panels that advertise
44/// nothing — which is most of them.
45///
46/// # Threading
47///
48/// Not synchronized: one present loop owns it, the same one that owns the
49/// [`BufferRing`](crate::BufferRing).
50#[derive(Debug)]
51pub struct FrameEconomy {
52    first: bool,
53    force_full: bool,
54    committed: u64,
55    skipped: u64,
56}
57
58impl Default for FrameEconomy {
59    /// The same as [`new`](Self::new): the first decision commits in full.
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl FrameEconomy {
66    /// A fresh economy, whose first decision is always a full commit.
67    #[must_use]
68    pub const fn new() -> Self {
69        Self {
70            first: true,
71            force_full: false,
72            committed: 0,
73            skipped: 0,
74        }
75    }
76
77    /// Decide this frame.
78    ///
79    /// `content_changed` is whether a layer produced a new frame or the scene
80    /// mutated since the last committed one. `damage_available` is whether the
81    /// producer supplied a bounded damage rectangle set, so a damaged commit is
82    /// possible at all.
83    ///
84    /// The first decision after construction or [`force_full`](Self::force_full)
85    /// is always [`FrameAction::CommitFull`]: the scanout buffer's contents are
86    /// otherwise undefined, or a modeset has just discarded them.
87    pub const fn decide(&mut self, content_changed: bool, damage_available: bool) -> FrameAction {
88        if self.first || self.force_full {
89            self.first = false;
90            self.force_full = false;
91            self.committed += 1;
92            return FrameAction::CommitFull;
93        }
94        if !content_changed {
95            self.skipped += 1;
96            return FrameAction::Skip;
97        }
98        self.committed += 1;
99        if damage_available {
100            FrameAction::CommitDamaged
101        } else {
102            FrameAction::CommitFull
103        }
104    }
105
106    /// Make the next decision a full commit.
107    ///
108    /// For after a modeset, a mode change or a rebind — anything that leaves
109    /// the previous scanout contents no longer applying, so damage against them
110    /// would repaint the wrong difference. One-shot: it applies to the next
111    /// decision and then clears.
112    pub const fn force_full(&mut self) {
113        self.force_full = true;
114    }
115
116    /// How many frames have been committed.
117    #[must_use]
118    pub const fn committed(&self) -> u64 {
119        self.committed
120    }
121
122    /// How many frames have been skipped — the ones that cost nothing.
123    #[must_use]
124    pub const fn skipped(&self) -> u64 {
125        self.skipped
126    }
127}