diff --git a/examples/auto_chr_background.nes b/examples/auto_chr_background.nes index 0db0b90..7d808ba 100644 Binary files a/examples/auto_chr_background.nes and b/examples/auto_chr_background.nes differ diff --git a/examples/bouncing_ball.nes b/examples/bouncing_ball.nes index 1685d42..e25c275 100644 Binary files a/examples/bouncing_ball.nes and b/examples/bouncing_ball.nes differ diff --git a/examples/comparisons.nes b/examples/comparisons.nes index d5566be..3579089 100644 Binary files a/examples/comparisons.nes and b/examples/comparisons.nes differ diff --git a/examples/function_chain.nes b/examples/function_chain.nes index 8a0119e..24a1073 100644 Binary files a/examples/function_chain.nes and b/examples/function_chain.nes differ diff --git a/examples/inline_asm_demo.nes b/examples/inline_asm_demo.nes index 75f57bd..fc097f7 100644 Binary files a/examples/inline_asm_demo.nes and b/examples/inline_asm_demo.nes differ diff --git a/examples/metasprite_demo.nes b/examples/metasprite_demo.nes index 12ae32b..2129f9c 100644 Binary files a/examples/metasprite_demo.nes and b/examples/metasprite_demo.nes differ diff --git a/examples/nested_structs.nes b/examples/nested_structs.nes index 6bd7cba..88d7ade 100644 Binary files a/examples/nested_structs.nes and b/examples/nested_structs.nes differ diff --git a/examples/noise_triangle_sfx.nes b/examples/noise_triangle_sfx.nes index a48e47e..475a4da 100644 Binary files a/examples/noise_triangle_sfx.nes and b/examples/noise_triangle_sfx.nes differ diff --git a/examples/palette_and_background.nes b/examples/palette_and_background.nes index af97467..4665e0d 100644 Binary files a/examples/palette_and_background.nes and b/examples/palette_and_background.nes differ diff --git a/examples/sfx_pitch_envelope.nes b/examples/sfx_pitch_envelope.nes index 1facd3e..bf39279 100644 Binary files a/examples/sfx_pitch_envelope.nes and b/examples/sfx_pitch_envelope.nes differ diff --git a/examples/sprite_flicker_demo.nes b/examples/sprite_flicker_demo.nes index d1d32c8..7755fcc 100644 Binary files a/examples/sprite_flicker_demo.nes and b/examples/sprite_flicker_demo.nes differ diff --git a/examples/uxrom_banked_to_banked.nes b/examples/uxrom_banked_to_banked.nes index e33c3b8..cdce1c9 100644 Binary files a/examples/uxrom_banked_to_banked.nes and b/examples/uxrom_banked_to_banked.nes differ diff --git a/examples/uxrom_user_banked.nes b/examples/uxrom_user_banked.nes index 9370ffb..62b038c 100644 Binary files a/examples/uxrom_user_banked.nes and b/examples/uxrom_user_banked.nes differ diff --git a/src/linker/mod.rs b/src/linker/mod.rs index bc7cb1b..ad5ea76 100644 --- a/src/linker/mod.rs +++ b/src/linker/mod.rs @@ -662,6 +662,7 @@ impl Linker { // every frame" hardware symptom into visible flicker that // the eye reconstructs across frames. let has_sprite_cycle = has_label(user_code, "__sprite_cycle_used"); + let has_p1_input = has_label(user_code, "__p1_input_used"); let has_p2_input = has_label(user_code, "__p2_input_used"); all_instructions.extend(runtime::gen_nmi(runtime::NmiOptions { has_ppu_updates, @@ -670,6 +671,7 @@ impl Linker { has_sprite_cycle, has_oam, has_p2_input, + has_p1_input, })); // IRQ handler diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index dee4df8..552ea0e 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -392,6 +392,23 @@ pub struct NmiOptions { /// cycles per frame (a `LDA abs`, an `LSR A`, and a `ROL zp` /// running 8 times). pub has_p2_input: bool, + /// When false, drop the three instructions that shift `$4016` + /// (JOY1) into `ZP_INPUT_P1`. If both `has_p1_input` and + /// `has_p2_input` are false the whole strobe-and-loop block + /// disappears — programs that never touch `button.*` pay + /// zero cycles for input sampling. + pub has_p1_input: bool, +} + +impl NmiOptions { + /// Whether the program reads any controller input — the + /// necessary condition for emitting the strobe write to + /// `$4016` and the 8-iteration shift loop. Skipped entirely + /// when both ports are unused. + #[must_use] + pub fn any_input(&self) -> bool { + self.has_p1_input || self.has_p2_input + } } #[must_use] @@ -403,6 +420,7 @@ pub fn gen_nmi(opts: NmiOptions) -> Vec { has_sprite_cycle, has_oam, has_p2_input, + has_p1_input, } = opts; let mut out = Vec::new(); @@ -506,32 +524,40 @@ pub fn gen_nmi(opts: NmiOptions) -> Vec { out.extend(gen_ppu_update_apply()); } - // Read controller 1 - out.push(Instruction::new(LDA, AM::Immediate(0x01))); - out.push(Instruction::new(STA, AM::Absolute(JOY1))); - out.push(Instruction::new(LDA, AM::Immediate(0x00))); - out.push(Instruction::new(STA, AM::Absolute(JOY1))); + // Controller sampling. The strobe write to $4016 latches both + // controller ports on the same clock, so the 8-iteration shift + // loop that follows can read whichever of the two the program + // actually uses. Programs that touch no `button.*` at all skip + // the whole block. + if has_p1_input || has_p2_input { + // Strobe: write 1 then 0 to $4016 so both port latches + // capture the current button state. + out.push(Instruction::new(LDA, AM::Immediate(0x01))); + out.push(Instruction::new(STA, AM::Absolute(JOY1))); + out.push(Instruction::new(LDA, AM::Immediate(0x00))); + out.push(Instruction::new(STA, AM::Absolute(JOY1))); - // Read 8 button bits from controller 1 ($4016) into ZP_INPUT_P1 - // — and, when `has_p2_input` is set, 8 bits from controller 2 - // ($4017) into ZP_INPUT_P2 in the same loop. Single-player - // programs drop the three P2 instructions (LDA abs, LSR A, ROL - // zp) and shave ~6 bytes plus ~30 cycles/frame off the NMI. - out.push(Instruction::new(LDX, AM::Immediate(0x08))); - out.push(Instruction::new(NOP, AM::Label("__read_input".into()))); - out.push(Instruction::new(LDA, AM::Absolute(JOY1))); - out.push(Instruction::new(LSR, AM::Accumulator)); - out.push(Instruction::new(ROL, AM::ZeroPage(ZP_INPUT_P1))); - if has_p2_input { - out.push(Instruction::new(LDA, AM::Absolute(0x4017))); // JOY2 - out.push(Instruction::new(LSR, AM::Accumulator)); - out.push(Instruction::new(ROL, AM::ZeroPage(ZP_INPUT_P2))); + // 8 iterations of read-and-shift. Each active port costs + // three instructions (LDA abs, LSR A, ROL zp) inside the + // loop; inactive ports emit nothing. + out.push(Instruction::new(LDX, AM::Immediate(0x08))); + out.push(Instruction::new(NOP, AM::Label("__read_input".into()))); + if has_p1_input { + out.push(Instruction::new(LDA, AM::Absolute(JOY1))); + out.push(Instruction::new(LSR, AM::Accumulator)); + out.push(Instruction::new(ROL, AM::ZeroPage(ZP_INPUT_P1))); + } + if has_p2_input { + out.push(Instruction::new(LDA, AM::Absolute(0x4017))); // JOY2 + out.push(Instruction::new(LSR, AM::Accumulator)); + out.push(Instruction::new(ROL, AM::ZeroPage(ZP_INPUT_P2))); + } + out.push(Instruction::implied(DEX)); + out.push(Instruction::new( + BNE, + AM::LabelRelative("__read_input".into()), + )); } - out.push(Instruction::implied(DEX)); - out.push(Instruction::new( - BNE, - AM::LabelRelative("__read_input".into()), - )); // Debug frame-overrun check. The frame flag is "set on NMI, // cleared by wait_frame". If we see it set at the top of a diff --git a/src/runtime/tests.rs b/src/runtime/tests.rs index f34a260..70dcc63 100644 --- a/src/runtime/tests.rs +++ b/src/runtime/tests.rs @@ -109,13 +109,44 @@ fn nmi_skips_oam_dma_by_default() { } #[test] -fn nmi_reads_controller() { - let nmi = gen_nmi(NmiOptions::default()); +fn nmi_reads_controller_when_p1_requested() { + let nmi = gen_nmi(NmiOptions { + has_p1_input: true, + ..NmiOptions::default() + }); // Should write strobe to $4016 let has_strobe = nmi .iter() .any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4016)); - assert!(has_strobe, "NMI should strobe controller"); + assert!(has_strobe, "NMI should strobe controller when P1 requested"); + let reads_joy1 = nmi + .iter() + .any(|i| i.opcode == LDA && i.mode == AM::Absolute(0x4016)); + assert!(reads_joy1, "NMI should read JOY1 when P1 requested"); +} + +#[test] +fn nmi_skips_strobe_when_no_input() { + // With both `has_p1_input` and `has_p2_input` unset, the NMI + // should emit neither the strobe write to $4016 nor any reads + // from the two controller ports — programs that never touch + // `button.*` pay zero cycles for input sampling. + let nmi = gen_nmi(NmiOptions::default()); + let has_strobe = nmi + .iter() + .any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4016)); + assert!( + !has_strobe, + "NMI must not strobe controllers when no input is requested" + ); + let reads_joy1 = nmi + .iter() + .any(|i| i.opcode == LDA && i.mode == AM::Absolute(0x4016)); + let reads_joy2 = nmi + .iter() + .any(|i| i.opcode == LDA && i.mode == AM::Absolute(0x4017)); + assert!(!reads_joy1, "NMI must not read JOY1 without P1 input"); + assert!(!reads_joy2, "NMI must not read JOY2 without P2 input"); } #[test] diff --git a/tests/emulator/goldens/noise_triangle_sfx.audio.hash b/tests/emulator/goldens/noise_triangle_sfx.audio.hash index e9c3f96..7dd752c 100644 --- a/tests/emulator/goldens/noise_triangle_sfx.audio.hash +++ b/tests/emulator/goldens/noise_triangle_sfx.audio.hash @@ -1 +1 @@ -752f9eee 132084 +a05d1284 132084 diff --git a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash index 20f5682..3e986d6 100644 --- a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash +++ b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash @@ -1 +1 @@ -fd72f4a8 132084 +ff926f72 132084