1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +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

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.

View file

@ -157,7 +157,13 @@ const DEFAULT_SPRITE_CHR: [u8; 16] = [
0b0011_1100, 0b0011_1100,
]; ];
/// Default palette data for M1 (writes to PPU $3F00). /// Default palette data for M1 (writes to PPU $3F00). Spliced into
/// PRG under [`DEFAULT_PALETTE_LABEL`] when the program has no
/// user-declared palette, and loaded by
/// [`runtime::gen_initial_palette_load`] via the same indirect-loop
/// path that user palettes use — keeps the reset-time palette
/// loader small (one code path, ~20 bytes) instead of the old
/// 170-byte per-entry unrolled store sequence.
const DEFAULT_PALETTE: [u8; 32] = [ const DEFAULT_PALETTE: [u8; 32] = [
// Background palettes // Background palettes
0x0F, 0x00, 0x10, 0x20, // palette 0 (black, dark gray, light gray, white) 0x0F, 0x00, 0x10, 0x20, // palette 0 (black, dark gray, light gray, white)
@ -171,6 +177,12 @@ const DEFAULT_PALETTE: [u8; 32] = [
0x0F, 0x12, 0x22, 0x32, // sprite palette 3 0x0F, 0x12, 0x22, 0x32, // sprite palette 3
]; ];
/// Label under which [`DEFAULT_PALETTE`] is spliced into PRG when
/// emitted. Prefixed with `__` so it can never collide with a
/// user-declared palette's label, which the asset pipeline prefixes
/// with `__palette_`.
const DEFAULT_PALETTE_LABEL: &str = "__default_palette";
impl Linker { impl Linker {
pub fn new(mirroring: Mirroring) -> Self { pub fn new(mirroring: Mirroring) -> Self {
Self { Self {
@ -414,7 +426,16 @@ impl Linker {
if let Some(first_palette) = palettes.first() { if let Some(first_palette) = palettes.first() {
all_instructions.extend(runtime::gen_initial_palette_load(&first_palette.label())); all_instructions.extend(runtime::gen_initial_palette_load(&first_palette.label()));
} else { } else {
all_instructions.extend(self.gen_palette_load()); // 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.
all_instructions.extend(runtime::gen_initial_palette_load(DEFAULT_PALETTE_LABEL));
} }
// Load the initial background if the program declared any. // Load the initial background if the program declared any.
@ -550,6 +571,18 @@ impl Linker {
for pal in palettes { for pal in palettes {
all_instructions.extend(runtime::gen_data_block(&pal.label(), pal.colors.to_vec())); 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() {
all_instructions.extend(runtime::gen_data_block(
DEFAULT_PALETTE_LABEL,
DEFAULT_PALETTE.to_vec(),
));
}
for bg in backgrounds { for bg in backgrounds {
all_instructions.extend(runtime::gen_data_block( all_instructions.extend(runtime::gen_data_block(
&bg.tiles_label(), &bg.tiles_label(),
@ -753,24 +786,4 @@ impl Linker {
fixed_bank_file_offset, fixed_bank_file_offset,
} }
} }
/// Generate instructions to load the default palette into the PPU.
fn gen_palette_load(&self) -> Vec<Instruction> {
let mut out = Vec::new();
// Set PPU address to $3F00 (palette start)
out.push(Instruction::new(LDA, AM::Absolute(0x2002))); // read PPU status to reset latch
out.push(Instruction::new(LDA, AM::Immediate(0x3F)));
out.push(Instruction::new(STA, AM::Absolute(0x2006))); // PPU addr high byte
out.push(Instruction::new(LDA, AM::Immediate(0x00)));
out.push(Instruction::new(STA, AM::Absolute(0x2006))); // PPU addr low byte
// Write all 32 palette bytes
for &color in &DEFAULT_PALETTE {
out.push(Instruction::new(LDA, AM::Immediate(color)));
out.push(Instruction::new(STA, AM::Absolute(0x2007))); // PPU data
}
out
}
} }

View file

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

View file

@ -1 +1 @@
49c411ce 132084 e064fdf0 132084

View file

@ -1 +1 @@
5146a336 132084 7b0caf36 132084

View file

@ -1 +1 @@
eec31111 132084 c1ba0463 132084