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, thiserror::Error)]
199#[non_exhaustive]
200pub enum FbInfoError {
201 #[error("could not parse the {attribute} attribute")]
203 Unparsable {
204 attribute: &'static str,
206 },
207
208 #[error("unsupported depth: {bits_per_pixel} bits per pixel")]
210 UnsupportedDepth {
211 bits_per_pixel: u32,
213 },
214
215 #[error("the framebuffer has no visible area")]
217 EmptyGeometry,
218
219 #[error("stride of {stride_bytes} bytes cannot hold a row needing {required}")]
221 StrideTooNarrow {
222 stride_bytes: u32,
224 required: u32,
226 },
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
235 fn reads_a_real_devices_attributes() {
236 let info = FbInfo::from_sysfs("1280,800", "U:1280x800p-0", "5120", "32")
237 .expect("a real device should parse");
238 assert_eq!(info.size, Size::new(1280, 800));
239 assert_eq!(info.stride_bytes, 5120);
240 assert_eq!(info.layout, PixelLayout::Xrgb8888);
241 assert_eq!(info.stride_pixels(), Some(1280));
242 assert_eq!(info.required_bytes(), 1280 * 800 * 4);
243 }
244
245 #[test]
246 fn padded_stride_is_honoured() {
247 let info = FbInfo::from_sysfs("1366,768", "U:1366x768p-60", "5504", "32").expect("parses");
249 assert_eq!(info.stride_bytes, 5504);
250 assert_eq!(info.stride_pixels(), Some(1376));
251 assert_ne!(info.stride_pixels(), Some(info.size.width));
252 }
253
254 #[test]
255 fn visible_size_comes_from_modes_not_the_panning_allocation() {
256 let info = FbInfo::from_sysfs("1280,1600", "U:1280x800p-60", "5120", "32").expect("parses");
258 assert_eq!(info.size, Size::new(1280, 800));
259 }
260
261 #[test]
262 fn an_empty_modes_attribute_falls_back_to_virtual_size() {
263 let info = FbInfo::from_sysfs("800,480", "", "3200", "32").expect("parses");
264 assert_eq!(info.size, Size::new(800, 480));
265 }
266
267 #[test]
268 fn a_mode_larger_than_the_allocation_is_clamped() {
269 let info = FbInfo::from_sysfs("800,480", "U:1920x1080p-60", "3200", "32").expect("parses");
270 assert_eq!(info.size, Size::new(800, 480));
271 }
272
273 #[test]
274 fn sixteen_bit_panels_are_supported() {
275 let info = FbInfo::from_sysfs("480,320", "U:480x320p-60", "960", "16").expect("parses");
276 assert_eq!(info.layout, PixelLayout::Rgb565);
277 assert_eq!(info.required_bytes(), 480 * 320 * 2);
278 }
279
280 #[test]
281 fn odd_depths_are_rejected_rather_than_guessed_at() {
282 assert_eq!(
283 FbInfo::from_sysfs("640,480", "", "1920", "24"),
284 Err(FbInfoError::UnsupportedDepth { bits_per_pixel: 24 })
285 );
286 assert!(matches!(
287 FbInfo::from_sysfs("640,480", "", "640", "8"),
288 Err(FbInfoError::UnsupportedDepth { .. })
289 ));
290 }
291
292 #[test]
293 fn a_stride_too_narrow_for_a_row_is_rejected() {
294 assert!(matches!(
295 FbInfo::from_sysfs("1280,800", "", "2560", "32"),
296 Err(FbInfoError::StrideTooNarrow { .. })
297 ));
298 }
299
300 #[test]
301 fn rubbish_attributes_are_reported_by_name() {
302 assert_eq!(
303 FbInfo::from_sysfs("nonsense", "", "5120", "32"),
304 Err(FbInfoError::Unparsable {
305 attribute: "virtual_size"
306 })
307 );
308 assert_eq!(
309 FbInfo::from_sysfs("1280,800", "", "wide", "32"),
310 Err(FbInfoError::Unparsable {
311 attribute: "stride"
312 })
313 );
314 }
315
316 #[test]
317 fn a_zero_dimension_is_rejected() {
318 assert_eq!(
319 FbInfo::from_sysfs("1280,0", "", "5120", "32"),
320 Err(FbInfoError::EmptyGeometry)
321 );
322 }
323
324 #[test]
325 fn rgb565_conversion_keeps_the_extremes_exact() {
326 assert_eq!(PixelLayout::to_rgb565(0xFF00_0000), 0x0000);
327 assert_eq!(PixelLayout::to_rgb565(0xFFFF_FFFF), 0xFFFF);
328 assert_eq!(PixelLayout::to_rgb565(0xFFFF_0000), 0xF800);
329 assert_eq!(PixelLayout::to_rgb565(0xFF00_FF00), 0x07E0);
330 assert_eq!(PixelLayout::to_rgb565(0xFF00_00FF), 0x001F);
331 }
332
333 #[test]
334 fn rgb565_conversion_is_monotonic() {
335 let mut previous = 0u16;
337 for level in 0..=255u32 {
338 let word = 0xFF00_0000 | (level << 8);
339 let green = PixelLayout::to_rgb565(word) & 0x07E0;
340 assert!(green >= previous, "green went backwards at {level}");
341 previous = green;
342 }
343 }
344}