diff options
| author | Miquel Sabaté Solà <mssola@mssola.com> | 2026-08-30 00:06:39 +0200 |
|---|---|---|
| committer | Miquel Sabaté Solà <mssola@mssola.com> | 2026-09-06 15:47:50 +0200 |
| commit | ad77503d953302f22a621093ba359e24526892ec (patch) | |
| tree | 8ac6bf8e8839d0ebfe1d2d654cab0870f5a67136 | |
| parent | e1b830309d86f6b1fff803ed94f34ce7002eb532 (diff) | |
| download | tools.nes-ad77503d953302f22a621093ba359e24526892ec.tar.gz tools.nes-ad77503d953302f22a621093ba359e24526892ec.zip | |
vnf: add support for standard joypads
This means adding constants so developers can actually trigger which key
presses are available at any given read, as well as a simple interface
that allows to add button presses and clear them.
Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
| -rw-r--r-- | crates/runrom/src/main.rs | 3 | ||||
| -rw-r--r-- | lib/vnf/src/lib.rs | 112 | ||||
| -rwxr-xr-x | scripts/test-e2e.sh | 3 | ||||
| -rw-r--r-- | tests/runrom/joypad.s | 119 | ||||
| -rw-r--r-- | tests/vnf-tests/src/main.rs | 40 |
5 files changed, 256 insertions, 21 deletions
diff --git a/crates/runrom/src/main.rs b/crates/runrom/src/main.rs index 4388766..8f52f4a 100644 --- a/crates/runrom/src/main.rs +++ b/crates/runrom/src/main.rs @@ -27,6 +27,7 @@ fn print_help() { println!(" -h, --help\t\t\tPrint this message and quit."); println!(" -n, --nasm-directory <PATH>\tPath to the .nasm/ directory."); println!(" -s, --start\t\t\tAddress from where to start (default: reset vector)."); + println!(" -u, --until-address\t\tRun until the given address is met."); println!(" -v, --version\t\t\tPrint version information."); std::process::exit(0); } @@ -163,7 +164,7 @@ fn parse_arguments() -> Args { } None => die("you need to specify a file for the '-n/--nasm' flag".to_string()), }, - "--until-address" => { + "-u" | "--until-address" => { until_address = args.next(); if until_address.is_none() { die("you need to specify a value for the --until-address flag!".to_string()); diff --git a/lib/vnf/src/lib.rs b/lib/vnf/src/lib.rs index 7fbe264..a46d291 100644 --- a/lib/vnf/src/lib.rs +++ b/lib/vnf/src/lib.rs @@ -173,31 +173,92 @@ impl Default for MemoryPolicy { } /// The state of the Joypad handshake process. -#[derive(Copy, Clone, Debug, Default)] +#[derive(Debug)] pub enum JoypadState { - #[default] + /// The Joypad is waiting for the read handshake to happen. Waiting, + + /// The joypad received the first byte on the read handshake. Now it's + /// waiting for the other byte to be written to switch to the 'Sending' + /// state. Received, + + /// The joypad can be read and has data available to it. Sending, } /// The state of a Joypad. -#[derive(Copy, Clone, Debug, Default)] +/// +/// NOTE: for now only the standard joypad is supported. +#[derive(Debug)] pub struct Joypad { + /// The current state of the joypad. pub state: JoypadState, - pub value: u8, + + /// Queue of pending input values. + pub values: Vec<u8>, + + /// The shift register. Whenever the joypad goes from the 'Received' state + /// to 'Sending', the first input in 'values' is removed and set to this + /// register. Then all reads will directly happen from this register. pub shift: u8, + + /// How many times the shift register has been read. When reaching its + /// limit, any subsequent reads will fail. pub reads: u8, } +impl Default for Joypad { + fn default() -> Self { + Self { + state: JoypadState::Waiting, + values: vec![], + shift: 0, + reads: 0, + } + } +} + impl Joypad { - /// Initialize the Joypad so it's ready to accept reads. - pub fn prepare_for_reads(&mut self) { - // TODO: I still have to prepare a proper interface to interact with - // joypads. - self.value = 0; - self.shift = self.value; + /// Use these constants to prepare the inputs for functions such as + /// push_inputs(). + /// + /// NOTE: the specific values here are sorted in reverse order. That is + /// because the consumer expects to get each bit from the shift chip in + /// reverse order: from least to most significant bits. + pub const BUTTON_RIGHT: u8 = 1 << 7; + pub const BUTTON_LEFT: u8 = 1 << 6; + pub const BUTTON_DOWN: u8 = 1 << 5; + pub const BUTTON_UP: u8 = 1 << 4; + pub const BUTTON_START: u8 = 1 << 3; + pub const BUTTON_SELECT: u8 = 1 << 2; + pub const BUTTON_B: u8 = 1 << 1; + pub const BUTTON_A: u8 = 1 << 0; + + /// Initialize the Joypad so it's ready to accept reads. Call this function + /// whenever the read handshake has been completed. + pub fn prepare_for_reads(&mut self) -> Result<(), String> { + if self.values.is_empty() { + return Err("no more input on the queue".to_string()); + } self.reads = 0; + + // NOTE: flip the bits as the consumer actually expects a 1 for + // unpressed and a 0 for pressed. This is not done directly in the + // 'BUTTON_*' constants out of convenience from the API point of view. + self.shift = !self.values.remove(0); + + Ok(()) + } + + /// Push the given 'inputs' to the queue. + pub fn push_inputs(&mut self, inputs: &[u8]) { + self.values.extend_from_slice(inputs); + } + + /// Remove all pending inputs from the queue. + pub fn clear(&mut self) { + self.values.clear(); } } @@ -401,10 +462,19 @@ impl Machine { should_report_apu: false, should_report_ppu: false, policy, - joypads: [Joypad::default(); 2], + joypads: [Joypad::default(), Joypad::default()], }) } + /// Push the given 'inputs' to the controller identified by 'id'. + /// + /// NOTE: for now only standard controllers 0 and 1 are supported. + pub fn push_inputs_to(&mut self, id: usize, inputs: &[u8]) { + assert!(matches!(id, 0 | 1)); + + self.joypads[id].push_inputs(inputs); + } + // Report to the standard output the current status of the machine. fn report(&mut self) { let space = if matches!( @@ -472,14 +542,16 @@ impl Machine { Err("joypad is not ready to send data!".to_string()) } JoypadState::Sending => { + // Increase the number of reads, and go back to 'Waiting' + // whenever the whole register has been read. jp.reads += 1; - if jp.reads > 7 { - Err("too many reads for the joypad state".to_string()) - } else { - let val = jp.shift & 0x01; // TODO: actually more bits are to be sent - jp.shift >>= 1; - Ok(val) + if jp.reads == 8 { + jp.state = JoypadState::Waiting; } + + let val = jp.shift & 0x01; + jp.shift >>= 1; + Ok(val) } } } @@ -508,7 +580,7 @@ impl Machine { if value != 0 { return Err(format!("expecting exacly a '0', '{}' received", value)); } - jp.prepare_for_reads(); + jp.prepare_for_reads()?; jp.state = JoypadState::Sending; Ok(()) } @@ -756,8 +828,10 @@ impl Machine { // Compare the given 'value' with the one from the current instruction. Then // set the proper bits from the status register. fn compare(&mut self, value: i16) -> Result<(), String> { - let res = value - self.current_instruction.value() as i16; + let memory = self.load()? as i16; + let res = value - memory; + // And set flags accordingly. self.status_register.zero = res == 0; self.status_register.negative = (res as u8 & 0x80) == 0x80; self.status_register.carry = (res as u16 & 0xFF00) != 0; diff --git a/scripts/test-e2e.sh b/scripts/test-e2e.sh index 7893274..e3f85e1 100755 --- a/scripts/test-e2e.sh +++ b/scripts/test-e2e.sh @@ -305,6 +305,9 @@ exit_code=$((exit_code + $?)) ## # vnf-tests +# Needed for run_joypad_test() +./target/debug/nasm --asan -Werror -w -o tests/out/joypad.nes tests/runrom/joypad.s + echo "test: vnf-tests" cargo run --bin vnf-tests tests/ diff --git a/tests/runrom/joypad.s b/tests/runrom/joypad.s new file mode 100644 index 0000000..18394dd --- /dev/null +++ b/tests/runrom/joypad.s @@ -0,0 +1,119 @@ +.segment "HEADER" + .byte 'N', 'E', 'S', $1A + .byte $02, $01 + .byte $00 + .byte $00 + +.segment "CHARS" +.byte 0 + +.segment "CODE" +;; asan:stack full + +;; NOTE: adapted from git.mssola.com/nes/code.nes +.scope Joypad + ;; Button masks. + BUTTON_A = 1 << 7 + BUTTON_B = 1 << 6 + BUTTON_UP = 1 << 3 + BUTTON_DOWN = 1 << 2 + + ;; Port addresses for controllers. + m_joypad1 = $4016 + + ;; After running a `joypad_read_*` function these two variables will contain + ;; the given result. + zp_buttons1 = $22 + + ;;; + ;; Safely read via a re-read algorithm the joypad as indexed by the X register + ;; (0 for controller 1; 1 for controller 2). + .proc read_x + jsr Joypad::unsafe_read_x + + ;; The main idea around a re-read algorithm is that you read the + ;; controller "unsafely" once, then you do it again and compare both + ;; reads. If they were the same then we are on the safe side. Otherwise + ;; we would need to loop until we get two identical reads. This sounds + ;; bad but in practice it's not so much (and hey, if it worked for Super + ;; Mario Bros. 3, it should work for us too :P). Otherwise there is the + ;; algorithm via OAM DMA, but it sure is tricky. + @reread: + lda Joypad::zp_buttons1, x + tay + jsr Joypad::unsafe_read_x + tya + cmp Joypad::zp_buttons1, x + bne @reread + + rts + .endproc + + ;;; + ;; Read the joypad as indexed by the X register (0 for controller 1; 1 for + ;; controller 2). This method is fast but it might be vulnerable to the DPCM + ;; bug (see: https://www.nesdev.org/wiki/Controller_reading_code). + .proc unsafe_read_x + ;; Start the latch process. + lda #$01 + sta Joypad::m_joypad1 + sta Joypad::zp_buttons1, x ; Bit as a guard for the loop below. + lsr + sta Joypad::m_joypad1 + + ;; Now the joypad is ready to accept reads. + @loop: + lda Joypad::m_joypad1, x + and #%00000011 ; Ignore bits other than controller. + cmp #$01 ; Set carry if and only if nonzero. + rol Joypad::zp_buttons1, x ; Carry -> bit 0; bit 7 -> Carry + bcc @loop + rts + .endproc +.endscope + +;; Shortcut for reading the joypad from the first player safely. +.macro READ_JOYPAD1 + ldx #$00 + jsr Joypad::read_x +.endmacro + +zp_count = $00 +zp_up = $01 +zp_down = $02 +zp_a = $03 +zp_b = $04 + +.proc reset + lda #0 + sta zp_count + sta zp_up + sta zp_down + sta zp_a + sta zp_b + + READ_JOYPAD1 + lda #(Joypad::BUTTON_UP | Joypad::BUTTON_A) + and Joypad::zp_buttons1 + beq :+ + inc zp_up + inc zp_a + inc zp_count + inc zp_count + +: + READ_JOYPAD1 + lda #(Joypad::BUTTON_DOWN | Joypad::BUTTON_B) + and Joypad::zp_buttons1 + beq :+ + inc zp_down + inc zp_b + inc zp_count + inc zp_count + +: + rts +.endproc + +.segment "VECTORS" + .addr reset, reset, reset diff --git a/tests/vnf-tests/src/main.rs b/tests/vnf-tests/src/main.rs index 58a19af..7ebf9dd 100644 --- a/tests/vnf-tests/src/main.rs +++ b/tests/vnf-tests/src/main.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use vnf::{Machine, MemoryPolicy}; +use vnf::{Joypad, Machine, MemoryPolicy}; use xixanta::opcodes::InstructionIdentifier; #[derive(Default)] @@ -92,6 +92,41 @@ fn run_break_mark_test(path: &String) -> Result<(), String> { Ok(()) } +fn run_joypad_test(path: &String) -> Result<(), String> { + let rom = PathBuf::from(path).join("out/joypad.nes"); + let mut machine = Machine::from(&rom, 0x8000, MemoryPolicy::default())?; + + // Manually change the PC to the reset function. + let len = machine.prg_rom.len(); + let high = (machine.prg_rom[len - 1] as u16) << 8; + let low = machine.prg_rom[len - 2] as u16; + machine.pc = (high + low) as usize; + + // Due to the code of joypad.s, there is a repeating read algorithm. Hence, + // we can to repeat each button combination. + machine.push_inputs_to( + 0, + &[ + // First read. + (Joypad::BUTTON_DOWN | Joypad::BUTTON_B), + (Joypad::BUTTON_DOWN | Joypad::BUTTON_B), + // Second read. + (Joypad::BUTTON_DOWN | Joypad::BUTTON_B), + (Joypad::BUTTON_DOWN | Joypad::BUTTON_B), + ], + ); + machine.run_function_mode = true; + machine.until_address(0xFFFF)?; + + assert_eq!(machine.ram[0x00].value, 2); + assert_eq!(machine.ram[0x01].value, 0); + assert_eq!(machine.ram[0x02].value, 1); + assert_eq!(machine.ram[0x03].value, 0); + assert_eq!(machine.ram[0x04].value, 1); + + Ok(()) +} + fn main() { let args = parse_arguments(); let file = PathBuf::from(args.file.clone()); @@ -103,4 +138,7 @@ fn main() { if let Err(e) = run_break_mark_test(&args.file) { die(e) } + if let Err(e) = run_joypad_test(&args.file) { + die(e) + } } |
