<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged emulation</title>
<link>https://coderlegion.com/tag/emulation</link>
<description>Powered by Question2Answer</description>
<item>
<title>Porting Test Drive II from SNES to PC, Part 5: Making scanline validation repo-owned</title>
<link>https://coderlegion.com/13288/porting-test-drive-ii-from-snes-to-pc-part-5-making-scanline-validation-repo-owned</link>
<description>&lt;h2 id=&quot;porting-test-drive-ii-from-snes-to-pc-part-5-making-scanline-validation-repo-owned&quot;&gt;Porting Test Drive II from SNES to PC, Part 5: Making scanline validation repo-owned&lt;/h2&gt;
&lt;p&gt;The cleanup thread in this repo is still product work.&lt;/p&gt;
&lt;p&gt;That point is easy to miss when a change is small. The commit I closed today, &lt;code&gt;224057d&lt;/code&gt; on 2026-03-19, does not add a new renderer, a new extractor, or a new visible gameplay result. What it does is remove another place where the archaeology loop could silently lean on mutable shared emulator output.&lt;/p&gt;
&lt;p&gt;That matters more than it sounds.&lt;/p&gt;
&lt;h3 id=&quot;the-last-cleanup-checkpoint-was-only-half-the-job&quot;&gt;The last cleanup checkpoint was only half the job&lt;/h3&gt;
&lt;p&gt;The previous cleanup commit, &lt;code&gt;d06ac64&lt;/code&gt;, moved the boot-probe path onto repo-owned outputs by default.&lt;/p&gt;
&lt;p&gt;That was a good checkpoint, but it did not close the whole class of problem.&lt;/p&gt;
&lt;p&gt;There was still a committed scanline-validation path that defaulted to Mesen's shared &lt;code&gt;LuaScriptData&lt;/code&gt; area:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;validation/mesen_scanline_step_test.lua&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tools/capture_visible_mode7_range.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That meant a helper could still read from a mutable emulator-owned location unless the caller was careful.&lt;/p&gt;
&lt;p&gt;In a repo like this, that is a real risk:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the same script can be run many times across different frame windows&lt;/li&gt;
&lt;li&gt;multiple archaeology lanes can be active in parallel&lt;/li&gt;
&lt;li&gt;old probe JSON can look plausible enough to reuse by mistake&lt;/li&gt;
&lt;li&gt;automation becomes harder to reason about when &quot;latest file wins&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The whole point of the cleanup track is to make that kind of accidental ambiguity harder.&lt;/p&gt;
&lt;h3 id=&quot;what-changed-in-this-checkpoint&quot;&gt;What changed in this checkpoint&lt;/h3&gt;
&lt;p&gt;This iteration was intentionally narrow.&lt;/p&gt;
&lt;p&gt;The generic Mesen launcher, &lt;code&gt;validation/run_mesen_capture.sh&lt;/code&gt;, now prepares the parent directory for &lt;code&gt;TD2_SCANLINE_TEST_OUTPUT_PREFIX&lt;/code&gt; before launching the emulator.&lt;/p&gt;
&lt;p&gt;That means the scanline probe can be given a repo-owned destination without relying on the caller to pre-create the directory tree.&lt;/p&gt;
&lt;p&gt;Then the committed visible-range helper, &lt;code&gt;tools/capture_visible_mode7_range.py&lt;/code&gt;, was updated so that it no longer defaults to shared &lt;code&gt;LuaScriptData&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Instead, when you run something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;python3 tools/capture_visible_mode7_range.py \
  1094 1101 \
  --output tools/out/visible_mode7_1094_1101.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the helper now derives a repo-owned raw probe prefix beside the aggregate output:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tools/out/visible_mode7_1094_1101_scanline_probe/td2_scanline_step_test.json&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;It also records that raw probe path in the aggregate JSON payload, so the run is easier to trace later.&lt;/p&gt;
