1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-09 17:28:00 +00:00

debug: add debug.frame_overrun_count() and debug.frame_overran() builtins

The frame-overrun counter at $07FF was previously only readable
via `peek(0x07FF)`, which forces every program that wants to
guard against missed frames to know the magic address. This adds
two named query expressions:

- `debug.frame_overrun_count()` — cumulative miss count since reset
- `debug.frame_overran()` — sticky bit cleared by the next wait_frame,
  so `debug.assert(not debug.frame_overran())` catches a miss in the
  previous window without waiting for the counter to roll over.

The sticky bit lives at $07FE alongside the existing counter and
is set inside the same NMI-time overrun branch. Release builds
emit none of the runtime side: the NMI handler still skips both
writes, the codegen `wait_frame` only clears $07FE in debug mode,
and committed example ROMs stay byte-identical.

The new expression form parses through `parse_primary`'s `KwDebug`
arm, so the existing `debug.log(...)` / `debug.assert(...)`
*statement* parser stays untouched. The analyzer rejects unknown
methods with E0201 and stray arguments with E0203 so typos don't
silently compile to a zero load.

https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
This commit is contained in:
Claude 2026-04-15 01:57:45 +00:00
parent bf7819ca0f
commit d4e613fb7c
No known key found for this signature in database
9 changed files with 401 additions and 7 deletions

View file

@ -89,13 +89,24 @@ pub const ZP_PENDING_BG_ATTRS_HI: u8 = 0x17;
/// handler whenever it fires while the previous frame's ready
/// flag is still set — which means the main loop didn't consume
/// it, so user code spent more than one vblank-to-vblank window
/// processing the last frame. Read it with `peek(0x07FF)` in
/// user code to see how many overruns have happened since reset,
/// or watch the address in a Mesen memory viewer. Placed at the
/// top of main RAM to minimise the chance of a collision with
/// analyzer-allocated variables (which grow from $0300 upward).
/// processing the last frame. Read it with `peek(0x07FF)` or
/// `debug.frame_overrun_count()` in user code to see how many
/// overruns have happened since reset, or watch the address in
/// a Mesen memory viewer. Placed at the top of main RAM to
/// minimise the chance of a collision with analyzer-allocated
/// variables (which grow from $0300 upward).
pub const DEBUG_FRAME_OVERRUN_ADDR: u16 = 0x07FF;
/// Debug-mode "did the previous frame overrun" sticky bit. Set
/// to 1 by the NMI handler at the same time as it bumps
/// [`DEBUG_FRAME_OVERRUN_ADDR`], and cleared to 0 by `wait_frame`
/// once the main loop catches up. Exposed to user code as
/// `debug.frame_overran()` — a per-frame "did this frame finish
/// in time" predicate suited for `debug.assert(!debug.frame_overran())`
/// guards. Lives one byte below the cumulative counter so the
/// two can be inspected together in a Mesen memory viewer.
pub const DEBUG_FRAME_OVERRUN_FLAG_ADDR: u16 = 0x07FE;
// ── Extra channel state ──
//
// The pulse-1 sfx and pulse-2 music channels live in zero page
@ -344,6 +355,16 @@ pub fn gen_nmi(has_ppu_updates: bool, has_audio: bool, debug_mode: bool) -> Vec<
INC,
AM::Absolute(DEBUG_FRAME_OVERRUN_ADDR),
));
// Set the per-frame sticky bit. It stays set until the
// next `wait_frame` clears it, so a single
// `debug.assert(!debug.frame_overran())` guard at the top
// of `on frame { ... }` catches any miss in the previous
// window.
out.push(Instruction::new(LDA, AM::Immediate(0x01)));
out.push(Instruction::new(
STA,
AM::Absolute(DEBUG_FRAME_OVERRUN_FLAG_ADDR),
));
out.push(Instruction::new(
NOP,
AM::Label("__debug_no_overrun".into()),