Skip to main content

drmkit_fmt/
table.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! `FormatTable`: a parsed, queryable view of a plane's `IN_FORMATS` blob.
5//!
6//! Port of the `FormatTable` half of `src/fmt/format_mod.hpp`.
7//!
8//! # Design contract (carried verbatim from the C++)
9//!
10//! - `IN_FORMATS` prunes the bipartite graph — **necessary, not sufficient**.
11//! - The atomic `TEST_ONLY` commit is ground truth for what a plane can scan
12//!   out.
13//! - [`BandwidthClass`](crate::`BandwidthClass`) is a cost input to placement
14//!   scoring, never a correctness gate.
15//!
16//! # Deviation: no `from_plane`
17//!
18//! The C++ `FormatTable::from_plane(fd, plane_id)` reads the blob off a DRM fd.
19//! Here the crate stays `no_std + alloc` and format-only: `drmkit-core`'s
20//! property layer fetches the blob bytes and hands them to [`FormatTable::from_blob`].
21//! Nothing is lost — the C++ already split the parser out for exactly this
22//! testability reason — and the format logic stays usable without a device.
23
24use alloc::vec::Vec;
25
26use crate::modifier::Modifier;
27
28/// A `(fourcc, modifier)` pair a plane accepts.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
30#[allow(
31    clippy::exhaustive_structs,
32    reason = "a value defined by its fields; there is nothing to add to it"
33)]
34pub struct FormatMod {
35    /// The `FourCC` pixel format.
36    pub fourcc: u32,
37    /// The layout modifier.
38    pub modifier: Modifier,
39}
40
41/// Byte layout of `struct drm_format_modifier_blob` (`drm_mode.h`).
42const BLOB_HEADER_LEN: usize = 24;
43/// Byte layout of `struct drm_format_modifier` (`drm_mode.h`): `formats` (u64),
44/// `offset` (u32), `pad` (u32), `modifier` (u64).
45const BLOB_MODIFIER_LEN: usize = 24;
46
47/// Parsed header fields we actually use.
48struct BlobHeader {
49    count_formats: u32,
50    formats_offset: u32,
51    count_modifiers: u32,
52    modifiers_offset: u32,
53}
54
55/// Read a little-endian `u32` at `offset`, or `None` if it would run past the
56/// end.
57fn read_u32(data: &[u8], offset: usize) -> Option<u32> {
58    let bytes: &[u8; 4] = data.get(offset..offset.checked_add(4)?)?.try_into().ok()?;
59    Some(u32::from_le_bytes(*bytes))
60}
61
62/// Read a little-endian `u64` at `offset`, or `None` if it would run past the
63/// end.
64fn read_u64(data: &[u8], offset: usize) -> Option<u64> {
65    let bytes: &[u8; 8] = data.get(offset..offset.checked_add(8)?)?.try_into().ok()?;
66    Some(u64::from_le_bytes(*bytes))
67}
68
69impl BlobHeader {
70    fn parse(data: &[u8]) -> Option<Self> {
71        if data.len() < BLOB_HEADER_LEN {
72            return None;
73        }
74        // version (u32) and flags (u32) occupy 0..8 and are not consulted:
75        // the kernel has only ever emitted version 1, and the C++ ignores them
76        // too. Reading them would invite a version gate the kernel does not
77        // honor.
78        Some(Self {
79            count_formats: read_u32(data, 8)?,
80            formats_offset: read_u32(data, 12)?,
81            count_modifiers: read_u32(data, 16)?,
82            modifiers_offset: read_u32(data, 20)?,
83        })
84    }
85}
86
87/// A plane's accepted `(fourcc, modifier)` pairs, sorted and de-duplicated.
88#[derive(Debug, Clone, Default, PartialEq, Eq)]
89pub struct FormatTable {
90    /// Sorted by `(fourcc, modifier)`; backs [`FormatTable::all`] and
91    /// [`FormatTable::supports`].
92    pairs: Vec<FormatMod>,
93}
94
95impl FormatTable {
96    /// An empty table, accepting nothing.
97    #[must_use]
98    pub const fn new() -> Self {
99        Self { pairs: Vec::new() }
100    }
101
102    /// Parse a raw `IN_FORMATS` blob payload.
103    ///
104    /// Malformed input yields an empty (or partial) table rather than an error:
105    /// a plane whose blob does not parse is simply a plane nothing is eligible
106    /// for, and the caller's fallback ("`formats[]` implies LINEAR only") is
107    /// the same either way. This matches the C++, which returns a default-
108    /// constructed table on every guard.
109    ///
110    /// # Robustness
111    ///
112    /// The blob's arrays may begin at offsets that are not naturally aligned.
113    /// In C++ that made indexing a typed pointer into the blob undefined
114    /// behavior, and it faulted on strict-alignment targets — the C++ fix was
115    /// to `memcpy` each element out. Here every field is read byte-wise from a
116    /// slice with a bounds-checked accessor, so alignment cannot arise as a
117    /// concern at all. Counts are validated against the remaining length before
118    /// use, in division form, so no `count * size` product can overflow.
119    ///
120    /// ```
121    /// use drmkit_fmt::FormatTable;
122    /// // Too short to hold a header: empty table, no panic.
123    /// assert!(FormatTable::from_blob(&[0u8; 4]).all().is_empty());
124    /// assert!(FormatTable::from_blob(&[]).all().is_empty());
125    /// ```
126    #[must_use]
127    pub fn from_blob(data: &[u8]) -> Self {
128        let mut table = Self::new();
129
130        let Some(header) = BlobHeader::parse(data) else {
131            return table;
132        };
133
134        let size = data.len();
135        let formats_offset = header.formats_offset as usize;
136        let modifiers_offset = header.modifiers_offset as usize;
137
138        // Bounds-check both arrays before touching them. Written in division
139        // form so the implied count * element-size products cannot overflow and
140        // silently bypass the check.
141        if formats_offset > size
142            || modifiers_offset > size
143            || header.count_formats as usize > (size - formats_offset) / 4
144            || header.count_modifiers as usize > (size - modifiers_offset) / BLOB_MODIFIER_LEN
145        {
146            return table;
147        }
148
149        for i in 0..header.count_modifiers as usize {
150            let record = modifiers_offset + i * BLOB_MODIFIER_LEN;
151            let (Some(formats_bitmask), Some(offset), Some(modifier)) = (
152                read_u64(data, record),
153                read_u32(data, record + 8),
154                read_u64(data, record + 16),
155            ) else {
156                continue;
157            };
158
159            // Each set bit in the mask names one format, at `offset + bit` in
160            // the format array.
161            for bit in 0..64u32 {
162                if (formats_bitmask >> bit) & 1 == 0 {
163                    continue;
164                }
165                let Some(index) = offset.checked_add(bit) else {
166                    continue; // malformed blob guard
167                };
168                if index >= header.count_formats {
169                    continue; // malformed blob guard
170                }
171                let Some(fourcc) = read_u32(data, formats_offset + index as usize * 4) else {
172                    continue;
173                };
174                table.pairs.push(FormatMod {
175                    fourcc,
176                    modifier: Modifier(modifier),
177                });
178            }
179        }
180
181        table.build_index();
182        table
183    }
184
185    /// Build a table from already-decoded pairs — for a caller that harvested
186    /// `IN_FORMATS` by other means and wants the queryable table without
187    /// re-reading the blob.
188    ///
189    /// Duplicates are removed; input order does not matter.
190    ///
191    /// ```
192    /// use drmkit_fmt::{FormatTable, Modifier, fourcc};
193    /// let table = FormatTable::from_pairs([
194    ///     (fourcc::XRGB8888, Modifier::LINEAR),
195    ///     (fourcc::XRGB8888, Modifier::LINEAR), // duplicate
196    ///     (fourcc::NV12, Modifier::LINEAR),
197    /// ]);
198    /// assert_eq!(table.all().len(), 2);
199    /// ```
200    pub fn from_pairs<I>(pairs: I) -> Self
201    where
202        I: IntoIterator<Item = (u32, Modifier)>,
203    {
204        let mut table = Self {
205            pairs: pairs
206                .into_iter()
207                .map(|(fourcc, modifier)| FormatMod { fourcc, modifier })
208                .collect(),
209        };
210        table.build_index();
211        table
212    }
213
214    /// Sort and de-duplicate. Some kernels list a pair twice.
215    fn build_index(&mut self) {
216        self.pairs.sort_unstable();
217        self.pairs.dedup();
218    }
219
220    /// Whether the plane advertises this exact `(fourcc, modifier)` pair.
221    ///
222    /// This is exact-match only. Family-aware eligibility — which is what the
223    /// allocator wants — is [`crate::layer_fits_plane`].
224    #[must_use]
225    pub fn supports(&self, fourcc: u32, modifier: Modifier) -> bool {
226        self.pairs
227            .binary_search(&FormatMod { fourcc, modifier })
228            .is_ok()
229    }
230
231    /// Every modifier advertised for `fourcc`, ascending.
232    pub fn modifiers_for(&self, fourcc: u32) -> impl Iterator<Item = Modifier> + '_ {
233        let start = self.pairs.partition_point(|p| p.fourcc < fourcc);
234        self.pairs[start..]
235            .iter()
236            .take_while(move |p| p.fourcc == fourcc)
237            .map(|p| p.modifier)
238    }
239
240    /// Every advertised pair, sorted by `(fourcc, modifier)`.
241    #[must_use]
242    pub fn all(&self) -> &[FormatMod] {
243        &self.pairs
244    }
245
246    /// Whether the table advertises nothing.
247    #[must_use]
248    pub fn is_empty(&self) -> bool {
249        self.pairs.is_empty()
250    }
251
252    /// Number of advertised pairs.
253    #[must_use]
254    pub fn len(&self) -> usize {
255        self.pairs.len()
256    }
257}
258
259/// Edge eligibility for the allocator's bipartite pre-solve: an `IN_FORMATS`
260/// check with family awareness.
261///
262/// `LINEAR` and the legacy `INVALID` sentinel are interchangeable — a layer
263/// tagged either matches a plane entry of either — mirroring the
264/// pre-`FormatTable` `supports_format_modifier` semantics.
265///
266/// A parameterized modifier ([`ModifierFamily`](crate::`ModifierFamily`)) also
267/// matches a plane advertising its family base; see that type for why the set
268/// of such families is deliberately small. Every other modifier must match
269/// exactly.
270///
271/// This prunes the graph. The atomic `TEST_ONLY` commit downstream remains the
272/// ground truth.
273///
274/// ```
275/// use drmkit_fmt::{FormatTable, Modifier, fourcc, layer_fits_plane, sand128_col_height};
276///
277/// // A vc4 plane advertises SAND128 with column height 0.
278/// let plane = FormatTable::from_pairs([(fourcc::NV12, sand128_col_height(0))]);
279///
280/// // rpi-hevc-dec hands over SAND128 with the coded height baked in.
281/// let produced = sand128_col_height(1080);
282///
283/// assert!(!plane.supports(fourcc::NV12, produced), "not an exact match");
284/// assert!(layer_fits_plane(&plane, fourcc::NV12, produced), "but it is eligible");
285/// ```
286#[must_use]
287pub fn layer_fits_plane(plane_formats: &FormatTable, fourcc: u32, m: Modifier) -> bool {
288    if plane_formats.supports(fourcc, m) {
289        return true;
290    }
291    if m.is_linear() || m.is_invalid() {
292        return plane_formats.supports(fourcc, Modifier::LINEAR)
293            || plane_formats.supports(fourcc, Modifier::INVALID);
294    }
295    if let Some(base) = m.family().base() {
296        return plane_formats.supports(fourcc, base);
297    }
298    false
299}