&lt;p&gt;That is the important pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one command&lt;/li&gt;
&lt;li&gt;one explicit aggregate output&lt;/li&gt;
&lt;li&gt;one repo-owned raw probe surface attached to that run&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No silent dependence on whatever happened to be left in &lt;code&gt;.mesen-config/Mesen2/LuaScriptData&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;why-this-matters-for-archaeology&quot;&gt;Why this matters for archaeology&lt;/h3&gt;
&lt;p&gt;This project is already juggling several different kinds of evidence:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;emulator captures&lt;/li&gt;
&lt;li&gt;bridge extracts&lt;/li&gt;
&lt;li&gt;direct ROM-derived builders&lt;/li&gt;
&lt;li&gt;runtime manifests&lt;/li&gt;
&lt;li&gt;scanline probes&lt;/li&gt;
&lt;li&gt;contract checks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is healthy, but only if the repo keeps a strong boundary between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;authoritative artifacts&lt;/li&gt;
&lt;li&gt;scratch intermediates&lt;/li&gt;
&lt;li&gt;stale leftovers from old experiments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Shared emulator output blurs that boundary.&lt;/p&gt;
&lt;p&gt;Repo-owned per-run output sharpens it again.&lt;/p&gt;
&lt;p&gt;That pays off immediately in a few ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;validation commands become easier to rerun without cleanup guesswork&lt;/li&gt;
&lt;li&gt;aggregate JSON can point back to the exact raw probe surface it used&lt;/li&gt;
&lt;li&gt;it becomes clearer which artifacts belong to the repo's evidence chain&lt;/li&gt;
&lt;li&gt;future automation can assume less hidden global state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of that is glamorous, but it is exactly the kind of work that keeps a reverse-engineering-heavy port from becoming fragile.&lt;/p&gt;
&lt;h3 id=&quot;what-this-checkpoint-does-not-solve&quot;&gt;What this checkpoint does not solve&lt;/h3&gt;
&lt;p&gt;It is important not to oversell this.&lt;/p&gt;
&lt;p&gt;This commit does not finish the validation-output cleanup track.&lt;/p&gt;
&lt;p&gt;In particular, it does not yet absorb the newer gameplay-oriented scanline wrapper that is currently sitting in the dirty worktree. That helper is part of active lane work and should be closed as its own checkpoint instead of being folded into a cleanup commit by accident.&lt;/p&gt;
&lt;p&gt;So the state after &lt;code&gt;224057d&lt;/code&gt; is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;boot-probe output defaults are repo-owned&lt;/li&gt;
&lt;li&gt;the committed visible-range scanline helper now follows the same policy&lt;/li&gt;
&lt;li&gt;the broader scanline/gameplay cleanup surface is still open&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the right level of scope for one iteration.&lt;/p&gt;
&lt;h3 id=&quot;where-the-project-stands-now&quot;&gt;Where the project stands now&lt;/h3&gt;
&lt;p&gt;The repo's overall posture is still the same:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lane 1 bank30 unresolved queue closure remains the official top gate&lt;/li&gt;
&lt;li&gt;Lane 2 tilemap provenance has solid contiguous coverage through &lt;code&gt;1086..1117&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Lane 3 gameplay archaeology is active and currently lives deep in visible-phase scanline work&lt;/li&gt;
&lt;li&gt;the cleanup/refocus track is still running in parallel so those archaeology lanes stay trustworthy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This checkpoint belongs to that last category.&lt;/p&gt;
&lt;p&gt;It is small, but it makes the validation surface more deterministic, and that gives the later archaeology results a better floor to stand on.&lt;/p&gt;
&lt;h3 id=&quot;next-step&quot;&gt;Next step&lt;/h3&gt;
&lt;p&gt;The next cleanup-side step is straightforward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;push the same repo-owned/per-run policy through the remaining scanline and gameplay wrappers&lt;/li&gt;
&lt;li&gt;keep trimming lingering doc examples that still assume shared emulator output&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After that, the project can keep pressing the more interesting blockers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;958..977&lt;/code&gt; bootstrap gap&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;986+&lt;/code&gt; final-screen composition gap&lt;/li&gt;
&lt;li&gt;the callback-family replacement work beyond the current intro coverage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the shape of progress here.&lt;/p&gt;
&lt;p&gt;Not every useful checkpoint produces a new frame.&lt;/p&gt;
&lt;p&gt;Some of them make sure the next frame result is actually believable.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13288/porting-test-drive-ii-from-snes-to-pc-part-5-making-scanline-validation-repo-owned</guid>
<pubDate>Sat, 21 Mar 2026 13:48:21 +0000</pubDate>
</item>
<item>
<title>Porting Test Drive II from SNES to PC, Part 3: Provenance instead of screenshots</title>
<link>https://coderlegion.com/13286/porting-test-drive-ii-from-snes-to-pc-part-3-provenance-instead-of-screenshots</link>
<description>&lt;h2 id=&quot;porting-test-drive-ii-from-snes-to-pc-part-3-provenance-instead-of-screenshots&quot;&gt;Porting Test Drive II from SNES to PC, Part 3: Provenance instead of screenshots&lt;/h2&gt;
&lt;p&gt;Once the SDL runtime could play a real intro path, the next bottleneck was not rendering. It was evidence.&lt;/p&gt;
&lt;p&gt;If a frame looked close, that was no longer enough. The project needed to answer harder questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which ROM chunk produced the visible tilemap?&lt;/li&gt;
&lt;li&gt;Which callback path was active when that chunk was used?&lt;/li&gt;
&lt;li&gt;Is this frame a stable source window, or just a screenshot that happens to match today?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the phase that starts on March 5 and dominates the repo through March 17.&lt;/p&gt;
&lt;h3 id=&quot;the-repo-grew-a-dedicated-archaeology-workspace&quot;&gt;The repo grew a dedicated archaeology workspace&lt;/h3&gt;
&lt;p&gt;Commit &lt;code&gt;6a12991&lt;/code&gt; on 2026-03-05 added the &lt;code&gt;rom_analysis/&lt;/code&gt; workspace and a lot of the tooling that now defines the back half of the project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;build_bank30_chunk_registry.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;build_mesen_design_pack.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;build_mesen_design_pack_range.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;extract_compression_header_manifest.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate_td2_chunks.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;summarize_l001210_trace.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;new roadmap and checkpoint docs under &lt;code&gt;rom_analysis/docs/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This was a useful structural move for two reasons.&lt;/p&gt;
&lt;p&gt;First, it separated &quot;runtime code that has to ship frames&quot; from &quot;analysis code that has to explain frames.&quot; Those concerns overlap, but they are not the same job.&lt;/p&gt;
&lt;p&gt;Second, it made the archaeology lane explicit. The repo stopped treating bank 10, bank 11, and bank 30 as vague future work. It started giving them artifacts, checkpoints, and pass or fail surfaces.&lt;/p&gt;
&lt;h3 id=&quot;l001210-became-the-main-provenance-hook&quot;&gt;&lt;code&gt;L001210&lt;/code&gt; became the main provenance hook&lt;/h3&gt;
&lt;p&gt;The core trace target in this phase is &lt;code&gt;L001210&lt;/code&gt;, the dispatcher at &lt;code&gt;00:9210&lt;/code&gt; that is tied to chunk usage. The project added probe-side tracing and summary tooling so it could move from static bank scanning to runtime-backed provenance.&lt;/p&gt;
&lt;p&gt;That flow looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;make -C tools l001210-probe L001210_PROBE_TOTAL_FRAMES=3600 MESEN_TIMEOUT_SECONDS=90
make -C tools l001210-trace-summary
make -C tools bank30-registry
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By itself, a bank scan tells you where candidate compressed blocks exist. It does not tell you which ones matter in a live frame window. The &lt;code&gt;L001210&lt;/code&gt; trace changed that.&lt;/p&gt;
&lt;p&gt;The bank-30 registry could now classify candidates by both decode status and runtime visibility. As of the current snapshot, the unresolved queue is still concrete:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1E:E91F&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1E:EE7F&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1E:DA96&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1E:9681&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is better than a hand-wavy &quot;some bank 30 work remains.&quot; It is a small list of named unresolved producers with evidence attached.&lt;/p&gt;
&lt;h3 id=&quot;design-packs-made-the-frame-data-usable-outside-the-emulator&quot;&gt;Design packs made the frame data usable outside the emulator&lt;/h3&gt;
&lt;p&gt;The project also stopped treating raw Mesen dumps as the final output format.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mesen_ppu_extract&lt;/code&gt; and the design-pack builders turned frame windows into bundles that can be inspected like assets instead of debugger state:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;per-layer visible renders&lt;/li&gt;
&lt;li&gt;decoded tilemaps&lt;/li&gt;
&lt;li&gt;tile sheets&lt;/li&gt;
&lt;li&gt;sprite analysis&lt;/li&gt;
&lt;li&gt;raw VRAM/CGRAM/OAM&lt;/li&gt;
&lt;li&gt;stable manifests for a frame or a frame range&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That gave the repo a middle layer between emulator internals and runtime playback. The port could still consume raw scene files, but the engineer could now inspect the same frame as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;raw SNES memory&lt;/li&gt;
&lt;li&gt;decoded tilemap entries&lt;/li&gt;
&lt;li&gt;sprite metadata&lt;/li&gt;
&lt;li&gt;a runtime sequence window&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is why later provenance work stays readable. The project has actual frame packages, not just screenshots and prose.&lt;/p&gt;
&lt;h3 id=&quot;provenance-windows-got-promoted-one-range-at-a-time&quot;&gt;Provenance windows got promoted one range at a time&lt;/h3&gt;
&lt;p&gt;The best example is the tilemap-to-ROM provenance chain around the late attract path.&lt;/p&gt;
&lt;p&gt;Commit &lt;code&gt;bd239a0&lt;/code&gt; on 2026-03-13 promoted provenance windows through &lt;code&gt;1101&lt;/code&gt;. The next commit, &lt;code&gt;7614c8f&lt;/code&gt;, pushed that contiguous reading through &lt;code&gt;1117&lt;/code&gt;. Those windows are not just captured frames. They carry a source argument.&lt;/p&gt;
&lt;p&gt;The current reading in the roadmap is very specific:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bg1&lt;/code&gt; is the visible main-screen layer across the promoted window&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chrBaseWords = 0x2000&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;runtime provenance steps through three chunk anchors:&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1094..1095 -&amp;gt; 0D:C4DC&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1096..1100 -&amp;gt; 07:BF49&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1101..1117 -&amp;gt; 07:C112&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the kind of statement that changes implementation strategy. Once a port effort knows which visible block comes from which chunk family, asset conversion and runtime replacement can be targeted. Without that, every new frame range looks like a one-off.&lt;/p&gt;
&lt;p&gt;The repo is careful here in a way I like: it does not overclaim continuity. It explicitly treats &lt;code&gt;1117&lt;/code&gt; as the current confidence edge and avoids pretending that later frames are solved by carry-over alone.&lt;/p&gt;
&lt;h3 id=&quot;the-same-tooling-started-reaching-gameplay&quot;&gt;The same tooling started reaching gameplay&lt;/h3&gt;
&lt;p&gt;This archaeology phase is not limited to front-end cleanup.&lt;/p&gt;
&lt;p&gt;Commit &lt;code&gt;36767ce&lt;/code&gt; captured the first deterministic track-1 seed window for frames &lt;code&gt;86..93&lt;/code&gt;. Commit &lt;code&gt;207d8f6&lt;/code&gt; followed with a bounded sweep. The current roadmap now says:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.mesen-config/Mesen2/SaveStates/game_11.mss&lt;/code&gt; is a usable deterministic seed&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b_hold&lt;/code&gt; is the first nontrivial dynamic lane&lt;/li&gt;
&lt;li&gt;the queue cursor equalization path is directly observed through frames &lt;code&gt;90..92&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is exactly the right style of progress for gameplay-era reverse engineering. It is not &quot;bank 10 is understood.&quot; It is &quot;this input schedule on this savestate produces this specific observable state transition window.&quot;&lt;/p&gt;
&lt;p&gt;The later timed-input extraction work in &lt;code&gt;802f804&lt;/code&gt; extends the same method to a different provenance problem. The repo recovered a later direct-hit cluster around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;7051 -&amp;gt; 0D:C4DC&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;7059 -&amp;gt; 07:BF49&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;7064 -&amp;gt; 07:C112&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Again, the useful part is not just that those frames were found. It is that they were found under a deterministic scripted-input lane and packaged with provenance outputs.&lt;/p&gt;
&lt;h3 id=&quot;what-changed-in-practice&quot;&gt;What changed in practice&lt;/h3&gt;
&lt;p&gt;Before this phase, the repo could say:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this frame range matches the emulator closely&lt;/li&gt;
&lt;li&gt;this runtime path seems to work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After this phase, the repo could say:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this frame range belongs to callback family &lt;code&gt;01:9FE5&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;this visible tile block maps to specific chunk producers&lt;/li&gt;
&lt;li&gt;this gameplay seed window is reproducible from a known savestate and input schedule&lt;/li&gt;
&lt;li&gt;these bank-30 candidates remain unresolved even after matrix sweeps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is a different level of engineering maturity.&lt;/p&gt;
&lt;p&gt;It also changes what &quot;port progress&quot; means. A project like this does not move linearly from renderer to gameplay. It moves by reducing uncertainty in the places where rendering, content, and control flow meet.&lt;/p&gt;
&lt;p&gt;By mid-March, &lt;code&gt;asmdump&lt;/code&gt; had become good at that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;deterministic capture&lt;/li&gt;
&lt;li&gt;design-ready frame bundles&lt;/li&gt;
&lt;li&gt;trace-backed chunk provenance&lt;/li&gt;
&lt;li&gt;promoted source windows with explicit confidence edges&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That set up the next phase cleanly. Once the repo could explain its frame windows, the next pressure point was obvious: the validation surface had to become stricter, and the repo itself had to get cleaner so generated output did not drown the actual evidence.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13286/porting-test-drive-ii-from-snes-to-pc-part-3-provenance-instead-of-screenshots</guid>
<pubDate>Sat, 21 Mar 2026 13:29:08 +0000</pubDate>
</item>
<item>
<title>Porting Test Drive II from SNES to PC, Part 2: The first native intro slice</title>
<link>https://coderlegion.com/13285/porting-test-drive-ii-from-snes-to-pc-part-2-the-first-native-intro-slice</link>
<description>&lt;h2 id=&quot;porting-test-drive-ii-from-snes-to-pc-part-2-the-first-native-intro-slice&quot;&gt;Porting Test Drive II from SNES to PC, Part 2: The first native intro slice&lt;/h2&gt;
&lt;p&gt;The first two days in &lt;code&gt;asmdump&lt;/code&gt; were about measurability. The next phase, from February 28 through March 2, was about replacing a narrow front-end slice with native code.&lt;/p&gt;
&lt;p&gt;That is the point where the project stopped proving that it could extract data and started proving that it could replay behavior.&lt;/p&gt;
&lt;h3 id=&quot;the-runtime-target-was-small-on-purpose&quot;&gt;The runtime target was small on purpose&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;port/&lt;/code&gt; runtime was never meant to be a full game engine in week one. It was a controlled execution environment for known assets and known frame windows.&lt;/p&gt;
&lt;p&gt;By this stage, &lt;code&gt;port/README.md&lt;/code&gt; already described a focused runtime:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SDL2 window and renderer&lt;/li&gt;
&lt;li&gt;fixed 60 Hz loop&lt;/li&gt;
&lt;li&gt;256x224 framebuffer&lt;/li&gt;
&lt;li&gt;direct SNES BG rendering from extracted &lt;code&gt;VRAM + CGRAM + PPU state&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;optional OBJ composition from extracted OAM&lt;/li&gt;
&lt;li&gt;sequence playback for extracted scene windows&lt;/li&gt;
&lt;li&gt;indexed palette-animation playback for compact native clips&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last distinction mattered immediately. Some intro segments are better represented as sampled frame sequences. Others are better represented as a native state machine driven by extracted data.&lt;/p&gt;
&lt;p&gt;The project did not try to answer that in the abstract. It answered it on one scene.&lt;/p&gt;
&lt;h3 id=&quot;why-the-ballistic-splash-was-the-right-first-target&quot;&gt;Why the Ballistic splash was the right first target&lt;/h3&gt;
&lt;p&gt;The Ballistic intro window is anchored cleanly at frame &lt;code&gt;654&lt;/code&gt;. It is also a good engineering target:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it is deterministic under no input&lt;/li&gt;
&lt;li&gt;it is presentation-heavy but structurally simple&lt;/li&gt;
&lt;li&gt;it is mostly a palette-driven BG scene, not a sprite-heavy gameplay state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The repo ended up carrying three versions of the same clip:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a measured reference clip from deterministic screenshots&lt;/li&gt;
&lt;li&gt;a ROM-derived clip generated from helper-scene CGRAM plus the &lt;code&gt;A39C&lt;/code&gt; palette ramp&lt;/li&gt;
&lt;li&gt;a compact callback asset for direct runtime playback via &lt;code&gt;ballistic_a39c&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That progression matters. The measured clip proves what the frame window should look like. The ROM-derived clip proves that the asset path can reconstruct it from source data. The callback asset proves that the runtime can execute a compact native representation instead of replaying a bag of screenshots.&lt;/p&gt;
&lt;p&gt;By the time commit &lt;code&gt;aa67d27&lt;/code&gt; landed on 2026-02-28, the repo had already marked the milestone directly: &quot;reproduce intro animation natively.&quot;&lt;/p&gt;
&lt;h3 id=&quot;hybrid-playback-was-a-feature-not-a-compromise&quot;&gt;Hybrid playback was a feature, not a compromise&lt;/h3&gt;
&lt;p&gt;One of the best early decisions in this project was to allow hybrid manifests instead of forcing every frame range into the same representation.&lt;/p&gt;
&lt;p&gt;That produced several useful runtime artifacts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tools/out/ballistic_native_sequence.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tools/out/ballistic_rom_sequence.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tools/out/ballistic_callback_sequence.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tools/out/intro_loop_hybrid_sequence.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tools/out/intro_loop_hybrid_bridge_visible_sequence.txt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The project used those manifests to keep progress moving while specific windows remained unresolved.&lt;/p&gt;
&lt;p&gt;The no-input intro loop is the clearest example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;direct runtime &lt;code&gt;ballistic_a39c&lt;/code&gt; playback covers the Ballistic segment&lt;/li&gt;
&lt;li&gt;sampled bootstrap playback bridges the unstable &lt;code&gt;958..977&lt;/code&gt; window&lt;/li&gt;
&lt;li&gt;queue-driven &lt;code&gt;snes_bg&lt;/code&gt; replacements cover frames &lt;code&gt;978&lt;/code&gt; and &lt;code&gt;982&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;later attract states can be swapped between sampled and native bridge-visible windows as coverage improves&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a better engineering shape than an all-or-nothing rewrite. Every new native window can replace a sampled window when it is good enough, without destabilizing the rest of the loop.&lt;/p&gt;
&lt;h3 id=&quot;the-bootstrap-and-bridge-work-forced-more-precise-tooling&quot;&gt;The bootstrap and bridge work forced more precise tooling&lt;/h3&gt;
&lt;p&gt;The easy story would have been &quot;render the Ballistic clip and move on.&quot; The real story is that getting beyond it forced the repo to model the transition frames precisely.&lt;/p&gt;
&lt;p&gt;The project added:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;repeatable &lt;code&gt;L00A00C&lt;/code&gt; scene building&lt;/li&gt;
&lt;li&gt;decoded WRAM queue summaries for the bootstrap window&lt;/li&gt;
&lt;li&gt;queue-driven scene builders for post-Ballistic frames&lt;/li&gt;
&lt;li&gt;OAM-aware &lt;code&gt;snes_bg&lt;/code&gt; playback&lt;/li&gt;
&lt;li&gt;bridge-visible Mode 7 scene models for the late attract path&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is how the repo got to statements that are actually useful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;frame &lt;code&gt;978&lt;/code&gt; and frame &lt;code&gt;982&lt;/code&gt; can be reconstructed from the queued VRAM/OAM path&lt;/li&gt;
&lt;li&gt;frame &lt;code&gt;986&lt;/code&gt; is close on the BG side but still wrong in OBJ composition&lt;/li&gt;
&lt;li&gt;late bridge-visible frames through &lt;code&gt;1093&lt;/code&gt; can run natively in the SDL runtime, even when screenshot parity is still open for some of them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The runtime command surface reflects that evolution:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;make -C port

