diff options
author | Ben Bridle <bridle.benjamin@gmail.com> | 2025-07-03 15:26:07 +1200 |
---|---|---|
committer | Ben Bridle <ben@derelict.engineering> | 2025-07-03 21:24:07 +1200 |
commit | 2accc78948fa4a18e37ab0bc405f9b2758acaa3e (patch) | |
tree | 2551180ef7fb8f67bfc826de4ad3daf2dd24942e /src/emulators | |
download | bedrock-pc-2accc78948fa4a18e37ab0bc405f9b2758acaa3e.zip |
Initial commit
Diffstat (limited to 'src/emulators')
-rw-r--r-- | src/emulators/graphical_emulator.rs | 339 | ||||
-rw-r--r-- | src/emulators/headless_emulator.rs | 105 | ||||
-rw-r--r-- | src/emulators/mod.rs | 21 |
3 files changed, 465 insertions, 0 deletions
diff --git a/src/emulators/graphical_emulator.rs b/src/emulators/graphical_emulator.rs new file mode 100644 index 0000000..598a5a2 --- /dev/null +++ b/src/emulators/graphical_emulator.rs @@ -0,0 +1,339 @@ +use crate::*; + +use gilrs::Gilrs; + + +pub struct GraphicalEmulator { + pub br: BedrockEmulator<GraphicalDeviceBus>, + pub debug: DebugState, + pub gilrs: Option<Gilrs>, + + 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<Request>) { + 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<Request>) { + 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(); + } +} diff --git a/src/emulators/headless_emulator.rs b/src/emulators/headless_emulator.rs new file mode 100644 index 0000000..cac58cf --- /dev/null +++ b/src/emulators/headless_emulator.rs @@ -0,0 +1,105 @@ +use crate::*; + + +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, config.symbols_path.as_ref()), + } + } + + pub fn load_program(&mut self, bytecode: &[u8]) { + self.br.core.mem.load_program(bytecode); + } + + fn sleep(&mut self) { + loop { + if self.br.dev.wake() { break; } + std::thread::sleep(TICK_DURATION); + } + } + + fn halt(&mut self) { + self.br.dev.stream.flush(); + info!("Program halted, exiting."); + self.debug.debug_full(&self.br.core); + std::process::exit(0); + } + + pub fn run(&mut self) -> ! { + loop { + if let Some(signal) = self.br.evaluate(BATCH_SIZE, self.debug.enabled) { + match signal { + Signal::Fork | Signal::Reset => self.br.reset(), + Signal::Sleep => self.sleep(), + Signal::Halt => self.halt(), + 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), + _ => (), + } + _ => (), + } + } + } + } +} + + +pub struct HeadlessDeviceBus { + pub system: SystemDevice, + pub memory: MemoryDevice, + pub math: MathDevice, + pub clock: ClockDevice, + pub stream: StreamDevice, + pub file: FileDevice, + pub wake_queue: WakeQueue, +} + +impl HeadlessDeviceBus { + pub fn new(config: &EmulatorConfig) -> Self { + Self { + system: SystemDevice::new(0b1111_0000_1100_0000), + memory: MemoryDevice::new(), + math: MathDevice::new(), + clock: ClockDevice::new(), + stream: StreamDevice::new(&config), + file: FileDevice::new(), + wake_queue: WakeQueue::new(), + } + } +} + +impl DeviceBus for HeadlessDeviceBus { + 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 ), + 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; + } +} diff --git a/src/emulators/mod.rs b/src/emulators/mod.rs new file mode 100644 index 0000000..8f04e4d --- /dev/null +++ b/src/emulators/mod.rs @@ -0,0 +1,21 @@ +mod headless_emulator; +mod graphical_emulator; + +pub use headless_emulator::*; +pub use graphical_emulator::*; + +use crate::*; + + +pub struct EmulatorConfig { + pub dimensions: ScreenDimensions, + pub fullscreen: bool, + pub zoom: NonZeroU32, + pub palette: Option<[Colour; 16]>, + pub show_cursor: bool, + pub decode_stdin: bool, + pub encode_stdout: bool, + pub symbols_path: Option<PathBuf>, + pub title: String, + pub icon: Option<Icon>, +} |