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 { 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, 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, pub show_cursor: bool, } impl GraphicalEmulator { pub fn new(config: &EmulatorConfig, debug: bool, verbose: bool) -> Self { let devices = GraphicalDeviceBus::new(config); Self { br: BedrockEmulator::new(devices), debug: DebugState::new(debug, verbose, config.symbols_path.as_ref()), 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(), show_cursor: config.show_cursor, } } 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.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 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) { 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_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), } 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) { 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.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 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(); } }