forked from arti/vt1200
149 lines
6.1 KiB
Markdown
149 lines
6.1 KiB
Markdown
# 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.
|
||
|
||
Potrace emits **y-up** GeoJSON; the framebuffer is y-down. The first build was
|
||
vertically mirrored, and the VRAM checker still said EXACT because it consumed
|
||
the same flipped data. `trace()` now flips y before simplification, and
|
||
`test_trace_preserves_vertical_position` pins the orientation. The preview PNGs
|
||
were the visible symptom.
|
||
|
||
## Pacing
|
||
|
||
`wait_vsync()` programs `DPYINT=1`, clears `INTPEND`, and polls the DI bit at
|
||
`0xC0000120`. The loop reads each record's `ticks` from the index and advances
|
||
a 1/360 s accumulator by 5 per vsync, so a 1/60 s record needs 6 units and a
|
||
held record needs 12. Measured with `BA_COUNT=60`: status at MAME frame 2580,
|
||
74 frames after takeover at 2506 — 60 source frames in 72 display frames plus
|
||
startup, i.e. the 60-on-72 cadence is real, not 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`. Packets are
|
||
10-byte headers (offset, length, seq, crc16) plus body; a final
|
||
offset==length-of-file / 4-byte-checksum packet is required, and the receiver
|
||
refuses to jump unless the whole-file checksum matches. Out-of-range packets
|
||
are dropped without reading or writing outside the buffer.
|
||
|
||
Status: the packet format, header offset, checksum path and sender are
|
||
unit-tested. The live MAME tap test reaches the payload but stops before UDP
|
||
bind: the marker shows `status=0x20` and no socket is open. The suspect is
|
||
`malloc_(2401942)` — the OS heap does not provide one contiguous block that
|
||
size. The debugger path is the working demo. A fixed low-RAM staging receive
|
||
or raw-LANCE loader is the next step. This is the one delivery-path gap.
|
||
|
||
## r5.01 OS/2 compiler check (2026-11-01)
|
||
|
||
The 1990 TMS340 r5.01 ANSI C toolchain, run under the patched
|
||
`2ine-tms34010` fork, builds this demo. The pipeline was:
|
||
|
||
```
|
||
gspac -k MAIN.C MAIN.IF
|
||
gspcg -m MAIN.IF MAIN.ASM MAIN.TMP # no gspopt for MAIN.C
|
||
gspa MAIN.ASM
|
||
gspac/gspopt -O2/gspcg/gspa for MOVIE.C
|
||
gspa FILL.ASM
|
||
gspa ENTRY.ASM
|
||
gsplnk PLAYER.CMD # .text 018C0000h, .bss 01C00000h
|
||
```
|
||
|
||
Result: `.text` `0x6300` bits = **3,168 bytes**, entry **`0x018C5C70`**.
|
||
MAME frame 0 and frame 180 are **EXACT** with
|
||
`BA_PAYLOAD=PLAYER.bin BA_ENTRY=0x018C5C70`.
|
||
|
||
Caveat: `gspopt -O2` on `MAIN.C` hoists the `CTRL_READY` polling load and
|
||
the payload waits forever at takeover. `MOVIE.C` is fine at `-O2`; build
|
||
`MAIN.C` without `gspopt`, or add `volatile` to the polling reads. `gspac`
|
||
and `gspcg` exit 139 at teardown but still write valid output files.
|
||
`gspa` 5.01 accepted `entry.asm`/`fill.asm` (including `sp`); the `b15`
|
||
round-trip difference does not affect this demo.
|
||
|
||
Speed A/B in MAME, 600 records with `BA_PACE=0` (takeover at ~frame 2505):
|
||
|
||
| build | status frame | processing frames |
|
||
|---|---|---|
|
||
| 1987, no optimizer | 3,922 | ~1,417 |
|
||
| r5.01, no optimizer | 3,856 | ~1,351 |
|
||
| r5.01, `MOVIE.C` at `-O2` | **3,605** | **~1,100** |
|
||
|
||
So the r5.01 compiler alone is ~5 % faster here, and the practical r5.01
|
||
recipe is **~29 % faster** than the 1987 build. MAME's TMS34010 interpreter
|
||
is not silicon; this is a relative measurement, not a unit-cycle claim.
|
||
|
||
## 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.
|