Skip to content

Debug Tools

On-device debug instrumentation for NGPC homebrew: CPU profiler via raster position, ring-buffer diagnostic log, and runtime assertions with on-screen fault display.


1. CPU Profiler — ngpc_debug

1.1 API

void ngpc_debug_begin(void);                                       /* mark start of logic */
void ngpc_debug_end(void);                                         /* mark end of logic */
void ngpc_debug_draw_bar(plane, pal_ok, pal_warn, pal_over);       /* visual CPU bar */
void ngpc_debug_print_pct(plane, pal, x, y);                       /* print "XX%" */
void ngpc_debug_print_fps(plane, pal, x, y);                       /* print "XXFPS" */
u8   ngpc_debug_get_lines(void);                                   /* raw scanlines used */
u8   ngpc_debug_get_pct(void);                                     /* CPU usage 0-100+ */

The profiler measures game logic duration using the hardware raster position register (HW_RAS_V at 0x8009): it reads the current scanline at begin and end, and computes the percentage of the 152-scanline frame consumed.

Color bar thresholds: - Green: < 80% — safe - Yellow: 80-100% — tight - Red: > 100% — frame overflow

All calls become no-ops when NGPC_DEBUG is defined as 0.

1.2 Typical Game Loop Usage

void main(void)
{
    ngpc_init();
    ngpc_load_sysfont();
    /* ... setup ... */

    while (1) {
        ngpc_vsync();
        ngpc_input_update();

        ngpc_debug_begin();
            game_update();
        ngpc_debug_end();

        ngpc_debug_draw_bar(GFX_SCR2, PAL_GREEN, PAL_YELLOW, PAL_RED);
        /* or: ngpc_debug_print_pct(GFX_SCR1, 0, 18, 0); */
    }
}

ngpc_debug_begin() / ngpc_debug_end() bracket only game logic, not rendering or VBlank-triggered code — to get an accurate budget reading.


2. Ring Buffer Log — ngpc_log

2.1 API

void ngpc_log_init(void);
void ngpc_log_clear(void);
void ngpc_log_hex(const char *label, u16 value);   /* log "LABEL: 0x1234" */
void ngpc_log_str(const char *label, const char *str);
void ngpc_log_dump(plane, pal, x, y);              /* render log to tilemap */
u8   ngpc_log_count(void);                         /* number of entries in buffer */

/* Convenience macros */
NGPC_LOG_HEX("PAD", ngpc_pad_held);
NGPC_LOG_STR("ST",  "RUN ");

Stores short diagnostic entries in a fixed-size ring buffer (~288 bytes RAM). New entries overwrite the oldest when the buffer is full.

2.2 Usage Notes

  • Useful on real hardware where stdout/serial is not available.
  • Display with ngpc_log_dump() on a spare tilemap plane or overlay.
  • ngpc_log_hex() formats the value as hexadecimal.
  • Macros (NGPC_LOG_HEX, NGPC_LOG_STR) compile to no-ops in release builds.
  • RAM cost: ~288 bytes for the ring buffer — significant on a 8 KB RAM budget. Disable in release.

3. Runtime Assert — ngpc_assert

NGPC_ASSERT(condition);

/* Examples */
NGPC_ASSERT(pointer != 0);
NGPC_ASSERT(index < ENTITY_MAX);

In debug builds: assertion failure displays an on-screen fault page (file + line, if available) and enters a blinking loop. The game does not continue.

In release builds: NGPC_ASSERT is compiled out completely — zero cost.

Assertion failures loop indefinitely. This is intentional — it forces the developer to notice the failure on screen. The watchdog will eventually reset the console, but the fault screen should be visible long enough to read.


4. Build Flags

Flag Effect
#define NGPC_DEBUG 1 Enable profiler (default in debug profile)
#define NGPC_DEBUG 0 Disable profiler — all calls become no-ops
#define NGP_PROFILE_RELEASE 1 Strip NGPC_ASSERT and NGPC_LOG_* macros

In the build Makefile, debug vs release is typically controlled by passing -DNGP_PROFILE_RELEASE=1 to CDEFS for release builds.


5. Faults the emulator counts for you — hw_safety

The three tools above run inside your ROM and cost it RAM and cycles. This one costs nothing: the NgpCraft emulator watches for a small set of conditions a real console punishes and most emulators never mention, and reports them with every run.

python ngpc_native.py run GAME.ngc --bios bios.bin --frames 600 --json
  → "hw_safety": {"counts": {"watchdog-starved": 0,
                             "system-stack": 0,
                             "flash-busy-fetch": 0}}
finding what it means why a console cares
watchdog-starved the watchdog counter ran out without being refreshed the console resets, mid-anything
system-stack XSP crossed above 0x6C00 into the BIOS's own page documented as a restart or a power-off
flash-busy-fetch the CPU fetched an instruction out of a flash chip that was programming or erasing that chip answers status, not memory: the CPU is running status bits (STORAGE §5.0c)

Each is counted, never fatal: a real console does not stop at the offending instruction either, it carries on into whatever follows. Each carries the PC that committed it, and --hw-guard turns the first two into a stop when you want the run to halt exactly there.

flash-busy-fetch is the one to know about, because it is the single unambiguous signature of an entire family of save bug — a driver missing its di, a stub left in the cartridge instead of copied to RAM, a chip reset sent to a chip still working. All three kill a console and all three pass silently on an emulator whose flash has no busy window, which is most of them. A clean run here is a claim about your save path that a screenshot cannot make.

6. Verifying a port against the original

When a game is ported from another machine, an emulator of the original is the best oracle. Practices that held up over a complete port:

  • One record per game update, taken by a breakpoint at the end of the update loop — not by sampling at a fixed interval: when the original chains updates back-to-back, sampling skips some.
  • Compare update by update, not frame by frame: the two machines may update at different rates.
  • Inject the original's randomness. If the original draws random numbers from a register (a Z80 ld a,r, for instance), log the value at each draw with a breakpoint and replay the list in the port's bench build. If that code sits in a banked area, filter by context: the same address is another routine in another bank.
  • Choose the reference machine at the right refresh rate. An emulated 50 Hz machine is fine for update-by-update comparisons but wrong for any duration in seconds if the original is a 60 Hz game: one port had timed a game-over screen and a door animation on the 50 Hz machine.
  • Force state between two updates, not at an arbitrary time (a breakpoint at the end of an update), otherwise results shift by ±1 update at random.
  • A bench that reports "identical" may be empty: check that what is compared actually happened (bullets spawned inside a wall compare equal because nothing exists).
  • Benches compare state, not the picture: capture the screen after each change too — an export offset bug went unseen by fully identical benches.
  • A one-update difference: log both update counters per update before touching the code; the bench may be the one out of step.
  • Keep the original's oddities when they influence play, mark them in the code and list them.

Quick Reference

Item Details
CPU profiler instrument ngpc_debug_begin() / ngpc_debug_end() around game logic only
Bar: green < 80% CPU
Bar: yellow 80-100% CPU
Bar: red > 100% — frame overflow
Profiler register HW_RAS_V (0x8009) — current scanline
Log buffer size ~288 bytes RAM
Log entry format Hex: "LABEL: 0x1234" — String: "LABEL: str"
Log overflow Ring buffer — oldest entry overwritten
Assert in debug On-screen fault page + blinking loop
Assert in release Compiled out (zero cost)
Disable all debug NGPC_DEBUG=0 + NGP_PROFILE_RELEASE=1

See Also