diff options
author | Astatin <[email protected]> | 2025-03-07 22:01:12 +0900 |
---|---|---|
committer | Astatin <[email protected]> | 2025-03-07 22:01:12 +0900 |
commit | df5a1c83d8c5d680e1bd4ef1c6793db964ebebea (patch) | |
tree | 4875d7634f915df26045a4f7c355422b531c372e | |
parent | 85fd7f345b360fa644732e194498eaf3eacefbf4 (diff) |
Add gamepad recorder & gamepad replay
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Astatin-logo.gbasm | 73 | ||||
-rw-r--r-- | Astatin-logo.png | bin | 0 -> 270 bytes | |||
-rw-r--r-- | assets/dmg_boot.bin.astatin | bin | 0 -> 256 bytes | |||
-rw-r--r-- | src/display.rs | 10 | ||||
-rw-r--r-- | src/gamepad.rs | 168 | ||||
-rw-r--r-- | src/io.rs | 10 | ||||
-rw-r--r-- | src/main.rs | 38 | ||||
-rw-r--r-- | src/state.rs | 11 |
9 files changed, 269 insertions, 42 deletions
@@ -2,3 +2,4 @@ target/ debug.txt assets/cgb_boot.bin assets/dmg_boot.bin +*.record diff --git a/Astatin-logo.gbasm b/Astatin-logo.gbasm new file mode 100644 index 0000000..23c25b9 --- /dev/null +++ b/Astatin-logo.gbasm @@ -0,0 +1,73 @@ +LD SP,$fffe + +EmptyVRAM: + LD HL, $8000 + + EmptyVRAM.loop: + LD A, $00 + LD (HL+), A + LD A, $A0 + CP H + JR NZ, =EmptyVRAM.loop + +SetupLogoTile: + LD C, $48 + LD DE, =Logo + LD HL, $8010 + SetupLogoTile.loop: + LD A, (DE) + LD (HL+), A + LD (HL+), A + LD (HL+), A + LD (HL+), A + INC DE + DEC C + JR NZ, =SetupLogoTile.loop + +LD A, $01 + +LogoFirstLine: + LD HL, $9800 + LogoFirstLine.loop: + LD (HL+), A + INC A + CP $0a + JR NZ, =LogoFirstLine.loop + +LogoSecondLine: + LD HL, $9820 + LogoSecondLine.loop: + LD (HL+), A + INC A + CP $13 + JR NZ, =LogoSecondLine.loop + +LD A, $fc +LD ($47), A + +LD A,$91 +LD ($40), A + +Loop: + JR =Loop +Logo: +.DB $3f, $ff, $f0, $f0 +.DB $c0, $f0, $f0, $f0 +.DB $00, $00, $00, $fe +.DB $00, $1e, $7f, $1e +.DB $00, $00, $80, $1f +.DB $00, $01, $07, $e1 +.DB $03, $e3, $f8, $e3 +.DB $c0, $c0, $00, $cf +.DB $00, $01, $01, $3c +.DB $ff, $f0, $f0, $f0 +.DB $f3, $f0, $f0, $f0 +.DB $e0, $fe, $1f, $fe +.DB $1e, $1e, $9e, $1e +.DB $00, $1f, $78, $1f +.DB $79, $f9, $79, $f9 +.DB $e3, $e3, $e3, $e3 +.DB $cf, $cf, $cf, $cf +.DB $cf, $0f, $0f, $0f + +.PADTO 0x100 diff --git a/Astatin-logo.png b/Astatin-logo.png Binary files differnew file mode 100644 index 0000000..206aaf1 --- /dev/null +++ b/Astatin-logo.png diff --git a/assets/dmg_boot.bin.astatin b/assets/dmg_boot.bin.astatin Binary files differnew file mode 100644 index 0000000..5102a24 --- /dev/null +++ b/assets/dmg_boot.bin.astatin diff --git a/src/display.rs b/src/display.rs index b4172f7..ef98835 100644 --- a/src/display.rs +++ b/src/display.rs @@ -2,7 +2,7 @@ use crate::consts::DISPLAY_UPDATE_SLEEP_TIME_MICROS; use crate::state::MemError; -use minifb::{Window, WindowOptions}; +use minifb::{Window, WindowOptions, ScaleMode, Scale}; use std::time::SystemTime; const COLORS: [u32; 4] = [0x00e0f8d0, 0x0088c070, 0x346856, 0x00081820]; @@ -68,7 +68,13 @@ impl Display { 512, 461, /*1200, 1080,*/ /* 160,144, */ - WindowOptions::default(), + WindowOptions { + // borderless: true, + // resize: true, + // scale_mode: ScaleMode::AspectRatioStretch, + // scale: Scale::FitScreen, + ..WindowOptions::default() + }, ) .unwrap(), framebuffer: [0; 160 * 144], diff --git a/src/gamepad.rs b/src/gamepad.rs index 4ea82f7..f59577a 100644 --- a/src/gamepad.rs +++ b/src/gamepad.rs @@ -1,5 +1,8 @@ -use crate::display::Display; +use crate::state; use gilrs::{Button, GamepadId, Gilrs}; +use state::GBState; +use std::fs::File; +use std::io::{Write, Read, ErrorKind}; use minifb::Key; pub struct Gamepad { @@ -8,7 +11,7 @@ pub struct Gamepad { } pub trait Input { - fn update_events(&mut self); + fn update_events(&mut self, cycles: u128, state: &GBState); fn get_action_gamepad_reg(&self) -> u8; fn get_direction_gamepad_reg(&self) -> u8; } @@ -38,7 +41,7 @@ impl Gamepad { } impl Input for Gamepad { - fn update_events(&mut self) { + fn update_events(&mut self, _cycles: u128, _state: &GBState) { while let Some(_) = self.gilrs.next_event() {} } @@ -95,50 +98,179 @@ impl Input for Gamepad { } } -impl Input for Display { - fn update_events(&mut self) {} +pub struct Keyboard { + action_reg: u8, + direction_reg: u8, +} - fn get_action_gamepad_reg(&self) -> u8 { +impl Keyboard { + pub fn new() -> Self { + Self { + action_reg: 0, + direction_reg: 0 + } + } +} + +impl Input for Keyboard { + fn update_events(&mut self, _cycles: u128, state: &GBState) { let mut res = 0xf; - if self.window.is_key_down(Key::A) { + if state.mem.display.window.is_key_down(Key::A) { res &= 0b1110; } - if self.window.is_key_down(Key::B) { + if state.mem.display.window.is_key_down(Key::B) { res &= 0b1101; } - if self.window.is_key_down(Key::Backspace) { + if state.mem.display.window.is_key_down(Key::Backspace) { res &= 0b1011; } - if self.window.is_key_down(Key::Enter) { + if state.mem.display.window.is_key_down(Key::Enter) { res &= 0b0111; } - res - } + self.action_reg = res; - fn get_direction_gamepad_reg(&self) -> u8 { let mut res = 0xf; - if self.window.is_key_down(Key::Right) { + if state.mem.display.window.is_key_down(Key::Right) { res &= 0b1110; } - if self.window.is_key_down(Key::Left) { + if state.mem.display.window.is_key_down(Key::Left) { res &= 0b1101; } - if self.window.is_key_down(Key::Up) { + if state.mem.display.window.is_key_down(Key::Up) { res &= 0b1011; } - if self.window.is_key_down(Key::Down) { + if state.mem.display.window.is_key_down(Key::Down) { res &= 0b0111; } - res + self.direction_reg = res; + } + + 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, state: &GBState) { + self.input.update_events(cycles, state); + + 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; + } + + 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, _state: &GBState) { + 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)), + }; + } + } + } + + fn get_action_gamepad_reg(&self) -> u8 { + self.action_reg + } + + fn get_direction_gamepad_reg(&self) -> u8 { + self.direction_reg + } +} + @@ -22,15 +22,9 @@ impl Memory { 0x42 => self.display.viewport_y, 0x43 => self.display.viewport_x, 0x41 => { - let mut ret = match self.display.lcd_interrupt_mode { - 3 => 0b01000000, - 2 => 0b00100000, - 1 => 0b00010000, - 0 => 0b00001000, - _ => 0, - }; + let mut ret = 0b00001000 << self.display.lcd_interrupt_mode; - ret |= if self.display.ly > 0x90 { + ret |= if self.display.ly >= 0x90 { 1 } else if self.display.stat < 80 { 2 diff --git a/src/main.rs b/src/main.rs index ee31f31..13381bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ pub mod opcodes; pub mod serial; pub mod state; -use crate::gamepad::{Gamepad, Input}; +use crate::gamepad::{Gamepad, Input, Keyboard, GamepadRecorder, GamepadReplay}; use crate::state::GBState; use clap::Parser; use std::time::SystemTime; @@ -30,6 +30,12 @@ struct Cli { #[arg(long)] fifo_output: Option<String>, + #[arg(long)] + record_input: Option<String>, + + #[arg(long)] + replay_input: Option<String>, + #[arg(short, long, default_value_t = false)] keyboard: bool, @@ -39,6 +45,7 @@ struct Cli { fn main() { let cli = Cli::parse(); + let mut total_cycle_counter: u128 = 0; println!("Initializing Gamepad..."); @@ -52,7 +59,6 @@ fn main() { _ => panic!("If using fifo serial, both input and output should be set"), }; - let mut gamepad = Gamepad::new(); let save_file = format!("{}.sav", &cli.rom); @@ -65,6 +71,18 @@ fn main() { ); } + let mut gamepad: Box<dyn Input> = if let Some(record_file) = cli.replay_input { + Box::new(GamepadReplay::new(record_file)) + } else if cli.keyboard { + Box::new(Keyboard::new()) + } else { + Box::new(Gamepad::new()) + }; + + if let Some(record_file) = cli.record_input { + gamepad = Box::new(GamepadRecorder::new(gamepad, record_file)); + }; + let mut nanos_sleep: i128 = 0; let mut halt_time = 0; let mut was_previously_halted = false; @@ -85,6 +103,9 @@ fn main() { 4 }; + state.cpu.dbg_cycle_counter += c; + total_cycle_counter += c as u128; + state.div_timer(c); state.tima_timer(c); state.update_display_interrupts(c); @@ -93,19 +114,12 @@ fn main() { nanos_sleep += c as i128 * (consts::CPU_CYCLE_LENGTH_NANOS as f32 / cli.speed) as i128; if nanos_sleep > 0 { - let (action_button_reg, direction_button_reg) = if cli.keyboard { - ( - state.mem.display.get_action_gamepad_reg(), - state.mem.display.get_direction_gamepad_reg(), - ) - } else { - gamepad.update_events(); + gamepad.update_events(total_cycle_counter, &state); - ( + let (action_button_reg, direction_button_reg) = ( gamepad.get_action_gamepad_reg(), gamepad.get_direction_gamepad_reg(), - ) - }; + ); // gamepad.check_special_actions(&mut state.is_debug); if state.mem.joypad_is_action diff --git a/src/state.rs b/src/state.rs index 81e841d..6379e2f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -43,6 +43,8 @@ pub struct CPU { pub pc: u16, // program counter pub sp: u16, // stack pointer + + pub dbg_cycle_counter: u64, } impl CPU { @@ -52,6 +54,8 @@ impl CPU { pc: PROGRAM_START_ADDRESS, sp: STACK_START_ADDRESS, + + dbg_cycle_counter: 0, } } @@ -84,9 +88,9 @@ impl CPU { } } - pub fn print_debug(&self) { + pub fn print_debug(&mut self) { println!( - "PC: 0x{:04x}, SP: 0x{:04x}, A: 0x{:02x}, BC: 0x{:04x}, DE: 0x{:04x}, HL: 0x{:04x}, F: 0x{:02x}", + "PC: 0x{:04x}, SP: 0x{:04x}, A: 0x{:02x}, BC: 0x{:04x}, DE: 0x{:04x}, HL: 0x{:04x}, F: 0x{:02x}. Since last dbg: {} cycles", self.pc, self.sp, self.r[reg::A as usize], @@ -94,7 +98,10 @@ impl CPU { self.r16(reg::DE), self.r16(reg::HL), self.r[reg::F as usize], + self.dbg_cycle_counter, ); + + self.dbg_cycle_counter = 0; } } |