summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBen Bridle <bridle.benjamin@gmail.com>2025-07-03 15:26:07 +1200
committerBen Bridle <ben@derelict.engineering>2025-07-03 21:24:07 +1200
commit2accc78948fa4a18e37ab0bc405f9b2758acaa3e (patch)
tree2551180ef7fb8f67bfc826de4ad3daf2dd24942e /src
downloadbedrock-pc-2accc78948fa4a18e37ab0bc405f9b2758acaa3e.zip
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/bin/br/config.rs122
-rw-r--r--src/bin/br/load.rs81
-rw-r--r--src/bin/br/main.rs217
-rw-r--r--src/debug.rs126
-rw-r--r--src/devices/clock_device.rs161
-rw-r--r--src/devices/file_device.rs376
-rw-r--r--src/devices/input_device.rs254
-rw-r--r--src/devices/math_device.rs199
-rw-r--r--src/devices/memory_device.rs186
-rw-r--r--src/devices/mod.rs17
-rw-r--r--src/devices/screen_device.rs450
-rw-r--r--src/devices/stream_device.rs239
-rw-r--r--src/devices/system_device.rs119
-rw-r--r--src/emulators/graphical_emulator.rs339
-rw-r--r--src/emulators/headless_emulator.rs105
-rw-r--r--src/emulators/mod.rs21
-rw-r--r--src/lib.rs31
-rw-r--r--src/types/buffered_file.rs143
-rw-r--r--src/types/controller.rs160
-rw-r--r--src/types/directory_listing.rs120
-rw-r--r--src/types/entry_type.rs37
-rw-r--r--src/types/file_path.rs288
-rw-r--r--src/types/mod.rs19
-rw-r--r--src/types/path_buffer.rs60
-rw-r--r--src/types/sprite_buffer.rs85
-rw-r--r--src/types/string_buffer.rs37
-rw-r--r--src/types/wake_queue.rs51
27 files changed, 4043 insertions, 0 deletions
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<ScreenDimensions> {
+ 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<u8> {
+ 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<MetadataColour>) -> Option<Colour> {
+ let c = colour?;
+ Some(Colour::from_rgb(c.red, c.green, c.blue))
+}
+
+
+pub fn parse_small_icon(bytes: Option<Vec<u8>>, bg: Colour, fg: Colour) -> Option<Icon> {
+ 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<Vec<u8>>, bg: Colour, fg: Colour) -> Option<Icon> {
+ 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<u8> {
+ 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<u8>,
+ pub path: Option<PathBuf>,
+}
+
+/// 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<LoadedProgram> {
+ // 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<LoadedProgram, std::io::Error> {
+ // 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<LoadedProgram, std::io::Error> {
+ 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<LoadedProgram, std::io::Error> {
+ let mut bytecode = Vec::<u8>::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 <path>`, where
+ <path> is the path to an assembled Bedrock program.
+
+ To load a Bedrock program from piped input, run `<command> | br`,
+ where <command> 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 <source> <output>`, where <source> is the
+ path of the source file and <output> is the path to write to.
+
+ To assemble and run a Bedrock program without saving to a file,
+ run `br asm <source> | br`, where <source> 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=<dim> (-s) Set the initial window size in the format <width>x<height>
+ --zoom=<scale> (-z) Set the pixel size for the screen (change with F5/F6)
+ --show-cursor (-c) Show the operating system cursor over the window
+ --palette=<pal> Set a debug colour palette in the format <rgb>,... (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<P: AsRef<Path>>(enabled: bool, symbols_path: Option<P>) -> 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<DebugSymbol>
+}
+
+impl DebugSymbols {
+ /// Load debug symbols from a symbols file.
+ pub fn from_path<P: AsRef<Path>>(path: Option<P>) -> 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<String>,
+}
+
+impl DebugSymbol {
+ pub fn from_line(line: &str) -> Option<Self> {
+ 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<Signal> {
+ 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<Instant>,
+ 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<Signal> {
+ 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<u8>,
+ 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<Signal> {
+ 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<u16>,
+ pub y_read: Option<u16>,
+ pub r_read: Option<u16>,
+ pub t_read: Option<u16>,
+ pub prod: Option<(u16, u16)>, // (low, high)
+ pub quot: Option<u16>,
+ pub rem: Option<u16>,
+}
+
+
+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<Signal> {
+ 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<Page>, // 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<Signal> {
+ 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<u8>,
+ pub bg: Vec<u8>,
+ 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<u16>,
+ pub fixed_height: Option<u16>,
+
+ pub palette_write: u16,
+ pub palette: [Colour; 16],
+ pub sprite_colours: u16,
+ pub sprite: SpriteBuffer,
+
+ pub accessed: bool,
+ pub wake: bool,
+}
+
+
+impl HasDimensions<u16> 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<Signal> {
+ 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<Vec<u8>>,
+ /// Bytes received in the current transmission.
+ stdin_queue: VecDeque<u8>,
+ /// Bytes received since stdin end-of-transmission.
+ stdin_excess: VecDeque<u8>,
+
+ 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<u8>,
+
+ 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<Signal> {
+ 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<Signal> {
+ 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<GraphicalDeviceBus>,
+ pub debug: DebugState,
+ pub gilrs: Option<Gilrs>,
+
+ 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<Request>) {
+ 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<Request>) {
+ 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<HeadlessDeviceBus>,
+ 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<PathBuf>,
+ pub title: String,
+ pub icon: Option<Icon>,
+}
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<u16>;
+pub type ScreenDimensions = geometry::Dimensions<u16>;
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<File>),
+ Write(BufWriter<File>),
+ 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<GamepadId>,
+ 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<BedrockFilePath>,
+ length: u32,
+ selected: Option<u32>,
+ child_path_buffer: BedrockPathBuffer,
+}
+
+
+impl DirectoryListing {
+ pub fn from_path(path: &BedrockFilePath) -> Option<Self> {
+ 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<EntryType> {
+ self.selected.and_then(|s| self.get(s).and_then(|i| i.entry_type()))
+ }
+
+ pub fn child_path(&self) -> Option<BedrockFilePath> {
+ 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<Ordering> {
+ 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<u8>,
+ entry_type: Option<EntryType>,
+}
+
+impl BedrockFilePath {
+ pub fn from_buffer(buffer: [u8; 256], base: &Path) -> Option<Self> {
+ 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<Self> {
+ 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<EntryType> {
+ self.entry_type
+ }
+
+ /// Get a path which represents the parent of this path.
+ pub fn parent(&self) -> Option<Self> {
+ #[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<PathBuf> {
+ // 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<u16> = 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<Vec<u8>> {
+ #[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<EntryType> {
+ #[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<Ordering> {
+ 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<u8>,
+ 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<u8>,
+}
+
+
+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<u8>,
+ pointer: usize,
+}
+
+impl Iterator for WakeIterator {
+ type Item = u8;
+
+ fn next(&mut self) -> Option<u8> {
+ let pointer = self.pointer;
+ self.pointer += 1;
+ self.queue.get(pointer).copied()
+ }
+}