aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAstatin <[email protected]>2025-03-07 22:01:12 +0900
committerAstatin <[email protected]>2025-03-07 22:01:12 +0900
commitdf5a1c83d8c5d680e1bd4ef1c6793db964ebebea (patch)
tree4875d7634f915df26045a4f7c355422b531c372e
parent85fd7f345b360fa644732e194498eaf3eacefbf4 (diff)
Add gamepad recorder & gamepad replay
-rw-r--r--.gitignore1
-rw-r--r--Astatin-logo.gbasm73
-rw-r--r--Astatin-logo.pngbin0 -> 270 bytes
-rw-r--r--assets/dmg_boot.bin.astatinbin0 -> 256 bytes
-rw-r--r--src/display.rs10
-rw-r--r--src/gamepad.rs168
-rw-r--r--src/io.rs10
-rw-r--r--src/main.rs38
-rw-r--r--src/state.rs11
9 files changed, 269 insertions, 42 deletions
diff --git a/.gitignore b/.gitignore
index c7841fc..b301fc1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
new file mode 100644
index 0000000..206aaf1
--- /dev/null
+++ b/Astatin-logo.png
Binary files differ
diff --git a/assets/dmg_boot.bin.astatin b/assets/dmg_boot.bin.astatin
new file mode 100644
index 0000000..5102a24
--- /dev/null
+++ b/assets/dmg_boot.bin.astatin
Binary files differ
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
+ }
+}
+
diff --git a/src/io.rs b/src/io.rs
index 87d6a2a..f426222 100644
--- a/src/io.rs
+++ b/src/io.rs
@@ -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;
}
}