From ad77503d953302f22a621093ba359e24526892ec Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Sun, 30 Aug 2026 00:06:39 +0200 Subject: vnf: add support for standard joypads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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à --- tests/runrom/joypad.s | 119 ++++++++++++++++++++++++++++++++++++++++++++ tests/vnf-tests/src/main.rs | 40 ++++++++++++++- 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 tests/runrom/joypad.s (limited to '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) + } } -- cgit v1.2.3