mirror of
https://github.com/Astatin3/CC2.git
synced 2026-06-09 00:18:00 -06:00
Add doom project (very important)
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "cc2-doom"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2.177"
|
||||
memmap2 = "0.9.10"
|
||||
|
||||
[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies]
|
||||
pixels = "0.15.0"
|
||||
winit = "0.29.15"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.2.43"
|
||||
@@ -0,0 +1,16 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=src/doom.c");
|
||||
println!("cargo:rerun-if-changed=src/PureDOOM.h");
|
||||
|
||||
cc::Build::new()
|
||||
.file("src/doom.c")
|
||||
.std("gnu99")
|
||||
.define("DOOM_IMPLEMENT_PRINT", None)
|
||||
.define("DOOM_IMPLEMENT_MALLOC", None)
|
||||
.define("DOOM_IMPLEMENT_FILE_IO", None)
|
||||
.define("DOOM_IMPLEMENT_GETTIME", None)
|
||||
.define("DOOM_IMPLEMENT_EXIT", None)
|
||||
.define("DOOM_IMPLEMENT_GETENV", None)
|
||||
.warnings(false)
|
||||
.compile("doom");
|
||||
}
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
cross build --release --target armv7-unknown-linux-musleabihf
|
||||
sshpass -p MTY4ODE2 scp ./target/armv7-unknown-linux-musleabihf/release/cc2-doom root@192.168.0.110:/root/
|
||||
Binary file not shown.
+48602
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
#define DOOM_IMPLEMENTATION
|
||||
#include "PureDOOM.h"
|
||||
@@ -0,0 +1,441 @@
|
||||
use std::error::Error;
|
||||
use std::ffi::CString;
|
||||
use std::fs;
|
||||
use std::os::raw::{c_char, c_int, c_uchar};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const DOOM_WIDTH: u32 = 320;
|
||||
const DOOM_HEIGHT: u32 = 200;
|
||||
const DOOM_FLAGS_HIDE_MOUSE_OPTIONS: c_int = 1;
|
||||
const DOOM_FLAGS_HIDE_SOUND_OPTIONS: c_int = 2;
|
||||
const DOOM_FLAGS_HIDE_MUSIC_OPTIONS: c_int = 4;
|
||||
const EMBEDDED_DOOM1_WAD: &[u8] = include_bytes!("../doom1.wad");
|
||||
|
||||
unsafe extern "C" {
|
||||
fn doom_set_resolution(width: c_int, height: c_int);
|
||||
fn doom_init(argc: c_int, argv: *mut *mut c_char, flags: c_int);
|
||||
fn doom_update();
|
||||
fn doom_get_framebuffer(channels: c_int) -> *const c_uchar;
|
||||
#[cfg(all(target_os = "linux", target_arch = "arm"))]
|
||||
fn doom_key_down(key: c_int);
|
||||
#[cfg(all(target_os = "linux", target_arch = "arm"))]
|
||||
fn doom_key_up(key: c_int);
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Box<dyn Error>> {
|
||||
let mut doom = Doom::new()?;
|
||||
video::run(&mut doom)
|
||||
}
|
||||
|
||||
pub struct Doom {
|
||||
_args: Vec<CString>,
|
||||
_argv: Vec<*mut c_char>,
|
||||
}
|
||||
|
||||
impl Doom {
|
||||
fn new() -> Result<Self, Box<dyn Error>> {
|
||||
let args = args_with_embedded_iwad()?;
|
||||
let mut args = args
|
||||
.into_iter()
|
||||
.map(|arg| CString::new(arg).map_err(|err| -> Box<dyn Error> { Box::new(err) }))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut argv = args
|
||||
.iter_mut()
|
||||
.map(|arg| arg.as_ptr() as *mut c_char)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
unsafe {
|
||||
doom_set_resolution(DOOM_WIDTH as c_int, DOOM_HEIGHT as c_int);
|
||||
doom_init(
|
||||
argv.len() as c_int,
|
||||
argv.as_mut_ptr(),
|
||||
DOOM_FLAGS_HIDE_MOUSE_OPTIONS
|
||||
| DOOM_FLAGS_HIDE_SOUND_OPTIONS
|
||||
| DOOM_FLAGS_HIDE_MUSIC_OPTIONS,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
_args: args,
|
||||
_argv: argv,
|
||||
})
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
unsafe {
|
||||
doom_update();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "arm"))]
|
||||
fn key_down(&mut self, key: c_int) {
|
||||
unsafe {
|
||||
doom_key_down(key);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "arm"))]
|
||||
fn key_up(&mut self, key: c_int) {
|
||||
unsafe {
|
||||
doom_key_up(key);
|
||||
}
|
||||
}
|
||||
|
||||
fn framebuffer_rgb(&self) -> &'static [u8] {
|
||||
let ptr = unsafe { doom_get_framebuffer(3) };
|
||||
assert!(!ptr.is_null(), "Doom returned a null framebuffer");
|
||||
unsafe { std::slice::from_raw_parts(ptr, (DOOM_WIDTH * DOOM_HEIGHT * 3) as usize) }
|
||||
}
|
||||
}
|
||||
|
||||
fn args_with_embedded_iwad() -> Result<Vec<String>, Box<dyn Error>> {
|
||||
let mut args = std::env::args().collect::<Vec<_>>();
|
||||
if has_iwad_arg(&args) {
|
||||
return Ok(args);
|
||||
}
|
||||
|
||||
let iwad = embedded_iwad_path();
|
||||
if fs::read(&iwad).ok().as_deref() != Some(EMBEDDED_DOOM1_WAD) {
|
||||
fs::write(&iwad, EMBEDDED_DOOM1_WAD)?;
|
||||
}
|
||||
|
||||
args.push("-iwad".to_string());
|
||||
args.push(iwad.to_string_lossy().into_owned());
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn has_iwad_arg(args: &[String]) -> bool {
|
||||
args.iter().any(|arg| arg.eq_ignore_ascii_case("-iwad"))
|
||||
}
|
||||
|
||||
fn embedded_iwad_path() -> PathBuf {
|
||||
std::env::temp_dir().join("cc2-doom-doom1.wad")
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "arm"))]
|
||||
mod video {
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::mem;
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
use crate::doom::{DOOM_HEIGHT, DOOM_WIDTH, Doom};
|
||||
use crate::framebuffer::FrameBuffer;
|
||||
|
||||
const DOOM_KEY_TAB: i32 = 9;
|
||||
const DOOM_KEY_ENTER: i32 = 13;
|
||||
const DOOM_KEY_ESCAPE: i32 = 27;
|
||||
const DOOM_KEY_SPACE: i32 = 32;
|
||||
const DOOM_KEY_APOSTROPHE: i32 = b'\'' as i32;
|
||||
const DOOM_KEY_COMMA: i32 = b',' as i32;
|
||||
const DOOM_KEY_MINUS: i32 = b'-' as i32;
|
||||
const DOOM_KEY_PERIOD: i32 = b'.' as i32;
|
||||
const DOOM_KEY_SLASH: i32 = b'/' as i32;
|
||||
const DOOM_KEY_SEMICOLON: i32 = b';' as i32;
|
||||
const DOOM_KEY_EQUALS: i32 = b'=' as i32;
|
||||
const DOOM_KEY_LEFT_BRACKET: i32 = b'[' as i32;
|
||||
const DOOM_KEY_RIGHT_BRACKET: i32 = b']' as i32;
|
||||
const DOOM_KEY_BACKSPACE: i32 = 127;
|
||||
const DOOM_KEY_CTRL: i32 = 0x80 + 0x1d;
|
||||
const DOOM_KEY_LEFT_ARROW: i32 = 0xac;
|
||||
const DOOM_KEY_UP_ARROW: i32 = 0xad;
|
||||
const DOOM_KEY_RIGHT_ARROW: i32 = 0xae;
|
||||
const DOOM_KEY_DOWN_ARROW: i32 = 0xaf;
|
||||
const DOOM_KEY_SHIFT: i32 = 0x80 + 0x36;
|
||||
const DOOM_KEY_ALT: i32 = 0x80 + 0x38;
|
||||
const DOOM_KEY_F1: i32 = 0x80 + 0x3b;
|
||||
const DOOM_KEY_F2: i32 = 0x80 + 0x3c;
|
||||
const DOOM_KEY_F3: i32 = 0x80 + 0x3d;
|
||||
const DOOM_KEY_F4: i32 = 0x80 + 0x3e;
|
||||
const DOOM_KEY_F5: i32 = 0x80 + 0x3f;
|
||||
const DOOM_KEY_F6: i32 = 0x80 + 0x40;
|
||||
const DOOM_KEY_F7: i32 = 0x80 + 0x41;
|
||||
const DOOM_KEY_F8: i32 = 0x80 + 0x42;
|
||||
const DOOM_KEY_F9: i32 = 0x80 + 0x43;
|
||||
const DOOM_KEY_F10: i32 = 0x80 + 0x44;
|
||||
const DOOM_KEY_F11: i32 = 0x80 + 0x57;
|
||||
const DOOM_KEY_F12: i32 = 0x80 + 0x58;
|
||||
const DOOM_KEY_PAUSE: i32 = 0xff;
|
||||
|
||||
pub fn run(doom: &mut Doom) -> Result<(), Box<dyn Error>> {
|
||||
let mut fb = FrameBuffer::new("/dev/fb0")?;
|
||||
let mut keyboard = StdinKeyboard::new()?;
|
||||
let offset_x = ((fb.xres as i32) - (DOOM_WIDTH as i32)) / 2;
|
||||
let offset_y = ((fb.yres as i32) - (DOOM_HEIGHT as i32)) / 2;
|
||||
|
||||
loop {
|
||||
keyboard.poll(doom)?;
|
||||
doom.update();
|
||||
fb.display_frame_at(
|
||||
doom.framebuffer_rgb(),
|
||||
DOOM_WIDTH,
|
||||
DOOM_HEIGHT,
|
||||
offset_x,
|
||||
offset_y,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
struct StdinKeyboard {
|
||||
fd: RawFd,
|
||||
original_termios: Option<libc::termios>,
|
||||
pressed: Vec<i32>,
|
||||
}
|
||||
|
||||
impl StdinKeyboard {
|
||||
fn new() -> io::Result<Self> {
|
||||
let fd = libc::STDIN_FILENO;
|
||||
let original_termios = set_raw_nonblocking(fd)?;
|
||||
Ok(Self {
|
||||
fd,
|
||||
original_termios,
|
||||
pressed: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn poll(&mut self, doom: &mut Doom) -> io::Result<()> {
|
||||
let mut bytes = [0u8; 64];
|
||||
let len = read_available(self.fd, &mut bytes)?;
|
||||
if len == 0 {
|
||||
self.release_all(doom);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.release_all(doom);
|
||||
|
||||
let mut i = 0;
|
||||
while i < len {
|
||||
let (key, consumed) = parse_key(&bytes[i..len]);
|
||||
i += consumed.max(1);
|
||||
|
||||
if let Some(key) = key {
|
||||
doom.key_down(key);
|
||||
self.pressed.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release_all(&mut self, doom: &mut Doom) {
|
||||
for key in self.pressed.drain(..) {
|
||||
doom.key_up(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StdinKeyboard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(termios) = self.original_termios {
|
||||
unsafe {
|
||||
libc::tcsetattr(self.fd, libc::TCSANOW, &termios);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_raw_nonblocking(fd: RawFd) -> io::Result<Option<libc::termios>> {
|
||||
let mut termios = unsafe { mem::zeroed::<libc::termios>() };
|
||||
let has_tty = unsafe { libc::tcgetattr(fd, &mut termios) } == 0;
|
||||
let original = has_tty.then_some(termios);
|
||||
|
||||
if has_tty {
|
||||
let mut raw = termios;
|
||||
raw.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
|
||||
raw.c_iflag &= !(libc::IXON | libc::ICRNL);
|
||||
raw.c_cc[libc::VMIN] = 0;
|
||||
raw.c_cc[libc::VTIME] = 0;
|
||||
|
||||
if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) } < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
|
||||
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
|
||||
if flags < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
if unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
Ok(original)
|
||||
}
|
||||
|
||||
fn read_available(fd: RawFd, out: &mut [u8]) -> io::Result<usize> {
|
||||
let result = unsafe { libc::read(fd, out.as_mut_ptr() as *mut libc::c_void, out.len()) };
|
||||
if result < 0 {
|
||||
let err = io::Error::last_os_error();
|
||||
return match err.kind() {
|
||||
io::ErrorKind::WouldBlock => Ok(0),
|
||||
_ => Err(err),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(result as usize)
|
||||
}
|
||||
|
||||
fn parse_key(bytes: &[u8]) -> (Option<i32>, usize) {
|
||||
if bytes.is_empty() {
|
||||
return (None, 0);
|
||||
}
|
||||
|
||||
if bytes[0] == b'\x1b' {
|
||||
return parse_escape(bytes);
|
||||
}
|
||||
|
||||
(byte_to_doom_key(bytes[0]), 1)
|
||||
}
|
||||
|
||||
fn parse_escape(bytes: &[u8]) -> (Option<i32>, usize) {
|
||||
if bytes.len() >= 3 && bytes[1] == b'[' {
|
||||
return match bytes[2] {
|
||||
b'A' => (Some(DOOM_KEY_UP_ARROW), 3),
|
||||
b'B' => (Some(DOOM_KEY_DOWN_ARROW), 3),
|
||||
b'C' => (Some(DOOM_KEY_RIGHT_ARROW), 3),
|
||||
b'D' => (Some(DOOM_KEY_LEFT_ARROW), 3),
|
||||
b'Z' => (Some(DOOM_KEY_TAB), 3),
|
||||
b'1' | b'2' | b'3' | b'4' | b'5' | b'6' if bytes.len() >= 4 => {
|
||||
parse_csi_tilde(bytes)
|
||||
}
|
||||
_ => (Some(DOOM_KEY_ESCAPE), 1),
|
||||
};
|
||||
}
|
||||
|
||||
if bytes.len() >= 3 && bytes[1] == b'O' {
|
||||
return match bytes[2] {
|
||||
b'P' => (Some(DOOM_KEY_F1), 3),
|
||||
b'Q' => (Some(DOOM_KEY_F2), 3),
|
||||
b'R' => (Some(DOOM_KEY_F3), 3),
|
||||
b'S' => (Some(DOOM_KEY_F4), 3),
|
||||
_ => (Some(DOOM_KEY_ESCAPE), 1),
|
||||
};
|
||||
}
|
||||
|
||||
(Some(DOOM_KEY_ESCAPE), 1)
|
||||
}
|
||||
|
||||
fn parse_csi_tilde(bytes: &[u8]) -> (Option<i32>, usize) {
|
||||
let tilde = bytes.iter().take(6).position(|&byte| byte == b'~');
|
||||
let Some(tilde) = tilde else {
|
||||
return (Some(DOOM_KEY_ESCAPE), 1);
|
||||
};
|
||||
|
||||
let key = match &bytes[2..tilde] {
|
||||
b"15" => Some(DOOM_KEY_F5),
|
||||
b"17" => Some(DOOM_KEY_F6),
|
||||
b"18" => Some(DOOM_KEY_F7),
|
||||
b"19" => Some(DOOM_KEY_F8),
|
||||
b"20" => Some(DOOM_KEY_F9),
|
||||
b"21" => Some(DOOM_KEY_F10),
|
||||
b"23" => Some(DOOM_KEY_F11),
|
||||
b"24" => Some(DOOM_KEY_F12),
|
||||
_ => None,
|
||||
};
|
||||
(key, tilde + 1)
|
||||
}
|
||||
|
||||
fn byte_to_doom_key(byte: u8) -> Option<i32> {
|
||||
match byte {
|
||||
b'\t' => Some(DOOM_KEY_TAB),
|
||||
b'\r' | b'\n' => Some(DOOM_KEY_ENTER),
|
||||
0x7f | 0x08 => Some(DOOM_KEY_BACKSPACE),
|
||||
0x01..=0x1a => Some((byte - 0x01 + b'a') as i32),
|
||||
b' ' => Some(DOOM_KEY_SPACE),
|
||||
b'\'' => Some(DOOM_KEY_APOSTROPHE),
|
||||
b',' => Some(DOOM_KEY_COMMA),
|
||||
b'-' => Some(DOOM_KEY_MINUS),
|
||||
b'.' => Some(DOOM_KEY_PERIOD),
|
||||
b'/' => Some(DOOM_KEY_SLASH),
|
||||
b'0'..=b'9' => Some(byte as i32),
|
||||
b';' => Some(DOOM_KEY_SEMICOLON),
|
||||
b'=' => Some(DOOM_KEY_EQUALS),
|
||||
b'[' => Some(DOOM_KEY_LEFT_BRACKET),
|
||||
b']' => Some(DOOM_KEY_RIGHT_BRACKET),
|
||||
b'a'..=b'z' => Some(byte as i32),
|
||||
b'A'..=b'Z' => Some(byte.to_ascii_lowercase() as i32),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
mod video {
|
||||
use std::error::Error;
|
||||
|
||||
use pixels::{Pixels, SurfaceTexture};
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event::{Event, WindowEvent};
|
||||
use winit::event_loop::{ControlFlow, EventLoop};
|
||||
use winit::window::WindowBuilder;
|
||||
|
||||
use crate::doom::{DOOM_HEIGHT, DOOM_WIDTH, Doom};
|
||||
|
||||
pub fn run(doom: &mut Doom) -> Result<(), Box<dyn Error>> {
|
||||
let event_loop = EventLoop::new()?;
|
||||
let window = WindowBuilder::new()
|
||||
.with_title("cc2-doom")
|
||||
.with_inner_size(LogicalSize::new(
|
||||
(DOOM_WIDTH * 3) as f64,
|
||||
(DOOM_HEIGHT * 3) as f64,
|
||||
))
|
||||
.with_min_inner_size(LogicalSize::new(DOOM_WIDTH as f64, DOOM_HEIGHT as f64))
|
||||
.build(&event_loop)?;
|
||||
let window = Box::leak(Box::new(window));
|
||||
|
||||
let surface = SurfaceTexture::new(DOOM_WIDTH * 3, DOOM_HEIGHT * 3, &*window);
|
||||
let mut pixels = Pixels::new(DOOM_WIDTH, DOOM_HEIGHT, surface)?;
|
||||
|
||||
event_loop.run(move |event, elwt| {
|
||||
elwt.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
match event {
|
||||
Event::AboutToWait => {
|
||||
doom.update();
|
||||
copy_rgb_to_rgba(doom.framebuffer_rgb(), pixels.frame_mut());
|
||||
if pixels.render().is_err() {
|
||||
elwt.exit();
|
||||
}
|
||||
}
|
||||
Event::WindowEvent {
|
||||
event: WindowEvent::CloseRequested,
|
||||
..
|
||||
} => elwt.exit(),
|
||||
Event::WindowEvent {
|
||||
event: WindowEvent::Resized(size),
|
||||
..
|
||||
} => {
|
||||
let _ = pixels.resize_surface(size.width, size.height);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_rgb_to_rgba(src: &[u8], dst: &mut [u8]) {
|
||||
for (rgb, rgba) in src.chunks_exact(3).zip(dst.chunks_exact_mut(4)) {
|
||||
rgba[0] = rgb[0];
|
||||
rgba[1] = rgb[1];
|
||||
rgba[2] = rgb[2];
|
||||
rgba[3] = 0xff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(
|
||||
all(target_os = "linux", target_arch = "arm"),
|
||||
all(target_os = "linux", target_arch = "x86_64")
|
||||
)))]
|
||||
mod video {
|
||||
use std::error::Error;
|
||||
use std::io::{Error as IoError, ErrorKind};
|
||||
|
||||
use crate::doom::Doom;
|
||||
|
||||
pub fn run(_doom: &mut Doom) -> Result<(), Box<dyn Error>> {
|
||||
Err(IoError::new(ErrorKind::Unsupported, "unsupported Doom video target").into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
use memmap2::{MmapMut, MmapOptions};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, Error, ErrorKind};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
const FBIOGET_VSCREENINFO: libc::c_ulong = 0x4600;
|
||||
const FBIOGET_FSCREENINFO: libc::c_ulong = 0x4602;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct FbFixScreenInfo {
|
||||
id: [u8; 16],
|
||||
smem_start: libc::c_ulong,
|
||||
smem_len: u32,
|
||||
fb_type: u32,
|
||||
type_aux: u32,
|
||||
visual: u32,
|
||||
xpanstep: u16,
|
||||
ypanstep: u16,
|
||||
ywrapstep: u16,
|
||||
line_length: u32,
|
||||
mmio_start: libc::c_ulong,
|
||||
mmio_len: u32,
|
||||
accel: u32,
|
||||
capabilities: u16,
|
||||
reserved: [u16; 2],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct FbVarScreenInfo {
|
||||
xres: u32,
|
||||
yres: u32,
|
||||
xres_virtual: u32,
|
||||
yres_virtual: u32,
|
||||
xoffset: u32,
|
||||
yoffset: u32,
|
||||
bits_per_pixel: u32,
|
||||
grayscale: u32,
|
||||
red: FbBitfield,
|
||||
green: FbBitfield,
|
||||
blue: FbBitfield,
|
||||
transp: FbBitfield,
|
||||
nonstd: u32,
|
||||
activate: u32,
|
||||
height: u32,
|
||||
width: u32,
|
||||
accel_flags: u32,
|
||||
pixclock: u32,
|
||||
left_margin: u32,
|
||||
right_margin: u32,
|
||||
upper_margin: u32,
|
||||
lower_margin: u32,
|
||||
hsync_len: u32,
|
||||
vsync_len: u32,
|
||||
sync: u32,
|
||||
vmode: u32,
|
||||
rotate: u32,
|
||||
colorspace: u32,
|
||||
reserved: [u32; 4],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct FbBitfield {
|
||||
offset: u32,
|
||||
length: u32,
|
||||
msb_right: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum PixelFormat {
|
||||
Rgb565,
|
||||
Native32,
|
||||
Native24,
|
||||
Native16,
|
||||
}
|
||||
|
||||
pub struct FrameBuffer {
|
||||
_file: File,
|
||||
fb_map: MmapMut,
|
||||
pub xres: u32,
|
||||
pub yres: u32,
|
||||
xoffset: u32,
|
||||
yoffset: u32,
|
||||
line_length: usize,
|
||||
bytes_per_pixel: usize,
|
||||
red: FbBitfield,
|
||||
green: FbBitfield,
|
||||
blue: FbBitfield,
|
||||
transp: FbBitfield,
|
||||
format: PixelFormat,
|
||||
}
|
||||
|
||||
impl FrameBuffer {
|
||||
pub fn new(device: &str) -> io::Result<Self> {
|
||||
let file = OpenOptions::new().read(true).write(true).open(device)?;
|
||||
|
||||
let mut fix_info = unsafe { std::mem::zeroed::<FbFixScreenInfo>() };
|
||||
ioctl_read(file.as_raw_fd(), FBIOGET_FSCREENINFO, &mut fix_info)?;
|
||||
|
||||
let mut var_info = unsafe { std::mem::zeroed::<FbVarScreenInfo>() };
|
||||
ioctl_read(file.as_raw_fd(), FBIOGET_VSCREENINFO, &mut var_info)?;
|
||||
|
||||
let bytes_per_pixel = match var_info.bits_per_pixel {
|
||||
16 => 2,
|
||||
24 => 3,
|
||||
32 => 4,
|
||||
bpp => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Unsupported,
|
||||
format!("unsupported framebuffer depth: {bpp} bpp"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if fix_info.smem_len == 0 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"framebuffer reports zero memory length",
|
||||
));
|
||||
}
|
||||
|
||||
let line_length = fix_info.line_length as usize;
|
||||
if line_length == 0 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"framebuffer reports zero line length",
|
||||
));
|
||||
}
|
||||
|
||||
let format = detect_format(
|
||||
var_info.bits_per_pixel,
|
||||
var_info.red,
|
||||
var_info.green,
|
||||
var_info.blue,
|
||||
);
|
||||
let fb_map = unsafe {
|
||||
MmapOptions::new()
|
||||
.len(fix_info.smem_len as usize)
|
||||
.map_mut(&file)?
|
||||
};
|
||||
|
||||
Ok(FrameBuffer {
|
||||
_file: file,
|
||||
fb_map,
|
||||
xres: var_info.xres,
|
||||
yres: var_info.yres,
|
||||
xoffset: var_info.xoffset,
|
||||
yoffset: var_info.yoffset,
|
||||
line_length,
|
||||
bytes_per_pixel,
|
||||
red: var_info.red,
|
||||
green: var_info.green,
|
||||
blue: var_info.blue,
|
||||
transp: var_info.transp,
|
||||
format,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn display_frame(&mut self, frame: &[u8], width: u32, height: u32) -> io::Result<()> {
|
||||
self.display_frame_at(frame, width, height, 0, 0)
|
||||
}
|
||||
|
||||
pub fn display_frame_at(
|
||||
&mut self,
|
||||
frame: &[u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
offset_x: i32,
|
||||
offset_y: i32,
|
||||
) -> io::Result<()> {
|
||||
let expected_len = rgb888_len(width, height)?;
|
||||
if frame.len() < expected_len {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"frame is too small for {width}x{height} RGB888: {} < {expected_len}",
|
||||
frame.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let Some(clip) = Clip::new(width, height, self.xres, self.yres, offset_x, offset_y) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let xoffset = self.xoffset as usize;
|
||||
let yoffset = self.yoffset as usize;
|
||||
let line_length = self.line_length;
|
||||
let bpp = self.bytes_per_pixel;
|
||||
let red = self.red;
|
||||
let green = self.green;
|
||||
let blue = self.blue;
|
||||
let transp = self.transp;
|
||||
let format = self.format;
|
||||
|
||||
for row in 0..clip.height {
|
||||
let src_y = clip.src_y + row;
|
||||
let dst_y = clip.dst_y + row;
|
||||
let src_start = ((src_y * width + clip.src_x) * 3) as usize;
|
||||
let src_end = src_start + (clip.width * 3) as usize;
|
||||
let src = &frame[src_start..src_end];
|
||||
|
||||
let dst_start =
|
||||
(dst_y as usize + yoffset) * line_length + (clip.dst_x as usize + xoffset) * bpp;
|
||||
let dst_end = dst_start + clip.width as usize * bpp;
|
||||
if dst_end > self.fb_map.len() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"visible framebuffer area exceeds mapped memory",
|
||||
));
|
||||
}
|
||||
|
||||
write_rgb888_row(
|
||||
src,
|
||||
&mut self.fb_map[dst_start..dst_end],
|
||||
format,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
transp,
|
||||
);
|
||||
}
|
||||
|
||||
// fbdev mappings are device memory. The writes above are the update;
|
||||
// msync can return EINVAL on framebuffer mappings even after pixels land.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn ioctl_read<T>(fd: std::os::fd::RawFd, request: libc::c_ulong, value: &mut T) -> io::Result<()> {
|
||||
let result = unsafe { libc::ioctl(fd, request.try_into().unwrap(), value as *mut T) };
|
||||
if result < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_format(
|
||||
bits_per_pixel: u32,
|
||||
red: FbBitfield,
|
||||
green: FbBitfield,
|
||||
blue: FbBitfield,
|
||||
) -> PixelFormat {
|
||||
match bits_per_pixel {
|
||||
16 if red.offset == 11
|
||||
&& red.length == 5
|
||||
&& green.offset == 5
|
||||
&& green.length == 6
|
||||
&& blue.offset == 0
|
||||
&& blue.length == 5 =>
|
||||
{
|
||||
PixelFormat::Rgb565
|
||||
}
|
||||
16 => PixelFormat::Native16,
|
||||
24 => PixelFormat::Native24,
|
||||
32 => PixelFormat::Native32,
|
||||
_ => unreachable!("unsupported framebuffer depth was rejected earlier"),
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb888_len(width: u32, height: u32) -> io::Result<usize> {
|
||||
width
|
||||
.checked_mul(height)
|
||||
.and_then(|pixels| pixels.checked_mul(3))
|
||||
.map(|len| len as usize)
|
||||
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, "RGB888 frame dimensions overflow"))
|
||||
}
|
||||
|
||||
fn write_rgb888_row(
|
||||
src: &[u8],
|
||||
dst: &mut [u8],
|
||||
format: PixelFormat,
|
||||
red: FbBitfield,
|
||||
green: FbBitfield,
|
||||
blue: FbBitfield,
|
||||
transp: FbBitfield,
|
||||
) {
|
||||
match format {
|
||||
PixelFormat::Rgb565 => write_rgb565_row(src, dst),
|
||||
PixelFormat::Native16 => write_native_row::<2>(src, dst, red, green, blue, transp),
|
||||
PixelFormat::Native24 => write_native_row::<3>(src, dst, red, green, blue, transp),
|
||||
PixelFormat::Native32 => write_native_row::<4>(src, dst, red, green, blue, transp),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_rgb565_row(src: &[u8], dst: &mut [u8]) {
|
||||
for (rgb, out) in src.chunks_exact(3).zip(dst.chunks_exact_mut(2)) {
|
||||
let r = (rgb[0] as u16 >> 3) << 11;
|
||||
let g = (rgb[1] as u16 >> 2) << 5;
|
||||
let b = rgb[2] as u16 >> 3;
|
||||
out.copy_from_slice(&(r | g | b).to_ne_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
fn write_native_row<const N: usize>(
|
||||
src: &[u8],
|
||||
dst: &mut [u8],
|
||||
red: FbBitfield,
|
||||
green: FbBitfield,
|
||||
blue: FbBitfield,
|
||||
transp: FbBitfield,
|
||||
) {
|
||||
for (rgb, out) in src.chunks_exact(3).zip(dst.chunks_exact_mut(N)) {
|
||||
let pixel = pack_pixel(rgb[0], rgb[1], rgb[2], red, green, blue, transp);
|
||||
out.copy_from_slice(&pixel.to_ne_bytes()[..N]);
|
||||
}
|
||||
}
|
||||
|
||||
fn pack_pixel(
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
red: FbBitfield,
|
||||
green: FbBitfield,
|
||||
blue: FbBitfield,
|
||||
transp: FbBitfield,
|
||||
) -> u32 {
|
||||
pack_channel(r, red) | pack_channel(g, green) | pack_channel(b, blue) | pack_alpha(transp)
|
||||
}
|
||||
|
||||
fn pack_channel(value: u8, field: FbBitfield) -> u32 {
|
||||
if field.length == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let max = (1u32 << field.length) - 1;
|
||||
let scaled = (value as u32 * max + 127) / 255;
|
||||
scaled << field.offset
|
||||
}
|
||||
|
||||
fn pack_alpha(field: FbBitfield) -> u32 {
|
||||
if field.length == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
((1u32 << field.length) - 1) << field.offset
|
||||
}
|
||||
|
||||
struct Clip {
|
||||
src_x: u32,
|
||||
src_y: u32,
|
||||
dst_x: u32,
|
||||
dst_y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl Clip {
|
||||
fn new(
|
||||
src_width: u32,
|
||||
src_height: u32,
|
||||
dst_width: u32,
|
||||
dst_height: u32,
|
||||
offset_x: i32,
|
||||
offset_y: i32,
|
||||
) -> Option<Self> {
|
||||
if src_width == 0 || src_height == 0 || dst_width == 0 || dst_height == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let src_x = offset_x.saturating_neg().max(0) as u32;
|
||||
let src_y = offset_y.saturating_neg().max(0) as u32;
|
||||
let dst_x = offset_x.max(0) as u32;
|
||||
let dst_y = offset_y.max(0) as u32;
|
||||
|
||||
if src_x >= src_width || src_y >= src_height || dst_x >= dst_width || dst_y >= dst_height {
|
||||
return None;
|
||||
}
|
||||
|
||||
let width = (src_width - src_x).min(dst_width - dst_x);
|
||||
let height = (src_height - src_y).min(dst_height - dst_y);
|
||||
if width == 0 || height == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Clip {
|
||||
src_x,
|
||||
src_y,
|
||||
dst_x,
|
||||
dst_y,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod doom;
|
||||
#[cfg(all(target_os = "linux", target_arch = "arm"))]
|
||||
mod framebuffer;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
doom::run()
|
||||
}
|
||||
Reference in New Issue
Block a user