diff --git a/Cargo.lock b/Cargo.lock index 2546636f..e3383e90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,17 @@ version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +[[package]] +name = "console" +version = "0.1.0" +dependencies = [ + "bytemuck", + "display-client", + "lz4", + "ulib", + "unicode-segmentation", +] + [[package]] name = "device-tree" version = "0.1.0" @@ -28,6 +39,31 @@ dependencies = [ "endian", ] +[[package]] +name = "display-client" +version = "0.1.0" +dependencies = [ + "display-proto", + "ulib", +] + +[[package]] +name = "display-proto" +version = "0.1.0" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "display-server" +version = "0.1.0" +dependencies = [ + "bytemuck", + "display-proto", + "linked_list_allocator", + "ulib", +] + [[package]] name = "elf" version = "0.2.0" @@ -130,3 +166,12 @@ checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" [[package]] name = "ulib" version = "0.1.0" +dependencies = [ + "linked_list_allocator", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" diff --git a/Cargo.toml b/Cargo.toml index f20bd760..01d450ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,11 @@ [workspace] resolver = "2" members = [ + "crates/console", "crates/device-tree", + "crates/display-client", + "crates/display-proto", + "crates/display-server", "crates/elf", "crates/endian", "crates/filesystem", diff --git a/crates/console/.gitignore b/crates/console/.gitignore new file mode 100644 index 00000000..50992530 --- /dev/null +++ b/crates/console/.gitignore @@ -0,0 +1 @@ +*.elf diff --git a/crates/console/Cargo.toml b/crates/console/Cargo.toml new file mode 100644 index 00000000..716b51a5 --- /dev/null +++ b/crates/console/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "console" +version = "0.1.0" +edition = "2021" + +[dependencies] +bytemuck = "1.21.0" +unicode-segmentation = "1.12" + +display-client = { path = "../display-client" } +lz4 = { path = "../lz4" } +ulib = { path = "../ulib", features = ["heap-impl"] } + +[dev-dependencies] +ulib = { path = "../ulib", features = ["test"] } diff --git a/crates/console/build.rs b/crates/console/build.rs new file mode 100644 index 00000000..fe806e18 --- /dev/null +++ b/crates/console/build.rs @@ -0,0 +1,6 @@ +fn main() { + let crate_root = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + println!("cargo::rerun-if-changed=../ulib/script.ld"); + println!("cargo::rustc-link-arg-bins=-T{crate_root}/../ulib/script.ld"); + println!("cargo::rustc-link-arg-bins=-n"); +} diff --git a/crates/console/build.sh b/crates/console/build.sh new file mode 100755 index 00000000..62cec4d7 --- /dev/null +++ b/crates/console/build.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -ex + +BIN="console" +TARGET=aarch64-unknown-none-softfloat +PROFILE=${PROFILE-"release"} + +cargo rustc --profile="${PROFILE}" \ + --target=${TARGET} -- \ + -C relocation-model=static + +if test "$PROFILE" = "dev" ; then + BINARY=../../target/${TARGET}/debug/${BIN} +else + BINARY=../../target/${TARGET}/${PROFILE}/${BIN} +fi + +cp "${BINARY}" "${BIN}".elf diff --git a/crates/console/ctrld-fixed-10r.pcf.lz4 b/crates/console/ctrld-fixed-10r.pcf.lz4 new file mode 100644 index 00000000..1bb0d317 Binary files /dev/null and b/crates/console/ctrld-fixed-10r.pcf.lz4 differ diff --git a/crates/console/src/color.rs b/crates/console/src/color.rs new file mode 100644 index 00000000..743ce5a7 --- /dev/null +++ b/crates/console/src/color.rs @@ -0,0 +1,23 @@ +pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> u32 { + u32::from_be_bytes([a, r, g, b]) +} + +pub const fn de_rgba(color: u32) -> (u8, u8, u8, u8) { + let [a, r, g, b] = color.to_be_bytes(); + (r, g, b, a) +} + +pub fn blend(one: u32, two: u32) -> u32 { + let [a, r, g, b] = one.to_be_bytes().map(u32::from); + if a == 255 { + return one; + } + let [_bg_a, bg_r, bg_g, bg_b] = two.to_be_bytes().map(u32::from); + // TODO: blend in linear space? + let (r, g, b) = ( + (r * a + bg_r * (255 - a)) / 255, + (g * a + bg_g * (255 - a)) / 255, + (b * a + bg_b * (255 - a)) / 255, + ); + u32::from_be_bytes([a, r, g, b].map(|b| b as u8)) +} diff --git a/crates/console/src/editor.rs b/crates/console/src/editor.rs new file mode 100644 index 00000000..d1fbe6f8 --- /dev/null +++ b/crates/console/src/editor.rs @@ -0,0 +1,616 @@ +// See also: +// - [Text Rendering Hates You](https://faultlore.com/blah/text-hates-you/) +// - [Text Editing Hates You Too](https://lord.io/text-editing-hates-you-too/) + +use core::ops::ControlFlow; + +use crate::color::rgba; +use crate::grid::{Colors, GridRef}; +use crate::vt100::{EmulatorState, GridCoords}; + +#[derive(Debug)] +pub enum Keypress { + Char(Modifiers, char, char), + Function(Modifiers, FuncKey), + Invalid([u8; 8]), +} + +#[derive(Debug)] +pub enum KeyEvent { + Press(Keypress), + Repeat(Keypress), + Release(Keypress), +} + +impl KeyEvent { + pub fn new(event: KeyEventMode, key: Keypress) -> Self { + match event { + KeyEventMode::Press => Self::Press(key), + KeyEventMode::Repeat => Self::Repeat(key), + KeyEventMode::Release => Self::Release(key), + } + } +} + +pub enum KeyEventMode { + Press, + Repeat, + Release, +} + +#[derive(PartialEq, Copy, Clone)] +pub struct Modifiers { + pub ctrl: bool, + pub shift: bool, + pub alt: bool, + pub meta: bool, +} + +impl Modifiers { + pub const NONE: Modifiers = Modifiers { + ctrl: false, + shift: false, + alt: false, + meta: false, + }; + pub const CTRL: Modifiers = Modifiers { + ctrl: true, + shift: false, + alt: false, + meta: false, + }; + pub const SHIFT: Modifiers = Modifiers { + ctrl: false, + shift: true, + alt: false, + meta: false, + }; +} + +impl core::fmt::Debug for Modifiers { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Modifiers(")?; + let mut first = true; + if self.ctrl { + first = false; + write!(f, "CTRL")?; + } + if self.shift { + if !first { + write!(f, " | ")?; + } + first = false; + write!(f, "SHIFT")?; + } + if self.alt { + if !first { + write!(f, " | ")?; + } + first = false; + write!(f, "ALT")?; + } + if self.meta { + if !first { + write!(f, " | ")?; + } + first = false; + write!(f, "META")?; + } + if first { + write!(f, "NONE")?; + } + write!(f, ")")?; + Ok(()) + } +} + +#[derive(Debug)] +pub enum FuncKey { + Up, + Down, + Left, + Right, + + Backspace, + Delete, + Enter, + Tab, + + Insert, + End, + Home, + Escape, + + PageUp, + PageDown, + + Func(u8), +} + +pub fn find_char_after(input: &[u8], cursor: usize) -> usize { + for i in cursor + 1..input.len() { + if !is_utf8_trailer(input[i]) { + return i; + } + } + input.len() +} + +pub fn find_char_before(input: &[u8], cursor: usize) -> usize { + for i in (0..cursor).rev() { + if !is_utf8_trailer(input[i]) { + return i; + } + } + 0 +} + +#[allow(dead_code)] +fn utf8_width(b: u8) -> Option { + match b { + _ if b & 0b10000000 == 0b00000000 => Some(1), + _ if b & 0b11100000 == 0b11000000 => Some(2), + _ if b & 0b11110000 == 0b11100000 => Some(3), + _ if b & 0b11110000 == 0b11110000 => Some(4), + _ => None, + } +} +fn is_utf8_trailer(b: u8) -> bool { + b & 0b11000000 == 0b10000000 +} + +#[derive(Clone, Copy)] +pub struct Cursor { + pub byte: usize, +} + +extern crate alloc; + +pub struct LineEditor { + pub buf: alloc::string::String, + pub cut_buffer: alloc::string::String, + pub primary: Cursor, + pub secondary: Cursor, + pub last_keypress: u64, +} + +impl LineEditor { + pub fn new() -> LineEditor { + LineEditor { + buf: alloc::string::String::with_capacity(256), + cut_buffer: alloc::string::String::with_capacity(256), + primary: Cursor { byte: 0 }, + secondary: Cursor { byte: 0 }, + last_keypress: 0, + } + } + + pub fn input(&mut self, text: &str) { + let range = self.selection_range(); + self.buf.replace_range(range.clone(), text); + + // TODO: text cols, rows + let new_cursor = range.start + text.len(); + let new_cursor = Cursor { byte: new_cursor }; + self.primary = new_cursor; + self.secondary = self.primary; + } + + pub fn paste_from_cut(&mut self) { + let range = self.selection_range(); + let text = &self.cut_buffer; + self.buf.replace_range(range.clone(), text); + + // TODO: text cols, rows + let new_cursor = range.start + text.len(); + let new_cursor = Cursor { byte: new_cursor }; + self.primary = new_cursor; + self.secondary = self.primary; + } + + pub fn range_width(&self, range: core::ops::Range) -> usize { + self.buf[range].chars().count() + // unicode_width_16::UnicodeWidthStr::width(&self.buf[range]) + } + + pub fn selection_range(&self) -> core::ops::Range { + if self.primary.byte <= self.secondary.byte { + self.primary.byte..self.secondary.byte + } else { + self.secondary.byte..self.primary.byte + } + } + + pub fn cursor_left(&mut self, only_primary: bool) -> Cursor { + let cursor = self.primary.byte; + let mut cur = unicode_segmentation::GraphemeCursor::new(cursor, self.buf.len(), false); + let mut end = cursor; + if cursor <= self.buf.len() { + if let Ok(b) = cur.prev_boundary(&self.buf, 0) { + if let Some(b) = b { + end = b; + } else { + end = cursor; + } + } else { + let next_char = find_char_before(self.buf.as_bytes(), cursor); + end = next_char; + } + } + if only_primary { + self.primary = Cursor { byte: end }; + } else { + self.primary = Cursor { byte: end }; + self.secondary = Cursor { byte: end }; + } + self.primary + } + + pub fn cursor_right(&mut self, only_primary: bool) -> Cursor { + let cursor = self.primary.byte; + let mut cur = unicode_segmentation::GraphemeCursor::new(cursor, self.buf.len(), false); + let mut end = cursor + 1; + if cursor <= self.buf.len() { + if let Ok(b) = cur.next_boundary(&self.buf, 0) { + if let Some(b) = b { + end = b; + } else { + end = cursor; + } + } else { + let next_char = find_char_after(self.buf.as_bytes(), cursor); + end = next_char; + } + } + end = usize::min(end, self.buf.len()); + if only_primary { + self.primary = Cursor { byte: end }; + } else { + self.primary = Cursor { byte: end }; + self.secondary = Cursor { byte: end }; + } + self.primary + } + + pub fn delete_left(&mut self) { + let cur = self.primary; + if self.primary.byte != self.secondary.byte { + self.delete_range(self.selection_range()); + } else { + let moved = self.cursor_left(false); + if cur.byte < moved.byte { + panic!("{:?} {:?}", cur.byte, moved.byte) + } + self.delete_range(moved.byte..cur.byte); + } + } + + pub fn delete_right(&mut self) { + let cur = self.primary; + if self.primary.byte != self.secondary.byte { + self.delete_range(self.selection_range()); + } else { + let moved = self.cursor_right(false); + self.delete_range(cur.byte..moved.byte); + } + } + + pub fn delete_range(&mut self, range: core::ops::Range) { + self.buf.replace_range(range.clone(), ""); + let primary = self.primary; + let secondary = self.secondary; + let (left, right) = if primary.byte <= secondary.byte { + (primary.byte, secondary.byte) + } else { + (secondary.byte, primary.byte) + }; + + let range_len = range.end - range.start; + let remap_idx = |idx| { + if idx > range.start && idx <= range.end { + range.start + } else if idx > range.end { + idx - range_len + } else { + idx + } + }; + + let new_left = remap_idx(left); + let new_right = remap_idx(right); + + if left == primary.byte { + self.primary = Cursor { byte: new_left }; + self.secondary = Cursor { byte: new_right }; + } else { + self.primary = Cursor { byte: new_right }; + self.secondary = Cursor { byte: new_left }; + } + } + + pub fn set_cursor(&mut self, byte: usize) { + self.primary = Cursor { byte }; + self.secondary = self.primary; + } + + pub fn clear(&mut self) { + self.set_cursor(0); + self.buf.clear(); + } + + pub fn set_cursor_range(&mut self, start: usize, end: usize) { + self.primary = Cursor { byte: end }; + self.secondary = Cursor { byte: start }; + } +} + +pub fn editor_input(editor: &mut LineEditor, key: KeyEvent, time_us: u64) -> ControlFlow<()> { + let c = match key { + KeyEvent::Press(c) => c, + KeyEvent::Repeat(c) => c, + KeyEvent::Release(_) => return ControlFlow::Continue(()), + }; + match c { + Keypress::Function(Modifiers::NONE, FuncKey::Right) => { + editor.cursor_right(false); + } + Keypress::Function(Modifiers::NONE, FuncKey::Left) => { + editor.cursor_left(false); + } + Keypress::Function(Modifiers::SHIFT, FuncKey::Right) => { + editor.cursor_right(true); + } + Keypress::Function(Modifiers::SHIFT, FuncKey::Left) => { + editor.cursor_left(true); + } + Keypress::Function(Modifiers::NONE, FuncKey::Delete) => { + editor.delete_right(); + } + Keypress::Function(Modifiers::NONE, FuncKey::Backspace) => { + editor.delete_left(); + } + Keypress::Function(Modifiers::NONE, FuncKey::End) + | Keypress::Char(Modifiers::CTRL, 'E' | 'e', _) => { + editor.set_cursor(editor.buf.len()); + } + Keypress::Function(Modifiers::NONE, FuncKey::Home) + | Keypress::Char(Modifiers::CTRL, 'A' | 'a', _) => { + editor.set_cursor(0); + } + Keypress::Char(Modifiers::CTRL, 'K' | 'k', _) => { + let mut range = editor.selection_range(); + if range.is_empty() { + range = range.start..editor.buf.len(); + } + let text = &editor.buf[range.clone()]; + editor.cut_buffer.replace_range(.., text); + editor.delete_range(range); + } + Keypress::Char(Modifiers::CTRL, 'Y' | 'y', _) => { + editor.paste_from_cut(); + } + Keypress::Char(Modifiers::CTRL, 'C' | 'c', _) => { + return ControlFlow::Break(()); + } + Keypress::Function(Modifiers::NONE, FuncKey::Enter) => { + editor.input("\n"); + } + Keypress::Function(Modifiers::NONE, FuncKey::Tab) => { + // tab_complete(); + } + Keypress::Char(Modifiers::CTRL, 'D' | 'd', _) => { + if editor.buf.is_empty() { + return ControlFlow::Break(()); + } + } + Keypress::Char( + Modifiers { + ctrl: false, + alt: false, + meta: false, + shift: _, + }, + _, + c, + ) => { + let mut buf = [0u8; 8]; + let str = c.encode_utf8(&mut buf); + editor.input(str); + } + _c => { + // eprintln!("{:?}", c); + } + } + editor.last_keypress = time_us; + + ControlFlow::Continue(()) +} + +pub fn draw_editor(editor: &LineEditor, grid: GridRef<'_>, color: u32, blink: bool) -> usize { + for r in 0..grid.rows { + for c in 0..grid.cols { + grid.chars[r * grid.stride + c] = ' '; + grid.colors[r * grid.stride + c] = Colors { + fg: color, + bg: rgba(0, 0, 0, 0), + }; + } + } + + let wrap_at = grid.cols; + let selection = editor.selection_range(); + let primary_pos = editor.primary.byte; + + let cursor_width = 1; + let line_height = 1; + + let mut x = 0; + let mut y = 0; + for (i, char) in editor.buf.char_indices() { + if y >= grid.rows { + break; + } + match char { + '\r' => continue, + '\n' => { + if i == primary_pos { + // TODO: slightly less weird behavior here (full line, cursor at end) + // Maybe make it always wrap a blank line if there isn't width for the cursor? + if x + cursor_width > wrap_at { + x = 0; + y += line_height; + } + if y >= grid.rows { + break; + } + if blink { + let bg_color = rgba(255, 255, 255, 255); + // TODO: proper background fill + grid.colors[grid.stride * y + x] = Colors { + fg: 0, + bg: bg_color, + }; + grid.chars[grid.stride * y + x] = ' '; + } + } + + x = 0; + y += line_height; + continue; + } + _ => (), + } + + let glyph_width = 1; + if x + glyph_width > wrap_at { + x = 0; + y += line_height; + } + if y >= grid.rows { + break; + } + + let mut colors = Colors { + fg: color, + bg: rgba(0, 0, 0, 0), + }; + if selection.contains(&i) || (i == primary_pos && blink) { + colors.fg = rgba(0, 0, 0, 255); + colors.bg = rgba(255, 255, 255, 255); + } + + grid.colors[grid.stride * y + x] = colors; + grid.chars[grid.stride * y + x] = char; + x += glyph_width; + } + + if blink && editor.buf.len() == primary_pos { + if x + cursor_width > wrap_at { + x = 0; + y += line_height; + } + if y < grid.rows { + let bg_color = rgba(255, 255, 255, 255); + // TODO: proper background fill + grid.colors[grid.stride * y + x] = Colors { + fg: 0, + bg: bg_color, + }; + grid.chars[grid.stride * y + x] = ' '; + } + } + + y + line_height +} + +pub fn draw_editor_into_console( + editor: &LineEditor, + grid: &mut EmulatorState, + color: u32, + blink: bool, +) { + let selection = editor.selection_range(); + let primary_pos = editor.primary.byte; + + let initial_cursor = grid.cursor; + grid.scrolled_rows = 0; + + for (i, char) in editor.buf.char_indices() { + match char { + '\r' => continue, + '\n' => { + if i == primary_pos { + // TODO: slightly less weird behavior here (full line, cursor at end) + // Maybe make it always wrap a blank line if there isn't width for the cursor? + grid.check_wrap(); + if blink { + let bg_color = rgba(255, 255, 255, 255); + grid.set_char_color( + grid.cursor, + ' ', + Colors { + fg: 0, + bg: bg_color, + }, + ); + } + } + grid.wrap(); + continue; + } + _ => (), + } + + let mut colors = Colors { + fg: color, + bg: rgba(0, 0, 0, 0), + }; + if selection.contains(&i) || (i == primary_pos && blink) { + colors.fg = rgba(0, 0, 0, 255); + colors.bg = rgba(255, 255, 255, 255); + } + + grid.set_char_color(grid.cursor, char, colors); + grid.cursor.col += 1; + grid.check_wrap(); + } + + if editor.buf.len() == primary_pos { + let bg_color = if blink { + rgba(255, 255, 255, 255) + } else { + rgba(0, 0, 0, 255) + }; + grid.check_wrap(); + grid.set_char_color( + grid.cursor, + ' ', + Colors { + fg: 0, + bg: bg_color, + }, + ); + } + + let bg_color = rgba(0, 0, 0, 0); + for c in grid.cursor.col + 1..grid.cols { + let pos = GridCoords { + row: grid.cursor.row, + col: c, + }; + grid.set_char_color( + pos, + ' ', + Colors { + fg: 0, + bg: bg_color, + }, + ); + } + + grid.cursor = GridCoords { + row: initial_cursor.row.saturating_sub(grid.scrolled_rows), + col: initial_cursor.col, + }; + grid.scrolled_rows = 0; +} diff --git a/crates/console/src/format/mod.rs b/crates/console/src/format/mod.rs new file mode 100644 index 00000000..35a51223 --- /dev/null +++ b/crates/console/src/format/mod.rs @@ -0,0 +1,2 @@ +pub mod pcf; +pub mod qoi; diff --git a/crates/console/src/format/pcf.rs b/crates/console/src/format/pcf.rs new file mode 100644 index 00000000..07bf1f49 --- /dev/null +++ b/crates/console/src/format/pcf.rs @@ -0,0 +1,972 @@ +#![allow(dead_code, nonstandard_style)] + +// https://fontforge.org/docs/techref/pcf-format.html +// TODO: explicitly little-endian header fields + +use core::mem::offset_of; + +use bytemuck::{try_cast_slice, try_from_bytes, AnyBitPattern, Pod, Zeroable}; + +type le_u32 = u32; + +#[derive(Copy, Clone, Debug)] +enum Endian { + Little, + Big, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct pcf_u32(u32); + +impl pcf_u32 { + fn read(self, endianness: Endian) -> u32 { + // While this looks completely wrong, Rust doesn't provide + // from endianness functions on u32s; however, to_le is exactly + // equivalent to from_le. (On a LE system, to_le does nothing; + // on a BE system, it reverses the byte order.) The same applies + // to to_be. + match endianness { + Endian::Little => self.0.to_le(), + Endian::Big => self.0.to_be(), + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct pcf_i32(i32); + +impl pcf_i32 { + fn read(self, endianness: Endian) -> i32 { + match endianness { + Endian::Little => self.0.to_le(), + Endian::Big => self.0.to_be(), + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct unaligned_pcf_u32([u8; 4]); + +impl unaligned_pcf_u32 { + fn read(self, endianness: Endian) -> u32 { + match endianness { + Endian::Little => u32::from_le_bytes(self.0), + Endian::Big => u32::from_be_bytes(self.0), + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct pcf_u16(u16); + +impl pcf_u16 { + fn read(self, endianness: Endian) -> u16 { + match endianness { + Endian::Little => self.0.to_le(), + Endian::Big => self.0.to_be(), + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct pcf_i16(i16); + +impl pcf_i16 { + fn read(self, endianness: Endian) -> i16 { + match endianness { + Endian::Little => self.0.to_le(), + Endian::Big => self.0.to_be(), + } + } +} + +// Manually inlined derive impls from bytemuck, since the +// derive macros triple compilation time + +unsafe impl Zeroable for pcf_u32 {} +unsafe impl Pod for pcf_u32 {} + +unsafe impl Zeroable for pcf_i32 {} +unsafe impl Pod for pcf_i32 {} + +unsafe impl Zeroable for unaligned_pcf_u32 {} +unsafe impl Pod for unaligned_pcf_u32 {} + +unsafe impl Zeroable for pcf_u16 {} +unsafe impl Pod for pcf_u16 {} + +unsafe impl Zeroable for pcf_i16 {} +unsafe impl Pod for pcf_i16 {} + +unsafe impl Zeroable for PCFHeader {} +unsafe impl AnyBitPattern for PCFHeader {} +unsafe impl Zeroable for toc_entry {} +unsafe impl AnyBitPattern for toc_entry {} +unsafe impl Zeroable for EntryType {} +unsafe impl AnyBitPattern for EntryType {} +unsafe impl Zeroable for EntryFormat {} +unsafe impl AnyBitPattern for EntryFormat {} +unsafe impl Zeroable for PropertiesTableHeader {} +unsafe impl AnyBitPattern for PropertiesTableHeader {} +unsafe impl Zeroable for Property {} +unsafe impl AnyBitPattern for Property {} +unsafe impl Zeroable for MetricsCompressed {} +unsafe impl AnyBitPattern for MetricsCompressed {} +unsafe impl Zeroable for MetricsUncompressed {} +unsafe impl AnyBitPattern for MetricsUncompressed {} +unsafe impl Zeroable for AcceleratorTable {} +unsafe impl AnyBitPattern for AcceleratorTable {} +unsafe impl Zeroable for AcceleratorInkbounds {} +unsafe impl AnyBitPattern for AcceleratorInkbounds {} +unsafe impl Zeroable for MetricsCompressedHeader {} +unsafe impl AnyBitPattern for MetricsCompressedHeader {} +unsafe impl Zeroable for MetricsUncompressedHeader {} +unsafe impl AnyBitPattern for MetricsUncompressedHeader {} +unsafe impl Zeroable for BitmapTableHeader {} +unsafe impl AnyBitPattern for BitmapTableHeader {} +unsafe impl Zeroable for EncodingTableHeader {} +unsafe impl AnyBitPattern for EncodingTableHeader {} +unsafe impl Zeroable for ScalableWidthHeader {} +unsafe impl AnyBitPattern for ScalableWidthHeader {} +unsafe impl Zeroable for GlyphNamesHeader {} +unsafe impl AnyBitPattern for GlyphNamesHeader {} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct PCFHeader { + header: [u8; 4], /* always "\1fcp" */ + table_count: le_u32, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct toc_entry { + type_: EntryType, /* See below, indicates which table */ + format: EntryFormat, /* See below, indicates how the data are formatted in the table */ + size: le_u32, /* In bytes */ + offset: le_u32, /* from start of file */ +} + +#[repr(transparent)] +#[derive(Copy, Clone, Debug)] +struct EntryType(le_u32); + +impl EntryType { + pub const PCF_PROPERTIES: u32 = (1 << 0); + pub const PCF_ACCELERATORS: u32 = (1 << 1); + pub const PCF_METRICS: u32 = (1 << 2); + pub const PCF_BITMAPS: u32 = (1 << 3); + pub const PCF_INK_METRICS: u32 = (1 << 4); + pub const PCF_BDF_ENCODINGS: u32 = (1 << 5); + pub const PCF_SWIDTHS: u32 = (1 << 6); + pub const PCF_GLYPH_NAMES: u32 = (1 << 7); + pub const PCF_BDF_ACCELERATORS: u32 = (1 << 8); +} + +#[repr(transparent)] +#[derive(Copy, Clone, Debug)] +pub struct EntryFormat(pub le_u32); + +impl EntryFormat { + pub const PCF_DEFAULT_FORMAT: u32 = 0x0000_0000; + pub const PCF_INKBOUNDS: u32 = 0x0000_0200; + pub const PCF_ACCEL_W_INKBOUNDS: u32 = 0x0000_0100; + pub const PCF_COMPRESSED_METRICS: u32 = 0x0000_0100; + + pub const PCF_GLYPH_PAD_MASK: u32 = (3 << 0); /* See the bitmap table for explanation */ + pub const PCF_BYTE_MASK: u32 = (1 << 2); /* If set then Most Sig Byte First */ + pub const PCF_BIT_MASK: u32 = (1 << 3); /* If set then Most Sig Bit First */ + pub const PCF_SCAN_UNIT_MASK: u32 = (3 << 4); /* See the bitmap table for explanation */ +} + +// Properties table format: +// - PropertiesTableHeader +// - Property[nprops] (note: 9 bytes each, unaligned) +// - pad to 4 byte boundary +// - string_size: u32 +// - string data + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct PropertiesTableHeader { + format: EntryFormat, /* Always stored with least significant byte first! */ + nprops: pcf_u32, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct Property { + name_offset: unaligned_pcf_u32, /* Offset into the following string table */ + is_string_prop: u8, // Why would you do this. + value: unaligned_pcf_u32, /* The value for integer props, the offset for string props */ +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct MetricsCompressed { + // Note: all fields are offset by 128, so actual value is (field - 128) + left_sided_bearing: u8, + right_side_bearing: u8, + character_width: u8, + character_ascent: u8, + character_descent: u8, + /* Implied character attributes field = 0 */ +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct MetricsUncompressed { + left_sided_bearing: pcf_i16, + right_side_bearing: pcf_i16, + character_width: pcf_i16, + character_ascent: pcf_i16, + character_descent: pcf_i16, + character_attributes: pcf_u16, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct AcceleratorTable { + format: EntryFormat, /* Always stored with least significant byte first! */ + noOverlap: u8, /* if for all i, max(metrics[i].rightSideBearing - metrics[i].characterWidth) */ + /* <= minbounds.leftSideBearing */ + constantMetrics: u8, /* Means the perchar field of the XFontStruct can be NULL */ + terminalFont: u8, /* constantMetrics true and forall characters: */ + /* the left side bearing==0 */ + /* the right side bearing== the character's width */ + /* the character's ascent==the font's ascent */ + /* the character's descent==the font's descent */ + constantWidth: u8, /* monospace font like courier */ + inkInside: u8, /* Means that all inked bits are within the rectangle with x between [0,charwidth] */ + /* and y between [-descent,ascent]. So no ink overlaps another char when drawing */ + inkMetrics: u8, /* true if the ink metrics differ from the metrics somewhere */ + drawDirection: u8, /* 0=>left to right, 1=>right to left */ + padding: u8, + fontAscent: pcf_i32, + fontDescent: pcf_i32, + maxOverlap: pcf_i32, /* ??? (sic) */ + minbounds: MetricsUncompressed, + maxbounds: MetricsUncompressed, +} + +#[repr(C)] +#[derive(Debug)] +pub struct AcceleratorUnpacked { + pub format: EntryFormat, + pub noOverlap: u8, + pub constantMetrics: u8, + pub terminalFont: u8, + pub constantWidth: u8, + pub inkInside: u8, + pub inkMetrics: u8, + pub drawDirection: u8, + pub padding: u8, + pub fontAscent: i32, + pub fontDescent: i32, + pub maxOverlap: i32, + pub minbounds: MetricsUnpacked, + pub maxbounds: MetricsUnpacked, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct AcceleratorInkbounds { + /* If format is PCF_ACCEL_W_INKBOUNDS then include the following fields */ + /* Otherwise those fields are not in the file and should be filled by duplicating min/maxbounds above */ + ink_minbounds: MetricsUncompressed, + ink_maxbounds: MetricsUncompressed, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct MetricsCompressedHeader { + format: EntryFormat, + metrics_count: pcf_u16, + // Followed by `metrics_count` MetricsCompressed structs + metrics_base: (), +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct MetricsUncompressedHeader { + format: EntryFormat, + metrics_count: pcf_u32, + // Followed by `metrics_count` MetricsUncompressed structs + metrics_base: (), +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct BitmapTableHeader { + format: EntryFormat, + glyph_count: pcf_u32, + // Followed by glyph_count offsets: [u32; glyph_count] + /* byte offsets to bitmap data */ + // Followed by bitmapSizes: [u32; 4] + /* the size the bitmap data will take up depending on various padding options */ + /* which one is actually used in the file is given by (format&3) */ + // Followed by bitmap_data: [u8; bitmapSizes[format & 0b11]] + /* the bitmap data. format contains flags that indicate: */ + /* the byte order (format&4 => LSByte first)*/ + /* the bit order (format&8 => LSBit first) */ + /* how each row in each glyph's bitmap is padded (format&3) */ + /* 0=>bytes, 1=>shorts, 2=>ints */ + /* what the bits are stored in (bytes, shorts, ints) (format>>4)&3 */ + /* 0=>bytes, 1=>shorts, 2=>ints */ +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct EncodingTableHeader { + format: EntryFormat, + min_char_or_byte2: pcf_u16, /* As in XFontStruct */ + max_char_or_byte2: pcf_u16, /* As in XFontStruct */ + min_byte1: pcf_u16, /* As in XFontStruct */ + max_byte1: pcf_u16, /* As in XFontStruct */ + default_char: pcf_u16, /* As in XFontStruct */ + // Followed by glyph_indices; + // array of (max_char_or_byte2 - min_char_or_byte2 + 1)*(max_byte1 - min_byte1 + 1) u16 entries, + /* Gives the glyph index that corresponds to each encoding value */ + /* a value of 0xffff means no glyph for that encoding */ + glyph_indices_base: (), +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct ScalableWidthHeader { + format: EntryFormat, + glyph_count: pcf_u32, + // Followed by swidths: [u32; glyph_count] +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct GlyphNamesHeader { + format: EntryFormat, + glyph_count: pcf_u32, + // Followed by glyph_count offsets: [u32; glyph_count] + // Followed by string_size: u32 + // Followed by string data [u8; string_size] +} + +impl EntryFormat { + fn endianness(&self) -> Endian { + match (self.0 & EntryFormat::PCF_BYTE_MASK) != 0 { + true => Endian::Big, + false => Endian::Little, + } + } +} + +struct PropertiesData<'a> { + endian: Endian, + properties: &'a [Property], + string_data: &'a [u8], +} +enum PropValue<'a> { + Number(u32), + String(&'a [u8]), +} +impl<'a> PropertiesData<'a> { + fn iter_props(&self) -> impl Iterator { + self.properties.iter().map(|prop| { + let name_off = prop.name_offset.read(self.endian) as usize; + let str_prop = prop.is_string_prop != 0; + let value = prop.value.read(self.endian); + + let str = core::ffi::CStr::from_bytes_until_nul(&self.string_data[name_off..]).unwrap(); + if str_prop { + let value = + core::ffi::CStr::from_bytes_until_nul(&self.string_data[value as usize..]) + .unwrap(); + (str.to_bytes(), PropValue::String(value.to_bytes())) + } else { + (str.to_bytes(), PropValue::Number(value)) + } + }) + } +} + +struct EncodingData<'a> { + endian: Endian, + char_mode: bool, + min_byte1: u32, + max_byte1: u32, + min_byte2: u32, + max_byte2: u32, + glyph_indices: &'a [pcf_u16], +} +impl<'a> EncodingData<'a> { + fn get_glyph(&self, c: char) -> Option { + let c = c as u32; + let index; + if self.char_mode { + index = c - self.min_byte2; + if c > self.max_byte2 { + return None; + } + } else { + let byte1 = (c >> 8) & 0xFF; + let byte2 = (c) & 0xFF; + if byte1 < self.min_byte1 + || byte1 > self.max_byte1 + || byte2 < self.min_byte2 + || byte2 > self.max_byte2 + { + // TODO: check > vs >= + return None; + } + index = (byte1 - self.min_byte1) * (self.max_byte2 - self.min_byte2 + 1) + + (byte2 - self.min_byte2); + } + let glyph = self + .glyph_indices + .get(index as usize) + .map(|i| i.read(self.endian)); + glyph.filter(|&v| v != 0xFFFF) + } +} + +struct GlyphNameData<'a> { + endian: Endian, + offsets: &'a [pcf_u32], + string_data: &'a [u8], +} +impl<'a> GlyphNameData<'a> { + fn get_name(&self, glyph: u16) -> Option<&'a [u8]> { + let offset = self.offsets.get(glyph as usize)?.read(self.endian) as usize; + let name = core::ffi::CStr::from_bytes_until_nul(&self.string_data[offset..]).unwrap(); + Some(name.to_bytes()) + } +} + +enum MetricsData<'a> { + Compressed(&'a [MetricsCompressed]), + Uncompressed(&'a [MetricsUncompressed], Endian), +} +#[derive(Debug)] +pub struct MetricsUnpacked { + pub left_sided_bearing: i16, + pub right_side_bearing: i16, + pub character_width: i16, + pub character_ascent: i16, + pub character_descent: i16, + pub character_attributes: u16, +} +impl MetricsCompressed { + fn unpack(&self) -> MetricsUnpacked { + MetricsUnpacked { + left_sided_bearing: self.left_sided_bearing as i16 - 0x80, + right_side_bearing: self.right_side_bearing as i16 - 0x80, + character_width: self.character_width as i16 - 0x80, + character_ascent: self.character_ascent as i16 - 0x80, + character_descent: self.character_descent as i16 - 0x80, + character_attributes: 0, + } + } +} +impl MetricsUncompressed { + fn unpack(&self, endian: Endian) -> MetricsUnpacked { + MetricsUnpacked { + left_sided_bearing: self.left_sided_bearing.read(endian), + right_side_bearing: self.right_side_bearing.read(endian), + character_width: self.character_width.read(endian), + character_ascent: self.character_ascent.read(endian), + character_descent: self.character_descent.read(endian), + character_attributes: self.character_attributes.read(endian), + } + } +} +impl<'a> MetricsData<'a> { + fn get_metrics(&self, glyph: u16) -> Option { + match *self { + MetricsData::Compressed(data) => Some(data.get(glyph as usize)?.unpack()), + MetricsData::Uncompressed(data, endian) => { + Some(data.get(glyph as usize)?.unpack(endian)) + } + } + } +} + +struct BitmapData<'a> { + endian: Endian, + pad_mode: u8, + size_mode: u8, + least_bit_first: bool, + offsets: &'a [pcf_u32], + bitmap: &'a [u8], +} +impl<'a> BitmapData<'a> { + pub fn unpack_bitmap( + &self, + glyph: u16, + width: usize, + height: usize, + buffer: &mut [bool], + start: usize, + buf_row_stride: usize, + // scale: usize, + ) -> Option<()> { + let offset = self.offsets.get(glyph as usize)?.read(self.endian); + + let row_pad = 1 << self.pad_mode; + let scan = 1 << self.size_mode; + + assert_eq!(scan, 1, "multi-byte scan for PCF not yet implemented"); + assert!(self.least_bit_first, "MSBit first PCF not yet implemented"); + + // TODO: caching and performance improvements + if scan == 1 { + let rows = height; + let row_size = width.div_ceil(8); + let row_stride = row_size.next_multiple_of(row_pad); + let offset = offset as usize; + let mut buf_row_start = start; + for r in 0..rows { + let row_base = offset + r * row_stride; + for c in 0..row_size { + let val = self.bitmap[row_base + c]; + for b in 0..8 { + if (val >> (7 - b)) & 1 != 0 { + let i = buf_row_start + (c * 8 + b); + buffer[i] = true; + } + } + } + buf_row_start += buf_row_stride; + } + } + Some(()) + } + + pub fn draw_bitmap( + &self, + glyph: u16, + width: usize, + height: usize, + buffer: &mut [u32], + start: usize, + buf_row_stride: usize, + scale: usize, + color: u32, + ) -> Option<()> { + let offset = self.offsets.get(glyph as usize)?.read(self.endian); + + let row_pad = 1 << self.pad_mode; + let scan = 1 << self.size_mode; + + assert_eq!(scan, 1, "multi-byte scan for PCF not yet implemented"); + assert!(self.least_bit_first, "MSBit first PCF not yet implemented"); + + // TODO: caching and performance improvements + if scan == 1 { + let rows = height; + let row_size = width.div_ceil(8); + let row_stride = row_size.next_multiple_of(row_pad); + // let safety_margin = (scale - 1) * buf_row_stride + (scale - 1); + + let mut bmp_row_base = offset as usize; + let mut dst_row_base = start; + for _ in 0..rows { + assert!(bmp_row_base + width / 8 < self.bitmap.len()); + for c in 0..width { + let (byte, bit) = (c / 8, c % 8); + if (self.bitmap[bmp_row_base + byte] >> (7 - bit)) & 1 != 0 { + let i = dst_row_base + c * scale; + // if i + safety_margin >= buffer.len() { + // continue; + // } + for ro in 0..scale { + for co in 0..scale { + // TODO: index out of bounds when writing char partially outside of buffer + buffer[i + ro * buf_row_stride + co] = color; + } + } + } + } + bmp_row_base += row_stride; + dst_row_base += buf_row_stride * scale; + } + } + + Some(()) + } +} + +pub struct LoadedPCF<'a> { + prop_data: PropertiesData<'a>, + encoding_data: EncodingData<'a>, + glyph_name_data: GlyphNameData<'a>, + metrics_data: MetricsData<'a>, + bitmap_data: BitmapData<'a>, + pub accelerator_data: AcceleratorUnpacked, +} + +#[derive(Copy, Clone)] +pub struct GlyphInfo { + pub glyph: u16, + pub width: usize, + pub height: usize, + pub pad_top: usize, + pub pad_left: usize, +} + +impl<'a> LoadedPCF<'a> { + pub fn dimensions(&self) -> (usize, usize) { + let bounds = &self.accelerator_data.maxbounds; + ( + (bounds.character_ascent + bounds.character_descent) as usize, + bounds.character_width as usize, + ) + } + // TODO: proper layouting + pub fn prep_char(&self, char: char) -> Option { + let glyph = self.encoding_data.get_glyph(char); + if let Some(glyph) = glyph { + let metrics = self.metrics_data.get_metrics(glyph).unwrap(); + let width = metrics.character_width as usize; + let height = (metrics.character_ascent + metrics.character_descent) as usize; + + let base_ascent = self.accelerator_data.maxbounds.character_ascent as usize; + let pad_top = base_ascent - metrics.character_ascent as usize; + let pad_left = metrics.left_sided_bearing as usize; + + Some(GlyphInfo { + glyph, + width, + height, + pad_top, + pad_left, + }) + } else { + // TODO: fallback glyph (box or something) + None + } + } + pub fn draw_glyph( + &self, + glyph: GlyphInfo, + buffer: &mut [u32], + start: usize, + row_stride: usize, + scale: usize, + color: u32, + ) { + let start = start + row_stride * scale * glyph.pad_top + glyph.pad_left * scale; + self.bitmap_data.draw_bitmap( + glyph.glyph, + glyph.width, + glyph.height, + buffer, + start, + row_stride, + scale, + color, + ); + } + pub fn unpack_glyph( + &self, + glyph: GlyphInfo, + buffer: &mut [bool], + start: usize, + row_stride: usize, + // scale: usize, + ) { + let start = start + row_stride * glyph.pad_top + glyph.pad_left; + self.bitmap_data.unpack_bitmap( + glyph.glyph, + glyph.width, + glyph.height, + buffer, + start, + row_stride, + ); + } + pub fn draw_char( + &self, + char: char, + buffer: &mut [u32], + start: usize, + row_stride: usize, + scale: usize, + color: u32, + ) -> Option { + let info = self.prep_char(char); + if let Some(info) = info { + self.draw_glyph(info, buffer, start, row_stride, scale, color); + } + info + } + pub fn draw_string( + &self, + str: &str, + buffer: &mut [u32], + start: usize, + wrap_at: Option, + row_stride: usize, + scale: usize, + color: u32, + ) -> usize { + let maxbounds = &self.accelerator_data.maxbounds; + let line_height = + scale * (maxbounds.character_ascent + maxbounds.character_descent) as usize; + let wrap_at = wrap_at.unwrap_or(usize::MAX); + let mut x = 0; + let mut y = 0; + for char in str.chars() { + match char { + '\r' => continue, + '\n' => { + x = 0; + y += line_height; + continue; + } + _ => (), + } + let glyph = self.prep_char(char); + if let Some(glyph) = glyph { + if x + glyph.width * scale >= wrap_at { + x = 0; + y += line_height; + } + self.draw_glyph( + glyph, + buffer, + start + y * row_stride + x, + row_stride, + scale, + color, + ); + x += glyph.width * scale; + } + } + y + line_height + } +} + +// pub fn debug_pcf(font: LoadedPCF<'_>) { +// let char = 'a'; +// let glyph = font.encoding_data.get_glyph(char); +// println!("{:?}: glyph {:?}, name {:?}", char, glyph, glyph.and_then(|g| core::str::from_utf8(font.glyph_name_data.get_name(g)?).ok())); + +// let metrics = glyph.and_then(|g| font.metrics_data.get_metrics(g)).unwrap(); +// println!("Metrics: {:?}", metrics); + +// font.bitmap_data.glyph_bitmap(glyph.unwrap(), metrics.character_width as usize, (metrics.character_ascent + metrics.character_descent) as usize); +// } + +pub fn load_pcf(data: &[u8]) -> Result, ()> { + let header: &PCFHeader = try_from_bytes(&data[..size_of::()]).unwrap(); + if header.header != *b"\x01fcp" { + // println!("Wrong header: {:?}", header.header); + return Err(()); + } + + // println!("{:?}", header); + + let table_count = header.table_count as usize; + let toc_base = size_of::(); + let toc_size = size_of::() * table_count as usize; + let toc: &[toc_entry] = try_cast_slice(&data[toc_base..][..toc_size]).unwrap(); + + let mut prop_data = None; + let mut encoding_data = None; + let mut glyph_name_data = None; + let mut metrics_data = None; + let mut bitmap_data = None; + let mut accelerator_data = None; + + for entry in toc.iter() { + // println!("{:?}", toc[i]); + // TODO: why are the sizes wrong??? + let table_data = &data[entry.offset as usize..]; + let table_data = &table_data[..(entry.size as usize).min(table_data.len())]; + + match entry.type_.0 { + EntryType::PCF_PROPERTIES => { + let header: &PropertiesTableHeader = + try_from_bytes(&table_data[..size_of::()]).unwrap(); + let endian = header.format.endianness(); + + let nprops = header.nprops.read(endian) as usize; + let properties_base = size_of::(); + let properties_end = properties_base + size_of::() * nprops; + let properties: &[Property] = + try_cast_slice(&table_data[properties_base..properties_end]).unwrap(); + + let string_base = properties_end.next_multiple_of(4); + let string_size: &pcf_u32 = + bytemuck::from_bytes(&table_data[string_base..string_base + 4]); + let string_size = string_size.read(endian) as usize; + let string_data = &table_data[string_base + 4..string_base + 4 + string_size]; + + prop_data = Some(PropertiesData { + endian, + properties, + string_data, + }); + } + EntryType::PCF_ACCELERATORS | EntryType::PCF_BDF_ACCELERATORS => { + let table: &AcceleratorTable = + try_from_bytes(&table_data[..size_of::()]).unwrap(); + let endian = table.format.endianness(); + + accelerator_data = Some(AcceleratorUnpacked { + format: table.format, + noOverlap: table.noOverlap, + constantMetrics: table.constantMetrics, + terminalFont: table.terminalFont, + constantWidth: table.constantWidth, + inkInside: table.inkInside, + inkMetrics: table.inkMetrics, + drawDirection: table.drawDirection, + padding: table.padding, + fontAscent: table.fontAscent.read(endian), + fontDescent: table.fontDescent.read(endian), + maxOverlap: table.maxOverlap.read(endian), + minbounds: table.minbounds.unpack(endian), + maxbounds: table.maxbounds.unpack(endian), + }); + } + EntryType::PCF_METRICS => { + let endian = entry.format.endianness(); + if (entry.format.0 & EntryFormat::PCF_COMPRESSED_METRICS) != 0 { + let header: &MetricsCompressedHeader = + try_from_bytes(&table_data[..size_of::()]) + .unwrap(); + let metrics_count = header.metrics_count.read(endian) as usize; + let metrics_base = offset_of!(MetricsCompressedHeader, metrics_base); + + let metrics_end = metrics_base + size_of::() * metrics_count; + let metrics: &[MetricsCompressed] = + try_cast_slice(&table_data[metrics_base..metrics_end]).unwrap(); + metrics_data = Some(MetricsData::Compressed(metrics)); + } else { + let header: &MetricsUncompressedHeader = + try_from_bytes(&table_data[..size_of::()]) + .unwrap(); + let metrics_count = header.metrics_count.read(endian) as usize; + let metrics_base = offset_of!(MetricsCompressedHeader, metrics_base); + + let metrics_end = + metrics_base + size_of::() * metrics_count; + let metrics: &[MetricsUncompressed] = + try_cast_slice(&table_data[metrics_base..metrics_end]).unwrap(); + metrics_data = Some(MetricsData::Uncompressed(metrics, endian)); + } + } + EntryType::PCF_BITMAPS => { + let header: &BitmapTableHeader = + try_from_bytes(&table_data[..size_of::()]).unwrap(); + let endian = header.format.endianness(); + + let glyph_count = header.glyph_count.read(endian) as usize; + let offsets_base = size_of::(); + let offsets_size = glyph_count * size_of::(); + let offsets: &[pcf_u32] = + try_cast_slice(&table_data[offsets_base..offsets_base + offsets_size]).unwrap(); + + let pad_mode = (header.format.0 & EntryFormat::PCF_GLYPH_PAD_MASK) as u8; + let size_mode = ((header.format.0 & EntryFormat::PCF_SCAN_UNIT_MASK) >> 3) as u8; + let least_bit_first = (header.format.0 & EntryFormat::PCF_BIT_MASK) != 0; + + let bitmap_base = offsets_base + offsets_size; + let bitmap_sizes_size = size_of::<[pcf_u32; 4]>(); + let bitmap_sizes: &[pcf_u32; 4] = + try_from_bytes(&table_data[bitmap_base..bitmap_base + bitmap_sizes_size]) + .unwrap(); + let bitmap_size = bitmap_sizes[pad_mode as usize].read(endian) as usize; + let bitmap = &table_data[bitmap_base + bitmap_sizes_size + ..bitmap_base + bitmap_sizes_size + bitmap_size]; + + bitmap_data = Some(BitmapData { + endian, + pad_mode, + size_mode, + least_bit_first, + offsets, + bitmap, + }); + } + EntryType::PCF_BDF_ENCODINGS => { + let header: &EncodingTableHeader = + try_from_bytes(&table_data[..size_of::()]).unwrap(); + let endian = header.format.endianness(); + + let min_byte1 = header.min_byte1.read(endian) as u32; + let max_byte1 = header.max_byte1.read(endian) as u32; + let min_byte2 = header.min_char_or_byte2.read(endian) as u32; + let max_byte2 = header.max_char_or_byte2.read(endian) as u32; + + let indices_base = offset_of!(EncodingTableHeader, glyph_indices_base); + let table_size = (max_byte2 - min_byte2 + 1) * (max_byte1 - min_byte1 + 1); + let glyph_indices: &[pcf_u16] = + try_cast_slice(&table_data[indices_base..][..table_size as usize]).unwrap(); + + let char_mode = min_byte1 == 0 && max_byte1 == 0; + + encoding_data = Some(EncodingData { + endian, + char_mode, + min_byte1, + max_byte1, + min_byte2, + max_byte2, + glyph_indices, + }); + } + EntryType::PCF_GLYPH_NAMES => { + let header: &GlyphNamesHeader = + try_from_bytes(&table_data[..size_of::()]).unwrap(); + let endian = header.format.endianness(); + + let glyph_count = header.glyph_count.read(endian) as usize; + let offsets_base = size_of::(); + let offsets_size = glyph_count * size_of::(); + let offsets: &[pcf_u32] = + try_cast_slice(&table_data[offsets_base..offsets_base + offsets_size]).unwrap(); + + let string_base = offsets_base + offsets_size; + let string_size: &pcf_u32 = + bytemuck::from_bytes(&table_data[string_base..string_base + 4]); + let string_size = string_size.read(endian) as usize; + let string_data = &table_data[string_base + 4..string_base + 4 + string_size]; + + glyph_name_data = Some(GlyphNameData { + endian, + offsets, + string_data, + }); + } + _ => { + // panic!(); + } + } + } + + let font = LoadedPCF { + prop_data: prop_data.unwrap(), + encoding_data: encoding_data.unwrap(), + glyph_name_data: glyph_name_data.unwrap(), + metrics_data: metrics_data.unwrap(), + bitmap_data: bitmap_data.unwrap(), + accelerator_data: accelerator_data.unwrap(), + }; + + // println!("## Properties:"); + // for (name, prop) in font.prop_data.iter_props() { + // match prop { + // PropValue::String(s) => println!("{}: {:?}", core::str::from_utf8(name).unwrap(), core::str::from_utf8(s).unwrap()), + // PropValue::Number(num) => println!("{}: {:?}", core::str::from_utf8(name).unwrap(), num), + // } + // } + + // println!("{:#?}", font.accelerator_data); + + Ok(font) +} diff --git a/crates/console/src/format/qoi.rs b/crates/console/src/format/qoi.rs new file mode 100644 index 00000000..4a523565 --- /dev/null +++ b/crates/console/src/format/qoi.rs @@ -0,0 +1,147 @@ +use crate::color::{de_rgba, rgba}; + +#[derive(Debug)] +pub struct QoiHeader { + pub magic: [u8; 4], // magic bytes "qoif" + pub width: u32, // image width in pixels (BE) + pub height: u32, // image height in pixels (BE) + pub channels: u8, // 3 = RGB, 4 = RGBA + pub colorspace: u8, // 0 = sRGB with linear alpha; 1 = all channels linear +} + +pub fn read_qoi_header(data: &[u8]) -> Option<(QoiHeader, &[u8])> { + data.split_at_checked(14) + .map(|(h, rest)| { + ( + QoiHeader { + magic: h[..4].try_into().unwrap(), + width: u32::from_be_bytes(h[4..][..4].try_into().unwrap()), + height: u32::from_be_bytes(h[8..][..4].try_into().unwrap()), + channels: h[12], + colorspace: h[13], + }, + rest, + ) + }) + .filter(|(h, _)| h.magic == *b"qoif") +} + +fn premultiply(pixel: u32) -> u32 { + // TODO: sRGB conversions + let (r, g, b, a) = de_rgba(pixel); + let (r, g, b) = ( + r as u32 * a as u32 / 255, + g as u32 * a as u32 / 255, + b as u32 * a as u32 / 255, + ); + rgba(r as u8, g as u8, b as u8, a) +} + +pub fn decode_qoi(header: &QoiHeader, mut stream: &[u8], output: &mut [u32], out_stride: usize) { + let mut history = [rgba(0, 0, 0, 0); 64]; + + // ...huh. If the initial alpha is wrong, the hash indexing breaks + // and makes lots of glitch patterns. + let mut prev = rgba(0, 0, 0, 0xFF); + let mut row_idx = 0; + let mut row_base = 0; + let img_width = header.width as usize; + + loop { + let pixel; + stream = match *stream { + // QOI_OP_RGB + [0b1111_1110, r, g, b, ref rest @ ..] => { + pixel = rgba(r, g, b, (prev >> 24) as u8); + rest + } + // QOI_OP_RGBA + [0b1111_1111, r, g, b, a, ref rest @ ..] => { + pixel = rgba(r, g, b, a); + rest + } + // QOI_OP_DIFF + [diff @ (0b0100_0000..=0b0111_1111), ref rest @ ..] => { + let db = ((diff >> 0) & 0b11).wrapping_sub(2); + let dg = ((diff >> 2) & 0b11).wrapping_sub(2); + let dr = ((diff >> 4) & 0b11).wrapping_sub(2); + + let (pr, pg, pb, pa) = de_rgba(prev); + pixel = rgba( + pr.wrapping_add(dr), + pg.wrapping_add(dg), + pb.wrapping_add(db), + pa, + ); + rest + } + // QOI_OP_LUMA + [diff_green @ (0b1000_0000..=0b1011_1111), diff, ref rest @ ..] => { + let dg = (diff_green & 0b0011_1111).wrapping_sub(32); + let db = ((diff >> 0) & 0b0000_1111).wrapping_sub(8).wrapping_add(dg); + let dr = ((diff >> 4) & 0b0000_1111).wrapping_sub(8).wrapping_add(dg); + + let (pr, pg, pb, pa) = de_rgba(prev); + pixel = rgba( + pr.wrapping_add(dr), + pg.wrapping_add(dg), + pb.wrapping_add(db), + pa, + ); + rest + } + // QOI_OP_RUN + [run @ (0b1100_0000..=0b1111_1111), ref rest @ ..] => { + let run = ((run & 0b0011_1111) + 1) as usize; + let color = premultiply(prev); + + let end = row_idx + run; + let row_end = end.min(img_width); + for i in row_idx..row_end { + output[row_base + i] = color; + } + let remainder = end - row_end; + for row in 0..remainder / img_width { + for c in 0..img_width { + output[row_base + (1 + row) * out_stride + c] = color; + } + } + for i in 0..remainder % img_width { + output[row_base + (1 + remainder / img_width) * out_stride + i] = color; + } + + row_base += ((row_idx + run) / img_width) * out_stride; + row_idx = (row_idx + run) % img_width; + + stream = rest; + continue; + } + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] => { + break; + } + // QOI_OP_INDEX + [idx @ (0b0000_0000..=0b0011_1111), ref rest @ ..] => { + pixel = history[idx as usize & 0b0011_1111]; + rest + } + _ => { + // invalid operation + break; + } + }; + + let (r, g, b, a) = de_rgba(pixel); + let hash = (r as usize * 3 + g as usize * 5 + b as usize * 7 + a as usize * 11) % 64; + history[hash] = pixel; + prev = pixel; + + output[row_base + row_idx] = premultiply(pixel); + + row_idx += 1; + + if row_idx >= header.width as usize { + row_idx = 0; + row_base += out_stride; + } + } +} diff --git a/crates/console/src/grid.rs b/crates/console/src/grid.rs new file mode 100644 index 00000000..12407b8b --- /dev/null +++ b/crates/console/src/grid.rs @@ -0,0 +1,57 @@ +use alloc::boxed::Box; +use alloc::vec; + +pub struct CharGrid { + pub chars: Box<[char]>, + pub colors: Box<[Colors]>, + pub rows: usize, + pub cols: usize, + pub stride: usize, +} + +#[derive(Copy, Clone)] +pub struct Colors { + pub fg: u32, + pub bg: u32, +} + +pub struct GridRef<'a> { + pub chars: &'a mut [char], + pub colors: &'a mut [Colors], + pub cols: usize, + pub rows: usize, + pub stride: usize, +} + +impl CharGrid { + pub fn region(&mut self, r: usize, c: usize, rows: usize, cols: usize) -> GridRef<'_> { + let base = r * self.stride + c; + GridRef { + chars: &mut self.chars[base..], + colors: &mut self.colors[base..], + cols, + rows, + stride: self.stride, + } + } + + pub fn new( + (width, height): (usize, usize), + char_dims: (usize, usize), + scale: usize, + hpad: usize, + vpad: usize, + fill_color: Colors, + ) -> Self { + let rows = (height - 2 * vpad) / (char_dims.0 * scale); + let cols = (width - 2 * hpad) / (char_dims.1 * scale); + let stride = cols; + Self { + chars: vec![' '; rows * stride].into_boxed_slice(), + colors: vec![fill_color; rows * stride].into_boxed_slice(), + rows, + cols, + stride, + } + } +} diff --git a/crates/console/src/input.rs b/crates/console/src/input.rs new file mode 100644 index 00000000..67327ead --- /dev/null +++ b/crates/console/src/input.rs @@ -0,0 +1,69 @@ +use display_client::proto; + +use crate::editor; + +pub fn remap_input( + input: proto::InputEvent, + modifiers: editor::Modifiers, +) -> Option { + use editor::{FuncKey, KeyEvent, Keypress}; + use proto::ScanCode; + if input.kind == proto::InputEvent::KIND_KEY { + let mode = input.data1; + let scan = input.data2; + + let char = proto::SCANCODES.get(scan as usize).copied().flatten(); + let resolved = if modifiers.shift { + proto::SCANCODES_SHIFTED + .get(scan as usize) + .copied() + .flatten() + .or(char) + } else { + char + }; + + let keypress = match ScanCode(scan) { + ScanCode::DOWN => Keypress::Function(modifiers, FuncKey::Down), + ScanCode::LEFT => Keypress::Function(modifiers, FuncKey::Left), + ScanCode::RIGHT => Keypress::Function(modifiers, FuncKey::Right), + ScanCode::UP => Keypress::Function(modifiers, FuncKey::Up), + + ScanCode::ENTER => Keypress::Function(modifiers, FuncKey::Enter), + ScanCode::TAB => Keypress::Function(modifiers, FuncKey::Tab), + + ScanCode::BACKSPACE => Keypress::Function(modifiers, FuncKey::Backspace), + ScanCode::DELETE => Keypress::Function(modifiers, FuncKey::Delete), + ScanCode::END => Keypress::Function(modifiers, FuncKey::End), + ScanCode::ESCAPE => Keypress::Function(modifiers, FuncKey::Escape), + ScanCode::HOME => Keypress::Function(modifiers, FuncKey::Home), + ScanCode::INSERT => Keypress::Function(modifiers, FuncKey::Insert), + // ScanCode::MENU => Keypress::Function(modifiers, FuncKey::Menu), + ScanCode::PAGE_DOWN => Keypress::Function(modifiers, FuncKey::PageDown), + ScanCode::PAGE_UP => Keypress::Function(modifiers, FuncKey::PageUp), + // ScanCode::LEFT_SHIFT => Keypress::Function(modifiers, FuncKey::LeftShift), + // ScanCode::RIGHT_SHIFT => Keypress::Function(modifiers, FuncKey::RightShift), + // ScanCode::LEFT_CTRL => Keypress::Function(modifiers, FuncKey::LeftCtrl), + // ScanCode::RIGHT_CTRL => Keypress::Function(modifiers, FuncKey::RightCtrl), + // ScanCode::LEFT_ALT => Keypress::Function(modifiers, FuncKey::LeftAlt), + // ScanCode::RIGHT_ALT => Keypress::Function(modifiers, FuncKey::RightAlt), + _ => { + if let Some(key) = char { + Keypress::Char(modifiers, key, resolved.unwrap_or(key)) + } else { + return None; + } + } + }; + + let event = match mode { + 1 => KeyEvent::Press(keypress), + 2 => KeyEvent::Release(keypress), + 3 => KeyEvent::Repeat(keypress), + _ => return None, + }; + Some(event) + } else { + None + } +} diff --git a/crates/console/src/main.rs b/crates/console/src/main.rs new file mode 100644 index 00000000..59a1f685 --- /dev/null +++ b/crates/console/src/main.rs @@ -0,0 +1,226 @@ +#![no_std] +#![cfg_attr(not(test), no_main)] + +extern crate alloc; +extern crate display_client; + +#[macro_use] +extern crate ulib; + +pub mod format; + +mod color; +pub mod editor; +mod grid; +mod input; +mod vt100; + +use display_client::proto; + +#[macro_use] +#[doc(hidden)] +pub(crate) mod macros { + // https://users.rust-lang.org/t/can-i-conveniently-compile-bytes-into-a-rust-program-with-a-specific-alignment/24049/2 + #[repr(C)] + pub struct AlignedAs { + pub _align: [Align; 0], + pub bytes: Bytes, + } + macro_rules! include_bytes_align { + ($align_ty:ty, $path:literal) => {{ + // const block expression to encapsulate the static + use $crate::macros::AlignedAs; + // this assignment is made possible by CoerceUnsized + static ALIGNED: &AlignedAs<$align_ty, [u8]> = &AlignedAs { + _align: [], + bytes: *include_bytes!($path), + }; + &ALIGNED.bytes + }}; + } +} + +use ulib::sys::{pipe, pwrite_all, spawn_elf, FileDesc, SpawnArgs}; + +#[no_mangle] +pub extern "C" fn main() { + // Useful font resources: + // - https://adafruit.github.io/web-bdftopcf/ + // - https://github.com/Tecate/bitmap-fonts + + let compressed_font = include_bytes_align!(u32, "../ctrld-fixed-10r.pcf.lz4"); + let size = lz4::frame::read_frame(compressed_font) + .unwrap() + .0 + .content_size() + .unwrap(); + let mut font_data = alloc::vec![0; size as usize]; + let font_data = lz4::decode_into(compressed_font, &mut font_data).unwrap(); + + let font = format::pcf::load_pcf(font_data).unwrap(); + + let mut buf = display_client::connect(); + + let (width, height) = ( + buf.video_meta.width as usize, + buf.video_meta.height as usize, + ); + let row_stride = buf.video_meta.row_stride as usize / 4; + + println!("Filling screen"); + buf.video_mem().fill(color::rgba(0, 0, 0, 255)); + + let char_dims = font.dimensions(); + let scale = 1; + let hpad = 2; + let vpad = 0; + + let fill_color = grid::Colors { + fg: color::rgba(255, 255, 255, 255), + bg: color::rgba(0, 0, 0, 0), + }; + let mut grid = grid::CharGrid::new( + (width, height), + font.dimensions(), + scale, + hpad, + vpad, + fill_color, + ); + + let mut emulator = vt100::EmulatorState::new(grid.rows, grid.cols); + + let mut modifiers = editor::Modifiers::NONE; + let mut editor = editor::LineEditor::new(); + + let cwd = 3; + let fd = ulib::sys::openat(cwd, b"shell.elf", 0, 0).unwrap(); + + let (_shell, shell_stdin_tx, shell_stdout_rx) = { + let (shell_stdin_rx, shell_stdin_tx) = pipe(0).unwrap(); + let (shell_stdout_rx, shell_stdout_tx) = pipe(0).unwrap(); + + let shell = spawn_elf(&SpawnArgs { + fd, + stdin: Some(shell_stdin_rx), + stdout: Some(shell_stdout_tx), + }) + .unwrap(); + (shell, shell_stdin_tx, shell_stdout_rx) + }; + + loop { + let time_us = unsafe { ulib::sys::sys_get_time_ms() as u64 } * 1000; + + while let Some(ev) = buf.server_to_client_queue().try_recv() { + match ev.kind { + proto::EventKind::INPUT => { + handle_input(ev, &mut modifiers, &mut editor, shell_stdin_tx, time_us) + } + _ => (), + } + } + + { + let data = buf.video_mem(); + data.fill(color::rgba(0, 0, 0, 255)); + + let mut buf = [0; 4096]; + while let Ok(n @ 1..) = ulib::sys::pread(shell_stdout_rx, &mut buf, 0) { + emulator.input(&buf[..n]); + } + + emulator.update(grid.region(0, 0, grid.rows, grid.cols)); + + let blink = ((time_us - editor.last_keypress) < 0_600_000 + || ((time_us - editor.last_keypress) % 1_200_000 < 600_000)) + && editor.selection_range().len() == 0; + + editor::draw_editor_into_console(&editor, &mut emulator, fill_color.fg, blink); + + render_grid(&grid, char_dims, scale, vpad, hpad, data, row_stride, &font); + } + + buf.client_to_server_queue() + .try_send(proto::Event { + kind: proto::EventKind::PRESENT, + data: [0; 7], + }) + .ok(); + + // signal(video)? for sync + ulib::sys::sem_down(buf.get_sem_fd(buf.present_sem)).unwrap(); + } +} + +fn render_grid( + grid: &grid::CharGrid, + char_dims: (usize, usize), + scale: usize, + vpad: usize, + hpad: usize, + data: &mut [u32], + row_stride: usize, + font: &format::pcf::LoadedPCF<'_>, +) { + for r in 0..grid.rows { + for c in 0..grid.cols { + let ch = grid.chars[r * grid.cols + c]; + let colors = grid.colors[r * grid.cols + c]; + + let y = char_dims.0 * scale * r + vpad; + let x = char_dims.1 * scale * c + hpad; + + if (colors.bg & 0xFF000000) != 0 { + for pr in 0..char_dims.0 * scale { + for pc in 0..char_dims.1 * scale { + let pixel = &mut data[(y + pr) * row_stride + x + pc]; + *pixel = color::blend(colors.bg, *pixel); + } + } + } + + if ch != ' ' && (colors.fg & 0xFF000000) != 0 { + font.draw_char(ch, data, y * row_stride + x, row_stride, scale, colors.fg); + } + } + } +} + +fn handle_input( + ev: proto::Event, + modifiers: &mut editor::Modifiers, + editor: &mut editor::LineEditor, + shell_stdin_tx: FileDesc, + time_us: u64, +) { + use proto::EventData; + let data = proto::InputEvent::parse(&ev).expect("TODO"); + if data.kind == proto::InputEvent::KIND_KEY { + match proto::ScanCode(data.data2) { + proto::ScanCode::LEFT_SHIFT => modifiers.shift = data.data1 == 1, + proto::ScanCode::RIGHT_SHIFT => modifiers.shift = data.data1 == 1, + proto::ScanCode::LEFT_CTRL => modifiers.ctrl = data.data1 == 1, + proto::ScanCode::RIGHT_CTRL => modifiers.ctrl = data.data1 == 1, + proto::ScanCode::LEFT_ALT => modifiers.alt = data.data1 == 1, + proto::ScanCode::RIGHT_ALT => modifiers.alt = data.data1 == 1, + _ => (), + }; + + if let Some(ev) = input::remap_input(data, *modifiers) { + if matches!( + ev, + editor::KeyEvent::Press(editor::Keypress::Function( + editor::Modifiers::NONE, + editor::FuncKey::Enter + )) + ) { + pwrite_all(shell_stdin_tx, editor.buf.as_bytes(), 0).unwrap(); + pwrite_all(shell_stdin_tx, b"\r", 0).unwrap(); + editor.clear(); + } else { + editor::editor_input(editor, ev, time_us); + } + } + } +} diff --git a/crates/console/src/vt100.rs b/crates/console/src/vt100.rs new file mode 100644 index 00000000..eb6e9254 --- /dev/null +++ b/crates/console/src/vt100.rs @@ -0,0 +1,164 @@ +use alloc::boxed::Box; +use alloc::vec; + +use crate::color::rgba; +use crate::grid::{Colors, GridRef}; + +// TODO: line/row separation +// TODO: handling double width chars (emoji) +pub struct EmulatorState { + pub rows: usize, + pub cols: usize, + pub scrolled_rows: usize, + stride: usize, + chars: Box<[char]>, + colors: Box<[Colors]>, + changed: bool, + + pub cursor: GridCoords, + cur_mode: Colors, +} + +#[derive(Copy, Clone)] +pub struct GridCoords { + pub row: usize, + pub col: usize, +} + +const DEFAULT_COLOR: Colors = Colors { + fg: rgba(255, 255, 255, 255), + bg: rgba(0, 0, 0, 0), +}; + +impl EmulatorState { + pub fn new(rows: usize, cols: usize) -> Self { + let stride = cols.next_multiple_of(4); + Self { + rows, + cols, + scrolled_rows: 0, + stride, + chars: vec![' '; rows * stride].into_boxed_slice(), + colors: vec![DEFAULT_COLOR; rows * stride].into_boxed_slice(), + changed: true, + cursor: GridCoords { row: 0, col: 0 }, + cur_mode: DEFAULT_COLOR, + } + } + + fn scroll(&mut self, distance: usize) { + if distance == 0 { + return; + } + for row in 0..self.rows.saturating_sub(distance) { + let dst_start = row * self.stride; + let dst_end = dst_start + self.stride; + let src_start = (row + distance) * self.stride - dst_end; + let src_end = src_start + self.stride; + + let (dst, src) = self.chars.split_at_mut(dst_end); + dst[dst_start..dst_end].copy_from_slice(&src[src_start..src_end]); + let (dst, src) = self.colors.split_at_mut(dst_end); + dst[dst_start..dst_end].copy_from_slice(&src[src_start..src_end]); + } + + let clear_start = self.rows.saturating_sub(distance); + let clear_end = self.rows; + let clear_idx_range = clear_start * self.stride..clear_end * self.stride; + + let fill_color = DEFAULT_COLOR; + self.chars[clear_idx_range.clone()].fill(' '); + self.colors[clear_idx_range].fill(fill_color); + + self.cursor.row = self.cursor.row.saturating_sub(distance); + self.scrolled_rows += distance; + } + + pub fn should_wrap(&self) -> bool { + self.cursor.col >= self.cols + } + pub fn check_wrap(&mut self) { + if self.should_wrap() { + self.wrap(); + } + } + pub fn wrap(&mut self) { + self.cursor.col = 0; + self.cursor.row += 1; + if self.cursor.row == self.rows { + self.scroll(1); + } + } + + pub fn set_char_color(&mut self, pos: GridCoords, char: char, mode: Colors) { + if pos.row < self.rows && pos.col < self.cols { + self.chars[pos.row * self.stride + pos.col] = char; + self.colors[pos.row * self.stride + pos.col] = mode; + } + } + + pub fn input(&mut self, text: &[u8]) { + for chunk in text.utf8_chunks() { + for c in chunk.valid().chars() { + match c { + '\r' => self.cursor.col = 0, + '\n' => self.wrap(), + '\t' => { + self.cursor.col = (self.cursor.col + 1).next_multiple_of(8); + self.check_wrap(); + } + _ => { + self.set_char_color(self.cursor, c, self.cur_mode); + self.cursor.col += 1; + self.check_wrap(); + } + } + } + for _ in chunk.invalid() { + self.set_char_color(self.cursor, ' ', self.cur_mode); + self.cursor.col += 1; + self.check_wrap(); + } + } + self.changed = true; + } + + pub fn update(&mut self, grid: GridRef<'_>) { + if self.changed { + for row in 0..self.rows.min(grid.rows) { + for col in 0..self.cols.min(grid.cols) { + let src = row * self.stride + col; + let dst = row * grid.stride + col; + grid.chars[dst] = self.chars[src]; + grid.colors[dst] = self.colors[src]; + } + } + } + } +} + +// #[test] +// fn term_test() { +// let width = 1280; +// let height = 720; +// let char_dims = (16, 8); + +// let scale = 2; +// let hpad = 2; +// let vpad = 0; +// let rows = (height - 2 * vpad) / (char_dims.0 * scale); +// let cols = (width - 2 * hpad) / (char_dims.1 * scale); + +// let mut text_buf = crate::TextBuf::new(); + +// let mut emulator = EmulatorState::new(rows - 2, cols - 2); +// // let mut log = LogGenerator::new(); + +// loop { +// if let Some(text) = log.next(&mut text_buf) { +// emulator.input(text); +// } + +// // emulator.update(&mut grid, rows - 2, cols - 2, cols + 1, cols); +// } +// } diff --git a/crates/display-client/Cargo.toml b/crates/display-client/Cargo.toml new file mode 100644 index 00000000..12324855 --- /dev/null +++ b/crates/display-client/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "display-client" +version = "0.1.0" +edition = "2021" + +[dependencies] +display-proto = { path = "../display-proto" } +ulib = { path = "../ulib" } + +[dev-dependencies] +ulib = { path = "../ulib", features = ["test"] } diff --git a/crates/display-client/src/lib.rs b/crates/display-client/src/lib.rs new file mode 100644 index 00000000..6659f456 --- /dev/null +++ b/crates/display-client/src/lib.rs @@ -0,0 +1,36 @@ +#![no_std] +#![cfg_attr(not(test), no_main)] + +pub extern crate display_proto as proto; + +#[allow(unused_imports)] +#[macro_use] +extern crate ulib; + +use proto::BufferHandle; +use ulib::sys::{mmap, recv, send}; + +pub fn connect() -> BufferHandle { + let server_socket = 12; + let message = ulib::sys::Message { + tag: 0x101, + objects: [u32::MAX, u32::MAX, u32::MAX, u32::MAX], + }; + send(server_socket, &message, &[], 0); + + let mut buf = [0u8; 64]; + let (_len, msg) = recv(server_socket, &mut buf, 0).unwrap(); + assert!(msg.tag == 0x100); + let fd = msg.objects[0]; + + let size = u64::from_le_bytes(buf[0..8].try_into().unwrap()); + let buffer = unsafe { mmap(0, size as usize, 0, 0, fd, 0) }.unwrap(); + let header = buffer.cast::(); + + let handle = unsafe { BufferHandle::new(header, &msg.objects) }; + handle +} + +pub unsafe fn disconnect(_buf: *mut proto::BufferHeader) { + // TODO: disconnect? +} diff --git a/crates/display-proto/Cargo.toml b/crates/display-proto/Cargo.toml new file mode 100644 index 00000000..2886fcc7 --- /dev/null +++ b/crates/display-proto/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "display-proto" +version = "0.1.0" +edition = "2021" + +[dependencies] +bytemuck = { version = "1.19.0", default-features = false } diff --git a/crates/display-proto/src/lib.rs b/crates/display-proto/src/lib.rs new file mode 100644 index 00000000..2c969031 --- /dev/null +++ b/crates/display-proto/src/lib.rs @@ -0,0 +1,437 @@ +#![no_std] + +extern crate core; + +mod local; + +pub use local::BufferHandle; + +use core::mem::MaybeUninit; +use core::sync::atomic::{AtomicU32, AtomicU8, Ordering}; + +#[allow(non_camel_case_types)] +type au8 = AtomicU8; +#[allow(non_camel_case_types)] +type au32 = AtomicU32; + +#[repr(C)] +#[derive(Copy, Clone, PartialEq)] +pub struct SemDescriptor(pub u32); + +#[repr(C)] +pub struct BufferHeader { + pub version: au32, + pub magic: au32, + pub kill_switch: au32, + pub last_words: [au8; 32], + + pub meta: GlobalMeta, + + pub client_to_server_queue: EventQueue, + pub server_to_client_queue: EventQueue, + + pub video_meta: VideoMeta, + pub term_meta: TermMeta, + + pub present_sem: SemDescriptor, +} + +#[repr(C)] +pub struct GlobalMeta { + pub segment_size: u32, + // Offset from the start of the buffer to the start of vmem + pub vmem_offset: u32, + pub vmem_size: u32, +} + +#[repr(C)] +pub struct VideoMeta { + pub width: u16, + pub height: u16, + pub row_stride: u16, + pub bytes_per_pixel: u8, + pub bit_layout: u8, + + pub present_ts: u64, +} + +#[repr(C)] +pub struct TermMeta { + pub rows: u16, + pub cols: u16, +} + +#[repr(C)] +#[derive(PartialEq, Eq)] +pub struct EventKind(pub u64); + +#[repr(C)] +pub struct Event { + pub kind: EventKind, + pub data: [u64; 7], +} + +const EVENT_BUF_SIZE: usize = 128; + +// TODO: Efficient ringbuffer (cache lines?) +// TODO: frozen event repr? (ie. bytemuck events?) +// head = index of next unused element slot +// tail = index of oldest element +// empty if head == tail (mod len) +// ((head - tail).mod(len) == 0) +// full if head == tail - 1 (mod len) +// ((head - (tail - 1)).mod(len) == 0) +// Wastes one slot, but that's probably fine +#[repr(C)] +pub struct EventQueue { + pub head: AtomicU32, + pub elems: core::cell::UnsafeCell<[MaybeUninit; EVENT_BUF_SIZE]>, + pub tail: AtomicU32, +} + +const _: () = assert!(EVENT_BUF_SIZE.next_power_of_two() == EVENT_BUF_SIZE); + +// SPSC ringbuffer; each side is expected to have a lock on their +// local copy. The shared memory is untrusted, so this must not +// trust the other end for safety or correctness. +impl EventQueue { + pub fn new() -> Self { + EventQueue { + head: AtomicU32::new(0), + tail: AtomicU32::new(0), + elems: core::cell::UnsafeCell::new([const { MaybeUninit::uninit() }; EVENT_BUF_SIZE]), + } + } + + fn empty(head: u32, tail: u32) -> bool { + head == tail + } + fn full(head: u32, tail: u32) -> bool { + head == tail.wrapping_add(EVENT_BUF_SIZE as u32) + } + pub fn try_send(&self, event: Event) -> Result<(), Event> { + let cur_head = self.head.load(Ordering::Relaxed); + let cur_tail = self.tail.load(Ordering::SeqCst); + + if Self::full(cur_head, cur_tail) { + return Err(event); + } + + let head_idx = (cur_head as usize).rem_euclid(EVENT_BUF_SIZE); + let elems = self.elems.get().cast::(); + let target = elems.wrapping_add(head_idx); + + // Safety: cur_head is always within range for the elems array. + // This does not create an intermediate reference, and just writes + // to a value within an UnsafeCell. If one side is buggy or malicious, + // this can only corrupt the data/queue state, and cannot influence + // the control flow on this side of the channel. + // + // Correctness: There is a single producer for this queue, so + // we can write data to the unused region without synchronization + // concerns. The tail pointer will only shrink the region, and + // not grow backwards; as this target is outside of the head..tail + // region, it is unused. + unsafe { + target.write_volatile(event); + } + + // After writing the event, fence and increment the head to make + // the event visible to the consumer. + // TODO: is SeqCst enough of a fence for this? + self.head.fetch_add(1, Ordering::SeqCst); + + Ok(()) + } + + pub fn try_recv(&self) -> Option { + let cur_head = self.head.load(Ordering::SeqCst); + let cur_tail = self.tail.load(Ordering::Relaxed); + // TODO: are the above loads enough of a fence? + + if Self::empty(cur_head, cur_tail) { + return None; + } + + let tail_idx = (cur_tail as usize).rem_euclid(EVENT_BUF_SIZE); + let elems = self.elems.get().cast::>(); + let target = elems.wrapping_add(tail_idx); + + // Safety: cur_tail is always within range for the elems array. + // This does not create an intermediate reference, and just writes + // to a value within an UnsafeCell. If one side is buggy or malicious, + // this can only corrupt the data/queue state, and cannot influence + // the control flow on this side of the channel. + // + // Correctness: There is a single consumer for this queue; + // events within the range (head, tail] are all initialized and unchanging. + // The producer will only reuse the slot once the tail has been incremented. + let event = unsafe { target.read_volatile().assume_init() }; + + self.tail.fetch_add(1, Ordering::SeqCst); + + Some(event) + } + + pub fn try_send_data(&self, data: E) -> Result<(), E> + where + E: EventData, + { + let event = Event { + kind: E::KIND, + data: data.serialize_data(), + }; + self.try_send(event).map_err(|_| data) + } +} + +pub trait EventData: Sized { + const KIND: EventKind; + fn parse(event: &Event) -> Option { + if event.kind == Self::KIND { + Self::parse_data(&event.data) + } else { + None + } + } + fn parse_data(data: &[u64; 7]) -> Option; + fn serialize_data(&self) -> [u64; 7]; +} + +impl EventKind { + pub const UNSET: EventKind = EventKind(0); + pub const PRESENT: EventKind = EventKind(1); + pub const INPUT: EventKind = EventKind(2); + pub const DISCONNECT: EventKind = EventKind(3); +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct PresentEvent; + +unsafe impl bytemuck::Zeroable for PresentEvent {} +unsafe impl bytemuck::AnyBitPattern for PresentEvent {} + +impl EventData for PresentEvent { + const KIND: EventKind = EventKind::PRESENT; + fn parse_data(data: &[u64; 7]) -> Option { + const _ASSERT: () = assert!(size_of::() <= size_of::<[u64; 7]>()); + let bytes = &bytemuck::bytes_of(data)[..size_of::()]; + Some(*bytemuck::from_bytes(bytes)) + } + fn serialize_data(&self) -> [u64; 7] { + [0; 7] + } +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct InputEvent { + pub kind: u32, + pub data1: u32, + pub data2: u32, + pub data3: u32, + pub data4: u32, +} +impl InputEvent { + pub const KIND_KEY: u32 = 1; + // data1 = mode (1 = press, 2 = release, 3 = repeat) + // data2 = scan code + pub const KIND_MOUSE: u32 = 2; + // data1 = mode (1 = move, 2 = down, 3 = up) + // data2 = x position (pixels) + // data3 = y position (pixels) + // data4 = button (1 = left, 2 = right, 3 = middle, ...) + pub const KIND_SCROLL: u32 = 3; + // data1 = x delta (pixels) + // data2 = y delta (pixels) +} + +unsafe impl bytemuck::Zeroable for InputEvent {} +unsafe impl bytemuck::AnyBitPattern for InputEvent {} +impl EventData for InputEvent { + const KIND: EventKind = EventKind::INPUT; + fn parse_data(data: &[u64; 7]) -> Option { + const _ASSERT: () = assert!(size_of::() <= size_of::<[u64; 7]>()); + let bytes = &bytemuck::bytes_of(data)[..size_of::()]; + Some(*bytemuck::from_bytes(bytes)) + } + fn serialize_data(&self) -> [u64; 7] { + let mut out = [0u64; 7]; + // TODO: serialization soundness; needs no uninitialized data, + // as it could leak secrets + let data: [u8; size_of::()] = unsafe { core::mem::transmute(*self) }; + bytemuck::cast_slice_mut(&mut out)[..size_of::()].copy_from_slice(&data); + out + } +} + +#[derive(PartialEq, Eq, Copy, Clone)] +pub struct ScanCode(pub u32); + +macro_rules! define_scancodes { + ($($name:ident = $num:literal $( ( $char:literal $( / $shift:literal )? ) )?),* $(,)?) => { + #[allow(unused)] + impl ScanCode { + $(pub const $name: ScanCode = ScanCode($num);)* + } + #[allow(unused)] + pub const SCANCODES: [Option; 256] = { + #[allow(unused_mut)] + let mut s = [None; 256]; + $($(s[$num] = Some($char);)?)* + s + }; + #[allow(unused)] + pub const SCANCODES_SHIFTED: [Option; 256] = { + #[allow(unused_mut)] + let mut s = [None; 256]; + $($($(s[$num] = Some($shift);)?)?)* + s + }; + }; +} + +define_scancodes! { + KEY0 = 0 ('0' / ')'), + KEY1 = 1 ('1' / '!'), + KEY2 = 2 ('2' / '@'), + KEY3 = 3 ('3' / '#'), + KEY4 = 4 ('4' / '$'), + KEY5 = 5 ('5' / '%'), + KEY6 = 6 ('6' / '^'), + KEY7 = 7 ('7' / '&'), + KEY8 = 8 ('8' / '*'), + KEY9 = 9 ('9' / '('), + A = 10 ('a' / 'A'), + B = 11 ('b' / 'B'), + C = 12 ('c' / 'C'), + D = 13 ('d' / 'D'), + E = 14 ('e' / 'E'), + F = 15 ('f' / 'F'), + G = 16 ('g' / 'G'), + H = 17 ('h' / 'H'), + I = 18 ('i' / 'I'), + J = 19 ('j' / 'J'), + K = 20 ('k' / 'K'), + L = 21 ('l' / 'L'), + M = 22 ('m' / 'M'), + N = 23 ('n' / 'N'), + O = 24 ('o' / 'O'), + P = 25 ('p' / 'P'), + Q = 26 ('q' / 'Q'), + R = 27 ('r' / 'R'), + S = 28 ('s' / 'S'), + T = 29 ('t' / 'T'), + U = 30 ('u' / 'U'), + V = 31 ('v' / 'V'), + W = 32 ('w' / 'W'), + X = 33 ('x' / 'X'), + Y = 34 ('y' / 'Y'), + Z = 35 ('z' / 'Z'), + F1 = 36, + F2 = 37, + F3 = 38, + F4 = 39, + F5 = 40, + F6 = 41, + F7 = 42, + F8 = 43, + F9 = 44, + F10 = 45, + F11 = 46, + F12 = 47, + F13 = 48, + F14 = 49, + F15 = 50, + DOWN = 51, + LEFT = 52, + RIGHT = 53, + UP = 54, + APOSTROPHE = 55 ('\'' / '"'), + BACKQUOTE = 56 ('`' / '~'), + BACKSLASH = 57 ('\\' / '|'), + COMMA = 58 (',' / '<'), + EQUAL = 59 ('=' / '+'), + LEFT_BRACKET = 60 ('[' / '{'), + MINUS = 61 ('-' / '_'), + PERIOD = 62 ('.' / '>'), + RIGHT_BRACKET = 63 (']' / '}'), + SEMICOLON = 64 (';' / ':'), + SLASH = 65 ('/' / '?'), + BACKSPACE = 66, + DELETE = 67, + END = 68, + ENTER = 69 ('\n'), + ESCAPE = 70, + HOME = 71, + INSERT = 72, + MENU = 73, + PAGE_DOWN = 74, + PAGE_UP = 75, + PAUSE = 76, + SPACE = 77 (' '), + TAB = 78 ('\t'), + NUM_LOCK = 79, + CAPS_LOCK = 80, + SCROLL_LOCK = 81, + LEFT_SHIFT = 82, + RIGHT_SHIFT = 83, + LEFT_CTRL = 84, + RIGHT_CTRL = 85, + NUM_PAD0 = 86 ('0'), + NUM_PAD1 = 87 ('1'), + NUM_PAD2 = 88 ('2'), + NUM_PAD3 = 89 ('3'), + NUM_PAD4 = 90 ('4'), + NUM_PAD5 = 91 ('5'), + NUM_PAD6 = 92 ('6'), + NUM_PAD7 = 93 ('7'), + NUM_PAD8 = 94 ('8'), + NUM_PAD9 = 95 ('9'), + NUM_PAD_DOT = 96 ('.'), + NUM_PAD_SLASH = 97 ('/'), + NUM_PAD_ASTERISK = 98 ('*'), + NUM_PAD_MINUS = 99 ('-'), + NUM_PAD_PLUS = 100 ('+'), + NUM_PAD_ENTER = 101 ('\n'), + LEFT_ALT = 102, + RIGHT_ALT = 103, + LEFT_SUPER = 104, + RIGHT_SUPER = 105, + UNKNOWN = 106, +} + +#[cfg(target_arch = "aarch64")] +pub fn memcpy128(dst: &mut [u128], src: &[u128]) { + let len = dst.len(); + assert_eq!(len, src.len()); + assert!(len % 64 == 0); + unsafe { + core::arch::asm!(r" + 1: + ldp {tmp1}, {tmp2}, [{src}, #0] + stp {tmp1}, {tmp2}, [{dst}, #0] + ldp {tmp1}, {tmp2}, [{src}, #16] + stp {tmp1}, {tmp2}, [{dst}, #16] + ldp {tmp1}, {tmp2}, [{src}, #32] + stp {tmp1}, {tmp2}, [{dst}, #32] + ldp {tmp1}, {tmp2}, [{src}, #48] + stp {tmp1}, {tmp2}, [{dst}, #48] + add {src}, {src}, #64 // TODO: figure out east way to use index increment + add {dst}, {dst}, #64 + subs {count}, {count}, #4 + b.hi 1b // if count > 0, loop + ", + src = in(reg) src.as_ptr(), + dst = in(reg) dst.as_mut_ptr(), + count = in(reg) len, + tmp1 = out(reg) _, tmp2 = out(reg) _, + ) + } +} + +#[cfg(not(target_arch = "aarch64"))] +pub fn memcpy128(dst: &mut [u128], src: &[u128]) { + dst.copy_from_slice(src) +} diff --git a/crates/display-proto/src/local.rs b/crates/display-proto/src/local.rs new file mode 100644 index 00000000..8c721eb1 --- /dev/null +++ b/crates/display-proto/src/local.rs @@ -0,0 +1,77 @@ +use super::{BufferHeader, EventQueue, GlobalMeta, SemDescriptor, VideoMeta}; + +pub struct BufferHandle { + buf: *mut BufferHeader, + pub global_meta: GlobalMeta, + pub video_meta: VideoMeta, + pub present_sem: SemDescriptor, + pub fds: [u32; 8], +} + +unsafe impl Send for BufferHandle {} +unsafe impl Sync for BufferHandle {} + +impl BufferHandle { + pub unsafe fn new(buf: *mut BufferHeader, fds: &[u32]) -> Self { + let global_meta = unsafe { (&raw const (*buf).meta).read_volatile() }; + let video_meta = unsafe { (&raw const (*buf).video_meta).read_volatile() }; + let present_sem = unsafe { (&raw const (*buf).present_sem).read_volatile() }; + + // TODO: ensure this wasn't changed before connection started... + // TODO: track actual size of the buffer (don't trust values from the buffer itself) + assert!(global_meta.vmem_offset + global_meta.vmem_size <= global_meta.segment_size); + + let mut local_fds = [u32::MAX; 8]; + local_fds[..fds.len()].copy_from_slice(fds); + + BufferHandle { + buf, + global_meta, + video_meta, + present_sem, + fds: local_fds, + } + } + + pub const fn client_to_server_queue(&mut self) -> &EventQueue { + unsafe { &(*self.buf).client_to_server_queue } + } + pub const fn server_to_client_queue(&mut self) -> &EventQueue { + unsafe { &(*self.buf).server_to_client_queue } + } + + // TODO: this is UB; need a volatile wrapper of some kind + // TODO: some way to expose this while retaining compiler write + // coalescing? (If the wrapper uses volatile, the compiler + // won't turn writes into simd writes) + pub fn video_mem(&mut self) -> &mut [u32] { + assert!( + self.global_meta.vmem_offset + self.global_meta.vmem_size + <= self.global_meta.segment_size + ); + + let vmem = self + .buf + .cast::() + .wrapping_byte_add(self.global_meta.vmem_offset as usize); + let vmem_slice = core::ptr::slice_from_raw_parts_mut( + vmem, + self.global_meta.vmem_size as usize / size_of::(), + ); + unsafe { &mut *vmem_slice } + } + + pub fn video_mem_u128(&mut self) -> &mut [u128] { + let buf = self.video_mem(); + let len_u32 = buf.len(); + let len_u128 = len_u32 * size_of::() / size_of::(); + let ptr = buf.as_mut_ptr().cast::(); + assert!(len_u32 * size_of::() == len_u128 * size_of::()); + assert!(ptr.is_aligned()); + unsafe { core::slice::from_raw_parts_mut(ptr, len_u128) } + } + + pub fn get_sem_fd(&self, sem: SemDescriptor) -> u32 { + self.fds[sem.0 as usize] + } +} diff --git a/crates/display-server/.gitignore b/crates/display-server/.gitignore new file mode 100644 index 00000000..50992530 --- /dev/null +++ b/crates/display-server/.gitignore @@ -0,0 +1 @@ +*.elf diff --git a/crates/display-server/Cargo.toml b/crates/display-server/Cargo.toml new file mode 100644 index 00000000..de828d33 --- /dev/null +++ b/crates/display-server/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "display-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +bytemuck = { version = "1.21.0" } +linked_list_allocator = "0.10" + +display-proto = { path = "../display-proto" } +ulib = { path = "../ulib", features = ["heap-impl"] } + +[dev-dependencies] +ulib = { path = "../ulib", features = ["test"] } diff --git a/crates/display-server/build.rs b/crates/display-server/build.rs new file mode 100644 index 00000000..fe806e18 --- /dev/null +++ b/crates/display-server/build.rs @@ -0,0 +1,6 @@ +fn main() { + let crate_root = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + println!("cargo::rerun-if-changed=../ulib/script.ld"); + println!("cargo::rustc-link-arg-bins=-T{crate_root}/../ulib/script.ld"); + println!("cargo::rustc-link-arg-bins=-n"); +} diff --git a/crates/display-server/build.sh b/crates/display-server/build.sh new file mode 100755 index 00000000..f7429e6b --- /dev/null +++ b/crates/display-server/build.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -ex + +BIN="display-server" +TARGET=aarch64-unknown-none-softfloat +PROFILE=${PROFILE-"release"} + +cargo rustc --profile="${PROFILE}" \ + --target=${TARGET} -- \ + -C relocation-model=static + +if test "$PROFILE" = "dev" ; then + BINARY=../../target/${TARGET}/debug/${BIN} +else + BINARY=../../target/${TARGET}/${PROFILE}/${BIN} +fi + +cp "${BINARY}" "${BIN}".elf diff --git a/crates/display-server/src/main.rs b/crates/display-server/src/main.rs new file mode 100644 index 00000000..5d2cb5fd --- /dev/null +++ b/crates/display-server/src/main.rs @@ -0,0 +1,346 @@ +#![no_std] +#![cfg_attr(not(test), no_main)] + +extern crate alloc; +extern crate display_proto as proto; + +#[macro_use] +extern crate ulib; + +use alloc::vec::Vec; +use proto::BufferHandle; +use ulib::sys::{dup3, mmap, recv_nonblock, send, FileDesc}; + +#[no_mangle] +fn main() { + let fb = init_fb(640, 480); + let server_socket = 13; + handle_conns(fb, server_socket); +} + +struct BufferInfo { + fd: u32, + present_sem_fd: u32, + size: usize, + mapped: *mut proto::BufferHeader, +} + +fn handle_incoming(_msg: ulib::sys::Message, _buf: &[u8], resp_socket: FileDesc) -> BufferHandle { + // TODO: proper listen + connect sockets + // (this just broadcasts a response to all listeners and hopes that there aren't race conditions) + + let buffer = init_buffer(); + + let fds = [buffer.fd, buffer.present_sem_fd]; + let handle = unsafe { proto::BufferHandle::new(buffer.mapped, &fds) }; + + let objects = [ + dup3(fds[0], u32::MAX, 0).unwrap(), + dup3(fds[1], u32::MAX, 0).unwrap(), + u32::MAX, + u32::MAX, + ]; + + let buf = u64::to_le_bytes(buffer.size as u64); + send( + resp_socket, + &ulib::sys::Message { + tag: 0x100, + objects, + }, + &buf, + 0, + ); + + handle +} + +fn init_buffer() -> BufferInfo { + println!("init_buffer"); + let screen_size = (640, 480); + let vmem_size = screen_size.0 * screen_size.1 * 4; + + let header_size = size_of::().next_multiple_of(4096); + let total_size = header_size + vmem_size; + + let fd = unsafe { ulib::sys::sys_memfd_create() } as u32; + + let buffer = unsafe { mmap(0, total_size, 0, 0, fd, 0) }.unwrap(); + println!("buffer allocated, {buffer:p}"); + + let present_sem_fd = ulib::sys::sem_create(0).unwrap(); + let present_sem = proto::SemDescriptor(1); + + let header = proto::BufferHeader { + version: 1.into(), + magic: u32::from_ne_bytes(*b"SBUF").into(), + kill_switch: 0.into(), + last_words: [const { core::sync::atomic::AtomicU8::new(0) }; 32], + + meta: proto::GlobalMeta { + segment_size: total_size as u32, + vmem_offset: header_size as u32, + vmem_size: vmem_size as u32, + }, + + client_to_server_queue: proto::EventQueue::new(), + server_to_client_queue: proto::EventQueue::new(), + + video_meta: proto::VideoMeta { + width: screen_size.0 as u16, + height: screen_size.1 as u16, + row_stride: (screen_size.0 as u16 * 4), + bytes_per_pixel: 4, + bit_layout: 0, + present_ts: 0, + }, + term_meta: proto::TermMeta { rows: 0, cols: 0 }, + + present_sem, + }; + + println!("Writing header"); + let ptr = buffer.cast::(); + unsafe { ptr.write(header) }; + println!("init done"); + + BufferInfo { + fd, + size: total_size, + present_sem_fd, + mapped: ptr, + } +} + +fn present(fb: &mut Framebuffer, buf: &[u128]) { + proto::memcpy128(&mut fb.data, &buf); + // Force writes to go through + core::hint::black_box(&mut *fb); +} + +#[allow(unused)] +struct Framebuffer { + fd: usize, + width: usize, + height: usize, + stride: usize, + data: &'static mut [u128], +} + +fn init_fb(width: usize, height: usize) -> Framebuffer { + let mut fb = ulib::sys::RawFB { + fd: 0, + size: 0, + pitch: 0, + width: 0, + height: 0, + }; + let buffer_fd = unsafe { ulib::sys::sys_acquire_fb(width, height, &mut fb) }; + println!("Buffer: {:?}", buffer_fd); + println!( + "buffer_size {}, width {}, height {}, pitch {}", + fb.size, fb.width, fb.height, fb.pitch + ); + + let mapped = unsafe { ulib::sys::mmap(0, fb.size, 0, 0, buffer_fd as u32, 0).unwrap() }; + let framebuf = unsafe { + core::slice::from_raw_parts_mut::(mapped.cast(), fb.size / size_of::()) + }; + + Framebuffer { + fd: buffer_fd as usize, + width: fb.width, + height: fb.height, + stride: fb.pitch / size_of::(), + data: framebuf, + } +} + +fn handle_conns(mut fb: Framebuffer, server_socket: FileDesc) { + let mut clients = Vec::new(); + + // TODO: synchronization approach + // Server: + // - should be notified when clients send events + // - should be able to multiplex clients on a single thread + // Client + // - must have a way to wait until the server has copied a frame + // out before updating it + // - should have some way to wait for server events (+ present + // requests?) + // TODO: vsync / rate management + + let mut to_remove = Vec::::new(); + + loop { + let mut buf = [0u64; 32]; + while let Ok((len, msg)) = recv_nonblock(server_socket, bytemuck::bytes_of_mut(&mut buf)) { + let buf = handle_incoming(msg, &bytemuck::bytes_of(&buf)[..len], server_socket); + clients.push(buf); + } + + let client_count = clients.len(); + for (i, buf) in clients.iter_mut().enumerate() { + let mut ev_limit = 10; + while let Some(msg) = buf.client_to_server_queue().try_recv() { + if ev_limit == 0 { + break; + } + ev_limit -= 1; + match msg.kind { + proto::EventKind::PRESENT => { + // HACK: treat newest window as active + if i == client_count - 1 { + use proto::EventData; + let proto::PresentEvent = proto::PresentEvent::parse(&msg).unwrap(); + + present(&mut fb, &buf.video_mem_u128()); + + // TODO: better sync system here? this will accumulate if client + // isn't waiting for acks + ulib::sys::sem_up(buf.get_sem_fd(buf.present_sem)).unwrap(); + + // TODO: better scheduling for this + handle_key_events(&mut *buf); + } else { + ulib::sys::sem_up(buf.get_sem_fd(buf.present_sem)).unwrap(); + } + } + proto::EventKind::DISCONNECT => { + // TODO: auto-disconnect on process exit? + println!("Client {} disconnected.", i); + to_remove.push(i); + break; + } + _ => { + // println!("{:?} {:?}", msg.kind.0, msg.data); + } + } + } + } + + if !to_remove.is_empty() { + to_remove.sort_by(|a, b| a.cmp(b).reverse()); + } + for remove in to_remove.drain(..) { + clients.remove(remove); + } + + // TODO: composite and present + // TODO: sleep until frame + unsafe { ulib::sys::sys_sleep_ms(1) }; + } +} + +fn handle_key_events(buf: &mut proto::BufferHandle) { + loop { + let key = unsafe { ulib::sys::sys_poll_key_event() }; + if key < 0 { + break; + } + let pressed = (key & 0x100) != 0; + let code = key & 0xFF; + let code = remap_keycode(code); + let event = proto::InputEvent { + kind: proto::InputEvent::KIND_KEY, + data1: if pressed { 1 } else { 2 }, + data2: code.0, + data3: 0, + data4: 0, + }; + buf.server_to_client_queue() + .try_send_data(event) + .map_err(drop) + .unwrap(); + } +} + +fn remap_keycode(code: isize) -> proto::ScanCode { + use proto::ScanCode; + match code { + 0x04 => ScanCode::A, + 0x05 => ScanCode::B, + 0x06 => ScanCode::C, + 0x07 => ScanCode::D, + 0x08 => ScanCode::E, + 0x09 => ScanCode::F, + 0x0A => ScanCode::G, + 0x0B => ScanCode::H, + 0x0C => ScanCode::I, + 0x0D => ScanCode::J, + 0x0E => ScanCode::K, + 0x0F => ScanCode::L, + 0x10 => ScanCode::M, + 0x11 => ScanCode::N, + 0x12 => ScanCode::O, + 0x13 => ScanCode::P, + 0x14 => ScanCode::Q, + 0x15 => ScanCode::R, + 0x16 => ScanCode::S, + 0x17 => ScanCode::T, + 0x18 => ScanCode::U, + 0x19 => ScanCode::V, + 0x1A => ScanCode::W, + 0x1B => ScanCode::X, + 0x1C => ScanCode::Y, + 0x1D => ScanCode::Z, + 0x1E => ScanCode::KEY1, + 0x1F => ScanCode::KEY2, + 0x20 => ScanCode::KEY3, + 0x21 => ScanCode::KEY4, + 0x22 => ScanCode::KEY5, + 0x23 => ScanCode::KEY6, + 0x24 => ScanCode::KEY7, + 0x25 => ScanCode::KEY8, + 0x26 => ScanCode::KEY9, + 0x27 => ScanCode::KEY0, + 0x28 => ScanCode::ENTER, + 0x29 => ScanCode::ESCAPE, + 0x2A => ScanCode::BACKSPACE, + 0x2B => ScanCode::TAB, + 0x2C => ScanCode::SPACE, + 0x2D => ScanCode::MINUS, // or underscore + 0x2E => ScanCode::EQUAL, + 0x2F => ScanCode::LEFT_BRACKET, + 0x30 => ScanCode::RIGHT_BRACKET, + 0x31 => ScanCode::BACKSLASH, + // 0x32 is non-us + 0x33 => ScanCode::SEMICOLON, + 0x34 => ScanCode::APOSTROPHE, + 0x35 => ScanCode::BACKQUOTE, + 0x36 => ScanCode::COMMA, + 0x37 => ScanCode::PERIOD, + 0x38 => ScanCode::SLASH, + 0x39 => ScanCode::CAPS_LOCK, + + 0x3A => ScanCode::F1, + 0x3B => ScanCode::F2, + 0x3C => ScanCode::F3, + // ... + 0x45 => ScanCode::F12, + // 0x46 => ScanCode::PRINT_SCREEN + 0x47 => ScanCode::SCROLL_LOCK, + 0x48 => ScanCode::PAUSE, + 0x49 => ScanCode::INSERT, + 0x4A => ScanCode::HOME, + 0x4B => ScanCode::PAGE_UP, + 0x4C => ScanCode::DELETE, + 0x4D => ScanCode::END, + 0x4E => ScanCode::PAGE_DOWN, + 0x4F => ScanCode::RIGHT, + 0x50 => ScanCode::LEFT, + 0x51 => ScanCode::DOWN, + 0x52 => ScanCode::UP, + 0x53 => ScanCode::NUM_LOCK, + + 0xE0 => ScanCode::LEFT_CTRL, + 0xE1 => ScanCode::LEFT_SHIFT, + 0xE2 => ScanCode::LEFT_ALT, + 0xE3 => ScanCode::LEFT_SHIFT, + 0xE4 => ScanCode::RIGHT_CTRL, + 0xE5 => ScanCode::RIGHT_SHIFT, + 0xE6 => ScanCode::RIGHT_ALT, + 0xE7 => ScanCode::RIGHT_SUPER, + _ => ScanCode::UNKNOWN, + } +} diff --git a/crates/init/ls.rs b/crates/init/ls.rs index 8a5fbee7..bbbf4246 100755 --- a/crates/init/ls.rs +++ b/crates/init/ls.rs @@ -29,7 +29,7 @@ exit # -->"##] extern crate ulib; #[no_mangle] -fn main(_chan: ulib::sys::ChannelDesc) { +fn main() { let root = 3; println!("Listing dir: {}", root); diff --git a/crates/init/shell.rs b/crates/init/shell.rs index 871a9db1..7aff7345 100755 --- a/crates/init/shell.rs +++ b/crates/init/shell.rs @@ -87,7 +87,7 @@ fn readline(reader: &mut LineReader) -> Result<&[u8], usize> { } #[no_mangle] -fn main(_chan: ulib::sys::ChannelDesc) { +fn main() { println!("Starting 🐚"); let root = 3; diff --git a/crates/init/src/main.rs b/crates/init/src/main.rs index 43799843..efe0d630 100644 --- a/crates/init/src/main.rs +++ b/crates/init/src/main.rs @@ -12,7 +12,24 @@ use ulib::sys::spawn_elf; pub extern "C" fn main() { println!("Running in usermode! (parent)"); + let (serve_a, serve_b) = ulib::sys::channel(); + + ulib::sys::dup3(serve_a, 12, 0).unwrap(); + ulib::sys::dup3(serve_b, 13, 0).unwrap(); + ulib::sys::close(serve_a).unwrap(); + ulib::sys::close(serve_b).unwrap(); + let root_fd = 3; + + let path = b"display-server.elf"; + let file = ulib::sys::openat(root_fd, path, 0, 0).unwrap(); + spawn_elf(&ulib::sys::SpawnArgs { + fd: file, + stdin: None, + stdout: None, + }) + .unwrap(); + let path = b"shell.elf"; let file = ulib::sys::openat(root_fd, path, 0, 0).unwrap(); diff --git a/crates/kernel/src/syscall/fb_hack.rs b/crates/kernel/src/syscall/fb_hack.rs index 4abc6894..24d4f173 100644 --- a/crates/kernel/src/syscall/fb_hack.rs +++ b/crates/kernel/src/syscall/fb_hack.rs @@ -1,11 +1,14 @@ +use alloc::collections::btree_map::BTreeMap; use alloc::sync::Arc; +use crate::arch::memory::palloc::{PhysicalPage, Size4KiB, PAGE_ALLOCATOR}; use crate::arch::memory::vmm::PAGE_SIZE; use crate::device::mailbox::RawFB; use crate::device::MAILBOX; use crate::event::async_handler::{run_async_handler, run_event_handler, HandlerContext}; use crate::event::context::Context; use crate::process::fd; +use crate::sync::SpinLock; /// syscall sys_alloc_fb(width: usize, height: usize) -> (fd, buffer_size: usize, width: usize, height: usize, pitch: usize) pub unsafe fn sys_acquire_fb(ctx: &mut Context) -> *mut Context { @@ -86,3 +89,80 @@ pub unsafe fn sys_poll_key_event(ctx: &mut Context) -> *mut Context { context.resume_return(code) }) } + +pub unsafe fn sys_memfd_create(ctx: &mut Context) -> *mut Context { + run_async_handler(ctx, async move |context: HandlerContext<'_>| { + let proc = context.cur_process().unwrap(); + let fd = Arc::new(MemFd::new()); + let fd = proc.file_descriptors.lock().insert(fd); + context.resume_return(fd) + }) +} + +struct MemFd { + pages: SpinLock>>, +} + +impl MemFd { + fn new() -> Self { + Self { + pages: SpinLock::new(BTreeMap::new()), + } + } +} + +impl Drop for MemFd { + fn drop(&mut self) { + let alloc = PAGE_ALLOCATOR.get(); + let pages = core::mem::take(&mut *self.pages.lock()); + for (_, page) in pages { + alloc.dealloc_frame(page); + } + } +} + +impl fd::FileDescriptor for MemFd { + fn is_same_file(&self, other: &dyn fd::FileDescriptor) -> bool { + let other = other.as_any().downcast_ref::(); + other.map(|o| core::ptr::eq(self, o)).unwrap_or(false) + } + fn kind(&self) -> fd::FileKind { + fd::FileKind::Regular + } + fn read<'a>( + &'a self, + _offset: u64, + _buf: &'a mut [u8], + ) -> fd::SmallFuture<'a, fd::FileDescResult> { + // TODO: impl read + fd::boxed_future(async move { Err(1).into() }) + } + fn write<'a>( + &'a self, + _offset: u64, + _buf: &'a [u8], + ) -> fd::SmallFuture<'a, fd::FileDescResult> { + // TODO: impl write + fd::boxed_future(async move { Err(1).into() }) + } + fn size<'a>(&'a self) -> fd::SmallFuture<'a, fd::FileDescResult> { + // TODO: Is size well defined for memfd? + fd::boxed_future(async move { Err(1).into() }) + } + fn mmap_page(&self, offset: u64) -> fd::SmallFuture> { + assert!(offset % PAGE_SIZE as u64 == 0); + + let page_addr = { + let mut pages = self.pages.lock(); + let frame = pages + .entry(offset as usize) + .or_insert_with(|| PAGE_ALLOCATOR.get().alloc_frame()); + frame.paddr + }; + + fd::boxed_future(async move { Some(fd::FileDescResult::ok(page_addr as u64)) }) + } + fn as_any(&self) -> &dyn core::any::Any { + self + } +} diff --git a/crates/kernel/src/syscall/file.rs b/crates/kernel/src/syscall/file.rs index 120b73c7..20520a33 100644 --- a/crates/kernel/src/syscall/file.rs +++ b/crates/kernel/src/syscall/file.rs @@ -12,7 +12,7 @@ bitflags::bitflags! { /// syscall dup3(old_fd: u32, new_fd: u32, flags: DupFlags) -> i64 pub unsafe fn sys_dup3(ctx: &mut Context) -> *mut Context { let old_fd = ctx.regs[0]; - let new_fd = ctx.regs[1]; + let mut new_fd = ctx.regs[1]; let flags = ctx.regs[2]; let Some(_flags) = u32::try_from(flags).ok().and_then(DupFlags::from_bits) else { @@ -30,11 +30,14 @@ pub unsafe fn sys_dup3(ctx: &mut Context) -> *mut Context { return context.resume_final(); }; - let to_close = guard.set(new_fd, old); - - if let Some(desc) = to_close { - // TODO: we should be careful about where/when fd destructors are run - drop(desc); + if new_fd == u32::MAX as usize { + new_fd = guard.insert(old); + } else { + let to_close = guard.set(new_fd, old); + if let Some(desc) = to_close { + // TODO: we should be careful about where/when fd destructors are run + drop(desc); + } } context.regs().regs[0] = new_fd; diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 9634d28d..311e4ac8 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -37,6 +37,7 @@ pub unsafe fn register_syscalls() { register_syscall_handler(22, time::sys_sleep_ms); register_syscall_handler(23, fb_hack::sys_acquire_fb); + register_syscall_handler(24, fb_hack::sys_memfd_create); register_syscall_handler(25, fb_hack::sys_poll_key_event); register_syscall_handler(26, semaphore::sys_sem_create); diff --git a/crates/kernel/src/syscall/proc.rs b/crates/kernel/src/syscall/proc.rs index e5de6368..cc701dd3 100644 --- a/crates/kernel/src/syscall/proc.rs +++ b/crates/kernel/src/syscall/proc.rs @@ -56,7 +56,7 @@ pub unsafe fn sys_spawn(ctx: &mut Context) -> *mut Context { if flags == 1 { // Same process, shared memory process = old_process.clone(); - wait_fd = (-1isize) as usize; + wait_fd = i32::MAX as usize; } else { process = Arc::new(old_process.fork().await); let descriptor = WaitFd(process.exit_code.clone()); diff --git a/crates/ulib/Cargo.toml b/crates/ulib/Cargo.toml index b4d6c354..7ddc9fb8 100644 --- a/crates/ulib/Cargo.toml +++ b/crates/ulib/Cargo.toml @@ -4,8 +4,11 @@ version = "0.1.0" edition = "2021" [dependencies] +linked_list_allocator = { version = "0.10", optional = true } [features] default = ["runtime"] runtime = [] +thread = [] +heap-impl = ["linked_list_allocator"] test = [] diff --git a/crates/ulib/src/heap_impl.rs b/crates/ulib/src/heap_impl.rs new file mode 100644 index 00000000..eadcc55c --- /dev/null +++ b/crates/ulib/src/heap_impl.rs @@ -0,0 +1,14 @@ +use linked_list_allocator::LockedHeap; + +// TODO: use mmap instead +static mut ARENA: [u8; 1 << 23] = [0; 1 << 23]; + +#[global_allocator] +static ALLOCATOR: LockedHeap = LockedHeap::empty(); + +pub(crate) unsafe fn init_heap() { + let heap_start = &raw mut ARENA; + let heap_end = (&raw mut ARENA).wrapping_add(1); + let heap_size = unsafe { heap_end.byte_offset_from(heap_start) }; + unsafe { ALLOCATOR.lock().init(heap_start.cast(), heap_size as usize) }; +} diff --git a/crates/ulib/src/lib.rs b/crates/ulib/src/lib.rs index 79e9b08a..61e0f186 100644 --- a/crates/ulib/src/lib.rs +++ b/crates/ulib/src/lib.rs @@ -4,30 +4,11 @@ pub mod runtime; pub mod spinlock; +pub mod stdout; pub mod sys; -pub struct Stdout; +#[cfg(feature = "thread")] +pub mod thread; -impl core::fmt::Write for Stdout { - fn write_str(&mut self, s: &str) -> core::fmt::Result { - sys::pwrite_all(1, s.as_bytes(), 0) - .map(|_| ()) - .map_err(|_| core::fmt::Error) - } -} - -#[macro_export] -macro_rules! print { - ($($arg:tt)*) => {{ - use core::fmt::Write; - write!($crate::Stdout, $($arg)*).ok(); - }}; -} - -#[macro_export] -macro_rules! println { - ($($arg:tt)*) => {{ - use core::fmt::Write; - writeln!($crate::Stdout, $($arg)*).ok(); - }}; -} +#[cfg(feature = "heap-impl")] +mod heap_impl; diff --git a/crates/ulib/src/runtime.rs b/crates/ulib/src/runtime.rs index 13f16063..922e8c50 100644 --- a/crates/ulib/src/runtime.rs +++ b/crates/ulib/src/runtime.rs @@ -1,10 +1,15 @@ unsafe extern "Rust" { - fn main(chan: crate::sys::ChannelDesc); + fn main(); } #[unsafe(no_mangle)] -extern "C" fn _start(x0: usize) -> ! { - unsafe { main(crate::sys::ChannelDesc(x0 as u32)) }; +extern "C" fn _start(_x0: usize) -> ! { + #[cfg(feature = "heap-impl")] + unsafe { + crate::heap_impl::init_heap() + }; + + unsafe { main() }; crate::sys::exit(0); } diff --git a/crates/ulib/src/stdout.rs b/crates/ulib/src/stdout.rs new file mode 100644 index 00000000..09ac8e5b --- /dev/null +++ b/crates/ulib/src/stdout.rs @@ -0,0 +1,27 @@ +use crate::sys::pwrite_all; + +pub struct Stdout; + +impl core::fmt::Write for Stdout { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + pwrite_all(1, s.as_bytes(), 0) + .map(|_| ()) + .map_err(|_| core::fmt::Error) + } +} + +#[macro_export] +macro_rules! print { + ($($arg:tt)*) => {{ + use core::fmt::Write; + write!($crate::stdout::Stdout, $($arg)*).ok(); + }}; +} + +#[macro_export] +macro_rules! println { + ($($arg:tt)*) => {{ + use core::fmt::Write; + writeln!($crate::stdout::Stdout, $($arg)*).ok(); + }}; +} diff --git a/crates/ulib/src/sys.rs b/crates/ulib/src/sys.rs index bb8e9791..78b03141 100644 --- a/crates/ulib/src/sys.rs +++ b/crates/ulib/src/sys.rs @@ -23,10 +23,6 @@ fn int_to_error(res: isize) -> Result { pub type FileDesc = u32; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct ChannelDesc(pub u32); - #[repr(C)] #[derive(Debug)] pub struct Message { @@ -119,6 +115,7 @@ unsafe extern "C" { pub fn sys_acquire_fb(width: usize, height: usize, res: *mut RawFB) -> isize; } +syscall!(24 => pub fn sys_memfd_create() -> isize); syscall!(25 => pub fn sys_poll_key_event() -> isize); syscall!(26 => pub fn sys_sem_create(value: usize) -> isize); @@ -131,26 +128,26 @@ syscall!(28 => pub fn sys_sem_down(fd: usize) -> isize); const FLAG_NO_BLOCK: usize = 1 << 0; -pub fn channel() -> (ChannelDesc, ChannelDesc) { +pub fn channel() -> (FileDesc, FileDesc) { let res = unsafe { sys_channel() }; - (ChannelDesc(res.0 as u32), ChannelDesc(res.1 as u32)) + (res.0 as u32, res.1 as u32) } -pub fn send(desc: ChannelDesc, msg: &Message, buf: &[u8], flags: usize) -> isize { - unsafe { sys_send(desc.0 as usize, msg, buf.as_ptr(), buf.len(), flags) } +pub fn send(desc: FileDesc, msg: &Message, buf: &[u8], flags: usize) -> isize { + unsafe { sys_send(desc as usize, msg, buf.as_ptr(), buf.len(), flags) } } -pub fn send_block(desc: ChannelDesc, msg: &Message, buf: &[u8]) -> isize { +pub fn send_block(desc: FileDesc, msg: &Message, buf: &[u8]) -> isize { send(desc, msg, buf, 0) } -pub fn send_nonblock(desc: ChannelDesc, msg: &Message, buf: &[u8]) -> isize { +pub fn send_nonblock(desc: FileDesc, msg: &Message, buf: &[u8]) -> isize { send(desc, msg, buf, FLAG_NO_BLOCK) } -pub fn recv(desc: ChannelDesc, buf: &mut [u8], flags: usize) -> Result<(usize, Message), isize> { +pub fn recv(desc: FileDesc, buf: &mut [u8], flags: usize) -> Result<(usize, Message), isize> { let mut msg = MaybeUninit::uninit(); let res = unsafe { sys_recv( - desc.0 as usize, + desc as usize, msg.as_mut_ptr(), buf.as_mut_ptr(), buf.len(), @@ -163,10 +160,10 @@ pub fn recv(desc: ChannelDesc, buf: &mut [u8], flags: usize) -> Result<(usize, M Err(res) } } -pub fn recv_block(desc: ChannelDesc, buf: &mut [u8]) -> Result<(usize, Message), isize> { +pub fn recv_block(desc: FileDesc, buf: &mut [u8]) -> Result<(usize, Message), isize> { recv(desc, buf, 0) } -pub fn recv_nonblock(desc: ChannelDesc, buf: &mut [u8]) -> Result<(usize, Message), isize> { +pub fn recv_nonblock(desc: FileDesc, buf: &mut [u8]) -> Result<(usize, Message), isize> { recv(desc, buf, FLAG_NO_BLOCK) } diff --git a/crates/ulib/src/thread.rs b/crates/ulib/src/thread.rs new file mode 100644 index 00000000..ffec551d --- /dev/null +++ b/crates/ulib/src/thread.rs @@ -0,0 +1,41 @@ +extern crate alloc; + +use alloc::boxed::Box; +use core::mem::MaybeUninit; + +use crate::sys::{exit, spawn}; + +pub fn spawn_thread(func: F) +where + F: FnOnce() + Send + 'static, +{ + let stack = Box::leak(alloc::vec![0; 8 * 8192 / 16].into_boxed_slice()); + + let (a, b, _c) = unsafe { stack.align_to_mut::>() }; + let func_ptr; + let sp; + if size_of::() == 0 { + func_ptr = a.as_mut_ptr_range().end as usize; + sp = a.as_mut_ptr_range().end as usize; + } else { + let f = b.last_mut().unwrap(); + f.write(func); + func_ptr = f as *mut _ as usize; + sp = (f as *mut _ as usize) & !0xF; + } + let sp = sp as *const u128; + assert!(sp.is_aligned()); + + extern "C" fn spawn_inner(ptr: *mut F) + where + F: FnOnce() + Send + 'static, + { + (unsafe { ptr.read() })(); + + // TODO: this leaks the stack + exit(0); + } + + const FLAG_CLONE: usize = 1; + unsafe { spawn(spawn_inner:: as usize, sp as usize, func_ptr, FLAG_CLONE).unwrap() }; +}