aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mssola@mssola.com>2026-08-30 00:06:39 +0200
committerMiquel Sabaté Solà <mssola@mssola.com>2026-09-06 15:47:50 +0200
commitad77503d953302f22a621093ba359e24526892ec (patch)
tree8ac6bf8e8839d0ebfe1d2d654cab0870f5a67136
parente1b830309d86f6b1fff803ed94f34ce7002eb532 (diff)
downloadtools.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.rs3
-rw-r--r--lib/vnf/src/lib.rs112
-rwxr-xr-xscripts/test-e2e.sh3
-rw-r--r--tests/runrom/joypad.s119
-rw-r--r--tests/vnf-tests/src/main.rs40
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)
+ }
}