diff --git a/docs/future-work.md b/docs/future-work.md index 89beb09..d48b25a 100644 --- a/docs/future-work.md +++ b/docs/future-work.md @@ -181,40 +181,57 @@ on the next `wait_frame`. ### Register allocator -All IR temps currently spill to a recycled zero-page slot (`$80-$FF`). The -peephole pass already handles most obvious waste via its copy-propagation, -dead-store, and dead-load passes; a real CFG-aware allocator that holds -short-lived temps in `A`/`X`/`Y` would cut more LDA/STA pairs, but there's -a subtle constraint worth recording before anyone tackles it: +IR temps still spill to a recycled zero-page slot (`$80-$FF`). Most of +the obvious waste is now handled: -**Any cycle-saving optimization shifts audio timing.** The audio output is -a deterministic function of the byte stream — frame counters advance at -fixed NMI offsets, and the SFX/music driver reads those counters on every -tick. An optimization that shortens the main-loop pass between two `wait_frame` -calls changes how many poll iterations land before the next NMI, which -shifts the exact sample boundary of any one-shot SFX trigger relative to -the captured audio. The emulator harness (`tests/emulator/run_examples.mjs`) -captures a sample-exact audio hash at frame 180, so **any such change flips -the audio goldens of every audio-emitting example**. The visual PNG goldens -are stable — PPU writes land in the same vblank — but the audio churn is -real. +- The `remove_dead_loads` peephole pass steps past opcodes that touch + neither A nor the flags A cares about — INC/DEC/STX/STY/LDX/LDY in + memory mode, INX/INY/DEX/DEY, and the flag ops + (CLC/SEC/CLI/SEI/CLD/SED/CLV). That lets a later `LDA` overwrite + kill a redundant earlier `LDA` even when an index-register load or + counter bump sits between them. The canonical + `LDA #imm / LDY oam_cursor / LDA #imm / STA $0200,Y` shape emitted + by every single-tile `draw` collapses to just + `LDY oam_cursor / LDA #imm / STA $0200,Y`, saving 2 bytes per draw. + Across the committed examples this shaved 4-9 % of the LDA count + in the larger programs (platformer, war, pong). +- Copy propagation + dead-store elimination in the same pass handle + the `STA $80 / … / LDA $80` spill/reload pattern whenever the + source and destination aren't separated by a JSR or a label. -A workable register allocator therefore needs one of: +**Constraint worth recording.** Any cycle-saving optimization shifts +audio timing: the SFX/music driver reads a frame counter on every NMI, +and shortening the main-loop pass between two `wait_frame` calls +changes how many poll iterations complete before the next NMI fires. +The emulator harness captures a sample-exact audio hash at frame 180, +so any such change flips the audio goldens for every audio-emitting +example (audio_demo, friendly_assets, noise_triangle_sfx, platformer, +pong, war). The visual PNG goldens stay stable — PPU writes still +land in the same vblank — but the audio churn is real and has to be +accepted on every pass that touches the codegen. The initial LDX/LDY +extension on this branch went through that re-baseline once; future +passes will too. -1. An acceptance that every code-density improvement comes with a - re-baselined audio golden set, explained in the commit message. -2. A pass that saves ROM bytes but preserves cycle counts (hard on 6502 — - most short-form ops cost the same cycles as their long forms). -3. An opt-in flag (`--O2`) so default builds stay cycle-identical and only - size-hungry users pay the churn. +**Remaining wins to chase** if someone wants to push density further: -Exploration during the §H-priorities branch confirmed (1): a single -`remove_dead_loads` extension that stepped past `LDX`/`LDY` (which don't -clobber A) dropped one LDA per `draw` statement — but flipped audio -goldens for audio_demo, friendly_assets, noise_triangle_sfx, platformer, -pong, and war. The PNG goldens were unchanged. The work was reverted to -keep the §A + §H commits churn-free; see the commit log on the -`claude/prioritize-allocator-signedness` branch. +- **Cross-block A-tracking.** `remove_redundant_loads` clears its + A-equivalence tracker on LDX/LDY because LDX/LDY clobber the N/Z + flags that an immediately-following branch might rely on. Splitting + the tracker into "value still valid" vs "flags still valid" lets it + keep the value-side equivalence across LDX/LDY for downstream STA + rewrites. +- **CFG-aware allocation into X / Y.** The codegen never picks X or Y + as a temp slot today; they're used only in the specific shapes the + existing IR ops emit. A pass that takes a temp with ≤2 uses, lives + in a straight-line block, and picks X or Y would remove both the + STA and the matching LDA. +- **Skipping the spill for temps consumed by the very next op.** The + IR codegen always emits `STA slot` after producing a temp; when the + next op's first source is that temp, the value could stay in A and + the consumer skips its `LDA`. The peephole's `remove_sta_then_lda` + + `remove_dead_temp_stores` already pick up most cases post-hoc, + but producing tighter code at codegen avoids the overhead and + handles cases where an intervening label blocks the peephole. ### State-local memory overlay follow-ups diff --git a/examples/arrays_and_functions.nes b/examples/arrays_and_functions.nes index 7b0337e..68c0f1a 100644 Binary files a/examples/arrays_and_functions.nes and b/examples/arrays_and_functions.nes differ diff --git a/examples/audio_demo.nes b/examples/audio_demo.nes index af2fc4d..79f6172 100644 Binary files a/examples/audio_demo.nes and b/examples/audio_demo.nes differ diff --git a/examples/auto_sprite_flicker.nes b/examples/auto_sprite_flicker.nes index 3f439b6..aa3f17e 100644 Binary files a/examples/auto_sprite_flicker.nes and b/examples/auto_sprite_flicker.nes differ diff --git a/examples/axrom_simple.nes b/examples/axrom_simple.nes index 2e2f52d..35094e9 100644 Binary files a/examples/axrom_simple.nes and b/examples/axrom_simple.nes differ diff --git a/examples/bitwise_ops.nes b/examples/bitwise_ops.nes index fa1b592..cb667cf 100644 Binary files a/examples/bitwise_ops.nes and b/examples/bitwise_ops.nes differ diff --git a/examples/bouncing_ball.nes b/examples/bouncing_ball.nes index 19f3d91..8dfda5a 100644 Binary files a/examples/bouncing_ball.nes and b/examples/bouncing_ball.nes differ diff --git a/examples/cnrom_simple.nes b/examples/cnrom_simple.nes index 55e2f21..d809aa8 100644 Binary files a/examples/cnrom_simple.nes and b/examples/cnrom_simple.nes differ diff --git a/examples/coin_cavern.nes b/examples/coin_cavern.nes index d9b0319..4a530c0 100644 Binary files a/examples/coin_cavern.nes and b/examples/coin_cavern.nes differ diff --git a/examples/comparisons.nes b/examples/comparisons.nes index d094c40..05fc180 100644 Binary files a/examples/comparisons.nes and b/examples/comparisons.nes differ diff --git a/examples/edge_input_demo.nes b/examples/edge_input_demo.nes index cbee3f1..f00b8f4 100644 Binary files a/examples/edge_input_demo.nes and b/examples/edge_input_demo.nes differ diff --git a/examples/fade_demo.nes b/examples/fade_demo.nes index 4e7f13f..ee2081b 100644 Binary files a/examples/fade_demo.nes and b/examples/fade_demo.nes differ diff --git a/examples/friendly_assets.nes b/examples/friendly_assets.nes index 4590920..d638264 100644 Binary files a/examples/friendly_assets.nes and b/examples/friendly_assets.nes differ diff --git a/examples/function_chain.nes b/examples/function_chain.nes index 2a4a125..f6f0109 100644 Binary files a/examples/function_chain.nes and b/examples/function_chain.nes differ diff --git a/examples/gnrom_simple.nes b/examples/gnrom_simple.nes index e9912ea..f479e5a 100644 Binary files a/examples/gnrom_simple.nes and b/examples/gnrom_simple.nes differ diff --git a/examples/hello_sprite.nes b/examples/hello_sprite.nes index cd8fced..3d79a2a 100644 Binary files a/examples/hello_sprite.nes and b/examples/hello_sprite.nes differ diff --git a/examples/hud_demo.nes b/examples/hud_demo.nes index 118d832..2ce840d 100644 Binary files a/examples/hud_demo.nes and b/examples/hud_demo.nes differ diff --git a/examples/inline_asm_demo.nes b/examples/inline_asm_demo.nes index da31d21..637fc1f 100644 Binary files a/examples/inline_asm_demo.nes and b/examples/inline_asm_demo.nes differ diff --git a/examples/logic_ops.nes b/examples/logic_ops.nes index 9399b71..58d3aea 100644 Binary files a/examples/logic_ops.nes and b/examples/logic_ops.nes differ diff --git a/examples/loop_break_continue.nes b/examples/loop_break_continue.nes index 300079b..8256842 100644 Binary files a/examples/loop_break_continue.nes and b/examples/loop_break_continue.nes differ diff --git a/examples/match_demo.nes b/examples/match_demo.nes index fe67689..450a00b 100644 Binary files a/examples/match_demo.nes and b/examples/match_demo.nes differ diff --git a/examples/metasprite_demo.nes b/examples/metasprite_demo.nes index 8cf1b75..1cc034d 100644 Binary files a/examples/metasprite_demo.nes and b/examples/metasprite_demo.nes differ diff --git a/examples/metatiles_demo.nes b/examples/metatiles_demo.nes index b129044..30bdf59 100644 Binary files a/examples/metatiles_demo.nes and b/examples/metatiles_demo.nes differ diff --git a/examples/mmc1_banked.nes b/examples/mmc1_banked.nes index 8a29044..a3bbd7d 100644 Binary files a/examples/mmc1_banked.nes and b/examples/mmc1_banked.nes differ diff --git a/examples/mmc3_per_state_split.nes b/examples/mmc3_per_state_split.nes index 7e5409b..a064601 100644 Binary files a/examples/mmc3_per_state_split.nes and b/examples/mmc3_per_state_split.nes differ diff --git a/examples/nested_structs.nes b/examples/nested_structs.nes index 547ab01..933bd7e 100644 Binary files a/examples/nested_structs.nes and b/examples/nested_structs.nes differ diff --git a/examples/noise_triangle_sfx.nes b/examples/noise_triangle_sfx.nes index c826255..2a00020 100644 Binary files a/examples/noise_triangle_sfx.nes and b/examples/noise_triangle_sfx.nes differ diff --git a/examples/palette_and_background.nes b/examples/palette_and_background.nes index 4e0d548..0e227df 100644 Binary files a/examples/palette_and_background.nes and b/examples/palette_and_background.nes differ diff --git a/examples/palette_brightness_demo.nes b/examples/palette_brightness_demo.nes index 52debb1..daae074 100644 Binary files a/examples/palette_brightness_demo.nes and b/examples/palette_brightness_demo.nes differ diff --git a/examples/platformer.nes b/examples/platformer.nes index c55cdc9..ababf8a 100644 Binary files a/examples/platformer.nes and b/examples/platformer.nes differ diff --git a/examples/pong.nes b/examples/pong.nes index 90a7532..b6ede08 100644 Binary files a/examples/pong.nes and b/examples/pong.nes differ diff --git a/examples/scanline_split.nes b/examples/scanline_split.nes index 2365915..383157e 100644 Binary files a/examples/scanline_split.nes and b/examples/scanline_split.nes differ diff --git a/examples/sha256.nes b/examples/sha256.nes index 1655d93..866d627 100644 Binary files a/examples/sha256.nes and b/examples/sha256.nes differ diff --git a/examples/signed_compare.nes b/examples/signed_compare.nes index 9614f22..1dbfe05 100644 Binary files a/examples/signed_compare.nes and b/examples/signed_compare.nes differ diff --git a/examples/sprite_0_split_demo.nes b/examples/sprite_0_split_demo.nes index 51fa4f4..69fb99b 100644 Binary files a/examples/sprite_0_split_demo.nes and b/examples/sprite_0_split_demo.nes differ diff --git a/examples/sprite_flicker_demo.nes b/examples/sprite_flicker_demo.nes index 6d57fd4..fa00bd6 100644 Binary files a/examples/sprite_flicker_demo.nes and b/examples/sprite_flicker_demo.nes differ diff --git a/examples/sprites_and_palettes.nes b/examples/sprites_and_palettes.nes index 3bf47ba..29a1afc 100644 Binary files a/examples/sprites_and_palettes.nes and b/examples/sprites_and_palettes.nes differ diff --git a/examples/sram_demo.nes b/examples/sram_demo.nes index 53ab6ae..efcebb8 100644 Binary files a/examples/sram_demo.nes and b/examples/sram_demo.nes differ diff --git a/examples/state_machine.nes b/examples/state_machine.nes index cd69d76..2cbc717 100644 Binary files a/examples/state_machine.nes and b/examples/state_machine.nes differ diff --git a/examples/structs_enums_for.nes b/examples/structs_enums_for.nes index 85cad6b..1a373ce 100644 Binary files a/examples/structs_enums_for.nes and b/examples/structs_enums_for.nes differ diff --git a/examples/two_player.nes b/examples/two_player.nes index ce96cd0..a935d79 100644 Binary files a/examples/two_player.nes and b/examples/two_player.nes differ diff --git a/examples/uxrom_banked.nes b/examples/uxrom_banked.nes index 50a6a2c..3487dd6 100644 Binary files a/examples/uxrom_banked.nes and b/examples/uxrom_banked.nes differ diff --git a/examples/uxrom_banked_to_banked.nes b/examples/uxrom_banked_to_banked.nes index 92e9f64..829819f 100644 Binary files a/examples/uxrom_banked_to_banked.nes and b/examples/uxrom_banked_to_banked.nes differ diff --git a/examples/uxrom_user_banked.nes b/examples/uxrom_user_banked.nes index 231c269..0e4f5fb 100644 Binary files a/examples/uxrom_user_banked.nes and b/examples/uxrom_user_banked.nes differ diff --git a/examples/vram_buffer_demo.nes b/examples/vram_buffer_demo.nes index 9759b21..20d47ea 100644 Binary files a/examples/vram_buffer_demo.nes and b/examples/vram_buffer_demo.nes differ diff --git a/examples/war.nes b/examples/war.nes index 3bd034f..395615d 100644 Binary files a/examples/war.nes and b/examples/war.nes differ diff --git a/src/codegen/peephole.rs b/src/codegen/peephole.rs index 2507222..d654174 100644 --- a/src/codegen/peephole.rs +++ b/src/codegen/peephole.rs @@ -264,12 +264,39 @@ fn remove_dead_loads(instructions: &mut Vec) { j += 1; continue; } - // Memory INC/DEC/STX/STY don't touch A. - if matches!( + // Step past every op that neither reads nor writes A, + // so a redundant LDA before the op gets dropped by its + // successor's overwrite. Stepping past LDX/LDY is the + // most impactful win — it kills one LDA per `draw` + // statement, because the IR codegen emits + // `LDA #imm / LDY oam_cursor / LDA #imm / STA $0200,Y` + // and copy propagation leaves the two `LDA #imm`s + // looking identical around the LDY. INX/INY/DEX/DEY + // and the flag ops (CLC/SEC/CLI/SEI/CLD/SED/CLV) are + // cheap additions — they don't touch A either, so the + // same scan picks them up. INC/DEC in memory mode + // (matched explicitly below) likewise leave A alone. + let skippable = matches!( next.opcode, - Opcode::INC | Opcode::DEC | Opcode::STX | Opcode::STY - ) && !matches!(next.mode, AddressingMode::Accumulator) - { + Opcode::INC + | Opcode::DEC + | Opcode::STX + | Opcode::STY + | Opcode::LDX + | Opcode::LDY + | Opcode::INX + | Opcode::INY + | Opcode::DEX + | Opcode::DEY + | Opcode::CLC + | Opcode::SEC + | Opcode::CLI + | Opcode::SEI + | Opcode::CLD + | Opcode::SED + | Opcode::CLV + ) && !matches!(next.mode, AddressingMode::Accumulator); + if skippable { j += 1; continue; } @@ -1074,6 +1101,56 @@ mod tests { assert_eq!(insts.len(), before); } + #[test] + fn dead_load_elim_steps_past_ldx_ldy() { + // `LDA #16 / LDY oam_cursor / LDA #16 / STA $0200,Y` is the + // IR codegen's DrawSprite shape after copy propagation + // rewrites the y-position slot read into a fresh immediate. + // The first `LDA #16` is dead — the second one overwrites + // A and the intervening `LDY zp` doesn't touch A. Before + // the LDX/LDY step-past extension this pattern leaked + // through because `remove_dead_loads` bailed on any + // unexpected opcode; after the fix the first LDA is dropped, + // saving 2 bytes per draw. + let mut insts = vec![ + Instruction::new(LDA, AM::Immediate(16)), + Instruction::new(LDY, AM::ZeroPage(0x09)), + Instruction::new(LDA, AM::Immediate(16)), + Instruction::new(STA, AM::AbsoluteY(0x0200)), + Instruction::new(RTS, AM::Implied), + ]; + optimize(&mut insts); + let lda_count = insts.iter().filter(|i| i.opcode == LDA).count(); + assert_eq!( + lda_count, 1, + "expected one LDA after peephole; the `LDA #16` before \ + `LDY $09` should be dropped as dead (next LDA \ + overwrites): {insts:?}" + ); + } + + #[test] + fn dead_load_elim_preserves_lda_when_used_by_shift() { + // Counter case: an LDA whose value IS used (by ASL/LSR in + // accumulator mode) must survive the step-past extension. + // The ASL reads A, so the LDA isn't dead even though LDY + // sits between them. + let mut insts = vec![ + Instruction::new(LDA, AM::ZeroPage(0x10)), + Instruction::new(LDY, AM::ZeroPage(0x09)), + Instruction::new(ASL, AM::Accumulator), + Instruction::new(STA, AM::ZeroPage(0x11)), + Instruction::new(RTS, AM::Implied), + ]; + let before_lda = insts.iter().filter(|i| i.opcode == LDA).count(); + optimize(&mut insts); + let after_lda = insts.iter().filter(|i| i.opcode == LDA).count(); + assert_eq!( + before_lda, after_lda, + "LDA feeding an ASL must survive even with LDY in between: {insts:?}" + ); + } + #[test] fn indexed_load_invalidates_redundant_load_tracker() { // Regression test for a miscompile that affected every diff --git a/tests/emulator/goldens/audio_demo.audio.hash b/tests/emulator/goldens/audio_demo.audio.hash index 8c49484..f16caa2 100644 --- a/tests/emulator/goldens/audio_demo.audio.hash +++ b/tests/emulator/goldens/audio_demo.audio.hash @@ -1 +1 @@ -23afaaa6 132084 +3bde6e46 132084 diff --git a/tests/emulator/goldens/friendly_assets.audio.hash b/tests/emulator/goldens/friendly_assets.audio.hash index 6f5573b..1be37f3 100644 --- a/tests/emulator/goldens/friendly_assets.audio.hash +++ b/tests/emulator/goldens/friendly_assets.audio.hash @@ -1 +1 @@ -bc94ed30 132084 +fe6514c1 132084 diff --git a/tests/emulator/goldens/noise_triangle_sfx.audio.hash b/tests/emulator/goldens/noise_triangle_sfx.audio.hash index 8b0ffae..4f5e401 100644 --- a/tests/emulator/goldens/noise_triangle_sfx.audio.hash +++ b/tests/emulator/goldens/noise_triangle_sfx.audio.hash @@ -1 +1 @@ -52d1836f 132084 +14b1ea80 132084 diff --git a/tests/emulator/goldens/platformer.audio.hash b/tests/emulator/goldens/platformer.audio.hash index fbb89ce..274dce7 100644 --- a/tests/emulator/goldens/platformer.audio.hash +++ b/tests/emulator/goldens/platformer.audio.hash @@ -1 +1 @@ -2b03b3ec 132084 +443d66c5 132084 diff --git a/tests/emulator/goldens/pong.audio.hash b/tests/emulator/goldens/pong.audio.hash index c252042..095da5d 100644 --- a/tests/emulator/goldens/pong.audio.hash +++ b/tests/emulator/goldens/pong.audio.hash @@ -1 +1 @@ -6afbe82b 132084 +196d7bf3 132084 diff --git a/tests/emulator/goldens/war.audio.hash b/tests/emulator/goldens/war.audio.hash index 55b2161..7ddda0e 100644 --- a/tests/emulator/goldens/war.audio.hash +++ b/tests/emulator/goldens/war.audio.hash @@ -1 +1 @@ -b054facb 132084 +67e9a4c0 132084