1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00

assets: PNG-sourced palettes and nametables, plus --memory-map PRG reporting

Implements three items from docs/future-work.md's
"PNG-sourced palette and nametable assets" section:

- `palette Name @palette("file.png")` — the parser accepts a PNG
  shortcut form; the asset resolver decodes the image via the
  new `png_to_palette` helper, mapping each pixel's RGB to the
  nearest NES master-palette index and building a 32-byte blob
  that enforces the universal-first-byte convention (same as
  the grouped-form parser). Errors cleanly on missing files or
  more than 16 unique colours.

- `background Name @nametable("file.png")` — the parser accepts
  a PNG shortcut form; the resolver decodes a 256×240 image into
  a 960-byte tile-index table (deduplicating up to 256 unique
  8×8 tiles) plus a 64-byte attribute table (bucketed by
  average quadrant brightness). CHR data is not yet generated
  automatically — callers still need to provide matching CHR
  via the existing sprite / `@chr(...)` pipeline; the
  limitation is documented on the `png_to_nametable` helper
  and can be lifted in a follow-up.

- `--memory-map` now prints a "PRG ROM data blobs" section
  listing each palette (32 B) and background (960 + 64 B)
  under its linker-assigned label, plus a grand total. The
  memory-map code is factored into `write_memory_map` which
  takes a writer so unit tests can drive it against a
  `Vec<u8>`. Memory-map printing moved to after the link step
  so palette/background CPU addresses are available.

Call-site changes: `resolve_palettes` and `resolve_backgrounds`
now take a `source_dir` path and return `Result<_, String>`
because PNG decoding can fail. Updated the CLI driver,
benches/compile.rs, and every integration-test compile helper.

All 23 committed examples rebuild byte-identical; 525 lib
tests + 72 integration tests + 3 bin tests pass; clippy clean.
This commit is contained in:
Claude 2026-04-14 03:01:32 +00:00
parent b575921c8e
commit 8610aecdac
No known key found for this signature in database
10 changed files with 1095 additions and 69 deletions

View file

