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}