From 2accc78948fa4a18e37ab0bc405f9b2758acaa3e Mon Sep 17 00:00:00 2001 From: Ben Bridle Date: Thu, 3 Jul 2025 15:26:07 +1200 Subject: Initial commit --- src/bin/br/config.rs | 122 ++++++++++ src/bin/br/load.rs | 81 +++++++ src/bin/br/main.rs | 217 +++++++++++++++++ src/debug.rs | 126 ++++++++++ src/devices/clock_device.rs | 161 +++++++++++++ src/devices/file_device.rs | 376 ++++++++++++++++++++++++++++++ src/devices/input_device.rs | 254 ++++++++++++++++++++ src/devices/math_device.rs | 199 ++++++++++++++++ src/devices/memory_device.rs | 186 +++++++++++++++ src/devices/mod.rs | 17 ++ src/devices/screen_device.rs | 450 ++++++++++++++++++++++++++++++++++++ src/devices/stream_device.rs | 239 +++++++++++++++++++ src/devices/system_device.rs | 119 ++++++++++ src/emulators/graphical_emulator.rs | 339 +++++++++++++++++++++++++++ src/emulators/headless_emulator.rs | 105 +++++++++ src/emulators/mod.rs | 21 ++ src/lib.rs | 31 +++ src/types/buffered_file.rs | 143 ++++++++++++ src/types/controller.rs | 160 +++++++++++++ src/types/directory_listing.rs | 120 ++++++++++ src/types/entry_type.rs | 37 +++ src/types/file_path.rs | 288 +++++++++++++++++++++++ src/types/mod.rs | 19 ++ src/types/path_buffer.rs | 60 +++++ src/types/sprite_buffer.rs | 85 +++++++ src/types/string_buffer.rs | 37 +++ src/types/wake_queue.rs | 51 ++++ 27 files changed, 4043 insertions(+) create mode 100644 src/bin/br/config.rs create mode 100644 src/bin/br/load.rs create mode 100644 src/bin/br/main.rs create mode 100644 src/debug.rs create mode 100644 src/devices/clock_device.rs create mode 100644 src/devices/file_device.rs create mode 100644 src/devices/input_device.rs create mode 100644 src/devices/math_device.rs create mode 100644 src/devices/memory_device.rs create mode 100644 src/devices/mod.rs create mode 100644 src/devices/screen_device.rs create mode 100644 src/devices/stream_device.rs create mode 100644 src/devices/system_device.rs create mode 100644 src/emulators/graphical_emulator.rs create mode 100644 src/emulators/headless_emulator.rs create mode 100644 src/emulators/mod.rs create mode 100644 src/lib.rs create mode 100644 src/types/buffered_file.rs create mode 100644 src/types/controller.rs create mode 100644 src/types/directory_listing.rs create mode 100644 src/types/entry_type.rs create mode 100644 src/types/file_path.rs create mode 100644 src/types/mod.rs create mode 100644 src/types/path_buffer.rs create mode 100644 src/types/sprite_buffer.rs create mode 100644 src/types/string_buffer.rs create mode 100644 src/types/wake_queue.rs (limited to 'src') diff --git a/src/bin/br/config.rs b/src/bin/br/config.rs new file mode 100644 index 0000000..56d5190 --- /dev/null +++ b/src/bin/br/config.rs @@ -0,0 +1,122 @@ +use crate::*; + + +#[derive(Copy, Clone, PartialEq)] +pub enum Mode { + Graphical, + Headless, + Dynamic, +} + +impl Mode { + pub fn from_str(string: &str) -> Self { + match string { + "g" | "graphical" => Self::Graphical, + "h" | "headless" => Self::Headless, + "d" | "dynamic" => Self::Dynamic, + _ => fatal!("Invalid mode string '{string}'"), + } + } +} + + +pub fn parse_dimensions(string: &str) -> ScreenDimensions { + fn parse_inner(string: &str) -> Option { + let (w_str, h_str) = string.trim().split_once('x')?; + Some( ScreenDimensions { + width: w_str.parse().ok()?, + height: h_str.parse().ok()?, + } ) + } + let dimensions = parse_inner(string).unwrap_or_else(|| { + fatal!("Invalid dimensions string '{string}'"); + }); + if dimensions.is_zero() { + fatal!("Screen dimensions must be greater than zero"); + } + return dimensions; +} + + +pub fn parse_palette(string: &str) -> [Colour; 16] { + fn decode_ascii_hex_digit(ascii: u8) -> Option { + match ascii { + b'0'..=b'9' => Some(ascii - b'0'), + b'a'..=b'f' => Some(ascii - b'a' + 10), + b'A'..=b'F' => Some(ascii - b'A' + 10), + _ => { None } + } + } + fn parse_inner(string: &str) -> Option<[Colour; 16]> { + let mut c = Vec::new(); + for token in string.split(',') { + let mut bytes = token.bytes(); + if bytes.len() != 3 { return None; } + let r = decode_ascii_hex_digit(bytes.next().unwrap())?; + let g = decode_ascii_hex_digit(bytes.next().unwrap())?; + let b = decode_ascii_hex_digit(bytes.next().unwrap())?; + c.push(Colour::from_rgb(r*17, g*17, b*17)); + } + Some(match c.len() { + 2 => [ c[0], c[1], c[0], c[1], c[0], c[1], c[0], c[1], + c[0], c[1], c[0], c[1], c[0], c[1], c[0], c[1] ], + 3 => [ c[0], c[1], c[0], c[2], c[0], c[1], c[0], c[2], + c[0], c[1], c[0], c[2], c[0], c[1], c[0], c[2] ], + 4 => [ c[0], c[1], c[2], c[3], c[0], c[1], c[2], c[3], + c[0], c[1], c[2], c[3], c[0], c[1], c[2], c[3] ], + 8 => [ c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], + c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7] ], + 16 => [ c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], + c[8], c[9], c[10], c[11], c[12], c[13], c[14], c[15] ], + _ => return None, + }) + } + parse_inner(string).unwrap_or_else(|| { + fatal!("Invalid palette string '{string}'"); + }) +} + + +pub fn parse_metadata_colour(colour: Option) -> Option { + let c = colour?; + Some(Colour::from_rgb(c.red, c.green, c.blue)) +} + + +pub fn parse_small_icon(bytes: Option>, bg: Colour, fg: Colour) -> Option { + let rgba = sprite_data_to_rgb(&bytes?, 3, bg, fg); + match Icon::from_rgba(rgba, 24, 24) { + Ok(icon) => Some(icon), + Err(err) => unreachable!("Error while parsing small icon data: {err}"), + } +} + +pub fn parse_large_icon(bytes: Option>, bg: Colour, fg: Colour) -> Option { + let rgba = sprite_data_to_rgb(&bytes?, 8, bg, fg); + match Icon::from_rgba(rgba, 64, 64) { + Ok(icon) => Some(icon), + Err(err) => unreachable!("Error while parsing large icon data: {err}"), + } +} + +fn sprite_data_to_rgb(bytes: &[u8], size: usize, bg: Colour, fg: Colour) -> Vec { + let sprites: Vec<&[u8]> = bytes.chunks_exact(8).collect(); + let mut rgba = Vec::new(); + for sprite_row in 0..size { + for pixel_row in 0..8 { + for sprite_column in 0..size { + let sprite = &sprites[sprite_column + (sprite_row * size)]; + let row = &sprite[pixel_row]; + for bit in 0..8 { + let state = row & (0x80 >> bit); + let colour = match state != 0 { + true => fg, + false => bg, + }; + rgba.extend_from_slice(&colour.as_rgba_array()); + } + } + } + } + return rgba; +} diff --git a/src/bin/br/load.rs b/src/bin/br/load.rs new file mode 100644 index 0000000..93a748c --- /dev/null +++ b/src/bin/br/load.rs @@ -0,0 +1,81 @@ +use crate::*; + +use std::io::Read; +use std::path::{Path, PathBuf}; + + +pub struct LoadedProgram { + pub bytecode: Vec, + pub path: Option, +} + +/// Load program from path or standard input. +pub fn load_program(path: Option<&PathBuf>) -> LoadedProgram { + if let Some(path) = path { + if let Ok(program) = load_program_from_file(path) { + let length = program.bytecode.len(); + info!("Loaded program from {path:?} ({length} bytes)"); + return program; + } else if let Some(program) = load_program_from_env(path) { + let length = program.bytecode.len(); + info!("Loaded program from {path:?} ({length} bytes)"); + return program; + } else { + fatal!("Could not read program from {path:?}"); + } + } else { + info!("Reading program from standard input..."); + if let Ok(program) = load_program_from_stdin() { + let length = program.bytecode.len(); + info!("Loaded program from standard input ({length} bytes)"); + return program; + } else { + fatal!("Could not read program from standard input"); + } + } +} + +/// Attempt to load program from a directory in the BEDROCK_PATH environment variable. +fn load_program_from_env(path: &Path) -> Option { + // Check that the path is a bare program name. + if path.is_relative() && path.components().count() == 1 { + for base_path in std::env::var("BEDROCK_PATH").ok()?.split(':') { + let mut base_path = PathBuf::from(base_path); + // Skip relative paths. + if !base_path.is_absolute() { continue; } + base_path.push(path); + if let Ok(program) = load_program_from_file(&base_path) { + return Some(program); + } + if path.extension().is_some() { continue; } + base_path.set_extension("br"); + if let Ok(program) = load_program_from_file(&base_path) { + return Some(program); + } + } + } + return None; +} + +/// Attempt to load program from a file path. +fn load_program_from_file(path: &Path) -> Result { + // Canonicalize paths so that symbolic links to program files resolve to + // the real program directory, which could contain a symbols file. + let path = match path.canonicalize() { + Ok(canonical) => canonical, + Err(_) => path.to_path_buf(), + }; + load_program_from_readable_source(std::fs::File::open(&path)?, Some(&path)) +} + +/// Attempt to load program from standard input. +fn load_program_from_stdin() -> Result { + load_program_from_readable_source(std::io::stdin(), None) +} + +/// Attempt to load program from a source that implements std::io::Read. +fn load_program_from_readable_source(source: impl Read, path: Option<&Path>) -> Result { + let mut bytecode = Vec::::new(); + source.take(65536).read_to_end(&mut bytecode)?; + return Ok(LoadedProgram { bytecode, path: path.map(|p| p.to_path_buf()) }); +} diff --git a/src/bin/br/main.rs b/src/bin/br/main.rs new file mode 100644 index 0000000..81c5ec9 --- /dev/null +++ b/src/bin/br/main.rs @@ -0,0 +1,217 @@ +#![feature(path_add_extension)] + +mod config; +mod load; +pub use config::*; +pub use load::*; + +use bedrock_asm::*; +use bedrock_core::*; +use bedrock_pc::*; +use log::*; +use switchboard::*; +use phosphor::*; + +use std::cmp::{min, max}; +use std::num::NonZeroU32; + + +fn main() { + let mut args = Switchboard::from_env(); + if let Some("asm") = args.peek() { + args.pop(); + assemble(args, "br asm"); + } + + // ----------------------------------------------------------------------- + + args.named("help").short('h'); + args.named("version"); + args.named("verbose").short('v'); + + if args.get("help").as_bool() { + print_help(); + std::process::exit(0); + } + if args.get("version").as_bool() { + let name = env!("CARGO_PKG_NAME"); + let version = env!("CARGO_PKG_VERSION"); + eprintln!("{name} v{version}"); + eprintln!("Written by Ben Bridle."); + std::process::exit(0); + } + if args.get("verbose").as_bool() { + log::set_log_level(log::LogLevel::Info); + } + + args.positional("source"); + args.named("debug").short('d'); + args.named("mode").default("dynamic"); + + args.named("palette"); + args.named("fullscreen").short('f'); + args.named("show-cursor").short('c'); + args.named("zoom").short('z').quick("3").default("1"); + args.named("size").short('s'); + args.named("decode-stdin").short('i'); + args.named("encode-stdout").short('o'); + args.raise_errors(); + + let source = args.get("source").as_path_opt(); + let debug = args.get("debug").as_bool(); + let mode = Mode::from_str(args.get("mode").as_str()); + let palette = args.get("palette").as_str_opt().map(parse_palette); + let fullscreen = args.get("fullscreen").as_bool(); + let show_cursor = args.get("show-cursor").as_bool(); + let zoom_raw = min(10, max(1, args.get("zoom").as_u32())); + let zoom = unsafe { NonZeroU32::new_unchecked(zoom_raw) }; + let dimensions = match args.get("size").as_str_opt() { + Some(string) => parse_dimensions(string), + None => DEFAULT_SCREEN_SIZE / (zoom_raw as u16), + }; + let decode_stdin = args.get("decode-stdin").as_bool(); + let encode_stdout = args.get("encode-stdout").as_bool(); + + // ----------------------------------------------------------------------- + + let LoadedProgram { bytecode, path } = load_program(source.as_ref()); + let mut title = String::from("Bedrock program"); + let mut icon = None; + + if let Some(metadata) = Metadata::from(&bytecode) { + let name = metadata.name().unwrap_or("unnamed".to_string()); + let authors = metadata.authors().unwrap_or_else(Vec::new); + let mut metadata_string = format!("Program is '{name}'"); + if authors.len() > 0 { + metadata_string.push_str(&format!(", by {}", authors[0])); } + if authors.len() > 1 { + metadata_string.push_str(" and others"); } + info!("{metadata_string}"); + + match name.split_once('/') { + Some((name, _version)) if !name.is_empty() => title = name.to_string(), + _ => title = name.to_string(), + } + let bg = parse_metadata_colour(metadata.bg_colour()).unwrap_or(Colour::rgb(0x1E1C26)); + let fg = parse_metadata_colour(metadata.fg_colour()).unwrap_or(Colour::rgb(0xED614F)); + match parse_large_icon(metadata.large_icon(), bg, fg) { + Some(large_icon) => icon = Some(large_icon), + None => match parse_small_icon(metadata.small_icon(), bg, fg) { + Some(small_icon) => icon = Some(small_icon), + None => (), + } + } + } else { + info!("Program does not contain metadata"); + } + + let symbols_path = path.as_ref().map(|p| { + let mut path = p.to_path_buf(); + path.add_extension("sym"); path + }); + + let config = EmulatorConfig { + dimensions, fullscreen, zoom, palette, show_cursor, + decode_stdin, encode_stdout, + symbols_path, title, icon, + }; + + if let Ok(phosphor) = Phosphor::new() { + match mode { + Mode::Dynamic => { + info!("Starting graphical emulator"); + let mut emulator = GraphicalEmulator::new(config, debug); + emulator.load_program(&bytecode); + emulator.run(phosphor, false); + } + Mode::Graphical => { + info!("Starting graphical emulator"); + let mut emulator = GraphicalEmulator::new(config, debug); + emulator.load_program(&bytecode); + emulator.run(phosphor, false); + } + Mode::Headless => { + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + } + } else { + match mode { + Mode::Dynamic => { + info!("Could not start graphical event loop"); + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + Mode::Graphical => { + fatal!("Could not start graphical event loop"); + } + Mode::Headless => { + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + } + } +} + + +fn print_help() { + eprintln!("\ +Usage: br [source] + br asm [source] [destination] + +Emulator and assembler for the Bedrock computer system. + +To access the assembler, run `br asm`. To learn how to use the +assembler, run `br asm --help`. + +Usage: + To load a Bedrock program from a file, run `br `, where + is the path to an assembled Bedrock program. + + To load a Bedrock program from piped input, run ` | br`, + where is a command that generates Bedrock bytecode. + + To assemble a Bedrock program from a source file and write to an + output file, run `br asm `, where is the + path of the source file and is the path to write to. + + To assemble and run a Bedrock program without saving to a file, + run `br asm | br`, where is the path of the + source file. + +Environment variables: + BEDROCK_PATH + A list of colon-separated paths that will be searched to find + a Bedrock program when the program doesn't exist in the current + directory. This allows the user to run frequently-used programs + from any directory. + BEDROCK_LIBS + A list of colon-separated paths that will be searched to find + Bedrock source code files to use as libraries when assembling a + Bedrock program. If a library file resolves an unresolved symbol + in the program being assembled, the library file will be merged + into the program. + +Arguments: + [program] Path to a Bedrock program to run + +Switches: + --fullscreen (-f) Start the program in fullscreen mode (toggle with F11) + --size= (-s) Set the initial window size in the format x + --zoom= (-z) Set the pixel size for the screen (change with F5/F6) + --show-cursor (-c) Show the operating system cursor over the window + --palette= Set a debug colour palette in the format ,... (toggle with F2) + --debug, (-d) Show debug information while the program is running + --decode-stdin (-i) Decode transmissions on standard input from text lines. + --encode-stdout (-o) Encode transmissions on standard output as text lines. + --help (-h) Print this help information + --verbose, (-v) Print additional information + --version Print the program version and exit +"); +} diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..b007d9f --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,126 @@ +use crate::*; + +use inked::{ink, InkedString}; + + +pub struct DebugState { + pub enabled: bool, + last_cycle: usize, + mark: Instant, + symbols: DebugSymbols, +} + +impl DebugState { + pub fn new>(enabled: bool, symbols_path: Option

) -> Self { + Self { + enabled, + last_cycle: 0, + mark: Instant::now(), + symbols: DebugSymbols::from_path(symbols_path), + } + } + + pub fn debug_full(&mut self, core: &BedrockCore) { + if self.enabled { + let prev_pc = core.mem.pc.wrapping_sub(1); + let cycle = core.cycle; + let delta = core.cycle.saturating_sub(self.last_cycle); + let elapsed = self.mark.elapsed(); + + eprintln!(" PC: 0x{prev_pc:04x} Cycle: {cycle} (+{delta} in {elapsed:.2?})"); + eprint!("WST: "); debug_stack(&core.wst); + eprint!("RST: "); debug_stack(&core.rst); + // Print information about the nearest symbol. + if let Some(symbol) = self.symbols.for_address(prev_pc) { + let name = &symbol.name; + let address = &symbol.address; + let mut string = InkedString::new(); + string.push(ink!("SYM: ")); + string.push(ink!("@{name}").blue()); + string.push(ink!(" 0x{address:04X}")); + if let Some(location) = &symbol.location { + string.push(ink!(" ")); + string.push(ink!("{location}").dim()); + } + string.eprintln(); + } + } + self.last_cycle = core.cycle; + self.mark = Instant::now(); + } + + pub fn debug_timing(&mut self, core: &BedrockCore) { + if self.enabled { + let cycle = core.cycle; + let delta = core.cycle.saturating_sub(self.last_cycle); + let elapsed = self.mark.elapsed(); + + eprintln!("Cycle: {cycle} (+{delta} in {elapsed:.2?})"); + } + self.last_cycle = core.cycle; + self.mark = Instant::now(); + } +} + + +fn debug_stack(stack: &Stack) { + for i in 0..(stack.sp as usize) { + eprint!("{:02X} ", stack.mem[i]); + } + eprintln!(); +} + + + +pub struct DebugSymbols { + pub symbols: Vec +} + +impl DebugSymbols { + /// Load debug symbols from a symbols file. + pub fn from_path>(path: Option

) -> Self { + let mut symbols = Vec::new(); + if let Some(path) = path { + if let Ok(string) = std::fs::read_to_string(path) { + for line in string.lines() { + if let Some(symbol) = DebugSymbol::from_line(line) { + symbols.push(symbol); + } + } + } + } + symbols.sort_by_key(|s| s.address); + Self { symbols } + } + + /// Return the symbol matching a given program address. + pub fn for_address(&self, address: u16) -> Option<&DebugSymbol> { + if self.symbols.is_empty() { return None; } + match self.symbols.binary_search_by_key(&address, |s| s.address) { + Ok(index) => self.symbols.get(index), + Err(index) => self.symbols.get(index.checked_sub(1)?), + } + } +} + + +pub struct DebugSymbol { + pub address: u16, + pub name: String, + pub location: Option, +} + +impl DebugSymbol { + pub fn from_line(line: &str) -> Option { + let (address, line) = line.split_once(' ')?; + let address = u16::from_str_radix(address, 16).ok()?; + if let Some((name, location)) = line.split_once(' ') { + let name = name.to_string(); + let location = Some(location.to_string()); + Some( DebugSymbol { address, name, location } ) + } else { + let name = line.to_string(); + Some( DebugSymbol { address, name, location: None } ) + } + } +} diff --git a/src/devices/clock_device.rs b/src/devices/clock_device.rs new file mode 100644 index 0000000..d06a92c --- /dev/null +++ b/src/devices/clock_device.rs @@ -0,0 +1,161 @@ +use crate::*; + +use chrono::prelude::*; + + +pub struct ClockDevice { + pub epoch: Instant, + pub uptime_read: u16, + + pub t1: CountdownTimer, + pub t2: CountdownTimer, + pub t3: CountdownTimer, + pub t4: CountdownTimer, +} + + +impl Device for ClockDevice { + fn read(&mut self, port: u8) -> u8 { + match port { + 0x0 => Local::now().year().saturating_sub(2000) as u8, + 0x1 => Local::now().month().saturating_sub(1) as u8, + 0x2 => Local::now().day().saturating_sub(1) as u8, + 0x3 => Local::now().hour() as u8, + 0x4 => Local::now().minute() as u8, + 0x5 => Local::now().second() as u8, + 0x6 => { self.uptime_read = self.uptime() as u16; + read_h!(self.uptime_read) }, + 0x7 => read_l!(self.uptime_read), + 0x8 => { self.t1.update(); read_h!(self.t1.read) }, + 0x9 => read_l!(self.t1.read), + 0xA => { self.t2.update(); read_h!(self.t2.read) }, + 0xB => read_l!(self.t2.read), + 0xC => { self.t3.update(); read_h!(self.t3.read) }, + 0xD => read_l!(self.t3.read), + 0xE => { self.t4.update(); read_h!(self.t4.read) }, + 0xF => read_l!(self.t4.read), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option { + match port { + 0x0 => (), + 0x1 => (), + 0x2 => (), + 0x3 => (), + 0x4 => (), + 0x5 => (), + 0x6 => (), + 0x7 => (), + 0x8 => write_h!(self.t1.write, value), + 0x9 => { write_l!(self.t1.write, value); self.t1.commit() }, + 0xA => write_h!(self.t2.write, value), + 0xB => { write_l!(self.t2.write, value); self.t2.commit() }, + 0xC => write_h!(self.t3.write, value), + 0xD => { write_l!(self.t3.write, value); self.t3.commit() }, + 0xE => write_h!(self.t4.write, value), + 0xF => { write_l!(self.t4.write, value); self.t4.commit() }, + _ => unreachable!(), + }; + return None; + } + + fn wake(&mut self) -> bool { + let t1 = self.t1.wake(); + let t2 = self.t2.wake(); + let t3 = self.t3.wake(); + let t4 = self.t4.wake(); + return t1 | t2 | t3 | t4; + } + + fn reset(&mut self) { + self.epoch = Instant::now(); + self.uptime_read = 0; + self.t1.reset(); + self.t2.reset(); + self.t3.reset(); + self.t4.reset(); + } +} + + +impl ClockDevice { + pub fn new() -> Self { + Self { + epoch: Instant::now(), + uptime_read: 0, + + t1: CountdownTimer::new(), + t2: CountdownTimer::new(), + t3: CountdownTimer::new(), + t4: CountdownTimer::new(), + } + } + + pub fn uptime(&self) -> u64 { + (self.epoch.elapsed().as_nanos() * 256 / 1_000_000_000) as u64 + } +} + + + +pub struct CountdownTimer { + pub end: Option, + pub read: u16, + pub write: u16, + pub wake: bool, +} + +impl CountdownTimer { + pub fn new() -> Self { + Self { + end: None, + read: 0, + write: 0, + wake: false, + } + } + + pub fn reset(&mut self) { + self.end = None; + self.read = 0; + self.write = 0; + self.wake = false; + } + + pub fn wake(&mut self) -> bool { + if let Some(end) = self.end { + if end <= Instant::now() { + self.end = None; + self.wake = true; + } + } + std::mem::take(&mut self.wake) + } + + pub fn update(&mut self) { + if let Some(end) = self.end { + let now = Instant::now(); + if end > now { + let nanos = (end - now).as_nanos(); + self.read = (nanos * 256 / 1_000_000_000) as u16; + } else { + self.read = 0; + self.end = None; + self.wake = true; + } + } else { + self.read = 0; + } + } + + pub fn commit(&mut self) { + if self.write > 0 { + let nanos = (self.write as u64) * 1_000_000_000 / 256; + self.end = Some(Instant::now() + Duration::from_nanos(nanos)); + } else { + self.end = None; + } + } +} diff --git a/src/devices/file_device.rs b/src/devices/file_device.rs new file mode 100644 index 0000000..7a64c8e --- /dev/null +++ b/src/devices/file_device.rs @@ -0,0 +1,376 @@ +use crate::*; + + +pub struct FileDevice { + pub base_path: PathBuf, + pub default_path: PathBuf, + + pub entry_buffer: BedrockPathBuffer, + pub action_buffer: BedrockPathBuffer, + pub path_buffer: BedrockPathBuffer, + + pub entry: Option<(Entry, BedrockFilePath)>, + pub cached_dir: Option<(Entry, BedrockFilePath)>, + + pub success: bool, + pub pointer_write: u32, + pub length_write: u32, + + pub enable_read: bool, + pub enable_write: bool, + pub enable_create: bool, + pub enable_move: bool, + pub enable_delete: bool, +} + + +impl Device for FileDevice { + fn read(&mut self, port: u8) -> u8 { + match port { + 0x0 => read_b!(self.entry.is_some()), + 0x1 => read_b!(self.success), + 0x2 => self.path_buffer.read(), + 0x3 => read_b!(self.entry_type()), + 0x4 => self.read_byte(), + 0x5 => self.read_byte(), + 0x6 => self.read_child_path(), + 0x7 => read_b!(self.child_type()), + 0x8 => read_hh!(self.pointer()), + 0x9 => read_hl!(self.pointer()), + 0xA => read_lh!(self.pointer()), + 0xB => read_ll!(self.pointer()), + 0xC => read_hh!(self.length()), + 0xD => read_hl!(self.length()), + 0xE => read_lh!(self.length()), + 0xF => read_ll!(self.length()), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option { + match port { + 0x0 => self.write_to_entry_port(value), + 0x1 => self.write_to_action_port(value), + 0x2 => self.path_buffer.set_pointer(value), + 0x3 => self.ascend_to_parent(), + 0x4 => self.write_byte(value), + 0x5 => self.write_byte(value), + 0x6 => self.set_child_path(value), + 0x7 => self.descend_to_child(), + 0x8 => write_hh!(self.pointer_write, value), + 0x9 => write_hl!(self.pointer_write, value), + 0xA => write_lh!(self.pointer_write, value), + 0xB => {write_ll!(self.pointer_write, value); self.commit_pointer()}, + 0xC => write_hh!(self.length_write, value), + 0xD => write_hl!(self.length_write, value), + 0xE => write_lh!(self.length_write, value), + 0xF => {write_ll!(self.length_write, value); self.commit_length()}, + _ => unreachable!(), + }; + return None; + } + + fn wake(&mut self) -> bool { + false + } + + fn reset(&mut self) { + todo!() + } +} + + +impl FileDevice { + pub fn new() -> Self { + #[cfg(target_family = "unix")] + let default_base: PathBuf = PathBuf::from("/"); + #[cfg(target_family = "windows")] + let default_base: PathBuf = PathBuf::from(""); + + // TODO: I'm not at all confident that the default path is correct + // when not being set as the current directory. + Self { + base_path: default_base, + default_path: match std::env::current_dir() { + Ok(dir) => PathBuf::from(dir), + Err(_) => PathBuf::from(""), + }, + + entry_buffer: BedrockPathBuffer::new(), + action_buffer: BedrockPathBuffer::new(), + path_buffer: BedrockPathBuffer::new(), + + entry: None, + cached_dir: None, + + success: false, + pointer_write: 0, + length_write: 0, + + enable_read: true, + enable_write: true, + enable_create: true, + enable_move: true, + enable_delete: false, + } + } + + /// Safely close the current entry, cleaning up entry variables. + pub fn close(&mut self) { + self.entry_buffer.clear(); + self.action_buffer.clear(); + self.path_buffer.clear(); + self.flush(); + + if let Some((Entry::Directory(mut dir), path)) = std::mem::take(&mut self.entry) { + // Prevent the selected child from persisting when loading from cache. + dir.deselect_child(); + self.cached_dir = Some((Entry::Directory(dir), path)); + } + } + + /// Open the entry at the given Bedrock path. + pub fn open(&mut self, path: BedrockFilePath) -> Result<(), ()> { + match path.entry_type() { + Some(EntryType::File) => { + let open_result = std::fs::OpenOptions::new() + .read(self.enable_read) + .write(self.enable_write) + .open(path.as_path()); + // Keep the current entry open if we can't open the new path. + if let Ok(file) = open_result { + self.close(); + self.path_buffer.populate(path.as_buffer()); + self.entry = Some((Entry::File(BufferedFile::new(file)), path)); + return Ok(()); + }; + } + Some(EntryType::Directory) => { + // Attempt to use the cached directory. + if let Some((dir, cached_path)) = std::mem::take(&mut self.cached_dir) { + if cached_path == path { + self.close(); + self.path_buffer.populate(cached_path.as_buffer()); + self.entry = Some((dir, cached_path)); + return Ok(()); + } + } + // Keep the current entry open if we can't open the new path. + if let Some(listing) = DirectoryListing::from_path(&path) { + self.close(); + self.path_buffer.populate(path.as_buffer()); + self.entry = Some((Entry::Directory(listing), path)); + return Ok(()); + }; + } + // The entry either doesn't exist or is not a file or directory. + None => (), + } + return Err(()); + } + + /// Process a byte received from the entry port. + pub fn write_to_entry_port(&mut self, byte: u8) { + if let Some(buffer) = self.entry_buffer.write(byte) { + self.close(); + match BedrockFilePath::from_buffer(buffer, &self.base_path) { + Some(path) => self.success = self.open(path).is_ok(), + None => self.success = false, + }; + } + } + + /// Process a byte received from the action port. + pub fn write_to_action_port(&mut self, byte: u8) { + if let Some(buffer) = self.action_buffer.write(byte) { + let destination_blank = buffer[0] == 0x00; + let destination = BedrockFilePath::from_buffer(buffer, &self.base_path); + self.success = false; + + if let Some((_, source)) = &self.entry { + if destination_blank { + if self.enable_delete { + self.success = delete_entry(&source.as_path()); + } + } else if let Some(dest) = destination { + if self.enable_move { + self.success = move_entry(&source.as_path(), &dest.as_path()); + } + } + } else if let Some(dest) = destination { + if self.enable_create { + self.success = create_file(&dest.as_path()); + } + } + self.close(); + } + } + + /// Attempt to open the parent directory of the current entry. + pub fn ascend_to_parent(&mut self) { + if let Some((_, path)) = &self.entry { + match path.parent() { + Some(parent) => self.success = self.open(parent).is_ok(), + None => self.success = false, + }; + } else { + match BedrockFilePath::from_path(&self.default_path, &self.base_path) { + Some(default) => self.success = self.open(default).is_ok(), + None => self.success = false, + }; + } + } + + /// Attempt to open the selected child of the current directory. + pub fn descend_to_child(&mut self) { + if let Some((Entry::Directory(dir), _)) = &self.entry { + match dir.child_path() { + Some(child) => self.success = self.open(child).is_ok(), + None => self.success = false, + }; + } else { + self.success = false; + } + } + + /// Return true if the current entry is a directory. + pub fn entry_type(&self) -> bool { + match self.entry { + Some((Entry::Directory(_), _)) => true, + _ => false, + } + } + + /// Read a byte from the path buffer of the selected child. + pub fn read_child_path(&mut self) -> u8 { + match &mut self.entry { + Some((Entry::Directory(dir), _)) => dir.child_path_buffer().read(), + _ => 0, + } + } + + pub fn set_child_path(&mut self, byte: u8) { + if let Some((Entry::Directory(dir), _)) = &mut self.entry { + dir.child_path_buffer().set_pointer(byte); + } + } + + /// Return true if the selected child is a directory. + pub fn child_type(&self) -> bool { + match &self.entry { + Some((Entry::Directory(dir), _)) => match dir.child_type() { + Some(EntryType::Directory) => true, + _ => false, + } + _ => false, + } + } + + /// Read a byte from the current file. + pub fn read_byte(&mut self) -> u8 { + match &mut self.entry { + Some((Entry::File(file), _)) => file.read(), + _ => 0, + } + } + + /// Writes a byte to the currently-open file. + pub fn write_byte(&mut self, byte: u8) { + match &mut self.entry { + Some((Entry::File(file), _)) => file.write(byte), + _ => (), + } + } + + pub fn pointer(&mut self) -> u32 { + match &mut self.entry { + Some((Entry::File(file), _)) => file.pointer(), + Some((Entry::Directory(dir), _)) => dir.selected(), + _ => 0, + } + } + + pub fn commit_pointer(&mut self) { + match &mut self.entry { + Some((Entry::File(file), _)) => file.set_pointer(self.pointer_write), + Some((Entry::Directory(dir), _)) => dir.set_selected(self.pointer_write), + _ => (), + } + } + + pub fn length(&mut self) -> u32 { + match &mut self.entry { + Some((Entry::File(file), _)) => file.length(), + Some((Entry::Directory(dir), _)) => dir.length(), + _ => 0, + } + } + + pub fn commit_length(&mut self) { + match &mut self.entry { + Some((Entry::File(file), _)) => file.set_length(self.length_write), + _ => (), + } + } + + pub fn flush(&mut self) { + if let Some((Entry::File(buffered_file), _)) = &mut self.entry { + let _ = buffered_file; + } + } +} + + +impl Drop for FileDevice { + fn drop(&mut self) { + self.flush(); + } +} + + +/// Create a new file if it doesn't already exist, returning true if successful. +pub fn create_file(destination: &Path) -> bool { + if entry_exists(destination) { + false + } else { + if let Some(parent_path) = destination.parent() { + let _ = std::fs::create_dir_all(parent_path); + } + std::fs::OpenOptions::new().write(true).create_new(true) + .open(destination).is_ok() + } +} + +/// Move an entry from one location to another, returning true if successful. +pub fn move_entry(source: &Path, destination: &Path) -> bool { + if !entry_exists(source) || entry_exists(destination) { + return false; + } + std::fs::rename(source, destination).is_ok() +} + +/// Delete an entry, returning true if successful. +pub fn delete_entry(source: &Path) -> bool { + use std::fs::{remove_file, remove_dir_all}; + use std::io::ErrorKind; + + match remove_file(source) { + Ok(_) => true, + Err(error) => match error.kind() { + ErrorKind::NotFound => true, + ErrorKind::IsADirectory => match remove_dir_all(source) { + Ok(_) => true, + Err(error) => match error.kind() { + ErrorKind::NotFound => true, + _ => false, + } + } + _ => false, + } + } +} + +/// Returns true if an entry already exists at the given path. +fn entry_exists(source: &Path) -> bool { + std::fs::metadata(source).is_ok() +} diff --git a/src/devices/input_device.rs b/src/devices/input_device.rs new file mode 100644 index 0000000..83e8039 --- /dev/null +++ b/src/devices/input_device.rs @@ -0,0 +1,254 @@ +use crate::*; + +use std::collections::VecDeque; + + +pub struct InputDevice { + pub cursor: ScreenPosition, + pub x_read: u16, + pub y_read: u16, + pub h_scroll_read: i16, + pub v_scroll_read: i16, + pub h_scroll: f32, + pub v_scroll: f32, + pub pointer_buttons: u8, + pub pointer_active: bool, + + pub navigation: u8, + pub modifiers: u8, + pub characters: VecDeque, + pub gamepad_1: OwnedGamepad, + pub gamepad_2: OwnedGamepad, + pub gamepad_3: OwnedGamepad, + pub gamepad_4: OwnedGamepad, + + pub accessed: bool, + pub wake: bool, +} + + +impl Device for InputDevice { + fn read(&mut self, port: u8) -> u8 { + self.accessed = true; + match port { + 0x0 => { self.x_read = self.cursor.x; read_h!(self.x_read) }, + 0x1 => read_l!(self.cursor.x), + 0x2 => { self.y_read = self.cursor.y; read_h!(self.y_read) }, + 0x3 => read_l!(self.cursor.y), + 0x4 => { self.update_horizontal_scroll(); read_h!(self.h_scroll_read) }, + 0x5 => read_l!(self.h_scroll_read), + 0x6 => { self.update_vertical_scroll(); read_h!(self.v_scroll_read) }, + 0x7 => read_l!(self.v_scroll_read), + 0x8 => read_b!(self.pointer_active), + 0x9 => self.pointer_buttons, + 0xA => self.characters.pop_front().unwrap_or(0), + 0xB => self.modifiers, + 0xC => self.gamepad_1.state() | self.navigation, + 0xD => self.gamepad_2.state(), + 0xE => self.gamepad_3.state(), + 0xF => self.gamepad_4.state(), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, _value: u8) -> Option { + let signal = if self.accessed { None } else { Some(Signal::Break) }; + self.accessed = true; + match port { + 0x0 => (), + 0x1 => (), + 0x2 => (), + 0x3 => (), + 0x4 => (), + 0x5 => (), + 0x6 => (), + 0x7 => (), + 0x8 => (), + 0x9 => (), + 0xA => self.characters.clear(), + 0xB => (), + 0xC => (), + 0xD => (), + 0xE => (), + 0xF => (), + _ => unreachable!(), + }; + return signal; + } + + fn wake(&mut self) -> bool { + self.accessed = true; + std::mem::take(&mut self.wake) + } + + fn reset(&mut self) { + self.cursor = ScreenPosition::ZERO; + self.x_read = 0; + self.y_read = 0; + self.h_scroll_read = 0; + self.v_scroll_read = 0; + self.h_scroll = 0.0; + self.v_scroll = 0.0; + self.pointer_active = false; + self.pointer_buttons = 0; + + self.navigation = 0; + self.modifiers = 0; + self.characters.clear(); + self.gamepad_1.reset(); + self.gamepad_2.reset(); + self.gamepad_3.reset(); + self.gamepad_4.reset(); + + self.accessed = false; + self.wake = false; + } +} + + +impl InputDevice { + pub fn new() -> Self { + Self { + cursor: ScreenPosition::ZERO, + x_read: 0, + y_read: 0, + h_scroll_read: 0, + v_scroll_read: 0, + h_scroll: 0.0, + v_scroll: 0.0, + pointer_active: false, + pointer_buttons: 0, + + navigation: 0, + modifiers: 0, + characters: VecDeque::new(), + gamepad_1: OwnedGamepad::new(1), + gamepad_2: OwnedGamepad::new(2), + gamepad_3: OwnedGamepad::new(3), + gamepad_4: OwnedGamepad::new(4), + + accessed: false, + wake: false, + } + } + + pub fn on_gamepad_event(&mut self, event: gilrs::Event) { + if let Some(g) = self.gamepad_1.register(event.id) { + self.wake |= g.process_event(&event); return; } + if let Some(g) = self.gamepad_2.register(event.id) { + self.wake |= g.process_event(&event); return; } + if let Some(g) = self.gamepad_3.register(event.id) { + self.wake |= g.process_event(&event); return; } + if let Some(g) = self.gamepad_4.register(event.id) { + self.wake |= g.process_event(&event); return; } + } + + pub fn on_cursor_enter(&mut self) { + self.pointer_active = true; + self.wake = true; + } + + pub fn on_cursor_exit(&mut self) { + self.pointer_active = false; + self.wake = true; + } + + pub fn on_cursor_move(&mut self, position: Position) { + self.pointer_active = true; + let cursor_position = ScreenPosition { + x: position.x as i16 as u16, + y: position.y as i16 as u16, + }; + if self.cursor != cursor_position { + self.cursor = cursor_position; + self.wake = true; + } + } + + pub fn on_mouse_button(&mut self, button: MouseButton, action: Action) { + let mask = match button { + MouseButton::Left => 0x80, + MouseButton::Right => 0x40, + MouseButton::Middle => 0x20, + _ => return, + }; + let pointer_buttons = match action { + Action::Pressed => self.pointer_buttons | mask, + Action::Released => self.pointer_buttons & !mask, + }; + if self.pointer_buttons != pointer_buttons { + self.pointer_buttons = pointer_buttons; + self.wake = true; + } + } + + pub fn on_horizontal_scroll(&mut self, delta: f32) { + self.h_scroll += delta; + self.h_scroll = self.h_scroll.clamp(-32768.0, 32767.0); + } + + pub fn on_vertical_scroll(&mut self, delta: f32) { + self.v_scroll += delta; + self.v_scroll = self.v_scroll.clamp(i16::MIN as f32, i16::MAX as f32); + } + + pub fn update_horizontal_scroll(&mut self) { + self.h_scroll_read = self.h_scroll.trunc() as i16; + self.h_scroll -= self.h_scroll.trunc(); + } + + pub fn update_vertical_scroll(&mut self) { + self.v_scroll_read = self.v_scroll.trunc() as i16; + self.v_scroll -= self.v_scroll.trunc(); + } + + pub fn on_character(&mut self, character: char) { + let character = match character { + '\r' => '\n', + _ => character, + }; + let mut bytes = [0; 4]; + let string = character.encode_utf8(&mut bytes); + for byte in string.bytes() { + self.characters.push_back(byte); + } + self.wake = true; + } + + pub fn on_keypress(&mut self, key: KeyCode, action: Action) { + let shift = self.modifiers & 0x40 != 0; + let mask = match key { + KeyCode::ArrowUp => 0x80, // up + KeyCode::ArrowDown => 0x40, // down + KeyCode::ArrowLeft => 0x20, // left + KeyCode::ArrowRight => 0x10, // right + KeyCode::Enter => 0x08, // confirm + KeyCode::Escape => 0x04, // cancel + KeyCode::Tab => match shift { // shift + false => 0x02, // next + true => 0x01 // previous + }, + _ => return, + }; + let navigation = match action { + Action::Pressed => self.navigation | mask, + Action::Released => self.navigation & !mask, + }; + if self.navigation != navigation { + self.navigation = navigation; + self.wake = true; + } + } + + pub fn on_modifier(&mut self, state: ModifiersState) { + let mut modifiers = 0; + if state.control_key() { modifiers |= 0x80 } + if state.shift_key() { modifiers |= 0x40 } + if state.alt_key() { modifiers |= 0x20 } + if state.super_key() { modifiers |= 0x10 } + if self.modifiers != modifiers { + self.modifiers = modifiers; + self.wake = true; + } + } +} diff --git a/src/devices/math_device.rs b/src/devices/math_device.rs new file mode 100644 index 0000000..e7043b9 --- /dev/null +++ b/src/devices/math_device.rs @@ -0,0 +1,199 @@ +use crate::*; + +const ANGLE_SCALE: f64 = 10430.378350470453; // 65536 / 2π + + +pub struct MathDevice { + pub x: u16, + pub y: u16, + pub r: u16, + pub t: u16, + pub x_read: Option, + pub y_read: Option, + pub r_read: Option, + pub t_read: Option, + pub prod: Option<(u16, u16)>, // (low, high) + pub quot: Option, + pub rem: Option, +} + + +impl Device for MathDevice { + fn read(&mut self, port: u8) -> u8 { + match port { + 0x0 => read_h!(self.x()), + 0x1 => read_l!(self.x()), + 0x2 => read_h!(self.y()), + 0x3 => read_l!(self.y()), + 0x4 => read_h!(self.r()), + 0x5 => read_l!(self.r()), + 0x6 => read_h!(self.t()), + 0x7 => read_l!(self.t()), + 0x8 => read_h!(self.prod().1), + 0x9 => read_l!(self.prod().1), + 0xA => read_h!(self.prod().0), + 0xB => read_l!(self.prod().0), + 0xC => read_h!(self.quot()), + 0xD => read_l!(self.quot()), + 0xE => read_h!(self.rem()), + 0xF => read_l!(self.rem()), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option { + match port { + 0x0 => { write_h!(self.x, value); self.clear_polar(); }, + 0x1 => { write_l!(self.x, value); self.clear_polar(); }, + 0x2 => { write_h!(self.y, value); self.clear_polar(); }, + 0x3 => { write_l!(self.y, value); self.clear_polar(); }, + 0x4 => { write_h!(self.r, value); self.clear_cartesian(); }, + 0x5 => { write_l!(self.r, value); self.clear_cartesian(); }, + 0x6 => { write_h!(self.t, value); self.clear_cartesian(); }, + 0x7 => { write_l!(self.t, value); self.clear_cartesian(); }, + 0x8 => (), + 0x9 => (), + 0xA => (), + 0xB => (), + 0xC => (), + 0xD => (), + 0xE => (), + 0xF => (), + _ => unreachable!(), + }; + return None; + } + + fn wake(&mut self) -> bool { + false + } + + fn reset(&mut self) { + self.x = 0; + self.y = 0; + self.r = 0; + self.t = 0; + self.clear_cartesian(); + self.clear_polar(); + } +} + + +impl MathDevice { + pub fn new() -> Self { + Self { + x: 0, + y: 0, + r: 0, + t: 0, + x_read: None, + y_read: None, + r_read: None, + t_read: None, + prod: None, + quot: None, + rem: None, + } + } + + pub fn clear_cartesian(&mut self) { + self.x_read = None; + self.y_read = None; + } + + pub fn clear_polar(&mut self) { + self.r_read = None; + self.t_read = None; + self.prod = None; + self.quot = None; + self.rem = None; + } + + pub fn x(&mut self) -> u16 { + match self.x_read { + Some(x) => x, + None => { + let r = self.r as f64; + let t = self.t as f64; + let angle = t / ANGLE_SCALE; + let x = angle.cos() * r; + self.x_read = match x > (i16::MIN as f64) && x < (i16::MAX as f64) { + true => Some(x as i16 as u16), + false => Some(0), + }; + self.x_read.unwrap() + } + } + } + + pub fn y(&mut self) -> u16 { + match self.y_read { + Some(y) => y, + None => { + let r = self.r as f64; + let t = self.t as f64; + let angle = t / ANGLE_SCALE; + let y = angle.sin() * r; + self.y_read = match y > (i16::MIN as f64) && y < (i16::MAX as f64) { + true => Some(y as i16 as u16), + false => Some(0), + }; + self.y_read.unwrap() + } + } + } + + pub fn r(&mut self) -> u16 { + match self.r_read { + Some(r) => r, + None => { + let sum = (self.x as f64).powi(2) + (self.y as f64).powi(2); + self.r_read = Some(sum.sqrt() as u16); + self.r_read.unwrap() + } + } + } + + pub fn t(&mut self) -> u16 { + match self.t_read { + Some(t) => t, + None => { + let x = self.x as i16 as f64; + let y = self.x as i16 as f64; + let angle = f64::atan2(y, x) * ANGLE_SCALE; + self.t_read = Some(angle as i16 as u16); + self.t_read.unwrap() + } + } + } + + pub fn prod(&mut self) -> (u16, u16) { + match self.prod { + Some(prod) => prod, + None => { + self.prod = Some(self.x.widening_mul(self.y)); + self.prod.unwrap() + } + } + } + + pub fn quot(&mut self) -> u16 { + match self.quot { + Some(quot) => quot, + None => { + self.quot = Some(self.x.checked_div(self.y).unwrap_or(0)); + self.quot.unwrap() + } + } + } + + pub fn rem(&mut self) -> u16 { + match self.rem { + Some(rem) => rem, + None => { + self.rem = Some(self.x.checked_rem(self.y).unwrap_or(0)); + self.rem.unwrap() + } + } + } +} diff --git a/src/devices/memory_device.rs b/src/devices/memory_device.rs new file mode 100644 index 0000000..d116ca7 --- /dev/null +++ b/src/devices/memory_device.rs @@ -0,0 +1,186 @@ +use crate::*; + +use std::cmp::min; + + +type Page = [u8; 256]; + + +pub struct MemoryDevice { + pub limit: u16, // maximum allocateable number of pages + pub pages: Vec, // all allocated pages + pub count_write: u16, // number of pages requested by program + pub count: usize, // number of pages allocated for use + pub copy_write: u16, + pub head_1: HeadAddress, + pub head_2: HeadAddress, +} + + +impl Device for MemoryDevice { + fn read(&mut self, port: u8) -> u8 { + match port { + 0x0 => read_h!(self.count), + 0x1 => read_l!(self.count), + 0x2 => read_h!(self.head_1.page), + 0x3 => read_l!(self.head_1.page), + 0x4 => read_h!(self.head_1.address), + 0x5 => read_l!(self.head_1.address), + 0x6 => self.read_head_1(), + 0x7 => self.read_head_1(), + 0x8 => 0x00, + 0x9 => 0x00, + 0xA => read_h!(self.head_2.page), + 0xB => read_l!(self.head_2.page), + 0xC => read_h!(self.head_2.address), + 0xD => read_l!(self.head_2.address), + 0xE => self.read_head_2(), + 0xF => self.read_head_2(), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option { + match port { + 0x0 => write_h!(self.count_write, value), + 0x1 => { write_l!(self.count_write, value); self.allocate(); }, + 0x2 => write_h!(self.head_1.page, value), + 0x3 => write_l!(self.head_1.page, value), + 0x4 => write_h!(self.head_1.address, value), + 0x5 => write_l!(self.head_1.address, value), + 0x6 => self.write_head_1(value), + 0x7 => self.write_head_1(value), + 0x8 => write_h!(self.copy_write, value), + 0x9 => { write_l!(self.copy_write, value); self.copy(); }, + 0xA => write_h!(self.head_2.page, value), + 0xB => write_l!(self.head_2.page, value), + 0xC => write_h!(self.head_2.address, value), + 0xD => write_l!(self.head_2.address, value), + 0xE => self.write_head_2(value), + 0xF => self.write_head_2(value), + _ => unreachable!(), + }; + return None; + } + + fn wake(&mut self) -> bool { + false + } + + fn reset(&mut self) { + self.pages.clear(); + self.count_write = 0; + self.count = 0; + self.copy_write = 0; + self.head_1.reset(); + self.head_2.reset(); + } +} + + +impl MemoryDevice { + pub fn new() -> Self { + Self { + limit: u16::MAX, + pages: Vec::new(), + count_write: 0, + count: 0, + copy_write: 0, + head_1: HeadAddress::new(), + head_2: HeadAddress::new(), + } + } + + pub fn read_head_1(&mut self) -> u8 { + let (page_i, byte_i) = self.head_1.get_indices(); + self.read_byte(page_i, byte_i) + } + + pub fn read_head_2(&mut self) -> u8 { + let (page_i, byte_i) = self.head_2.get_indices(); + self.read_byte(page_i, byte_i) + } + + fn read_byte(&self, page_i: usize, byte_i: usize) -> u8 { + match self.pages.get(page_i) { + Some(page) => page[byte_i], + None => 0, + } + } + + pub fn write_head_1(&mut self, value: u8) { + let (page_i, byte_i) = self.head_1.get_indices(); + self.write_byte(page_i, byte_i, value); + } + + pub fn write_head_2(&mut self, value: u8) { + let (page_i, byte_i) = self.head_2.get_indices(); + self.write_byte(page_i, byte_i, value); + } + + fn write_byte(&mut self, page_i: usize, byte_i: usize, value: u8) { + match self.pages.get_mut(page_i) { + Some(page) => page[byte_i] = value, + None => if page_i < self.count { + self.pages.resize(page_i + 1, [0; 256]); + self.pages[page_i][byte_i] = value; + } + } + } + + pub fn allocate(&mut self) { + self.count = min(self.count_write, self.limit) as usize; + // Defer allocation of new pages. + self.pages.truncate(self.count as usize); + } + + pub fn copy(&mut self) { + let src = self.head_2.page as usize; + let dest = self.head_1.page as usize; + let n = self.copy_write as usize; + + // Pre-allocate destination pages as needed. + let allocate = min(dest + n, self.count); + if allocate > self.pages.len() { + self.pages.resize(allocate, [0; 256]); + } + + for i in 0..n { + let src_page = match self.pages.get(src + i) { + Some(src_page) => src_page.to_owned(), + None => [0; 256], + }; + match self.pages.get_mut(dest + i) { + Some(dest) => *dest = src_page, + None => break, + }; + } + } +} + + +pub struct HeadAddress { + pub page: u16, + pub address: u16, +} + +impl HeadAddress { + pub fn new() -> Self { + Self { + page: 0, + address: 0, + } + } + + pub fn reset(&mut self) { + self.page = 0; + self.address = 0; + } + + pub fn get_indices(&mut self) -> (usize, usize) { + let page_i = (self.page + (self.address / 256)) as usize; + let byte_i = (self.address % 256) as usize; + self.address = self.address.wrapping_add(1); + (page_i, byte_i) + } +} diff --git a/src/devices/mod.rs b/src/devices/mod.rs new file mode 100644 index 0000000..aa98a49 --- /dev/null +++ b/src/devices/mod.rs @@ -0,0 +1,17 @@ +mod system_device; +mod memory_device; +mod math_device; +mod clock_device; +mod input_device; +mod screen_device; +mod stream_device; +mod file_device; + +pub use system_device::*; +pub use memory_device::*; +pub use math_device::*; +pub use clock_device::*; +pub use input_device::*; +pub use screen_device::*; +pub use stream_device::*; +pub use file_device::*; diff --git a/src/devices/screen_device.rs b/src/devices/screen_device.rs new file mode 100644 index 0000000..483bcca --- /dev/null +++ b/src/devices/screen_device.rs @@ -0,0 +1,450 @@ +use crate::*; + +use geometry::*; +use phosphor::*; + + +pub type Sprite = [[u8; 8]; 8]; + +#[derive(Clone, Copy)] +pub enum Layer { Fg, Bg } + + +pub struct ScreenDevice { + /// Each byte represents a screen pixel, left-to-right and top-to-bottom. + // Only the bottom four bits of each byte are used. + pub fg: Vec, + pub bg: Vec, + pub dirty: bool, + + pub cursor: ScreenPosition, + pub vector: ScreenPosition, + + pub dimensions: ScreenDimensions, + pub dirty_dimensions: bool, + pub width_write: u16, + pub height_write: u16, + pub fixed_width: Option, + pub fixed_height: Option, + + pub palette_write: u16, + pub palette: [Colour; 16], + pub sprite_colours: u16, + pub sprite: SpriteBuffer, + + pub accessed: bool, + pub wake: bool, +} + + +impl HasDimensions for ScreenDevice { + fn dimensions(&self) -> ScreenDimensions { + self.dimensions + } +} + + +impl Device for ScreenDevice { + fn read(&mut self, port: u8) -> u8 { + self.accessed = true; + match port { + 0x0 => read_h!(self.cursor.x), + 0x1 => read_l!(self.cursor.x), + 0x2 => read_h!(self.cursor.y), + 0x3 => read_l!(self.cursor.y), + 0x4 => read_h!(self.dimensions.width), + 0x5 => read_l!(self.dimensions.width), + 0x6 => read_h!(self.dimensions.height), + 0x7 => read_l!(self.dimensions.height), + 0x8 => 0, + 0x9 => 0, + 0xA => 0, + 0xB => 0, + 0xC => 0, + 0xD => 0, + 0xE => 0, + 0xF => 0, + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option { + let signal = if self.accessed { None } else { Some(Signal::Break) }; + self.accessed = true; + match port { + 0x0 => write_h!(self.cursor.x, value), + 0x1 => write_l!(self.cursor.x, value), + 0x2 => write_h!(self.cursor.y, value), + 0x3 => write_l!(self.cursor.y, value), + 0x4 => write_h!(self.width_write, value), + 0x5 => { write_l!(self.width_write, value); self.resize_width() } + 0x6 => write_h!(self.height_write, value), + 0x7 => { write_l!(self.height_write, value); self.resize_height() } + 0x8 => write_h!(self.palette_write, value), + 0x9 => { write_l!(self.palette_write, value); self.set_palette() } + 0xA => write_h!(self.sprite_colours, value), + 0xB => write_l!(self.sprite_colours, value), + 0xC => self.sprite.push_byte(value), + 0xD => self.sprite.push_byte(value), + 0xE => self.draw_dispatch(value), + 0xF => self.move_cursor(value), + _ => unreachable!(), + }; + return signal; + } + + fn wake(&mut self) -> bool { + self.accessed = true; + std::mem::take(&mut self.wake) + } + + fn reset(&mut self) { + self.fg.clear(); + self.bg.clear(); + self.dirty = false; + + self.cursor = ScreenPosition::ZERO; + self.vector = ScreenPosition::ZERO; + + self.dirty_dimensions = true; + self.width_write = 0; + self.height_write = 0; + self.fixed_width = None; + self.fixed_height = None; + + self.palette_write = 0; + self.palette = [ + Colour::grey(0x00), Colour::grey(0xFF), Colour::grey(0x55), Colour::grey(0xAA), + Colour::grey(0x00), Colour::grey(0xFF), Colour::grey(0x55), Colour::grey(0xAA), + Colour::grey(0x00), Colour::grey(0xFF), Colour::grey(0x55), Colour::grey(0xAA), + Colour::grey(0x00), Colour::grey(0xFF), Colour::grey(0x55), Colour::grey(0xAA), + ]; + self.sprite_colours = 0; + self.sprite = SpriteBuffer::new(); + + self.accessed = false; + self.wake = false; + } +} + + +impl ScreenDevice { + pub fn new(config: &EmulatorConfig) -> Self { + let area = config.dimensions.area_usize(); + + Self { + fg: vec![0; area], + bg: vec![0; area], + dirty: false, + + cursor: ScreenPosition::ZERO, + vector: ScreenPosition::ZERO, + + dimensions: config.dimensions, + dirty_dimensions: true, + width_write: 0, + height_write: 0, + fixed_width: None, + fixed_height: None, + + palette_write: 0, + palette: [Colour::BLACK; 16], + sprite_colours: 0, + sprite: SpriteBuffer::new(), + + accessed: false, + wake: false, + } + } + + /// Resize screen to match window dimensions. + pub fn resize(&mut self, dimensions: phosphor::Dimensions) { + // Replace dimensions with fixed dimensions. + let screen_dimensions = ScreenDimensions { + width: match self.fixed_width { + Some(fixed_width) => fixed_width, + None => dimensions.width as u16, + }, + height: match self.fixed_height { + Some(fixed_height) => fixed_height, + None => dimensions.height as u16, + }, + }; + let old_dimensions = self.dimensions; + if self.dimensions != screen_dimensions { + self.dimensions = screen_dimensions; + self.resize_layers(old_dimensions); + self.wake = true; + } + } + + /// Internal resize. + fn resize_width(&mut self) { + self.fixed_width = Some(self.width_write); + self.dirty_dimensions = true; + let old_dimensions = self.dimensions; + if self.dimensions.width != self.width_write { + self.dimensions.width = self.width_write; + self.resize_layers(old_dimensions); + } + } + + /// Internal resize. + fn resize_height(&mut self) { + self.fixed_height = Some(self.height_write); + self.dirty_dimensions = true; + let old_dimensions = self.dimensions; + if self.dimensions.height != self.height_write { + self.dimensions.height = self.height_write; + self.resize_layers(old_dimensions); + } + } + + fn resize_layers(&mut self, old_dimensions: ScreenDimensions) { + use std::cmp::{min, Ordering}; + + let old_width = old_dimensions.width as usize; + let old_height = old_dimensions.height as usize; + let new_width = self.dimensions.width as usize; + let new_height = self.dimensions.height as usize; + let new_area = self.dimensions.area_usize(); + let y_range = 0..min(old_height, new_height); + let new_colour = match self.fg.last() { + None | Some(0) => *self.bg.last().unwrap_or(&0), + Some(colour) => *colour, + }; + + match new_width.cmp(&old_width) { + Ordering::Less => { + for y in y_range { + let src = y * old_width; + let dest = y * new_width; + let len = new_width; + self.fg.copy_within(src..src+len, dest); + self.bg.copy_within(src..src+len, dest); + } + self.fg.resize(new_area, 0); + self.bg.resize(new_area, new_colour); + }, + Ordering::Greater => { + self.fg.resize(new_area, 0); + self.bg.resize(new_area, new_colour); + for y in y_range.rev() { + let src = y * old_width; + let dest = y * new_width; + let len = old_width; + self.fg.copy_within(src..src+len, dest); + self.bg.copy_within(src..src+len, dest); + self.fg[dest+len..dest+new_width].fill(0); + self.bg[dest+len..dest+new_width].fill(new_colour); + } + }, + Ordering::Equal => { + self.fg.resize(new_area, 0); + self.bg.resize(new_area, new_colour); + }, + }; + + self.dirty = true; + } + + pub fn set_palette(&mut self) { + let i = (self.palette_write >> 12 ) as usize; + let r = (self.palette_write >> 8 & 0xF) as u8 * 17; + let g = (self.palette_write >> 4 & 0xF) as u8 * 17; + let b = (self.palette_write & 0xF) as u8 * 17; + let colour = Colour::from_rgb(r, g, b); + if self.palette[i] != colour { + self.palette[i] = colour; + self.dirty = true; + } + } + + pub fn draw_dispatch(&mut self, draw: u8) { + match draw >> 4 { + 0x0 => self.op_draw_pixel(Layer::Bg, draw), + 0x1 => self.op_draw_sprite(Layer::Bg, draw), + 0x2 => self.op_fill_layer(Layer::Bg, draw), + 0x3 => self.op_draw_sprite(Layer::Bg, draw), + 0x4 => self.op_draw_line(Layer::Bg, draw), + 0x5 => self.op_draw_line(Layer::Bg, draw), + 0x6 => self.op_draw_rect(Layer::Bg, draw), + 0x7 => self.op_draw_rect(Layer::Bg, draw), + 0x8 => self.op_draw_pixel(Layer::Fg, draw), + 0x9 => self.op_draw_sprite(Layer::Fg, draw), + 0xA => self.op_fill_layer(Layer::Fg, draw), + 0xB => self.op_draw_sprite(Layer::Fg, draw), + 0xC => self.op_draw_line(Layer::Fg, draw), + 0xD => self.op_draw_line(Layer::Fg, draw), + 0xE => self.op_draw_rect(Layer::Fg, draw), + 0xF => self.op_draw_rect(Layer::Fg, draw), + _ => unreachable!(), + } + self.vector = self.cursor; + self.dirty = true; + } + + pub fn move_cursor(&mut self, value: u8) { + let distance = (value & 0x3F) as u16; + match value >> 6 { + 0b00 => self.cursor.x = self.cursor.x.wrapping_add(distance), + 0b01 => self.cursor.y = self.cursor.y.wrapping_add(distance), + 0b10 => self.cursor.x = self.cursor.x.wrapping_sub(distance), + 0b11 => self.cursor.y = self.cursor.y.wrapping_sub(distance), + _ => unreachable!(), + }; + } + + /// Colour must already be masked by 0xF. + pub fn draw_pixel(&mut self, layer: Layer, x: u16, y: u16, colour: u8) { + if x < self.dimensions.width && y < self.dimensions.height { + let index = x as usize + (self.dimensions.width as usize * y as usize); + match layer { + Layer::Fg => self.fg[index] = colour, + Layer::Bg => self.bg[index] = colour, + }; + } + } + + fn op_draw_pixel(&mut self, layer: Layer, draw: u8) { + self.draw_pixel(layer, self.cursor.x, self.cursor.y, draw & 0xF); + } + + fn op_fill_layer(&mut self, layer: Layer, draw: u8) { + match layer { + Layer::Fg => self.fg.fill(draw & 0xF), + Layer::Bg => self.bg.fill(draw & 0xF), + } + } + + fn op_draw_sprite(&mut self, layer: Layer, draw: u8) { + let sprite = match draw & 0x20 != 0 { + true => self.sprite.read_2bit_sprite(draw), + false => self.sprite.read_1bit_sprite(draw), + }; + let colours = [ + (self.sprite_colours >> 12 & 0x000F) as u8, + (self.sprite_colours >> 8 & 0x000F) as u8, + (self.sprite_colours >> 4 & 0x000F) as u8, + (self.sprite_colours & 0x000F) as u8, + ]; + let cx = self.cursor.x; + let cy = self.cursor.y; + + if draw & 0x08 != 0 { + // Draw sprite with transparent background + for y in 0..8 { + for x in 0..8 { + let index = sprite[y as usize][x as usize] as usize; + if index != 0 { + let px = cx.wrapping_add(x); + let py = cy.wrapping_add(y); + self.draw_pixel(layer, px, py, colours[index]); + } + } + } + } else { + // Draw sprite with opaque background + for y in 0..8 { + for x in 0..8 { + let index = sprite[y as usize][x as usize] as usize; + let px = cx.wrapping_add(x); + let py = cy.wrapping_add(y); + self.draw_pixel(layer, px, py, colours[index]); + } + } + } + } + + fn op_draw_line(&mut self, layer: Layer, draw: u8) { + let mut x: i16 = self.cursor.x as i16; + let mut y: i16 = self.cursor.y as i16; + let x_end: i16 = self.vector.x as i16; + let y_end: i16 = self.vector.y as i16; + + let dx: i32 = ((x_end as i32) - (x as i32)).abs(); + let dy: i32 = -((y_end as i32) - (y as i32)).abs(); + let sx: i16 = if x < x_end { 1 } else { -1 }; + let sy: i16 = if y < y_end { 1 } else { -1 }; + let mut e1: i32 = dx + dy; + + if draw & 0x10 != 0 { + // Draw 1-bit textured line. + let sprite = self.sprite.read_1bit_sprite(draw); + let c1 = (self.sprite_colours >> 8 & 0xF) as u8; + let c0 = (self.sprite_colours >> 12 & 0xF) as u8; + let opaque = draw & 0x08 == 0; + loop { + let sprite_pixel = sprite[(y as usize) % 8][(x as usize) % 8]; + if sprite_pixel != 0 { self.draw_pixel(layer, x as u16, y as u16, c1); } + else if opaque { self.draw_pixel(layer, x as u16, y as u16, c0); } + if x == x_end && y == y_end { break; } + let e2 = e1 << 1; + if e2 >= dy { e1 += dy; x += sx; } + if e2 <= dx { e1 += dx; y += sy; } + } + } else { + // Draw solid line. + let colour = draw & 0xF; + loop { + self.draw_pixel(layer, x as u16, y as u16, colour); + if x == x_end && y == y_end { break; } + let e2 = e1 << 1; + if e2 >= dy { e1 += dy; x += sx; } + if e2 <= dx { e1 += dx; y += sy; } + } + } + } + + fn op_draw_rect(&mut self, layer: Layer, draw: u8) { + macro_rules! clamp { + ($v:expr, $max:expr) => { + if $v > 0x7FFF { 0 } else if $v > $max { $max } else { $v } + }; + } + macro_rules! out_of_bounds { + ($axis:ident, $max:expr) => {{ + let c = self.cursor.$axis; + let v = self.vector.$axis; + c >= $max && v >= $max && (c >= 0x8000) == (v >= 0x8000) + }}; + } + + let out_of_bounds_x = out_of_bounds!(x, self.dimensions.width); + let out_of_bounds_y = out_of_bounds!(y, self.dimensions.height); + if out_of_bounds_x || out_of_bounds_y { return; } + + // Get bounding box. + let mut l = clamp!(self.vector.x, self.dimensions.width -1); + let mut r = clamp!(self.cursor.x, self.dimensions.width -1); + let mut t = clamp!(self.vector.y, self.dimensions.height -1); + let mut b = clamp!(self.cursor.y, self.dimensions.height -1); + if l > r { std::mem::swap(&mut l, &mut r) }; + if t > b { std::mem::swap(&mut t, &mut b) }; + + if draw & 0x10 != 0 { + // Draw 1-bit textured rectangle. + let sprite = self.sprite.read_1bit_sprite(draw); + let c1 = (self.sprite_colours >> 8 & 0xF) as u8; + let c0 = (self.sprite_colours >> 12 & 0xF) as u8; + let opaque = draw & 0x08 == 0; + for y in t..=b { + for x in l..=r { + let sprite_colour = sprite[(y as usize) % 8][(x as usize) % 8]; + if sprite_colour != 0 { self.draw_pixel(layer, x, y, c1); } + else if opaque { self.draw_pixel(layer, x, y, c0); } + } + } + } else { + // Draw solid rectangle. + let colour = draw & 0xF; + for y in t..=b { + for x in l..=r { + self.draw_pixel(layer, x, y, colour); + } + } + } + } +} + + diff --git a/src/devices/stream_device.rs b/src/devices/stream_device.rs new file mode 100644 index 0000000..1e67166 --- /dev/null +++ b/src/devices/stream_device.rs @@ -0,0 +1,239 @@ +use crate::*; + +use std::collections::VecDeque; +use std::io::{BufRead, Stdout, Write}; +use std::sync::mpsc::{self, TryRecvError}; + + +pub struct StreamDevice { + /// True if a source is connected to stdin. + stdin_connected: bool, + /// True if a transmission is in progress. + stdin_control: bool, + stdin_rx: mpsc::Receiver>, + /// Bytes received in the current transmission. + stdin_queue: VecDeque, + /// Bytes received since stdin end-of-transmission. + stdin_excess: VecDeque, + + stdout: Stdout, + /// True if a sink is connected to stdout. + stdout_connected: bool, + + /// True if stdin is transmission-encoded. + decode_stdin: bool, + /// True if stdout should be transmission-encoded. + encode_stdout: bool, + /// Half-byte buffer for decoding stdin. + decode_buffer: Option, + + wake: bool, +} + + +impl Device for StreamDevice { + fn read(&mut self, port: u8) -> u8 { + match port { + 0x0 => read_b!(self.stdin_connected), + 0x1 => read_b!(self.stdout_connected), + 0x2 => read_b!(self.stdin_control), + 0x3 => read_b!(true), + 0x4 => self.stdin_length(), + 0x5 => read_b!(true), + 0x6 => self.stdin_read(), + 0x7 => self.stdin_read(), + 0x8 => todo!(), + 0x9 => todo!(), + 0xA => todo!(), + 0xB => todo!(), + 0xC => todo!(), + 0xD => todo!(), + 0xE => todo!(), + 0xF => todo!(), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option { + match port { + 0x0 => (), + 0x1 => (), + 0x2 => self.stdin_start_transmission(), + 0x3 => self.stdout_end_transmission(), + 0x4 => (), + 0x5 => (), + 0x6 => self.stdout_write(value), + 0x7 => self.stdout_write(value), + 0x8 => todo!(), + 0x9 => todo!(), + 0xA => todo!(), + 0xB => todo!(), + 0xC => todo!(), + 0xD => todo!(), + 0xE => todo!(), + 0xF => todo!(), + _ => unreachable!(), + }; + return None; + } + + fn wake(&mut self) -> bool { + self.fetch_stdin_data(); + std::mem::take(&mut self.wake) + } + + fn reset(&mut self) { + todo!() + } +} + + +impl StreamDevice { + pub fn new(config: &EmulatorConfig) -> Self { + // Spawn a thread to enable non-blocking reads of stdin. + let (stdin_tx, stdin_rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || loop { + let mut stdin = std::io::stdin().lock(); + match stdin.fill_buf() { + Ok(buf) if !buf.is_empty() => { + let length = buf.len(); + stdin_tx.send(buf.to_vec()).unwrap(); + stdin.consume(length); + } + _ => break, + }; + }); + + Self { + stdin_connected: true, + stdin_control: false, + stdin_rx, + stdin_queue: VecDeque::new(), + stdin_excess: VecDeque::new(), + + stdout: std::io::stdout(), + stdout_connected: true, + + decode_stdin: config.decode_stdin, + encode_stdout: config.encode_stdout, + decode_buffer: None, + + wake: true, + } + } + + pub fn stdin_length(&mut self) -> u8 { + self.fetch_stdin_data(); + self.stdin_queue.len().try_into().unwrap_or(u8::MAX) + } + + /// Start a transmission on stdin. + pub fn stdin_start_transmission(&mut self) { + self.stdin_control = true; + } + + pub fn stdin_read(&mut self) -> u8 { + self.fetch_stdin_data(); + self.stdin_queue.pop_front().unwrap_or(0) + } + + pub fn stdout_write(&mut self, value: u8) { + macro_rules! hex { + ($value:expr) => { match $value { + 0x0..=0x9 => $value + b'0', + 0xA..=0xF => $value - 0x0A + b'A', + _ => unreachable!("Cannot encode value as hex digit: 0x{:02X}", $value), + } }; + } + if self.encode_stdout { + let encoded = [hex!(value >> 4), hex!(value & 0xF), b' ']; + self.stdout_write_raw(&encoded); + } else { + self.stdout_write_raw(&[value]); + }; + } + + fn stdout_write_raw(&mut self, bytes: &[u8]) { + if let Err(_) = self.stdout.write_all(bytes) { + if self.stdout_connected { + self.stdout_connected = false; + self.wake = true; // wake because stdout was disconnected. + } + } + } + + /// End the current transmission on stdout. + pub fn stdout_end_transmission(&mut self) { + self.stdout_write_raw(&[b'\n']); + } + + /// Fetch all pending data from stdin. + pub fn fetch_stdin_data(&mut self) { + while self.stdin_control { + match self.stdin_excess.pop_front() { + Some(byte) => self.fetch_byte(byte), + None => break, + } + } + loop { + match self.stdin_rx.try_recv() { + Ok(tx) => { + for byte in tx { + match self.stdin_control { + true => self.fetch_byte(byte), + false => self.stdin_excess.push_back(byte), + } + } + } + Err(TryRecvError::Empty) => { + break; + } + Err(TryRecvError::Disconnected) => { + self.stdin_control = false; + if self.stdin_connected { + self.stdin_connected = false; + self.wake = true; // wake because stdin was disconnected. + } + break; + } + } + } + } + + fn fetch_byte(&mut self, byte: u8) { + if self.decode_stdin { + let decoded = match byte { + b'0'..=b'9' => byte - b'0', + b'a'..=b'f' => byte - b'a' + 0x0A, + b'A'..=b'F' => byte - b'A' + 0x0A, + b'\n' => { + self.decode_buffer = None; + self.stdin_control = false; + self.wake = true; // wake because a transmission ended. + return; + }, + _ => return, + }; + if let Some(high) = std::mem::take(&mut self.decode_buffer) { + self.stdin_queue.push_back((high << 4) | decoded); + self.wake = true; // wake because a byte was received. + } else { + self.decode_buffer = Some(decoded); + } + } else { + self.stdin_queue.push_back(byte); + self.wake = true; // wake because a byte was received. + } + } + + pub fn flush(&mut self) { + let _ = self.stdout.flush(); + } +} + + +impl Drop for StreamDevice { + fn drop(&mut self) { + self.flush(); + } +} diff --git a/src/devices/system_device.rs b/src/devices/system_device.rs new file mode 100644 index 0000000..097c616 --- /dev/null +++ b/src/devices/system_device.rs @@ -0,0 +1,119 @@ +use crate::*; + + +pub struct SystemDevice { + /// Name and version of this system. + pub name: StringBuffer, + /// Authors of this system. + pub authors: StringBuffer, + /// Mask of all devices permitted to wake from sleep. + pub wake_mask: u16, + /// Slot number of device that most recently woke the system. + pub wake_slot: u8, + /// True if the system has been put to sleep. + pub asleep: bool, + /// Mask of all connected devices. + pub connected_devices: u16, + /// Name of the first custom device. + pub custom1: StringBuffer, + /// Name of the second custom device. + pub custom2: StringBuffer, + /// Name of the third custom device. + pub custom3: StringBuffer, + /// Name of the fourth custom device. + pub custom4: StringBuffer, +} + + +impl Device for SystemDevice { + fn read(&mut self, port: u8) -> u8 { + match port { + 0x0 => 0x00, + 0x1 => 0x00, + 0x2 => self.wake_slot, + 0x3 => 0x00, + 0x4 => self.custom1.read(), + 0x5 => self.custom2.read(), + 0x6 => self.custom3.read(), + 0x7 => self.custom4.read(), + 0x8 => self.name.read(), + 0x9 => self.authors.read(), + 0xA => 0x00, + 0xB => 0x00, + 0xC => 0x00, + 0xD => 0x00, + 0xE => read_h!(self.connected_devices), + 0xF => read_l!(self.connected_devices), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option { + match port { + 0x0 => write_h!(self.wake_mask, value), + 0x1 => { + write_l!(self.wake_mask, value); + self.asleep = true; + return Some(Signal::Sleep); + } + 0x2 => (), + 0x3 => match value { + 0 => return Some(Signal::Reset), + _ => return Some(Signal::Fork), + } + 0x4 => self.custom1.restart(), + 0x5 => self.custom2.restart(), + 0x6 => self.custom3.restart(), + 0x7 => self.custom4.restart(), + 0x8 => self.name.restart(), + 0x9 => self.authors.restart(), + 0xA => (), + 0xB => (), + 0xC => (), + 0xD => (), + 0xE => (), + 0xF => (), + _ => unreachable!(), + }; + return None; + } + + fn wake(&mut self) -> bool { + true + } + + fn reset(&mut self) { + self.wake_mask = 0; + self.wake_slot = 0; + self.custom1.restart(); + self.custom2.restart(); + self.custom3.restart(); + self.custom4.restart(); + self.name.restart(); + self.authors.restart(); + } +} + + +impl SystemDevice { + pub fn new(connected_devices: u16) -> Self { + let pkg_name = env!("CARGO_PKG_NAME"); + let pkg_version = env!("CARGO_PKG_VERSION"); + let pkg_authors = env!("CARGO_PKG_AUTHORS"); + let name = format!("{pkg_name}/{pkg_version}"); + let authors = pkg_authors.replace(':', "\n"); + + Self { + name: StringBuffer::from_str(&name), + authors: StringBuffer::from_str(&authors), + wake_mask: 0, + wake_slot: 0, + asleep: false, + connected_devices, + custom1: StringBuffer::new(), + custom2: StringBuffer::new(), + custom3: StringBuffer::new(), + custom4: StringBuffer::new(), + } + } +} diff --git a/src/emulators/graphical_emulator.rs b/src/emulators/graphical_emulator.rs new file mode 100644 index 0000000..598a5a2 --- /dev/null +++ b/src/emulators/graphical_emulator.rs @@ -0,0 +1,339 @@ +use crate::*; + +use gilrs::Gilrs; + + +pub struct GraphicalEmulator { + pub br: BedrockEmulator, + pub debug: DebugState, + pub gilrs: Option, + + pub fullscreen: bool, + pub scale: u32, + pub render_mark: Instant, // last time screen was rendered + pub frame_mark: Instant, // refreshes when clean + pub replace_palette: bool, + pub config: EmulatorConfig, +} + +impl GraphicalEmulator { + pub fn new(config: EmulatorConfig, debug: bool) -> Self { + let gilrs = match Gilrs::new() { + Ok(gilrs) => Some(gilrs), + Err(err) => { + info!("Could not start gamepad listener: {}", err); + None + } + }; + + Self { + br: BedrockEmulator::new(GraphicalDeviceBus::new(&config)), + debug: DebugState::new(debug, config.symbols_path.as_ref()), + gilrs, + + fullscreen: config.fullscreen, + scale: config.zoom.into(), + replace_palette: config.palette.is_some(), + render_mark: Instant::now(), + frame_mark: Instant::now(), + config, + } + } + + pub fn load_program(&mut self, bytecode: &[u8]) { + self.br.core.mem.load_program(bytecode); + } + + pub fn run(self, mut phosphor: Phosphor, visible: bool) { + let window = WindowBuilder { + dimensions: Some(self.dimensions()), + size_bounds: Some(self.size_bounds()), + fullscreen: self.fullscreen, + scale: self.scale, + title: Some(self.config.title.clone()), + cursor: match self.config.show_cursor { + true => Some(CursorIcon::Default), + false => None, + }, + icon: self.config.icon.clone(), + program: Box::new(self), + visible, + }; + + phosphor.add_window(window); + phosphor.run().unwrap(); + } + + pub fn size_bounds(&self) -> SizeBounds { + match self.fullscreen { + true => SizeBounds { + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + false => SizeBounds { + min_width: self.br.dev.screen.fixed_width.map(u32::from), + max_width: self.br.dev.screen.fixed_width.map(u32::from), + min_height: self.br.dev.screen.fixed_height.map(u32::from), + max_height: self.br.dev.screen.fixed_height.map(u32::from), + }, + } + } + + pub fn dimensions(&self) -> Dimensions { + Dimensions { + width: self.br.dev.screen.dimensions.width.into(), + height: self.br.dev.screen.dimensions.height.into(), + } + } +} + + +pub struct GraphicalDeviceBus { + pub system: SystemDevice, + pub memory: MemoryDevice, + pub math: MathDevice, + pub clock: ClockDevice, + pub input: InputDevice, + pub screen: ScreenDevice, + pub stream: StreamDevice, + pub file: FileDevice, + pub wake_queue: WakeQueue, +} + +impl GraphicalDeviceBus { + pub fn new(config: &EmulatorConfig) -> Self { + Self { + system: SystemDevice::new(0b1111_1100_1100_0000), + memory: MemoryDevice::new(), + math: MathDevice::new(), + clock: ClockDevice::new(), + input: InputDevice::new(), + screen: ScreenDevice::new(&config), + stream: StreamDevice::new(&config), + file: FileDevice::new(), + wake_queue: WakeQueue::new(), + } + } +} + + +impl DeviceBus for GraphicalDeviceBus { + fn get_device(&mut self, slot: u8) -> Option<&mut dyn Device> { + match slot { + 0x0 => Some(&mut self.system), + 0x1 => Some(&mut self.memory), + 0x2 => Some(&mut self.math ), + 0x3 => Some(&mut self.clock ), + 0x4 => Some(&mut self.input ), + 0x5 => Some(&mut self.screen), + 0x8 => Some(&mut self.stream), + 0x9 => Some(&mut self.file ), + _ => None + } + } + + fn wake(&mut self) -> bool { + for slot in self.wake_queue.iter(self.system.wake_mask) { + if let Some(device) = self.get_device(slot) { + if device.wake() { + self.system.wake_slot = slot; + self.system.asleep = false; + self.wake_queue.wake(slot); + return true; + } + } + } + return false; + } +} + + +impl WindowProgram for GraphicalEmulator { + fn handle_event(&mut self, event: Event, r: &mut EventWriter) { + match event { + Event::CloseRequest => r.write(Request::CloseWindow), + Event::CursorEnter => self.br.dev.input.on_cursor_enter(), + Event::CursorExit => self.br.dev.input.on_cursor_exit(), + Event::CursorMove(p) => self.br.dev.input.on_cursor_move(p), + Event::Resize(d) => self.br.dev.screen.resize(d), + Event::CharacterInput(c) => self.br.dev.input.on_character(c), + Event::ModifierChange(m) => self.br.dev.input.on_modifier(m), + Event::MouseButton { button, action } => self.br.dev.input.on_mouse_button(button, action), + Event::FocusChange(_) => (), + Event::Initialise => (), + Event::ScrollLines { axis, distance } => match axis { + Axis::Horizontal => self.br.dev.input.on_horizontal_scroll(distance / 20.0), + Axis::Vertical => self.br.dev.input.on_vertical_scroll(distance / 20.0), + } + Event::ScrollPixels { axis, distance } => match axis { + Axis::Horizontal => self.br.dev.input.on_horizontal_scroll(distance), + Axis::Vertical => self.br.dev.input.on_vertical_scroll(distance), + } + + Event::Close => (), + Event::KeyboardInput { key, action } => { + self.br.dev.input.on_keypress(key, action); + if action == Action::Pressed { + match key { + KeyCode::F2 => { + self.replace_palette = !self.replace_palette; + r.write(Request::Redraw); + }, + KeyCode::F5 => { + self.scale = std::cmp::max(1, self.scale.saturating_sub(1)); + r.write(Request::SetPixelScale(self.scale)); + }, + KeyCode::F6 => { + self.scale = self.scale.saturating_add(1); + r.write(Request::SetPixelScale(self.scale)); + }, + KeyCode::F11 => { + self.fullscreen = !self.fullscreen; + r.write(Request::SetFullscreen(self.fullscreen)); + }, + _ => (), + } + } + } + _ => (), + } + } + + fn process(&mut self, requests: &mut EventWriter) { + self.br.dev.stream.flush(); + if let Some(gilrs) = &mut self.gilrs { + while let Some(event) = gilrs.next_event() { + self.br.dev.input.on_gamepad_event(event); + } + } + + if self.br.dev.system.asleep { + // Stay asleep if there are no pending wake events. + if !self.br.dev.wake() { + if self.br.dev.screen.dirty { + requests.write(Request::Redraw); + } + std::thread::sleep(TICK_DURATION); + return; + } + + // Wait for the current frame to be rendered. + if self.br.dev.screen.dirty { + requests.write(Request::Redraw); + std::thread::sleep(TICK_DURATION); + return; + } + } + + // Run the processor for the remainder of the frame. + let frame_end = Instant::now() + TICK_DURATION; + while Instant::now() < frame_end { + if let Some(signal) = self.br.evaluate(BATCH_SIZE, self.debug.enabled) { + match signal { + Signal::Break => { + if self.br.dev.input.accessed || self.br.dev.screen.accessed { + requests.write(Request::SetVisible(true)); + } + } + Signal::Fork | Signal::Reset => { + self.br.reset(); + } + Signal::Sleep => { + self.br.dev.system.asleep = true; + break; + } + Signal::Halt => { + self.br.dev.stream.flush(); + info!("Program halted, exiting."); + self.debug.debug_full(&self.br.core); + requests.write(Request::CloseWindow); + break; + } + Signal::Debug(debug_signal) => match debug_signal { + Debug::Debug1 => self.debug.debug_full(&self.br.core), + Debug::Debug2 => self.debug.debug_timing(&self.br.core), + _ => (), + } + } + } + } + + if std::mem::take(&mut self.br.dev.screen.dirty_dimensions) { + requests.write(Request::SetSizeBounds(self.size_bounds())); + } + + if self.br.dev.screen.dirty { + if self.br.dev.system.asleep { + requests.write(Request::Redraw); + } else if self.frame_mark.elapsed() > MAX_FRAME_DURATION { + requests.write(Request::Redraw); + } + } else { + self.frame_mark = Instant::now(); + } + } + + fn render(&mut self, buffer: &mut Buffer, _full: bool) { + let screen = &mut self.br.dev.screen; + + // Generate table for calculating pixel colours from layer values. + // A given screen pixel will be rendered as the colour given by + // table[fg][bg], where fg and bg are the corresponding layer values. + let mut table = [Colour::BLACK; 256]; + let palette = match self.config.palette { + Some(palette) => match self.replace_palette { + true => palette, + false => screen.palette, + } + None => screen.palette, + }; + table[0..16].clone_from_slice(&palette); + for i in 1..16 { table[i*16..(i+1)*16].fill(palette[i]); } + + // Copy pixels to buffer when it is the same size as the screen. + if buffer.area_usize() == screen.area_usize() { + for (i, colour) in buffer.iter_mut().enumerate() { + let fg = screen.fg[i]; + let bg = screen.bg[i]; + let index = unsafe { fg.unchecked_shl(4) | bg }; + *colour = table[index as usize]; + // TODO: merge fg and bg: *colour = table[screen.bg[i] as usize]; + } + // Copy pixels to buffer when it is a different size to the screen. + } else { + let buffer_width = buffer.width() as usize; + let buffer_height = buffer.height() as usize; + let screen_width = screen.width() as usize; + let screen_height = screen.height() as usize; + let width = std::cmp::min(buffer_width, screen_width ); + let height = std::cmp::min(buffer_height, screen_height); + + let mut bi = 0; + let mut si = 0; + for _ in 0..height { + let bi_next = bi + buffer_width; + let si_next = si + screen_width; + for _ in 0..width { + let fg = screen.fg[si]; + let bg = screen.bg[si]; + let index = unsafe { fg.unchecked_shl(4) | bg }; + buffer[bi] = table[index as usize]; + bi += 1; + si += 1; + } + // Fill remaining right edge with background colour. + buffer[bi..bi_next].fill(table[0]); + bi = bi_next; + si = si_next; + } + // Fill remaining bottom edge with background colour. + buffer[bi..].fill(table[0]); + } + + screen.dirty = false; + self.render_mark = Instant::now(); + self.frame_mark = Instant::now(); + } +} diff --git a/src/emulators/headless_emulator.rs b/src/emulators/headless_emulator.rs new file mode 100644 index 0000000..cac58cf --- /dev/null +++ b/src/emulators/headless_emulator.rs @@ -0,0 +1,105 @@ +use crate::*; + + +pub struct HeadlessEmulator { + pub br: BedrockEmulator, + pub debug: DebugState, +} + +impl HeadlessEmulator { + pub fn new(config: &EmulatorConfig, debug: bool) -> Self { + Self { + br: BedrockEmulator::new(HeadlessDeviceBus::new(config)), + debug: DebugState::new(debug, config.symbols_path.as_ref()), + } + } + + pub fn load_program(&mut self, bytecode: &[u8]) { + self.br.core.mem.load_program(bytecode); + } + + fn sleep(&mut self) { + loop { + if self.br.dev.wake() { break; } + std::thread::sleep(TICK_DURATION); + } + } + + fn halt(&mut self) { + self.br.dev.stream.flush(); + info!("Program halted, exiting."); + self.debug.debug_full(&self.br.core); + std::process::exit(0); + } + + pub fn run(&mut self) -> ! { + loop { + if let Some(signal) = self.br.evaluate(BATCH_SIZE, self.debug.enabled) { + match signal { + Signal::Fork | Signal::Reset => self.br.reset(), + Signal::Sleep => self.sleep(), + Signal::Halt => self.halt(), + Signal::Debug(debug_signal) => match debug_signal { + Debug::Debug1 => self.debug.debug_full(&self.br.core), + Debug::Debug2 => self.debug.debug_timing(&self.br.core), + _ => (), + } + _ => (), + } + } + } + } +} + + +pub struct HeadlessDeviceBus { + pub system: SystemDevice, + pub memory: MemoryDevice, + pub math: MathDevice, + pub clock: ClockDevice, + pub stream: StreamDevice, + pub file: FileDevice, + pub wake_queue: WakeQueue, +} + +impl HeadlessDeviceBus { + pub fn new(config: &EmulatorConfig) -> Self { + Self { + system: SystemDevice::new(0b1111_0000_1100_0000), + memory: MemoryDevice::new(), + math: MathDevice::new(), + clock: ClockDevice::new(), + stream: StreamDevice::new(&config), + file: FileDevice::new(), + wake_queue: WakeQueue::new(), + } + } +} + +impl DeviceBus for HeadlessDeviceBus { + fn get_device(&mut self, slot: u8) -> Option<&mut dyn Device> { + match slot { + 0x0 => Some(&mut self.system), + 0x1 => Some(&mut self.memory), + 0x2 => Some(&mut self.math ), + 0x3 => Some(&mut self.clock ), + 0x8 => Some(&mut self.stream), + 0x9 => Some(&mut self.file ), + _ => None + } + } + + fn wake(&mut self) -> bool { + for slot in self.wake_queue.iter(self.system.wake_mask) { + if let Some(device) = self.get_device(slot) { + if device.wake() { + self.system.wake_slot = slot; + self.system.asleep = false; + self.wake_queue.wake(slot); + return true; + } + } + } + return false; + } +} diff --git a/src/emulators/mod.rs b/src/emulators/mod.rs new file mode 100644 index 0000000..8f04e4d --- /dev/null +++ b/src/emulators/mod.rs @@ -0,0 +1,21 @@ +mod headless_emulator; +mod graphical_emulator; + +pub use headless_emulator::*; +pub use graphical_emulator::*; + +use crate::*; + + +pub struct EmulatorConfig { + pub dimensions: ScreenDimensions, + pub fullscreen: bool, + pub zoom: NonZeroU32, + pub palette: Option<[Colour; 16]>, + pub show_cursor: bool, + pub decode_stdin: bool, + pub encode_stdout: bool, + pub symbols_path: Option, + pub title: String, + pub icon: Option, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f260042 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,31 @@ +#![feature(bigint_helper_methods)] +#![feature(seek_stream_len)] +#![feature(unchecked_shifts)] + +mod debug; +mod devices; +mod emulators; +mod types; + +pub use debug::*; +pub use devices::*; +pub use emulators::*; +pub use types::*; + +use bedrock_core::*; +use log::*; +use phosphor::*; + +use std::num::NonZeroU32; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +pub const BATCH_SIZE: usize = 1000; +pub const TICK_DURATION: Duration = Duration::from_nanos( 1_000_000_000/256 ); +pub const MIN_FRAME_DURATION: Duration = Duration::from_millis( 10 ); +pub const MAX_FRAME_DURATION: Duration = Duration::from_millis( 500 ); +pub const DEFAULT_SCREEN_SIZE: ScreenDimensions = ScreenDimensions::new(800,600); +pub const DEFAULT_SCREEN_SCALE: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1) }; + +pub type ScreenPosition = geometry::Point; +pub type ScreenDimensions = geometry::Dimensions; diff --git a/src/types/buffered_file.rs b/src/types/buffered_file.rs new file mode 100644 index 0000000..5cdf0ea --- /dev/null +++ b/src/types/buffered_file.rs @@ -0,0 +1,143 @@ +use std::fs::File; +use std::io::{BufReader, BufWriter, Read, Write}; +use std::io::{ErrorKind, Seek, SeekFrom}; + + +pub struct BufferedFile { + file: AccessMode, +} + +impl BufferedFile { + pub fn new(file: File) -> Self { + Self { + file: AccessMode::Read(BufReader::new(file)), + } + } + + pub fn close(&mut self) { + self.file = AccessMode::None; + } + + pub fn read(&mut self) -> u8 { + let mut buffer = [0u8; 1]; + + let read_result = match &mut self.file { + AccessMode::Read(reader) => reader.read_exact(&mut buffer), + AccessMode::Write(writer) => { + let address = writer.stream_position().unwrap(); + let file = std::mem::take(&mut self.file).unwrap(); + let mut reader = BufReader::new(file); + reader.seek(SeekFrom::Start(address)).unwrap(); + let read_result = reader.read_exact(&mut buffer); + self.file = AccessMode::Read(reader); + read_result + } + AccessMode::None => unreachable!(), + }; + + match read_result { + Ok(_) => buffer[0], + Err(error) => match error.kind() { + ErrorKind::UnexpectedEof => 0, + _ => { log::error!("BufferedFile::read: {error:?}"); 0 }, + } + } + } + + pub fn write(&mut self, byte: u8) { + let mut buffer = [byte; 1]; + + let write_result = match &mut self.file { + AccessMode::Write(writer) => writer.write_all(&mut buffer), + AccessMode::Read(reader) => { + let address = reader.stream_position().unwrap(); + let file = std::mem::take(&mut self.file).unwrap(); + let mut writer = BufWriter::new(file); + writer.seek(SeekFrom::Start(address)).unwrap(); + let write_result = writer.write_all(&mut buffer); + self.file = AccessMode::Write(writer); + write_result + } + AccessMode::None => unreachable!(), + }; + + write_result.unwrap(); + } + + pub fn pointer(&mut self) -> u32 { + let position = match &mut self.file { + AccessMode::Read(reader) => reader.stream_position(), + AccessMode::Write(writer) => writer.stream_position(), + AccessMode::None => unreachable!(), + }; + u32::try_from(position.unwrap()).unwrap_or(u32::MAX) + } + + pub fn set_pointer(&mut self, pointer: u32) { + let position = SeekFrom::Start(pointer as u64); + match &mut self.file { + AccessMode::Read(reader) => reader.seek(position).unwrap(), + AccessMode::Write(writer) => writer.seek(position).unwrap(), + AccessMode::None => unreachable!(), + }; + } + + pub fn length(&mut self) -> u32 { + let length = match &mut self.file { + AccessMode::Read(reader) => reader.stream_len(), + AccessMode::Write(writer) => writer.stream_len(), + AccessMode::None => unreachable!(), + }; + u32::try_from(length.unwrap()).unwrap_or(u32::MAX) + } + + pub fn set_length(&mut self, length: u32) { + match &mut self.file { + AccessMode::Read(_) => { + let file = std::mem::take(&mut self.file).unwrap(); + file.set_len(length as u64).unwrap(); + self.file = AccessMode::Read(BufReader::new(file)); + } + AccessMode::Write(_) => { + let file = std::mem::take(&mut self.file).unwrap(); + file.set_len(length as u64).unwrap(); + self.file = AccessMode::Read(BufReader::new(file)); + } + AccessMode::None => unreachable!(), + }; + } + + pub fn flush(&mut self) { + if let AccessMode::Write(writer) = &mut self.file { + let _ = writer.flush(); + } + } +} + +impl Drop for BufferedFile { + fn drop(&mut self) { + self.flush() + } +} + +enum AccessMode { + Read(BufReader), + Write(BufWriter), + None, +} + +impl AccessMode { + pub fn unwrap(self) -> File { + match self { + Self::Read(reader) => reader.into_inner(), + Self::Write(writer) => writer.into_inner().unwrap(), + Self::None => unreachable!(), + } + } +} + +impl Default for AccessMode { + fn default() -> Self { + Self::None + } +} diff --git a/src/types/controller.rs b/src/types/controller.rs new file mode 100644 index 0000000..76b77e3 --- /dev/null +++ b/src/types/controller.rs @@ -0,0 +1,160 @@ +use crate::*; + +pub use gilrs::{Gilrs, GamepadId}; + + +pub struct OwnedGamepad { + tag: usize, + id: Option, + gamepad: Gamepad, +} + +impl OwnedGamepad { + pub fn new(tag: usize) -> Self { + Self { tag, id: None, gamepad: Gamepad::new() } + } + + /// Returns Some if the ID owns this gamepad. + pub fn register(&mut self, new_id: GamepadId) -> Option<&mut Gamepad> { + if let Some(id) = self.id { + match id == new_id { + true => Some(&mut self.gamepad), + false => None, + } + } else { + self.id = Some(new_id); + info!("Registered gamepad {}", self.tag); + Some(&mut self.gamepad) + } + } + + pub fn state(&self) -> u8 { + self.gamepad.state + } + + pub fn reset(&mut self) { + self.gamepad.reset(); + } +} + + +pub struct Gamepad { + pub state: u8, + l_up: bool, + l_down: bool, + l_left: bool, + l_right: bool, + r_up: bool, + r_down: bool, + r_left: bool, + r_right: bool, + d_up: bool, + d_down: bool, + d_left: bool, + d_right: bool, + a: bool, + b: bool, + x: bool, + y: bool, +} + +impl Gamepad { + pub fn new() -> Self { + Self { + state: 0, + l_up: false, + l_down: false, + l_left: false, + l_right: false, + r_up: false, + r_down: false, + r_left: false, + r_right: false, + d_up: false, + d_down: false, + d_left: false, + d_right: false, + a: false, + b: false, + x: false, + y: false, + } + } + + pub fn reset(&mut self) { + self.state = 0; + self.l_up = false; + self.l_down = false; + self.l_left = false; + self.l_right = false; + self.r_up = false; + self.r_down = false; + self.r_left = false; + self.r_right = false; + self.d_up = false; + self.d_down = false; + self.d_left = false; + self.d_right = false; + self.a = false; + self.b = false; + self.x = false; + self.y = false; + } + + // Returns true if the state changed. + pub fn process_event(&mut self, event: &gilrs::Event) -> bool { + macro_rules! schmitt { + ($name_neg:ident, $name_pos:ident, $v:expr) => {{ + if self.$name_neg { if $v > -0.40 { self.$name_neg = false; } } + else { if $v < -0.50 { self.$name_neg = true; } } + if self.$name_pos { if $v < 0.40 { self.$name_pos = false; } } + else { if $v > 0.50 { self.$name_pos = true; } } + }}; + } + + match event.event { + gilrs::EventType::ButtonPressed(button, _) => match button { + gilrs::Button::South => self.a = true, + gilrs::Button::East => self.b = true, + gilrs::Button::West => self.x = true, + gilrs::Button::North => self.y = true, + gilrs::Button::DPadUp => self.d_up = true, + gilrs::Button::DPadDown => self.d_down = true, + gilrs::Button::DPadLeft => self.d_left = true, + gilrs::Button::DPadRight => self.d_right = true, + _ => (), + } + gilrs::EventType::ButtonReleased(button, _) => match button { + gilrs::Button::South => self.a = false, + gilrs::Button::East => self.b = false, + gilrs::Button::West => self.x = false, + gilrs::Button::North => self.y = false, + gilrs::Button::DPadUp => self.d_up = false, + gilrs::Button::DPadDown => self.d_down = false, + gilrs::Button::DPadLeft => self.d_left = false, + gilrs::Button::DPadRight => self.d_right = false, + _ => (), + } + gilrs::EventType::AxisChanged(axis, v, _) => match axis { + gilrs::Axis::LeftStickX => schmitt!(l_left, l_right, v), + gilrs::Axis::LeftStickY => schmitt!(l_down, l_up, v), + gilrs::Axis::RightStickX => schmitt!(r_left, r_right, v), + gilrs::Axis::RightStickY => schmitt!(r_down, r_up, v), + _ => (), + } + _ => (), + } + + let old_state = self.state; + self.state = 0; + if self.l_up | self.r_up | self.d_up { self.state |= 0x80; } + if self.l_down | self.r_down | self.d_down { self.state |= 0x40; } + if self.l_left | self.r_left | self.d_left { self.state |= 0x20; } + if self.l_right | self.r_right | self.d_right { self.state |= 0x10; } + if self.a { self.state |= 0x08; } + if self.b { self.state |= 0x04; } + if self.x { self.state |= 0x02; } + if self.y { self.state |= 0x01; } + old_state != self.state + } +} diff --git a/src/types/directory_listing.rs b/src/types/directory_listing.rs new file mode 100644 index 0000000..f079217 --- /dev/null +++ b/src/types/directory_listing.rs @@ -0,0 +1,120 @@ +use crate::*; + + +pub struct DirectoryListing { + children: Vec, + length: u32, + selected: Option, + child_path_buffer: BedrockPathBuffer, +} + + +impl DirectoryListing { + pub fn from_path(path: &BedrockFilePath) -> Option { + macro_rules! unres { + ($result:expr) => { match $result { Ok(v) => v, Err(_) => continue} }; + } + macro_rules! unopt { + ($option:expr) => { match $option { Some(v) => v, None => continue} }; + } + + #[cfg(target_family = "windows")] { + if path.as_path().components().count() == 0 { + return Some(Self::construct_virtual_root()) + } + } + + let mut children = Vec::new(); + if let Ok(dir_listing) = std::fs::read_dir(path.as_path()) { + for (i, entry_result) in dir_listing.enumerate() { + // Firebreak to prevent emulator from consuming an absurd amount + // of memory when opening too large of a directory. + if i == (u16::MAX as usize) { + break; + } + + let entry = unres!(entry_result); + let entry_path = unopt!(BedrockFilePath::from_path(&entry.path(), path.base())); + if entry_path.is_hidden() { + continue; + } + + children.push(entry_path); + } + } + + children.sort(); + let length = u32::try_from(children.len()).ok()?; + let selected = None; + let child_path_buffer = BedrockPathBuffer::new(); + Some( Self { children, length, selected, child_path_buffer } ) + } + + /// Generate entries for a virtual root directory. + #[cfg(target_family = "windows")] + fn construct_virtual_root() -> Self { + let mut children = Vec::new(); + let base = PathBuf::from(""); + let drive_bits = unsafe { + windows::Win32::Storage::FileSystem::GetLogicalDrives() + }; + for i in 0..26 { + if drive_bits & (0x1 << i) != 0 { + let letter: char = (b'A' + i).into(); + let path = PathBuf::from(format!("{letter}:/")); + if let Some(drive) = BedrockFilePath::from_path(&path, &base) { + children.push(drive); + } + } + } + + let length = children.len() as u32; + let selected = None; + let child_path_buffer = BedrockPathBuffer::new(); + Self { children, length, selected, child_path_buffer } + } + + /// Attempts to return a directory child by index. + pub fn get(&self, index: u32) -> Option<&BedrockFilePath> { + self.children.get(index as usize) + } + + pub fn length(&self) -> u32 { + self.length + } + + /// Returns the index of the selected child, or zero if no child is selected. + pub fn selected(&self) -> u32 { + self.selected.unwrap_or(0) + } + + /// Attempts to select a child by index. + pub fn set_selected(&mut self, index: u32) { + if let Some(child) = self.get(index) { + let buffer = child.as_buffer(); + self.child_path_buffer.populate(buffer); + self.selected = Some(index); + } else { + self.child_path_buffer.clear(); + self.selected = None; + } + } + + pub fn deselect_child(&mut self) { + self.child_path_buffer.clear(); + self.selected = None; + } + + pub fn child_path_buffer(&mut self) -> &mut BedrockPathBuffer { + &mut self.child_path_buffer + } + + pub fn child_type(&self) -> Option { + self.selected.and_then(|s| self.get(s).and_then(|i| i.entry_type())) + } + + pub fn child_path(&self) -> Option { + self.selected.and_then(|s| self.get(s).and_then(|i| Some(i.clone()))) + } +} + diff --git a/src/types/entry_type.rs b/src/types/entry_type.rs new file mode 100644 index 0000000..6a9ac2d --- /dev/null +++ b/src/types/entry_type.rs @@ -0,0 +1,37 @@ +use crate::*; + +use std::cmp::Ordering; + + +pub enum Entry { + File(BufferedFile), + Directory(DirectoryListing), +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum EntryType { + File, + Directory, +} + +impl PartialOrd for EntryType { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (EntryType::Directory, EntryType::Directory) => Some(Ordering::Equal ), + (EntryType::Directory, EntryType::File ) => Some(Ordering::Less ), + (EntryType::File, EntryType::Directory) => Some(Ordering::Greater), + (EntryType::File, EntryType::File ) => Some(Ordering::Equal ), + } + } +} + +impl Ord for EntryType { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (EntryType::Directory, EntryType::Directory) => Ordering::Equal , + (EntryType::Directory, EntryType::File ) => Ordering::Less , + (EntryType::File, EntryType::Directory) => Ordering::Greater, + (EntryType::File, EntryType::File ) => Ordering::Equal , + } + } +} diff --git a/src/types/file_path.rs b/src/types/file_path.rs new file mode 100644 index 0000000..7e6dbe8 --- /dev/null +++ b/src/types/file_path.rs @@ -0,0 +1,288 @@ +use crate::*; + +use std::cmp::Ordering; +use std::ffi::OsString; +use std::path::Component; + + +#[derive(Clone)] +pub struct BedrockFilePath { + /// Sandbox directory + base: PathBuf, + /// Path relative to sandbox directory + relative: PathBuf, + bytes: Vec, + entry_type: Option, +} + +impl BedrockFilePath { + pub fn from_buffer(buffer: [u8; 256], base: &Path) -> Option { + let base = base.to_path_buf(); + let relative = buffer_to_path(buffer)?; + let bytes = path_to_bytes(&relative)?; + let entry_type = get_entry_type(base.join(&relative)); + assert_path_is_safe(&relative, &base)?; + Some(Self { base, relative, bytes, entry_type }) + } + + /// Construct an instance from an absolute path and a prefix of that path. + pub fn from_path(path: &Path, base: &Path) -> Option { + let base = base.to_path_buf(); + let relative = path.strip_prefix(&base).ok()?.to_path_buf(); + let bytes = path_to_bytes(&relative)?; + let entry_type = get_entry_type(base.join(&relative)); + assert_path_is_safe(&relative, &base)?; + Some( Self { base, relative, bytes, entry_type } ) + } + + /// Get the base path used by this path. + pub fn base(&self) -> &Path { + &self.base + } + + /// Get this path as a Bedrock-style path, which can be passed to + /// a Bedrock program. + pub fn as_bytes(&self) -> &[u8] { + &self.bytes + } + + /// Get this path as a byte buffer, from which a CircularPathBuffer + /// can be populated. + pub fn as_buffer(&self) -> [u8; 256] { + let mut buffer: [u8; 256] = [0; 256]; + buffer[..self.bytes.len()].copy_from_slice(&self.bytes); + return buffer; + } + + /// Get this path as an absolute operating-system path, which can + /// be used to open a file or directory. + pub fn as_path(&self) -> PathBuf { + self.base.join(&self.relative) + } + + /// Get the entry type of this path. + pub fn entry_type(&self) -> Option { + self.entry_type + } + + /// Get a path which represents the parent of this path. + pub fn parent(&self) -> Option { + #[cfg(target_family = "unix")] { + Self::from_path(self.as_path().parent()?, &self.base) + } + #[cfg(target_family = "windows")] { + if self.base.components().count() != 0 { + // Sandboxed path, cannot ascend to a virtual root directory. + Self::from_path(self.as_path().parent()?, &self.base) + } else { + // Unsandboxed path, we can ascend to a virtual root directory. + match self.as_path().parent() { + // Ascend to concrete parent directory. + Some(parent) => Self::from_path(parent, &self.base), + // Ascend into a virtual root directory. + None => { + if self.relative.components().count() != 0 { + // Ascend from concrete path to virtual root. + let blank = PathBuf::from(""); + BedrockFilePath::from_path(&blank, &blank) + } else { + // Cannot ascend above the virtual root. + None + } + }, + } + } + } + } + + /// Returns true if the file would be hidden by the default file browser. + pub fn is_hidden(&self) -> bool { + #[cfg(target_family = "unix")] { + if let Some(stem) = self.relative.file_stem() { + if let Some(string) = stem.to_str() { + return string.starts_with('.'); + } + } + } + #[cfg(target_family = "windows")] { + use std::os::windows::fs::MetadataExt; + // See https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + // const FILE_ATTRIBUTE_HIDDEN: u32 = 0x00000002; + // const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: u32 = 0x00002000; + if let Ok(metadata) = std::fs::metadata(self.as_path()) { + return metadata.file_attributes() & 0x2002 != 0; + } + } + return false; + } +} + + +/// Converts the contents of a CircularPathBuffer to a relative path. +fn buffer_to_path(bytes: [u8; 256]) -> Option { + // The buffer must be non-empty and slash-prefixed. + if bytes[0] != ('/' as u8) { + return None; + } + + // Find the index of the first null byte. + let mut null_i = None; + for (i, b) in bytes.iter().enumerate() { + if *b == 0x00 { + null_i = Some(i); + break; + } + } + // Take a slice, excluding the leading slash, up to the trailing null. + let slice = &bytes[1..null_i?]; + + #[cfg(target_family = "unix")] { + use std::os::unix::ffi::OsStringExt; + let vec = Vec::from(slice); + return Some(OsString::from_vec(vec).into()) + } + #[cfg(target_family = "windows")] { + use std::os::windows::ffi::OsStringExt; + let mut string = String::from_utf8_lossy(slice).to_string(); + // Convert drive-current-directory paths to drive-root paths. This is + // needed because the paths C: and C:/ point to separate directories, + // but trailing forward-slashes are optional in Bedrock. + if string.ends_with(':') { + string.push('/'); + } + let utf16: Vec = string.replace(r"/", r"\").encode_utf16().collect(); + return Some(OsString::from_wide(&utf16).into()) + } +} + +/// Convert an operating system path to a Bedrock-style byte path. +/// +/// A byte path contains at most 255 bytes, and is not null-terminated. +fn path_to_bytes(path: &Path) -> Option> { + #[cfg(target_family = "unix")] + let string = path.as_os_str().to_str()?.to_string(); + #[cfg(target_family = "windows")] + let string = path.as_os_str().to_str()?.replace(r"\", r"/"); + + // Remove any trailing forward-slash and add a leading forward-slash. + let mut prefixed_string = String::from("/"); + prefixed_string.push_str(string.trim_end_matches('/')); + let slice = prefixed_string.as_bytes(); + + // Error if bytes does not fit into a CircularPathBuffer. + if slice.len() > 255 { return None; } + + Some(Vec::from(slice)) +} + +/// Returns true if a relative path can be safely attached to a base without +/// breaking out of the sandbox. +fn assert_path_is_safe(relative: &Path, _base: &Path) -> Option<()> { + #[cfg(target_family = "unix")] { + // Error if path contains special components. + for component in relative.components() { + match component { + Component::Normal(_) => continue, + _ => return None, + } + } + } + #[cfg(target_family = "windows")] { + // If the base path is empty, the relative path needs to be able to + // contain the prefix and root element. If the base path is not + // empty, the relative path must not contain these elements else + // they will override the base path when joined. + if _base.components().count() != 0 { + for component in relative.components() { + match component { + Component::Normal(_) => continue, + _ => return None, + } + } + } + } + return Some(()); +} + +fn get_entry_type(absolute: PathBuf) -> Option { + #[cfg(target_family = "windows")] { + // If path is empty, this is a virtual root directory. + if absolute.components().count() == 0 { + return Some(EntryType::Directory) + } + } + let metadata = std::fs::metadata(absolute).ok()?; + if metadata.is_file() { + Some(EntryType::File) + } else if metadata.is_dir() { + Some(EntryType::Directory) + } else { + None + } +} + +impl std::fmt::Debug for BedrockFilePath { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + self.as_path().fmt(f) + } +} + +// --------------------------------------------------------------------------- + +impl PartialEq for BedrockFilePath { + fn eq(&self, other: &Self) -> bool { + self.bytes == other.bytes && self.entry_type == other.entry_type + } +} + +impl Eq for BedrockFilePath {} + +impl PartialOrd for BedrockFilePath { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for BedrockFilePath { + fn cmp(&self, other: &Self) -> Ordering { + match self.entry_type.cmp(&other.entry_type) { + Ordering::Equal => compare_ascii_slices(&self.bytes, &other.bytes), + ordering => ordering, + } + } +} + +/// Compare two ASCII byte-slices in case-agnostic alphabetic order. +fn compare_ascii_slices(left: &[u8], right: &[u8]) -> Ordering { + let l = std::cmp::min(left.len(), right.len()); + let lhs = &left[..l]; + let rhs = &right[..l]; + + for i in 0..l { + let a = remap_ascii(lhs[i]); + let b = remap_ascii(rhs[i]); + match a.cmp(&b) { + Ordering::Equal => (), + non_eq => return non_eq, + } + } + + left.len().cmp(&right.len()) +} + +/// Remap ASCII values so that they sort in case-agnostic alphabetic order: +/// +/// ```text +/// !"#$%&'()*+,-./0123456789:;<=>? +/// @`AaBbCcDdEeFfGgHhIiJjKkLlMmNnOo +/// PpQqRrSsTtUuVvWwXxYyZz[{\|]}^~_ +/// ``` +fn remap_ascii(c: u8) -> u8 { + if 0x40 <= c && c <= 0x5F { + (c - 0x40) * 2 + 0x40 + } else if 0x60 <= c && c <= 0x7F { + (c - 0x60) * 2 + 0x41 + } else { + c + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..1cc90d3 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,19 @@ +mod buffered_file; +mod controller; +mod directory_listing; +mod entry_type; +mod file_path; +mod path_buffer; +mod sprite_buffer; +mod string_buffer; +mod wake_queue; + +pub use buffered_file::*; +pub use controller::*; +pub use directory_listing::*; +pub use entry_type::*; +pub use file_path::*; +pub use path_buffer::*; +pub use sprite_buffer::*; +pub use string_buffer::*; +pub use wake_queue::*; diff --git a/src/types/path_buffer.rs b/src/types/path_buffer.rs new file mode 100644 index 0000000..d6a0861 --- /dev/null +++ b/src/types/path_buffer.rs @@ -0,0 +1,60 @@ +pub struct BedrockPathBuffer { + buffer: [u8; 256], + pointer: u8, +} + +impl BedrockPathBuffer { + pub fn new() -> Self { + Self { buffer: [0; 256] , pointer: 0 } + } + + /// Clear the buffer, returning the previous buffer contents. + pub fn clear(&mut self) -> [u8; 256] { + self.pointer = 0; + std::mem::replace(&mut self.buffer, [0; 256]) + } + + /// Reset the pointer and hot-swap the byte buffer. + pub fn populate(&mut self, buffer: [u8; 256]) { + self.pointer = 0; + self.buffer = buffer; + } + + /// Move internal pointer to the start of the path or file name. + /// + /// If value is non-zero, the pointer will be moved to the byte + /// directly following the final forward-slash. + pub fn set_pointer(&mut self, value: u8) { + self.pointer = 0; + // Set the pointer to the start of the filename if value is truthy. + if value != 0x00 { + for (i, c) in self.buffer.iter().enumerate() { + match c { + b'/' => self.pointer = (i as u8).saturating_add(1), + 0x00 => break, + _ => continue, + } + } + } + } + + /// Read a single byte from the buffer. + pub fn read(&mut self) -> u8 { + let pointer = self.pointer as usize; + self.pointer = self.pointer.wrapping_add(1); + self.buffer[pointer] + } + + /// Write a single byte to the buffer. + /// + /// If a null-byte is written, the buffer will be cleared and returned. + pub fn write(&mut self, byte: u8) -> Option<[u8; 256]> { + if byte == 0x00 { + Some(self.clear()) + } else { + self.buffer[self.pointer as usize] = byte; + self.pointer = self.pointer.saturating_add(1); + None + } + } +} diff --git a/src/types/sprite_buffer.rs b/src/types/sprite_buffer.rs new file mode 100644 index 0000000..74c7b55 --- /dev/null +++ b/src/types/sprite_buffer.rs @@ -0,0 +1,85 @@ +use crate::*; + + +pub struct SpriteBuffer { + pub mem: [u8; 16], + pub pointer: usize, + pub cached: Option<(Sprite, u8)>, +} + +impl SpriteBuffer { + pub fn new() -> Self { + Self { + mem: [0; 16], + pointer: 0, + cached: None, + } + } + + pub fn push_byte(&mut self, byte: u8) { + self.mem[self.pointer] = byte; + self.pointer = (self.pointer + 1) % 16; + self.cached = None; + } + + pub fn read_1bit_sprite(&mut self, draw: u8) -> Sprite { + if let Some((sprite, transform)) = self.cached { + if transform == (draw & 0x77) { + return sprite; + } + } + macro_rules! c { + ($v:ident=mem[$p:ident++]) => { let $v = self.mem[$p % 16]; $p = $p.wrapping_add(1); }; + ($v:ident=mem[--$p:ident]) => { $p = $p.wrapping_sub(1); let $v = self.mem[$p % 16]; }; + } + let mut sprite = [[0; 8]; 8]; + let mut p = match draw & 0x02 != 0 { + true => self.pointer, + false => self.pointer + 8, + }; + match draw & 0x07 { + 0x0 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[y][x] = l>>(7-x) & 1; } } }, + 0x1 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[y][x] = l>>( x) & 1; } } }, + 0x2 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[y][x] = l>>(7-x) & 1; } } }, + 0x3 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[y][x] = l>>( x) & 1; } } }, + 0x4 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[x][y] = l>>(7-x) & 1; } } }, + 0x5 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[x][y] = l>>( x) & 1; } } }, + 0x6 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[x][y] = l>>(7-x) & 1; } } }, + 0x7 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[x][y] = l>>( x) & 1; } } }, + _ => unreachable!(), + } + self.cached = Some((sprite, draw & 0x77)); + return sprite; + } + + pub fn read_2bit_sprite(&mut self, draw: u8) -> Sprite { + if let Some((sprite, transform)) = self.cached { + if transform == (draw & 0x77) { + return sprite; + } + } + macro_rules! c { + ($v:ident=mem[$p:ident++]) => { let $v = self.mem[$p % 16]; $p = $p.wrapping_add(1); }; + ($v:ident=mem[--$p:ident]) => { $p = $p.wrapping_sub(1); let $v = self.mem[$p % 16]; }; + } + let mut sprite = [[0; 8]; 8]; + let mut p = match draw & 0x02 != 0 { + true => self.pointer, + false => self.pointer + 8, + }; + let mut s = p + 8; + match draw & 0x07 { + 0x0 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=7-x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x1 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i= x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x2 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=7-x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x3 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i= x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x4 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=7-x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x5 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i= x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x6 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=7-x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x7 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i= x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } }, + _ => unreachable!(), + } + self.cached = Some((sprite, draw & 0x77)); + return sprite; + } +} diff --git a/src/types/string_buffer.rs b/src/types/string_buffer.rs new file mode 100644 index 0000000..7751d9f --- /dev/null +++ b/src/types/string_buffer.rs @@ -0,0 +1,37 @@ +pub struct StringBuffer { + pub bytes: Vec, + pub pointer: usize, +} + +impl StringBuffer { + pub fn new() -> Self { + Self { + bytes: Vec::new(), + pointer: 0, + } + } + + pub fn from_str(text: &str) -> Self { + let mut new = Self::new(); + new.set_str(text); + new + } + + pub fn set_str(&mut self, text: &str) { + self.bytes = text.bytes().collect(); + self.pointer = 0; + } + + pub fn read(&mut self) -> u8 { + if let Some(byte) = self.bytes.get(self.pointer) { + self.pointer += 1; + *byte + } else { + 0 + } + } + + pub fn restart(&mut self) { + self.pointer = 0; + } +} diff --git a/src/types/wake_queue.rs b/src/types/wake_queue.rs new file mode 100644 index 0000000..41d815b --- /dev/null +++ b/src/types/wake_queue.rs @@ -0,0 +1,51 @@ +pub struct WakeQueue { + queue: Vec, +} + + +impl WakeQueue { + pub fn new() -> Self { + Self { + queue: [0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,0x9,0xA,0xB,0xC,0xD,0xE,0xF].into(), + } + } + + /// Iterate over all masked devices in last-woken order. + pub fn iter(&self, mask: u16) -> WakeIterator { + let mut queue = Vec::new(); + for i in &self.queue { + if mask & (0x8000 >> i) != 0 { + queue.push(*i); + } + } + // Add system device last. + if mask & 0x8000 != 0 { + queue.push(0); + } + WakeIterator { queue, pointer: 0 } + } + + /// Push a device to the back of the queue. + pub fn wake(&mut self, id: u8) { + if let Some(index) = self.queue.iter().position(|e| *e == id) { + self.queue.remove(index); + self.queue.push(id); + } + } +} + + +pub struct WakeIterator { + queue: Vec, + pointer: usize, +} + +impl Iterator for WakeIterator { + type Item = u8; + + fn next(&mut self) -> Option { + let pointer = self.pointer; + self.pointer += 1; + self.queue.get(pointer).copied() + } +} -- cgit v1.2.3-70-g09d2