Files
vt1200/harness/badapple
arti eabc3051be feat(badapple): the demo as a tool -- rdebug delivery, clip builder, one command
A new folder so the demo can be reproduced instead of re-derived: the delivery
and the clip packing were living in throwaway scripts under /tmp, and the only
written route to a real clip was an ffmpeg recipe that packs MSB first -- the
order that mirrors every eight pixels on a display.

  harness/badapple/rdebug_load.py    image -> terminal RAM over the firmware's
                                     own remote-debug service (TCP 771), then
                                     jump to the entry from vidstream.env
  harness/badapple/clip_from_video.py video -> LSB-first 1-bpp frames for
                                     make_tiles.py --clip
  harness/badapple/demo.sh           ICMP gate, load, send; one command
  harness/badapple/README.md         setup, run in MAME, run on real hardware,
                                     what success looks like, failure modes

The loader records the two protocol facts that cost time to find: a 512-byte
poke chunk (1024 is mis-parsed as a following opcode: "Invalid command
0xA5A5A5A5"), and that nothing can be probed after the jump, so the image is
read back BEFORE jumping and the run is observed from the machine afterwards.

Tests cover what a wrong assumption would silently corrupt -- chunk arithmetic,
the linkage the entry point is read from, and pixel 0 landing on bit 0 -- and
the tool was exercised end to end against MAME: demo.sh loaded the payload and
the terminal reported DICT_BYTES=32768, DICT_BAD=0, RX_OK=28, TILES_DROP=0
while the sender saw frames_rx=129, drops=0.

153 tests pass (7 new).
2026-10-31 20:20:35 +02:00
..

Bad Apple on a VT1200

The demo end to end: a payload is delivered over the network into a running terminal, and the picture is streamed over the network to it, which paints it from a 32 KB tile dictionary plus per-frame deltas. Nothing here uses a debugger — that path (harness/mame/vidstream.lua) exists to test the payload against MAME's debugger API and is not how anything reaches a machine.

harness/badapple/
├── README.md            this file
├── demo.sh              one command: wait for the OS, load, send
├── rdebug_load.py       payload image → terminal RAM over rdebug (TCP 771)
├── clip_from_video.py   video → raw 1-bpp frames for make_tiles.py
└── tests/               unit tests for the two Python tools

How the delivery works

step transport what it does
boot — harness/mame/run.sh net (MAME) or the real unit powering up
readiness ICMP demo.sh waits for the terminal's own stack to answer
payload rdebug, TCP 771 poke the image in 512-byte chunks, read it back, jump to the entry
dictionary Ethernet, ethertype 0x88B7 23 datagrams, one TILES_DICT_CHUNK each
pictures same one TILES_FULL or TILES_DELTA per clip frame

rdebug is the firmware's own remote-debug service: peek/poke/jump, the same protocol dwt/exploit.py speaks (dwt/notes/CROSS.md). On hardware it is the delivery path; in MAME it works because the LANCE is attached to a host tap.

Two facts about it are load-bearing and were measured:

  • A 512-byte poke chunk, not 1024. The firmware documents a 1024-byte clamp, but a 1024-byte poke is mis-parsed — it reads the body's first word as the next opcode and answers Invalid command 0xA5A5A5A5 for an 0xA5-filled chunk. 512 is accepted and reads back byte for byte.
  • Nothing can be probed after the jump. A non-returning payload takes the remote-debug process with it, so rdebug_load.py verifies the image before jumping, and the run is observed afterwards from the machine itself (the screen, or MAME's RAM/VRAM dumps). One demo per boot.

Setup

cd /path/to/vt1200

# 1. MAME + the driver (skip if you already have vt1200 built)
harness/mame/build.sh

# 2. the payload (TI SDK under DOSBox)
harness/udpstream/build-stream.sh        # -> harness/udpstream/build/vidstream.bin

# 3. a clip cache (skip to use the committed 30-frame synthetic clip)
python3 harness/badapple/clip_from_video.py bad-apple.mp4 /tmp/badapple --fps 15
python3 harness/udpstream/make_tiles.py --clip /tmp/badapple --out /tmp/badapple-tiles

clip_from_video.py writes LSB-first 1-bpp rows (bit 0 of a byte is the leftmost pixel), which is the order both the terminal's scanout and the payload's encoder use. Do not substitute ffmpeg's format=monob: it packs MSB first and mirrors every eight pixels on the display.

make_tiles.py prints what it built — on the real clip (3,292 frames at 15 fps, 1280x960) that is 256/256 dictionary entries, 55 keyframes, mean delta 496 B, 1.63 MB of stream, PSNR 24.4 dB.

Run it in MAME

Terminal 1 — the machine, with the LANCE on the host tap (harness/mame/cfg-net selects taptun; the tap is tap-mess-<uid>-0 in bridge vt1200, 192.168.99.1/24):

VT1200_VRAM_REAL=1 harness/mame/run.sh net

VT1200_VRAM_REAL=1 renders the framebuffer at the real unit's geometry (2048-bit / 256-byte rows, 1280 px). Without it the window uses the driver's derived 128-byte pitch and the tile picture is sheared — the divergence is documented in harness/mame/RENDER_REPORT.md §8, and it is a property of the renderer, not of the demo.

Terminal 2 — the demo:

harness/badapple/demo.sh --tiles /tmp/badapple-tiles --frames 3292 --fps 15

With no arguments demo.sh plays the committed 30-frame synthetic cache: a smoke test that needs no clip.

Run it on a real VT1200

The terminal must be reachable over Ethernet and answering rdebug on TCP 771 — that is all this demo needs, no debug pod and no special build of the firmware.

# the unit's address, and the host interface that reaches it
harness/badapple/demo.sh --host <unit-ip> --iface <host-iface> \
    --tiles /tmp/badapple-tiles --frames 3292 --fps 15

Notes that differ from the emulator:

  • Pacing is not needed for the dictionary upload: the payload's receive ring is 32 descriptors, one more than the 23-datagram upload, so the whole dictionary lands before the first chunk is copied. On a slower link the wire paces it for you.
  • VT1200_VRAM_REAL does not exist there — the unit already uses its own layout; the switch only affects MAME's renderer.
  • The image is position-dependent and is linked at bit 0x018C0000 with its entry at the address in harness/udpstream/build/vidstream.env. Both are read from that file, so a rebuild moves the entry without editing anything.

What success looks like

demo.sh prints the load verification and the sender's per-frame CSV. The sender's verdict is about the transport:

PASS: 2 status frames, last frames_rx=100 (drops=0 crc_err=0)

The terminal's own counters, read from a MAME RAM dump (byte offsets from MARK_PAGE = bit 0x01810000), say what it actually did:

cell offset good value
DICT_BYTES 0x54 32768 (and DICT_READY 0x50 = 1)
DICT_BAD 0x58 0
RX_OK 0x30 climbs with painted frames
TILES_DROP 0x5c 0
RX_DGRAM 0x48 datagrams consumed

harness/mame/tile_check.py --replay is the pixel verdict for the MAME replay path; for a live run the screen is the evidence.

Failure modes worth knowing

symptom cause
demo.sh: ... did not answer within 180s the terminal is not on the tap, or run.sh net was started without the LANCE interface selected in its cfg
poke reads back a mismatch the image is being loaded over a stale link, or the payload was built for a different load address
DICT_BYTES stuck below 32768 datagrams lost before the payload saw them — check the tap and the sender's interface
picture sheared in MAME the window is rendering at the derived 128-byte pitch; use VT1200_VRAM_REAL=1
jump seems to hang the service expected: the payload never returns, so rdebug is gone until the machine is rebooted