mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 17:06:04 +00:00
Programs that put functions in switchable banks can now call across
bank boundaries — `bank A { fun step() { helper() } }` where
`helper` lives in `bank B` used to panic in the IR codegen. Three
small pieces unblock it:
1. **Generic trampoline.** `runtime/gen_bank_trampoline` no longer
takes a `fixed_bank_index` argument. Instead it reads the
caller's current bank from `ZP_BANK_CURRENT`, pushes it on the
hardware stack, switches to the target, JSRs the entry, then
pulls and restores the saved bank. The same per-callee stub
works for fixed→banked and banked→banked direction; nested
trampolines compose because each PHA/PLA pair sits inside its
own JSR/RTS frame. `gen_mapper_init` seeds `ZP_BANK_CURRENT`
with the fixed bank index for any banked mapper so the very
first cross-bank call from the fixed bank still restores to
the fixed bank (matching pre-banked-banked semantics).
2. **Codegen drops the panic.** The `Some(from), Some(to)` arm in
the call-resolution switch now emits `JSR __tramp_<name>` like
the fixed→banked case instead of panicking. Banked→fixed calls
still go direct (the fixed bank is always mapped at $C000).
3. **Bank-namespaced local labels.** Two banks emitting the same
`__ir_cmp_e_8` would trip the linker's discovery-pass duplicate-
label check the moment any banked code generated a comparison.
The new `local_label_suffix` helper prefixes the suffix with the
current bank name when banked code is being emitted, leaving
fixed-bank label generation untouched (so existing examples are
byte-identical apart from the trampoline / init bytes
themselves).
The new `examples/uxrom_banked_to_banked.ne` demonstrates the path
end-to-end: `bank Logic { fun step() { ... clamp() } }` calls
`bank Helpers { fun clamp() { ... } }` once per frame. The harness
golden is committed alongside it. The five existing banked example
ROMs change byte-for-byte because of the new trampoline shape and
the seed-ZP_BANK_CURRENT init, but their emulator goldens still
match exactly — observable behaviour is unchanged.
https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
74 lines
2.6 KiB
Text
74 lines
2.6 KiB
Text
// UxROM Banked-to-Banked — first NEScript example to exercise a
|
|
// switchable-bank function calling *another* switchable-bank function.
|
|
//
|
|
// The previous user-banked example (`uxrom_user_banked.ne`) only put
|
|
// fixed → banked calls through the trampoline path; here `bank Logic`
|
|
// holds `step()` and `bank Helpers` holds `clamp()`, and `step` calls
|
|
// `clamp` once per frame. The codegen emits `JSR __tramp_clamp` from
|
|
// inside bank Logic, which lands in the fixed-bank trampoline that
|
|
// saves the current bank (Logic), switches to Helpers, runs the body,
|
|
// then restores Logic on the way out — see runtime/gen_bank_trampoline
|
|
// for the PHA/PLA implementation.
|
|
//
|
|
// The harness captures frame 180 somewhere along the sweep, so any
|
|
// regression in the trampoline's save/restore would either leave the
|
|
// wrong bank mapped at $8000 (subsequent sprite reads would corrupt)
|
|
// or crash before the OAM update happened.
|
|
//
|
|
// Build: cargo run -- build examples/uxrom_banked_to_banked.ne
|
|
|
|
game "UxROM Banked to Banked" {
|
|
mapper: UxROM
|
|
mirroring: horizontal
|
|
}
|
|
|
|
// Globals live in the fixed bank's RAM and are reachable from any
|
|
// bank via direct zero-page / absolute addressing — bank switching
|
|
// only affects the $8000-$BFFF code window.
|
|
var px: u8 = 80
|
|
var dir: u8 = 0 // 0 = sweep right, 1 = sweep left
|
|
|
|
bank Logic {
|
|
// The "main" banked function. Lives in bank Logic and calls
|
|
// into bank Helpers via the fixed-bank trampoline emitted
|
|
// by the linker.
|
|
fun step() {
|
|
if dir == 0 {
|
|
px = px + 1
|
|
} else {
|
|
px = px - 1
|
|
}
|
|
// Cross-bank call into Helpers. The codegen sees a
|
|
// current_bank of "Logic" and a callee bank of "Helpers"
|
|
// and emits `JSR __tramp_clamp` — the trampoline lives
|
|
// in the fixed bank, saves the caller's bank
|
|
// (ZP_BANK_CURRENT == Logic), switches to Helpers, runs
|
|
// the body, then restores Logic before returning here.
|
|
clamp()
|
|
}
|
|
}
|
|
|
|
bank Helpers {
|
|
// Bounce the sprite between two pixel rails. Self-contained
|
|
// — only reads/writes the global zero-page slots, no calls
|
|
// back out of the bank, so the trampoline never has to
|
|
// recursively unwind a third level.
|
|
fun clamp() {
|
|
if px == 176 {
|
|
dir = 1
|
|
}
|
|
if px == 80 {
|
|
dir = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
on frame {
|
|
// Single fixed → banked trampoline call. Inside, `step` does a
|
|
// banked → banked call into `clamp` — that second hop is the
|
|
// path the new trampoline implementation enables.
|
|
step()
|
|
draw Smiley at: (px, 112)
|
|
}
|
|
|
|
start Main
|