use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use denise::{BufferAge, Frame, PixelFormat, Rect, Size, Surface, SurfaceError};
use memmap2::{MmapMut, MmapOptions};
use crate::error::FbdevError;
use crate::info::{FbInfo, PixelLayout};
const DEV_DIR: &str = "/dev";
const SYSFS_DIR: &str = "/sys/class/graphics";
#[derive(Debug)]
pub struct FbdevSurface {
map: MmapMut,
shadow: Vec<u32>,
info: FbInfo,
path: PathBuf,
first_frame: bool,
}
impl FbdevSurface {
pub fn open(path: impl AsRef<Path>) -> Result<Self, FbdevError> {
let path = path.as_ref().to_path_buf();
let name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or(FbdevError::NoDevice)?;
let info = read_info(name)?;
let file = OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.map_err(|source| FbdevError::Open {
path: path.clone(),
source,
})?;
let required = info.required_bytes();
let map =
unsafe { MmapOptions::new().len(required).map_mut(&file) }.map_err(FbdevError::Map)?;
if map.len() < required {
return Err(FbdevError::TooSmall {
required,
actual: map.len(),
});
}
Ok(Self {
map,
shadow: vec![0; info.size.width as usize * info.size.height as usize],
info,
path,
first_frame: true,
})
}
pub fn open_first() -> Result<Self, FbdevError> {
let dir = std::fs::read_dir(DEV_DIR).map_err(|source| FbdevError::Sysfs {
path: PathBuf::from(DEV_DIR),
source,
})?;
let mut nodes: Vec<PathBuf> = dir
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
name.strip_prefix("fb").is_some_and(|rest| {
!rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit())
})
})
})
.collect();
nodes.sort();
let mut last = None;
for path in nodes {
match Self::open(&path) {
Ok(surface) => return Ok(surface),
Err(err) => last = Some(err),
}
}
Err(last.unwrap_or(FbdevError::NoDevice))
}
pub fn info(&self) -> FbInfo {
self.info
}
pub fn path(&self) -> &Path {
&self.path
}
fn publish(&mut self, region: Rect) {
let Some(r) = region.clip_to_size(self.info.size) else {
return;
};
let width = self.info.size.width as usize;
let bpp = self.info.layout.bytes_per_pixel();
let stride = self.info.stride_bytes as usize;
let x0 = r.x as usize;
let run = r.width as usize;
for y in r.y as usize..r.bottom() as usize {
let src = &self.shadow[y * width + x0..y * width + x0 + run];
let start = y * stride + x0 * bpp;
match self.info.layout {
PixelLayout::Xrgb8888 => {
let bytes: &[u8] = bytemuck::cast_slice(src);
self.map[start..start + bytes.len()].copy_from_slice(bytes);
}
PixelLayout::Rgb565 => {
let dst = &mut self.map[start..start + run * 2];
for (pixel, out) in src.iter().zip(dst.chunks_exact_mut(2)) {
out.copy_from_slice(&PixelLayout::to_rgb565(*pixel).to_le_bytes());
}
}
}
}
}
}
impl Surface for FbdevSurface {
fn size(&self) -> Size {
self.info.size
}
fn scale_factor(&self) -> f32 {
1.0
}
fn format(&self) -> PixelFormat {
PixelFormat::Xrgb8888
}
fn acquire(&mut self) -> Result<Frame<'_>, SurfaceError> {
let size = self.info.size;
let age = if self.first_frame {
BufferAge::Undefined
} else {
BufferAge::Frames(1)
};
Frame::new(
&mut self.shadow,
size,
size.width,
PixelFormat::Xrgb8888,
age,
)
}
fn present(&mut self, damage: &[Rect]) -> Result<(), SurfaceError> {
if self.first_frame {
self.first_frame = false;
self.publish(Rect::from_size(self.info.size));
return Ok(());
}
for region in damage {
self.publish(*region);
}
Ok(())
}
}
fn read_info(name: &str) -> Result<FbInfo, FbdevError> {
let base = Path::new(SYSFS_DIR).join(name);
let read = |attribute: &str| -> Result<String, FbdevError> {
let path = base.join(attribute);
std::fs::read_to_string(&path).map_err(|source| FbdevError::Sysfs { path, source })
};
let modes = read("modes").unwrap_or_default();
Ok(FbInfo::from_sysfs(
&read("virtual_size")?,
&modes,
&read("stride")?,
&read("bits_per_pixel")?,
)?)
}