DEV Community

Nivando Soares
Nivando Soares

Posted on

Porting Test Drive II from SNES to PC, Part 1: Day 0 to a measurable ROM

Porting Test Drive II from SNES to PC, Part 1: Day 0 to a measurable ROM

Between February 26 and March 19, 2026, the asmdump repo moved through 45 commits. It started as a raw reverse-engineering dump and ended this stretch with a C/SDL runtime, deterministic emulator probes, tilemap provenance tooling, and regression gates.

This first part covers the first two days: what was in the repo on day 0, why the project did not start with gameplay code, and how the initial extraction and validation toolchain changed the shape of the problem.

Day 0: the repo was a reverse-engineering base

Commit a683c61 on 2026-02-26 is the real starting point. It imported:

  • bank0.asm through bank31.asm
  • game.smc and game.sym
  • ROM build files like Makefile, linkfile, main.s, snes.asm, and spc700.asm

That is enough to rebuild and inspect the ROM. It is not enough to port it.

There was no extraction pipeline. There was no deterministic capture harness. There was no stable asset format that a PC runtime could load. The repo knew a lot about addresses, but very little about contracts.

The absence of a direct decompilation path matters here. bank0.asm already exposed recognizable reset, NMI, IRQ, and callback scheduling logic, but the gameplay-facing banks were not in a state where a straight assembly-to-C pass would be a good use of time:

  • bank10.asm and bank11.asm still mixed code and data heavily
  • bank30.asm looked more like structured tables and dispatcher data than a clean list of callable functions
  • game.sym exposed labels and coverage, not behavior

So the early plan was not "translate bank 0, then bank 1, then bank 10". The early plan was:

  1. Make the original ROM measurable.
  2. Extract typed data from it.
  3. Rebuild systems on the PC side against those measured outputs.

That decision is still the right one. A reliable port needs a reference harness before it needs more C files.

The first burst of tooling

February 27 has nine commits. That entire day is the actual day-1 port effort.

The important commits are:

  • c867efd - SNES asset extraction toolchain scaffolding
  • fcc30b5 - scene, sprite, and road visualization tooling
  • f8785b9 - fully integrated Qt asset viewer UI
  • 2bc9dc2 - initial scripts, sprite translation, capture data, and the first runtime skeleton

The repo stopped treating the ROM as a monolith and started turning it into inspectable outputs:

  • palette dumps as JSON
  • VRAM reconstruction and boot-screen manifests
  • decompressed chunk outputs
  • tile sheets rendered to neutral image formats
  • Mesen-based screenshot and state capture
  • scene visualizers that could be driven outside the emulator

Representative commands from that stage still describe the approach well:

./validation/run_mesen_capture.sh

python3 tools/extract_bank3_palettes.py game.smc tools/out/bank3_palettes.json
python3 tools/extract_boot_screen_manifest.py game.smc tools/out/bank1_boot_screen.json
python3 tools/build_boot_vram.py game.smc tools/out/bank1_boot_screen.json tools/out/bank1_boot_vram_variant0.bin --json-out tools/out/bank1_boot_vram_variant0.json
python3 tools/decompress_td2_chunk.py game.smc tools/out/bank7_42fb_8000.bin --bank 7 --addr 0x8000 --json-out tools/out/bank7_42fb_8000.json
Enter fullscreen mode Exit fullscreen mode

None of that is glamorous. All of it is necessary.

The early validation/ scripts mattered because they made the emulator part of the build surface instead of a manual debugging tool. mesen_capture.lua gave the project deterministic frame dumps and input logs. mesen_probe_boot.lua started recording boot and title-state selectors frame by frame. Once those existed, "what does the ROM do here?" stopped being a memory question and became a file question.

Typed outputs changed the project

The most important change in those first two days was not the number of tools. It was the move toward typed outputs.

There is a real difference between:

  • "I have a PNG of the title screen"
  • and "I have VRAM, CGRAM, OAM, PPU state, decompressed source chunks, and the command sequence that built this screen"

The first is useful for discussion. The second is useful for engineering.

By the end of the initial toolchain burst, the repo already had examples of that second category:

  • tools/out/bank3_palettes.json
  • tools/out/bank1_boot_screen.json
  • tools/out/bank1_boot_vram_variant0.json
  • tools/out/bank7_42fb_8000.json
  • deterministic Mesen capture logs under .mesen-config/Mesen2/LuaScriptData/

That gave the project a clean boundary between "ROM-side fact" and "PC-side implementation". It also made later runtime work much less fragile. The SDL renderer could target stable files instead of scraping live emulator output or hard-coding one-off frame knowledge.

Why the visualizers mattered

fcc30b5 and f8785b9 are easy to dismiss as UI work, but they are part of the core engineering story.

The early scene, sprite, road, and Qt viewer tools turned opaque ROM state into something an engineer could inspect without rebuilding the entire mental model every session. That is not polish. That is throughput.

On a project like this, the fast path is rarely "write more code". The fast path is usually:

  • expose the data
  • make it inspectable
  • reduce the number of manual emulator steps between a hypothesis and a rendered result

That principle shows up all over the repo after day 1. The later design packs, provenance windows, regression gates, and scanline samplers are all extensions of the same idea.

What the repo looked like after the first real day

By the end of 2bc9dc2, the repo had crossed a line:

  • it still was not a gameplay port
  • it still was not a decompilation
  • but it was no longer just a disassembly dump

It had become a measurable workbench:

  • deterministic captures
  • extraction scripts
  • decompression tooling
  • typed manifests
  • visual inspection tools
  • the first C/SDL runtime skeleton under port/

That last item is where the next part starts. Once the reference side was strong enough, the obvious next step was not more extraction. It was to take one narrow front-end slice and get it running natively.

That slice ended up being the Ballistic intro path, and it forced the project to answer a harder question than "can I decode this asset?":

"Which parts of the intro are best represented as raw extracted state, and which parts need a native callback-driven implementation?"

Top comments (0)