The `seg.ooffs` field in our ca65 .dbg output was off by 16 — it was
emitting the PRG-relative fixed-bank offset when ca65's convention
(and Mesen's DbgImporter.cs:301 math:
`Address = val - seg.start + ooffs - headerSize`) expects the raw
output-file offset, *including* the iNES header. The practical
consequence: every label Mesen resolved via the .dbg was 16 bytes
short of its true PRG offset, which silently corrupted source-line
mapping for the first bytes of each function.
Fix is a one-liner — drop the `saturating_sub(16)` and feed
`linked.fixed_bank_file_offset` straight into the ooffs field. Unit
tests in debug_symbols.rs updated to assert the new values (ooffs=16
for NROM, 16+16K*N for banked).
The Mesen probe (`tests/mesen/probe.lua`) is expanded in the same
change, because the sabotage test that caught this bug is also the
cleanest demonstration the probe is working:
* checks all four entry-point labels resolve and land inside the
fixed bank's CPU window ($C000-$FFFF);
* asserts the linker's relative ordering (main_loop < Main_frame
< nmi);
* registers a startFrame callback, waits three frames, and verifies
PC is still in the fixed bank + that `emu.read(main_loop.address,
nesPrgRom)` returns 0xA5 (the LDA-zp opcode the runtime always
places as main_loop's first instruction). The 0xA5 constant is
what catches the ooffs regression — a less-specific "not 0xFF"
check coincidentally passed even with ooffs=0 because the shifted
address still landed on real code.
Verified locally by running the probe against hello_sprite's ROM
with four different `.dbg` mutations and confirming each triggers
the expected exit code.
https://claude.ai/code/session_01DfN3pKJLryr7vvNFBpcqmC
Run Mesen2's `--testRunner` mode in CI, point it at a NEScript-built
ROM + auto-loaded .dbg, and assert via Lua that the four entry-point
labels (`nmi`, `irq`, `Main_frame`, `main_loop`) we promised to emit
actually resolve. Failures encode which assertion broke into the exit
code so CI can report it without stdout (Mesen's emu.log is internal).
The setup needed three workarounds, each documented inline in the
workflow:
* `touch settings.json` to skip Mesen's first-run GUI wizard, which
blocks the --testRunner dispatch in Program.cs:74. Contents don't
matter — Configuration.Deserialize falls back to defaults on parse
error.
* `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1` to keep .NET from loading
system libstdc++ via libicuuc. On Ubuntu 24.04 the dual libstdc++
presence (system + MesenCore.so's bundled static copy) crashes
MesenCore's static regex initialiser with std::bad_cast before any
user code runs.
* `xvfb-run` because Mesen's Avalonia UI calls XOpenDisplay before
--testRunner is dispatched.
This is a separate workflow file from ci.yml because it depends on the
40 MB Mesen2 binary download + xvfb + sdl2, none of which the existing
jobs need. Cached by Mesen version so reruns are fast.
https://claude.ai/code/session_01DfN3pKJLryr7vvNFBpcqmC