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