diff options
author | Ben Bridle <bridle.benjamin@gmail.com> | 2024-10-28 20:25:01 +1300 |
---|---|---|
committer | Ben Bridle <bridle.benjamin@gmail.com> | 2024-10-28 20:29:12 +1300 |
commit | 1a830a3d1b9d99653322d5ae49ea8165de7ed9d0 (patch) | |
tree | 798e77b6fcf2438b1c2538a67efe856a2f7cb979 /src/emulators | |
parent | 03c4b069e1806af256730639cefdae115b24401a (diff) | |
download | bedrock-pc-1a830a3d1b9d99653322d5ae49ea8165de7ed9d0.zip |
Rewrite emulatorv1.0.0-alpha1
This is a complete rewrite and restructure of the entire emulator
project, as part of the effort in locking down the Bedrock specification
and in creating much better tooling for creating and using Bedrock
programs.
This commit adds a command-line argument scheme, an embedded assembler,
a headless emulator for use in non-graphical environments, deferred
window creation for programs that do not access the screen device,
and new versions of phosphor and bedrock-core. The new version of
phosphor supports multi-window programs, which will make it possible to
implement program forking in the system device later on, and the new
version of bedrock-core implements the final core specification.
Diffstat (limited to 'src/emulators')
-rw-r--r-- | src/emulators/graphical_emulator.rs | 374 | ||||
-rw-r--r-- | src/emulators/headless_emulator.rs | 124 |
2 files changed, 498 insertions, 0 deletions
diff --git a/src/emulators/graphical_emulator.rs b/src/emulators/graphical_emulator.rs new file mode 100644 index 0000000..1e048de --- /dev/null +++ b/src/emulators/graphical_emulator.rs @@ -0,0 +1,374 @@ +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, + pub fullscreen: bool, + pub scale: u32, + pub render_mark: Instant, + pub debug_palette: Option<[Colour; 16]>, + pub show_debug_palette: bool, +} + +impl GraphicalEmulator { + pub fn new(config: &EmulatorConfig, debug: bool) -> Self { + let devices = GraphicalDeviceBus::new(config); + Self { + br: BedrockEmulator::new(devices), + debug: DebugState::new(debug), + dimensions: config.dimensions, + fullscreen: config.fullscreen, + scale: config.scale, + render_mark: Instant::now(), + debug_palette: config.debug_palette, + show_debug_palette: config.debug_palette.is_some(), + } + } + + pub fn load_program(&mut self, bytecode: &[u8]) { + 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.debug.print("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 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_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), + }, + + } + } + + pub fn dimensions(&self) -> Dimensions { + Dimensions { + width: u32::from(self.br.dev.scr.dimensions.width), + height: u32::from(self.br.dev.scr.dimensions.height), + } + } +} + + +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::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_horizontal_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_horizontal_scroll(distance / 20.0), + } + Event::FileDrop(_path) => todo!("FileDrop"), + + Event::Close => (), + Event::KeyboardInput { key, action } => { + self.br.dev.inp.on_keypress(key, action); + if action == Action::Pressed { + match key { + KeyCode::F2 => { + self.show_debug_palette = !self.show_debug_palette; + r.write(Request::Redraw); + }, + KeyCode::F5 => { + self.scale = std::cmp::max(1, self.scale.saturating_sub(1)); + r.write(Request::SetPixelScale(self.scale)); + }, + KeyCode::F6 => { + self.scale = self.scale.saturating_add(1); + r.write(Request::SetPixelScale(self.scale)); + }, + KeyCode::F11 => { + self.fullscreen = !self.fullscreen; + r.write(Request::SetFullscreen(self.fullscreen)); + }, + _ => (), + } + } + } + } + } + + fn process(&mut self, requests: &mut EventWriter<Request>) { + self.br.dev.loc.flush(); + + if self.br.dev.sys.asleep { + // Stay asleep if there are no pending wake events. + if !self.br.dev.wake() { + if self.br.dev.scr.dirty { + requests.write(Request::Redraw); + } + std::thread::sleep(MIN_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); + return; + } + } + + // Run the processor for the remainder of the frame. + let frame_end = Instant::now() + MIN_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.debug.print("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 std::mem::take(&mut self.br.dev.scr.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 { + requests.write(Request::Redraw); + } else if elapsed > MAX_FRAME_DURATION { + requests.write(Request::Redraw); + } + } else { + self.render_mark = Instant::now(); + } + } + + fn render(&mut self, buffer: &mut Buffer, _full: bool) { + let screen = &mut self.br.dev.scr; + + // 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, + false => screen.palette, + } + None => screen.palette, + }; + table[0..16].clone_from_slice(&palette); + for i in 1..16 { table[i*16..(i+1)*16].fill(palette[i]); } + + // Copy pixels to buffer when it is the same size as the screen. + if buffer.area_usize() == screen.area_usize() { + for (i, colour) in buffer.iter_mut().enumerate() { + let fg = screen.fg[i]; + let bg = screen.bg[i]; + let index = unsafe { fg.unchecked_shl(4) | bg }; + *colour = table[index as usize]; + + // TODO: merge fg and bg: *colour = table[screen.bg[i] as usize]; + } + // Copy pixels to buffer when it is a different size to the screen. + } else { + let buffer_width = buffer.width() as usize; + let buffer_height = buffer.height() as usize; + let screen_width = screen.width() as usize; + let screen_height = screen.height() as usize; + let width = std::cmp::min(buffer_width, screen_width ); + let height = std::cmp::min(buffer_height, screen_height); + + let mut bi = 0; + let mut si = 0; + for _ in 0..height { + let bi_next = bi + buffer_width; + let si_next = si + screen_width; + for _ in 0..width { + let fg = screen.fg[si]; + let bg = screen.bg[si]; + let index = unsafe { fg.unchecked_shl(4) | bg }; + buffer[bi] = table[index as usize]; + bi += 1; + si += 1; + } + // Fill remaining right edge with background colour. + buffer[bi..bi_next].fill(table[0]); + bi = bi_next; + si = si_next; + } + // Fill remaining bottom edge with background colour. + buffer[bi..].fill(table[0]); + } + + screen.dirty = false; + self.render_mark = Instant::now(); + } +} diff --git a/src/emulators/headless_emulator.rs b/src/emulators/headless_emulator.rs new file mode 100644 index 0000000..418ea9c --- /dev/null +++ b/src/emulators/headless_emulator.rs @@ -0,0 +1,124 @@ +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, +} + +impl HeadlessDeviceBus { + pub fn new(config: &EmulatorConfig) -> 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(), + } + } +} + +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 + } + } + + 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 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!(0x3, clk); + rouse!(0x2, mat); + rouse!(0x1, mem); + rouse!(0x0, sys); + return false; + } +} + + +pub struct HeadlessEmulator { + pub br: BedrockEmulator<HeadlessDeviceBus>, + pub debug: DebugState, +} + +impl HeadlessEmulator { + pub fn new(config: &EmulatorConfig, debug: bool) -> Self { + Self { + br: BedrockEmulator::new(HeadlessDeviceBus::new(config)), + debug: DebugState::new(debug), + } + } + + pub fn load_program(&mut self, bytecode: &[u8]) { + self.br.core.mem.load_program(bytecode); + } + + 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.debug.print("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); + } + _ => (), + } + } + } +} |