diff --git a/bad-apple/README.md b/bad-apple/README.md index a96420f..a3ab315 100644 --- a/bad-apple/README.md +++ b/bad-apple/README.md @@ -1,12 +1,102 @@ -# Bad Apple on VT1200 — 30 s vector prototype +# Bad Apple on a VT1200 — 30 s vector prototype -This is the standalone, polygon-styled 30-second Bad Apple demo described in -[`docs/superpowers/specs/2026-10-31-bad-apple-vector-design.md`](../docs/superpowers/specs/2026-10-31-bad-apple-vector-design.md). +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 host pipeline turns the first 30 s of `hd60.mp4` into a 640×512 black-and-white -vector picture, packs only changed 32×32 tiles as rectangles, and a TMS34010 -payload paints them with `FILL L`. The payload takes over the terminal, so a hang -ends at a power cycle. +The payload takes over the terminal: it disables interrupts and never returns. +**Recovery is a power cycle.** The ROM is never modified. -Status: under construction. See the implementation plan at -[`docs/superpowers/plans/2026-11-01-bad-apple-vector.md`](../docs/superpowers/plans/2026-11-01-bad-apple-vector.md). +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) + +## 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`. + +## Run in MAME (debugger path) + +```sh +# one frame, then dump VRAM at MAME frame 2700 +bad-apple/tools/run_mame.sh one + +# 5 s slice, paced +BA_MOVIE=bad-apple/data5/movie.bin BA_START=0 BA_COUNT=300 \ + bad-apple/tools/run_mame.sh play + +# full 30 s, paced +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` exits when the payload sets its status marker. 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 implemented but **not yet verified on the unit or over MAME's +tap** in this worktree: the tap was down. The OS socket API it uses is the one +already proven by `harness/udpstream/udpstream.c`. 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`. diff --git a/bad-apple/REPORT.md b/bad-apple/REPORT.md new file mode 100644 index 0000000..c810bdc --- /dev/null +++ b/bad-apple/REPORT.md @@ -0,0 +1,96 @@ +# Bad Apple 30 s vector prototype — report + +Date: 2026-11-01 +Design: [`2026-10-31-bad-apple-vector-design.md`](../docs/superpowers/specs/2026-10-31-bad-apple-vector-design.md) + +## What was built + +* Host pipeline: video → temporal median → blur/threshold → potrace + straight-line polygons → RDP → even-odd rasteriser → 32×32 tile deltas → + `movie.bin`. +* Payload: TMS34010 C decoder + `FILL L` primitive, 16-bit marker page, + display-interrupt pacing, UDP receive path. +* Tools: MAME debugger loader, VRAM checker, UDP sender, rdebug load script. + +## Measured numbers (first 30 s, 1,800 source frames) + +| quantity | value | +|---|---| +| source | 960×720, 60 fps, first 30 s | +| logical grid | 640×512, 2×2 screen blocks | +| records | 1,757 (43 source frames dropped by the cost gate) | +| data | **2,401,942 bytes** (cap 3.03 MB, gate 3.0 MB) | +| dirty tiles/frame | mean 44.9, p50 47, p95 72, max 320 | +| rectangles/frame | mean 393, p50 409, p95 670, max 1,175 | +| estimated cost/frame | mean 7.47 ms, p50 7.95 ms, **p95 12.01 ms**, max 37.99 ms | +| gate | 13.9 ms (one 72 Hz refresh) | + +The stream fits with ~0.6 MB spare. The p95 frame is inside one refresh; the +max is a scene cut that necessarily takes more than one. + +## Exactness evidence (MAME) + +`tools/check_vram.py` unpacks the 256-byte-stride VRAM dump, downsamples 2×2, +and compares bit-for-bit against the Python application of the records. + +| run | movie frame | result | +|---|---|---| +| test pattern | 0 | **EXACT** | +| 5 s slice | 0 | **EXACT** | +| 5 s slice | 180 | **EXACT** | +| 5 s slice | 297 | **EXACT** | +| 30 s stream | 0 | **EXACT** | +| 30 s stream | 900 | **EXACT** | + +The one-frame MAME run also proved the FILL geometry: a logical black +rectangle 10..109 × 20..69 became exactly screen x 20..219. The first version +was exactly twice as wide; the cause was the OS leaving PSIZE at 2 bpp. The +primitive now re-asserts PSIZE=1 on every call. + +## Pacing + +`wait_vsync()` programs `DPYINT=1`, clears `INTPEND`, and polls the DI bit at +`0xC0000120`. With `BA_PACE=1`, ten source frames took roughly fifteen MAME +frames (startup included), i.e. the 60-on-72 cadence is being honoured rather +than free-running. + +## The performance gap MAME cannot close + +MAME needed ~2.1 emulated frames per movie record while replaying frames 0..900 +of the full stream. The encoder's model says a mean record is 7.47 ms of a +13.9 ms refresh, so MAME's TMS34010 interpreter is several times slower than the +model. MAME proves the picture and the delta chain; it cannot prove the unit's +60 fps. The unit's measured `FILL` rate (0.40 µs per 16-pixel word) is the +number the cost model uses. A real-unit run is the remaining performance +measurement. + +## Network delivery + +`player-net.bin` uses the OS UDP socket API (`socket_`, `bind_`, +`recvfrom_`, `malloc_`) to receive `movie.bin` on port 7001 into a heap buffer, +then DINTs and plays. The sender is `tools/send_movie.py`. + +Status: the packet format and sender are unit-tested; the MAME tap was down in +this worktree, so the UDP receive was not exercised end-to-end. The same OS +socket API is already proven on the unit by `harness/udpstream/udpstream.c`. +This is the one delivery-path gap. + +## Regression + +The no-env MAME baseline at 200 frames is unchanged: + +``` +--- distinct PCs sampled: 2 + PC=3FDCE400 samples=199 + PC=3FDCE350 samples=1 +``` + +## Remaining gaps + +1. Real-unit 30 s performance: needs a power-cycle run with + `tools/load.sh`. +2. UDP receive on the unit: socket API proven, this payload not yet. +3. Incremental painting can show a brief wipe on a cut; accepted for this + prototype. +4. Potrace/RDP temporal jitter was reduced with a 3-frame median but not + measured as a motion score.