diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bin/br.rs | 521 | ||||
-rw-r--r-- | src/bin/br/config.rs | 122 | ||||
-rw-r--r-- | src/bin/br/load.rs | 81 | ||||
-rw-r--r-- | src/bin/br/main.rs | 233 | ||||
-rw-r--r-- | src/debug.rs | 127 | ||||
-rw-r--r-- | src/devices.rs | 20 | ||||
-rw-r--r-- | src/devices/clock_device.rs | 251 | ||||
-rw-r--r-- | src/devices/file_device.rs | 300 | ||||
-rw-r--r-- | src/devices/file_device/operations.rs | 47 | ||||
-rw-r--r-- | src/devices/input_device.rs | 265 | ||||
-rw-r--r-- | src/devices/math_device.rs | 224 | ||||
-rw-r--r-- | src/devices/memory_device.rs | 240 | ||||
-rw-r--r-- | src/devices/mod.rs | 17 | ||||
-rw-r--r-- | src/devices/remote_device.rs | 35 | ||||
-rw-r--r-- | src/devices/screen_device.rs | 296 | ||||
-rw-r--r-- | src/devices/stream_device.rs (renamed from src/devices/local_device.rs) | 198 | ||||
-rw-r--r-- | src/devices/system_device.rs | 160 | ||||
-rw-r--r-- | src/emulators.rs | 32 | ||||
-rw-r--r-- | src/emulators/graphical_emulator.rs | 396 | ||||
-rw-r--r-- | src/emulators/headless_emulator.rs | 164 | ||||
-rw-r--r-- | src/emulators/mod.rs | 24 | ||||
-rw-r--r-- | src/lib.rs | 47 | ||||
-rw-r--r-- | src/metadata.rs | 127 | ||||
-rw-r--r-- | src/types/buffered_file.rs (renamed from src/devices/file_device/buffered_file.rs) | 7 | ||||
-rw-r--r-- | src/types/controller.rs | 166 | ||||
-rw-r--r-- | src/types/directory_listing.rs (renamed from src/devices/file_device/directory_listing.rs) | 2 | ||||
-rw-r--r-- | src/types/entry_type.rs (renamed from src/devices/file_device/entry.rs) | 3 | ||||
-rw-r--r-- | src/types/file_path.rs (renamed from src/devices/file_device/bedrock_file_path.rs) | 3 | ||||
-rw-r--r-- | src/types/mod.rs | 19 | ||||
-rw-r--r-- | src/types/path_buffer.rs (renamed from src/devices/file_device/bedrock_path_buffer.rs) | 0 | ||||
-rw-r--r-- | src/types/sprite_buffer.rs | 85 | ||||
-rw-r--r-- | src/types/string_buffer.rs | 37 | ||||
-rw-r--r-- | src/types/wake_queue.rs | 51 |
33 files changed, 2231 insertions, 2069 deletions
diff --git a/src/bin/br.rs b/src/bin/br.rs deleted file mode 100644 index 431ae39..0000000 --- a/src/bin/br.rs +++ /dev/null @@ -1,521 +0,0 @@ -use bedrock_asm::*; -use bedrock_pc::*; -use phosphor::*; - -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; -use std::process::exit; - - -const NORMAL: &str = "\x1b[0m"; -const BOLD: &str = "\x1b[1m"; -const WHITE: &str = "\x1b[37m"; -const RED: &str = "\x1b[31m"; -const BLUE: &str = "\x1b[34m"; - -static mut VERBOSE: bool = false; - -macro_rules! verbose { - ($($tokens:tt)*) => { if unsafe { VERBOSE } { - eprint!("{BOLD}{BLUE}[INFO]{NORMAL}: "); eprint!($($tokens)*); - eprintln!("{NORMAL}"); - } }; -} -macro_rules! error { - ($($tokens:tt)*) => {{ - eprint!("{BOLD}{RED}[ERROR]{WHITE}: "); eprint!($($tokens)*); - eprintln!("{NORMAL}"); - }}; -} - - -fn main() { - let args = Arguments::from_env_or_exit(); - if args.version { - 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.verbose { - unsafe { VERBOSE = true; } - } - match args.subcommand { - ArgumentsCmd::Run(run) => main_run(run), - ArgumentsCmd::Asm(asm) => main_asm(asm), - } -} - -fn main_run(args: Run) { - let program_path = args.program.as_ref().map(|p| p.as_path()); - let Bytecode { bytes: bytecode, path } = load_bytecode(program_path); - let symbols_path = path.as_ref().map(|p| { - let mut path = p.to_path_buf(); - path.set_extension("br.sym"); - path - }); - - let metadata = parse_metadata(&bytecode); - if metadata.is_none() { - verbose!("Could not read program metadata"); - } - - let mut config = EmulatorConfig { - dimensions: ScreenDimensions::ZERO, - fullscreen: args.fullscreen, - scale: args.scale(), - debug_palette: args.palette(), - show_cursor: args.show_cursor, - initial_transmission: None, - decode_stdin: args.decode_stdin, - encode_stdout: args.encode_stdout, - symbols_path, - }; - let phosphor = Phosphor::new(); - - if phosphor.is_ok() && args.dimensions().is_some() { - verbose!("Starting graphical emulator"); - let mut phosphor = phosphor.unwrap(); - config.dimensions = args.dimensions().unwrap(); - let cursor = match config.show_cursor { - true => Some(CursorIcon::Default), - false => None, - }; - - let mut graphical = GraphicalEmulator::new(&config, args.debug, unsafe {VERBOSE}); - graphical.load_program(&bytecode); - if let EmulatorSignal::Promote = graphical.run() { - let program_name = match &metadata { - Some(metadata) => match &metadata.name { - Some(name) => name.to_string(), - None => String::from("Bedrock"), - } - None => String::from("Bedrock"), - }; - let window = WindowBuilder { - dimensions: Some(graphical.dimensions()), - size_bounds: Some(graphical.size_bounds()), - fullscreen: graphical.fullscreen, - scale: graphical.scale, - title: Some(program_name), - cursor, - icon: None, - program: Box::new(graphical), - }; - - phosphor.create_window(window); - phosphor.run().unwrap(); - } - } else { - verbose!("Starting headless emulator"); - let mut headless = HeadlessEmulator::new(&config, args.debug, unsafe {VERBOSE}); - headless.load_program(&bytecode); - headless.run(args.debug); - }; - - std::process::exit(0); -} - -fn load_bytecode(path: Option<&Path>) -> Bytecode { - // TODO: Etch file location into bytecode. - if let Some(path) = path { - if let Ok(bytecode) = load_bytecode_from_file(path) { - let length = bytecode.bytes.len(); - let path = bytecode.path(); - verbose!("Loaded program from {path:?} ({length} bytes)"); - return bytecode; - } else if let Some(bytecode) = load_bytecode_from_bedrock_path(path) { - let length = bytecode.bytes.len(); - let path = bytecode.path(); - verbose!("Loaded program from {path:?} ({length} bytes)"); - return bytecode; - } else { - error!("Could not read program from {path:?}, exiting"); - exit(1); - } - } else { - verbose!("Reading program from standard input..."); - if let Ok(bytecode) = load_bytecode_from_stdin() { - let length = bytecode.bytes.len(); - verbose!("Loaded program from standard input ({length} bytes)"); - return bytecode; - } else { - error!("Could not read program from standard input, exiting"); - exit(1); - } - } -} - -/// Attempt to load bytecode from a directory in the BEDROCK_PATH environment variable. -fn load_bytecode_from_bedrock_path(path: &Path) -> Option<Bytecode> { - 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); - if !base_path.is_absolute() { continue; } - base_path.push(path); - verbose!("Attempting to load program from {base_path:?}"); - if let Ok(bytecode) = load_bytecode_from_file(&base_path) { - return Some(bytecode); - } - if path.extension().is_some() { continue; } - base_path.set_extension("br"); - verbose!("Attempting to load program from {base_path:?}"); - if let Ok(bytecode) = load_bytecode_from_file(&base_path) { - return Some(bytecode); - } - } - } - return None; -} - -/// Attempt to load bytecode from a file path. -fn load_bytecode_from_file(path: &Path) -> Result<Bytecode, 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_bytecode_from_readable_source(std::fs::File::open(&path)?, Some(&path)) -} - -/// Attempt to load bytecode from standard input. -fn load_bytecode_from_stdin() -> Result<Bytecode, std::io::Error> { - load_bytecode_from_readable_source(std::io::stdin(), None) -} - -/// Attempt to load bytecode from a source that implements std::io::Read. -fn load_bytecode_from_readable_source(source: impl Read, path: Option<&Path>) -> Result<Bytecode, std::io::Error> { - let mut bytes = Vec::<u8>::new(); - source.take(65536).read_to_end(&mut bytes)?; - return Ok(Bytecode { bytes, path: path.map(|p| p.to_path_buf()) }); -} - -struct Bytecode { - bytes: Vec<u8>, - path: Option<PathBuf>, -} - -impl Bytecode { - fn path(&self) -> String { - match &self.path { - Some(path) => path.as_os_str().to_string_lossy().to_string(), - None => String::from("<unknown>"), - } - } -} - - -fn main_asm(args: Asm) { - // ----------------------------------------------------------------------- - // RESOLVE syntactic symbols - let ext = args.ext.unwrap_or(String::from("brc")); - let source_path = args.source.clone().map(|p| { - p.canonicalize().unwrap_or(p) - }); - - let mut resolver = if let Some(path) = &source_path { - match SourceUnit::from_path(&path, &ext) { - Ok(source_unit) => SymbolResolver::from_source_unit(source_unit), - Err(err) => { - match err { - ParseError::InvalidExtension => error!( - "File {path:?} has invalid extension, must be '.{ext}'"), - ParseError::NotFound => error!( - "File {path:?} was not found"), - ParseError::InvalidUtf8 => error!( - "File {path:?} does not contain valid UTF-8 text"), - ParseError::NotReadable => error!( - "File {path:?} is not readable"), - ParseError::IsADirectory => error!( - "File {path:?} is a directory"), - ParseError::Unknown => error!( - "Unknown error while attempting to read from {path:?}") - }; - exit(1); - } - } - } else { - let mut source_code = String::new(); - verbose!("Reading program source from standard input"); - if let Err(err) = std::io::stdin().read_to_string(&mut source_code) { - error!("Could not read from standard input"); - eprintln!("{err:?}"); - exit(1); - } - let path = "<standard input>"; - let source_unit = SourceUnit::from_source_code(source_code, path); - SymbolResolver::from_source_unit(source_unit) - }; - // Load project libraries. - if let Some(path) = &source_path { - if !args.no_libs && !args.no_project_libs { - let project_library = gather_project_libraries(path, &ext); - resolver.add_library_units(project_library); - } - } - // Load environment libraries. - if !args.no_libs && !args.no_env_libs { - for env_library in gather_environment_libraries(&ext) { - resolver.add_library_units(env_library); - } - } - resolver.resolve(); - - // ----------------------------------------------------------------------- - // PRINT information, generate merged source code - if args.tree { - print_source_tree(&resolver); - } - if print_resolver_errors(&resolver) { - std::process::exit(1); - }; - let merged_source = match resolver.get_merged_source_code() { - Ok(merged_source) => merged_source, - Err(ids) => { - print_cyclic_source_units(&ids, &resolver); - std::process::exit(1); - }, - }; - if args.resolve && !args.check { - write_bytes_and_exit(merged_source.as_bytes(), args.output.as_ref()); - } - - // ----------------------------------------------------------------------- - // PARSE semantic tokens from merged source code - let path = Some("<merged source>"); - let mut semantic_tokens = generate_semantic_tokens(&merged_source, path); - if print_semantic_errors(&semantic_tokens, &merged_source) { - std::process::exit(1); - }; - - // ----------------------------------------------------------------------- - // GENERATE symbols file and bytecode - let bytecode = generate_bytecode(&mut semantic_tokens); - - if args.symbols && !args.check { - if let Some(path) = &args.output { - let mut symbols_path = path.to_path_buf(); - symbols_path.set_extension("br.sym"); - let symbols = generate_symbols_file(&semantic_tokens); - if let Err(err) = std::fs::write(&symbols_path, symbols) { - verbose!("Could not write to symbols path {symbols_path:?}"); - eprintln!("{err:?}"); - } else { - verbose!("Saved debug symbols to {symbols_path:?}"); - } - } - } - - let length = bytecode.len(); - let percentage = (length as f32 / 65536.0 * 100.0).round() as u16; - verbose!("Assembled program in {length} bytes ({percentage}% of maximum)"); - - if !args.check { - write_bytes_and_exit(&bytecode, args.output.as_ref()); - } -} - -fn write_bytes_and_exit<P: AsRef<Path>>(bytes: &[u8], path: Option<&P>) -> ! { - if let Some(path) = path { - if let Err(err) = std::fs::write(path, bytes) { - error!("Could not write to path {:?}", path.as_ref()); - eprintln!("{err:?}"); - exit(1); - } - } else { - if let Err(err) = std::io::stdout().write_all(bytes) { - error!("Could not write to standard output"); - eprintln!("{err:?}"); - exit(1); - } - } - exit(0); -} - - -xflags::xflags! { - /// Integrated Bedrock assembler and emulator. - /// - /// The arguments and options shown are for running a Bedrock program. - /// To see the documentation for the assembler, run `br asm --help` - /// - /// Usage: - /// To load a Bedrock program from a file, run `br [path]`, where - /// [path] is the path of 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 which 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 which 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. - cmd arguments { - /// Print additional debug information - optional -v, --verbose - /// Print the assembler version and exit - optional --version - - /// Run a Bedrock program (this is the default command) - default cmd run { - /// Path to a Bedrock program to run - optional program: PathBuf - - /// Show debug information while the program is running - optional -d, --debug - /// Start the program in fullscreen mode (toggle with F11) - optional -f, --fullscreen - /// Set the initial window size in the format <width>x<height> - optional -s, --screen size: ParsedDimensions - /// Set the pixel size for the screen (change with F5/F6) - optional -z, --zoom factor: u32 - /// Set a debug colour palette in the format <rgb>,... (toggle with F2) - optional --palette colours: ParsedPalette - /// Show the operating system cursor over the window - optional --show-cursor - /// Decode standard input - optional -i, --decode-stdin - /// Encode standard output - optional -o, --encode-stdout - } - - /// Assemble a Bedrock program from source code - cmd asm { - /// Bedrock source code file to assemble. - optional source: PathBuf - /// Destination path for assembler output. - optional output: PathBuf - /// File extension to identify source files. - optional ext: String - - /// Don't include libraries or resolve references. - optional --no-libs - /// Don't include project libraries - optional --no-project-libs - /// Don't include environment libraries. - optional --no-env-libs - - /// Show the resolved source file heirarchy - optional --tree - /// Assemble the program without saving any output - optional --check - /// Only return resolved source code. - optional --resolve - /// Generate debug symbols with file extension '.br.sym' - optional --symbols - } - } -} - - -impl Run { - pub fn dimensions(&self) -> Option<geometry::Dimensions<u16>> { - let size = match &self.screen { - Some(parsed) => geometry::Dimensions::new(parsed.width, parsed.height), - None => DEFAULT_SCREEN_SIZE / (self.scale() as u16), - }; - match size.is_zero() { - true => None, - false => Some(size), - } - } - - pub fn scale(&self) -> u32 { - std::cmp::max(1, self.zoom.unwrap_or(1)) - } - - pub fn palette(&self) -> Option<[Colour; 16]> { - self.palette.as_ref().map(|p| p.palette) - } -} - - -#[derive(Debug)] -struct ParsedDimensions { width: u16, height: u16 } -impl std::str::FromStr for ParsedDimensions { - type Err = ParsedDimensionsError; - fn from_str(s: &str) -> Result<Self, Self::Err> { - if s == "none" { - Ok( Self { width: 0, height: 0 } ) - } else { - let (w_str, h_str) = s.split_once('x').ok_or(ParsedDimensionsError)?; - Ok( Self { - width: u16::from_str(w_str).or(Err(ParsedDimensionsError))?, - height: u16::from_str(h_str).or(Err(ParsedDimensionsError))?, - } ) - } - } -} - -pub struct ParsedDimensionsError; -impl std::fmt::Display for ParsedDimensionsError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - f.write_str("try giving a value like 800x600") - } -} - - -#[derive(Debug)] -pub struct ParsedPalette { palette: [Colour; 16] } -impl std::str::FromStr for ParsedPalette { - type Err = ParsedPaletteError; - fn from_str(s: &str) -> Result<Self, Self::Err> { - fn decode_ascii_hex_digit(ascii: u8) -> Result<u8, ParsedPaletteError> { - match ascii { - b'0'..=b'9' => Ok(ascii - b'0'), - b'a'..=b'f' => Ok(ascii - b'a' + 10), - b'A'..=b'F' => Ok(ascii - b'A' + 10), - _ => { Err(ParsedPaletteError)} - } - } - let mut colours = Vec::new(); - for token in s.split(',') { - let mut bytes = token.bytes(); - if bytes.len() != 3 { return Err(ParsedPaletteError); } - 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())?; - colours.push(Colour::from_rgb(r*17, g*17, b*17)); - } - let c = &colours; - let palette = match colours.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 Err(ParsedPaletteError), - }; - Ok( Self { palette }) - } -} - -pub struct ParsedPaletteError; -impl std::fmt::Display for ParsedPaletteError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - f.write_str("try giving a value like 000,fff,880,808") - } -} 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..da11a18 --- /dev/null +++ b/src/bin/br/main.rs @@ -0,0 +1,233 @@ +#![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.named("trust-files"); + 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 trust_files = args.get("trust-files").as_bool(); + + // ----------------------------------------------------------------------- + + let LoadedProgram { bytecode, path } = load_program(source.as_ref()); + let mut title = String::from("Bedrock program"); + let mut icon = None; + + let metadata = Metadata::from(&bytecode); + + if let Some(ref metadata) = metadata { + 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 name = metadata.and_then(|m| m.name()).and_then(|n| match n.split_once('/') { + Some((name, _)) => Some(name.to_string()), + None => Some(n), + }); + let identifier = name.as_ref().and_then( + |n| Some(n.to_lowercase().chars().filter_map( + |c| c.is_alphanumeric().then_some(c) + ).collect()) + ); + + let config = EmulatorConfig { + dimensions, fullscreen, zoom, palette, show_cursor, + decode_stdin, encode_stdout, trust_files, + symbols_path, name, identifier, title, icon, + }; + + match Phosphor::new() { + Ok(phosphor) => match mode { + Mode::Dynamic => { + info!("Starting graphical emulator (hidden)"); + 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, true); + } + Mode::Headless => { + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + } + Err(err) => match mode { + Mode::Dynamic => { + eprintln!("EventLoopError: {err:?}"); + 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 => { + eprintln!("EventLoopError: {err:?}"); + 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. + --trust-files Give the program unrestricted access to the file system. + --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 index 6270948..a0746e7 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,86 +1,85 @@ -use bedrock_core::*; +use crate::*; -use std::path::Path; -use std::time::Instant; - - -const NORMAL: &str = "\x1b[0m"; -const BOLD: &str = "\x1b[1m"; -const DIM: &str = "\x1b[2m"; -const YELLOW: &str = "\x1b[33m"; -const BLUE: &str = "\x1b[34m"; +use inked::{ink, InkedString}; pub struct DebugState { pub enabled: bool, - pub verbose: bool, last_cycle: usize, - last_mark: Instant, + mark: Instant, symbols: DebugSymbols, } impl DebugState { - pub fn new<P: AsRef<Path>>(enabled: bool, verbose: bool, symbols_path: Option<P>) -> Self { + pub fn new<P: AsRef<Path>>(enabled: bool, symbols_path: Option<P>) -> Self { Self { enabled, - verbose, last_cycle: 0, - last_mark: Instant::now(), - symbols: DebugSymbols::from_path_opt(symbols_path), - } - } - - pub fn info(&self, string: &str) { - if self.verbose { - eprintln!("{BOLD}{BLUE}[INFO]{NORMAL}: {string}{NORMAL}"); + mark: Instant::now(), + symbols: DebugSymbols::from_path(symbols_path), } } - pub fn debug_summary(&mut self, core: &BedrockCore) { + pub fn debug_full(&mut self, core: &BedrockCore) { if self.enabled { let prev_pc = core.mem.pc.wrapping_sub(1); - eprintln!("\n PC: 0x{:04x} Cycles: {} (+{} in {:.2?})", - prev_pc, core.cycle, - core.cycle.saturating_sub(self.last_cycle), - self.last_mark.elapsed(), - ); - eprint!("WST: "); - debug_stack(&core.wst, 0x10); - eprint!("RST: "); - debug_stack(&core.rst, 0x10); - // Print information about the closest symbol. + 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; - eprint!("SYM: {BLUE}@{name}{NORMAL} 0x{address:04x}"); + 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 { - eprint!(" {DIM}{location}{NORMAL}"); + string.push(ink!(" ")); + string.push(ink!("{location}").dim()); } - eprintln!(); + string.eprintln(); } + 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.last_mark = Instant::now(); + self.mark = Instant::now(); } } -fn debug_stack(stack: &Stack, len: usize) { - for i in 0..len { - if i == stack.sp as usize { eprint!("{YELLOW}"); } - eprint!("{:02x} ", stack.mem[i]); +fn debug_stack(stack: &Stack) { + for i in 0..(stack.sp as usize) { + eprint!("{:02X} ", stack.mem[i]); } - eprintln!("{NORMAL}"); + eprintln!(); } -struct DebugSymbols { - symbols: Vec<DebugSymbol> + +pub struct DebugSymbols { + pub symbols: Vec<DebugSymbol> } impl DebugSymbols { /// Load debug symbols from a symbols file. - pub fn from_path_opt<P: AsRef<Path>>(path: Option<P>) -> Self { + 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) { @@ -95,36 +94,34 @@ impl DebugSymbols { 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; } - let symbol = 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)?)?, - }; - Some(&symbol) + 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)?), + } } } -struct DebugSymbol { - address: u16, - name: String, - location: Option<String>, + +pub struct DebugSymbol { + pub address: u16, + pub name: String, + pub location: Option<String>, } impl DebugSymbol { pub fn from_line(line: &str) -> Option<Self> { - if let Some((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 } ) - } + 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 { - None + let name = line.to_string(); + Some( DebugSymbol { address, name, location: None } ) } } } diff --git a/src/devices.rs b/src/devices.rs deleted file mode 100644 index 2221152..0000000 --- a/src/devices.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod system_device; -mod memory_device; -mod math_device; -mod clock_device; -mod input_device; -mod screen_device; -mod local_device; -mod remote_device; -mod file_device; - -pub use system_device::SystemDevice; -pub use memory_device::MemoryDevice; -pub use math_device::MathDevice; -pub use clock_device::ClockDevice; -pub use input_device::InputDevice; -pub use screen_device::ScreenDevice; -pub use local_device::LocalDevice; -pub use remote_device::RemoteDevice; -pub use file_device::FileDevice; - diff --git a/src/devices/clock_device.rs b/src/devices/clock_device.rs index 7cc877e..d06a92c 100644 --- a/src/devices/clock_device.rs +++ b/src/devices/clock_device.rs @@ -1,113 +1,18 @@ -use bedrock_core::*; +use crate::*; use chrono::prelude::*; -use std::time::{Duration, Instant}; - - -macro_rules! fn_read_timer { - ($fn_name:ident($read:ident, $end:ident)) => { - pub fn $fn_name(&mut self) { - let uptime = self.uptime(); - if self.$end > uptime { - self.$read = (self.$end.saturating_sub(uptime)) as u16; - } else { - if self.$end > 0 { - self.$end = 0; - self.wake = true; - } - self.$read = 0; - } - } - }; -} - -macro_rules! fn_set_timer { - ($fn_name:ident($write:ident, $end:ident)) => { - pub fn $fn_name(&mut self) { - let uptime = self.uptime(); - if self.$write > 0 { - self.$end = uptime.saturating_add(self.$write as u32); - } else { - self.$end = 0; - } - } - }; -} pub struct ClockDevice { - pub wake: bool, - - pub start: Instant, + pub epoch: Instant, pub uptime_read: u16, - pub t1_end: u32, - pub t2_end: u32, - pub t3_end: u32, - pub t4_end: u32, - pub t1_read: u16, - pub t2_read: u16, - pub t3_read: u16, - pub t4_read: u16, - pub t1_write: u16, - pub t2_write: u16, - pub t3_write: u16, - pub t4_write: u16, + pub t1: CountdownTimer, + pub t2: CountdownTimer, + pub t3: CountdownTimer, + pub t4: CountdownTimer, } -impl ClockDevice { - pub fn new() -> Self { - Self { - start: Instant::now(), - uptime_read: 0, - wake: false, - - t1_end: 0, - t2_end: 0, - t3_end: 0, - t4_end: 0, - t1_read: 0, - t2_read: 0, - t3_read: 0, - t4_read: 0, - t1_write: 0, - t2_write: 0, - t3_write: 0, - t4_write: 0, - } - } - - pub fn uptime(&self) -> u32 { - (self.start.elapsed().as_millis() / 4) as u32 - } - - pub fn read_uptime(&mut self) { - self.uptime_read = self.uptime() as u16; - } - - fn_read_timer!{ read_t1(t1_read, t1_end) } - fn_read_timer!{ read_t2(t2_read, t2_end) } - fn_read_timer!{ read_t3(t3_read, t3_end) } - fn_read_timer!{ read_t4(t4_read, t4_end) } - - fn_set_timer!{ set_t1(t1_write, t1_end) } - fn_set_timer!{ set_t2(t2_write, t2_end) } - fn_set_timer!{ set_t3(t3_write, t3_end) } - fn_set_timer!{ set_t4(t4_write, t4_end) } - - - pub fn time_remaining(&mut self) -> Option<Duration> { - use std::cmp::max; - let uptime = self.uptime(); - - let end = max(self.t1_end, max(self.t2_end, max(self.t3_end, self.t4_end))); - let remaining = end.saturating_sub(uptime); - match remaining > 0 { - true => Some(Duration::from_millis(remaining as u64) * 4), - false => None, - } - } -} impl Device for ClockDevice { fn read(&mut self, port: u8) -> u8 { @@ -118,16 +23,17 @@ impl Device for ClockDevice { 0x3 => Local::now().hour() as u8, 0x4 => Local::now().minute() as u8, 0x5 => Local::now().second() as u8, - 0x6 => { self.read_uptime(); read_h!(self.uptime_read) }, - 0x7 => read_l!(self.uptime_read), - 0x8 => { self.read_t1(); read_h!(self.t1_read) }, - 0x9 => read_l!(self.t1_read), - 0xa => { self.read_t2(); read_h!(self.t2_read) }, - 0xb => read_l!(self.t2_read), - 0xc => { self.read_t3(); read_h!(self.t3_read) }, - 0xd => read_l!(self.t3_read), - 0xe => { self.read_t4(); read_h!(self.t4_read) }, - 0xf => read_l!(self.t4_read), + 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!(), } } @@ -142,33 +48,114 @@ impl Device for ClockDevice { 0x5 => (), 0x6 => (), 0x7 => (), - 0x8 => write_h!(self.t1_write, value), - 0x9 => { write_l!(self.t1_write, value); self.set_t1() }, - 0xa => write_h!(self.t2_write, value), - 0xb => { write_l!(self.t2_write, value); self.set_t2() }, - 0xc => write_h!(self.t3_write, value), - 0xd => { write_l!(self.t3_write, value); self.set_t3() }, - 0xe => write_h!(self.t4_write, value), - 0xf => { write_l!(self.t4_write, value); self.set_t4() }, + 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 uptime = self.uptime(); - macro_rules! check_timer { - ($end:ident) => { - if self.$end > 0 && self.$end <= uptime { - self.$end = 0; - self.wake = true; - } - }; + 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; } - check_timer!(t1_end); - check_timer!(t2_end); - check_timer!(t3_end); - check_timer!(t4_end); - return std::mem::take(&mut self.wake); } } diff --git a/src/devices/file_device.rs b/src/devices/file_device.rs index 61966b1..83f0a56 100644 --- a/src/devices/file_device.rs +++ b/src/devices/file_device.rs @@ -1,20 +1,4 @@ -mod bedrock_file_path; -mod bedrock_path_buffer; -mod buffered_file; -mod directory_listing; -mod entry; -mod operations; - -use buffered_file::BufferedFile; -use bedrock_file_path::BedrockFilePath; -use bedrock_path_buffer::BedrockPathBuffer; -use directory_listing::DirectoryListing; -use entry::{Entry, EntryType}; -use operations::{create_file, move_entry, delete_entry}; - -use bedrock_core::*; - -use std::path::{Component, Path, PathBuf}; +use crate::*; pub struct FileDevice { @@ -25,13 +9,14 @@ pub struct FileDevice { pub action_buffer: BedrockPathBuffer, pub path_buffer: BedrockPathBuffer, - pub entry: Option<(Entry, BedrockFilePath)>, - pub cached_dir: Option<(Entry, BedrockFilePath)>, + pub entry: Option<(Entry, BedrockFilePath, Instant)>, + pub cached_dir: Option<(Entry, BedrockFilePath, Instant)>, - pub success: bool, + pub error: bool, pub pointer_write: u32, pub length_write: u32, + pub enable: bool, pub enable_read: bool, pub enable_write: bool, pub enable_create: bool, @@ -39,21 +24,95 @@ pub struct FileDevice { pub enable_delete: bool, } + +impl Device for FileDevice { + fn read(&mut self, port: u8) -> u8 { + if !self.enable { return 0x00; } + match port { + 0x0 => read_b!(self.entry.is_some()), + 0x1 => read_b!(std::mem::take(&mut self.error)), + 0x2 => self.read_byte(), + 0x3 => self.read_byte(), + 0x4 => self.path_buffer.read(), + 0x5 => read_b!(self.entry_type()), + 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> { + if !self.enable { return None; } + match port { + 0x0 => self.write_to_entry_port(value), + 0x1 => self.write_to_action_port(value), + 0x2 => self.write_byte(value), + 0x3 => self.write_byte(value), + 0x4 => self.path_buffer.set_pointer(value), + 0x5 => self.ascend_to_parent(), + 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 { + pub fn new(config: &EmulatorConfig) -> Self { #[cfg(target_family = "unix")] let default_base: PathBuf = PathBuf::from("/"); #[cfg(target_family = "windows")] let default_base: PathBuf = PathBuf::from(""); + let current_dir = match std::env::current_dir() { + Ok(dir) => PathBuf::from(dir), + Err(_) => PathBuf::from(""), + }; + + let (enable, base_path, default_path) = if config.trust_files { + (true, default_base, current_dir) + } else if let Some(config_dir) = dirs_next::config_dir() { + let bedrock_dir = config_dir.join("bedrock"); + let identifier = config.identifier.clone().unwrap_or("default".to_string()); + let sandbox_dir = bedrock_dir.join(identifier); + vagabond::make_directory(&sandbox_dir).unwrap(); + (true, sandbox_dir.clone(), sandbox_dir) + } else { + error!("Could not determine sandbox path for file device"); + (false, default_base, current_dir) + }; + // 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(""), - }, + base_path, + default_path, entry_buffer: BedrockPathBuffer::new(), action_buffer: BedrockPathBuffer::new(), @@ -62,10 +121,11 @@ impl FileDevice { entry: None, cached_dir: None, - success: false, + error: false, pointer_write: 0, length_write: 0, + enable, enable_read: true, enable_write: true, enable_create: true, @@ -74,6 +134,12 @@ impl FileDevice { } } + pub fn check_success(&mut self, success: bool) { + if !success { + self.error = true; + } + } + /// Safely close the current entry, cleaning up entry variables. pub fn close(&mut self) { self.entry_buffer.clear(); @@ -81,10 +147,10 @@ impl FileDevice { self.path_buffer.clear(); self.flush(); - if let Some((Entry::Directory(mut dir), path)) = std::mem::take(&mut self.entry) { + if let Some((Entry::Directory(mut dir), path, time)) = 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)); + self.cached_dir = Some((Entry::Directory(dir), path, time)); } } @@ -100,17 +166,17 @@ impl FileDevice { if let Ok(file) = open_result { self.close(); self.path_buffer.populate(path.as_buffer()); - self.entry = Some((Entry::File(BufferedFile::new(file)), path)); + self.entry = Some((Entry::File(BufferedFile::new(file)), path, Instant::now())); 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 { + // Attempt to use the cached directory if not too old. + if let Some((dir, cached_path, time)) = std::mem::take(&mut self.cached_dir) { + if cached_path == path && time.elapsed() < Duration::from_secs(1) { self.close(); self.path_buffer.populate(cached_path.as_buffer()); - self.entry = Some((dir, cached_path)); + self.entry = Some((dir, cached_path, time)); return Ok(()); } } @@ -118,7 +184,7 @@ impl FileDevice { if let Some(listing) = DirectoryListing::from_path(&path) { self.close(); self.path_buffer.populate(path.as_buffer()); - self.entry = Some((Entry::Directory(listing), path)); + self.entry = Some((Entry::Directory(listing), path, Instant::now())); return Ok(()); }; } @@ -132,10 +198,16 @@ impl FileDevice { 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, - }; + // Attempt to open file if buffer was not empty. + if buffer[0] != 0 { + let success = match BedrockFilePath::from_buffer(buffer, &self.base_path) { + Some(path) => self.open(path).is_ok(), + None => false, + }; + self.check_success(success); + } else { + self.check_success(true); + } } } @@ -144,22 +216,23 @@ impl FileDevice { 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 let Some((_, source, _)) = &self.entry { if destination_blank { if self.enable_delete { - self.success = delete_entry(&source.as_path()); + self.check_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()); + self.check_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.check_success(create_file(&dest.as_path())); } + } else { + self.check_success(false); } self.close(); } @@ -167,35 +240,38 @@ impl FileDevice { /// 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, + if let Some((_, path, _)) = &self.entry { + let success = match path.parent() { + Some(parent) => self.open(parent).is_ok(), + None => false, }; + self.check_success(success); } else { - match BedrockFilePath::from_path(&self.default_path, &self.base_path) { - Some(default) => self.success = self.open(default).is_ok(), - None => self.success = false, + let success = match BedrockFilePath::from_path(&self.default_path, &self.base_path) { + Some(default) => self.open(default).is_ok(), + None => false, }; + self.check_success(success); } } /// 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, + if let Some((Entry::Directory(dir), _, _)) = &self.entry { + let success = match dir.child_path() { + Some(child) => self.open(child).is_ok(), + None => false, }; + self.check_success(success); } else { - self.success = false; + self.check_success(false); } } /// Return true if the current entry is a directory. pub fn entry_type(&self) -> bool { match self.entry { - Some((Entry::Directory(_), _)) => true, + Some((Entry::Directory(_), _, _)) => true, _ => false, } } @@ -203,13 +279,13 @@ impl FileDevice { /// 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(), + 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 { + if let Some((Entry::Directory(dir), _, _)) = &mut self.entry { dir.child_path_buffer().set_pointer(byte); } } @@ -217,7 +293,7 @@ impl FileDevice { /// 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((Entry::Directory(dir), _, _)) => match dir.child_type() { Some(EntryType::Directory) => true, _ => false, } @@ -228,7 +304,7 @@ impl FileDevice { /// Read a byte from the current file. pub fn read_byte(&mut self) -> u8 { match &mut self.entry { - Some((Entry::File(file), _)) => file.read(), + Some((Entry::File(file), _, _)) => file.read(), _ => 0, } } @@ -236,102 +312,100 @@ impl FileDevice { /// 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), + 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(), + 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), + 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(), + 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), + 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 { + if let Some((Entry::File(buffered_file), _, _)) = &mut self.entry { let _ = buffered_file; } } } + impl Drop for FileDevice { fn drop(&mut self) { self.flush(); } } -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!(), + +/// 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() } +} - 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; +/// 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() +} - fn wake(&mut self) -> bool { - false +/// 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/file_device/operations.rs b/src/devices/file_device/operations.rs deleted file mode 100644 index 3a3f81b..0000000 --- a/src/devices/file_device/operations.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::io::ErrorKind; -use std::path::Path; - - -/// 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 { - match std::fs::remove_file(source) { - Ok(_) => true, - Err(e) => match e.kind() { - ErrorKind::NotFound => true, - ErrorKind::IsADirectory => match std::fs::remove_dir_all(source) { - Ok(_) => true, - Err(e) => match e.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 index 9b7038c..3ebeb4c 100644 --- a/src/devices/input_device.rs +++ b/src/devices/input_device.rs @@ -1,84 +1,149 @@ use crate::*; -use bedrock_core::*; -use phosphor::*; use std::collections::VecDeque; -macro_rules! fn_on_scroll { - ($fn_name:ident($value:ident, $delta:ident)) => { - pub fn $fn_name(&mut self, delta: f32) { - self.$delta += delta; - while self.$delta >= 1.0 { - self.$value = self.$value.saturating_add(1); - self.$delta -= 1.0; - self.wake = true; - } - while self.$delta <= -1.0 { - self.$value = self.$value.saturating_sub(1); - self.$delta += 1.0; - self.wake = true; - } - } - }; -} - pub struct InputDevice { - pub wake: bool, - pub accessed: bool, - - pub pointer_active: bool, - pub pointer_buttons: u8, - pub position: ScreenPosition, + 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 h_scroll: i8, - pub v_scroll: i8, - pub h_scroll_delta: f32, - pub v_scroll_delta: f32, - - pub keyboard_active: bool, - pub characters: VecDeque<u8>, 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!(), + } + } - pub gamepad_1: u8, - pub gamepad_2: u8, - pub gamepad_3: u8, - pub gamepad_4: u8, + 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 { - wake: false, - accessed: false, - - pointer_active: false, - pointer_buttons: 0, - - position: ScreenPosition::ZERO, + 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, - h_scroll: 0, - v_scroll: 0, - h_scroll_delta: 0.0, - v_scroll_delta: 0.0, - - keyboard_active: true, - characters: VecDeque::new(), - modifiers: 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), - gamepad_1: 0, - gamepad_2: 0, - gamepad_3: 0, - gamepad_4: 0, + accessed: false, + wake: false, } } + #[cfg(feature = "gamepad")] + 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; @@ -90,12 +155,13 @@ impl InputDevice { } pub fn on_cursor_move(&mut self, position: Position) { - let screen_position = ScreenPosition { + self.pointer_active = true; + let cursor_position = ScreenPosition { x: position.x as i16 as u16, y: position.y as i16 as u16, }; - if self.position != screen_position { - self.position = screen_position; + if self.cursor != cursor_position { + self.cursor = cursor_position; self.wake = true; } } @@ -103,8 +169,8 @@ impl InputDevice { pub fn on_mouse_button(&mut self, button: MouseButton, action: Action) { let mask = match button { MouseButton::Left => 0x80, - MouseButton::Middle => 0x40, - MouseButton::Right => 0x20, + MouseButton::Right => 0x40, + MouseButton::Middle => 0x20, _ => return, }; let pointer_buttons = match action { @@ -117,15 +183,26 @@ impl InputDevice { } } - fn_on_scroll!(on_horizontal_scroll(h_scroll, h_scroll_delta)); - fn_on_scroll!(on_vertical_scroll(v_scroll, v_scroll_delta)); + pub fn on_horizontal_scroll(&mut self, delta: f32) { + self.h_scroll += delta; + self.h_scroll = self.h_scroll.clamp(-32768.0, 32767.0); + if self.h_scroll.abs() >= 1.0 { self.wake = true; } + } - pub fn read_horizontal_scroll(&mut self) -> u8 { - std::mem::take(&mut self.h_scroll) as u8 + 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); + if self.v_scroll.abs() >= 1.0 { self.wake = true; } } - pub fn read_vertical_scroll(&mut self) -> u8 { - std::mem::take(&mut self.v_scroll) as u8 + 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) { @@ -178,57 +255,3 @@ impl InputDevice { } } } - -impl Device for InputDevice { - fn read(&mut self, port: u8) -> u8 { - self.accessed = true; - match port { - 0x0 => read_b!(self.pointer_active), - 0x1 => self.pointer_buttons, - 0x2 => self.read_horizontal_scroll(), - 0x3 => self.read_vertical_scroll(), - 0x4 => { self.x_read = self.position.x as u16; read_h!(self.x_read) }, - 0x5 => read_l!(self.position.x), - 0x6 => { self.y_read = self.position.y as u16; read_h!(self.y_read) }, - 0x7 => read_l!(self.position.y), - 0x8 => read_b!(self.keyboard_active), - 0x9 => self.characters.pop_front().unwrap_or(0), - 0xa => self.navigation, - 0xb => self.modifiers, - 0xc => self.gamepad_1, - 0xd => self.gamepad_2, - 0xe => self.gamepad_3, - 0xf => self.gamepad_4, - _ => unreachable!(), - } - } - - fn write(&mut self, port: u8, _value: u8) -> Option<Signal> { - self.accessed = true; - match port { - 0x0 => (), - 0x1 => (), - 0x2 => (), - 0x3 => (), - 0x4 => (), - 0x5 => (), - 0x6 => (), - 0x7 => (), - 0x8 => (), - 0x9 => self.characters.clear(), - 0xa => (), - 0xb => (), - 0xc => (), - 0xd => (), - 0xe => (), - 0xf => (), - _ => unreachable!(), - }; - return None; - } - - fn wake(&mut self) -> bool { - self.accessed = true; - std::mem::take(&mut self.wake) - } -} diff --git a/src/devices/math_device.rs b/src/devices/math_device.rs index 015545e..e7043b9 100644 --- a/src/devices/math_device.rs +++ b/src/devices/math_device.rs @@ -1,59 +1,168 @@ -use bedrock_core::*; +use crate::*; +const ANGLE_SCALE: f64 = 10430.378350470453; // 65536 / 2Ï€ -pub struct MathDevice { - pub op1: u16, - pub op2: u16, - pub sqrt: Option<u16>, - pub atan: Option<u16>, +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 { - op1: 0, - op2: 0, - - sqrt: None, - atan: None, + 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(&mut self) { - self.sqrt = None; - self.atan = 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; + self.rem = None; } - pub fn atan(&mut self) -> u16 { - match self.atan { - Some(atan) => atan, + pub fn x(&mut self) -> u16 { + match self.x_read { + Some(x) => x, None => { - let x = self.op1 as i16 as f64; - let y = self.op2 as i16 as f64; - const SCALE: f64 = 10430.378350470453; // PI * 32768 - self.atan = Some((f64::atan2(x, y) * SCALE) as i16 as u16); - self.atan.unwrap() + 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 sqrt(&mut self) -> u16 { - match self.sqrt { - Some(sqrt) => sqrt, + pub fn y(&mut self) -> u16 { + match self.y_read { + Some(y) => y, None => { - let input = ((self.op1 as u32) << 16) | (self.op2 as u32); - self.sqrt = Some((input as f64).sqrt() as u16); - self.sqrt.unwrap() + 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() } } } @@ -62,7 +171,7 @@ impl MathDevice { match self.prod { Some(prod) => prod, None => { - self.prod = Some(self.op1.widening_mul(self.op2)); + self.prod = Some(self.x.widening_mul(self.y)); self.prod.unwrap() } } @@ -72,7 +181,7 @@ impl MathDevice { match self.quot { Some(quot) => quot, None => { - self.quot = Some(self.op1.checked_div(self.op2).unwrap_or(0)); + self.quot = Some(self.x.checked_div(self.y).unwrap_or(0)); self.quot.unwrap() } } @@ -82,60 +191,9 @@ impl MathDevice { match self.rem { Some(rem) => rem, None => { - self.rem = Some(self.op1.checked_rem(self.op2).unwrap_or(0)); + self.rem = Some(self.x.checked_rem(self.y).unwrap_or(0)); self.rem.unwrap() } } } } - -impl Device for MathDevice { - fn read(&mut self, port: u8) -> u8 { - match port { - 0x0 => read_h!(self.op1), - 0x1 => read_l!(self.op1), - 0x2 => read_h!(self.op2), - 0x3 => read_l!(self.op2), - 0x4 => read_h!(self.sqrt()), - 0x5 => read_l!(self.sqrt()), - 0x6 => read_h!(self.atan()), - 0x7 => read_l!(self.atan()), - 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.op1, value); self.clear(); }, - 0x1 => { write_l!(self.op1, value); self.clear(); }, - 0x2 => { write_h!(self.op2, value); self.clear(); }, - 0x3 => { write_l!(self.op2, value); self.clear(); }, - 0x4 => (), - 0x5 => (), - 0x6 => (), - 0x7 => (), - 0x8 => (), - 0x9 => (), - 0xa => (), - 0xb => (), - 0xc => (), - 0xd => (), - 0xe => (), - 0xf => (), - _ => unreachable!(), - }; - return None; - } - - fn wake(&mut self) -> bool { - false - } -} diff --git a/src/devices/memory_device.rs b/src/devices/memory_device.rs index 0128d55..d116ca7 100644 --- a/src/devices/memory_device.rs +++ b/src/devices/memory_device.rs @@ -1,93 +1,151 @@ -use bedrock_core::*; +use crate::*; -type Page = [u8; 256]; +use std::cmp::min; -macro_rules! fn_read_head { - ($fn_name:ident($offset:ident, $address:ident)) => { - pub fn $fn_name(&mut self) -> u8 { - let page_i = (self.$offset + (self.$address / 256)) as usize; - let byte_i = (self.$address % 256) as usize; - self.$address = self.$address.wrapping_add(1); - match self.pages.get(page_i) { - Some(page) => page[byte_i], - None => 0, - } - } - }; -} -macro_rules! fn_write_head { - ($fn_name:ident($offset:ident, $address:ident)) => { - pub fn $fn_name(&mut self, byte: u8) { - let page_i = (self.$offset + (self.$address / 256)) as usize; - let byte_i = (self.$address % 256) as usize; - self.$address = self.$address.wrapping_add(1); - match self.pages.get_mut(page_i) { - Some(page) => page[byte_i] = byte, - None => if page_i < self.provisioned { - self.pages.resize(page_i + 1, [0; 256]); - self.pages[page_i][byte_i] = byte; - } - } - } - }; -} +type Page = [u8; 256]; pub struct MemoryDevice { - pub limit: u16, // maximum provisionable number of pages - pub requested: u16, // number of pages requested by program - pub provisioned: usize, // number of pages provisioned for use + 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, +} - pub offset_1: u16, - pub address_1: u16, - pub offset_2: u16, - pub address_2: u16, - pub copy_length: u16, +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, - requested: 0, - provisioned: 0, pages: Vec::new(), + count_write: 0, + count: 0, + copy_write: 0, + head_1: HeadAddress::new(), + head_2: HeadAddress::new(), + } + } - offset_1: 0, - address_1: 0, - offset_2: 0, - address_2: 0, + 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) + } - copy_length: 0, + 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, } } - fn_read_head! { read_head_1( offset_1, address_1) } - fn_read_head! { read_head_2( offset_2, address_2) } - fn_write_head!{ write_head_1(offset_1, address_1) } - fn_write_head!{ write_head_2(offset_2, address_2) } + 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); + } - pub fn provision(&mut self) { - self.provisioned = std::cmp::min(self.requested, self.limit) as usize; + 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.provisioned as usize); + self.pages.truncate(self.count as usize); } pub fn copy(&mut self) { - let src = self.offset_2 as usize; - let dest = self.offset_1 as usize; - let count = self.copy_length as usize; + 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 pages_needed = std::cmp::min(dest + count, self.provisioned); - if pages_needed > self.pages.len() { - self.pages.resize(pages_needed, [0; 256]); + let allocate = min(dest + n, self.count); + if allocate > self.pages.len() { + self.pages.resize(allocate, [0; 256]); } - for i in 0..count { + for i in 0..n { let src_page = match self.pages.get(src + i) { Some(src_page) => src_page.to_owned(), None => [0; 256], @@ -100,53 +158,29 @@ impl MemoryDevice { } } -impl Device for MemoryDevice { - fn read(&mut self, port: u8) -> u8 { - match port { - 0x0 => self.read_head_1(), - 0x1 => self.read_head_1(), - 0x2 => read_h!(self.offset_1), - 0x3 => read_l!(self.offset_1), - 0x4 => read_h!(self.address_1), - 0x5 => read_l!(self.address_1), - 0x6 => read_h!(self.provisioned), - 0x7 => read_l!(self.provisioned), - 0x8 => self.read_head_2(), - 0x9 => self.read_head_2(), - 0xa => read_h!(self.offset_2), - 0xb => read_l!(self.offset_2), - 0xc => read_h!(self.address_2), - 0xd => read_l!(self.address_2), - 0xe => 0x00, - 0xf => 0x00, - _ => unreachable!(), + +pub struct HeadAddress { + pub page: u16, + pub address: u16, +} + +impl HeadAddress { + pub fn new() -> Self { + Self { + page: 0, + address: 0, } } - fn write(&mut self, port: u8, value: u8) -> Option<Signal> { - match port { - 0x0 => self.write_head_1(value), - 0x1 => self.write_head_1(value), - 0x2 => write_h!(self.offset_1, value), - 0x3 => write_l!(self.offset_1, value), - 0x4 => write_h!(self.address_1, value), - 0x5 => write_l!(self.address_1, value), - 0x6 => write_h!(self.requested, value), - 0x7 => { write_l!(self.requested, value); self.provision(); }, - 0x8 => self.write_head_2(value), - 0x9 => self.write_head_2(value), - 0xa => write_h!(self.offset_2, value), - 0xb => write_l!(self.offset_2, value), - 0xc => write_h!(self.address_2, value), - 0xd => write_l!(self.address_2, value), - 0xe => write_h!(self.copy_length, value), - 0xf => { write_l!(self.copy_length, value); self.copy(); }, - _ => unreachable!(), - }; - return None; + pub fn reset(&mut self) { + self.page = 0; + self.address = 0; } - fn wake(&mut self) -> bool { - false + 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/remote_device.rs b/src/devices/remote_device.rs deleted file mode 100644 index f50ac7a..0000000 --- a/src/devices/remote_device.rs +++ /dev/null @@ -1,35 +0,0 @@ -use bedrock_core::*; - - -pub struct RemoteDevice { - -} - -impl RemoteDevice { - pub fn new() -> Self { - Self { - - } - } -} - -impl Device for RemoteDevice { - fn read(&mut self, _port: u8) -> u8 { - todo!() - // match port { - // _ => unreachable!(), - // } - } - - fn write(&mut self, _port: u8, _value: u8) -> Option<Signal> { - todo!() - // match port { - // _ => unreachable!(), - // }; - // return None; - } - - fn wake(&mut self) -> bool { - false - } -} diff --git a/src/devices/screen_device.rs b/src/devices/screen_device.rs index a10ab20..483bcca 100644 --- a/src/devices/screen_device.rs +++ b/src/devices/screen_device.rs @@ -1,21 +1,18 @@ use crate::*; -use bedrock_core::*; use geometry::*; use phosphor::*; -type Sprite = [[u8; 8]; 8]; + +pub type Sprite = [[u8; 8]; 8]; + #[derive(Clone, Copy)] pub enum Layer { Fg, Bg } pub struct ScreenDevice { - pub wake: bool, - pub accessed: bool, - /// Each byte represents a screen pixel, left-to-right and top-to-bottom. // Only the bottom four bits of each byte are used. - // TODO: Consider using the high bit of each pixel byte as a dirty bit. pub fg: Vec<u8>, pub bg: Vec<u8>, pub dirty: bool, @@ -32,18 +29,110 @@ pub struct ScreenDevice { pub palette_write: u16, pub palette: [Colour; 16], - pub colours: u16, + 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 { - wake: false, - accessed: false, - fg: vec![0; area], bg: vec![0; area], dirty: false, @@ -60,12 +149,15 @@ impl ScreenDevice { palette_write: 0, palette: [Colour::BLACK; 16], - colours: 0, + sprite_colours: 0, sprite: SpriteBuffer::new(), + + accessed: false, + wake: false, } } - /// External resize. + /// Resize screen to match window dimensions. pub fn resize(&mut self, dimensions: phosphor::Dimensions) { // Replace dimensions with fixed dimensions. let screen_dimensions = ScreenDimensions { @@ -158,9 +250,9 @@ impl ScreenDevice { 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 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; @@ -193,7 +285,7 @@ impl ScreenDevice { } pub fn move_cursor(&mut self, value: u8) { - let distance = (value & 0x3f) as u16; + 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), @@ -203,7 +295,7 @@ impl ScreenDevice { }; } - /// Colour must already be masked by 0xf. + /// 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); @@ -215,13 +307,13 @@ impl ScreenDevice { } fn op_draw_pixel(&mut self, layer: Layer, draw: u8) { - self.draw_pixel(layer, self.cursor.x, self.cursor.y, draw & 0xf); + 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), + Layer::Fg => self.fg.fill(draw & 0xF), + Layer::Bg => self.bg.fill(draw & 0xF), } } @@ -231,10 +323,10 @@ impl ScreenDevice { false => self.sprite.read_1bit_sprite(draw), }; let colours = [ - (self.colours >> 12 & 0x000f) as u8, - (self.colours >> 8 & 0x000f) as u8, - (self.colours >> 4 & 0x000f) as u8, - (self.colours & 0x000f) as u8, + (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; @@ -279,8 +371,8 @@ impl ScreenDevice { if draw & 0x10 != 0 { // Draw 1-bit textured line. let sprite = self.sprite.read_1bit_sprite(draw); - let c1 = (self.colours >> 8 & 0xf) as u8; - let c0 = (self.colours >> 12 & 0xf) as u8; + 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]; @@ -293,7 +385,7 @@ impl ScreenDevice { } } else { // Draw solid line. - let colour = draw & 0xf; + let colour = draw & 0xF; loop { self.draw_pixel(layer, x as u16, y as u16, colour); if x == x_end && y == y_end { break; } @@ -307,7 +399,7 @@ impl ScreenDevice { 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 } + if $v > 0x7FFF { 0 } else if $v > $max { $max } else { $v } }; } macro_rules! out_of_bounds { @@ -333,8 +425,8 @@ impl ScreenDevice { if draw & 0x10 != 0 { // Draw 1-bit textured rectangle. let sprite = self.sprite.read_1bit_sprite(draw); - let c1 = (self.colours >> 8 & 0xf) as u8; - let c0 = (self.colours >> 12 & 0xf) as u8; + 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 { @@ -345,7 +437,7 @@ impl ScreenDevice { } } else { // Draw solid rectangle. - let colour = draw & 0xf; + let colour = draw & 0xF; for y in t..=b { for x in l..=r { self.draw_pixel(layer, x, y, colour); @@ -355,146 +447,4 @@ impl ScreenDevice { } } -impl Device for ScreenDevice { - fn read(&mut self, port: u8) -> u8 { - self.accessed = true; - match port { - 0x0 => read_h!(self.dimensions.width), - 0x1 => read_l!(self.dimensions.width), - 0x2 => read_h!(self.dimensions.height), - 0x3 => read_l!(self.dimensions.height), - 0x4 => read_h!(self.cursor.x), - 0x5 => read_l!(self.cursor.x), - 0x6 => read_h!(self.cursor.y), - 0x7 => read_l!(self.cursor.y), - 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> { - self.accessed = true; - match port { - 0x0 => write_h!(self.width_write, value), - 0x1 => { write_l!(self.width_write, value); self.resize_width(); }, - 0x2 => write_h!(self.height_write, value), - 0x3 => { write_l!(self.height_write, value); self.resize_height(); }, - 0x4 => write_h!(self.cursor.x, value), - 0x5 => write_l!(self.cursor.x, value), - 0x6 => write_h!(self.cursor.y, value), - 0x7 => write_l!(self.cursor.y, value), - 0x8 => write_h!(self.palette_write, value), - 0x9 => { write_l!(self.palette_write, value); self.set_palette(); }, - 0xa => write_h!(self.colours, value), - 0xb => write_l!(self.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 None; - } - - fn wake(&mut self) -> bool { - self.accessed = true; - std::mem::take(&mut self.wake) - } -} - -impl HasDimensions<u16> for ScreenDevice { - fn dimensions(&self) -> ScreenDimensions { - self.dimensions - } -} - - -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/devices/local_device.rs b/src/devices/stream_device.rs index c6456de..1e67166 100644 --- a/src/devices/local_device.rs +++ b/src/devices/stream_device.rs @@ -1,36 +1,95 @@ use crate::*; -use bedrock_core::*; - use std::collections::VecDeque; use std::io::{BufRead, Stdout, Write}; use std::sync::mpsc::{self, TryRecvError}; -pub struct LocalDevice { - wake: bool, - +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, + 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 LocalDevice { - pub fn new(config: &EmulatorConfig) -> Self { - // Fill input queue with initial transmission. - let mut stdin_queue = VecDeque::new(); - if let Some(bytes) = &config.initial_transmission { - for byte in bytes { stdin_queue.push_front(*byte) } + +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 { @@ -46,20 +105,20 @@ impl LocalDevice { }); Self { - wake: true, - stdin_connected: true, stdin_control: false, stdin_rx, - stdin_queue, + stdin_queue: VecDeque::new(), stdin_excess: VecDeque::new(), - stdout: std::io::stdout(), + stdout: std::io::stdout(), stdout_connected: true, decode_stdin: config.decode_stdin, encode_stdout: config.encode_stdout, decode_buffer: None, + + wake: true, } } @@ -68,7 +127,8 @@ impl LocalDevice { self.stdin_queue.len().try_into().unwrap_or(u8::MAX) } - pub fn stdin_enable(&mut self) { + /// Start a transmission on stdin. + pub fn stdin_start_transmission(&mut self) { self.stdin_control = true; } @@ -81,12 +141,12 @@ impl LocalDevice { 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), + 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' ']; + let encoded = [hex!(value >> 4), hex!(value & 0xF), b' ']; self.stdout_write_raw(&encoded); } else { self.stdout_write_raw(&[value]); @@ -102,12 +162,12 @@ impl LocalDevice { } } - pub fn stdout_disable(&mut self) { - if self.encode_stdout { - self.stdout_write_raw(&[b'\n']); - } + /// 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() { @@ -115,21 +175,26 @@ impl LocalDevice { None => break, } } - 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), + 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) => (), - Err(TryRecvError::Disconnected) => { - self.stdin_control = false; - if self.stdin_connected { - self.stdin_connected = false; - self.wake = true; // wake because stdin was disconnected. + 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; } } } @@ -139,8 +204,8 @@ impl LocalDevice { 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'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; @@ -167,61 +232,8 @@ impl LocalDevice { } -impl Drop for LocalDevice { +impl Drop for StreamDevice { fn drop(&mut self) { self.flush(); } } - - -impl Device for LocalDevice { - fn read(&mut self, port: u8) -> u8 { - match port { - 0x0 => read_b!(self.stdin_connected), - 0x1 => 0xff, - 0x2 => read_b!(self.stdin_control), - 0x3 => 0xff, - 0x4 => self.stdin_length(), - 0x5 => 0xff, - 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_enable(), - 0x3 => self.stdout_disable(), - 0x4 => self.stdin_queue.clear(), - 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) - } -} diff --git a/src/devices/system_device.rs b/src/devices/system_device.rs index 383bb08..097c616 100644 --- a/src/devices/system_device.rs +++ b/src/devices/system_device.rs @@ -1,80 +1,78 @@ -use bedrock_core::*; +use crate::*; pub struct SystemDevice { - pub name: ReadBuffer, - pub authors: ReadBuffer, - pub can_wake: u16, - pub wake_id: u8, + /// 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 SystemDevice { - pub fn new() -> Self { - let pkg_version = env!("CARGO_PKG_VERSION"); - let pkg_name = env!("CARGO_PKG_NAME"); - let pkg_authors = env!("CARGO_PKG_AUTHORS"); - let name_str = format!("{pkg_name}/{pkg_version}"); - let authors_str = pkg_authors.replace(":", "\n"); - Self { - name: ReadBuffer::from_str(&name_str), - authors: ReadBuffer::from_str(&authors_str), - can_wake: 0, - wake_id: 0, - asleep: false, - } - } - - pub fn can_wake(&self, dev_id: u8) -> bool { - test_bit!(self.can_wake, 0x8000 >> dev_id) - } -} impl Device for SystemDevice { fn read(&mut self, port: u8) -> u8 { match port { - 0x0 => self.name.read(), - 0x1 => self.authors.read(), - 0x2 => 0x00, + 0x0 => 0x00, + 0x1 => 0x00, + 0x2 => self.wake_slot, 0x3 => 0x00, - 0x4 => 0x00, - 0x5 => 0x00, - 0x6 => 0b1111_1100, - 0x7 => 0b0000_0000, // TODO: Update when fs and stream implemented - 0x8 => 0x00, - 0x9 => 0x00, - 0xa => self.wake_id, - 0xb => 0x00, - 0xc => 0x00, - 0xd => 0x00, - 0xe => 0x00, - 0xf => 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 => self.name.pointer = 0, - 0x1 => self.authors.pointer = 0, - 0x2 => (), - 0x3 => (), - 0x4 => (), - 0x5 => (), - 0x6 => (), - 0x7 => (), - 0x8 => write_h!(self.can_wake, value), - 0x9 => { - write_l!(self.can_wake, value); + 0x0 => write_h!(self.wake_mask, value), + 0x1 => { + write_l!(self.wake_mask, value); self.asleep = true; return Some(Signal::Sleep); - }, - 0xa => (), - 0xb => return Some(Signal::Fork), - 0xc => (), - 0xd => (), - 0xe => (), - 0xf => (), + } + 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; @@ -83,29 +81,39 @@ impl Device for SystemDevice { 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(); + } } -pub struct ReadBuffer { - pub bytes: Vec<u8>, - pub pointer: usize, -} +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"); -impl ReadBuffer { - pub fn from_str(text: &str) -> Self { Self { - bytes: text.bytes().collect(), - pointer: 0, - } - } - - pub fn read(&mut self) -> u8 { - let pointer = self.pointer; - self.pointer += 1; - match self.bytes.get(pointer) { - Some(byte) => *byte, - None => 0, + 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.rs b/src/emulators.rs deleted file mode 100644 index 56f7181..0000000 --- a/src/emulators.rs +++ /dev/null @@ -1,32 +0,0 @@ -mod headless_emulator; -mod graphical_emulator; - -pub use headless_emulator::{HeadlessEmulator, HeadlessDeviceBus}; -pub use graphical_emulator::{GraphicalEmulator, GraphicalDeviceBus}; - -use crate::*; - -use phosphor::Colour; - -use std::path::PathBuf; - - -pub enum EmulatorSignal { - Promote, - Halt, -} - - -pub struct EmulatorConfig { - pub dimensions: ScreenDimensions, - pub fullscreen: bool, - pub scale: u32, - pub debug_palette: Option<[Colour; 16]>, - pub show_cursor: bool, - - pub initial_transmission: Option<Vec<u8>>, - pub decode_stdin: bool, - pub encode_stdout: bool, - - pub symbols_path: Option<PathBuf>, -} diff --git a/src/emulators/graphical_emulator.rs b/src/emulators/graphical_emulator.rs index 14848c6..503a8f4 100644 --- a/src/emulators/graphical_emulator.rs +++ b/src/emulators/graphical_emulator.rs @@ -1,128 +1,42 @@ use crate::*; -use bedrock_core::*; - -use phosphor::*; - -use std::time::Instant; - - -pub struct GraphicalDeviceBus { - pub sys: SystemDevice, - pub mem: MemoryDevice, - pub mat: MathDevice, - pub clk: ClockDevice, - pub inp: InputDevice, - pub scr: ScreenDevice, - pub loc: LocalDevice, - pub rem: RemoteDevice, - pub fs1: FileDevice, - pub fs2: FileDevice, -} - -impl GraphicalDeviceBus { - pub fn new(config: &EmulatorConfig) -> Self { - Self { - sys: SystemDevice::new(), - mem: MemoryDevice::new(), - mat: MathDevice::new(), - clk: ClockDevice::new(), - inp: InputDevice::new(), - scr: ScreenDevice::new(config), - loc: LocalDevice::new(config), - rem: RemoteDevice::new(), - fs1: FileDevice::new(), - fs2: FileDevice::new(), - } - } - - pub fn graphical(&self) -> bool { - self.inp.accessed || self.scr.accessed - } -} - -impl DeviceBus for GraphicalDeviceBus { - fn read(&mut self, port: u8) -> u8 { - match port & 0xf0 { - 0x00 => self.sys.read(port & 0x0f), - 0x10 => self.mem.read(port & 0x0f), - 0x20 => self.mat.read(port & 0x0f), - 0x30 => self.clk.read(port & 0x0f), - 0x40 => self.inp.read(port & 0x0f), - 0x50 => self.scr.read(port & 0x0f), - 0x80 => self.loc.read(port & 0x0f), - 0x90 => self.rem.read(port & 0x0f), - 0xa0 => self.fs1.read(port & 0x0f), - 0xb0 => self.fs2.read(port & 0x0f), - _ => 0 - } - } - - fn write(&mut self, port: u8, value: u8) -> Option<Signal> { - match port & 0xf0 { - 0x00 => self.sys.write(port & 0x0f, value), - 0x10 => self.mem.write(port & 0x0f, value), - 0x20 => self.mat.write(port & 0x0f, value), - 0x30 => self.clk.write(port & 0x0f, value), - 0x40 => self.inp.write(port & 0x0f, value), - 0x50 => self.scr.write(port & 0x0f, value), - 0x80 => self.loc.write(port & 0x0f, value), - 0x90 => self.rem.write(port & 0x0f, value), - 0xa0 => self.fs1.write(port & 0x0f, value), - 0xb0 => self.fs2.write(port & 0x0f, value), - _ => None - } - } - - fn wake(&mut self) -> bool { - macro_rules! rouse { - ($id:expr, $dev:ident) => { - if self.sys.can_wake($id) && self.$dev.wake() { - self.sys.wake_id = $id; - self.sys.asleep = false; - return true; - } - }; - } - rouse!(0xb, fs2); - rouse!(0xa, fs1); - rouse!(0x9, rem); - rouse!(0x8, loc); - rouse!(0x5, scr); - rouse!(0x4, inp); - rouse!(0x3, clk); - rouse!(0x2, mat); - rouse!(0x1, mem); - rouse!(0x0, sys); - return false; - } -} - pub struct GraphicalEmulator { pub br: BedrockEmulator<GraphicalDeviceBus>, pub debug: DebugState, - pub dimensions: ScreenDimensions, + #[cfg(feature = "gamepad")] + pub gilrs: Option<Gilrs>, + pub fullscreen: bool, + pub visible: bool, pub scale: u32, - pub render_mark: Instant, - pub debug_palette: Option<[Colour; 16]>, - pub show_debug_palette: bool, - pub show_cursor: bool, + 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, verbose: bool) -> Self { - let devices = GraphicalDeviceBus::new(config); + pub fn new(config: EmulatorConfig, debug: bool) -> Self { Self { - br: BedrockEmulator::new(devices), - debug: DebugState::new(debug, verbose, config.symbols_path.as_ref()), - dimensions: config.dimensions, + br: BedrockEmulator::new(GraphicalDeviceBus::new(&config)), + debug: DebugState::new(debug, config.symbols_path.as_ref()), + + #[cfg(feature = "gamepad")] + gilrs: match Gilrs::new() { + Ok(gilrs) => Some(gilrs), + Err(err) => { + info!("Could not start gamepad listener: {}", err); + None + } + }, + fullscreen: config.fullscreen, - scale: config.scale, + scale: config.zoom.into(), + replace_palette: config.palette.is_some(), render_mark: Instant::now(), - debug_palette: config.debug_palette, - show_debug_palette: config.debug_palette.is_some(), - show_cursor: config.show_cursor, + frame_mark: Instant::now(), + config, + visible: false, } } @@ -130,70 +44,108 @@ impl GraphicalEmulator { self.br.core.mem.load_program(bytecode); } - pub fn run(&mut self) -> EmulatorSignal { - loop { - match self.br.evaluate(BATCH_SIZE, self.debug.enabled) { - Some(Signal::Fork) => { - self.br.core.mem.pc = 0; - self.br.core.wst.sp = 0; - self.br.core.rst.sp = 0; - } - Some(Signal::Sleep) => loop { - if self.br.dev.graphical() { - return EmulatorSignal::Promote; - } - if self.br.dev.wake() { break; } - std::thread::sleep(MIN_TICK_DURATION); - } - Some(Signal::Halt) => { - self.br.dev.loc.flush(); - self.debug.info("Program halted, exiting."); - self.debug.debug_summary(&self.br.core); - return EmulatorSignal::Halt; - } - Some(Signal::Debug1) => { - self.debug.debug_summary(&self.br.core); - } - _ => (), - } - - if self.br.dev.graphical() { - return EmulatorSignal::Promote; - } - } + pub fn run(mut self, mut phosphor: Phosphor, visible: bool) { + self.visible = visible; + 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 { - macro_rules! to_u32 { - ($opt:expr) => { - match $opt { - Some(a) => Some(u32::from(a)), - None => None, - } - }; - } match self.fullscreen { true => SizeBounds { - min_width: None, - max_width: None, + min_width: None, + max_width: None, min_height: None, max_height: None, }, false => SizeBounds { - min_width: to_u32!(self.br.dev.scr.fixed_width), - max_width: to_u32!(self.br.dev.scr.fixed_width), - min_height: to_u32!(self.br.dev.scr.fixed_height), - max_height: to_u32!(self.br.dev.scr.fixed_height), + 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: u32::from(self.br.dev.scr.dimensions.width), - height: u32::from(self.br.dev.scr.dimensions.height), + 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(&config), + 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; } } @@ -202,33 +154,31 @@ 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.inp.on_cursor_enter(), - Event::CursorExit => self.br.dev.inp.on_cursor_exit(), - Event::CursorMove(p) => self.br.dev.inp.on_cursor_move(p), - Event::Resize(d) => self.br.dev.scr.resize(d), - Event::CharacterInput(c) => self.br.dev.inp.on_character(c), - Event::ModifierChange(m) => self.br.dev.inp.on_modifier(m), - Event::MouseButton { button, action } => - self.br.dev.inp.on_mouse_button(button, action), + 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.inp.on_horizontal_scroll(distance), - Axis::Vertical => self.br.dev.inp.on_vertical_scroll(distance), + Axis::Horizontal => self.br.dev.input.on_horizontal_scroll(distance), + Axis::Vertical => self.br.dev.input.on_vertical_scroll(distance), } Event::ScrollPixels { axis, distance } => match axis { - Axis::Horizontal => self.br.dev.inp.on_horizontal_scroll(distance / 20.0), - Axis::Vertical => self.br.dev.inp.on_vertical_scroll(distance / 20.0), + 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::FileDrop(_path) => todo!("FileDrop"), Event::Close => (), Event::KeyboardInput { key, action } => { - self.br.dev.inp.on_keypress(key, action); + self.br.dev.input.on_keypress(key, action); if action == Action::Pressed { match key { KeyCode::F2 => { - self.show_debug_palette = !self.show_debug_palette; + self.replace_palette = !self.replace_palette; r.write(Request::Redraw); }, KeyCode::F5 => { @@ -247,83 +197,101 @@ impl WindowProgram for GraphicalEmulator { } } } + _ => (), } } fn process(&mut self, requests: &mut EventWriter<Request>) { - self.br.dev.loc.flush(); + self.br.dev.stream.flush(); + + #[cfg(feature = "gamepad")] + 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.sys.asleep { + if self.br.dev.system.asleep { // Stay asleep if there are no pending wake events. if !self.br.dev.wake() { - if self.br.dev.scr.dirty { + if self.br.dev.screen.dirty { requests.write(Request::Redraw); } - std::thread::sleep(MIN_TICK_DURATION); + std::thread::sleep(TICK_DURATION); return; } // Wait for the current frame to be rendered. - if self.br.dev.scr.dirty { - if self.render_mark.elapsed() > MIN_FRAME_DURATION { - requests.write(Request::Redraw); - } - std::thread::sleep(MIN_TICK_DURATION); + 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() + MIN_TICK_DURATION; + let frame_end = Instant::now() + TICK_DURATION; while Instant::now() < frame_end { - match self.br.evaluate(BATCH_SIZE, self.debug.enabled) { - Some(Signal::Fork) => { - todo!("Fork") - } - Some(Signal::Sleep) => { - self.br.dev.sys.asleep = true; - break; - } - Some(Signal::Halt) => { - self.br.dev.loc.flush(); - self.debug.info("Program halted, exiting."); - self.debug.debug_summary(&self.br.core); - requests.write(Request::CloseWindow); - break; - } - Some(Signal::Debug1) => { - self.debug.debug_summary(&self.br.core); + if let Some(signal) = self.br.evaluate(BATCH_SIZE, self.debug.enabled) { + match signal { + Signal::Break => { + } + 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.scr.dirty_dimensions) { + if !self.visible { + if self.br.dev.input.accessed || self.br.dev.screen.accessed { + info!("Making window visible"); + requests.write(Request::SetVisible(true)); + self.visible = true; + } + } + + if std::mem::take(&mut self.br.dev.screen.dirty_dimensions) { requests.write(Request::SetSizeBounds(self.size_bounds())); } - if self.br.dev.scr.dirty { - let elapsed = self.render_mark.elapsed(); - if self.br.dev.sys.asleep && elapsed > MIN_FRAME_DURATION { + if self.br.dev.screen.dirty { + if self.br.dev.system.asleep { requests.write(Request::Redraw); - } else if elapsed > MAX_FRAME_DURATION { + } else if self.frame_mark.elapsed() > MAX_FRAME_DURATION { requests.write(Request::Redraw); } } else { - self.render_mark = Instant::now(); + self.frame_mark = Instant::now(); } } fn render(&mut self, buffer: &mut Buffer, _full: bool) { - let screen = &mut self.br.dev.scr; + 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.debug_palette { - Some(debug_palette) => match self.show_debug_palette { - true => debug_palette, + let palette = match self.config.palette { + Some(palette) => match self.replace_palette { + true => palette, false => screen.palette, } None => screen.palette, @@ -338,7 +306,6 @@ impl WindowProgram for GraphicalEmulator { 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. @@ -374,5 +341,6 @@ impl WindowProgram for GraphicalEmulator { 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 index f215db3..770bae3 100644 --- a/src/emulators/headless_emulator.rs +++ b/src/emulators/headless_emulator.rs @@ -1,125 +1,105 @@ use crate::*; -use bedrock_core::*; -pub struct HeadlessDeviceBus { - pub sys: SystemDevice, - pub mem: MemoryDevice, - pub mat: MathDevice, - pub clk: ClockDevice, - pub loc: LocalDevice, - pub rem: RemoteDevice, - pub fs1: FileDevice, - pub fs2: FileDevice, +pub struct HeadlessEmulator { + pub br: BedrockEmulator<HeadlessDeviceBus>, + pub debug: DebugState, } -impl HeadlessDeviceBus { - pub fn new(config: &EmulatorConfig) -> Self { +impl HeadlessEmulator { + pub fn new(config: &EmulatorConfig, debug: bool) -> Self { Self { - sys: SystemDevice::new(), - mem: MemoryDevice::new(), - mat: MathDevice::new(), - clk: ClockDevice::new(), - loc: LocalDevice::new(config), - rem: RemoteDevice::new(), - fs1: FileDevice::new(), - fs2: FileDevice::new(), + br: BedrockEmulator::new(HeadlessDeviceBus::new(config)), + debug: DebugState::new(debug, config.symbols_path.as_ref()), } } -} -impl DeviceBus for HeadlessDeviceBus { - fn read(&mut self, port: u8) -> u8 { - match port & 0xf0 { - 0x00 => self.sys.read(port & 0x0f), - 0x10 => self.mem.read(port & 0x0f), - 0x20 => self.mat.read(port & 0x0f), - 0x30 => self.clk.read(port & 0x0f), - 0x80 => self.loc.read(port & 0x0f), - 0x90 => self.rem.read(port & 0x0f), - 0xa0 => self.fs1.read(port & 0x0f), - 0xb0 => self.fs2.read(port & 0x0f), - _ => 0 - } + pub fn load_program(&mut self, bytecode: &[u8]) { + self.br.core.mem.load_program(bytecode); } - fn write(&mut self, port: u8, value: u8) -> Option<Signal> { - match port & 0xf0 { - 0x00 => self.sys.write(port & 0x0f, value), - 0x10 => self.mem.write(port & 0x0f, value), - 0x20 => self.mat.write(port & 0x0f, value), - 0x30 => self.clk.write(port & 0x0f, value), - 0x80 => self.loc.write(port & 0x0f, value), - 0x90 => self.rem.write(port & 0x0f, value), - 0xa0 => self.fs1.write(port & 0x0f, value), - 0xb0 => self.fs2.write(port & 0x0f, value), - _ => None + fn sleep(&mut self) { + loop { + if self.br.dev.wake() { break; } + std::thread::sleep(TICK_DURATION); } } - fn wake(&mut self) -> bool { - macro_rules! rouse { - ($id:expr, $dev:ident) => { - if self.sys.can_wake($id) && self.$dev.wake() { - self.sys.wake_id = $id; - self.sys.asleep = false; - return true; + 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), + _ => (), + } + _ => (), } - }; + } } - rouse!(0xb, fs2); - rouse!(0xa, fs1); - rouse!(0x9, rem); - rouse!(0x8, loc); - rouse!(0x3, clk); - rouse!(0x2, mat); - rouse!(0x1, mem); - rouse!(0x0, sys); - return false; } } -pub struct HeadlessEmulator { - pub br: BedrockEmulator<HeadlessDeviceBus>, - pub debug: DebugState, +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 HeadlessEmulator { - pub fn new(config: &EmulatorConfig, debug: bool, verbose: bool) -> Self { +impl HeadlessDeviceBus { + pub fn new(config: &EmulatorConfig) -> Self { Self { - br: BedrockEmulator::new(HeadlessDeviceBus::new(config)), - debug: DebugState::new(debug, verbose, config.symbols_path.as_ref()), + system: SystemDevice::new(0b1111_0000_1100_0000), + memory: MemoryDevice::new(), + math: MathDevice::new(), + clock: ClockDevice::new(), + stream: StreamDevice::new(&config), + file: FileDevice::new(&config), + wake_queue: WakeQueue::new(), } } +} - pub fn load_program(&mut self, bytecode: &[u8]) { - self.br.core.mem.load_program(bytecode); +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 + } } - pub fn run(&mut self, debug: bool) -> EmulatorSignal { - loop { - match self.br.evaluate(BATCH_SIZE, self.debug.enabled) { - Some(Signal::Fork) => { - self.br.core.mem.pc = 0; - self.br.core.wst.sp = 0; - self.br.core.rst.sp = 0; - } - Some(Signal::Sleep) => loop { - if self.br.dev.wake() { break; } - std::thread::sleep(MIN_TICK_DURATION); - } - Some(Signal::Halt) => { - self.br.dev.loc.flush(); - self.debug.info("Program halted, exiting."); - self.debug.debug_summary(&self.br.core); - return EmulatorSignal::Halt; - } - Some(Signal::Debug1) => if debug { - self.debug.debug_summary(&self.br.core); + 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..d4a58f9 --- /dev/null +++ b/src/emulators/mod.rs @@ -0,0 +1,24 @@ +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 trust_files: bool, + pub symbols_path: Option<PathBuf>, + pub name: Option<String>, + pub identifier: Option<String>, + pub title: String, + pub icon: Option<Icon>, +} @@ -1,23 +1,28 @@ #![feature(bigint_helper_methods)] -#![feature(unchecked_shifts)] #![feature(seek_stream_len)] -#![feature(io_error_more)] +#![feature(unchecked_shifts)] mod debug; mod devices; mod emulators; -mod metadata; +mod types; -pub use debug::DebugState; +pub use debug::*; pub use devices::*; pub use emulators::*; -pub use metadata::*; +pub use types::*; + +use bedrock_core::*; +use log::*; +use phosphor::*; use std::num::NonZeroU32; -use std::time::Duration; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + pub const BATCH_SIZE: usize = 1000; -pub const MIN_TICK_DURATION: Duration = Duration::from_millis( 4 ); -pub const MIN_FRAME_DURATION: Duration = Duration::from_millis( 14 ); +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) }; @@ -25,10 +30,24 @@ pub const DEFAULT_SCREEN_SCALE: NonZeroU32 = unsafe { NonZeroU32::new_unchecked( pub type ScreenPosition = geometry::Point<u16>; pub type ScreenDimensions = geometry::Dimensions<u16>; -#[macro_export] -macro_rules! error { - ($source:expr, $($tokens:tt)*) => {{ - eprint!("[ERROR] [{}]: ", $source); - eprintln!($($tokens)*); - }}; + +pub fn run_program(bytecode: &[u8], config: EmulatorConfig) { + let mut args = switchboard::Switchboard::from_env(); + args.named("verbose").short('v'); + if args.get("verbose").as_bool() { + log::set_log_level(log::LogLevel::Info); + } + + match Phosphor::new() { + Ok(phosphor) => { + info!("Starting graphical emulator"); + let mut emulator = GraphicalEmulator::new(config, false); + emulator.load_program(&bytecode); + emulator.run(phosphor, true); + } + Err(err) => { + eprintln!("EventLoopError: {err:?}"); + fatal!("Could not start graphical event loop"); + } + } } diff --git a/src/metadata.rs b/src/metadata.rs deleted file mode 100644 index 7692434..0000000 --- a/src/metadata.rs +++ /dev/null @@ -1,127 +0,0 @@ -use phosphor::Colour; - - -pub fn parse_metadata(bytecode: &[u8]) -> Option<ProgramMetadata> { - MetadataParser::from_bytecode(bytecode).parse() -} - - -struct MetadataParser<'a> { - bytecode: &'a [u8], -} - -impl<'a> MetadataParser<'a> { - pub fn from_bytecode(bytecode: &'a [u8]) -> Self { - Self { bytecode } - } - - pub fn parse(self) -> Option<ProgramMetadata> { - macro_rules! array { - ($len:expr, $slice:expr) => {{ - let mut array = [0; $len]; - for i in 0..$len { array[i] = *$slice.get(i).unwrap_or(&0); } - array - }}; - } - - if self.range(0x00, 3) != &[0x41, 0x00, 0x20] { - return None; - } - - let (name, version) = split_name_version(self.string(self.double(0x10))); - let authors = self.string(self.double(0x12)).map(|string| { - string.lines().map(|line| line.to_string()).collect() - }); - - Some( ProgramMetadata { - bedrock_string: array!(7, self.range(0x03, 7)), - program_memory_size: match self.double(0x0a) { - 0 => 65536, double => double as usize }, - working_stack_size: match self.byte(0x0c) { - 0 => 256, byte => byte as usize }, - return_stack_size: match self.byte(0x0d) { - 0 => 256, byte => byte as usize }, - required_devices: self.double(0x0e), - name, - version, - authors, - description: self.string(self.double(0x14)), - path_buffer: match self.double(0x16) { - 0 => None, addr => Some(addr as usize) }, - small_icon: match self.double(0x18) { - 0 => None, addr => Some(array!(72, self.range(addr, 72))) }, - large_icon: match self.double(0x1a) { - 0 => None, addr => Some(array!(512, self.range(addr, 512))) }, - bg_colour: parse_colour(self.double(0x1c)), - fg_colour: parse_colour(self.double(0x1e)), - } ) - } - - fn byte(&self, address: u16) -> u8 { - *self.bytecode.get(address as usize).unwrap_or(&0) - } - - fn double(&self, address: u16) -> u16 { - u16::from_be_bytes([ - *self.bytecode.get(address as usize).unwrap_or(&0), - *self.bytecode.get(address as usize + 1).unwrap_or(&0), - ]) - } - - fn range(&self, address: u16, length: usize) -> &[u8] { - let start = address as usize; - let end = start + length; - self.bytecode.get(start..end).unwrap_or(&[]) - } - - fn string(&self, address: u16) -> Option<String> { - if address == 0 { return None; } - let start = address as usize; - let mut end = start; - while let Some(byte) = self.bytecode.get(end) { - match byte { 0 => break, _ => end += 1 } - } - let range = self.bytecode.get(start..end)?; - Some( String::from_utf8_lossy(range).to_string() ) - } -} - -fn split_name_version(string: Option<String>) -> (Option<String>, Option<String>) { - if let Some(string) = string { - match string.split_once('/') { - Some((left, right)) => (Some(left.to_string()), Some(right.to_string())), - None => (Some(string), None), - } - } else { - (None, None) - } -} - -fn parse_colour(double: u16) -> Option<Colour> { - let i = (double >> 12 ) as usize; - let r = (double >> 8 & 0xf) as u8 * 17; - let g = (double >> 4 & 0xf) as u8 * 17; - let b = (double & 0xf) as u8 * 17; - match i { - 0 => Some(Colour::from_rgb(r, g, b)), - _ => None, - } -} - - -pub struct ProgramMetadata { - pub bedrock_string: [u8; 7], - pub program_memory_size: usize, - pub working_stack_size: usize, - pub return_stack_size: usize, - pub required_devices: u16, - pub name: Option<String>, - pub version: Option<String>, - pub authors: Option<Vec<String>>, - pub description: Option<String>, - pub path_buffer: Option<usize>, - pub small_icon: Option<[u8; 72]>, - pub large_icon: Option<[u8; 512]>, - pub bg_colour: Option<Colour>, - pub fg_colour: Option<Colour>, -} diff --git a/src/devices/file_device/buffered_file.rs b/src/types/buffered_file.rs index f965950..5cdf0ea 100644 --- a/src/devices/file_device/buffered_file.rs +++ b/src/types/buffered_file.rs @@ -1,10 +1,7 @@ use std::fs::File; -use std::io::{BufReader, BufWriter}; -use std::io::{Read, Write}; +use std::io::{BufReader, BufWriter, Read, Write}; use std::io::{ErrorKind, Seek, SeekFrom}; -use crate::*; - pub struct BufferedFile { file: AccessMode, @@ -42,7 +39,7 @@ impl BufferedFile { Ok(_) => buffer[0], Err(error) => match error.kind() { ErrorKind::UnexpectedEof => 0, - _ => { error!("BufferedFile::read", "{error:?}"); 0 }, + _ => { log::error!("BufferedFile::read: {error:?}"); 0 }, } } } diff --git a/src/types/controller.rs b/src/types/controller.rs new file mode 100644 index 0000000..42d3f8c --- /dev/null +++ b/src/types/controller.rs @@ -0,0 +1,166 @@ +use crate::*; + +#[cfg(feature = "gamepad")] +pub use gilrs::{Gilrs, GamepadId}; + + +pub struct OwnedGamepad { + tag: usize, + #[cfg(feature = "gamepad")] + id: Option<GamepadId>, + #[cfg(not(feature = "gamepad"))] + id: Option<()>, + gamepad: Gamepad, +} + +impl OwnedGamepad { + pub fn new(tag: usize) -> Self { + Self { tag, id: None, gamepad: Gamepad::new() } + } + + /// Returns Some if the ID owns this gamepad. + #[cfg(feature = "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. + #[cfg(feature = "gamepad")] + 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/devices/file_device/directory_listing.rs b/src/types/directory_listing.rs index 465efc7..f079217 100644 --- a/src/devices/file_device/directory_listing.rs +++ b/src/types/directory_listing.rs @@ -1,4 +1,4 @@ -use super::*; +use crate::*; pub struct DirectoryListing { diff --git a/src/devices/file_device/entry.rs b/src/types/entry_type.rs index d604bb7..6a9ac2d 100644 --- a/src/devices/file_device/entry.rs +++ b/src/types/entry_type.rs @@ -1,7 +1,8 @@ -use super::*; +use crate::*; use std::cmp::Ordering; + pub enum Entry { File(BufferedFile), Directory(DirectoryListing), diff --git a/src/devices/file_device/bedrock_file_path.rs b/src/types/file_path.rs index fdd8f79..7e6dbe8 100644 --- a/src/devices/file_device/bedrock_file_path.rs +++ b/src/types/file_path.rs @@ -1,7 +1,8 @@ -use super::*; +use crate::*; use std::cmp::Ordering; use std::ffi::OsString; +use std::path::Component; #[derive(Clone)] 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/devices/file_device/bedrock_path_buffer.rs b/src/types/path_buffer.rs index d6a0861..d6a0861 100644 --- a/src/devices/file_device/bedrock_path_buffer.rs +++ b/src/types/path_buffer.rs 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() + } +} |