summaryrefslogtreecommitdiff
path: root/src/emulators/graphical_emulator.rs
diff options
context:
space:
mode:
authorBen Bridle <bridle.benjamin@gmail.com>2025-07-03 15:26:07 +1200
committerBen Bridle <ben@derelict.engineering>2025-07-03 21:24:07 +1200
commit2accc78948fa4a18e37ab0bc405f9b2758acaa3e (patch)
tree2551180ef7fb8f67bfc826de4ad3daf2dd24942e /src/emulators/graphical_emulator.rs
downloadbedrock-pc-2accc78948fa4a18e37ab0bc405f9b2758acaa3e.zip
Initial commit
Diffstat (limited to 'src/emulators/graphical_emulator.rs')
-rw-r--r--src/emulators/graphical_emulator.rs339
1 files changed, 339 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();
+ }
+}