Repository navigation
Debugging with MesenCE emulator
MesenCE (the community-maintained continuation of Mesen2, formerly known as "Mesen2") is the recommended debugger to use with PVSnesLib on Windows, Linux and macOS.
It gives you a full 65816 debugger (breakpoints, call stack, memory tools) plus an Event Viewer that lets you see exactly when, on which scanline and cycle, something happens on real SNES hardware timing.
First, please [download]https://github.com/nesdev-org/MesenCE/releases/) the version corresponding to your system then install it and validate that the emulator works correctly.
- Launch MesenCE and open your ROM via File → Open Rom... (or drag-and-drop the
.sfc/.smcfile onto the window). - Go to File → Import/Export → Integration Settings and enable Automatically load DBG/MSL debug symbols. From now on, MesenCE will automatically load your
.sym/.dbgfile whenever it matches the ROM name. - Open the debugger with Debug → Debugger (or
Ctrl+D). You should see your PVSnesLib labels (function names) in the disassembly view instead of bare addresses.
Every time you rebuild your ROM, just reload it in MesenCE (File → Reload Rom, Ctrl+R or Shift+F1 depending on version) — the debugger will refresh the disassembly and re-import the matching symbol file.
Open the breakpoint list from the debugger window (Debug → Breakpoints, or the "Breakpoints" tab/panel inside the debugger). From there:
-
Add: right-click the list and select Add, or click the
+button. - Edit: double-click an existing breakpoint.
- Enable/disable: check or uncheck it in the list.
- Delete: right-click it and select Delete.
- Jump to code: right-click it and select Go to location.
For each breakpoint you can configure:
| Field | Description |
|---|---|
| Breakpoint Type | The memory type the breakpoint applies to (CPU, PPU registers, VRAM, WRAM, SRAM, ROM, etc). |
| Break On | Which access type triggers it: Read, Write and/or Execute. |
| Address / Range | A single address or a range of addresses. You can type a label name here if symbols are loaded (e.g. updateSprites). |
| Condition (optional) | An expression (same syntax as the Watch List) that must be true for the breakpoint to actually trigger — e.g. A == 5 or X + Y == 5. |
| Mark on Event Viewer | When checked (the M column), every time this breakpoint's condition is met, a mark is drawn on the Event Viewer timeline — even if Break Execution is off. This is the key feature for visualizing when something happens. |
| Break Execution | If unchecked, the breakpoint won't actually pause emulation — useful when you only want the Event Viewer mark, not an interruption. |
PVSnesLib exposes a helper that lets you plant a breakpoint straight from your source, without knowing the address in advance:
#include <snes/console.h>
void main(void) {
// ... your game logic ...
consoleMesenBreakpoint(); // execution stops here in MesenCE
// ... more logic ...
}consoleMesenBreakpoint() executes a WDM opcode that MesenCE recognizes and can break on. It's a convenient "debugger breakpoint" you can leave in your code and toggle on/off from MesenCE's CPU execute-breakpoint settings (or simply remove for release builds), without hunting for the corresponding assembly address by hand.
-
A specific function: set an Execute breakpoint on the function's label (e.g.
oamInit) to stop the moment it's called. - A variable being corrupted: set a Write breakpoint on its RAM address/label to catch every write, then use the condition field to filter for an unexpected value.
-
A PPU register access: set a breakpoint on the register's address (e.g.
$2100forINIDISP,$4210forRDNMI) to catch timing-sensitive reads/writes.
Open it via Debug → Event Viewer. The Event Viewer draws a small dot for every tracked event (register reads/writes, NMI, IRQ, breakpoint marks, DMA, etc.) at the exact scanline/cycle position where it occurred, overlaid on a representation of the PPU's scan — essentially a picture of your frame's timeline.
Key things to know:
- Reading the grid: each row is a scanline, each column position within a row corresponds to a cycle (dot/PPU clock) within that scanline. Hovering over a mark shows the exact scanline and cycle (and, depending on the version, the master clock cycle) at which the event occurred, along with the event type and value.
- Event categories: use the side panel to toggle which event types are shown (CPU register reads/writes, NMI, IRQ, DMA/HDMA channels, breakpoint marks, etc.) and to configure their colors, so you can visually separate, say, your HDMA writes from your sprite-DMA writes.
- Breakpoint marks: any breakpoint with Mark on Event Viewer enabled shows up here as its own colored dot, whether or not it actually pauses execution. This lets you place several "instrumentation" breakpoints (execution disabled) throughout your code and see, frame after frame, exactly where in the scanline/cycle timeline each one fires — a very effective way to measure how much time a piece of logic actually consumes.
- Show previous frame's events: enable this option to keep the entire last full frame visible (e.g. all NTSC scanlines) instead of only the events since the last pre-render scanline. This is what you want when comparing "how far into the frame did my logic finish" across successive frames.
- Scanline/cycle budget reminder: NTSC SNES has 262 scanlines per frame (~340 dots/cycles each), PAL has 312. Anything landing near or past scanline 224 (start of VBlank on a 224-line screen) or overlapping with your NMI mark means you're eating into (or overrunning) VBlank.
This combines breakpoints/marks with the Event Viewer to answer a very common question: "How much time is left in my frame after my game logic runs?"
- At the end of your per-frame logic, call a small routine that reads
REG_RDNMI($4210) and then executesWAIto wait for the next NMI:
WFVBlank:
LDA.l $004210 ; reading REG_RDNMI creates a mark in the Event Viewer
WAI ; halt the CPU until the NMI fires
rtl// gameloop
while (1) {
doGameLogic();
WFVBlank(); // marks "logic finished" then waits for NMI
doVblankDma();
}- Make sure your NMI handler reads/clears
REG_RDNMI, otherwiseWAIcan return immediately and the measurement becomes meaningless. - Open the Event Viewer and locate the
$4210read mark and the NMI mark on the timeline. - The gap in scanlines/cycles between the two marks is your remaining frame headroom before VBlank starts. If the gap shrinks over time or disappears, your logic is close to (or already) overrunning the frame budget.
You can apply the same idea with consoleMesenBreakpoint() or plain conditional breakpoints (execution disabled, Mark on Event Viewer enabled) at the start/end of any function you want to profile — the Event Viewer will show you precisely where in the scanline/cycle grid each mark lands, frame after frame.
Show the end of logic as a point in the Mesen Event Viewer and then wait for NMI. The gap between this point and NMI shows remaining CPU time before VBlank.
Custom asm WaitForVBlank Method:
WFVBlank:
LDA.l $004210 ; read REG_RDNMI produces point in Event Viewer
WAI ; wait for NMI
rtl
.ends
LDA reads REG_RDNMI. Mesen displays this read as a point in the Event Viewer. WAI stops the CPU until NMI occurs. RTL returns to the caller afterward.
Your NMI handler must read or clear REG_RDNMI. If not cleared WAI may return immediately which removes the wait and invalidates the measurement.
Gameloop pattern:
while(1) {
do_logic();
WFVBlank();
do_vblank_dma();
}
The REG_RDNMI point marks the end of logic. The NMI event marks the start of VBlank. The gap between both events is headroom. If there is no gap logic is at the limit. If NMI occurs while logic is still running frame time is exceeded.
The screenshot shows that only a few scanlines remain before VBlank. This indicates that an overrun is close. Consider optimizing the logic or spreading work across multiple frames. If the number of free scanlines continues to shrink a real overrun will occur.

- Introduction
- Hello World example
- Gamepad and peripherals
- Backgrounds
- Sprites
- Dynamic Sprites
- Meta Sprites
- Sounds and Musics
- SNES ROM Header