# Bad Apple on a VT1200 — 30 s vector prototype A standalone, polygon-styled 30-second Bad Apple demo for the VT1200 (TMS34010). The host pipeline turns the first 30 s of `hd60.mp4` into a 640×512 binary picture (2×2 screen blocks), traces it into simplified straight-edged polygons, and packs only changed 32×32 tiles as rectangles. The payload paints those rectangles with `FILL L`. The payload takes over the terminal: it disables interrupts and never returns. **Recovery is a power cycle.** The ROM is never modified. Design: [`../docs/superpowers/specs/2026-10-31-bad-apple-vector-design.md`](../docs/superpowers/specs/2026-10-31-bad-apple-vector-design.md) Plan: [`../docs/superpowers/plans/2026-11-01-bad-apple-vector.md`](../docs/superpowers/plans/2026-11-01-bad-apple-vector.md) Measurements and gaps: [`REPORT.md`](REPORT.md) ## Source video `hd60.mp4` was **downloaded by the agent** in the earlier OpenCode session **“Bad Apple demo tool”** (`ses_e6d6b504dffeIRKLu3RqZAiFRp`, message seq 641), not supplied by the user. The design doc's “human-supplied” wording is inaccurate. - Item page: - Direct file: - The fetch command was `curl -sL -o hd60.mp4 ""`. - Measured: **34,903,925 bytes**, **960×720**, **60 fps**, **219.521451 s**, h264 / yuv420p, AAC stereo. - Local path used by this demo: `/tmp/opencode/badapple-real/hd60.mp4`. The archive.org item name is scrambled (`720-x-960`); the file inside is 960×720, 4:3. ## Quick start — current MAME demo From the repository root: ```sh # 1. video -> data/movie.bin cd bad-apple python3 -m pipeline.build \ --video /tmp/opencode/badapple-real/hd60.mp4 \ --out data --duration 30 cd .. # 2. payload bad-apple/player/build.sh # 3. watch it bad-apple/tools/run_mame_gui.sh ``` `run_mame_gui.sh` starts MAME with `VT1200_SKIP_LEVEL1=1`, boots the OS, loads the payload at frame 2500, lets it take over, writes the movie into low RAM, and plays it in a window. Expect the 30 s clip at full screen, upright. `BA_END` (default 8000) is the MAME frame at which the loader script exits; raise it if your run is slower. The **network path is currently disabled in practice**: the OS heap cannot provide one contiguous 2.4 MB `malloc_` block, so `tools/load.sh` stops before it binds UDP. Use the debugger path above. See `REPORT.md`. ## Build the 30 s stream ```sh cd bad-apple python3 -m pipeline.build \ --video /tmp/opencode/badapple-real/hd60.mp4 \ --out data --duration 30 ``` Outputs: | file | size | what | |---|---|---| | `data/movie.bin` | 2,401,942 B | 1,757 records, 43 source frames dropped | | `data/manifest.json` | small | data size, dirty-tile/rect/cost percentiles | | `data/preview/frame_*.png` | — | still previews | Measured: dirty tiles mean 44.9 / p95 72; rectangles mean 393 / p95 670; estimated paint cost mean 7.47 ms / p95 12.01 ms against a 13.9 ms refresh gate. The data area is 3.03 MB, so this fits with ~0.6 MB spare. ## Build the payload ```sh bad-apple/player/build.sh # debugger build: build/player.bin NET=1 bad-apple/player/build.sh # network build: build/player-net.bin ``` The 1987 TI TMS34010 SDK runs under DOSBox. The C sources are K&R C: the 1987 compiler rejects ANSI prototypes and `const`. `.text` is linked at bit `0x018C0000`, `.bss` at `0x01C00000`. The 1990 **r5.01 OS/2** toolchain under `2ine-tms34010` also builds it (see `tools/tms34010-sdk/2INE-501.md` for setup). Build `MAIN.C` without `gspopt -O2`: the optimizer hoists the `CTRL_READY` polling load and the payload waits forever at takeover. `MOVIE.C` is fine at `-O2`. The verified r5.01 build is `.text` 3,168 bytes, entry `0x018C5C70`, and is MAME-exact at frames 0 and 180. `gspac`/`gspcg` exit 139 at teardown but still write valid files. ## Run in MAME (debugger path) For a window, use `tools/run_mame_gui.sh` (above). For scripted/headless checks: ```sh # one frame, then dump VRAM at MAME frame 2700 VT1200_DUMP_VRAM_FRAMES=2700 VT1200_DUMP_DIR=/tmp/ba-one \ bad-apple/tools/run_mame.sh one # 5 s slice, paced (headless) BA_MOVIE=bad-apple/data5/movie.bin BA_START=0 BA_COUNT=300 \ bad-apple/tools/run_mame.sh play # full 30 s, paced (headless) BA_MOVIE=bad-apple/data/movie.bin BA_MOVIE_BASE=0x00080000 \ BA_WAIT=1 BA_PACE=1 bad-apple/tools/run_mame.sh play ``` `run_mame.sh` runs with `-video none`; it exits at `BA_END`. The debugger loader writes the movie into high RAM (`0x01D00000`) for small slices; for the full 30 s it uses `BA_WAIT=1`, lets the payload take over, then writes low RAM at `0x00080000` — the same ordering as the real delivery path. ## Deliver to a real unit (network path) The terminal must be booted and answering ping. The payload is loaded over `rdebug`, then receives `movie.bin` over UDP into an OS `malloc` buffer, then disables interrupts and plays. ```sh bad-apple/tools/load.sh --host --movie bad-apple/data/movie.bin ``` **This path is currently blocked** at `malloc_(2401942)`: the OS heap does not give one contiguous 2.4 MB block, so the payload never binds UDP. The debugger path above is the working demo. A fixed low-RAM staging receive or raw-LANCE loader is the next step. See `REPORT.md`. ## Data format Header 36 bytes (`BAPL`, version, geometry, frame count, index/data offsets). Index entry: `u32 offset, u8 ticks, u8 flags`. Record: `u16 dirty tile count`, then per tile `u16 tile index, u16 (bg<<15)|rect count`, then 3-byte rects (`x0:5 y0:5 w-1:5 h-1:5`). A tile is repainted by filling its 64×64 screen block with the background colour, then filling the minority-colour rectangles. ## Gotchas this prototype records * **PSIZE is re-asserted on every FILL.** The resident OS leaves PSIZE at 2 bpp; a one-time write in `_fill_init_` was overwritten, and every span came out twice as wide. The fix is the per-call `movi 1 / move @0C0000150h`. * **VRAM polarity is inverted from the canvas**: bit 1 = white, bit 0 = black. `tools/check_vram.py` downsamples with that polarity. * **The OS dies before the movie is in low RAM.** The network path receives into an OS heap buffer while the stack is alive, then DINTs. The debugger path does the same ordering with `BA_WAIT=1`. * **MAME is not the unit's performance verdict.** MAME needed ~2.1 emulated frames per movie record on the full stream; the unit's 50 MHz FILL rate is the number that matters. See `REPORT.md`. * **The marker/control block lives in `.bss`**, not at bit `0x01810000`: a 2.4 MB heap allocation can cross that address. `player.env` publishes `MARK_PTR`; `load_mame.lua` reads it. * **Pacing reads each record's `ticks`.** 60 source frames took 74 MAME frames (72 display frames plus startup), so the 60-on-72 cadence is measured, not assumed.