Skip to content

Debugging with MesenCE

Alekmaul edited this page Oct 2, 2026 · 1 revision

Debugging with MesenCE

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.

This page covers:

See also: PVSneslib and Mesen2 for the VBlank-headroom tutorial, and Logging with Mesen2 emulator for printf-style logging via a Lua script.


1. Build your ROM with debug symbols

Before you can get meaningful labels (function names, variable names) in the debugger, build your project with debug information enabled:

export PVSNESLIB_DEBUG=1
make

This produces extra build output next to your .sfc/.smc ROM, including a symbol file. When MesenCE's Automatically load DBG/MSL debug symbols option is enabled (see below), it will automatically pick up a .sym/.dbg file that has the same name as your ROM and sits in the same folder (e.g. mygame.sfc + mygame.sym). This is what lets the debugger show your C function names and labels instead of raw addresses.

If your symbol file has a different name or location, you can import it manually from File → Import/Export → Import Labels in the debugger window.


2. Loading the ROM in MesenCE

  1. Download MesenCE from the releases page and install it.
  2. Launch MesenCE and open your ROM via File → Open Rom... (or drag-and-drop the .sfc/.smc file onto the window).
  3. Go to File → Import/Export → Integration Settings and enable Automatically load DBG/MSL debug symbols. From now on, MesenCE will automatically load your .sym/.dbg file whenever it matches the ROM name.
  4. 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.


3. Adding breakpoints

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.

Breaking from your C code directly

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.

Typical PVSnesLib breakpoint targets

  • 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. $2100 for INIDISP, $4210 for RDNMI) to catch timing-sensitive reads/writes.

4. Using the Event Viewer

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.

5. Practical example: measuring frame headroom

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?"

  1. At the end of your per-frame logic, call a small routine that reads REG_RDNMI ($4210) and then executes WAI to 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();
}
  1. Make sure your NMI handler reads/clears REG_RDNMI, otherwise WAI can return immediately and the measurement becomes meaningless.
  2. Open the Event Viewer and locate the $4210 read mark and the NMI mark on the timeline.
  3. 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.


See also

Clone this wiki locally