Skip to main content

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}