mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 00:45: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:
parent
033d399565
commit
37974611ae
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.
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
49c411ce 132084
|
||||
e064fdf0 132084
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
5146a336 132084
|
||||
7b0caf36 132084
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
eec31111 132084
|
||||
c1ba0463 132084
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue