1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-12 02:30:57 +00:00

linker: shrink default palette load from inline stores to loop

The reset-time "no user palette" path was emitting 32 unrolled
`LDA #imm / STA $2007` pairs (~170 bytes) to write the built-in
palette. Replace it with the same indirect-loop loader the
user-palette path already uses (runtime::gen_initial_palette_load),
with the 32-byte default palette spliced into PRG under a
`__default_palette` data block. Net saving is ~120 bytes — ~20
bytes of code + 32 bytes of data vs ~170 bytes of unrolled stores.

Delete `Linker::gen_palette_load` (dead after the refactor) and its
unit test. Replace with two tests covering the observable
behaviour: the default palette bytes appear in PRG when no user
palette is declared, and the `__default_palette` label is
suppressed when the user does declare a palette.

Audio goldens flip again for audio_demo, noise_triangle_sfx, and
sfx_pitch_envelope. These are the three audio examples that don't
declare their own palette — shrinking the default-palette load
shifts their audio tick's absolute address by ~120 bytes, which
changes branch page-crossing timing and therefore the exact APU
register write sample offsets. Same class of drift as the
mul/divide gating commit.

https://claude.ai/code/session_016kM6P7PukktBDqTZexrrAN
This commit is contained in:
Claude 2026-04-16 13:07:45 +00:00
parent 033d399565
commit 37974611ae
No known key found for this signature in database
32 changed files with 74 additions and 46 deletions

View file

@ -633,30 +633,45 @@ fn link_banked_chr_rom_survives_with_switchable_banks() {
}
#[test]
fn palette_load_writes_to_ppu() {
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.
let linker = Linker::new(Mirroring::Horizontal);
let palette_insts = linker.gen_palette_load();
let user_code = vec![Instruction::new(NOP, AM::Label("__ir_main_loop".into()))];
let rom = linker.link(&user_code);
// Should write to PPU address register ($2006) twice
let ppu_addr_writes: Vec<_> = palette_insts
.iter()
.filter(|i| i.opcode == STA && i.mode == AM::Absolute(0x2006))
.collect();
assert_eq!(
ppu_addr_writes.len(),
2,
"should set PPU address (high and low bytes)"
);
// The first four bytes of DEFAULT_PALETTE are {0x0F, 0x00, 0x10,
// 0x20}; they should appear verbatim in the PRG portion of the
// iNES file (bytes 16..16+16_384). We look for that 4-byte
// sequence rather than matching the full 32 bytes so this stays
// robust against minor palette tweaks.
let prg = &rom[16..16 + 16_384];
let found = prg.windows(4).any(|w| w == [0x0F, 0x00, 0x10, 0x20]);
assert!(found, "default palette bytes should appear in PRG");
}
// Should write 32 palette bytes to $2007
let ppu_data_writes: Vec<_> = palette_insts
.iter()
.filter(|i| i.opcode == STA && i.mode == AM::Absolute(0x2007))
.collect();
assert_eq!(
ppu_data_writes.len(),
32,
"should write all 32 palette bytes"
#[test]
fn no_default_palette_blob_when_user_palette_present() {
// A program that declares its own palette should suppress the
// built-in fallback entirely — the `__default_palette` label
// never gets emitted, and the assembler's label table doesn't
// contain it.
use crate::assets::PaletteData;
let linker = Linker::new(Mirroring::Horizontal);
let user_code = vec![Instruction::new(NOP, AM::Label("__ir_main_loop".into()))];
let user_pal = PaletteData {
name: "Menu".into(),
colors: [0x0F; 32],
};
let result =
linker.link_banked_with_ppu_detailed(&user_code, &[], &[], &[], &[user_pal], &[], &[]);
assert!(
!result.labels.contains_key("__default_palette"),
"default palette must be suppressed when user palette is present"
);
}