mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 00:45:38 +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
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.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
examples/war.nes
BIN
examples/war.nes
Binary file not shown.
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
b52a0b1e 132084
|
||||
23afaaa6 132084
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 1,023 B After Width: | Height: | Size: 1.1 KiB |
|
|
@ -1 +1 @@
|
|||
9387feb3 132084
|
||||
bc94ed30 132084
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
a05d1284 132084
|
||||
52d1836f 132084
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 43 KiB |
|
|
@ -1 +1 @@
|
|||
83c0eee4 132084
|
||||
ea23d9c4 132084
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
b7d5b53f 132084
|
||||
555a22e1 132084
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ff926f72 132084
|
||||
8142ffb7 132084
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
51b560a7 132084
|
||||
e3805766 132084
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue