drmkit_fmt/modifier.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! The 64-bit DRM format modifier, its vendor families, and what each does to
5//! bus traffic.
6//!
7//! Port of the `Modifier` / `BandwidthClass` / `classify` / `rotation_compatible`
8//! surface of `src/fmt/format_mod.hpp`.
9
10use core::fmt;
11
12/// DRM modifier vendor identifiers (`DRM_FORMAT_MOD_VENDOR_*`, the top 8 bits).
13pub mod vendor {
14 /// No vendor: `LINEAR` and the `INVALID` sentinel live here.
15 pub const NONE: u8 = 0x00;
16 /// Intel.
17 pub const INTEL: u8 = 0x01;
18 /// AMD.
19 pub const AMD: u8 = 0x02;
20 /// NVIDIA.
21 pub const NVIDIA: u8 = 0x03;
22 /// Samsung.
23 pub const SAMSUNG: u8 = 0x04;
24 /// Qualcomm.
25 pub const QCOM: u8 = 0x05;
26 /// Vivante.
27 pub const VIVANTE: u8 = 0x06;
28 /// Broadcom (Raspberry Pi vc4 / V3D).
29 pub const BROADCOM: u8 = 0x07;
30 /// Arm.
31 pub const ARM: u8 = 0x08;
32 /// Allwinner.
33 pub const ALLWINNER: u8 = 0x09;
34 /// Amlogic.
35 pub const AMLOGIC: u8 = 0x0a;
36 /// `StarFive` / `VeriSilicon` DC8200.
37 ///
38 /// Out-of-tree (`starfive-tech/linux`), and it collides with mainline:
39 /// `drm_fourcc.h` now assigns `0x0b` to `DRM_FORMAT_MOD_VENDOR_MTK`, so a
40 /// `MediaTek` modifier classifies and describes as VS. Carried, as upstream
41 /// carries it, because the `VisionFive` 2 is a first-class HIL target.
42 pub const VS: u8 = 0x0b;
43}
44
45/// Build a modifier value from a vendor and a 56-bit body, matching the
46/// `fourcc_mod_code` macro.
47#[must_use]
48pub const fn mod_code(vendor: u8, body: u64) -> u64 {
49 ((vendor as u64) << 56) | (body & 0x00ff_ffff_ffff_ffff)
50}
51
52/// Legacy `DRM_FORMAT_MOD_ARM_16X16_BLOCK_U_INTERLEAVED`.
53///
54/// Type field [`arm_type::MISC`] with body 1, i.e. `0x0810_0000_0000_0001`.
55/// Worth naming explicitly: `fourcc_mod_code(ARM, 1)` — which the C++ used as
56/// its `#ifndef` fallback for headers predating the type-field encoding, until
57/// drm-cxx `2eba043` — is **not** this value. It is
58/// `DRM_FORMAT_MOD_ARM_AFBC(BLOCK_SIZE_16x16)`, so a build picking up that
59/// fallback would classify 16x16 AFBC as tiling and silently under-count its
60/// bandwidth saving.
61pub const ARM_16X16_BLOCK_U_INTERLEAVED: Modifier =
62 Modifier(mod_code(vendor::ARM, (arm_type::MISC << 52) | 1));
63
64/// Arm modifier type field (bits 55:52).
65pub mod arm_type {
66 /// Arm Framebuffer Compression.
67 pub const AFBC: u64 = 0x0;
68 /// Miscellaneous tiling.
69 pub const MISC: u64 = 0x1;
70 /// Arm Fixed Rate Compression.
71 pub const AFRC: u64 = 0x2;
72}
73
74/// AFBC superblock-size codes (low 4 bits of the modifier body).
75pub mod afbc_block {
76 /// 16x16 superblocks.
77 pub const SIZE_16X16: u64 = 1;
78 /// 32x8 superblocks.
79 pub const SIZE_32X8: u64 = 2;
80 /// 64x4 superblocks.
81 pub const SIZE_64X4: u64 = 3;
82 /// Mixed 32x8 / 64x4 superblocks.
83 pub const SIZE_32X8_64X4: u64 = 4;
84}
85
86/// AFBC feature flags.
87pub mod afbc_flag {
88 /// YUV transform.
89 pub const YTR: u64 = 1 << 4;
90 /// Split-block layout.
91 pub const SPLIT: u64 = 1 << 5;
92 /// Sparse layout.
93 pub const SPARSE: u64 = 1 << 6;
94 /// Constant bit rate.
95 pub const CBR: u64 = 1 << 7;
96 /// Tiled header.
97 pub const TILED: u64 = 1 << 8;
98 /// Solid color opaque blocks.
99 pub const SC: u64 = 1 << 9;
100 /// Double buffering.
101 pub const DB: u64 = 1 << 10;
102 /// Block-check header.
103 pub const BCH: u64 = 1 << 11;
104 /// Uncompressed storage mode.
105 pub const USM: u64 = 1 << 12;
106}
107
108/// Broadcom layout codes (low byte of the modifier body).
109pub mod broadcom {
110 /// vc4 T-tiled.
111 pub const VC4_T_TILED: u64 = 1;
112 /// SAND, 32-byte columns.
113 pub const SAND32: u64 = 2;
114 /// SAND, 64-byte columns.
115 pub const SAND64: u64 = 3;
116 /// SAND, 128-byte columns.
117 pub const SAND128: u64 = 4;
118 /// SAND, 256-byte columns.
119 pub const SAND256: u64 = 5;
120 /// V3D UIF.
121 pub const UIF: u64 = 6;
122}
123
124/// Bit position of the AMD "displayable DCC" flag within the modifier.
125///
126/// `AMD_FMT_MOD_DCC_SHIFT` is 13 and is stable across GFX generations — only
127/// the tile encoding is generation-specific — so reading this bit is exactly
128/// what the canonical `AMD_FMT_MOD_GET(DCC, ...)` accessor extracts.
129const AMD_DCC_SHIFT: u64 = 13;
130
131/// A DRM format modifier.
132///
133/// The default is [`Modifier::LINEAR`], matching the C++ `Modifier` whose
134/// member initializer is `DRM_FORMAT_MOD_LINEAR`.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
136#[allow(
137 clippy::exhaustive_structs,
138 reason = "a value defined by its fields; there is nothing to add to it"
139)]
140pub struct Modifier(pub u64);
141
142impl Default for Modifier {
143 fn default() -> Self {
144 Self::LINEAR
145 }
146}
147
148impl Modifier {
149 /// Raw, untiled bytes (`DRM_FORMAT_MOD_LINEAR`).
150 pub const LINEAR: Self = Self(0);
151
152 /// The legacy "implementation-defined layout" sentinel
153 /// (`DRM_FORMAT_MOD_INVALID`).
154 ///
155 /// Treated as interchangeable with [`Modifier::LINEAR`] for eligibility,
156 /// mirroring the pre-`FormatTable` `supports_format_modifier` semantics.
157 pub const INVALID: Self = Self(mod_code(vendor::NONE, (1 << 56) - 1));
158
159 /// The vendor field (top 8 bits), as `fourcc_mod_get_vendor` reports it.
160 #[must_use]
161 pub const fn vendor(self) -> u8 {
162 (self.0 >> 56) as u8
163 }
164
165 /// The 56-bit body below the vendor field.
166 #[must_use]
167 pub const fn body(self) -> u64 {
168 self.0 & 0x00ff_ffff_ffff_ffff
169 }
170
171 /// Whether this is [`Modifier::LINEAR`].
172 #[must_use]
173 pub const fn is_linear(self) -> bool {
174 self.0 == Self::LINEAR.0
175 }
176
177 /// Whether this is the [`Modifier::INVALID`] sentinel.
178 #[must_use]
179 pub const fn is_invalid(self) -> bool {
180 self.0 == Self::INVALID.0
181 }
182
183 /// Whether the AMD "displayable DCC" bit is set.
184 #[must_use]
185 const fn amd_has_dcc(self) -> bool {
186 self.vendor() == vendor::AMD && ((self.0 >> AMD_DCC_SHIFT) & 0x1) != 0
187 }
188}
189
190impl From<u64> for Modifier {
191 fn from(value: u64) -> Self {
192 Self(value)
193 }
194}
195
196impl From<Modifier> for u64 {
197 fn from(value: Modifier) -> Self {
198 value.0
199 }
200}
201
202/// What a modifier does to *bus traffic*.
203///
204/// Best-effort classification by vendor and bit inspection, used only for
205/// allocator scoring. **Correctness never depends on this being right** — the
206/// atomic `TEST_ONLY` commit is the arbiter of what a plane can scan out.
207///
208/// Deliberately **not** `#[non_exhaustive]`. It mirrors a closed C++ enum, and
209/// consumers switch on it to assign placement bonuses. Leaving it open would
210/// force every one of them to write a catch-all arm, which is exactly how a new
211/// class would silently inherit some other class's bonus. Adding a variant
212/// should break those consumers so each decides what the new class is worth.
213#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
214#[allow(
215 clippy::exhaustive_enums,
216 reason = "drmkit-planes scores it with an exhaustive match, so a new class fails to compile until it is scored"
217)]
218pub enum BandwidthClass {
219 /// Raw bytes, poor DRAM locality.
220 Linear,
221 /// Same byte count, better locality: Broadcom UIF/SAND/T, Arm 16x16
222 /// interleaved, NVIDIA block-linear with compression type 0.
223 Tiling,
224 /// Fewer bytes on the bus, data-dependent: AFBC/AFRC, AMD DCC, Qualcomm
225 /// UBWC, NVIDIA block-linear with a nonzero compression type.
226 Compression,
227}
228
229/// Classify a modifier's effect on scanout bandwidth.
230///
231/// ```
232/// use drmkit_fmt::{BandwidthClass, Modifier, classify};
233/// assert_eq!(classify(Modifier::LINEAR), BandwidthClass::Linear);
234/// assert_eq!(classify(Modifier::INVALID), BandwidthClass::Linear);
235/// ```
236#[must_use]
237pub const fn classify(m: Modifier) -> BandwidthClass {
238 if m.is_linear() || m.is_invalid() {
239 return BandwidthClass::Linear;
240 }
241
242 match m.vendor() {
243 vendor::ARM => {
244 // Legacy 16x16 interleaved is tiling, not AFBC. Its type field is
245 // already MISC, so the type check below would reach the same
246 // verdict; the explicit arm mirrors the C++ and documents intent.
247 if m.0 == ARM_16X16_BLOCK_U_INTERLEAVED.0 {
248 return BandwidthClass::Tiling;
249 }
250 let ty = (m.0 >> 52) & 0xf;
251 // AFBC and AFRC are true compression; MISC is tiling.
252 if ty == arm_type::AFBC || ty == arm_type::AFRC {
253 BandwidthClass::Compression
254 } else {
255 BandwidthClass::Tiling
256 }
257 }
258 vendor::AMD => {
259 if m.amd_has_dcc() {
260 BandwidthClass::Compression
261 } else {
262 BandwidthClass::Tiling
263 }
264 }
265 // QCOM_COMPRESSED is UBWC.
266 vendor::QCOM => BandwidthClass::Compression,
267 vendor::NVIDIA => {
268 // Bits 25:23 are the block-linear "lossless framebuffer compression
269 // type" field: 0 = none, 1-4 = compressed (ROP/3D layouts 1 and 2,
270 // CDE horizontal, CDE vertical). Mask the full 3 bits -- a 2-bit
271 // mask misses CDE vertical (type 4 = 0b100) and calls it Tiling.
272 if ((m.0 >> 23) & 0x7) != 0 {
273 BandwidthClass::Compression
274 } else {
275 BandwidthClass::Tiling
276 }
277 }
278 vendor::VS => {
279 // VS packs a 2-bit TYPE at bits 55:54 -- NORMAL (tiling) vs
280 // COMPRESSED (`DEC400` lossless FB compression). VS_LINEAR is the
281 // all-zero body.
282 if m.body() == 0 {
283 return BandwidthClass::Linear;
284 }
285 if ((m.0 >> 54) & 0x3) == 0x1 {
286 BandwidthClass::Compression
287 } else {
288 BandwidthClass::Tiling
289 }
290 }
291 // Broadcom UIF/SAND/T buy locality only, never byte savings.
292 // Unknown non-linear: assume tiled, never free.
293 _ => BandwidthClass::Tiling,
294 }
295}
296
297/// Display-plane rotation. 90/270 transpose the scanout fetch order; 0/180 do
298/// not.
299#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
300#[allow(
301 clippy::exhaustive_enums,
302 reason = "a closed set: there is no further variant to add"
303)]
304pub enum Rotation {
305 /// No rotation.
306 #[default]
307 Rotate0,
308 /// 90 degrees.
309 Rotate90,
310 /// 180 degrees.
311 Rotate180,
312 /// 270 degrees.
313 Rotate270,
314}
315
316impl Rotation {
317 /// Whether this rotation transposes the scanout fetch order.
318 #[must_use]
319 pub const fn transposes(self) -> bool {
320 matches!(self, Self::Rotate90 | Self::Rotate270)
321 }
322}
323
324/// Can a buffer with modifier `m` be scanned out under rotation `r`?
325///
326/// A 90/270 rotation transposes the fetch order, which the display engine
327/// cannot follow for a `LINEAR` buffer (a rotated fetch needs a tiled layout)
328/// or for an AMD DCC metadata surface. Every other layout is left to the atomic
329/// `TEST_ONLY` commit to confirm. 0/180 never transpose, so all modifiers pass.
330///
331/// This is a pre-filter for the negotiator only — correctness still rests on
332/// the commit.
333///
334/// ```
335/// use drmkit_fmt::{Modifier, Rotation, rotation_compatible};
336/// assert!(rotation_compatible(Modifier::LINEAR, Rotation::Rotate180));
337/// assert!(!rotation_compatible(Modifier::LINEAR, Rotation::Rotate90));
338/// ```
339#[must_use]
340pub const fn rotation_compatible(m: Modifier, r: Rotation) -> bool {
341 if !r.transposes() {
342 return true; // 0/180 keep the walk order: any modifier is fine
343 }
344 if m.is_linear() {
345 return false; // a rotated fetch needs a tiled layout, not raw bytes
346 }
347 // AMD DCC metadata cannot be walked transposed (the same bit classify uses).
348 !m.amd_has_dcc()
349}
350
351impl fmt::Display for Modifier {
352 /// Renders the human-readable description from [`crate::describe()`].
353 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354 f.write_str(&crate::describe(*self))
355 }
356}