mirror of
https://github.com/imjasonh/nescript
synced 2026-07-09 09:18:01 +00:00
codegen: emit gate markers at end of generate() to protect peephole
Move the six gate-marker label emissions (__mul_used, __div_used, __oam_used, __default_sprite_used, __p1_input_used, __p2_input_used) out of the inline IR-op lowering paths and into a new `emit_trailing_markers()` helper that runs once at the end of `generate()`. The IR walk now just flips a bool per marker; the label emit happens after every instruction has been lowered, so the marker never lands in the middle of a peephole-sensitive sequence. Fixes a real peephole interaction that surfaced after rebasing on main's `codegen: skip parameter-spill prologue for leaf functions` + `peephole: drop dead LDA #imm before mem-INC/DEC + JMP` improvements: an inline `__oam_used:` label inside `IrOp::DrawSprite` split the dead-load-elimination block, leaving the `STA $130 / LDA $130` redundant store+load pair that main's peephole would otherwise have collapsed to a plain `LDA #imm`. The stale bytes shifted the NMI handler by a few bytes, which shifted `on frame` execution enough that `examples/palette_and_background.ne` captured phase 1 (WarmReds) at frame 180 instead of phase 2 (CoolBlues). Regenerates every example ROM against the new codegen (all gate behaviour is unchanged — the linker still sees the same markers, just at the tail of the user stream instead of interleaved) and updates the goldens that shifted: seven audio-hash drifts (all audio-bearing programs, same cycle-accurate-APU-timing story as every prior NMI layout change) and two pixel goldens — the one- pixel sprite-position drift in `comparisons.png` that we already tolerate, plus the phase-capture flip in `palette_and_background.png`. https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN
This commit is contained in:
parent
e5bc325a71
commit
c09f9c0caa
43 changed files with 56 additions and 84 deletions
|
|
@ -882,6 +882,15 @@ impl<'a> IrCodeGen<'a> {
|
|||
self.emit(JMP, AM::Label("__debug_halt".to_string()));
|
||||
}
|
||||
|
||||
// Pay-as-you-go marker labels for flags that got set during
|
||||
// the IR walk above. Emitting them here (rather than at the
|
||||
// call sites) keeps the markers out of peephole-sensitive
|
||||
// instruction sequences — a label inside `IrOp::DrawSprite`
|
||||
// or `IrOp::ReadInput`, for instance, would otherwise split
|
||||
// the dead-load-elimination block and leave stale ZP
|
||||
// stores in the stream.
|
||||
self.emit_trailing_markers();
|
||||
|
||||
// Snapshot the fixed-bank instruction stream before we
|
||||
// start emitting the banked function bodies into their own
|
||||
// streams. Programs without any banked functions skip the
|
||||
|
|
@ -1229,10 +1238,13 @@ impl<'a> IrCodeGen<'a> {
|
|||
}
|
||||
IrOp::Mul(d, a, b) => {
|
||||
// Software multiply: multiplicand in A, multiplier in $02.
|
||||
// Drop the `__mul_used` marker so the linker knows to
|
||||
// Flag the `__mul_used` marker so the linker knows to
|
||||
// link the `__multiply` subroutine in — programs that
|
||||
// don't multiply skip ~50 bytes of runtime.
|
||||
self.emit_mul_marker();
|
||||
// don't multiply skip ~50 bytes of runtime. The label
|
||||
// itself is emitted at the end of generate() so it
|
||||
// doesn't disturb peephole-sensitive instruction
|
||||
// sequences here.
|
||||
self.mul_used = true;
|
||||
self.load_temp(*a);
|
||||
self.emit(PHA, AM::Implied); // Save for __multiply contract
|
||||
let b_addr = self.temp_addr(*b);
|
||||
|
|
@ -1279,10 +1291,11 @@ impl<'a> IrCodeGen<'a> {
|
|||
IrOp::Div(d, a, b) => {
|
||||
// Software divide: dividend in A, divisor in $02.
|
||||
// `__divide` returns quotient in A and leaves
|
||||
// remainder in ZP $03. Emit the `__div_used` marker
|
||||
// remainder in ZP $03. Flag the `__div_used` marker
|
||||
// so the linker links the `__divide` subroutine in;
|
||||
// programs that don't divide skip ~70 bytes.
|
||||
self.emit_div_marker();
|
||||
// programs that don't divide skip ~70 bytes. The
|
||||
// label is emitted at the end of generate().
|
||||
self.div_used = true;
|
||||
self.load_temp(*a);
|
||||
self.emit(PHA, AM::Implied);
|
||||
let b_addr = self.temp_addr(*b);
|
||||
|
|
@ -1297,7 +1310,7 @@ impl<'a> IrCodeGen<'a> {
|
|||
// of ZP $03 afterwards. Same `__div_used` marker as
|
||||
// `IrOp::Div` — modulo doesn't have a separate
|
||||
// runtime routine.
|
||||
self.emit_div_marker();
|
||||
self.div_used = true;
|
||||
self.load_temp(*a);
|
||||
self.emit(PHA, AM::Implied);
|
||||
let b_addr = self.temp_addr(*b);
|
||||
|
|
@ -1421,14 +1434,16 @@ impl<'a> IrCodeGen<'a> {
|
|||
y,
|
||||
frame,
|
||||
} => {
|
||||
// Drop the `__oam_used` marker so the linker knows
|
||||
// to emit the OAM DMA + shadow-init + default-sprite
|
||||
// Flag the `__oam_used` marker so the linker knows
|
||||
// to emit the OAM DMA + shadow-init + default-palette
|
||||
// machinery. Programs that never `draw` skip all of
|
||||
// those paths — no DMA cycles per NMI, no $FE OAM
|
||||
// shadow fill, no built-in smiley reserved in CHR,
|
||||
// and the default palette is suppressed too when the
|
||||
// program has no other visual output.
|
||||
self.emit_oam_marker();
|
||||
// program has no other visual output. The label is
|
||||
// emitted at the end of generate() so it doesn't
|
||||
// split a peephole block here.
|
||||
self.oam_used = true;
|
||||
// Runtime OAM-cursor-based draw. Each frame handler
|
||||
// resets `ZP_OAM_CURSOR` to 0 after the OAM clear; a
|
||||
// `draw` loads the cursor into Y, writes the four
|
||||
|
|
@ -1476,12 +1491,12 @@ impl<'a> IrCodeGen<'a> {
|
|||
// the marker off and tile 0 becomes user-available
|
||||
// as a blank (all-$00) background tile.
|
||||
if let Some(f) = frame {
|
||||
self.emit_default_sprite_marker();
|
||||
self.default_sprite_used = true;
|
||||
self.load_temp(*f);
|
||||
} else if let Some(&tile) = self.sprite_tiles.get(sprite_name) {
|
||||
self.emit(LDA, AM::Immediate(tile));
|
||||
} else {
|
||||
self.emit_default_sprite_marker();
|
||||
self.default_sprite_used = true;
|
||||
self.emit(LDA, AM::Immediate(0));
|
||||
}
|
||||
self.emit(STA, AM::AbsoluteY(0x0201));
|
||||
|
|
@ -1504,15 +1519,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
|
||||
// Flag 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`.
|
||||
// Labels are emitted at the end of generate().
|
||||
if *player == 1 {
|
||||
self.emit_p2_input_marker();
|
||||
self.p2_input_used = true;
|
||||
} else {
|
||||
self.emit_p1_input_marker();
|
||||
self.p1_input_used = true;
|
||||
}
|
||||
// $01 = P1 input byte, $08 = P2 input byte
|
||||
let addr = if *player == 1 { 0x08 } else { 0x01 };
|
||||
|
|
@ -2112,78 +2128,34 @@ impl<'a> IrCodeGen<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Emit the `__mul_used` marker label at most once per program.
|
||||
/// The linker uses this to decide whether to link in the
|
||||
/// `gen_multiply` subroutine. Programs that never multiply (or
|
||||
/// that only multiply by constant powers of two — those are
|
||||
/// strength-reduced to shifts) skip the runtime entirely.
|
||||
fn emit_mul_marker(&mut self) {
|
||||
if !self.mul_used {
|
||||
/// Emit the marker labels for flags that got set somewhere
|
||||
/// during `generate()`'s IR walk. Called once, at the end of
|
||||
/// `generate()`, so the labels land after every instruction
|
||||
/// that could have set a flag — never splitting a
|
||||
/// peephole-sensitive block at the flag's call site.
|
||||
///
|
||||
/// Each label is a zero-byte pseudo-op. The linker looks them
|
||||
/// up by name via `has_label` and turns on the gated runtime
|
||||
/// feature. Programs that never set a given flag emit nothing
|
||||
/// for it — no label, no lookup hit, no gated code.
|
||||
fn emit_trailing_markers(&mut self) {
|
||||
if self.mul_used {
|
||||
self.emit_label("__mul_used");
|
||||
self.mul_used = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit the `__div_used` marker label at most once per program.
|
||||
/// The linker uses this to decide whether to link in the
|
||||
/// `gen_divide` subroutine. Both `IrOp::Div` and `IrOp::Mod`
|
||||
/// trigger this marker because modulo reuses the divide
|
||||
/// routine. Programs whose divide/modulo operations all use
|
||||
/// constant power-of-two divisors have already been rewritten
|
||||
/// to shifts / masks by the optimizer and never reach here.
|
||||
fn emit_div_marker(&mut self) {
|
||||
if !self.div_used {
|
||||
if self.div_used {
|
||||
self.emit_label("__div_used");
|
||||
self.div_used = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit the `__oam_used` marker label at most once per program.
|
||||
/// Triggered by `IrOp::DrawSprite`. The linker drops the OAM
|
||||
/// DMA + shadow-init + default-palette paths when this marker
|
||||
/// is absent, shaving cycles out of every NMI and bytes out of
|
||||
/// PRG for programs that don't draw sprites.
|
||||
fn emit_oam_marker(&mut self) {
|
||||
if !self.oam_used {
|
||||
if self.oam_used {
|
||||
self.emit_label("__oam_used");
|
||||
self.oam_used = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit the `__default_sprite_used` marker label at most once
|
||||
/// per program. Triggered by `IrOp::DrawSprite` when the sprite
|
||||
/// name doesn't resolve to a user declaration (fall-back to
|
||||
/// tile 0) or when a runtime `frame:` override could land on
|
||||
/// tile 0. The linker uses this to decide whether to copy the
|
||||
/// built-in smiley into CHR tile 0; programs whose draws all
|
||||
/// resolve to declared sprites with static frames skip the
|
||||
/// emit and get tile 0 back as a blank background tile.
|
||||
fn emit_default_sprite_marker(&mut self) {
|
||||
if !self.default_sprite_used {
|
||||
if self.default_sprite_used {
|
||||
self.emit_label("__default_sprite_used");
|
||||
self.default_sprite_used = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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 {
|
||||
if self.p2_input_used {
|
||||
self.emit_label("__p2_input_used");
|
||||
self.p2_input_used = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue