diff --git a/examples/audio_demo.nes b/examples/audio_demo.nes index cecd4e0..1041513 100644 Binary files a/examples/audio_demo.nes and b/examples/audio_demo.nes differ diff --git a/examples/bitwise_ops.nes b/examples/bitwise_ops.nes index 9f57276..7921e14 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 6970f4a..b769195 100644 Binary files a/examples/bouncing_ball.nes and b/examples/bouncing_ball.nes differ diff --git a/examples/coin_cavern.nes b/examples/coin_cavern.nes index 6ae24af..c80a9f3 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 f98c42d..887c722 100644 Binary files a/examples/comparisons.nes and b/examples/comparisons.nes differ diff --git a/examples/friendly_assets.nes b/examples/friendly_assets.nes index fe80777..25a0ebb 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 84a89d7..aa6a2d2 100644 Binary files a/examples/function_chain.nes and b/examples/function_chain.nes differ diff --git a/examples/hello_sprite.nes b/examples/hello_sprite.nes index 03e695c..d6f2d25 100644 Binary files a/examples/hello_sprite.nes and b/examples/hello_sprite.nes differ diff --git a/examples/inline_asm_demo.nes b/examples/inline_asm_demo.nes index fd89a0e..2b8a9cb 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 d2f40bd..9055961 100644 Binary files a/examples/logic_ops.nes and b/examples/logic_ops.nes differ diff --git a/examples/match_demo.nes b/examples/match_demo.nes index 7bdc158..741a551 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 6fcad6f..0baf3d4 100644 Binary files a/examples/metasprite_demo.nes and b/examples/metasprite_demo.nes differ diff --git a/examples/mmc1_banked.nes b/examples/mmc1_banked.nes index 4c90e57..5cfe88a 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 88a2425..a776605 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 5fa3196..c63f0c4 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 e4fdf68..e7b884b 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 cd24ddc..df8defb 100644 Binary files a/examples/palette_and_background.nes and b/examples/palette_and_background.nes differ diff --git a/examples/platformer.nes b/examples/platformer.nes index ab75555..d26e6a2 100644 Binary files a/examples/platformer.nes and b/examples/platformer.nes differ diff --git a/examples/pong.nes b/examples/pong.nes index 24e4069..ce53f01 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 d39912d..03a34f2 100644 Binary files a/examples/scanline_split.nes and b/examples/scanline_split.nes differ diff --git a/examples/sfx_pitch_envelope.nes b/examples/sfx_pitch_envelope.nes index 8989f4d..8f646da 100644 Binary files a/examples/sfx_pitch_envelope.nes and b/examples/sfx_pitch_envelope.nes differ diff --git a/examples/sprite_flicker_demo.nes b/examples/sprite_flicker_demo.nes index 2481636..51954f3 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 59e1e2f..d6450df 100644 Binary files a/examples/sprites_and_palettes.nes and b/examples/sprites_and_palettes.nes differ diff --git a/examples/state_machine.nes b/examples/state_machine.nes index 948a84a..b8cbf27 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 e9df64d..155d8f3 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 db4c774..167dbd6 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 15844ed..f6178bc 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 fb01da7..7fb8de8 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 8c98c50..43efe01 100644 Binary files a/examples/uxrom_user_banked.nes and b/examples/uxrom_user_banked.nes differ diff --git a/examples/war.nes b/examples/war.nes index 5020e68..2e58168 100644 Binary files a/examples/war.nes and b/examples/war.nes differ diff --git a/src/codegen/ir_codegen.rs b/src/codegen/ir_codegen.rs index 516ca0c..3327865 100644 --- a/src/codegen/ir_codegen.rs +++ b/src/codegen/ir_codegen.rs @@ -178,6 +178,14 @@ pub struct IrCodeGen<'a> { /// masks; runtime divide only survives for non-constant or /// non-power-of-two divisors. div_used: bool, + /// Set to true the first time we lower an `IrOp::DrawSprite`. + /// Drives the `__oam_used` marker label. The linker reads this + /// to decide whether to splice the OAM DMA into NMI, emit the + /// `$FE`-fill OAM shadow init inside `gen_init`'s RAM clear, + /// reserve the default smiley tile at CHR tile 0, and keep the + /// default palette load. Programs that never `draw` pay zero + /// for any of those paths. + oam_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 @@ -357,6 +365,7 @@ impl<'a> IrCodeGen<'a> { ppu_update_used: false, mul_used: false, div_used: false, + oam_used: false, source_locs: Vec::new(), next_source_loc: 0, emit_source_locs: false, @@ -1387,6 +1396,14 @@ impl<'a> IrCodeGen<'a> { y, frame, } => { + // Drop the `__oam_used` marker so the linker knows + // to emit the OAM DMA + shadow-init + default-sprite + // 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(); // 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 @@ -2073,6 +2090,18 @@ impl<'a> IrCodeGen<'a> { } } + /// Emit the `__oam_used` marker label at most once per program. + /// Triggered by `IrOp::DrawSprite`. The linker drops the OAM + /// DMA + shadow-init + default-sprite-tile paths when this + /// marker is absent, shaving cycles out of every NMI and bytes + /// out of PRG/CHR for programs that don't draw sprites. + fn emit_oam_marker(&mut self) { + if !self.oam_used { + self.emit_label("__oam_used"); + self.oam_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 diff --git a/src/linker/mod.rs b/src/linker/mod.rs index 2a482c6..261c577 100644 --- a/src/linker/mod.rs +++ b/src/linker/mod.rs @@ -411,10 +411,28 @@ impl Linker { total_banks, )); + // Whether the program produces any visual output. True if + // the user declared a palette / sprite / background, or if + // user code contains the `__oam_used` marker (emitted by + // `IrOp::DrawSprite`). A purely audio- or compute-only + // program is happy to leave the PPU fully silent — no + // palette load, no rendering enable, no default-sprite + // smiley in CHR — so we gate the reset-time palette + // machinery on this flag. See the sprite-chr / OAM-DMA + // gates below for the other places it cascades. + let has_visual_output = !palettes.is_empty() + || !sprites.is_empty() + || !backgrounds.is_empty() + || has_label(user_code, "__oam_used"); + // Load the initial palette. If the program declared any // `palette` blocks, use the first one; otherwise fall back // to the built-in default palette so sprites show up in a - // reasonable colour scheme without any user setup. + // reasonable colour scheme without any user setup. Skipped + // entirely for programs with no visual output — those leave + // palette RAM in its power-on state (undefined on real + // hardware, zeros under jsnes / Mesen) which is fine since + // nothing gets rendered. // // IMPORTANT: `gen_init` leaves rendering fully disabled so // these $2006/$2007 writes are safe. We re-enable rendering @@ -425,16 +443,17 @@ impl Linker { // about the first 72 bytes of a 1024-byte nametable load. if let Some(first_palette) = palettes.first() { all_instructions.extend(runtime::gen_initial_palette_load(&first_palette.label())); - } else { - // No user palette: fall back to a sensible built-in - // palette so sprites show up in a reasonable colour - // scheme without any user setup. Uses the same indirect - // loop loader as the user-palette path (reads a 32-byte - // blob through a ZP pointer) — ~20 bytes of code plus a - // 32-byte data block that gets spliced in below, versus - // the ~170 bytes the old inline-stores path cost. The - // data block lives alongside the user palette blobs so - // the label resolves in the normal assembly pass. + } else if has_visual_output { + // No user palette but the program does render something + // — fall back to a sensible built-in palette so the + // sprites show up in a reasonable colour scheme without + // any user setup. Uses the same indirect loop loader as + // the user-palette path (reads a 32-byte blob through a + // ZP pointer) — ~20 bytes of code plus a 32-byte data + // block that gets spliced in below, versus the ~170 + // bytes the old inline-stores path cost. The data block + // lives alongside the user palette blobs so the label + // resolves in the normal assembly pass. all_instructions.extend(runtime::gen_initial_palette_load(DEFAULT_PALETTE_LABEL)); } @@ -453,8 +472,13 @@ impl Linker { // rendering on. Programs with a declared background get // bg+sprites ($1E); programs without get sprites only ($10) // to preserve the pre-fix behaviour of example ROMs that - // rely on a hidden nametable. - all_instructions.extend(runtime::gen_enable_rendering(has_user_background)); + // rely on a hidden nametable. Programs with no visual + // output at all leave PPU_MASK at $00 from `gen_init` — + // the PPU stays silent, saves 4 bytes and avoids exposing + // an undefined palette on real hardware. + if has_visual_output { + all_instructions.extend(runtime::gen_enable_rendering(has_user_background)); + } // User code (var init + main loop) all_instructions.extend(user_code.iter().cloned()); @@ -571,13 +595,13 @@ impl Linker { for pal in palettes { all_instructions.extend(runtime::gen_data_block(&pal.label(), pal.colors.to_vec())); } - // When the program has no user palette, splice the built-in - // default palette blob under `__default_palette` so the - // reset-time loop loader above can resolve it. Programs - // that declare a palette fall through the user path and - // skip this entirely — saves 32 bytes of ROM data when the - // default is unused. - if palettes.is_empty() { + // When the program needs the built-in default palette (i.e. + // it produces visual output but declared no palette of its + // own), splice the 32-byte blob under `__default_palette` + // so the reset-time loop loader above can resolve it. + // Programs that declare a palette OR have no visual output + // skip this entirely. + if palettes.is_empty() && has_visual_output { all_instructions.extend(runtime::gen_data_block( DEFAULT_PALETTE_LABEL, DEFAULT_PALETTE.to_vec(), diff --git a/src/linker/tests.rs b/src/linker/tests.rs index 61f7524..47f430c 100644 --- a/src/linker/tests.rs +++ b/src/linker/tests.rs @@ -634,14 +634,17 @@ fn link_banked_chr_rom_survives_with_switchable_banks() { #[test] fn default_palette_blob_present_when_no_user_palette() { - // With no user palette, the linker emits the shared reset-time - // loop loader (which writes twice to `$2006` and loops writing - // through `$2007`) and splices a 32-byte `__default_palette` - // data block into PRG. The end-to-end ROM should contain the - // default palette bytes verbatim at some offset in the fixed - // bank. + // With no user palette but some visual output (the IR codegen + // drops an `__oam_used` marker whenever it lowers a `draw`), + // the linker emits the shared reset-time loop loader and + // splices a 32-byte `__default_palette` data block into PRG. + // The end-to-end ROM should contain the default palette bytes + // verbatim at some offset in the fixed bank. let linker = Linker::new(Mirroring::Horizontal); - let user_code = vec![Instruction::new(NOP, AM::Label("__ir_main_loop".into()))]; + let user_code = vec![ + Instruction::new(NOP, AM::Label("__oam_used".into())), + Instruction::new(NOP, AM::Label("__ir_main_loop".into())), + ]; let rom = linker.link(&user_code); // The first four bytes of DEFAULT_PALETTE are {0x0F, 0x00, 0x10, @@ -654,6 +657,27 @@ fn default_palette_blob_present_when_no_user_palette() { assert!(found, "default palette bytes should appear in PRG"); } +#[test] +fn default_palette_suppressed_when_no_visual_output() { + // An audio- or compute-only program (no palette declared, no + // sprites, no backgrounds, no `draw`) shouldn't pay for the + // built-in default palette: no `__default_palette` label in + // the symbol table, and the 32 bytes of palette data must not + // appear anywhere in PRG. + let linker = Linker::new(Mirroring::Horizontal); + let user_code = vec![Instruction::new(NOP, AM::Label("__ir_main_loop".into()))]; + let result = linker.link_banked_with_ppu_detailed(&user_code, &[], &[], &[], &[], &[], &[]); + assert!( + !result.labels.contains_key("__default_palette"), + "default palette label must not be defined without visual output" + ); + let prg = &result.rom[16..16 + 16_384]; + assert!( + !prg.windows(4).any(|w| w == [0x0F, 0x00, 0x10, 0x20]), + "default palette bytes must not appear in PRG when suppressed" + ); +} + #[test] fn no_default_palette_blob_when_user_palette_present() { // A program that declares its own palette should suppress the diff --git a/tests/emulator/goldens/audio_demo.audio.hash b/tests/emulator/goldens/audio_demo.audio.hash index 7fbc913..9ed40e1 100644 --- a/tests/emulator/goldens/audio_demo.audio.hash +++ b/tests/emulator/goldens/audio_demo.audio.hash @@ -1 +1 @@ -e064fdf0 132084 +dceef796 132084 diff --git a/tests/emulator/goldens/comparisons.png b/tests/emulator/goldens/comparisons.png index 9096daf..e0dd216 100644 Binary files a/tests/emulator/goldens/comparisons.png and b/tests/emulator/goldens/comparisons.png differ diff --git a/tests/emulator/goldens/friendly_assets.audio.hash b/tests/emulator/goldens/friendly_assets.audio.hash index 0dca2ac..c671279 100644 --- a/tests/emulator/goldens/friendly_assets.audio.hash +++ b/tests/emulator/goldens/friendly_assets.audio.hash @@ -1 +1 @@ -68089bdb 132084 +a9a829f7 132084 diff --git a/tests/emulator/goldens/noise_triangle_sfx.audio.hash b/tests/emulator/goldens/noise_triangle_sfx.audio.hash index ee351f8..435c42c 100644 --- a/tests/emulator/goldens/noise_triangle_sfx.audio.hash +++ b/tests/emulator/goldens/noise_triangle_sfx.audio.hash @@ -1 +1 @@ -7b0caf36 132084 +be86c2a2 132084 diff --git a/tests/emulator/goldens/platformer.audio.hash b/tests/emulator/goldens/platformer.audio.hash index 359b2c8..4bbfe8e 100644 --- a/tests/emulator/goldens/platformer.audio.hash +++ b/tests/emulator/goldens/platformer.audio.hash @@ -1 +1 @@ -5cc6d0f5 132084 +c9c2333d 132084 diff --git a/tests/emulator/goldens/pong.audio.hash b/tests/emulator/goldens/pong.audio.hash index 778c04f..1145110 100644 --- a/tests/emulator/goldens/pong.audio.hash +++ b/tests/emulator/goldens/pong.audio.hash @@ -1 +1 @@ -714f679e 132084 +b7d5b53f 132084 diff --git a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash index a4eb4f4..8d0c60d 100644 --- a/tests/emulator/goldens/sfx_pitch_envelope.audio.hash +++ b/tests/emulator/goldens/sfx_pitch_envelope.audio.hash @@ -1 +1 @@ -c1ba0463 132084 +9c881965 132084 diff --git a/tests/emulator/goldens/sfx_pitch_envelope.png b/tests/emulator/goldens/sfx_pitch_envelope.png index ab3d666..2842948 100644 Binary files a/tests/emulator/goldens/sfx_pitch_envelope.png and b/tests/emulator/goldens/sfx_pitch_envelope.png differ diff --git a/tests/emulator/goldens/war.audio.hash b/tests/emulator/goldens/war.audio.hash index 7dc1df6..03cc9de 100644 --- a/tests/emulator/goldens/war.audio.hash +++ b/tests/emulator/goldens/war.audio.hash @@ -1 +1 @@ -575adcb6 132084 +b7b5b9a0 132084