@ -1104,8 +1104,10 @@ fn compile_banked(source: &str) -> Vec<u8> {
.expect("sprite resolution should succeed");
let sfx = assets::resolve_sfx(&program).expect("sfx resolution should succeed");
let music = assets::resolve_music(&program).expect("music resolution should succeed");
let palettes = assets::resolve_palettes(&program);
let backgrounds = assets::resolve_backgrounds(&program);
let palettes = assets::resolve_palettes(&program, Path::new("."))
.expect("palette resolution should succeed");
let backgrounds = assets::resolve_backgrounds(&program, Path::new("."))
.expect("background resolution should succeed");
let mut codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program)
.with_sprites(&sprites)
@ -1847,6 +1849,98 @@ fn e2e_banked_chr_rom_is_preserved() {
assert_ne!(&rom[chr_start..chr_start + 16], &[0u8; 16]);
}
#[test]
fn e2e_png_palette_source_compiles_and_splices_bytes_into_prg() {
// Full pipeline: parse `palette Main @palette("fixture.png")`,
// resolve the PNG into a 32-byte blob via the asset resolver,
// and verify the resulting bytes land in PRG ROM. We write a
// 2×1 test fixture (pure black + pure red) to a tempdir so
// the test is self-contained and deterministic.
use image::{Rgb, RgbImage};
use nescript::codegen::IrCodeGen;
use nescript::linker::LinkedRom;
let dir = std::env::temp_dir();
let png_path = dir.join("nescript_e2e_palette.png");
let mut img = RgbImage::new(2, 1);
img.put_pixel(0, 0, Rgb([0, 0, 0]));
img.put_pixel(1, 0, Rgb([248, 0, 0]));
img.save(&png_path).unwrap();
let source = r#"
game "PngPalette" { mapper: NROM }
palette Main @palette("nescript_e2e_palette.png")
on frame { wait_frame }
start Main
"#;
let (program, diags) = nescript::parser::parse(source);
assert!(diags.is_empty(), "unexpected parse errors: {diags:?}");
let program = program.expect("parse should succeed");
let analysis = analyzer::analyze(&program);
assert!(analysis.diagnostics.iter().all(|d| !d.is_error()));
// Resolve with the tempdir as the source dir so the
// relative PNG path lands on the fixture we just wrote.
let palettes =
assets::resolve_palettes(&program, &dir).expect("palette resolution should succeed");
let backgrounds = assets::resolve_backgrounds(&program, &dir).expect("bg ok");
assert_eq!(palettes.len(), 1);
assert_eq!(palettes[0].name, "Main");
// First two bytes should map via `nearest_nes_color` to black
// and a red-ish index. We re-run the mapper so the test
// doesn't hard-code the NES palette table.
let e_black = assets::nearest_nes_color(0, 0, 0);
let e_red = assets::nearest_nes_color(248, 0, 0);
assert_eq!(palettes[0].colors[0], e_black);
assert_eq!(palettes[0].colors[1], e_red);
// Every sub-palette first byte equals the universal.
for slot in 0..8 {
assert_eq!(palettes[0].colors[slot * 4], e_black);
}
// Link the program and verify the 32-byte blob shows up in PRG
// ROM at the linker-assigned label.
let sprites = assets::resolve_sprites(&program, Path::new(".")).unwrap();
let sfx = assets::resolve_sfx(&program).unwrap();
let music = assets::resolve_music(&program).unwrap();
let mut ir_program = nescript::ir::lower(&program, &analysis);
nescript::optimizer::optimize(&mut ir_program);
let mut codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program)
.with_sprites(&sprites)
.with_audio(&sfx, &music);
let mut instructions = codegen.generate(&ir_program);
nescript::codegen::peephole::optimize(&mut instructions);
let linker = Linker::with_mapper(program.game.mirroring, program.game.mapper);
let link: LinkedRom = linker.link_banked_with_ppu_detailed(
&instructions,
&sprites,
&sfx,
&music,
&palettes,
&backgrounds,
&[],
);
let pal_label = palettes[0].label();
let pal_addr = link
.labels
.get(&pal_label)
.copied()
.expect("palette label should be emitted");
// Translate the CPU address into a byte offset inside the
// fixed bank. NROM: the fixed bank starts at file offset 16
// (past the iNES header) and maps to CPU $C000-$FFFF.
let rom_offset = link.fixed_bank_file_offset + (pal_addr as usize - 0xC000);
let prg_bytes = &link.rom[rom_offset..rom_offset + 32];
assert_eq!(
prg_bytes, &palettes[0].colors,
"PRG ROM should contain the decoded palette blob verbatim"
);
let _ = std::fs::remove_file(&png_path);
}
/// Same as `compile_banked` but lets the caller toggle whether the IR
/// optimizer runs. Used to cover the `--no-opt` CLI flag: compiling
/// with the optimizer disabled must still produce a valid iNES ROM.
@ -1874,8 +1968,10 @@ fn compile_banked_with_opts(source: &str, optimize: bool) -> Vec<u8> {
.expect("sprite resolution should succeed");
let sfx = assets::resolve_sfx(&program).expect("sfx resolution should succeed");
let music = assets::resolve_music(&program).expect("music resolution should succeed");
let palettes = assets::resolve_palettes(&program);
let backgrounds = assets::resolve_backgrounds(&program);
let palettes = assets::resolve_palettes(&program, Path::new("."))
.expect("palette resolution should succeed");
let backgrounds = assets::resolve_backgrounds(&program, Path::new("."))
.expect("background resolution should succeed");
let mut codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program)
.with_sprites(&sprites)
@ -1979,8 +2075,10 @@ fn compile_with_debug_artifacts(source: &str, debug: bool) -> (Vec<u8>, String,
.expect("sprite resolution should succeed");
let sfx = assets::resolve_sfx(&program).expect("sfx resolution should succeed");
let music = assets::resolve_music(&program).expect("music resolution should succeed");
let palettes = assets::resolve_palettes(&program);
let backgrounds = assets::resolve_backgrounds(&program);
let palettes = assets::resolve_palettes(&program, Path::new("."))
.expect("palette resolution should succeed");
let backgrounds = assets::resolve_backgrounds(&program, Path::new("."))
.expect("background resolution should succeed");
let mut codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program)
.with_sprites(&sprites)
@ -2141,8 +2239,10 @@ fn debug_build_emits_bounds_check_halt_routine() {
let sprites = assets::resolve_sprites(&program, Path::new(".")).unwrap();
let sfx = assets::resolve_sfx(&program).unwrap();
let music = assets::resolve_music(&program).unwrap();
let palettes = assets::resolve_palettes(&program);
let backgrounds = assets::resolve_backgrounds(&program);
let palettes = assets::resolve_palettes(&program, Path::new("."))
.expect("palette resolution should succeed");
let backgrounds = assets::resolve_backgrounds(&program, Path::new("."))
.expect("background resolution should succeed");
let mut cg_debug = IrCodeGen::new(&analysis.var_allocations, &ir_program)
.with_sprites(&sprites)