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}