docs(badapple): usage, measurements and gaps

This commit is contained in:
arti committed 2026-11-01 10:20:23 +02:00
1 parent 8a1cd1bfb1
commit 4013f75434
2 files changed
+195 -9

No files matched your search

+99 -9
View File
@@ -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 <unit-ip> --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`.
+96
View File
@@ -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.