Files
vt1200/harness/badapple
arti 8f831eaf33 fix(video): model the real unit's VR319 monitor
The harness forced the VR150 monitor switch, so the firmware wrote the OS
screen at 1024 px / 128-byte rows.  VT1200_VRAM_REAL then re-read that
framebuffer at the VR319 pitch, which sheared the OS and wrapped the right
quarter of every row.

Model VR319 (switch 0x60) by default -- the real unit's mode -- so the
firmware itself programs 1280x1024 with a 256-byte row pitch (CONVDP=0x14).
The renderer's pitch follows VT1200_MONITOR, and the destination span now
comes from HEBLNK..HSBLNK instead of x=0, placing the active 1280 px at
x=336 in MAME's 1696-wide raster.

Verified: clean Terminal Manager at frame 600, legible faithful diagnostics
menu and level-1 error screens, tile mode PASS with tile_check 1280/1280
tiles, tablet/mouse pixel-identical.  VT1200_MONITOR=VR150 reproduces the
old geometry; the regression baseline moves 198 -> 199 (2 PCs) and is
recorded in harness/mame/README.md.
2026-10-31 20:53:51 +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):

harness/mame/run.sh net

The harness now models the real unit's VR319 monitor, so the firmware itself uses the 1280-px / 2048-bit (256-byte) row layout the tile payload targets — and the Terminal Manager before it renders correctly at the same geometry. No VT1200_VRAM_REAL is needed (that knob forced the pitch without changing the firmware's layout, which is what used to shear the OS screen). To reproduce the old derived 1024-px layout, run with VT1200_MONITOR=VR150; the tile picture will then be sheared, as before. See harness/mame/RENDER_REPORT.md §8.

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 VT1200_MONITOR=VR150 is selected (the old derived 1024-px layout); leave the default VR319
jump seems to hang the service expected: the payload never returns, so rdebug is gone until the machine is rebooted