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}