Skip to main content

denise_fbdev/
surface.rs

1//! The framebuffer surface: map it, draw into a shadow, publish the damage.
2
3use std::fs::OpenOptions;
4use std::path::{Path, PathBuf};
5
6use denise::{BufferAge, Frame, PixelFormat, Rect, Size, Surface, SurfaceError};
7use memmap2::{MmapMut, MmapOptions};
8
9use crate::error::FbdevError;
10use crate::info::{FbInfo, PixelLayout};
11
12/// Where framebuffer nodes live.
13const DEV_DIR: &str = "/dev";
14/// Where their attributes live.
15const SYSFS_DIR: &str = "/sys/class/graphics";
16
17/// A framebuffer, drawn into through a shadow buffer.
18///
19/// # Why a shadow buffer
20///
21/// The mapped framebuffer is being scanned out continuously; there is nothing to
22/// flip to. Rendering into it directly would put every intermediate state on
23/// screen — the clear before the redraw above all — which reads as flicker on
24/// exactly the panels this backend exists for. So drawing goes to memory we own
25/// and [`present`](Surface::present) copies out only the damaged rows.
26///
27/// That also makes the buffer age honest: the shadow persists frame to frame, so
28/// it is [`BufferAge::Frames(1)`](BufferAge::Frames) and incremental repaint works
29/// exactly as it does on DRM.
30///
31/// # What this cannot do
32///
33/// There is no page flip and no vsync. A copy that lands mid-scanout will tear,
34/// and nothing here can prevent it — `FBIO_WAITFORVSYNC` is optional and widely
35/// unimplemented. Keeping damage small keeps the tear small, which is the only
36/// mitigation fbdev offers.
37#[derive(Debug)]
38pub struct FbdevSurface {
39    map: MmapMut,
40    shadow: Vec<u32>,
41    info: FbInfo,
42    path: PathBuf,
43    /// Set until the first present, so the whole surface is published once.
44    first_frame: bool,
45}
46
47impl FbdevSurface {
48    /// Opens a specific node, such as `/dev/fb0`.
49    pub fn open(path: impl AsRef<Path>) -> Result<Self, FbdevError> {
50        let path = path.as_ref().to_path_buf();
51        let name = path
52            .file_name()
53            .and_then(|n| n.to_str())
54            .ok_or(FbdevError::NoDevice)?;
55
56        let info = read_info(name)?;
57
58        let file = OpenOptions::new()
59            .read(true)
60            .write(true)
61            .open(&path)
62            .map_err(|source| FbdevError::Open {
63                path: path.clone(),
64                source,
65            })?;
66
67        // The length has to be given explicitly. A device node reports a size of
68        // zero from `stat`, so letting memmap2 infer it maps nothing at all.
69        let required = info.required_bytes();
70
71        // SAFETY: a framebuffer is device memory whose extent is fixed by the mode,
72        // and `required` is derived from the geometry the driver just reported, so
73        // the mapping cannot outrun it. The usual hazard of mapping a file —
74        // another process truncating it underneath us — does not apply to a device
75        // node. Nothing else in this process maps it.
76        let map =
77            unsafe { MmapOptions::new().len(required).map_mut(&file) }.map_err(FbdevError::Map)?;
78
79        if map.len() < required {
80            return Err(FbdevError::TooSmall {
81                required,
82                actual: map.len(),
83            });
84        }
85
86        Ok(Self {
87            map,
88            shadow: vec![0; info.size.width as usize * info.size.height as usize],
89            info,
90            path,
91            first_frame: true,
92        })
93    }
94
95    /// Opens the first framebuffer node that can be read and understood.
96    pub fn open_first() -> Result<Self, FbdevError> {
97        let dir = std::fs::read_dir(DEV_DIR).map_err(|source| FbdevError::Sysfs {
98            path: PathBuf::from(DEV_DIR),
99            source,
100        })?;
101
102        let mut nodes: Vec<PathBuf> = dir
103            .filter_map(Result::ok)
104            .map(|entry| entry.path())
105            .filter(|path| {
106                path.file_name()
107                    .and_then(|name| name.to_str())
108                    .is_some_and(|name| {
109                        name.strip_prefix("fb").is_some_and(|rest| {
110                            !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit())
111                        })
112                    })
113            })
114            .collect();
115        nodes.sort();
116
117        let mut last = None;
118        for path in nodes {
119            match Self::open(&path) {
120                Ok(surface) => return Ok(surface),
121                Err(err) => last = Some(err),
122            }
123        }
124        Err(last.unwrap_or(FbdevError::NoDevice))
125    }
126
127    /// The geometry in force.
128    pub fn info(&self) -> FbInfo {
129        self.info
130    }
131
132    /// The node this surface was opened from.
133    pub fn path(&self) -> &Path {
134        &self.path
135    }
136
137    /// Copies one damaged region from the shadow into the framebuffer.
138    fn publish(&mut self, region: Rect) {
139        let Some(r) = region.clip_to_size(self.info.size) else {
140            return;
141        };
142
143        let width = self.info.size.width as usize;
144        let bpp = self.info.layout.bytes_per_pixel();
145        let stride = self.info.stride_bytes as usize;
146        let x0 = r.x as usize;
147        let run = r.width as usize;
148
149        for y in r.y as usize..r.bottom() as usize {
150            let src = &self.shadow[y * width + x0..y * width + x0 + run];
151            let start = y * stride + x0 * bpp;
152
153            match self.info.layout {
154                PixelLayout::Xrgb8888 => {
155                    // Same word layout on both sides, so this is a plain copy.
156                    let bytes: &[u8] = bytemuck::cast_slice(src);
157                    self.map[start..start + bytes.len()].copy_from_slice(bytes);
158                }
159                PixelLayout::Rgb565 => {
160                    let dst = &mut self.map[start..start + run * 2];
161                    for (pixel, out) in src.iter().zip(dst.chunks_exact_mut(2)) {
162                        out.copy_from_slice(&PixelLayout::to_rgb565(*pixel).to_le_bytes());
163                    }
164                }
165            }
166        }
167    }
168}
169
170impl Surface for FbdevSurface {
171    fn size(&self) -> Size {
172        self.info.size
173    }
174
175    fn scale_factor(&self) -> f32 {
176        1.0
177    }
178
179    fn format(&self) -> PixelFormat {
180        // Always what the shadow is, whatever the panel turns out to want.
181        PixelFormat::Xrgb8888
182    }
183
184    fn acquire(&mut self) -> Result<Frame<'_>, SurfaceError> {
185        let size = self.info.size;
186        let age = if self.first_frame {
187            BufferAge::Undefined
188        } else {
189            // One shadow, kept between frames: exactly one frame stale.
190            BufferAge::Frames(1)
191        };
192
193        Frame::new(
194            &mut self.shadow,
195            size,
196            size.width,
197            PixelFormat::Xrgb8888,
198            age,
199        )
200    }
201
202    fn present(&mut self, damage: &[Rect]) -> Result<(), SurfaceError> {
203        if self.first_frame {
204            // The framebuffer holds whatever the console left behind.
205            self.first_frame = false;
206            self.publish(Rect::from_size(self.info.size));
207            return Ok(());
208        }
209
210        for region in damage {
211            self.publish(*region);
212        }
213        Ok(())
214    }
215}
216
217/// Reads a device's geometry from `/sys/class/graphics/<name>/`.
218fn read_info(name: &str) -> Result<FbInfo, FbdevError> {
219    let base = Path::new(SYSFS_DIR).join(name);
220    let read = |attribute: &str| -> Result<String, FbdevError> {
221        let path = base.join(attribute);
222        std::fs::read_to_string(&path).map_err(|source| FbdevError::Sysfs { path, source })
223    };
224
225    // `modes` is optional: an empty one falls back to `virtual_size`.
226    let modes = read("modes").unwrap_or_default();
227
228    Ok(FbInfo::from_sysfs(
229        &read("virtual_size")?,
230        &modes,
231        &read("stride")?,
232        &read("bits_per_pixel")?,
233    )?)
234}