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 synchronised: one present loop owns it, the same one that owns the
49/// [`BufferRing`](crate::BufferRing).
50#[derive(Debug, Default)]
51pub struct FrameEconomy {
52    first: bool,
53    force_full: bool,
54    committed: u64,
55    skipped: u64,
56}
57
58impl FrameEconomy {
59    /// A fresh economy, whose first decision is always a full commit.
60    #[must_use]
61    pub const fn new() -> Self {
62        Self {
63            first: true,
64            force_full: false,
65            committed: 0,
66            skipped: 0,
67        }
68    }
69
70    /// Decide this frame.
71    ///
72    /// `content_changed` is whether a layer produced a new frame or the scene
73    /// mutated since the last committed one. `damage_available` is whether the
74    /// producer supplied a bounded damage rectangle set, so a damaged commit is
75    /// possible at all.
76    ///
77    /// The first decision after construction or [`force_full`](Self::force_full)
78    /// is always [`FrameAction::CommitFull`]: the scanout buffer's contents are
79    /// otherwise undefined, or a modeset has just discarded them.
80    pub const fn decide(&mut self, content_changed: bool, damage_available: bool) -> FrameAction {
81        if self.first || self.force_full {
82            self.first = false;
83            self.force_full = false;
84            self.committed += 1;
85            return FrameAction::CommitFull;
86        }
87        if !content_changed {
88            self.skipped += 1;
89            return FrameAction::Skip;
90        }
91        self.committed += 1;
92        if damage_available {
93            FrameAction::CommitDamaged
94        } else {
95            FrameAction::CommitFull
96        }
97    }
98
99    /// Make the next decision a full commit.
100    ///
101    /// For after a modeset, a mode change or a rebind — anything that leaves
102    /// the previous scanout contents no longer applying, so damage against them
103    /// would repaint the wrong difference. One-shot: it applies to the next
104    /// decision and then clears.
105    pub const fn force_full(&mut self) {
106        self.force_full = true;
107    }
108
109    /// How many frames have been committed.
110    #[must_use]
111    pub const fn committed(&self) -> u64 {
112        self.committed
113    }
114
115    /// How many frames have been skipped — the ones that cost nothing.
116    #[must_use]
117    pub const fn skipped(&self) -> u64 {
118        self.skipped
119    }
120}