summaryrefslogtreecommitdiff
path: root/src/emulators
diff options
context:
space:
mode:
Diffstat (limited to 'src/emulators')
-rw-r--r--src/emulators/graphical_emulator.rs374
-rw-r--r--src/emulators/headless_emulator.rs124
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);
+ }
+ _ => (),
+ }
+ }
+ }
+}