mirror of
https://github.com/imjasonh/nescript
synced 2026-07-09 01:16:12 +00:00
palette/background: first-class declarations with reset-time load and runtime swaps
Re-adds `palette Name { colors: [...] }` and
`background Name { tiles: [...], attributes: [...] }` as first-class
declarations, plus `set_palette Name` and `load_background Name`
statements for runtime swaps. Unlike the previous iteration that
quietly no-op'd, this one is fully wired through the pipeline and
its behavior is pinned by both unit tests and an emulator golden.
Pipeline:
- Lexer: re-adds `palette`, `background`, `set_palette`,
`load_background` keywords and tokenizes them.
- AST: `PaletteDecl` (name + 1..=32 colour bytes) and `BackgroundDecl`
(name + 0..=960 tile bytes + 0..=64 attribute bytes) live in
`Program`. `Statement::SetPalette` and `Statement::LoadBackground`
name-reference these declarations.
- Parser: `palette Name { colors: [...] }` / `background Name
{ tiles: [...], attributes: [...] }` blocks and their statement
forms parse via the existing byte-array helper.
- Analyzer: validates colour indices ($00-$3F), palette length
(<=32), nametable length (<=960), attribute length (<=64), and
duplicate decl names. `set_palette` / `load_background` targets
must reference a declared name (E0502 otherwise). When a program
declares palette or background, the analyzer bumps the user
zero-page allocator's starting address from `$10` to `$18` to
reserve `$11-$17` for the runtime update handshake — programs
that don't use the feature keep the old layout so their emulator
goldens stay byte-exact.
- Assets: `PaletteData` and `BackgroundData` resolve declarations
into zero-padded fixed-size blobs (32 / 960 / 64 bytes) and
expose `label()` / `tiles_label()` / `attrs_label()` for codegen
to reference.
- IR: new `IrOp::SetPalette(String)` and
`IrOp::LoadBackground(String)`; lowering forwards the names
verbatim.
- Codegen: `gen_set_palette` writes the palette label pointer into
ZP `$12/$13` and ORs bit 0 into the update flags at `$11`;
`gen_load_background` does the same for tile/attribute pointers
at `$14/$15/$16/$17` with bit 1. Both emit a `__ppu_update_used`
marker so the linker splices in the NMI apply helper only when
the feature is actually used.
- Runtime: `gen_initial_palette_load` and
`gen_initial_background_load` write the first declared
palette/background at reset time (before rendering is enabled,
where PPU writes are safe). `gen_nmi(has_ppu_updates)` takes a
new flag; when true it splices `gen_ppu_update_apply` at the top
of the NMI body, which checks the `$11` flags byte and copies
pending palette / nametable data to `$3F00` / `$2000` inside
vblank. All helpers use only ZP $02/$03 as scratch at reset time
and never clobber ZP slots live across NMI.
- Linker: new `link_banked_with_ppu` takes slice of `PaletteData` /
`BackgroundData`; splices each blob as a labelled data block in
PRG ROM, picks the first-declared as the reset-time load target,
enables background rendering automatically when a background is
declared, and threads `has_ppu_updates` into `gen_nmi`. Old
`link_banked` remains as a thin wrapper for callers without
palette/background data so existing tests don't shift.
Tests:
- Lexer: tokenization of the 4 new keywords (single added test case).
- Parser: 5 new tests for `palette` / `background` decls with and
without attributes, plus `set_palette` / `load_background`
statements.
- Analyzer: 9 new tests covering acceptance of declared
palettes/backgrounds, E0502 for unknown names, E0201 for
out-of-range NES colors and oversized blobs, E0501 for duplicate
names, and the zero-page-layout guard (palette/bg decls bump ZP
start; no decls keeps it at $10).
- Resolver: 3 new tests for zero-padding, truncation of oversized
decls, and label derivation.
- IR: 2 new lowering tests for `set_palette` and `load_background`.
- Integration: 5 new tests — blob contents spliced verbatim into
PRG, `STA $12` / `STA $14` emitted by set_palette /
load_background codegen, and a regression guard that programs
without palette/background still land user vars at $10.
- Emulator: new `examples/palette_and_background.ne` driven by a
frame counter that toggles between `CoolBlues` / `WarmReds` and
`TitleScreen` / `StageOne` every 90 frames. Golden PNG and audio
hash checked in under `tests/emulator/goldens/` and verified via
`node run_examples.mjs` — rendered image shows the blue
`CoolBlues` palette with the nametable populated from
`TitleScreen`.
Docs:
- `README.md` adds the feature to the headline list and the example
table.
- `docs/language-guide.md` restores the palette/background sections
with the full 32-byte layout table and `set_palette` /
`load_background` statement references.
- `docs/future-work.md` replaces the "removed as dead code" entry
with the remaining gaps (PNG-sourced palette and nametable
assets, cross-vblank large background updates, memory-map
reporting).
- `spec.md` restores the grammar productions and usage examples.
- `examples/README.md` lists the new demo.
All 497 unit + integration tests pass. Clippy clean. All 21
emulator goldens match after the update pass.
https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
This commit is contained in:
parent
fdb1ec7c91
commit
d98c7f3d82
29 changed files with 1757 additions and 46 deletions
|
|
@ -60,6 +60,26 @@ pub const ZP_SFX_COUNTER: u8 = 0x0A;
|
|||
/// advances to the next (pitch, duration) pair.
|
||||
pub const ZP_MUSIC_COUNTER: u8 = 0x0B;
|
||||
|
||||
// ── PPU update handshake ──
|
||||
//
|
||||
// When a program declares `palette` or `background` blocks the
|
||||
// analyzer reserves `$11-$17` as runtime state for the vblank-safe
|
||||
// update path. User code sets these from inside a frame handler
|
||||
// (via `set_palette` / `load_background`), and the NMI handler
|
||||
// applies any pending update while the PPU is blanked, then
|
||||
// clears the flags.
|
||||
|
||||
/// Bitfield of pending PPU updates.
|
||||
/// bit 0 = 1 → palette at `ZP_PENDING_PALETTE_*` is pending
|
||||
/// bit 1 = 1 → background at `ZP_PENDING_BG_TILES_*` / `_ATTRS_*` is pending
|
||||
pub const ZP_PPU_UPDATE_FLAGS: u8 = 0x11;
|
||||
pub const ZP_PENDING_PALETTE_LO: u8 = 0x12;
|
||||
pub const ZP_PENDING_PALETTE_HI: u8 = 0x13;
|
||||
pub const ZP_PENDING_BG_TILES_LO: u8 = 0x14;
|
||||
pub const ZP_PENDING_BG_TILES_HI: u8 = 0x15;
|
||||
pub const ZP_PENDING_BG_ATTRS_LO: u8 = 0x16;
|
||||
pub const ZP_PENDING_BG_ATTRS_HI: u8 = 0x17;
|
||||
|
||||
/// Generate the NES hardware initialization sequence.
|
||||
/// This runs at RESET and sets up the hardware before user code.
|
||||
pub fn gen_init() -> Vec<Instruction> {
|
||||
|
|
@ -151,7 +171,13 @@ pub fn gen_init() -> Vec<Instruction> {
|
|||
|
||||
/// Generate the NMI handler.
|
||||
/// Called every vblank by the NES hardware.
|
||||
pub fn gen_nmi() -> Vec<Instruction> {
|
||||
///
|
||||
/// `has_ppu_updates` controls whether the handler runs the
|
||||
/// palette / nametable update helper. When false, the handler skips
|
||||
/// that block entirely so programs that never call `set_palette` /
|
||||
/// `load_background` pay zero cycles or bytes for the feature.
|
||||
#[must_use]
|
||||
pub fn gen_nmi(has_ppu_updates: bool) -> Vec<Instruction> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
// Save registers
|
||||
|
|
@ -167,6 +193,14 @@ pub fn gen_nmi() -> Vec<Instruction> {
|
|||
out.push(Instruction::new(LDA, AM::Immediate(0x02)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(OAM_DMA)));
|
||||
|
||||
// PPU updates: check the flags byte, apply any pending palette
|
||||
// or background write. Runs inside vblank where $2006/$2007
|
||||
// writes are safe. Gated on `has_ppu_updates` so programs that
|
||||
// never touch palette or background decls skip this entirely.
|
||||
if has_ppu_updates {
|
||||
out.extend(gen_ppu_update_apply());
|
||||
}
|
||||
|
||||
// Read controller 1
|
||||
out.push(Instruction::new(LDA, AM::Immediate(0x01)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(JOY1)));
|
||||
|
|
@ -212,6 +246,265 @@ pub fn gen_irq() -> Vec<Instruction> {
|
|||
vec![Instruction::implied(RTI)]
|
||||
}
|
||||
|
||||
/// Generate the in-NMI PPU update helper. Checks
|
||||
/// [`ZP_PPU_UPDATE_FLAGS`] and, if any bit is set, copies the
|
||||
/// corresponding blob from PRG ROM to PPU RAM via `$2006`/`$2007`.
|
||||
/// Safe because the NMI fires at the start of vblank, giving
|
||||
/// ~2273 CPU cycles of safe PPU write time — enough for a full
|
||||
/// palette (32 bytes, ~200 cycles) plus a full nametable
|
||||
/// (1024 bytes, ~6500 cycles; this doesn't fit in a single frame
|
||||
/// so big updates should be staged by the caller).
|
||||
///
|
||||
/// For simplicity and to keep the NMI bounded, this helper writes
|
||||
/// the palette first and the nametable second, and only one of
|
||||
/// each can be pending at a time. If a nametable write is larger
|
||||
/// than vblank allows the program is responsible for either
|
||||
/// keeping rendering disabled or splitting the update.
|
||||
///
|
||||
/// The helper clears the pending flag only for updates it actually
|
||||
/// applied, so if a program ever queues a palette and a nametable
|
||||
/// in the same frame both land on the same NMI.
|
||||
fn gen_ppu_update_apply() -> Vec<Instruction> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
// Read flags. If zero, jump straight to the done label.
|
||||
out.push(Instruction::new(LDA, AM::ZeroPage(ZP_PPU_UPDATE_FLAGS)));
|
||||
out.push(Instruction::new(
|
||||
BEQ,
|
||||
AM::LabelRelative("__ppu_update_done".into()),
|
||||
));
|
||||
|
||||
// ── palette update (bit 0) ────────────────────────────────
|
||||
// Check bit 0; if clear, skip to background.
|
||||
out.push(Instruction::new(AND, AM::Immediate(0x01)));
|
||||
out.push(Instruction::new(
|
||||
BEQ,
|
||||
AM::LabelRelative("__ppu_update_no_palette".into()),
|
||||
));
|
||||
// Set PPU addr to $3F00.
|
||||
out.push(Instruction::new(LDA, AM::Absolute(PPU_STATUS)));
|
||||
out.push(Instruction::new(LDA, AM::Immediate(0x3F)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2006)));
|
||||
out.push(Instruction::new(LDA, AM::Immediate(0x00)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2006)));
|
||||
// Loop: write 32 bytes via `LDA (zp),Y` indirect-indexed from
|
||||
// the pending palette pointer at $12/$13.
|
||||
out.push(Instruction::new(LDY, AM::Immediate(0x00)));
|
||||
out.push(Instruction::new(NOP, AM::Label("__ppu_pal_loop".into())));
|
||||
out.push(Instruction::new(LDA, AM::IndirectY(ZP_PENDING_PALETTE_LO)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2007)));
|
||||
out.push(Instruction::implied(INY));
|
||||
out.push(Instruction::new(CPY, AM::Immediate(32)));
|
||||
out.push(Instruction::new(
|
||||
BNE,
|
||||
AM::LabelRelative("__ppu_pal_loop".into()),
|
||||
));
|
||||
|
||||
out.push(Instruction::new(
|
||||
NOP,
|
||||
AM::Label("__ppu_update_no_palette".into()),
|
||||
));
|
||||
|
||||
// ── background update (bit 1) ────────────────────────────
|
||||
out.push(Instruction::new(LDA, AM::ZeroPage(ZP_PPU_UPDATE_FLAGS)));
|
||||
out.push(Instruction::new(AND, AM::Immediate(0x02)));
|
||||
out.push(Instruction::new(
|
||||
BEQ,
|
||||
AM::LabelRelative("__ppu_update_no_bg".into()),
|
||||
));
|
||||
// Nametable 0 starts at $2000.
|
||||
out.push(Instruction::new(LDA, AM::Absolute(PPU_STATUS)));
|
||||
out.push(Instruction::new(LDA, AM::Immediate(0x20)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2006)));
|
||||
out.push(Instruction::new(LDA, AM::Immediate(0x00)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2006)));
|
||||
// Write 960 bytes as 4 loops of 240 (so Y fits in u8) — simpler
|
||||
// to write as an outer X counter across 4 × 240-byte pages.
|
||||
// X = 4 pages to go
|
||||
out.push(Instruction::new(LDX, AM::Immediate(4)));
|
||||
out.push(Instruction::new(NOP, AM::Label("__ppu_bg_outer".into())));
|
||||
out.push(Instruction::new(LDY, AM::Immediate(0x00)));
|
||||
out.push(Instruction::new(NOP, AM::Label("__ppu_bg_inner".into())));
|
||||
out.push(Instruction::new(LDA, AM::IndirectY(ZP_PENDING_BG_TILES_LO)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2007)));
|
||||
out.push(Instruction::implied(INY));
|
||||
out.push(Instruction::new(CPY, AM::Immediate(240)));
|
||||
out.push(Instruction::new(
|
||||
BNE,
|
||||
AM::LabelRelative("__ppu_bg_inner".into()),
|
||||
));
|
||||
// After each 240-byte block, bump the pointer by 240 so the
|
||||
// next block reads from the following chunk of the blob.
|
||||
out.push(Instruction::new(LDA, AM::ZeroPage(ZP_PENDING_BG_TILES_LO)));
|
||||
out.push(Instruction::new(CLC, AM::Implied));
|
||||
out.push(Instruction::new(ADC, AM::Immediate(240)));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(ZP_PENDING_BG_TILES_LO)));
|
||||
out.push(Instruction::new(LDA, AM::ZeroPage(ZP_PENDING_BG_TILES_HI)));
|
||||
out.push(Instruction::new(ADC, AM::Immediate(0)));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(ZP_PENDING_BG_TILES_HI)));
|
||||
out.push(Instruction::implied(DEX));
|
||||
out.push(Instruction::new(
|
||||
BNE,
|
||||
AM::LabelRelative("__ppu_bg_outer".into()),
|
||||
));
|
||||
// Now write the 64-byte attribute table (at $23C0 — right after
|
||||
// the nametable we just wrote). The PPU auto-increment sits at
|
||||
// $23C0 already since we wrote exactly 960 bytes after $2000.
|
||||
out.push(Instruction::new(LDY, AM::Immediate(0x00)));
|
||||
out.push(Instruction::new(
|
||||
NOP,
|
||||
AM::Label("__ppu_bg_attr_loop".into()),
|
||||
));
|
||||
out.push(Instruction::new(LDA, AM::IndirectY(ZP_PENDING_BG_ATTRS_LO)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2007)));
|
||||
out.push(Instruction::implied(INY));
|
||||
out.push(Instruction::new(CPY, AM::Immediate(64)));
|
||||
out.push(Instruction::new(
|
||||
BNE,
|
||||
AM::LabelRelative("__ppu_bg_attr_loop".into()),
|
||||
));
|
||||
out.push(Instruction::new(
|
||||
NOP,
|
||||
AM::Label("__ppu_update_no_bg".into()),
|
||||
));
|
||||
|
||||
// Clear all pending flags. Programs re-queue every frame if
|
||||
// they want repeating updates.
|
||||
out.push(Instruction::new(LDA, AM::Immediate(0x00)));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(ZP_PPU_UPDATE_FLAGS)));
|
||||
|
||||
out.push(Instruction::new(NOP, AM::Label("__ppu_update_done".into())));
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Emit a reset-time write of a 32-byte palette blob (referenced
|
||||
/// by label) to PPU `$3F00-$3F1F`. Rendering must be disabled
|
||||
/// when this runs (it is, between `gen_init` and the linker's PPU
|
||||
/// rendering-enable step). Uses the scratch ZP slots `$02/$03` to
|
||||
/// hold the indirect pointer — safe because nothing else runs
|
||||
/// between `gen_init` and user code.
|
||||
#[must_use]
|
||||
pub fn gen_initial_palette_load(label: &str) -> Vec<Instruction> {
|
||||
let mut out = Vec::new();
|
||||
out.push(Instruction::new(LDA, AM::Absolute(PPU_STATUS))); // reset latch
|
||||
out.push(Instruction::new(LDA, AM::Immediate(0x3F)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2006)));
|
||||
out.push(Instruction::new(LDA, AM::Immediate(0x00)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2006)));
|
||||
// Stash the palette label into scratch ZP for indirect LDA.
|
||||
out.push(Instruction::new(LDA, AM::SymbolLo(label.to_string())));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(0x02)));
|
||||
out.push(Instruction::new(LDA, AM::SymbolHi(label.to_string())));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(0x03)));
|
||||
out.push(Instruction::new(LDY, AM::Immediate(0x00)));
|
||||
let loop_label = format!("__init_pal_loop_{label}");
|
||||
out.push(Instruction::new(NOP, AM::Label(loop_label.clone())));
|
||||
out.push(Instruction::new(LDA, AM::IndirectY(0x02)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2007)));
|
||||
out.push(Instruction::implied(INY));
|
||||
out.push(Instruction::new(CPY, AM::Immediate(32)));
|
||||
out.push(Instruction::new(BNE, AM::LabelRelative(loop_label)));
|
||||
out
|
||||
}
|
||||
|
||||
/// Emit a reset-time write of a 960-byte nametable + 64-byte
|
||||
/// attribute table blob to nametable 0 (`$2000-$23FF`). Rendering
|
||||
/// must be disabled when this runs. The caller passes the label of
|
||||
/// the tiles blob and the label of the attribute blob separately —
|
||||
/// the linker emits them as adjacent data blocks so they can be
|
||||
/// resolved independently.
|
||||
#[must_use]
|
||||
pub fn gen_initial_background_load(tiles_label: &str, attrs_label: &str) -> Vec<Instruction> {
|
||||
let mut out = Vec::new();
|
||||
out.push(Instruction::new(LDA, AM::Absolute(PPU_STATUS)));
|
||||
out.push(Instruction::new(LDA, AM::Immediate(0x20)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2006)));
|
||||
out.push(Instruction::new(LDA, AM::Immediate(0x00)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2006)));
|
||||
|
||||
// Write 960 bytes of tile data as 4 × 240 using two nested
|
||||
// counters. We stash the outer page index in a scratch ZP slot
|
||||
// because X is too small to index a 960-byte range directly.
|
||||
out.push(Instruction::new(LDX, AM::Immediate(0x00)));
|
||||
let page_loop = format!("__init_bg_page_{tiles_label}");
|
||||
let inner_loop = format!("__init_bg_inner_{tiles_label}");
|
||||
out.push(Instruction::new(NOP, AM::Label(page_loop.clone())));
|
||||
// Per-page offset: X*240. Computed via Y and clamped at 240.
|
||||
out.push(Instruction::new(LDY, AM::Immediate(0x00)));
|
||||
out.push(Instruction::new(NOP, AM::Label(inner_loop.clone())));
|
||||
// Fetch byte at blob[X*240 + Y]. We materialize the effective
|
||||
// absolute address by unrolling 4 separate LDA Absolute,Y
|
||||
// instructions, one per page, dispatched on X.
|
||||
// For simplicity and correctness we take the slower path:
|
||||
// compute (blob + X*240) as a ZP pointer and read via
|
||||
// `LDA (zp),Y`.
|
||||
// ZP scratch at $02/$03 (same slots used by the multiply/divide
|
||||
// contract; gen_init runs before any user code so they're free).
|
||||
out.push(Instruction::new(LDA, AM::SymbolLo(tiles_label.to_string())));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(0x02)));
|
||||
out.push(Instruction::new(LDA, AM::SymbolHi(tiles_label.to_string())));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(0x03)));
|
||||
// Add X*240 to the low byte (high byte carries via ADC).
|
||||
// Actually — to keep this simple, we instead track bytes
|
||||
// remaining as a 16-bit counter and use a generic LDA (ZP),Y
|
||||
// loop. Rewrite the routine as a flat byte-counted loop.
|
||||
// (Undo the per-page setup above by rebuilding the output
|
||||
// vector from scratch.)
|
||||
out.clear();
|
||||
|
||||
out.push(Instruction::new(LDA, AM::Absolute(PPU_STATUS)));
|
||||
out.push(Instruction::new(LDA, AM::Immediate(0x20)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2006)));
|
||||
out.push(Instruction::new(LDA, AM::Immediate(0x00)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2006)));
|
||||
|
||||
// Load tile blob pointer into $02/$03 scratch slots.
|
||||
out.push(Instruction::new(LDA, AM::SymbolLo(tiles_label.to_string())));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(0x02)));
|
||||
out.push(Instruction::new(LDA, AM::SymbolHi(tiles_label.to_string())));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(0x03)));
|
||||
|
||||
// 4 pages × 240 bytes each = 960 bytes total.
|
||||
out.push(Instruction::new(LDX, AM::Immediate(4)));
|
||||
let outer = format!("__init_bg_outer_{tiles_label}");
|
||||
let inner = format!("__init_bg_inner_{tiles_label}");
|
||||
out.push(Instruction::new(NOP, AM::Label(outer.clone())));
|
||||
out.push(Instruction::new(LDY, AM::Immediate(0x00)));
|
||||
out.push(Instruction::new(NOP, AM::Label(inner.clone())));
|
||||
out.push(Instruction::new(LDA, AM::IndirectY(0x02)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2007)));
|
||||
out.push(Instruction::implied(INY));
|
||||
out.push(Instruction::new(CPY, AM::Immediate(240)));
|
||||
out.push(Instruction::new(BNE, AM::LabelRelative(inner)));
|
||||
// Advance pointer by 240.
|
||||
out.push(Instruction::new(LDA, AM::ZeroPage(0x02)));
|
||||
out.push(Instruction::new(CLC, AM::Implied));
|
||||
out.push(Instruction::new(ADC, AM::Immediate(240)));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(0x02)));
|
||||
out.push(Instruction::new(LDA, AM::ZeroPage(0x03)));
|
||||
out.push(Instruction::new(ADC, AM::Immediate(0)));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(0x03)));
|
||||
out.push(Instruction::implied(DEX));
|
||||
out.push(Instruction::new(BNE, AM::LabelRelative(outer)));
|
||||
|
||||
// Now the 64 attribute bytes land at $23C0 — the PPU auto-
|
||||
// increment is already there after the 960 tile writes.
|
||||
out.push(Instruction::new(LDA, AM::SymbolLo(attrs_label.to_string())));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(0x02)));
|
||||
out.push(Instruction::new(LDA, AM::SymbolHi(attrs_label.to_string())));
|
||||
out.push(Instruction::new(STA, AM::ZeroPage(0x03)));
|
||||
out.push(Instruction::new(LDY, AM::Immediate(0x00)));
|
||||
let attr_loop = format!("__init_bg_attr_{attrs_label}");
|
||||
out.push(Instruction::new(NOP, AM::Label(attr_loop.clone())));
|
||||
out.push(Instruction::new(LDA, AM::IndirectY(0x02)));
|
||||
out.push(Instruction::new(STA, AM::Absolute(0x2007)));
|
||||
out.push(Instruction::implied(INY));
|
||||
out.push(Instruction::new(CPY, AM::Immediate(64)));
|
||||
out.push(Instruction::new(BNE, AM::LabelRelative(attr_loop)));
|
||||
out
|
||||
}
|
||||
|
||||
/// Zero-page locations used by multiply/divide routines.
|
||||
const ZP_MUL_OPERAND: u8 = 0x02;
|
||||
const ZP_MUL_RESULT_HI: u8 = 0x03;
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ fn init_assembles_without_error() {
|
|||
|
||||
#[test]
|
||||
fn nmi_saves_and_restores_registers() {
|
||||
let nmi = gen_nmi();
|
||||
let nmi = gen_nmi(false);
|
||||
// First three instructions should push A, X, Y
|
||||
assert_eq!(nmi[0].opcode, PHA);
|
||||
assert_eq!(nmi[1].opcode, TXA);
|
||||
|
|
@ -86,7 +86,7 @@ fn nmi_saves_and_restores_registers() {
|
|||
|
||||
#[test]
|
||||
fn nmi_triggers_oam_dma() {
|
||||
let nmi = gen_nmi();
|
||||
let nmi = gen_nmi(false);
|
||||
let has_dma = nmi
|
||||
.iter()
|
||||
.any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4014));
|
||||
|
|
@ -95,7 +95,7 @@ fn nmi_triggers_oam_dma() {
|
|||
|
||||
#[test]
|
||||
fn nmi_reads_controller() {
|
||||
let nmi = gen_nmi();
|
||||
let nmi = gen_nmi(false);
|
||||
// Should write strobe to $4016
|
||||
let has_strobe = nmi
|
||||
.iter()
|
||||
|
|
@ -105,7 +105,7 @@ fn nmi_reads_controller() {
|
|||
|
||||
#[test]
|
||||
fn nmi_sets_frame_flag() {
|
||||
let nmi = gen_nmi();
|
||||
let nmi = gen_nmi(false);
|
||||
let has_flag = nmi
|
||||
.iter()
|
||||
.any(|i| i.opcode == STA && i.mode == AM::ZeroPage(ZP_FRAME_FLAG));
|
||||
|
|
@ -114,7 +114,7 @@ fn nmi_sets_frame_flag() {
|
|||
|
||||
#[test]
|
||||
fn nmi_assembles_without_error() {
|
||||
let nmi = gen_nmi();
|
||||
let nmi = gen_nmi(false);
|
||||
let result = asm::assemble(&nmi, 0xF000);
|
||||
assert!(!result.bytes.is_empty());
|
||||
assert!(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue