mirror of
https://github.com/imjasonh/nescript
synced 2026-07-09 01:16:12 +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:
parent
033d399565
commit
37974611ae
32 changed files with 74 additions and 46 deletions
|
|
@ -157,7 +157,13 @@ const DEFAULT_SPRITE_CHR: [u8; 16] = [
|
|||
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] = [
|
||||
// Background palettes
|
||||
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
|
||||
];
|
||||
|
||||
/// 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 {
|
||||
pub fn new(mirroring: Mirroring) -> Self {
|
||||
Self {
|
||||
|
|
@ -414,7 +426,16 @@ impl Linker {
|
|||
if let Some(first_palette) = palettes.first() {
|
||||
all_instructions.extend(runtime::gen_initial_palette_load(&first_palette.label()));
|
||||
} 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.
|
||||
|
|
@ -550,6 +571,18 @@ 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() {
|
||||
all_instructions.extend(runtime::gen_data_block(
|
||||
DEFAULT_PALETTE_LABEL,
|
||||
DEFAULT_PALETTE.to_vec(),
|
||||
));
|
||||
}
|
||||
for bg in backgrounds {
|
||||
all_instructions.extend(runtime::gen_data_block(
|
||||
&bg.tiles_label(),
|
||||
|
|
@ -753,24 +786,4 @@ impl Linker {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue