mirror of
https://github.com/imjasonh/nescript
synced 2026-07-09 09:18:01 +00:00
runtime: gate controller-1 reads, skip whole input block when unused
With `has_p1_input` false, drop the three-instruction JOY1 shift block from the NMI's input loop. With both `has_p1_input` and `has_p2_input` false, drop the strobe write to \$4016 as well — the entire controller-sampling block disappears. Audio- or compute-only programs that never touch `button.*` pay zero cycles for input sampling. The IR codegen's `__p1_input_used` marker (emitted alongside the P2 one in the previous commit) now drives this path through a new `NmiOptions::has_p1_input` bool and an `NmiOptions::any_input()` helper that's true when either port is active. Savings for a truly non-interactive program: - ~18 bytes of NMI code (strobe + loop scaffold + the 6 bytes of per-port shifting that the P2 gate already caught). - ~80 cycles per frame (the 4 cycles of strobe plus the 5 cycles of DEX/BNE × 8 that the loop would otherwise run; net of the loop overhead that's ~40 cycles, but jsnes measures it as ~80 because the JOY1 read itself was 4c × 8). Two audio goldens flip — the two audio-only examples whose NMI shifts forward by ~27 bytes once the strobe-and-loop block is gone. Same cycle-accurate-APU-timing drift as every prior NMI layout change. https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN
This commit is contained in:
parent
0de1d60c33
commit
53c454669d
18 changed files with 88 additions and 29 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Instruction> {
|
|||
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<Instruction> {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
752f9eee 132084
|
||||
a05d1284 132084
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
fd72f4a8 132084
|
||||
ff926f72 132084
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue