drmkit_dumb/mapping.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Scoped CPU views over a buffer's linear pixel storage.
5//!
6//! Port of `src/buffer_mapping.hpp`. Home of **invariant 6**.
7
8/// Intent of a CPU mapping.
9///
10/// Backends that distinguish read-only from read-write traffic (GBM, through
11/// `gbm_bo_map`'s transfer flags) honor this; dumb buffers ignore it. Picking
12/// the narrowest mode that fits is always correct: on a backend that stages,
13/// a read-only mapping needs no flush back on unmap.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
15#[repr(u8)]
16#[allow(
17 clippy::exhaustive_enums,
18 reason = "a closed set: there is no further variant to add"
19)]
20pub enum MapAccess {
21 /// Read only.
22 Read = 0,
23 /// Write only.
24 Write = 1,
25 /// Read and write.
26 #[default]
27 ReadWrite = 2,
28}
29
30/// A borrowed view over a dumb buffer's linear pixel storage.
31///
32/// # Invariant 6: a zero-cost view of a lifetime mmap
33///
34/// A dumb buffer is mmap'd once at creation and the mapping is held for the
35/// buffer's whole life, cache-coherent by kernel construction. Acquiring this
36/// view therefore costs nothing — no ioctl, no syscall — and **dropping it
37/// costs nothing either**.
38///
39/// In C++ that is a documented promise: `BufferMapping` carries a function
40/// pointer that happens to be null for dumb buffers, and a comment explains
41/// that the destructor is a no-op. Here it is structural. This type has no
42/// [`Drop`] implementation at all, so there is no destructor to become
43/// non-trivial by accident — adding one would be a visible, reviewable change
44/// rather than a silent regression in per-frame cost.
45///
46/// The borrow is equally load-bearing: the view cannot outlive the buffer it
47/// came from, so the use-after-free the C++ warns about ("holding a mapping
48/// past the buffer's destructor") does not compile.
49///
50/// The lifetime-mmap decision matters beyond convenience — `DumbScanoutSink`'s
51/// pacing depends on `map()` being free per frame — so it is pinned rather
52/// than merely documented.
53#[derive(Debug)]
54pub struct Mapping<'a> {
55 pixels: &'a mut [u8],
56 stride: u32,
57 width: u32,
58 height: u32,
59 access: MapAccess,
60}
61
62impl<'a> Mapping<'a> {
63 pub(crate) fn new(
64 pixels: &'a mut [u8],
65 stride: u32,
66 width: u32,
67 height: u32,
68 access: MapAccess,
69 ) -> Self {
70 Self {
71 pixels,
72 stride,
73 width,
74 height,
75 access,
76 }
77 }
78
79 /// Whether the view covers no storage.
80 #[must_use]
81 pub const fn is_empty(&self) -> bool {
82 self.pixels.is_empty()
83 }
84
85 /// The linear pixel storage, read only.
86 ///
87 /// The first row starts at index 0; subsequent rows are [`stride`](Self::stride)
88 /// bytes apart.
89 #[must_use]
90 pub const fn pixels(&self) -> &[u8] {
91 self.pixels
92 }
93
94 /// The linear pixel storage, mutable.
95 #[must_use]
96 pub const fn pixels_mut(&mut self) -> &mut [u8] {
97 self.pixels
98 }
99
100 /// Bytes per row.
101 ///
102 /// The kernel may pad this above `width * bytes_per_pixel` — amdgpu's DC
103 /// applies a 256-byte rule, for one. Honor it rather than recomputing.
104 #[must_use]
105 pub const fn stride(&self) -> u32 {
106 self.stride
107 }
108
109 /// Buffer width in pixels.
110 #[must_use]
111 pub const fn width(&self) -> u32 {
112 self.width
113 }
114
115 /// Buffer height in pixels.
116 #[must_use]
117 pub const fn height(&self) -> u32 {
118 self.height
119 }
120
121 /// The access mode this view was acquired with.
122 ///
123 /// Recorded but unused at the dumb-buffer level; a backend maintaining
124 /// dirty state across map cycles can switch on it.
125 #[must_use]
126 pub const fn access(&self) -> MapAccess {
127 self.access
128 }
129
130 /// One row, read only, or `None` if `row` is past the bottom.
131 #[must_use]
132 pub fn row(&self, row: u32) -> Option<&[u8]> {
133 let (start, end) = self.row_range(row)?;
134 self.pixels.get(start..end)
135 }
136
137 /// One row, mutable, or `None` if `row` is past the bottom.
138 #[must_use]
139 pub fn row_mut(&mut self, row: u32) -> Option<&mut [u8]> {
140 let (start, end) = self.row_range(row)?;
141 self.pixels.get_mut(start..end)
142 }
143
144 fn row_range(&self, row: u32) -> Option<(usize, usize)> {
145 if row >= self.height {
146 return None;
147 }
148 let stride = self.stride as usize;
149 let start = (row as usize).checked_mul(stride)?;
150 Some((start, start.checked_add(stride)?))
151 }
152}