DEV Community

Nivando Soares
Nivando Soares

Posted on

Porting Test Drive II from SNES to PC, Part 19: Splitting the post-1093 attract window with active Mesen traces

Porting Test Drive II from SNES to PC, Part 19: Splitting the post-1093 attract window with active Mesen traces

The previous checkpoint closed the 986..1093 late-attract block as one continuous callback family.

That was enough to say who owned the window.

It was not enough to say why the continuation after 1093 stopped behaving like one uniform block.

This checkpoint fixes that by adding one more layer between "frame dump" and "render mismatch":

an activity trace built directly from Mesen probe output.

Why this mattered

Before this change, the repo could already answer:

  • what the frame looked like
  • which callback family was active at selected anchors
  • whether bounded write-point probes hit VRAM, CGRAM, or OAM

What it still could not answer cleanly was:

  • whether a late mismatch boundary matched a real behavior change
  • whether the continuation after 1093 was still one producer shape
  • whether the next useful split should follow ownership, DMA, or Mode 7 programming

That is the gap the new trace closes.

The new tool

The repo now has a dedicated normalizer:

  • tools/build_mesen_activity_trace.py

It takes one td2_boot_probe.json plus the optional detailed probe traces:

  • *_dma_writes.json
  • *_vram_writes.json
  • *_mode7_writes.json

and turns them into one frame-oriented activity report with:

  • active main callback
  • active IRQ callback
  • sampled state tuple
  • grouped DMA events
  • grouped direct VRAM/CGRAM writes
  • grouped Mode 7 register programming events

The important part is not the JSON format by itself.

The important part is that the late attract lane can now reason about producer behavior with the same granularity it was already using for visual contracts.

The bounded run

I kept the run narrow and targeted to the first ambiguous continuation:

  • frames 1094..1117

The exact commands were:

MESEN_RELEASE_DIR=/home/nivando-soares/Mesen2/bin/linux-x64/Release \
MESEN_TIMEOUT_SECONDS=180 \
TD2_BOOT_PROBE_OUTPUT_PREFIX=tools/out/activity_trace_1094_1117/td2_boot_probe \
TD2_BOOT_PROBE_TOTAL_FRAMES=1118 \
TD2_BOOT_PROBE_TRACE_START_FRAME=1094 \
TD2_BOOT_PROBE_TRACE_END_FRAME=1117 \
TD2_BOOT_PROBE_TRACE_DMA=1 \
TD2_BOOT_PROBE_TRACE_VRAM=1 \
TD2_BOOT_PROBE_TRACE_MODE7=1 \
./validation/run_mesen_probe_boot.sh

python3 tools/build_mesen_activity_trace.py \
  tools/out/activity_trace_1094_1117/td2_boot_probe.json \
  tools/out/activity_trace_1094_1117/activity_trace.json \
  --markdown-out tools/out/activity_trace_1094_1117/activity_trace.md
Enter fullscreen mode Exit fullscreen mode

The promoted outputs are:

  • tools/out/activity_trace_1094_1117/activity_trace.json
  • tools/out/activity_trace_1094_1117/activity_trace.md
  • rom_analysis/docs/intro_01_9fe5_activity_trace_1094_1117.md

What the trace showed

The result is sharper than another screenshot comparison would have been.

Across 1094..1117:

  • there are no direct VRAM/CGRAM writes
  • the callback switch lands exactly at 1102
  • the repeated OAM DMA continues only through 1113
  • the late Mode 7 M7A/M7D programming on scanline 231 disappears after 1101

More concretely:

  • 1094..1101
    • active main callback: 01:9FE5
    • repeated 00:0700 -> 00:2104 544-byte OAM DMA is still present
    • the late scanline-231 M7A/M7D packet is still present
  • 1102..1113
    • active main callback: 00:8029
    • the same repeated OAM DMA is still present
    • the late scanline-231 M7A/M7D packet is gone
  • 1114..1117
    • active main callback: 00:8029
    • the repeated OAM DMA is gone too
    • no direct VRAM/CGRAM writes appear here either

The sampled state tuple stays stable across the 1102 callback switch:

  • $0204 = 1
  • $0206 = 13
  • $040A = 17
  • $0054 = 128

That matters because it rules out a cheap explanation like "the whole scene state jumped."

It did not.

The behavior changed before the sampled state tuple did.

Why this is useful

This changes the pipeline in a practical way.

Before this checkpoint, the late attract lane mostly had:

  • screenshots
  • main_visible.ppm
  • ppu_state.json
  • tilemap provenance
  • coarse producer traces

After this checkpoint, it also has a causal layer:

  • which callback family owned each subwindow
  • whether OAM uploads were still happening
  • whether the late Mode 7 packet was still happening
  • whether a boundary was real or just visual noise

So the post-1093 continuation no longer has to be treated as one vague tail.

It can now be reasoned about as three concrete subwindows:

  • 1094..1101
  • 1102..1113
  • 1114..1117

That is exactly the kind of split the next archaeology step needs.

Validation

This checkpoint used bounded validation only:

python3 -m py_compile tools/build_mesen_activity_trace.py
Enter fullscreen mode Exit fullscreen mode

plus:

  • a smoke read against legacy trace payloads
  • one fresh headless Mesen run for 1094..1117

That was enough to falsify the tool and confirm the late-window behavior split without turning the checkpoint into another full regression sweep.

What comes next

The next useful step is not to keep restating that the post-1093 block differs.

The next step is to explain the two new boundaries separately:

  • why 1102..1113 keeps OAM DMA after the callback switch
  • why 1114..1117 stops even that and becomes a no-direct-upload tail

That is a much better starting point for the following renderer and ownership work than "something changes after 1093."

The headline for this checkpoint is simple:

the repo can now map specific game behavior actively through Mesen, and the late attract continuation after 1093 is no longer one block but three behaviorally distinct windows.

Top comments (0)