use crate::*; pub struct GraphicalEmulator { pub br: BedrockEmulator, pub debug: DebugState, pub fullscreen: bool, pub scale: u32, pub render_mark: Instant, pub show_override_palette: bool, pub config: EmulatorConfig, } impl GraphicalEmulator { pub fn new(config: EmulatorConfig, debug: bool) -> Self { Self { br: BedrockEmulator::new(GraphicalDeviceBus::new(&config)), debug: DebugState::new(debug, config.symbols_path.as_ref()), fullscreen: config.fullscreen, scale: config.zoom.into(), show_override_palette: config.override_palette.is_some(), render_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) { let cursor = match self.config.show_cursor { true => Some(CursorIcon::Default), false => None, }; let program_name = match &self.config.metadata { Some(metadata) => match &metadata.name { Some(name) => name.to_string(), None => String::from("Bedrock"), } None => String::from("Bedrock"), }; let window = WindowBuilder { dimensions: Some(self.dimensions()), size_bounds: Some(self.size_bounds()), fullscreen: self.fullscreen, scale: self.scale, title: Some(program_name), cursor, icon: parse_icon_from_config(&self.config), program: Box::new(self), }; phosphor.create_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, } impl GraphicalDeviceBus { pub fn new(config: &EmulatorConfig) -> Self { Self { system: SystemDevice::new(0b1111_1100_1010_0000), memory: MemoryDevice::new(), math: MathDevice::new(), clock: ClockDevice::new(), input: InputDevice::new(), screen: ScreenDevice::new(&config), stream: StreamDevice::new(&config), file: FileDevice::new(), } } } impl DeviceBus for GraphicalDeviceBus { fn read(&mut self, port: u8) -> u8 { match port & 0xf0 { 0x00 => self.system.read(port & 0x0f), 0x10 => self.memory.read(port & 0x0f), 0x20 => self.math .read(port & 0x0f), 0x30 => self.clock .read(port & 0x0f), 0x40 => self.input .read(port & 0x0f), 0x50 => self.screen.read(port & 0x0f), 0x80 => self.stream.read(port & 0x0f), 0xa0 => self.file .read(port & 0x0f), _ => 0 } } fn write(&mut self, port: u8, value: u8) -> Option { match port & 0xf0 { 0x00 => self.system.write(port & 0x0f, value), 0x10 => self.memory.write(port & 0x0f, value), 0x20 => self.math .write(port & 0x0f, value), 0x30 => self.clock .write(port & 0x0f, value), 0x40 => self.input .write(port & 0x0f, value), 0x50 => self.screen.write(port & 0x0f, value), 0x80 => self.stream.write(port & 0x0f, value), 0xa0 => self.file .write(port & 0x0f, value), _ => None } } fn wake(&mut self) -> bool { macro_rules! rouse { ($id:expr, $dev:ident) => { let is_eligible = test_bit!(self.system.wakers, 0x8000 >> $id); if is_eligible && self.$dev.wake() { self.system.waker = $id; self.system.asleep = false; return true; } }; } rouse!(0xa, file ); rouse!(0x8, stream); rouse!(0x5, screen); rouse!(0x4, input ); rouse!(0x3, clock ); 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), Axis::Vertical => self.br.dev.input.on_vertical_scroll(distance), } Event::ScrollPixels { 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::FileDrop(_path) => todo!("FileDrop"), Event::Close => (), Event::KeyboardInput { key, action } => { self.br.dev.input.on_keypress(key, action); if action == Action::Pressed { match key { KeyCode::F2 => { self.show_override_palette = !self.show_override_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 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(MIN_TICK_DURATION); return; } // Wait for the current frame to be rendered. if self.br.dev.screen.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.system.asleep = true; break; } Some(Signal::Halt) => { self.br.dev.stream.flush(); log::info!("Program halted, exiting."); self.debug.debug_summary(&self.br.core); requests.write(Request::CloseWindow); break; } Some(Signal::Debug(Debug::Debug1)) => { self.debug.debug_summary(&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 { let elapsed = self.render_mark.elapsed(); if self.br.dev.system.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.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.override_palette { Some(override_palette) => match self.show_override_palette { true => override_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(); } } fn parse_icon_from_config(config: &EmulatorConfig) -> Option { let metadata = config.metadata.as_ref()?; let bytes = metadata.small_icon.as_ref()?; let bg = metadata.bg_colour.unwrap_or(Colour::BLACK); let fg = metadata.bg_colour.unwrap_or(Colour::WHITE); let rgba = sprite_icon_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}"), } } fn sprite_icon_to_rgb(bytes: &[u8], size: usize, bg: Colour, fg: Colour) -> Vec { 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; }