All SDK docs

Host Tools

Analysing frames on a laptop with the real emitter, capturing the list the console actually ran, and the measurement rules that keep results honest.

The tools in sdk/uvm2-sdk/tools/ are standalone programs that link the real emitter (uvm2_draw.c and the Rust beam model) against a simulated bus. You can analyse a frame on a laptop exactly as the cartridge would build it. They aren't part of any build; each one's header gives its command line.

Building them

sdk/uvm2-sdk/tools/build_host_tools.sh

This links each tool with the shared host stubs and with -DUVM2_HZ=0. Without that flag the 50 Hz lock pads every list to 30,000 cycles, and a tool measuring a frame ends up measuring the padding.

The tools

toolanswers
uvm2_list_count.cHow many commands, bus words and bus cycles a real dumped frame needs, broken down by VIA register, plus the geometry that explains it: stroke lengths, how many are chained, jump distances, brightness changes, and the longest run with no re-centring.
uvm2_anatomy.cDecodes the list command by command for a synthetic chain. A totals table says how much; this says what, and it's the only thing that shows a delay hanging off the wrong register.
uvm2_op_cost.cThe marginal cost of each primitive (move, draw, intensity, chained draw).
uvm2_order.cHow much reordering strokes would save, in five orderings, on a real dump.
uvm2_ramp_cost.c, uvm2_budget.c, uvm2_gapped_budget.cRamp and frame-budget models.
uvm2_smp_test.cSample injection coverage against scene complexity.
list_from_ram.pyThe list the console is replaying, read over SWD (static screens).
list_from_sd.pyThe list from the SD card, written by the game or the Debug Cart BIOS. No probe needed.
list_from_rtt.pyThe list from the Debug Cart BIOS's RTT dump.
beam_sim.pyPlays a list against an ideal beam and reports what the list gets wrong. Draws the lit strokes as an SVG, with the blanked beam's path in faint red.

Getting the list the console actually ran

There are three ways, depending on what you have. None of them halts the console.

With the probe, on a static screen

python3 sdk/uvm2-sdk/tools/list_from_ram.py $ELF list.json

See Debugging. Builds with the list in PSRAM need --psram <UVM2_CMD_CAPACITY>.

From the SD card, no probe

In a .um2, the game calls this once a frame:

uvm2_dump_list_on_buttons("debug/list.bin", mask);   /* writes once per press */

It writes the last closed frame with a header (count, frame, dropped, ramps_clamped, a hash). The screen goes dark while the card writes, so this is a capture, not a per-frame log. uvm2_dump_diag says whether it ran and, if not, why.

Debug Cart: the BIOS does this for every game on buttons 3 + 4, into DEBUG/LIST.BIN. That file must already exist on the card at 40 KB or more, because the BIOS can only overwrite in place: dd if=/dev/zero of=LIST.BIN bs=1k count=40.

python3 sdk/uvm2-sdk/tools/list_from_sd.py /Volumes/<card>/DEBUG/LIST.BIN list.json

The reader refuses a file whose magic, length or hash is wrong, and warns if dropped isn't zero.

Over RTT (Debug Cart only)

Freeze the frame and hold buttons 1 + 2 while probe-rs attach --chip RP235x <bios.elf> (which halts nothing) saves the RTT output, then:

python3 sdk/uvm2-sdk/tools/list_from_rtt.py rtt.log list.json

The UVMC2 has no RTT.

Then replay it

python3 sdk/uvm2-sdk/tools/beam_sim.py list.json lit.svg

beam_sim.py reports ramps started with the zero clamp on (must be 0), lit cycles under the clamp, the frame's length, and how long the integrators ran dark and free. A slow dark sweep becomes a visible line once the brightness is turned up. The model is ideal, so any fault it shows is in the list, not in the console.


Feeding uvm2_list_count

The input is lines of x0 y0 x1 y1 z, in the units the game passes to v_directDraw32. The arcade ports' host harnesses already dump that format:

cd game/tacscan && make host && ./build/host_ts 60 2>/tmp/frame.txt

Mind the units. The AAE ports build their host harness with -DNO_PI, which sets the screen multiplier to 1, so the dump is in the game's units. The real target multiplies by 36 and recentres. Feed game units to the emitter as they are and it measures ramps 36 times too short:

MUL=36 OX=-13356 OY=-13968 /tmp/count /tmp/frame.txt    # AAE ports
/tmp/count /tmp/frame.txt                               # identity

Rules that were learned the hard way

  • Compare the binaries before comparing the measurements. Four identical results in a row usually means the artefact never rebuilt. Check its timestamp before checking the code.
  • A flag that doesn't reach the compiler isn't a flag. A variable set twice in a Makefile, with the second one winning, cost a day. Check flags.make in the CMake build directory.
  • A knob that doesn't move the picture may be disconnected, not irrelevant. Several counters exist only to prove a branch actually runs.
  • The instrument perturbs the measurement. A live SWD panel pulled one game down to 11 fps. Probe quietly; measure in silence.
  • One console isn't evidence. Artefacts once blamed on the drawing turned out to be one worn-out console. Any beam constant tuned by eye on one machine is suspect.
  • Bisect with one criterion, written down first, and check both ends of the range.
  • An empty grep isn't proof of absence. grep skips files it considers binary without saying so.
  • If an instrumented path prints nothing at all, it failed before its first trace. Look upstream of the first print, not at the logic.