1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/examples/uxrom_user_banked.ne

69 lines
2.5 KiB
Text
Raw Normal View History

codegen: user code in switchable banks via cross-bank trampolines Adds a `bank Foo { fun bar() { ... } }` parser form so user functions can opt into living in a switchable PRG bank instead of the fixed bank, plus the IR codegen, runtime, and linker work to make calls across the bank boundary actually run. Programs that don't use the new syntax produce byte-identical ROMs to before — verified by rebuilding every existing example and diffing. Pipeline shape: * Parser accepts both `bank Foo: prg` (legacy reserved slot) and `bank Foo { fun ... }` (functions land in the named bank). Nested functions get tagged `bank: Some("Foo")` on the FunDecl + IrFunction. * Analyzer bumps the user zero-page start past `$10` whenever the program declares any banked function, so `__bank_select`'s STA into ZP_BANK_CURRENT can't clobber a user variable. Programs without banked functions keep the legacy `$10` start. * IrCodeGen emits each banked function into its own per-bank instruction stream (`banked_streams: HashMap<String, Vec<Instruction>>`) while the fixed-bank stream gets the dispatcher loop + state handlers + top-level functions, exactly like before. Cross-bank calls from the fixed bank rewrite `JSR __ir_fn_<name>` to `JSR __tramp_<name>`; in-bank calls stay direct. Banked → fixed calls are direct (the fixed bank is always mapped at $C000-$FFFF). Banked → other-banked calls aren't supported in this pass and panic loudly during codegen. * Runtime's `gen_bank_trampoline` takes the trampoline label and entry label as parameters now (one trampoline per banked function, not one per bank) so the linker can request any number of stubs. * Linker assembles banked banks twice: a discovery pass to learn each bank's labels, then a final pass that seeds the merged label table so banked code can JSR into the fixed bank's runtime helpers (math, audio, etc.). The fixed-bank assembler is also seeded with the cross-bank labels so the trampolines' `JSR __ir_fn_<name>` resolves into the bank's $8000 window. New `asm::assemble_with_labels` / `asm::assemble_discover_labels` helpers wire this up. * PrgBank carries `Vec<Instruction>` + a list of `BankTrampoline` requests now, replacing the old `data: Vec<u8>` + single `entry_label: Option<String>` shape. The compiler populates both from the codegen output; the linker's two-pass assembly handles the rest. New example: `examples/uxrom_user_banked.ne` puts a sprite-stepping helper inside `bank Extras { fun step_animation() { ... } }`. The fixed-bank state handler calls it via the generated trampoline, and the harness golden locks in pixel + audio output at frame 180. UxROM is the only mapper exercised by the new example. MMC1 and MMC3 also work through the same path (the linker emits the right mapper-specific bank-select code), but no example uses them yet — the existing `mmc1_banked.ne` / `mmc3_per_state_split.ne` keep their fixed-bank-only layout. Limitations carried forward: * No banked → banked cross-bank calls (panics in codegen). * No greedy size-packing; placement is explicit-only. * MMC3 state handlers don't get banked (the per-state split path is untouched).
2026-04-14 11:41:20 +00:00
// UxROM User-Banked — first NEScript example to put real user code
// inside a switchable bank. The `bank Extras { fun ... }` block tells
// the linker that `step_animation` lives in the named PRG bank
// instead of the fixed bank; the IR codegen emits a
// `__tramp_step_animation` stub in the fixed bank, and every call
// from a state handler now goes through the trampoline (which
// selects bank 0 at $8000-$BFFF, JSRs the function, then restores
// the fixed bank before returning).
//
// UxROM is the friendliest mapper for this work because its
// `__bank_select` routine is a single bus-conflict-safe write to
// $FFF0; MMC1's serial shift register and MMC3's two-register dance
// would both work but add more moving parts to an already-large
// pipeline change.
//
// The example sweeps a smiley left-and-right entirely from inside
// the banked helper. The helper reads two globals (`px`, `dir`) and
// updates them in place — no parameters, no return value, so the
// fixed-bank call site is a single `JSR __tramp_step_animation`.
// The harness captures frame 180 somewhere along the sweep, so any
// regression in bank-switching, trampoline emission, or banked-stream
// assembly will flip the golden.
//
// Build: cargo run -- build examples/uxrom_user_banked.ne
game "UxROM User Banked" {
mapper: UxROM
mirroring: horizontal
}
// The banked helper drives `px` directly so the fixed-bank state
// handler is just `JSR __tramp_step_animation; draw Smiley`. `dir`
// is 0 while sweeping right and 1 while sweeping left.
var px: u8 = 110
var dir: u8 = 0
bank Extras {
// Step the smiley one pixel each frame, bouncing off x = 110
// and x = 150. The whole computation lives in the switchable
// bank: nothing here references any fixed-bank function, so the
// bank's instruction stream resolves cleanly during the linker's
// two-pass assembly without needing a second trampoline.
fun step_animation() {
if dir == 0 {
px = px + 1
if px == 150 {
dir = 1
}
} else {
px = px - 1
if px == 110 {
dir = 0
}
}
}
}
on frame {
// Trampoline call: the codegen emits `JSR __tramp_step_animation`
// because the function is tagged `bank: Some("Extras")`. The
// trampoline lives in the fixed bank, switches to bank 0, JSRs
// `__ir_fn_step_animation` at its $8000-window address, then
// switches back before returning.
step_animation()
draw Smiley at: (px, 120)
}
start Main