Skip to main content

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}