1use core::fmt;
13
14use denise::Size;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum PixelLayout {
26 Xrgb8888,
29 Rgb565,
32}
33
34impl PixelLayout {
35 #[inline]
37 pub const fn bytes_per_pixel(self) -> usize {
38 match self {
39 PixelLayout::Xrgb8888 => 4,
40 PixelLayout::Rgb565 => 2,
41 }
42 }
43
44 pub const fn from_bits_per_pixel(bpp: u32) -> Option<Self> {
46 match bpp {
47 32 => Some(PixelLayout::Xrgb8888),
48 16 => Some(PixelLayout::Rgb565),
49 _ => None,
50 }
51 }
52
53 #[inline]
58 pub const fn to_rgb565(word: u32) -> u16 {
59 let r = (word >> 19) & 0x1F;
60 let g = (word >> 10) & 0x3F;
61 let b = (word >> 3) & 0x1F;
62 ((r << 11) | (g << 5) | b) as u16
63 }
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub struct FbInfo {
69 pub size: Size,
71 pub stride_bytes: u32,
77 pub bits_per_pixel: u32,
79 pub layout: PixelLayout,
81}
82
83impl FbInfo {
84 pub fn from_sysfs(
90 virtual_size: &str,
91 modes: &str,
92 stride: &str,
93 bits_per_pixel: &str,
94 ) -> Result<Self, FbInfoError> {
95 let virtual_size = parse_virtual_size(virtual_size)?;
96 let size = parse_modes(modes).unwrap_or(virtual_size);
97
98 let size = Size::new(
100 size.width.min(virtual_size.width),
101 size.height.min(virtual_size.height),
102 );
103
104 let stride_bytes: u32 = stride.trim().parse().map_err(|_| FbInfoError::Unparsable {
105 attribute: "stride",
106 })?;
107
108 let bits_per_pixel: u32 =
109 bits_per_pixel
110 .trim()
111 .parse()
112 .map_err(|_| FbInfoError::Unparsable {
113 attribute: "bits_per_pixel",
114 })?;
115
116 let layout = PixelLayout::from_bits_per_pixel(bits_per_pixel)
117 .ok_or(FbInfoError::UnsupportedDepth { bits_per_pixel })?;
118
119 if size.is_empty() {
120 return Err(FbInfoError::EmptyGeometry);
121 }
122
123 let minimum = size.width as usize * layout.bytes_per_pixel();
124 if (stride_bytes as usize) < minimum {
125 return Err(FbInfoError::StrideTooNarrow {
126 stride_bytes,
127 required: minimum as u32,
128 });
129 }
130
131 Ok(Self {
132 size,
133 stride_bytes,
134 bits_per_pixel,
135 layout,
136 })
137 }
138
139 pub fn required_bytes(&self) -> usize {
141 self.stride_bytes as usize * (self.size.height as usize - 1)
143 + self.size.width as usize * self.layout.bytes_per_pixel()
144 }
145
146 pub fn stride_pixels(&self) -> Option<u32> {
148 let bpp = self.layout.bytes_per_pixel() as u32;
149 self.stride_bytes
150 .is_multiple_of(bpp)
151 .then(|| self.stride_bytes / bpp)
152 }
153}
154
155impl fmt::Display for FbInfo {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 write!(
158 f,
159 "{}x{} {}bpp {:?}, stride {} bytes",
160 self.size.width, self.size.height, self.bits_per_pixel, self.layout, self.stride_bytes
161 )
162 }
163}
164
165fn parse_virtual_size(text: &str) -> Result<Size, FbInfoError> {
167 let (w, h) = text.trim().split_once(',').ok_or(FbInfoError::Unparsable {
168 attribute: "virtual_size",
169 })?;
170 let width = w.trim().parse().map_err(|_| FbInfoError::Unparsable {
171 attribute: "virtual_size",
172 })?;
173 let height = h.trim().parse().map_err(|_| FbInfoError::Unparsable {
174 attribute: "virtual_size",
175 })?;
176 Ok(Size::new(width, height))
177}
178
179fn parse_modes(text: &str) -> Option<Size> {
184 let line = text.lines().next()?.trim();
187 let geometry = line.rsplit_once(':').map_or(line, |(_, rest)| rest);
188 let (width, rest) = geometry.split_once('x')?;
189
190 let width: u32 = width.trim().parse().ok()?;
191 let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
192 let height: u32 = digits.parse().ok()?;
193
194 (width > 0 && height > 0).then(|| Size::new(width, height))
195}
196
197#[derive(Clone, Copy, Debug, PartialEq, Eq)]
199#[non_exhaustive]
200pub enum FbInfoError {
201 Unparsable {
203 attribute: &'static str,
205 },
206
207 UnsupportedDepth {
209 bits_per_pixel: u32,
211 },
212
213 EmptyGeometry,
215
216 StrideTooNarrow {
218 stride_bytes: u32,
220 required: u32,
222 },
223}
224
225impl fmt::Display for FbInfoError {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 match self {
228 Self::Unparsable { attribute } => {
229 write!(f, "could not parse the {attribute} attribute")
230 }
231 Self::UnsupportedDepth { bits_per_pixel } => {
232 write!(f, "unsupported depth: {bits_per_pixel} bits per pixel")
233 }
234 Self::EmptyGeometry => f.write_str("the framebuffer has no visible area"),
235 Self::StrideTooNarrow {
236 stride_bytes,
237 required,
238 } => write!(
239 f,
240 "stride of {stride_bytes} bytes cannot hold a row needing {required}"
241 ),
242 }
243 }
244}
245
246impl core::error::Error for FbInfoError {}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
254 fn reads_a_real_devices_attributes() {
255 let info = FbInfo::from_sysfs("1280,800", "U:1280x800p-0", "5120", "32")
256 .expect("a real device should parse");
257 assert_eq!(info.size, Size::new(1280, 800));
258 assert_eq!(info.stride_bytes, 5120);
259 assert_eq!(info.layout, PixelLayout::Xrgb8888);
260 assert_eq!(info.stride_pixels(), Some(1280));
261 assert_eq!(info.required_bytes(), 1280 * 800 * 4);
262 }
263
264 #[test]
265 fn padded_stride_is_honoured() {
266 let info = FbInfo::from_sysfs("1366,768", "U:1366x768p-60", "5504", "32").expect("parses");
268 assert_eq!(info.stride_bytes, 5504);
269 assert_eq!(info.stride_pixels(), Some(1376));
270 assert_ne!(info.stride_pixels(), Some(info.size.width));
271 }
272
273 #[test]
274 fn visible_size_comes_from_modes_not_the_panning_allocation() {
275 let info = FbInfo::from_sysfs("1280,1600", "U:1280x800p-60", "5120", "32").expect("parses");
277 assert_eq!(info.size, Size::new(1280, 800));
278 }
279
280 #[test]
281 fn an_empty_modes_attribute_falls_back_to_virtual_size() {
282 let info = FbInfo::from_sysfs("800,480", "", "3200", "32").expect("parses");
283 assert_eq!(info.size, Size::new(800, 480));
284 }
285
286 #[test]
287 fn a_mode_larger_than_the_allocation_is_clamped() {
288 let info = FbInfo::from_sysfs("800,480", "U:1920x1080p-60", "3200", "32").expect("parses");
289 assert_eq!(info.size, Size::new(800, 480));
290 }
291
292 #[test]
293 fn sixteen_bit_panels_are_supported() {
294 let info = FbInfo::from_sysfs("480,320", "U:480x320p-60", "960", "16").expect("parses");
295 assert_eq!(info.layout, PixelLayout::Rgb565);
296 assert_eq!(info.required_bytes(), 480 * 320 * 2);
297 }
298
299 #[test]
300 fn odd_depths_are_rejected_rather_than_guessed_at() {
301 assert_eq!(
302 FbInfo::from_sysfs("640,480", "", "1920", "24"),
303 Err(FbInfoError::UnsupportedDepth { bits_per_pixel: 24 })
304 );
305 assert!(matches!(
306 FbInfo::from_sysfs("640,480", "", "640", "8"),
307 Err(FbInfoError::UnsupportedDepth { .. })
308 ));
309 }
310
311 #[test]
312 fn a_stride_too_narrow_for_a_row_is_rejected() {
313 assert!(matches!(
314 FbInfo::from_sysfs("1280,800", "", "2560", "32"),
315 Err(FbInfoError::StrideTooNarrow { .. })
316 ));
317 }
318
319 #[test]
320 fn rubbish_attributes_are_reported_by_name() {
321 assert_eq!(
322 FbInfo::from_sysfs("nonsense", "", "5120", "32"),
323 Err(FbInfoError::Unparsable {
324 attribute: "virtual_size"
325 })
326 );
327 assert_eq!(
328 FbInfo::from_sysfs("1280,800", "", "wide", "32"),
329 Err(FbInfoError::Unparsable {
330 attribute: "stride"
331 })
332 );
333 }
334
335 #[test]
336 fn a_zero_dimension_is_rejected() {
337 assert_eq!(
338 FbInfo::from_sysfs("1280,0", "", "5120", "32"),
339 Err(FbInfoError::EmptyGeometry)
340 );
341 }
342
343 #[test]
344 fn rgb565_conversion_keeps_the_extremes_exact() {
345 assert_eq!(PixelLayout::to_rgb565(0xFF00_0000), 0x0000);
346 assert_eq!(PixelLayout::to_rgb565(0xFFFF_FFFF), 0xFFFF);
347 assert_eq!(PixelLayout::to_rgb565(0xFFFF_0000), 0xF800);
348 assert_eq!(PixelLayout::to_rgb565(0xFF00_FF00), 0x07E0);
349 assert_eq!(PixelLayout::to_rgb565(0xFF00_00FF), 0x001F);
350 }
351
352 #[test]
353 fn rgb565_conversion_is_monotonic() {
354 let mut previous = 0u16;
356 for level in 0..=255u32 {
357 let word = 0xFF00_0000 | (level << 8);
358 let green = PixelLayout::to_rgb565(word) & 0x07E0;
359 assert!(green >= previous, "green went backwards at {level}");
360 previous = green;
361 }
362 }
363}