1use 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
21struct 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 scaling: AtomicU64,
30}
31
32impl Inner {
33 fn scaling(&self) -> f64 {
34 f64::from_bits(self.scaling.load(Ordering::Relaxed))
35 }
36}
37
38pub struct FbdevOutput {
40 inner: Arc<Inner>,
41 id: String,
42}
43
44pub(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 #[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 #[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 #[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 pub fn with_device(mut device: Box<dyn IFbDevice>, options: FbDevOutputOptions) -> io::Result<Arc<Self>> {
120 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 pub fn id(&self) -> &str {
164 &self.id
165 }
166
167 pub fn var_screen_info(&self) -> fb_var_screeninfo {
170 self.inner.var_info
171 }
172
173 pub fn fix_screen_info(&self) -> fb_fix_screeninfo {
176 self.inner.fixed_info
177 }
178
179 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
187struct 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 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 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 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 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 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 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 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}