1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00

runtime: gate controller-2 reads in NMI on __p2_input_used

Drop the three-instruction JOY2 shift block (`LDA $4017 / LSR A /
ROL ZP_INPUT_P2`) from inside the NMI's 8-iteration input loop
when user code never reads controller 2. IR codegen emits the
`__p2_input_used` marker from `IrOp::ReadInput(_, 1)`; the linker
threads the flag through a new `NmiOptions::has_p2_input` bool,
and `gen_nmi` writes the shift block only when the flag is set.

Savings for single-player programs:
 - ~6 bytes of NMI code.
 - ~30 cycles per frame (3 instructions × 8 loop iterations, each
   6-8 cycles depending on addressing — LDA abs is 4, LSR A is 2,
   ROL zp is 5, so ~11 cycles × 8 = ~88 cycles; rounded down for
   the page-crossing penalty landing differently in the new layout).

This commit also fixes the IR codegen to drop the matching
`__p1_input_used` marker from `IrOp::ReadInput(_, 0)`, even though
the next commit is the one that actually consumes it. Landing the
two markers together keeps the IR codegen's per-op bookkeeping
coherent.

Six audio goldens flip (every program that reads input + plays
audio) with the expected NMI-layout-shift cycle drift.

https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN
This commit is contained in:
Claude 2026-04-16 13:43:33 +00:00
parent bd30ac3010
commit 0de1d60c33
No known key found for this signature in database
40 changed files with 70 additions and 11 deletions

View file

@ -386,6 +386,12 @@ pub struct NmiOptions {
/// so the DMA is ~520 wasted cycles per NMI. Skipping it saves
/// those cycles plus 9 bytes of NMI code.
pub has_oam: bool,
/// When false, drop the three instructions that shift `$4017`
/// (JOY2) into `ZP_INPUT_P2` from the NMI's 8-iteration input
/// loop. Single-player programs save ~6 bytes of code and ~30
/// cycles per frame (a `LDA abs`, an `LSR A`, and a `ROL zp`
/// running 8 times).
pub has_p2_input: bool,
}
#[must_use]
@ -396,6 +402,7 @@ pub fn gen_nmi(opts: NmiOptions) -> Vec<Instruction> {
debug_mode,
has_sprite_cycle,
has_oam,
has_p2_input,
} = opts;
let mut out = Vec::new();
@ -506,16 +513,20 @@ pub fn gen_nmi(opts: NmiOptions) -> Vec<Instruction> {
out.push(Instruction::new(STA, AM::Absolute(JOY1)));
// Read 8 button bits from controller 1 ($4016) into ZP_INPUT_P1
// and 8 button bits from controller 2 ($4017) into ZP_INPUT_P2
// simultaneously — shift each port's carry into its ZP byte.
// — 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)));
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)));
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,