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

compiler: audio driver, u16 arithmetic, multi-scanline, slot recycling

Five language features and optimizations from the planned-work backlog:

- **Minimal audio driver**: `play`/`start_music`/`stop_music` now generate
  APU pulse-1/pulse-2 writes from a builtin SFX/music name table, and
  the NMI handler gains a `JSR __audio_tick` splice (via the linker's
  `__audio_used` marker lookup) that ages an SFX countdown counter and
  mutes pulse 1 when the tone expires. Programs that never trigger
  audio pay zero ROM cost.

- **u16 arithmetic and comparisons**: new IR ops `LoadVarHi`, `StoreVarHi`,
  `Add16`, `Sub16`, and six `Cmp*16` variants. The lowering context
  tracks variable types via the analyzer's symbol table and routes
  expressions through the 8-bit or 16-bit path based on operand width.
  Add16 emits `CLC;ADC;ADC` with carry propagating naturally into the
  high byte; compares dispatch high-byte-first with a short-circuit
  low-byte fallback. Fixes a silent miscompile where `big += 1` on a
  u16 var only incremented the low byte.

- **Multi-scanline handlers per state**: `gen_scanline_irq` now
  dispatches on `(current_state, ZP_SCANLINE_STEP)` and reloads the
  MMC3 counter with the delta to the next scanline in the same state.
  `gen_scanline_reload` resets the step counter at the top of each
  NMI so a state with multiple handlers fires them in ascending line
  order. Previously only the first handler per state ever fired.

- **IR temp slot recycling**: `build_use_counts` pre-scans each
  function to count per-temp uses; `retire_op_sources` decrements
  the counts after each op and pushes dead slots back onto
  `free_slots` for later allocation. `bitwise_ops.ne` used to crash
  (debug) or miscompile (release) once it hit 128 concurrent temps;
  with recycling the same function now uses ~4 slots instead of 136.

