Skip to main content

ferroui_linuxframebuffer/output/
fbdev_output.rs

1//! The framebuffer device as an output (the port of `FbdevOutput.cs`):
2//! frames are drawn in software into the memory of the device.
3
4use super::fb_dev_back_buffer::{lock_mapped, FbDevBackBuffer, FbShared};
5use super::fb_device::IFbDevice;
6use super::{FbDevOutputOptions, IOutputBackend};
7use crate::native_unsafe_methods::{fb_bitfield, fb_fix_screeninfo, fb_var_screeninfo};
8use ferroui_base::platform::surfaces::{
9    FramebufferLockProperties, FuncFramebufferRenderTarget, IFramebufferPlatformSurface, IFramebufferRenderTarget,
10    IPlatformRenderSurface,
11};
12use ferroui_base::platform::{ILockedFramebuffer, PixelFormat, RenderTargetSceneInfo};
13use ferroui_base::{PixelSize, Vector};
14use std::any::Any;
15use std::cell::RefCell;
16use std::io;
17use std::rc::Rc;
18use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
19use std::sync::{Arc, Mutex, PoisonError};
20
21/// What the output and its render targets share.
22struct Inner {
23    fb: Arc<FbShared>,
24    fixed_info: fb_fix_screeninfo,
25    var_info: fb_var_screeninfo,
26    options: FbDevOutputOptions,
27    locked_at_least_once: AtomicBool,
28    /// The bits of the scaling, a floating point number.
29    scaling: AtomicU64,
30}
31
32impl Inner {
33    fn scaling(&self) -> f64 {
34        f64::from_bits(self.scaling.load(Ordering::Relaxed))
35    }
36}
37
38/// A framebuffer device as the output of the platform.
39pub struct FbdevOutput {
40    inner: Arc<Inner>,
41    id: String,
42}
43
44/// Writes the description of `format` into the variable information
45/// (`SetBpp`).
46pub(crate) fn set_bpp(var_info: &mut fb_var_screeninfo, format: PixelFormat) -> io::Result<()> {
47    let eight = fb_bitfield { offset: 0, length: 8, msb_right: 0 };
48    if format == PixelFormat::RGBA8888 {
49        var_info.bits_per_pixel = 32;
50        var_info.grayscale = 0;
51        var_info.red = eight;
52        var_info.blue = eight;
53        var_info.green = eight;
54        var_info.transp = eight;
55        var_info.green.offset = 8;
56        var_info.blue.offset = 16;
57        var_info.transp.offset = 24;
58    } else if format == PixelFormat::BGRA8888 {
59        var_info.bits_per_pixel = 32;
60        var_info.grayscale = 0;
61        var_info.red = eight;
62        var_info.blue = eight;
63        var_info.green = eight;
64        var_info.transp = eight;
65        var_info.green.offset = 8;
66        var_info.red.offset = 16;
67        var_info.transp.offset = 24;
68    } else if format == PixelFormat::RGB565 {
69        var_info.bits_per_pixel = 16;
70        var_info.grayscale = 0;
71        var_info.red = fb_bitfield::default();
72        var_info.blue = fb_bitfield::default();
73        var_info.green = fb_bitfield::default();
74        var_info.transp = fb_bitfield::default();
75        var_info.red.length = 5;
76        var_info.green.offset = 5;
77        var_info.green.length = 6;
78        var_info.blue.offset = 11;
79        var_info.blue.length = 5;
80    } else {
81        return Err(io::Error::new(io::ErrorKind::Unsupported, format!("Pixel format {format:?} is not supported")));
82    }
83    Ok(())
84}
85
86impl FbdevOutput {
87    /// Opens the framebuffer device `file_name`, or the one the
88    /// environment variable `FRAMEBUFFER` names, or `/dev/fb0`.
89    #[cfg(target_os = "linux")]
90    pub fn new(file_name: Option<&str>) -> io::Result<Arc<Self>> {
91        Self::with_format(file_name, None)
92    }
93
94    /// Opens a framebuffer device and asks it for `format`.
95    #[cfg(target_os = "linux")]
96    pub fn with_format(file_name: Option<&str>, format: Option<PixelFormat>) -> io::Result<Arc<Self>> {
97        Self::with_options(FbDevOutputOptions {
98            file_name: file_name.map(str::to_owned),
99            pixel_format: format,
100            ..Default::default()
101        })
102    }
103
104    /// Opens the framebuffer device of the options.
105    #[cfg(target_os = "linux")]
106    pub fn with_options(options: FbDevOutputOptions) -> io::Result<Arc<Self>> {
107        let file_name = options
108            .file_name
109            .clone()
110            .or_else(|| std::env::var("FRAMEBUFFER").ok())
111            .unwrap_or_else(|| "/dev/fb0".to_owned());
112        let device = super::LinuxFbDevice::open(&file_name)?;
113        Self::with_device(Box::new(device), options)
114    }
115
116    /// The output over a device that is already open. An addition: with a
117    /// [`FileFbDevice`](super::FileFbDevice) the output runs without a
118    /// framebuffer device.
119    pub fn with_device(mut device: Box<dyn IFbDevice>, options: FbDevOutputOptions) -> io::Result<Arc<Self>> {
120        // `Init`. A failure drops the device, which closes it.
121        let mut var_info = device.get_var_screen_info()?;
122
123        if let Some(format) = options.pixel_format {
124            set_bpp(&mut var_info, format)?;
125
126            if device.put_var_screen_info(&var_info).is_err() {
127                var_info.transp = fb_bitfield::default();
128            }
129
130            let _ = device.put_var_screen_info(&var_info);
131
132            var_info = device.get_var_screen_info()?;
133
134            if var_info.bits_per_pixel != 32 {
135                return Err(io::Error::other("Unable to set 32-bit display mode"));
136            }
137        }
138
139        let fixed_info = device.get_fix_screen_info()?;
140
141        let mapped_length = fixed_info.line_length as usize * var_info.yres as usize;
142        let mapping = device.map(mapped_length)?;
143
144        let id = fixed_info.id_string();
145        let scaling = options.scaling;
146        Ok(Arc::new(Self {
147            inner: Arc::new(Inner {
148                fb: Arc::new(FbShared {
149                    device: Mutex::new(Some(device)),
150                    mapping: Mutex::new(Some(Arc::new(mapping))),
151                }),
152                fixed_info,
153                var_info,
154                options,
155                locked_at_least_once: AtomicBool::new(false),
156                scaling: AtomicU64::new(scaling.to_bits()),
157            }),
158            id,
159        }))
160    }
161
162    /// The identification string of the device.
163    pub fn id(&self) -> &str {
164        &self.id
165    }
166
167    /// The variable information of the device the output draws with. An
168    /// addition, for diagnostics.
169    pub fn var_screen_info(&self) -> fb_var_screeninfo {
170        self.inner.var_info
171    }
172
173    /// The fixed information of the device the output draws with. An
174    /// addition, for diagnostics.
175    pub fn fix_screen_info(&self) -> fb_fix_screeninfo {
176        self.inner.fixed_info
177    }
178
179    /// Releases the mapped memory (once the frame that uses it has ended)
180    /// and closes the device.
181    pub fn dispose(&self) {
182        self.inner.fb.mapping.lock().unwrap_or_else(PoisonError::into_inner).take();
183        self.inner.fb.device.lock().unwrap_or_else(PoisonError::into_inner).take();
184    }
185}
186
187/// What a render target of the output keeps between frames: the back
188/// buffer, which the reference keeps in the output (`_backBuffer`).
189struct TargetState {
190    inner: Arc<Inner>,
191    back_buffer: RefCell<Option<FbDevBackBuffer>>,
192}
193
194impl TargetState {
195    fn lock(&self) -> (Rc<dyn ILockedFramebuffer>, FramebufferLockProperties) {
196        let inner = &self.inner;
197        let Some(mapping) = inner.fb.mapping() else {
198            panic!("Cannot access a disposed object: LinuxFramebuffer");
199        };
200
201        let dpi = Vector::new(96.0, 96.0) * inner.scaling();
202
203        if inner.options.render_directly_to_mapped_memory {
204            let properties = FramebufferLockProperties {
205                previous_frame_is_retained: inner.locked_at_least_once.swap(true, Ordering::Relaxed),
206            };
207            inner.fb.wait_for_vsync();
208            return (lock_mapped(mapping, &inner.var_info, &inner.fixed_info, dpi), properties);
209        }
210
211        let retained = inner.locked_at_least_once.swap(true, Ordering::Relaxed) && self.back_buffer.borrow().is_some();
212        let properties = FramebufferLockProperties { previous_frame_is_retained: retained };
213        let mut back_buffer = self.back_buffer.borrow_mut();
214        let back_buffer = back_buffer.get_or_insert_with(|| {
215            FbDevBackBuffer::new(
216                inner.fb.clone(),
217                inner.fixed_info,
218                inner.var_info,
219                inner.options.use_async_front_buffer_blit == Some(true),
220            )
221        });
222        (back_buffer.lock(dpi), properties)
223    }
224}
225
226impl IPlatformRenderSurface for FbdevOutput {
227    fn as_framebuffer_surface(&self) -> Option<&dyn IFramebufferPlatformSurface> {
228        Some(self)
229    }
230
231    fn as_any(&self) -> &dyn Any {
232        self
233    }
234}
235
236impl IFramebufferPlatformSurface for FbdevOutput {
237    fn create_framebuffer_render_target(&self) -> Rc<dyn IFramebufferRenderTarget> {
238        let state = TargetState { inner: self.inner.clone(), back_buffer: RefCell::new(None) };
239        Rc::new(FuncFramebufferRenderTarget::with_scene_info(move |_: &RenderTargetSceneInfo| state.lock(), true))
240    }
241}
242
243impl IOutputBackend for FbdevOutput {
244    fn pixel_size(&self) -> PixelSize {
245        let device = self.inner.fb.device.lock().unwrap_or_else(PoisonError::into_inner);
246        let Some(device) = device.as_ref() else {
247            panic!("Cannot access a disposed object: LinuxFramebuffer");
248        };
249        match device.get_var_screen_info() {
250            Ok(nfo) => PixelSize::new(nfo.xres as i32, nfo.yres as i32),
251            Err(error) => panic!("{error}"),
252        }
253    }
254
255    fn scaling(&self) -> f64 {
256        self.inner.scaling()
257    }
258
259    fn set_scaling(&self, value: f64) {
260        self.inner.scaling.store(value.to_bits(), Ordering::Relaxed);
261    }
262
263    fn as_render_surface(self: Arc<Self>) -> Arc<dyn IPlatformRenderSurface> {
264        self
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    // Not from the reference, which has no tests for this file. The
271    // device is a plain file.
272    use super::*;
273    use crate::output::FileFbDevice;
274
275    fn scene() -> RenderTargetSceneInfo {
276        RenderTargetSceneInfo::new(
277            PixelSize::new(1, 1),
278            1.0,
279            ferroui_base::rendering::composition::CompositionTransparencyLevel::None,
280        )
281    }
282
283    fn temp_file(name: &str) -> String {
284        let dir = std::env::temp_dir().join(format!("ferroui-linuxfb-{}", std::process::id()));
285        std::fs::create_dir_all(&dir).unwrap();
286        dir.join(name).to_string_lossy().into_owned()
287    }
288
289    fn var_info(width: u32, height: u32, format: PixelFormat) -> fb_var_screeninfo {
290        let mut var_info = fb_var_screeninfo { xres: width, yres: height, ..Default::default() };
291        set_bpp(&mut var_info, format).unwrap();
292        var_info
293    }
294
295    #[test]
296    fn set_bpp_writes_the_bit_fields_of_the_three_formats() {
297        let rgba = var_info(1, 1, PixelFormat::RGBA8888);
298        assert_eq!((rgba.bits_per_pixel, rgba.red.offset, rgba.green.offset, rgba.blue.offset), (32, 0, 8, 16));
299        assert_eq!((rgba.transp.offset, rgba.transp.length, rgba.red.length), (24, 8, 8));
300
301        let bgra = var_info(1, 1, PixelFormat::BGRA8888);
302        assert_eq!((bgra.bits_per_pixel, bgra.red.offset, bgra.green.offset, bgra.blue.offset), (32, 16, 8, 0));
303
304        // The fields as the reference writes them: red in the low bits.
305        let rgb565 = var_info(1, 1, PixelFormat::RGB565);
306        assert_eq!(rgb565.bits_per_pixel, 16);
307        assert_eq!((rgb565.red.offset, rgb565.red.length), (0, 5));
308        assert_eq!((rgb565.green.offset, rgb565.green.length), (5, 6));
309        assert_eq!((rgb565.blue.offset, rgb565.blue.length), (11, 5));
310        assert_eq!(rgb565.transp, fb_bitfield::default());
311
312        let mut other = fb_var_screeninfo::default();
313        assert!(set_bpp(&mut other, ferroui_base::platform::PixelFormats::GRAY8).is_err());
314    }
315
316    fn draw(output: &FbdevOutput, target: &Rc<dyn IFramebufferRenderTarget>, value: u8) -> FramebufferLockProperties {
317        let (fb, properties) = target.lock(&scene());
318        assert_eq!(fb.size(), output.pixel_size());
319        fb.with_data(&mut |data| data.fill(value));
320        fb.dispose();
321        properties
322    }
323
324    #[test]
325    fn a_frame_arrives_in_the_file_when_it_is_disposed() {
326        let path = temp_file("back-buffer.fb");
327        let device = FileFbDevice::create(&path, var_info(8, 4, PixelFormat::BGRA8888)).unwrap();
328        let output = FbdevOutput::with_device(Box::new(device), FbDevOutputOptions::default()).unwrap();
329        assert_eq!(output.id(), "file");
330        assert_eq!(output.pixel_size(), PixelSize::new(8, 4));
331
332        let target = output.create_framebuffer_render_target();
333        assert!(target.retains_frame_contents());
334
335        let (fb, properties) = target.lock(&scene());
336        assert!(!properties.previous_frame_is_retained);
337        assert_eq!((fb.row_bytes(), fb.format()), (32, PixelFormat::BGRA8888));
338        assert_eq!(fb.dpi(), Vector::new(96.0, 96.0));
339        fb.with_data(&mut |data| data.fill(0x5a));
340        // Nothing is on the device before the frame ends.
341        assert!(std::fs::read(&path).unwrap().iter().all(|&b| b == 0));
342        fb.dispose();
343        let content = std::fs::read(&path).unwrap();
344        assert_eq!(content.len(), 8 * 4 * 4);
345        assert!(content.iter().all(|&b| b == 0x5a));
346
347        // The second frame finds the first one in the back buffer.
348        let (fb, properties) = target.lock(&scene());
349        assert!(properties.previous_frame_is_retained);
350        fb.with_data(&mut |data| assert!(data.iter().all(|&b| b == 0x5a)));
351        // A frame that is dropped without being disposed is not shown.
352        fb.with_data(&mut |data| data.fill(0x11));
353        drop(fb);
354        assert!(std::fs::read(&path).unwrap().iter().all(|&b| b == 0x5a));
355
356        output.set_scaling(2.0);
357        let (fb, _) = target.lock(&scene());
358        assert_eq!(fb.dpi(), Vector::new(192.0, 192.0));
359        fb.dispose();
360        let _ = std::fs::remove_file(&path);
361    }
362
363    #[test]
364    fn a_frame_drawn_directly_is_in_the_file_at_once() {
365        let path = temp_file("direct.fb");
366        let device = FileFbDevice::create(&path, var_info(4, 2, PixelFormat::RGBA8888)).unwrap();
367        let options = FbDevOutputOptions { render_directly_to_mapped_memory: true, ..Default::default() };
368        let output = FbdevOutput::with_device(Box::new(device), options).unwrap();
369        let target = output.create_framebuffer_render_target();
370
371        let (fb, properties) = target.lock(&scene());
372        assert!(!properties.previous_frame_is_retained);
373        assert_eq!(fb.format(), PixelFormat::RGBA8888);
374        fb.with_data(&mut |data| data.fill(0x33));
375        assert!(std::fs::read(&path).unwrap().iter().all(|&b| b == 0x33));
376        fb.dispose();
377        assert!(draw(&output, &target, 0x44).previous_frame_is_retained);
378        let _ = std::fs::remove_file(&path);
379    }
380
381    #[test]
382    fn the_blit_thread_copies_the_frame_before_the_next_one_is_locked() {
383        let path = temp_file("async.fb");
384        let device = FileFbDevice::create(&path, var_info(16, 16, PixelFormat::BGRA8888)).unwrap();
385        let options = FbDevOutputOptions { use_async_front_buffer_blit: Some(true), ..Default::default() };
386        let output = FbdevOutput::with_device(Box::new(device), options).unwrap();
387        let target = output.create_framebuffer_render_target();
388        for value in 1..=5u8 {
389            draw(&output, &target, value);
390            // Locking waits for the transfer of the frame before.
391            let (fb, _) = target.lock(&scene());
392            assert!(std::fs::read(&path).unwrap().iter().all(|&b| b == value));
393            drop(fb);
394        }
395        drop(target);
396        let _ = std::fs::remove_file(&path);
397    }
398
399    #[test]
400    fn a_requested_format_is_put_and_one_of_16_bits_is_refused_as_the_reference_does() {
401        let path = temp_file("format.fb");
402        let device = FileFbDevice::create(&path, var_info(4, 4, PixelFormat::BGRA8888)).unwrap();
403        let options = FbDevOutputOptions { pixel_format: Some(PixelFormat::RGBA8888), ..Default::default() };
404        let output = FbdevOutput::with_device(Box::new(device), options).unwrap();
405        let (fb, _) = output.create_framebuffer_render_target().lock(&scene());
406        assert_eq!(fb.format(), PixelFormat::RGBA8888);
407        drop(fb);
408
409        let device = FileFbDevice::create(&path, var_info(4, 4, PixelFormat::BGRA8888)).unwrap();
410        let options = FbDevOutputOptions { pixel_format: Some(PixelFormat::RGB565), ..Default::default() };
411        let error = FbdevOutput::with_device(Box::new(device), options).err().unwrap();
412        assert_eq!(error.to_string(), "Unable to set 32-bit display mode");
413
414        // A device that already has 16 bits is drawn in them.
415        let device = FileFbDevice::create(&path, var_info(4, 4, PixelFormat::RGB565)).unwrap();
416        let output = FbdevOutput::with_device(Box::new(device), FbDevOutputOptions::default()).unwrap();
417        let (fb, _) = output.create_framebuffer_render_target().lock(&scene());
418        assert_eq!((fb.format(), fb.row_bytes()), (PixelFormat::RGB565, 8));
419        drop(fb);
420        let _ = std::fs::remove_file(&path);
421    }
422
423    #[test]
424    #[should_panic(expected = "Cannot access a disposed object: LinuxFramebuffer")]
425    fn a_disposed_output_cannot_be_locked() {
426        let path = temp_file("disposed.fb");
427        let device = FileFbDevice::create(&path, var_info(2, 2, PixelFormat::BGRA8888)).unwrap();
428        let output = FbdevOutput::with_device(Box::new(device), FbDevOutputOptions::default()).unwrap();
429        let target = output.create_framebuffer_render_target();
430        output.dispose();
431        let _ = std::fs::remove_file(&path);
432        let _ = target.lock(&scene());
433    }
434}