Porting Test Drive II from SNES to PC: from disassembly dump to port workbench
The current asmdump project started from a simple idea:
rebuild The Duel: Test Drive II as a faithful PC port in C and SDL, but do it from measured behavior and extracted assets instead of pretending the disassembly is already ready for a straight translation.
That distinction shaped the whole project.
The repo did not begin as a gameplay port. It began as a reverse-engineering base: assembly banks, a ROM image, symbol coverage, and a working ROM rebuild path. That was useful, but it was not yet a platform for porting. There was no deterministic capture workflow, no stable asset pipeline, no validation harness, and no clear contract between "what the original game does" and "what the PC runtime should reproduce."
The main development objective from the start was to change that.
The original objective
The goal in PORT_PLAN.md is still the correct one:
- build a faithful PC reimplementation in C with SDL
- treat it as a runtime rebuild, not a direct assembly-to-C conversion
- drive the implementation from extracted content and verified behavior
That approach was chosen for practical reasons.
Some parts of the disassembly are already readable. bank0.asm exposes recognizable reset, NMI, IRQ, and callback scheduling logic. But the gameplay-facing banks are not yet in a form where a direct decompilation effort would be the fastest path to a shippable result:
-
bank10.asmandbank11.asmstill mix code and data heavily -
bank30.asmbehaves more like structured dispatch and content tables than a clean set of well-isolated functions - the symbol file gives address coverage, not architecture
So the working strategy became:
- make the original ROM measurable
- extract typed content and frame-state artifacts from it
- build a native PC runtime against those artifacts
- reverse-engineer only as deep as needed to replace sampled playback with native systems
That is the base idea behind the repo. Everything else follows from it.
What "measurable" meant in practice
The first real milestone was not gameplay code. It was a reference harness.
The project added deterministic Mesen-based workflows for:
- frame dumps
- input logs
- VRAM, CGRAM, and OAM capture
- PPU state export
- boot and callback selector tracing
That changed the project from "I can inspect the ROM" to "I can reproduce and record specific frame windows under controlled conditions."
This is the line that matters in a port:
- a screenshot is useful
- a screenshot plus VRAM, CGRAM, OAM, PPU state, input schedule, and trace data is actionable
Once the repo could produce that second kind of output, the port stopped depending on memory and manual emulator work. Questions like "what happens at frame 978?" or "which callback is active in this attract window?" could be answered with artifacts instead of guesswork.
The first development objective after that
Once the ROM was measurable, the next objective was to create a stable extraction pipeline.
That pipeline now covers a lot of ground:
- palette extraction
- boot and helper-scene manifests
- partial VRAM reconstruction
- compressed chunk scanning and decompression
- Mesen-driven scene extraction
- design-pack generation for tilemaps, tilesets, and sprites
- range-based sequence manifests for runtime playback
The useful shift here was not just "more scripts." It was the move toward typed outputs.
The project started producing artifacts that the runtime and the reverse-engineering work could both rely on:
- JSON manifests
- binary memory dumps
- decoded tilemaps
- stable per-frame scene folders
- machine-readable provenance summaries
That made the port architecture much cleaner. The PC runtime could target stable extracted formats instead of depending on ad hoc one-off exporters or hand-maintained notes.
Why the first native target was the intro
The project did not jump straight into gameplay. It chose the front-end and attract path first.
That was a good development objective because the intro has several properties that make it useful as a port target:
- deterministic no-input windows
- visually rich output
- strong dependency on rendering correctness
- lower dependence on gameplay physics and AI contracts
The Ballistic splash became the first native slice. The repo now carries three useful representations of that sequence:
- a measured clip derived from deterministic captures
- a ROM-derived clip rebuilt from helper-scene assets and palette behavior
- a compact callback asset played directly by the SDL runtime
That progression is the project in miniature.
First measure the output. Then rebuild it from source data. Then replace the sampled representation with a native one where the behavior is understood well enough.
The runtime evolved around that exact need. port/ is not trying to be the whole game yet. It is a controlled execution environment for:
- direct SNES BG scene playback from extracted
VRAM + CGRAM + PPU state - optional OBJ composition from OAM
- exact sampled sequence playback
- compact indexed animation playback
- callback-driven native playback for known front-end paths
That is why the current intro loop is hybrid by design. Some windows are solved natively. Some are still replayed from extracted state. Some windows are accurate against bridge-visible state but still not fully screenshot-exact. The runtime is built to let those representations coexist without blocking progress.
The reverse-engineering objective changed after the first native slice
Once the runtime could replay real intro paths, the main question changed.
The project was no longer asking only:
- can this data be extracted?
- can this frame be rendered?
It now had to ask:
- where does this visible state come from?
- which chunk producer or callback family owns it?
- which frame windows are exact, which are derived, and which are still placeholders?
That is why the repo grew rom_analysis/ as a focused archaeology workspace.
The new objective there is not "read more assembly." It is tighter:
- map bank-0 scheduling into bank-30 and gameplay handoff paths
- identify the contracts for bank 10 physics and bank 11 rendering
- tie visible tilemaps back to ROM chunk provenance
- turn frame windows into explainable data, not just correct-looking outputs
The L001210 tracing and bank-30 registry work are central to that. They let the repo move from static bank scans to runtime-backed statements about chunk usage. The tilemap provenance windows do the same thing from the visible side: they turn frame ranges into source arguments instead of image comparisons.
That is an important development objective for any port that intends to survive beyond its first demo. A frame that merely looks right is fragile. A frame whose source and control path are understood is portable.
Validation became part of the architecture
A project like this eventually reaches a point where more output is not enough. It needs contracts.
That is the current stage of asmdump.
The repo now has:
- regression gates for known intro windows
- callback-state contract checks
- synthetic renderer fixtures for isolated bugs
- documented checkpoints for archaeology lanes
This is one of the strongest changes in the repo so far. Validation is no longer implicit. It is becoming executable.
That matters because the project now has several parallel surfaces moving at once:
- extraction tooling
- SDL runtime changes
- emulator bridge work
- provenance and archaeology tools
- gameplay-seed experiments
Without explicit gates, the repo could make impressive-looking progress while quietly regressing solved windows. The current validation layer is how the project avoids that.
Current status
As of the March 19, 2026 snapshot, the repo has moved through 45 commits and now behaves like a real port workbench instead of a disassembly dump.
What is already real:
- a modular C/SDL runtime under
port/ - deterministic Mesen-based validation and probe workflows
- extraction of palettes, chunks, scene windows, and frame-state artifacts
- design-pack generation for tilemaps and sprite inspection
- native Ballistic playback and hybrid intro-loop playback
- bridge-visible native intro coverage through frame
1093 - tilemap provenance windows through
1117 - deterministic gameplay seed windows for early track-1 analysis
- regression gates and callback-state contracts
What is still not done:
- no gameplay system is ported end to end
- the
958..977bootstrap gap is still unresolved - the final-screen composition gap after
982is still open - bank 10, bank 11, and bank 30 contracts are not complete enough for full gameplay implementation
- some late intro windows are bridge-accurate but not yet final-screen accurate
That is the right kind of unfinished state. The repo does not just know that work remains. It knows where the work remains and has artifacts around those boundaries.
The development objectives now
The current objectives are not vague. They are already encoded in the roadmap.
The immediate development priorities are:
- keep cleaning the repo and validation surface
- close renderer correctness gaps with targeted fixtures
- continue replacing sampled front-end windows with native callback/state playback
- finish the unresolved bootstrap and late-intro composition edges
- continue bank-30, bank-10, and bank-11 archaeology until gameplay systems have usable contracts
There is also a broader objective underneath those tasks:
turn the current intro-focused workbench into a reusable platform for gameplay-era porting.
That means:
- more explicit contracts
- more stable extracted formats
- less dependence on personal environment state
- more per-run validation isolation
- stronger links between measured runtime behavior and PC-side implementation
Why this approach is working
The project is making progress because it is not forcing one method onto every problem.
It uses:
- emulator capture for measurement
- extraction scripts for typed content
- native runtime code for replacement paths
- archaeology tooling for unresolved control flow
- regression gates for stabilization
That mix is what makes the current state believable.
The repo is not just accumulating notes and screenshots. It is building a system that can:
- record a target
- extract its inputs
- replay it natively or from extracted state
- compare the result
- explain where the visible data came from
- keep known-good windows from drifting
That is the actual development story so far.
The project began with a disassembly and a ROM. The objective was never just to understand them. The objective was to turn that understanding into a PC runtime that can eventually replace the original game scene by scene, contract by contract, until the measured reference is no longer needed for the solved parts.
That is still where the work is going.
Top comments (0)