- **INC/DEC peephole fold + improved dead-load elimination**:
  `fold_inc_dec` collapses `LDA addr; CLC; ADC #1; STA addr` into
  a single `INC addr` (and the SEC/SBC variant into `DEC addr`),
  saving 5 bytes and 5 cycles per increment. The fold is suppressed
  when the next instruction reads carry. `remove_dead_loads` now
  walks past INC/DEC/STX/STY (which don't touch A) to find the
  actual next A-use, catching more dead loads.

Tests: 331 unit + 39 integration (up from 313 + 37), including new
guards for audio, u16, multi-scanline, and slot recycling.

https://claude.ai/code/session_01A8qk3gw2jWSzdiXBZPZSFE
This commit is contained in:
Claude 2026-04-12 22:21:53 +00:00
parent 9ebf58f7db
commit 9a539ea068
No known key found for this signature in database
12 changed files with 2108 additions and 144 deletions

View file

@ -163,15 +163,18 @@ These are defined in `ErrorCode` but never emitted:
## 12. Audio
**Status**: `play`, `start_music`, `stop_music` keywords are lexed but produce
no output. No audio driver exists.
**Status**: A minimal APU driver is now in place. `play SfxName` emits
pulse-1 register writes from a builtin SFX table, `start_music` holds a
tone on pulse 2, `stop_music` mutes pulse 2. The NMI handler gains a
`JSR __audio_tick` splice whenever user code triggered audio so the SFX
countdown counter can mute the tone when its frames are up. Unused
programs pay zero ROM cost.
**What's needed**:
**Still TODO for a full audio solution**:
- `@sfx("file.nsf")` / `@music("file.ftm")` asset directives
- Audio driver running in the NMI handler (after OAM DMA)
- `play SfxName` → trigger one-shot sound effect
- `start_music TrackName` / `stop_music` → start/stop background music
- FamiTracker export format parsing (complex — consider using existing tools)
- FamiTracker export format parsing
- Multi-channel music driver with note tables and envelopes
- Per-SFX duration override in the declaration rather than the builtin table
---
@ -282,6 +285,49 @@ spills to zero-page $02 for comparisons. A proper allocator would:
These items were documented as future work but have since been implemented:
- **Minimal audio driver**`src/runtime/mod.rs::gen_audio_tick()`
plus codegen for `IrOp::PlaySfx` / `StartMusic` / `StopMusic`. SFX
and music map through a builtin name table (`coin`, `jump`, `hit`,
`theme`, etc.) to APU pulse 1/2 register writes. The NMI handler
gains a `JSR __audio_tick` splice (via the linker's `__audio_used`
marker lookup) that decrements a per-frame counter and mutes the
channel when the tone's time is up. Programs without audio pay
zero ROM cost.
- **u16 arithmetic and comparisons** — new IR ops `LoadVarHi`,
`StoreVarHi`, `Add16`, `Sub16`, `CmpEq16` through `CmpGtEq16`. The
lowering context tracks variable types via the analyzer's symbol
table and routes each expression through the 8-bit or 16-bit path
based on operand width. Initializers, compound assignments, and
comparisons all preserve both bytes. The codegen emits
`CLC;ADC;ADC` for Add16 with carry propagating naturally, and
compare-high-then-compare-low dispatch for the six comparison
variants.
- **Multi-scanline on_scanline per state**`gen_scanline_irq` now
dispatches on `(current_state, ZP_SCANLINE_STEP)` and reloads the
MMC3 counter with the delta to the next scanline in the same
state. `gen_scanline_reload` resets the step counter at the top
of each NMI so a state with multiple handlers fires them in
ascending line order.
- **IR temp slot recycling**`build_use_counts` pre-scans each
function to count per-temp uses; `retire_op_sources` decrements
the counts after each op runs and pushes dead slots back onto
`free_slots` for later allocation. Previously, `bitwise_ops.ne`
crashed (debug) or silently miscompiled (release) once it
allocated more than 128 concurrent temps. With recycling the
same function now uses ~4 slots instead of 136.
- **INC/DEC peephole fold**`fold_inc_dec` collapses
`LDA addr; CLC; ADC #1; STA addr` into a single `INC addr`
(and the SEC/SBC/#1 variant into `DEC addr`). Saves 5 bytes and
5 cycles per increment. The fold is suppressed when the next
instruction is a carry-dependent branch (`BCC`/`BCS`) since
INC/DEC don't update the carry flag.
- **Peephole dead-load elimination across passive ops** — the
old `remove_dead_loads` only dropped an LDA if the very next
instruction was another A-writer. Now it walks past
INC/DEC/STX/STY (which don't touch A) to find the actual
next A-use, catching more dead loads produced by copy
propagation.
- **State machine dispatch** — CMP + BNE + JMP trampoline dispatch table,
`current_state` in ZP $03, on_enter/on_exit handlers as JSR targets
- **Function call codegen** — JSR to function labels, ZP $04-$07 param passing,
@ -440,18 +486,26 @@ These items were documented as future work but have since been implemented:
For someone picking up this codebase, the recommended order of work:
1. **u16 / array / nested struct fields** — current structs only
allow u8/i8/bool fields. The layout machinery is ready for more
types but the codegen only handles 1-byte loads/stores.
2. **Audio driver** — the `play`/`start_music`/`stop_music`
statements parse but don't generate any code. A minimal NSF-style
driver running in NMI would unblock game projects.
3. **Multi-scanline on_scanline reload** — the current codegen
supports one scanline per state but not a chain of handlers at
different scanlines in the same frame.
4. **Register allocator** — proper A/X/Y allocation to replace
1. **u16 / array / nested struct fields** — u16 *globals* now work
end-to-end (load/store, +/-, comparisons all propagate through
16-bit IR ops). Struct fields and array elements are still u8-only
— the layout machinery needs to grow multi-byte field offsets.
2. **Full multi-channel music driver** — the current audio engine
holds a single tone on each of pulse 1 (SFX) and pulse 2 (music).
A proper NSF-style driver with note tables, envelopes, and a
multi-track stream would unblock real game music.
3. **Register allocator** — proper A/X/Y allocation to replace
zero-page spills used by the current IR codegen. Partially
mitigated by peephole passes but still wasteful in some cases.
5. **Match statement**`match x { 0 => ... , 1 => ... }` would be
useful for state dispatch and enum-driven logic.
8. **Text / HUD layer** — font sheet + layout system for scores.
mitigated by peephole passes + the new slot recycler, but still
wasteful in some cases (every temp spills to a ZP slot even if
its live range is one op wide).
4. **Text / HUD layer** — font sheet + layout system for scores.
5. **Cross-block temp live-range analysis** — the current slot
recycler is function-local; temps that flow across block
boundaries always get a dedicated slot for the full function.
A proper CFG-aware live range interference graph would let more
temps share slots.
6. **Peephole: drop LDA dead across unconditional JMPs** — after
the INC/DEC fold we sometimes leave an `LDA #1` whose value is
consumed by nothing before the next `JMP __ir_main_loop`. Local
analysis can't prove it's dead; a cross-block pass could.