mirror of
https://github.com/imjasonh/nescript
synced 2026-07-10 09:42:37 +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:
parent
bd30ac3010
commit
0de1d60c33
40 changed files with 70 additions and 11 deletions
|
|
@ -196,6 +196,18 @@ pub struct IrCodeGen<'a> {
|
|||
/// `false` and reclaim the 16 CHR bytes of tile 0 as a blank
|
||||
/// background tile.
|
||||
default_sprite_used: bool,
|
||||
/// Set to true the first time we lower an `IrOp::ReadInput`
|
||||
/// targeting player 1. Drives the `__p1_input_used` marker
|
||||
/// label — the runtime's NMI skips the three instructions that
|
||||
/// shift `$4016` into `ZP_INPUT_P1` when nobody reads the
|
||||
/// player-1 byte.
|
||||
p1_input_used: bool,
|
||||
/// Set to true the first time we lower an `IrOp::ReadInput`
|
||||
/// targeting player 2. Drives the `__p2_input_used` marker
|
||||
/// label — single-player programs skip the `$4017` read inside
|
||||
/// the NMI's shift loop, saving ~6 bytes of code and ~30 cycles
|
||||
/// per frame.
|
||||
p2_input_used: bool,
|
||||
/// Source-location markers produced from [`IrOp::SourceLoc`].
|
||||
/// Each entry is a `(label_name, span)` pair — the codegen
|
||||
/// emits a unique label-definition pseudo-op at the current
|
||||
|
|
@ -377,6 +389,8 @@ impl<'a> IrCodeGen<'a> {
|
|||
div_used: false,
|
||||
oam_used: false,
|
||||
default_sprite_used: false,
|
||||
p1_input_used: false,
|
||||
p2_input_used: false,
|
||||
source_locs: Vec::new(),
|
||||
next_source_loc: 0,
|
||||
emit_source_locs: false,
|
||||
|
|
@ -1490,6 +1504,16 @@ impl<'a> IrCodeGen<'a> {
|
|||
self.emit(INC, AM::ZeroPage(ZP_OAM_CURSOR));
|
||||
}
|
||||
IrOp::ReadInput(dest, player) => {
|
||||
// Drop the per-player input marker so the linker
|
||||
// can decide whether to keep that port's shift
|
||||
// block inside NMI. IR uses `player_index` 0 = P1
|
||||
// and 1 = P2; the ZP bytes the NMI populates match
|
||||
// the constants at the top of `runtime/mod.rs`.
|
||||
if *player == 1 {
|
||||
self.emit_p2_input_marker();
|
||||
} else {
|
||||
self.emit_p1_input_marker();
|
||||
}
|
||||
// $01 = P1 input byte, $08 = P2 input byte
|
||||
let addr = if *player == 1 { 0x08 } else { 0x01 };
|
||||
self.emit(LDA, AM::ZeroPage(addr));
|
||||
|
|
@ -2141,6 +2165,28 @@ impl<'a> IrCodeGen<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Emit the `__p1_input_used` marker label at most once per
|
||||
/// program. Triggered by `IrOp::ReadInput` with `player == 0`.
|
||||
/// Programs that never read controller 1 skip the three NMI
|
||||
/// instructions that shift `$4016` into `ZP_INPUT_P1`.
|
||||
fn emit_p1_input_marker(&mut self) {
|
||||
if !self.p1_input_used {
|
||||
self.emit_label("__p1_input_used");
|
||||
self.p1_input_used = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit the `__p2_input_used` marker label at most once per
|
||||
/// program. Triggered by `IrOp::ReadInput` with `player == 1`.
|
||||
/// Single-player programs skip the three NMI instructions that
|
||||
/// shift `$4017` into `ZP_INPUT_P2`.
|
||||
fn emit_p2_input_marker(&mut self) {
|
||||
if !self.p2_input_used {
|
||||
self.emit_label("__p2_input_used");
|
||||
self.p2_input_used = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit the MMC3 `__irq_user` handler that dispatches on the
|
||||
/// `(current_state, scanline_step)` pair. Supports multiple
|
||||
/// `on scanline(N)` handlers per state — they fire in ascending
|
||||
|
|
|
|||
|
|
@ -662,12 +662,14 @@ 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_p2_input = has_label(user_code, "__p2_input_used");
|
||||
all_instructions.extend(runtime::gen_nmi(runtime::NmiOptions {
|
||||
has_ppu_updates,
|
||||
has_audio,
|
||||
debug_mode,
|
||||
has_sprite_cycle,
|
||||
has_oam,
|
||||
has_p2_input,
|
||||
}));
|
||||
|
||||
// IRQ handler
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue