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}