./port/build/td2_port --sequence ./tools/out/ballistic_callback_sequence.txt
./port/build/td2_port --sequence ./tools/out/intro_loop_hybrid_sequence.txt
./port/build/td2_port --sequence ./tools/out/intro_loop_hybrid_bridge_visible_sequence.txt
./port/build/td2_port --headless --sequence ./tools/out/intro_loop_hybrid_bridge_visible_sequence.txt --sequence-no-loop --frames 1418 --dump-prefix ./port/build/intro_loop_hybrid_bridge_visible
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point the port is no longer just &quot;an SDL viewer for extracted images.&quot; It is executing multiple representations of front-end state:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pure extracted scene windows&lt;/li&gt;
&lt;li&gt;compact callback assets&lt;/li&gt;
&lt;li&gt;palette-driven indexed animations&lt;/li&gt;
&lt;li&gt;queue-derived bridge scenes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the real milestone.&lt;/p&gt;
&lt;h3 id=&quot;the-code-had-to-be-reorganized-early&quot;&gt;The code had to be reorganized early&lt;/h3&gt;
&lt;p&gt;Commit &lt;code&gt;8858823&lt;/code&gt; on 2026-03-01 is one of the most practical commits in the whole history. It split a &lt;code&gt;3083&lt;/code&gt;-line &lt;code&gt;main.c&lt;/code&gt; into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;td2_types.h&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;td2_io.c/.h&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;td2_ppu.c/.h&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a much smaller &lt;code&gt;main.c&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That same commit also added:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docs/bootstrap_9D69_analysis.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docs/bank10_11_30_notes.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;port/test_regression.sh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not incidental cleanup. It is the point where the runtime stopped being a prototype and started becoming an environment that other port work could live inside.&lt;/p&gt;
&lt;p&gt;There are two reasons that matters:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The PPU and scene-loading path were already complicated enough to deserve their own modules.&lt;/li&gt;
&lt;li&gt;The reverse-engineering notes and the runtime code had started to depend on each other in both directions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once the project reached hybrid intro playback, runtime work and archaeology work were no longer separate lanes. The runtime needed more exact hardware facts. The archaeology needed a runtime that could replay extracted states and make errors visible.&lt;/p&gt;
&lt;h3 id=&quot;first-native-launch-means-more-than-the-window-opened-&quot;&gt;First native launch means more than &quot;the window opened&quot;&lt;/h3&gt;
&lt;p&gt;Commit &lt;code&gt;87a3f67&lt;/code&gt; on 2026-03-02 is named &quot;first native launch running intro anim.&quot; The important part of that message is not &quot;launch.&quot; It is &quot;running intro anim.&quot;&lt;/p&gt;
&lt;p&gt;At that point, the project had already moved from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;raw ROM and disassembly&lt;/li&gt;
&lt;li&gt;to deterministic captures and extraction&lt;/li&gt;
&lt;li&gt;to a PC runtime capable of replaying a real front-end path with native logic in the loop&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the point where the questions get more expensive and more interesting.&lt;/p&gt;
&lt;p&gt;You are no longer asking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can I decode this chunk?&lt;/li&gt;
&lt;li&gt;Can I render this tilemap?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You are asking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which frames still need sampled playback?&lt;/li&gt;
&lt;li&gt;Which callbacks can be promoted to native code?&lt;/li&gt;
&lt;li&gt;Where does the remaining mismatch come from: BG state, OAM state, queue timing, Mode 7 math, or presentation order?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is why the next phase in the repo turns hard toward provenance, callback tracing, and window-by-window archaeology. Once the renderer can play the intro, the bottleneck is no longer drawing. The bottleneck is explaining exactly where each visible window comes from.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13285/porting-test-drive-ii-from-snes-to-pc-part-2-the-first-native-intro-slice</guid>
<pubDate>Sat, 21 Mar 2026 13:28:35 +0000</pubDate>
</item>
<item>
<title>Porting Test Drive II from SNES to PC, Part 1: Day 0 to a measurable ROM</title>
<link>https://coderlegion.com/13284/porting-test-drive-ii-from-snes-to-pc-part-1-day-0-to-a-measurable-rom</link>
<description>&lt;h2 id=&quot;porting-test-drive-ii-from-snes-to-pc-part-1-day-0-to-a-measurable-rom&quot;&gt;Porting Test Drive II from SNES to PC, Part 1: Day 0 to a measurable ROM&lt;/h2&gt;
&lt;p&gt;Between February 26 and March 19, 2026, the &lt;code&gt;asmdump&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h3 id=&quot;day-0-the-repo-was-a-reverse-engineering-base&quot;&gt;Day 0: the repo was a reverse-engineering base&lt;/h3&gt;
&lt;p&gt;Commit &lt;code&gt;a683c61&lt;/code&gt; on 2026-02-26 is the real starting point. It imported:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bank0.asm&lt;/code&gt; through &lt;code&gt;bank31.asm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;game.smc&lt;/code&gt; and &lt;code&gt;game.sym&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ROM build files like &lt;code&gt;Makefile&lt;/code&gt;, &lt;code&gt;linkfile&lt;/code&gt;, &lt;code&gt;main.s&lt;/code&gt;, &lt;code&gt;snes.asm&lt;/code&gt;, and &lt;code&gt;spc700.asm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is enough to rebuild and inspect the ROM. It is not enough to port it.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;The absence of a direct decompilation path matters here. &lt;code&gt;bank0.asm&lt;/code&gt; 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:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bank10.asm&lt;/code&gt; and &lt;code&gt;bank11.asm&lt;/code&gt; still mixed code and data heavily&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bank30.asm&lt;/code&gt; looked more like structured tables and dispatcher data than a clean list of callable functions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;game.sym&lt;/code&gt; exposed labels and coverage, not behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the early plan was not &quot;translate bank 0, then bank 1, then bank 10&quot;. The early plan was:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make the original ROM measurable.&lt;/li&gt;
&lt;li&gt;Extract typed data from it.&lt;/li&gt;
&lt;li&gt;Rebuild systems on the PC side against those measured outputs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That decision is still the right one. A reliable port needs a reference harness before it needs more C files.&lt;/p&gt;
&lt;h3 id=&quot;the-first-burst-of-tooling&quot;&gt;The first burst of tooling&lt;/h3&gt;
&lt;p&gt;February 27 has nine commits. That entire day is the actual day-1 port effort.&lt;/p&gt;
&lt;p&gt;The important commits are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;c867efd&lt;/code&gt; - SNES asset extraction toolchain scaffolding&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fcc30b5&lt;/code&gt; - scene, sprite, and road visualization tooling&lt;/li&gt;
&lt;li&gt;&lt;code&gt;f8785b9&lt;/code&gt; - fully integrated Qt asset viewer UI&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2bc9dc2&lt;/code&gt; - initial scripts, sprite translation, capture data, and the first runtime skeleton&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The repo stopped treating the ROM as a monolith and started turning it into inspectable outputs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;palette dumps as JSON&lt;/li&gt;
&lt;li&gt;VRAM reconstruction and boot-screen manifests&lt;/li&gt;
&lt;li&gt;decompressed chunk outputs&lt;/li&gt;
&lt;li&gt;tile sheets rendered to neutral image formats&lt;/li&gt;
&lt;li&gt;Mesen-based screenshot and state capture&lt;/li&gt;
&lt;li&gt;scene visualizers that could be driven outside the emulator&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Representative commands from that stage still describe the approach well:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;./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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;None of that is glamorous. All of it is necessary.&lt;/p&gt;
&lt;p&gt;The early &lt;code&gt;validation/&lt;/code&gt; scripts mattered because they made the emulator part of the build surface instead of a manual debugging tool. &lt;code&gt;mesen_capture.lua&lt;/code&gt; gave the project deterministic frame dumps and input logs. &lt;code&gt;mesen_probe_boot.lua&lt;/code&gt; started recording boot and title-state selectors frame by frame. Once those existed, &quot;what does the ROM do here?&quot; stopped being a memory question and became a file question.&lt;/p&gt;
&lt;h3 id=&quot;typed-outputs-changed-the-project&quot;&gt;Typed outputs changed the project&lt;/h3&gt;
&lt;p&gt;The most important change in those first two days was not the number of tools. It was the move toward typed outputs.&lt;/p&gt;
&lt;p&gt;There is a real difference between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;I have a PNG of the title screen&quot;&lt;/li&gt;
&lt;li&gt;and &quot;I have VRAM, CGRAM, OAM, PPU state, decompressed source chunks, and the command sequence that built this screen&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first is useful for discussion. The second is useful for engineering.&lt;/p&gt;
&lt;p&gt;By the end of the initial toolchain burst, the repo already had examples of that second category:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tools/out/bank3_palettes.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tools/out/bank1_boot_screen.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tools/out/bank1_boot_vram_variant0.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tools/out/bank7_42fb_8000.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;deterministic Mesen capture logs under &lt;code&gt;.mesen-config/Mesen2/LuaScriptData/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That gave the project a clean boundary between &quot;ROM-side fact&quot; and &quot;PC-side implementation&quot;. 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.&lt;/p&gt;
&lt;h3 id=&quot;why-the-visualizers-mattered&quot;&gt;Why the visualizers mattered&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fcc30b5&lt;/code&gt; and &lt;code&gt;f8785b9&lt;/code&gt; are easy to dismiss as UI work, but they are part of the core engineering story.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;On a project like this, the fast path is rarely &quot;write more code&quot;. The fast path is usually:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;expose the data&lt;/li&gt;
&lt;li&gt;make it inspectable&lt;/li&gt;
&lt;li&gt;reduce the number of manual emulator steps between a hypothesis and a rendered result&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h3 id=&quot;what-the-repo-looked-like-after-the-first-real-day&quot;&gt;What the repo looked like after the first real day&lt;/h3&gt;
&lt;p&gt;By the end of &lt;code&gt;2bc9dc2&lt;/code&gt;, the repo had crossed a line:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it still was not a gameplay port&lt;/li&gt;
&lt;li&gt;it still was not a decompilation&lt;/li&gt;
&lt;li&gt;but it was no longer just a disassembly dump&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It had become a measurable workbench:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;deterministic captures&lt;/li&gt;
&lt;li&gt;extraction scripts&lt;/li&gt;
&lt;li&gt;decompression tooling&lt;/li&gt;
&lt;li&gt;typed manifests&lt;/li&gt;
&lt;li&gt;visual inspection tools&lt;/li&gt;
&lt;li&gt;the first C/SDL runtime skeleton under &lt;code&gt;port/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;That slice ended up being the Ballistic intro path, and it forced the project to answer a harder question than &quot;can I decode this asset?&quot;:&lt;/p&gt;
&lt;p&gt;&quot;Which parts of the intro are best represented as raw extracted state, and which parts need a native callback-driven implementation?&quot;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13284/porting-test-drive-ii-from-snes-to-pc-part-1-day-0-to-a-measurable-rom</guid>
<pubDate>Sat, 21 Mar 2026 13:28:13 +0000</pubDate>
</item>
</channel>
</rss>