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.
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 0xA5A5A5A5for an0xA5-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, sordebug_load.pyverifies 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_REALdoes 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
0x018C0000with its entry at the address inharness/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 |