aboutsummaryrefslogtreecommitdiff
path: root/src/desktop
diff options
context:
space:
mode:
authorAstatin <[email protected]>2025-04-03 18:35:03 +0200
committerAstatin <[email protected]>2025-04-03 18:35:03 +0200
commit9a8e4117be8d30109229600346e7d9561c52a3e3 (patch)
tree6d2531b675e609d3d5d734732f1328769dddf557 /src/desktop
parentc1fb610e198d785aa57440b86c69587e5caaf563 (diff)
Separate core from desktop target
Diffstat (limited to 'src/desktop')
-rw-r--r--src/desktop/audio.rs54
-rw-r--r--src/desktop/input.rs287
-rw-r--r--src/desktop/load_save.rs69
-rw-r--r--src/desktop/mod.rs5
-rw-r--r--src/desktop/serial.rs130
-rw-r--r--src/desktop/window.rs125
6 files changed, 670 insertions, 0 deletions
diff --git a/src/desktop/audio.rs b/src/desktop/audio.rs
new file mode 100644
index 0000000..60d32df
--- /dev/null
+++ b/src/desktop/audio.rs
@@ -0,0 +1,54 @@
+use rodio::{OutputStream, Sink, Source};
+
+use crate::io::Audio;
+use crate::audio::SAMPLE_RATE;
+use std::time::Duration;
+
+pub struct RodioAudio {
+ _stream: OutputStream,
+ _sink: Sink,
+}
+
+struct RodioWave<W: Iterator + Send + 'static>(W);
+
+impl<W: Iterator + Send + 'static> Iterator for RodioWave<W> where <W as Iterator>::Item: rodio::Sample {
+ type Item = W::Item;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.0.next()
+ }
+}
+
+impl<W: Iterator + Send + 'static> Source for RodioWave<W> where <W as Iterator>::Item: rodio::Sample {
+ fn current_frame_len(&self) -> Option<usize> {
+ None
+ }
+
+ fn channels(&self) -> u16 {
+ 1
+ }
+
+ fn sample_rate(&self) -> u32 {
+ SAMPLE_RATE
+ }
+
+ fn total_duration(&self) -> Option<Duration> {
+ None
+ }
+}
+
+
+impl Audio for RodioAudio {
+ fn new<S: Iterator<Item = f32> + Send + 'static>(wave: S) -> Self {
+ let (stream, stream_handle) = OutputStream::try_default().unwrap();
+
+ let sink = Sink::try_new(&stream_handle).unwrap();
+ sink.append(RodioWave(wave));
+
+
+ RodioAudio {
+ _stream: stream,
+ _sink: sink,
+ }
+ }
+}
diff --git a/src/desktop/input.rs b/src/desktop/input.rs
new file mode 100644
index 0000000..1a2a212
--- /dev/null
+++ b/src/desktop/input.rs
@@ -0,0 +1,287 @@
+use std::fs::File;
+use std::io::{ErrorKind, Read, Write};
+
+use gilrs::{Button, GamepadId, Gilrs};
+use crate::desktop::window::Keys;
+use crate::io::Input;
+use winit::keyboard::KeyCode;
+
+pub struct Gamepad {
+ gilrs: Gilrs,
+ gamepad_id: Option<GamepadId>,
+}
+
+impl Gamepad {
+ pub fn new() -> Self {
+ let gilrs = Gilrs::new().unwrap();
+
+ let gamepad_id = if let Some((gamepad_id, _gamepad)) = gilrs.gamepads().next() {
+ println!("Found Gamepad id: {:?}", gamepad_id);
+ Some(gamepad_id)
+ } else {
+ println!("No gamepad found");
+ None
+ };
+
+ Self { gilrs, gamepad_id }
+ }
+
+ pub fn check_special_actions(&self, is_debug: &mut bool) {
+ if let Some(gamepad_id) = self.gamepad_id {
+ if let Some(gamepad) = self.gilrs.connected_gamepad(gamepad_id) {
+ *is_debug = gamepad.is_pressed(Button::West);
+ }
+ }
+ }
+}
+
+impl Input for Gamepad {
+ fn update_events(&mut self, _cycles: u128) -> Option<u128> {
+ while let Some(_) = self.gilrs.next_event() {}
+ None
+ }
+
+ fn get_action_gamepad_reg(&self) -> u8 {
+ let mut res = 0xf;
+
+ if let Some(gamepad_id) = self.gamepad_id {
+ if let Some(gamepad) = self.gilrs.connected_gamepad(gamepad_id) {
+ if gamepad.is_pressed(Button::East) {
+ res &= 0b1110;
+ }
+
+ if gamepad.is_pressed(Button::South) {
+ res &= 0b1101;
+ }
+
+ if gamepad.is_pressed(Button::Select) {
+ res &= 0b1011;
+ }
+
+ if gamepad.is_pressed(Button::Start) {
+ res &= 0b0111;
+ }
+ }
+ }
+
+ res
+ }
+
+ fn get_direction_gamepad_reg(&self) -> u8 {
+ let mut res = 0xf;
+
+ if let Some(gamepad_id) = self.gamepad_id {
+ if let Some(gamepad) = self.gilrs.connected_gamepad(gamepad_id) {
+ if gamepad.is_pressed(Button::DPadRight) {
+ res &= 0b1110;
+ }
+
+ if gamepad.is_pressed(Button::DPadLeft) {
+ res &= 0b1101;
+ }
+
+ if gamepad.is_pressed(Button::DPadUp) {
+ res &= 0b1011;
+ }
+
+ if gamepad.is_pressed(Button::DPadDown) {
+ res &= 0b0111;
+ }
+ }
+ }
+
+ res
+ }
+}
+
+pub struct Keyboard {
+ keys: Keys,
+ action_reg: u8,
+ direction_reg: u8,
+}
+
+impl Keyboard {
+ pub fn new(keys: Keys) -> Self {
+ Self {
+ keys,
+ action_reg: 0,
+ direction_reg: 0,
+ }
+ }
+}
+
+impl Input for Keyboard {
+ fn update_events(&mut self, _cycles: u128) -> Option<u128> {
+ let mut res = 0xf;
+ let keys = self.keys.borrow();
+
+ if keys.contains(&KeyCode::KeyA) {
+ res &= 0b1110;
+ }
+
+ if keys.contains(&KeyCode::KeyB) {
+ res &= 0b1101;
+ }
+
+ if keys.contains(&KeyCode::Backspace) {
+ res &= 0b1011;
+ }
+
+ if keys.contains(&KeyCode::Enter) {
+ res &= 0b0111;
+ }
+
+ self.action_reg = res;
+
+ let mut res = 0xf;
+
+ if keys.contains(&KeyCode::ArrowRight) {
+ res &= 0b1110;
+ }
+
+ if keys.contains(&KeyCode::ArrowLeft) {
+ res &= 0b1101;
+ }
+
+ if keys.contains(&KeyCode::ArrowUp) {
+ res &= 0b1011;
+ }
+
+ if keys.contains(&KeyCode::ArrowDown) {
+ res &= 0b0111;
+ }
+
+ self.direction_reg = res;
+
+ None
+ }
+
+ fn get_action_gamepad_reg(&self) -> u8 {
+ self.action_reg
+ }
+
+ fn get_direction_gamepad_reg(&self) -> u8 {
+ self.direction_reg
+ }
+}
+
+pub struct GamepadRecorder {
+ input: Box<dyn Input>,
+ record_file: File,
+ action_reg: u8,
+ direction_reg: u8,
+}
+
+impl GamepadRecorder {
+ pub fn new(input: Box<dyn Input>, record_file: String) -> Self {
+ Self {
+ input,
+ record_file: File::create(record_file).expect("Couldn't create gamepad record file"),
+ action_reg: 0xff,
+ direction_reg: 0xff,
+ }
+ }
+}
+
+impl Input for GamepadRecorder {
+ fn update_events(&mut self, cycles: u128) -> Option<u128> {
+ self.input.update_events(cycles);
+
+ let new_action_reg = self.input.get_action_gamepad_reg();
+ let new_direction_reg = self.input.get_direction_gamepad_reg();
+
+ if self.action_reg != new_action_reg || self.direction_reg != new_direction_reg {
+ println!(
+ "input update on cycle {} ! 0x{:02x} 0x{:02x}",
+ cycles, new_action_reg, new_direction_reg
+ );
+ if let Err(err) = self.record_file.write_all(&cycles.to_le_bytes()) {
+ eprintln!("Failed to write to record file: {}", err);
+ };
+ if let Err(err) = self
+ .record_file
+ .write_all(&[new_action_reg, new_direction_reg])
+ {
+ eprintln!("Failed to write to record file: {}", err);
+ }
+ if let Err(err) = self.record_file.flush() {
+ eprintln!("Failed to flush record file writes: {}", err);
+ }
+ }
+
+ self.action_reg = new_action_reg;
+ self.direction_reg = new_direction_reg;
+ None
+ }
+
+ fn get_action_gamepad_reg(&self) -> u8 {
+ self.action_reg
+ }
+
+ fn get_direction_gamepad_reg(&self) -> u8 {
+ self.direction_reg
+ }
+}
+
+pub struct GamepadReplay {
+ record_file: File,
+ action_reg: u8,
+ direction_reg: u8,
+ next_cycle_update: Option<u128>,
+}
+
+impl GamepadReplay {
+ pub fn new(record_file: String) -> Self {
+ let mut file = File::open(record_file).expect("Couldn't open gamepad record file");
+
+ let mut cycles_le: [u8; 16] = [0; 16];
+
+ let next_cycle_update = match file.read_exact(&mut cycles_le) {
+ Err(err) if err.kind() == ErrorKind::UnexpectedEof => None,
+ Err(err) => panic!("{}", err),
+ Ok(_) => Some(u128::from_le_bytes(cycles_le)),
+ };
+
+ Self {
+ record_file: file,
+ action_reg: 0xff,
+ direction_reg: 0xff,
+ next_cycle_update,
+ }
+ }
+}
+
+impl Input for GamepadReplay {
+ fn update_events(&mut self, cycles: u128) -> Option<u128> {
+ if let Some(next_cycle_update) = self.next_cycle_update {
+ if cycles >= next_cycle_update {
+ let mut inputs: [u8; 2] = [0; 2];
+
+ self.record_file
+ .read_exact(&mut inputs)
+ .expect("Unexpected EOF after cycle but before input");
+
+ self.action_reg = inputs[0];
+ self.direction_reg = inputs[1];
+
+ let mut cycles_le: [u8; 16] = [0; 16];
+
+ self.next_cycle_update = match self.record_file.read_exact(&mut cycles_le) {
+ Err(err) if err.kind() == ErrorKind::UnexpectedEof => None,
+ Err(err) => panic!("{}", err),
+ Ok(_) => Some(u128::from_le_bytes(cycles_le)),
+ };
+ }
+ }
+
+ return self.next_cycle_update;
+ }
+
+ fn get_action_gamepad_reg(&self) -> u8 {
+ self.action_reg
+ }
+
+ fn get_direction_gamepad_reg(&self) -> u8 {
+ self.direction_reg
+ }
+}
diff --git a/src/desktop/load_save.rs b/src/desktop/load_save.rs
new file mode 100644
index 0000000..dfe2083
--- /dev/null
+++ b/src/desktop/load_save.rs
@@ -0,0 +1,69 @@
+use std::fs::File;
+use std::io::{Write, Read};
+use crate::io::LoadSave;
+
+pub struct FSLoadSave {
+ rom_file: String,
+ save_file: String,
+}
+
+impl FSLoadSave {
+ pub fn new(rom_file: impl Into<String>, save_file: impl Into<String>) -> Self {
+ Self {
+ rom_file: rom_file.into(),
+ save_file: save_file.into(),
+ }
+ }
+}
+
+impl LoadSave for FSLoadSave {
+ type Error = std::io::Error;
+
+ fn load_rom(&self, rom: &mut [u8]) -> Result<(), std::io::Error> {
+ let mut f = File::open(&self.rom_file)?;
+
+ f.read(rom)?;
+
+ return Ok(());
+ }
+
+ fn load_bootrom(&self, boot_rom: &mut [u8]) -> Result<(), std::io::Error> {
+ println!("MBC: {:02x}", boot_rom[0x147]);
+ println!("CGB: {:02x}", boot_rom[0x143]);
+
+ if boot_rom[0x143] == 0x80 || boot_rom[0x143] == 0xc0 {
+ unimplemented!("CGB Boot rom is not implemented");
+ // let bytes = include_bytes!("../assets/cgb_boot.bin");
+
+ // self.boot_rom[..0x900].copy_from_slice(bytes);
+ // self.cgb_mode = true;
+ // self.display.cgb_mode = true;
+ } else {
+ let bytes = include_bytes!("../../assets/dmg_boot.bin");
+
+ boot_rom[..0x100].copy_from_slice(bytes);
+ }
+
+ Ok(())
+ }
+
+ fn load_external_ram(&self, external_ram: &mut [u8]) -> Result<(), std::io::Error> {
+ let mut f = File::open(&self.save_file)?;
+
+ f.read(external_ram)?;
+
+ println!("Save file loaded from \"{}\"!", self.save_file);
+
+ Ok(())
+ }
+
+ fn save_external_ram(&self, external_ram: &[u8]) -> Result<(), std::io::Error> {
+ let mut f = File::create(&self.save_file)?;
+
+ f.write_all(&external_ram)?;
+
+ println!("Save written to \"{}\"!", self.save_file);
+
+ Ok(())
+ }
+}
diff --git a/src/desktop/mod.rs b/src/desktop/mod.rs
new file mode 100644
index 0000000..af773b6
--- /dev/null
+++ b/src/desktop/mod.rs
@@ -0,0 +1,5 @@
+pub mod window;
+pub mod input;
+pub mod audio;
+pub mod load_save;
+pub mod serial;
diff --git a/src/desktop/serial.rs b/src/desktop/serial.rs
new file mode 100644
index 0000000..342f7f5
--- /dev/null
+++ b/src/desktop/serial.rs
@@ -0,0 +1,130 @@
+use std::fs::File;
+use std::io::{Read, Write};
+use std::sync::mpsc::{self, Receiver, Sender};
+use std::thread;
+
+use crate::io::Serial;
+
+pub struct UnconnectedSerial {}
+
+impl Serial for UnconnectedSerial {
+ fn write(&mut self, byte: u8) {
+ println!("Writing {} to unconnected serial", byte);
+ }
+
+ fn read(&mut self) -> u8 {
+ println!("Reading 0 from unconnected serial");
+ 0
+ }
+
+ fn new_transfer(&mut self) -> bool {
+ false
+ }
+
+ fn clock_master(&mut self) -> bool {
+ false
+ }
+
+ fn set_clock_master(&mut self, _clock_master: bool) {}
+}
+
+pub struct FIFOPackage {
+ t: bool,
+ value: u8,
+}
+
+pub struct FIFOSerial {
+ input: Receiver<u8>,
+ output: Sender<FIFOPackage>,
+ clock_change: Receiver<bool>,
+ last_read_byte: u8,
+ clock_master: bool,
+}
+
+impl FIFOSerial {
+ pub fn new(input_path: String, output_path: String) -> FIFOSerial {
+ let (tx, input) = mpsc::channel::<u8>();
+ let (clock_tx, clock_change) = mpsc::channel::<bool>();
+ thread::spawn(move || {
+ let mut input_f = File::open(input_path).unwrap();
+ loop {
+ let mut byte = [0, 0];
+
+ input_f.read(&mut byte).unwrap();
+ if byte[0] == 1 {
+ tx.send(byte[1]).unwrap();
+ } else {
+ clock_tx.send(byte[1] == 0).unwrap();
+ }
+ }
+ });
+
+ let (output, rx) = mpsc::channel::<FIFOPackage>();
+ thread::spawn(move || {
+ let mut output_f = File::create(output_path).unwrap();
+ for b in rx.iter() {
+ if b.t {
+ output_f.write(&[1, b.value]).unwrap();
+ } else {
+ output_f.write(&[0, b.value]).unwrap();
+ }
+ }
+ });
+
+ FIFOSerial {
+ input,
+ output,
+ clock_change,
+ last_read_byte: 0xff,
+ clock_master: false,
+ }
+ }
+}
+
+impl Serial for FIFOSerial {
+ fn write(&mut self, byte: u8) {
+ println!("Writing {} to fifo serial", byte);
+ if let Err(err) = self.output.send(FIFOPackage {
+ t: true,
+ value: byte,
+ }) {
+ eprintln!("Error while sending serial package: {}", err);
+ };
+ }
+
+ fn read(&mut self) -> u8 {
+ println!("Reading {} from fifo serial", self.last_read_byte);
+ self.last_read_byte
+ }
+
+ fn new_transfer(&mut self) -> bool {
+ match self.input.try_recv() {
+ Ok(byte) => {
+ println!("Received: {}", byte);
+ self.last_read_byte = byte;
+ true
+ }
+ _ => false,
+ }
+ }
+ fn clock_master(&mut self) -> bool {
+ match self.clock_change.try_recv() {
+ Ok(byte) => {
+ println!("Received clock change, master: {}", byte);
+ self.clock_master = byte;
+ }
+ _ => {}
+ };
+ self.clock_master
+ }
+
+ fn set_clock_master(&mut self, clock_master: bool) {
+ self.clock_master = clock_master;
+ if let Err(err) = self.output.send(FIFOPackage {
+ t: false,
+ value: (if clock_master { 1 } else { 0 }),
+ }) {
+ eprintln!("Error while sending serial package: {}", err);
+ }
+ }
+}
diff --git a/src/desktop/window.rs b/src/desktop/window.rs
new file mode 100644
index 0000000..14ac3a7
--- /dev/null
+++ b/src/desktop/window.rs
@@ -0,0 +1,125 @@
+use std::cell::RefCell;
+use std::collections::HashSet;
+use std::rc::Rc;
+use std::sync::Arc;
+use std::time::Duration;
+
+use crate::io::{WindowSignal, Window};
+
+use pixels::{Error, Pixels, SurfaceTexture};
+use winit::dpi::LogicalSize;
+use winit::event::{Event, WindowEvent};
+use winit::event_loop::EventLoop;
+use winit::keyboard::{KeyCode, PhysicalKey};
+use winit::platform::pump_events::EventLoopExtPumpEvents;
+use winit::window::{Window as WinitWindow, WindowBuilder};
+use winit_input_helper::WinitInputHelper;
+
+const WIDTH: u32 = 160;
+const HEIGHT: u32 = 144;
+
+pub type Keys = Rc<RefCell<HashSet<KeyCode>>>;
+
+pub struct DesktopWindow<'a> {
+ event_loop: EventLoop<()>,
+ input: WinitInputHelper,
+ window: Arc<WinitWindow>,
+ pixels: Pixels<'a>,
+ pub keys: Keys,
+}
+
+fn draw(frame: &mut [u8], fb: &[u32; 160 * 144]) {
+ for (i, pixel) in frame.chunks_exact_mut(4).enumerate() {
+ pixel.copy_from_slice(&((fb[i] << 8) | 0xff).to_be_bytes())
+ }
+}
+
+impl<'a> DesktopWindow<'a> {
+ pub fn new() -> Result<Self, Error> {
+ let event_loop = EventLoop::new().unwrap();
+ let input = WinitInputHelper::new();
+ let window = Arc::new({
+ let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64);
+ WindowBuilder::new()
+ .with_title("Gameboy Emulator")
+ .with_inner_size(size)
+ .with_min_inner_size(size)
+ .build(&event_loop)
+ .unwrap()
+ });
+
+ let pixels = {
+ let window_size = window.inner_size();
+ let surface_texture =
+ SurfaceTexture::new(window_size.width, window_size.height, window.clone());
+ Pixels::new(WIDTH, HEIGHT, surface_texture)?
+ };
+
+ Ok(Self {
+ event_loop,
+ input,
+ window,
+ pixels,
+ keys: Rc::new(HashSet::new().into()),
+ })
+ }
+}
+
+impl<'a> Window for DesktopWindow<'a> {
+ fn update(&mut self, fb: &[u32; 160 * 144]) -> Option<WindowSignal> {
+ let mut res = None;
+ let mut keys = (*self.keys).borrow_mut();
+ self.event_loop
+ .pump_events(Some(Duration::ZERO), |event, elwt| {
+ if let Event::WindowEvent {
+ event: WindowEvent::RedrawRequested,
+ ..
+ } = event
+ {
+ draw(self.pixels.frame_mut(), fb);
+ if let Err(err) = self.pixels.render() {
+ eprintln!("Error during render: {}", err);
+ return;
+ }
+ }
+
+ if let Event::WindowEvent {
+ window_id: _,
+ event:
+ WindowEvent::KeyboardInput {
+ device_id: _,
+ event: ref keyboard_event,
+ is_synthetic: _,
+ },
+ } = event
+ {
+ if let PhysicalKey::Code(keycode) = keyboard_event.physical_key {
+ if keyboard_event.state.is_pressed() {
+ keys.insert(keycode);
+ } else {
+ keys.remove(&keycode);
+ }
+ }
+ }
+
+ if self.input.update(&event) {
+ if self.input.close_requested() {
+ elwt.exit();
+ res = Some(WindowSignal::Exit);
+ return;
+ }
+
+ if let Some(size) = self.input.window_resized() {
+ if let Err(err) = self.pixels.resize_surface(size.width, size.height) {
+ eprintln!("Error during resize: {}", err);
+ return;
+ }
+ }
+
+ self.window.request_redraw();
+ }
+ });
+
+ res
+ }
+}