Skip to main content

emblema_hal/
format.rs

1//! Geometry and pixel format types shared by the rendering and presentation
2//! axes.
3//!
4//! DRM fourcc codes and format modifiers live here rather than in the
5//! presentation layer because format negotiation runs *between* the two axes:
6//! a presentation target advertises what it can scan out, the HAL context
7//! advertises what it can render to and export, and the intersection is what
8//! gets allocated.
9
10/// A 2D extent in pixels.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct Extent2D {
13    pub width: u32,
14    pub height: u32,
15}
16
17impl Extent2D {
18    pub const fn new(width: u32, height: u32) -> Self {
19        Self { width, height }
20    }
21
22    /// Total pixel count, widened so large surfaces cannot overflow.
23    pub const fn area(self) -> u64 {
24        self.width as u64 * self.height as u64
25    }
26
27    pub const fn is_empty(self) -> bool {
28        self.width == 0 || self.height == 0
29    }
30}
31
32/// Pixel formats the renderer can target.
33///
34/// Color is sRGB-encoded f32 internally; these describe storage at the API
35/// boundary and at target write, and no transfer function sits between the
36/// two.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum PixelFormat {
39    /// 8-bit RGBA, unsigned normalized.
40    Rgba8Unorm,
41    /// 8-bit RGBA with sRGB transfer on write.
42    Rgba8UnormSrgb,
43    /// 8-bit BGRA, the common scanout order.
44    Bgra8Unorm,
45    /// 8-bit BGRA with sRGB transfer on write.
46    Bgra8UnormSrgb,
47    /// 10-bit color with 2-bit alpha, preferred when the pipeline is HDR-aware.
48    Rgb10A2Unorm,
49    /// 16-bit float per channel, for intermediate targets.
50    Rgba16Float,
51    /// One 8-bit channel, unsigned normalized.
52    ///
53    /// For data that is coverage rather than color — a glyph atlas is the
54    /// case — where storing the same byte four times costs four times the
55    /// memory and four times the bandwidth to sample it.
56    R8Unorm,
57}
58
59impl PixelFormat {
60    pub const fn bytes_per_pixel(self) -> u32 {
61        match self {
62            Self::Rgba8Unorm
63            | Self::Rgba8UnormSrgb
64            | Self::Bgra8Unorm
65            | Self::Bgra8UnormSrgb
66            | Self::Rgb10A2Unorm => 4,
67            Self::Rgba16Float => 8,
68            Self::R8Unorm => 1,
69        }
70    }
71
72    /// The format an offscreen layer takes when the frame lands in this one.
73    ///
74    /// Following the root is what keeps the cost where the choice was made: a
75    /// caller who asks for a floating-point surface gets layers that can hold
76    /// what it holds, and a caller who does not pays nothing. A layer is an
77    /// intermediate of *this* frame, so it should carry at least what the frame
78    /// it composites into can carry.
79    ///
80    /// Stated as a match rather than as identity because two formats here would
81    /// make bad layers. A single-channel one has nowhere to put color at all.
82    /// And ten-bit color with two-bit alpha is worse for a layer than the
83    /// eight-bit target it would replace, because a layer's alpha is group
84    /// opacity -- a value that gets composited -- rather than a scanout channel
85    /// nothing reads back.
86    ///
87    /// Never an sRGB format, whatever the root is. The pipeline carries encoded
88    /// components, so a target that encodes on write would encode them a second
89    /// time and a layer would come back paler than the same content drawn
90    /// straight onto the frame.
91    ///
92    /// This followed the root for a while, and was right to: color was light
93    /// then, eight bits of *linear* color band visibly in the darks, and a dark
94    /// ramp resolving forty distinct tones drawn straight into an sRGB frame
95    /// came back as six through a layer holding linear eight-bit color. Neither
96    /// half of that can happen now -- a layer's eight bits are spaced by the
97    /// transfer function because the values arriving already are.
98    pub const fn intermediate(self) -> Self {
99        match self {
100            Self::Rgba16Float => Self::Rgba16Float,
101            // Never the sRGB sibling of an eight-bit format, even when the
102            // frame it composites into is one. The pipeline holds encoded
103            // components, so a target that encodes on write would encode them a
104            // second time -- a layer would come back washed out against the
105            // same content drawn directly. This did follow the frame, back when
106            // the values reaching it were light.
107            _ => Self::Rgba8Unorm,
108        }
109    }
110
111    /// How far apart two representable values are in this format's storage,
112    /// or zero where the question does not apply.
113    ///
114    /// The distance a dither has to bridge. It is stated in storage units
115    /// rather than in light, which for [`Self::Rgba8UnormSrgb`] and its sibling
116    /// is not the same thing: the hardware encodes on write, so a step there is
117    /// a step of the *encoded* value and the light it stands for varies across
118    /// the range by a factor of about thirty. Anything acting on this number
119    /// therefore has to know which space it is in, which is what
120    /// [`Self::is_srgb`] answers.
121    ///
122    /// Zero for [`Self::Rgba16Float`], where there is no fixed quantum to
123    /// bridge -- half's precision is relative, so a step near black is minute
124    /// and a dither sized for one near white would swamp it. Zero for
125    /// [`Self::R8Unorm`] as well, which holds coverage rather than color.
126    pub const fn quantization_step(self) -> f32 {
127        match self {
128            Self::Rgba8Unorm | Self::Rgba8UnormSrgb | Self::Bgra8Unorm | Self::Bgra8UnormSrgb => {
129                1.0 / 255.0
130            }
131            Self::Rgb10A2Unorm => 1.0 / 1023.0,
132            Self::Rgba16Float | Self::R8Unorm => 0.0,
133        }
134    }
135
136    /// Whether the renderer can draw into this format.
137    ///
138    /// It cannot draw into one that encodes on write. The pipeline carries
139    /// sRGB-encoded components from the API boundary onward, so a target
140    /// applying the transfer function would apply it a second time and the
141    /// picture would come back over a third too bright at mid gray.
142    ///
143    /// Stated once and consulted from three places, because it has been got
144    /// wrong at two of them and neither showed up in a test that renders
145    /// pixels. The swapchain preferred an sRGB surface format and DRM scanout
146    /// rendered through an sRGB image view, both for the same reason -- they
147    /// were written while the pipeline carried light, where encoding on write
148    /// was exactly what was wanted. A rule that lives in one place can be
149    /// re-read; three copies of an argument get updated one at a time.
150    pub const fn is_drawable(self) -> bool {
151        !self.is_srgb()
152    }
153
154    /// Whether writes to this format apply an sRGB transfer function.
155    pub const fn is_srgb(self) -> bool {
156        matches!(self, Self::Rgba8UnormSrgb | Self::Bgra8UnormSrgb)
157    }
158
159    /// The DRM fourcc this format scans out as, if any.
160    ///
161    /// Intermediate formats have no scanout representation and return `None`,
162    /// which is what keeps them out of negotiation.
163    pub const fn fourcc(self) -> Option<Fourcc> {
164        match self {
165            Self::Rgba8Unorm | Self::Rgba8UnormSrgb => Some(Fourcc::ABGR8888),
166            Self::Bgra8Unorm | Self::Bgra8UnormSrgb => Some(Fourcc::ARGB8888),
167            Self::Rgb10A2Unorm => Some(Fourcc::XRGB2101010),
168            // Neither is anything a display controller scans out: one is an
169            // intermediate precision and the other is coverage rather than
170            // color. Returning nothing is what keeps both out of negotiation.
171            Self::Rgba16Float | Self::R8Unorm => None,
172        }
173    }
174}
175
176/// A DRM fourcc format code.
177///
178/// Stored as the packed little-endian character code the kernel uses, so it
179/// can be handed to KMS unchanged.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
181pub struct Fourcc(pub u32);
182
183impl Fourcc {
184    pub const fn new(code: [u8; 4]) -> Self {
185        Self(u32::from_le_bytes(code))
186    }
187
188    pub const ARGB8888: Self = Self::new(*b"AR24");
189    pub const XRGB8888: Self = Self::new(*b"XR24");
190    pub const ABGR8888: Self = Self::new(*b"AB24");
191    pub const XBGR8888: Self = Self::new(*b"XB24");
192    pub const XRGB2101010: Self = Self::new(*b"XR30");
193    pub const ARGB2101010: Self = Self::new(*b"AR30");
194
195    /// The code as its four characters, for logging.
196    pub const fn to_bytes(self) -> [u8; 4] {
197        self.0.to_le_bytes()
198    }
199}
200
201impl std::fmt::Display for Fourcc {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        let b = self.to_bytes();
204        for c in b {
205            write!(f, "{}", c as char)?;
206        }
207        Ok(())
208    }
209}
210
211/// A DRM format modifier describing the memory layout of a buffer.
212///
213/// Getting this right is a bandwidth question, not a correctness one: falling
214/// back to [`Modifier::LINEAR`] when a vendor tiled or compressed layout was
215/// available can halve effective memory bandwidth on an embedded panel. The
216/// chosen modifier is therefore always logged, and a negotiation that cannot
217/// find a common non-linear layout is reported rather than silently accepted.
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
219pub struct Modifier(pub u64);
220
221impl Modifier {
222    /// Plain linear layout. Universally supported and universally slow.
223    pub const LINEAR: Self = Self(0);
224
225    /// The "driver picks" sentinel, `DRM_FORMAT_MOD_INVALID`.
226    ///
227    /// Valid only where no explicit modifier negotiation happened. It must
228    /// never reach an atomic commit that was built from a negotiated set.
229    pub const INVALID: Self = Self(0x00ff_ffff_ffff_ffff);
230
231    pub const fn is_linear(self) -> bool {
232        self.0 == Self::LINEAR.0
233    }
234
235    pub const fn is_valid(self) -> bool {
236        self.0 != Self::INVALID.0
237    }
238}
239
240impl std::fmt::Display for Modifier {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        if self.is_linear() {
243            write!(f, "LINEAR")
244        } else if !self.is_valid() {
245            write!(f, "INVALID")
246        } else {
247            write!(f, "{:#018x}", self.0)
248        }
249    }
250}
251
252/// A format paired with the layouts a device will accept for it.
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct FormatModifierSet {
255    pub fourcc: Fourcc,
256    pub modifiers: Vec<Modifier>,
257}
258
259impl FormatModifierSet {
260    pub fn new(fourcc: Fourcc, modifiers: impl Into<Vec<Modifier>>) -> Self {
261        Self {
262            fourcc,
263            modifiers: modifiers.into(),
264        }
265    }
266
267    /// Modifiers common to both sides, preferring non-linear layouts.
268    ///
269    /// Order matters: the caller takes the first entry, so linear sorts last
270    /// and is chosen only when nothing better is shared.
271    pub fn intersect(&self, other: &Self) -> Option<Self> {
272        if self.fourcc != other.fourcc {
273            return None;
274        }
275        let mut modifiers: Vec<Modifier> = self
276            .modifiers
277            .iter()
278            .filter(|m| other.modifiers.contains(m))
279            .copied()
280            .collect();
281        if modifiers.is_empty() {
282            return None;
283        }
284        modifiers.sort_by_key(|m| (m.is_linear(), m.0));
285        Some(Self {
286            fourcc: self.fourcc,
287            modifiers,
288        })
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn fourcc_round_trips_through_characters() {
298        assert_eq!(Fourcc::new(*b"AR24"), Fourcc::ARGB8888);
299        assert_eq!(&Fourcc::ARGB8888.to_bytes(), b"AR24");
300        assert_eq!(Fourcc::XRGB2101010.to_string(), "XR30");
301    }
302
303    #[test]
304    fn scanout_formats_map_to_fourcc_and_intermediates_do_not() {
305        assert_eq!(PixelFormat::Bgra8Unorm.fourcc(), Some(Fourcc::ARGB8888));
306        assert_eq!(PixelFormat::Rgba8Unorm.fourcc(), Some(Fourcc::ABGR8888));
307        // Rgba16Float is an intermediate target; it must stay out of
308        // negotiation rather than be offered to KMS.
309        assert_eq!(PixelFormat::Rgba16Float.fourcc(), None);
310    }
311
312    #[test]
313    fn srgb_variants_are_distinguished_from_linear_ones() {
314        assert!(PixelFormat::Bgra8UnormSrgb.is_srgb());
315        assert!(!PixelFormat::Bgra8Unorm.is_srgb());
316        // The sRGB transfer is a write-time property, not a storage one.
317        assert_eq!(
318            PixelFormat::Bgra8UnormSrgb.bytes_per_pixel(),
319            PixelFormat::Bgra8Unorm.bytes_per_pixel()
320        );
321    }
322
323    #[test]
324    fn modifier_sentinels_are_distinct() {
325        assert!(Modifier::LINEAR.is_linear());
326        assert!(Modifier::LINEAR.is_valid());
327        assert!(!Modifier::INVALID.is_valid());
328        assert_eq!(Modifier::LINEAR.to_string(), "LINEAR");
329        assert_eq!(Modifier::INVALID.to_string(), "INVALID");
330    }
331
332    #[test]
333    fn intersection_prefers_non_linear_layouts() {
334        let vendor = Modifier(0x0100_0000_0000_0001);
335        let render = FormatModifierSet::new(Fourcc::ARGB8888, vec![Modifier::LINEAR, vendor]);
336        let scanout = FormatModifierSet::new(Fourcc::ARGB8888, vec![vendor, Modifier::LINEAR]);
337
338        let common = render.intersect(&scanout).expect("shared modifiers");
339        // Linear sorts last so the caller taking the first entry gets the
340        // bandwidth-preserving layout.
341        assert_eq!(common.modifiers.first(), Some(&vendor));
342        assert_eq!(common.modifiers.last(), Some(&Modifier::LINEAR));
343    }
344
345    #[test]
346    fn intersection_fails_loudly_rather_than_falling_back() {
347        let render = FormatModifierSet::new(Fourcc::ARGB8888, vec![Modifier(1)]);
348        let scanout = FormatModifierSet::new(Fourcc::ARGB8888, vec![Modifier(2)]);
349        // No shared layout is a negotiation failure the caller must report,
350        // not an invitation to substitute LINEAR.
351        assert_eq!(render.intersect(&scanout), None);
352
353        let other_format = FormatModifierSet::new(Fourcc::XRGB2101010, vec![Modifier(1)]);
354        assert_eq!(render.intersect(&other_format), None);
355    }
356
357    #[test]
358    fn extent_area_does_not_overflow_at_large_sizes() {
359        let huge = Extent2D::new(u32::MAX, 2);
360        assert_eq!(huge.area(), u32::MAX as u64 * 2);
361        assert!(Extent2D::new(0, 1080).is_empty());
362    }
363}