use crate::*; use gilrs::Gilrs; pub struct GraphicalEmulator { pub br: BedrockEmulator, pub debug: DebugState, pub gilrs: Option, pub fullscreen: bool, pub scale: u32, pub render_mark: Instant, // last time screen was rendered pub frame_mark: Instant, // refreshes when clean pub replace_palette: bool, pub config: EmulatorConfig, } impl GraphicalEmulator { pub fn new(config: EmulatorConfig, debug: bool) -> Self { let gilrs = match Gilrs::new() { Ok(gilrs) => Some(gilrs), Err(err) => { info!("Could not start gamepad listener: {}", err); None } }; Self { br: BedrockEmulator::new(GraphicalDeviceBus::new(&config)), debug: DebugState::new(debug, config.symbols_path.as_ref()), gilrs, fullscreen: config.fullscreen, scale: config.zoom.into(), replace_palette: config.palette.is_some(), render_mark: Instant::now(), frame_mark: Instant::now(), config, } } pub fn load_program(&mut self, bytecode: &[u8]) { self.br.core.mem.load_program(bytecode); } pub fn run(self, mut phosphor: Phosphor, visible: bool) { let window = WindowBuilder { dimensions: Some(self.dimensions()), size_bounds: Some(self.size_bounds()), fullscreen: self.fullscreen, scale: self.scale, title: Some(self.config.title.clone()), cursor: match self.config.show_cursor { true => Some(CursorIcon::Default), false => None, }, icon: self.config.icon.clone(), program: Box::new(self), visible, }; phosphor.add_window(window); phosphor.run().unwrap(); } pub fn size_bounds(&self) -> SizeBounds { match self.fullscreen { true => SizeBounds { min_width: None, max_width: None, min_height: None, max_height: None, }, false => SizeBounds { min_width: self.br.dev.screen.fixed_width.map(u32::from), max_width: self.br.dev.screen.fixed_width.map(u32::from), min_height: self.br.dev.screen.fixed_height.map(u32::from), max_height: self.br.dev.screen.fixed_height.map(u32::from), }, } } pub fn dimensions(&self) -> Dimensions { Dimensions { width: self.br.dev.screen.dimensions.width.into(), height: self.br.dev.screen.dimensions.height.into(), } } } pub struct GraphicalDeviceBus { pub system: SystemDevice, pub memory: MemoryDevice, pub math: MathDevice, pub clock: ClockDevice, pub input: InputDevice, pub screen: ScreenDevice, pub stream: StreamDevice, pub file: FileDevice, pub wake_queue: WakeQueue, } impl GraphicalDeviceBus { pub fn new(config: &EmulatorConfig) -> Self { Self { system: SystemDevice::new(0b1111_1100_1100_0000), memory: MemoryDevice::new(), math: MathDevice::new(), clock: ClockDevice::new(), input: InputDevice::new(), screen: ScreenDevice::new(&config), stream: StreamDevice::new(&config), file: FileDevice::new(), wake_queue: WakeQueue::new(), } } } impl DeviceBus for GraphicalDeviceBus { fn get_device(&mut self, slot: u8) -> Option<&mut dyn Device> { match slot { 0x0 => Some(&mut self.system), 0x1 => Some(&mut self.memory), 0x2 => Some(&mut self.math ), 0x3 => Some(&mut self.clock ), 0x4 => Some(&mut self.input ), 0x5 => Some(&mut self.screen), 0x8 => Some(&mut self.stream), 0x9 => Some(&mut self.file ), _ => None } } fn wake(&mut self) -> bool { for slot in self.wake_queue.iter(self.system.wake_mask) { if let Some(device) = self.get_device(slot) { if device.wake() { self.system.wake_slot = slot; self.system.asleep = false; self.wake_queue.wake(slot); return true; } } } return false; } } impl WindowProgram for GraphicalEmulator { fn handle_event(&mut self, event: Event, r: &mut EventWriter) { match event { Event::CloseRequest => r.write(Request::CloseWindow), Event::CursorEnter => self.br.dev.input.on_cursor_enter(), Event::CursorExit => self.br.dev.input.on_cursor_exit(), Event::CursorMove(p) => self.br.dev.input.on_cursor_move(p), Event::Resize(d) => self.br.dev.screen.resize(d), Event::CharacterInput(c) => self.br.dev.input.on_character(c), Event::ModifierChange(m) => self.br.dev.input.on_modifier(m), Event::MouseButton { button, action } => self.br.dev.input.on_mouse_button(button, action), Event::FocusChange(_) => (), Event::Initialise => (), Event::ScrollLines { axis, distance } => match axis { Axis::Horizontal => self.br.dev.input.on_horizontal_scroll(distance / 20.0), Axis::Vertical => self.br.dev.input.on_vertical_scroll(distance / 20.0), } Event::ScrollPixels { axis, distance } => match axis { Axis::Horizontal => self.br.dev.input.on_horizontal_scroll(distance), Axis::Vertical => self.br.dev.input.on_vertical_scroll(distance), } Event::Close => (), Event::KeyboardInput { key, action } => { self.br.dev.input.on_keypress(key, action); if action == Action::Pressed { match key { KeyCode::F2 => { self.replace_palette = !self.replace_palette; r.write(Request::Redraw); }, KeyCode::F5 => { self.scale = std::cmp::max(1, self.scale.saturating_sub(1)); r.write(Request::SetPixelScale(self.scale)); }, KeyCode::F6 => { self.scale = self.scale.saturating_add(1); r.write(Request::SetPixelScale(self.scale)); }, KeyCode::F11 => { self.fullscreen = !self.fullscreen; r.write(Request::SetFullscreen(self.fullscreen)); }, _ => (), } } } _ => (), } } fn process(&mut self, requests: &mut EventWriter) { self.br.dev.stream.flush(); if let Some(gilrs) = &mut self.gilrs { while let Some(event) = gilrs.next_event() { self.br.dev.input.on_gamepad_event(event); } } if self.br.dev.system.asleep { // Stay asleep if there are no pending wake events. if !self.br.dev.wake() { if self.br.dev.screen.dirty { requests.write(Request::Redraw); } std::thread::sleep(TICK_DURATION); return; } // Wait for the current frame to be rendered. if self.br.dev.screen.dirty { requests.write(Request::Redraw); std::thread::sleep(TICK_DURATION); return; } } // Run the processor for the remainder of the frame. let frame_end = Instant::now() + TICK_DURATION; while Instant::now() < frame_end { if let Some(signal) = self.br.evaluate(BATCH_SIZE, self.debug.enabled) { match signal { Signal::Break => { if self.br.dev.input.accessed || self.br.dev.screen.accessed { requests.write(Request::SetVisible(true)); } } Signal::Fork | Signal::Reset => { self.br.reset(); } Signal::Sleep => { self.br.dev.system.asleep = true; break; } Signal::Halt => { self.br.dev.stream.flush(); info!("Program halted, exiting."); self.debug.debug_full(&self.br.core); requests.write(Request::CloseWindow); break; } Signal::Debug(debug_signal) => match debug_signal { Debug::Debug1 => self.debug.debug_full(&self.br.core), Debug::Debug2 => self.debug.debug_timing(&self.br.core), _ => (), } } } } if std::mem::take(&mut self.br.dev.screen.dirty_dimensions) { requests.write(Request::SetSizeBounds(self.size_bounds())); } if self.br.dev.screen.dirty { if self.br.dev.system.asleep { requests.write(Request::Redraw); } else if self.frame_mark.elapsed() > MAX_FRAME_DURATION { requests.write(Request::Redraw); } } else { self.frame_mark = Instant::now(); } } fn render(&mut self, buffer: &mut Buffer, _full: bool) { let screen = &mut self.br.dev.screen; // Generate table for calculating pixel colours from layer values. // A given screen pixel will be rendered as the colour given by // table[fg][bg], where fg and bg are the corresponding layer values. let mut table = [Colour::BLACK; 256]; let palette = match self.config.palette { Some(palette) => match self.replace_palette { true => palette, false => screen.palette, } None => screen.palette, }; table[0..16].clone_from_slice(&palette); for i in 1..16 { table[i*16..(i+1)*16].fill(palette[i]); } // Copy pixels to buffer when it is the same size as the screen. if buffer.area_usize() == screen.area_usize() { for (i, colour) in buffer.iter_mut().enumerate() { let fg = screen.fg[i]; let bg = screen.bg[i]; let index = unsafe { fg.unchecked_shl(4) | bg }; *colour = table[index as usize]; // TODO: merge fg and bg: *colour = table[screen.bg[i] as usize]; } // Copy pixels to buffer when it is a different size to the screen. } else { let buffer_width = buffer.width() as usize; let buffer_height = buffer.height() as usize; let screen_width = screen.width() as usize; let screen_height = screen.height() as usize; let width = std::cmp::min(buffer_width, screen_width ); let height = std::cmp::min(buffer_height, screen_height); let mut bi = 0; let mut si = 0; for _ in 0..height { let bi_next = bi + buffer_width; let si_next = si + screen_width; for _ in 0..width { let fg = screen.fg[si]; let bg = screen.bg[si]; let index = unsafe { fg.unchecked_shl(4) | bg }; buffer[bi] = table[index as usize]; bi += 1; si += 1; } // Fill remaining right edge with background colour. buffer[bi..bi_next].fill(table[0]); bi = bi_next; si = si_next; } // Fill remaining bottom edge with background colour. buffer[bi..].fill(table[0]); } screen.dirty = false; self.render_mark = Instant::now(); self.frame_mark = Instant::now(); } }