28 KiB
Bad Apple, 30 s, vector polygons — design
Date: 2026-10-31 Status: rev 1 — first draft, awaiting approval. Author: this session.
This is a separate prototype from
2026-10-31-badapple-60p-design.md
(the raw-tile, full-length, streamed design in harness/badapple/). It does
not supersede it and does not change the existing harness. It answers a
different brief: a standalone, first-30-seconds, polygon-styled demo in a
new top-level bad-apple/ folder.
Decisions marked human: were given in this session. Decisions marked call: are the builder's proposals, not yet agreed.
1. Intent
Play the first 30 seconds of
/tmp/opencode/badapple-real/hd60.mp4 on a VT1200, full screen at
1280×1024, as flat black-and-white polygon art in the spirit of
Another World — not a dithered bitmap and not a 32×32 tile dictionary.
The picture is quantised to straight-edged polygons, then to a coarse logical
grid, then to rectangles that the TMS34010's FILL engine can paint fast.
After a small bootstrap is delivered over rdebug, the payload takes over the
machine and receives the movie data over Ethernet; playback is then entirely
local from the VT1200's 4 MB of main RAM. Recovery is a hardware restart.
2. What the human partner decided
- Source:
/tmp/opencode/badapple-real/hd60.mp4, first 30 s. - Frame: fill the whole 1280×1024 panel; no letterboxed 4:3.
- Look: black-and-white polygons, similar to Another World; drop dithered sections and threshold/simplify them into flat shapes.
- Motion: 60 fps is the target; complex scenes may drop frames.
- Ownership: after the initial load, the program may take the whole 4 MB of RAM; the OS may die; recovery is a power cycle.
- Delivery:
rdebugis the bootstrap path; the network loader inharness/badapple/is fair game to make the big load fast. - Location: a new top-level
bad-apple/folder.
3. Success criteria
bad-apple/reproduces the whole pipeline from the source video with one command per stage, and documents the data budget it measured.- In MAME, the first 30 s plays full-screen, recognisably Bad Apple, with clean straight-edged shapes and no corrupt or stale frames. Typical frames run at 60 fps; heavy frames may drop to 30 fps or hold for one refresh. The baseline repaints dirty tiles incrementally; a brief wipe on a large change is accepted for this prototype (§8.5).
- The movie data plus code fits in the VT1200's 4 MB of main RAM, excluding the 256 KB framebuffer in video RAM.
- On the real unit, the same payload is delivered by
rdebugplus the Ethernet loader and plays without a host attached. - Existing
harness/behaviour and the MAME no-env regression baseline are untouched. The ROM stays byte-identical.
4. Non-goals
- The full 3:39 clip (the 30 s limit is what makes local RAM playback possible).
- Audio (the VT1200 has no audio path in this harness).
- Grey, colour, dither, or anti-aliased pixels; this is 1 bpp.
- Pixel-exact reproduction of the source. The target is a recognisable, clean vector reinterpretation.
- Coexistence with the resident OS after takeover.
- A general-purpose video player; this is a demo with a fixed 30 s stream.
5. Constraints and measured facts
5.1 Hardware
| item | value | source |
|---|---|---|
| CPU | TMS34010FNL-50, 50 MHz, 1 state = 160 ns | harness/mame/vt1200.cpp |
| panel | 1280×1024 visible, 2048-bit (256-byte) row pitch, 72 Hz | RENDER_REPORT.md, driver |
| main RAM | 4 MiB at bit address 0 | vt1200.cpp address map |
| video RAM | separate, at bit 0x04000000 |
vt1200.cpp |
FILL L on the unit |
0.40 µs per destination word (16 pixels), plus ~2.6–3.3 µs per call | 2026-10-31-badapple-60p-design.md §5 |
| full 1280×1024 repaint | ~82k words ≈ 33 ms | derived from the unit rate |
| 60 fps budget | 16.7 ms ≈ 41k words ≈ 667k pixels | derived |
| 72 Hz refresh | 13.9 ms per refresh | driver |
The 72 Hz display and 60 fps source have no common period; the player must pace a 60 Hz timeline against a 72 Hz refresh (§8.6).
5.2 Source
| item | value |
|---|---|
| video | 960×720, 60 fps, h264, 219.5 s, 13,167 frames |
| audio | AAC stereo, ignored |
| first 30 s | 1,800 frames |
| black coverage | mean 55 %, p75 80 %, p95 98 % (1280×1024 threshold) |
The source is already high-contrast black-on-white; it is the ideal input for a threshold-and-trace pipeline.
5.3 The measured data budget
A host-side pass over the first 30 s at a 640×512 logical grid (each logical pixel is a 2×2 screen block), hard threshold, no vectorisation yet, measured the tile-delta format in §8:
| quantity | mean | p50 | p95 | max |
|---|---|---|---|---|
| dirty 32×32 logical tiles per frame | 47.4 | 49 | 79 | 295 |
| rectangles per frame | 437 | 451 | 820 | 1,381 |
| bytes per frame (5 B tile header + 3 B/rect) | 1,548 | 1,598 | 2,855 | 5,618 |
Total for 1,800 frames: 2.79 MB. The data area is
0x010000..0x317FFF = 3.03 MB, so this leaves ~0.24 MB of headroom. A
scene cut touches up to 295 tiles, still only 5.6 KB and one slow repaint.
The vectorisation step will change these numbers; the first milestone re-measures them. The gate is ≤ 3.0 MB and a p95 paint cost within the refresh budget. If vectorisation pushes past 3.0 MB, the fallback is stronger simplification or a 4×4 logical grid, not a shorter clip without asking.
Why 3.03 MB and not 3.5 MB: the movie data starts at byte 0x010000 and
must stop below the payload's rdebug origin at bit 0x018C0000 = byte
0x318000. 0x318000 - 0x010000 = 0x308000 = 3.03 MB. Moving the payload
origin would cost the one free block the real loader is known to accept, so
the design keeps the origin and caps the data instead.
6. Approach
6.1 Why not store rectangles for every frame?
A full-frame rectangle list is simple but forces a full repaint: at the unit's 0.40 µs/word, 1280×1024 costs ~33 ms, i.e. two 60 fps frames. The design must paint only what changed. The measured dirty-tile format does that: typical frames touch ~47 tiles (≈192k screen pixels ≈ 4.8 ms of fill), and the data still fits in 4 MB.
6.2 Why a 640×512 logical grid?
At 1×1 (1280×1024) the same delta format measures ~4× the data (≈10.8 MB) and does not fit. At 2×2 it fits with room to spare, and 640×512 is still four times the resolution of Another World's 320×200. The 2×2 quantisation is also part of the requested look: it turns pixel-level fuzz into flat blocks with straight polygon edges.
call: 640×512 logical, 2×2 screen blocks. If the first milestone shows the data is unexpectedly small, 1×1 can be re-measured as a quality mode; it is not the baseline.
6.3 Why polygons then rectangles?
The polygon step is the look: threshold, despeckle, trace contours, simplify
to straight segments, and fill the result. That produces the flat, angular
Another World silhouette. The runtime does not rasterise polygons; the
host rasterises the simplified polygons back to the 640×512 grid and decomposes
each dirty tile into rectangles. Rectangles are what FILL L paints fastest
and most simply. The player is a rectangle decoder, not a polygon engine.
6.4 Why not LINE?
LINE costs 5 states/pixel (≈800 ns/pixel), while FILL costs ~25 ns/pixel
on the unit. Filling a polygon with LINE outlines would be 30× slower and
would not fill the interior. LINE is reserved for an optional debug view
that draws polygon outlines; the animation uses FILL L only. The request
said "FILL and/or LINE"; this is the FILL branch, for a measured reason.
7. Pipeline
hd60.mp4
│ ffmpeg: first 30 s, 60 fps, 640×512 gray
▼
gray frames ──► temporal median (3) ──► 3×3 blur ──► threshold
│
▼
binary frames ──► potrace -a 0 (straight polygons) ──► RDP simplify
│
▼
simplified polygons ──► rasterise to 640×512 binary
│
▼
tile diff (32×32 logical) ──► per-tile background + rectangles
│
▼
movie.bin + index + manifest.json
│
▼
player payload (TMS34010 C + FILL asm)
7.1 Extract and stylise
ffmpeg -t 30 -vf fps=60,scale=640:512:flags=lanczos,format=gray.- Temporal median over 3 frames before threshold. This is the main defence against dither flicker and per-frame contour boiling; it is cheap on the host and does not cost the player anything.
- 3×3 box/Gaussian blur then threshold at 127. This removes halftone patterns and turns them into flat regions, as requested.
- Despeckle: drop connected components below ~8 logical pixels.
- Trace:
potrace -b geojson -a 0 -t 4 -u 1, which emits straight-line polygons only (-a 0disables curves). Parse the GeoJSON rings. - Simplify: Ramer–Douglas–Peucker on each ring, epsilon 1.5 logical pixels (tunable), minimum segment 2 px. This is the "straight lines" step.
- Rasterise: even-odd scanline fill of the simplified rings back to 640×512. The result is the frame the player will actually show.
All parameters live in one pipeline/params.json so the look can be tuned
without touching code. A preview command writes side-by-side PNGs of source,
thresholded, traced and final frames.
7.2 Tile-delta format
The logical frame is divided into 32×32 logical tiles (64×64 screen pixels): 20 × 16 = 320 tiles, row-major.
For each frame, compare with the previous frame. For every tile with any changed pixel:
- choose the tile's background colour (black or white) — the majority colour in that tile;
- decompose the minority colour into rectangles (merge identical horizontal runs vertically);
- emit the tile index, the background bit, the rectangle count, and the rectangles.
The player paints a dirty tile by:
FILLthe whole 64×64 screen tile with the background colour (one call);FILLeach rectangle with the opposite colour.
This reconstructs the tile exactly, including erasing old ink. Unchanged tiles are not touched. The background choice keeps mostly-black tiles cheap (one fill, few white holes).
Record layout (little-endian):
header:
magic "BAPL" 4
version u16
logical_width u16 (640)
logical_height u16 (512)
scale u16 (2)
tile_logical u16 (32)
tiles_x, tiles_y u16 (20, 16)
frame_count u32
index_offset u32
data_offset u32
source_fps u16 (60)
display_hz u16 (72)
index[frame_count]:
offset u32 (byte offset of the record)
ticks u8 (1 = 1/60 s, 2 = held for 2/60 s, ...)
flags u8 (bit 0: scene cut / full repaint)
record:
dirty_tile_count u16
for each dirty tile:
tile_index u16
bg_and_count u16 (bit 15 = background colour, bits 0-14 = rect count)
rects[count]:
x0:5 y0:5 w:5 h:5 packed into 3 bytes
(logical tile coordinates; w,h are stored as w-1,h-1 so 1..32 fits in 5 bits)
Rectangles use the minority colour; the tile background is the majority. A rect count of 0 is legal (a solid tile). The count is 15 bits, so the theoretical maximum of 512 horizontal runs in a 32×32 tile cannot overflow; the decoder rejects a count larger than the remaining record bytes.
Sizing: 5 bytes of tile header + 3 bytes per rect. The measured mean is 1.55 KB/frame, total 2.79 MB for 1,800 frames. A scene-cut record with 295 tiles and ~1,300 rects is ~5.6 KB, still one record.
7.3 Frame-rate adaptation
The encoder estimates each record's cost from the tile and rectangle areas
(at 0.40 µs per 16-pixel word), the per-FILL call overhead (~3 µs), and the
C decoder's per-rect work. The calibration constants come from milestone 3;
the initial values are 0.40 µs/word, 3 µs/call and 1 µs/rect. The gate is
13.9 ms — one 72 Hz refresh — not the 16.7 ms nominal 60 fps period,
because a record that misses 13.9 ms loses a whole refresh on the 72 Hz panel.
If a record exceeds the gate:
- first try stronger simplification for that frame (larger RDP epsilon, larger minimum feature);
- if it still exceeds the gate, hold the previous frame for an extra
1/60 s and compute the next delta from the held frame. The index
ticksfield records the hold.
This is build-time frame dropping, so the delta chain stays exact. The player never skips a record at runtime: every record is a delta from the one before, so skipping one would leave its tiles stale. If the real unit is slower than the encoder's estimate, the movie stretches — every frame is still shown, in order, with the correct picture — rather than dropping or corrupting frames. That is the safe failure mode; the encoder is re-tuned from the unit's measured cost if the stretch is noticeable.
7.4 Scene cuts
A large frame-to-frame difference is a cut, not motion. The encoder marks the
record flags & 1, and emits every tile as dirty. The player repaints the
screen tile by tile. This costs one slow frame (~30–40 ms) and may drop the
next refresh; that is the documented price of a cut. Future work could split a
cut over two frames if it looks bad.
8. Player
8.1 Memory layout
| region | byte address | use |
|---|---|---|
0x000000–0x00FFFF |
64 KB | low staging / scratch; free after load |
0x010000–0x317FFF |
3.03 MB | movie data (movie.bin) |
0x318000–0x3FFFFF |
0.9 MB | payload code, LANCE block, read-only tables, stack |
VRAM 0x04000000 (bit) |
256 KB | single framebuffer (separate) |
0x018C0000 is a bit address = byte 0x318000, the low end of the high
code block. The existing loader's origin is reused so rdebug needs no new
address. The movie data starts at byte 0x010000 and is loaded over the
network after takeover; it overwrites the OS, which is dead by then.
The LANCE block (init block, rings, RX buffers) lives in the high code block,
following harness/udpstream/vidstream.c, which places its RX buffers near
byte 0x3BC000 and is documented as having run on the unit. The 64 KB low
reservation is a fallback staging area if the real chip turns out to
truncate a high RX buffer address; the loader then receives low and copies
up. Milestone 5 settles this before the full 30 s run. It is a loader change,
not a player change.
8.2 Takeover
The payload is entered from rdebug. It:
- publishes a marker word;
- executes
DINTonce, for good, and stops the OS's LANCE use; - takes the LANCE over with the verified init/ring sequence and receives
movie.bininto0x010000(the OS is already dead; the display controller keeps scanning out because it was configured before takeover); - verifies the movie header and a whole-file checksum;
- reads the display registers the OS programmed (
DPYCTL,DPYSTRT,DPYINT, pitch) and publishes them for diagnosis; it inherits the mode rather than hardcoding one; - clears the screen to white with one full-screen
FILL; - enters the playback loop.
Interrupts stay disabled. The OS's timer and DUART ISRs are never entered again. The LANCE is stopped after the load; the player no longer needs it. The OS dies before the first byte lands in low RAM — that is what makes overwriting its heap and ISRs safe. The earlier wording, "receive while the OS is alive, then DINT", was wrong; it would crash mid-load.
Recovery: a hang after step 2 is a power cycle. The README says so in the first screenful.
8.3 Loader
Two paths, same payload:
-
MAME / development: a Lua script writes
movie.bininto MAME's RAM at byte0x010000and the payload at0x318000, then jumps. Slow but fine for 5 s slices. -
Real unit / full 30 s:
rdebugwrites a small bootstrap into the high block. The bootstrap disables interrupts, takes the LANCE over with the proven register/ring sequence fromharness/udpstream(copied, not edited), receivesmovie.binin 1,450-byte datagrams into0x010000, ACKs every window, and jumps to the player entry. The host sender is a small Python tool using the same raw-Ethernet framing asharness/badapple/harness/udpstream.The baseline places the RX buffers in the high code block, as
vidstream.cdoes. If the real chip truncates a high buffer address, the fallback receives into a 32 KB ring below 64 KB and copies each datagram up withMOVE/word copy. That copy rate is measured in milestone 5; it is a loader-only fallback, not the baseline.
The protocol is deliberately simple: u32 offset, u16 length, u16 seq,
u16 crc, then data. The receiver ACKs the highest contiguous offset every
32 datagrams; the sender retransmits from the first gap. A final whole-file
CRC-32 is checked before the jump. If the load fails, the payload paints an
error block and halts rather than jumping into a partial movie.
8.4 Rendering
All drawing goes through one assembly primitive:
; fill_rect(x, y, w, h, color)
; x,y,w,h in screen pixels (already scaled by 2)
; color 0 = black, 0xFFFF = white
It sets DADDR, DYDX, DPTCH=0x800, PSIZE=1, PMASK=0, CONTROL
replace, and COLOR1, then executes FILL L. Constants are programmed once
per run; only DADDR, DYDX and COLOR1 change per call, and FILL destroys
DADDR/DYDX, so those two are reloaded each time. This is the same
implied-operand discipline as harness/udpstream/vram.asm, which is already
verified on the unit.
The C player converts each packed tile rect to screen coordinates:
sx = (tile_x*32 + x0) * 2
sy = (tile_y*32 + y0) * 2
sw = (w + 1) * 2 // stored as w-1
sh = (h + 1) * 2 // stored as h-1
and calls fill_rect. A dirty tile is one background fill plus its rects.
Hoisting: PSIZE, PMASK, CONTROL, DPTCH are set once at startup.
CONVDP/CONVSP are not needed for FILL L (linear destination), but are
set to 0x14 for consistency with the verified primitive.
8.5 Frame buffer and presentation
Baseline: one framebuffer, incremental dirty-tile updates. Drawing a record takes ~2–10 ms for a typical frame, a fraction of the 13.9 ms refresh. Updates are visible as they land; the TMS34010's VRAM is dual-ported, so the display does not stall the CPU. At 60 fps the eye usually reads this as motion, but a large change can show a brief top-to-bottom wipe.
Tear-free is a separate mode, not free. It needs either:
- two framebuffers and a vblank flip. The real panel uses a 256-byte row stride, so one 1280×1024 buffer is 256 KB, not 160 KB; two are 512 KB. Whether the unit has that much VRAM is not established. It also doubles the fill work unless each delta is applied to both buffers, which costs the p95 frame its refresh.
- beam chasing. Draw each dirty tile only after the scanout has passed it,
so the new content appears on the next frame. This is tear-free with one
buffer but needs
VCOUNTscheduling and leaves one frame of latency.
Neither is in the baseline. human: incremental painting is acceptable for the prototype, so the baseline is one framebuffer and dirty-tile updates. Milestone 2 still measures VRAM capacity for the record, but a tear-free mode is not built unless the incremental look fails on the unit.
8.6 Pacing: 60 fps content on a 72 Hz panel
The player polls the TMS34010 display interrupt pending bit (INTPEND DI) at
each vertical blank; interrupts remain disabled, so this is a poll, not an
ISR. A 16.16 fixed-point accumulator advances the 60 Hz timeline:
each vsync: now += 60/72
if now >= next_frame_time:
draw next record // never skip a record
next_frame_time += record.ticks
if now - next_frame_time > 1.0: // fell behind
next_frame_time = now // slow down, do not skip
If a record's paint overruns a vsync, the movie stretches: the next record
is drawn as soon as the CPU is free, and the timeline is re-based to now
rather than accumulating debt. Records are never skipped, because each one is
a delta from the previous picture. This produces the correct average rate on
a unit that keeps up, and slow motion — not corruption — on one that does
not. The 5:6 cadence (five source frames per six refreshes) is visible only as
the usual 60-on-72 judder; it is a hardware property, not a decoder stall.
If INTPEND polling proves unreliable in MAME, the fallback is a DUART counter
programmed as a 60 Hz timebase, polled with interrupts off. That is a builder
detail; the timeline arithmetic is unchanged.
9. Verification
9.1 Host pipeline tests
- Frame count, dimensions and LSB-first bit order (the harness's long-standing trap: an MSB-first packer mirrors every eight pixels).
- Polygon round-trip: rasterised simplified polygons differ from the thresholded source only in the intended simplified regions; report PSNR and changed-pixel count, not a pass/fail on aesthetics.
- Tile-delta round-trip: applying every record to a blank 640×512 canvas reproduces the encoded frame bit for bit.
- Budget: total
movie.bin≤ 3.0 MB (the hard cap is 3.03 MB), p95 paint cost ≤ 13.9 ms including the C decoder andFILLcall overhead. - Manifest records the actual numbers (data size, dirty-tile distribution, paint-cost distribution, dropped frames).
9.2 MAME pixel verdict
The player publishes a marker page in high RAM:
frame_index, ticks_played, last_record_bytes, paint_loops, data_error
VT1200_DUMP_RAM and VT1200_DUMP_VRAM at the same MAME frame give both the
marker and the picture. A checker reads the marker, reconstructs the expected
logical frame from the encoder's records, downsamples the 2×2 VRAM to
640×512, and compares exactly. A single wrong bit is a bug, not a PSNR.
Sample frames: 0, 60, 300, 600, 900, 1200, 1500, 1799, plus every scene cut. Also run a 5 s slice first; do not debug the full 30 s before one frame is exact.
9.3 MAME motion verdict
Run with -window, throttle at the emulated 72 Hz, and capture with
x11grab as the existing run-gui.sh does. Check:
- no half-painted or stale regions after a cut;
- no accumulating offset (a delta applied twice or skipped);
- the 30 s ends on the expected last frame;
- the paint-cost histogram matches the encoder's estimate.
MAME cannot prove the real unit's 60 fps: its TMS34010 interpreter has a different instruction/cycle mix from silicon. It proves correctness and gives a relative cost. The unit is the performance verdict.
9.4 Real unit
The existing harness/badapple/demo.sh boot gate (wait for the OS's ICMP
reply, then load over rdebug) is the model. The new bad-apple/load.sh:
- waits for the unit's stack;
rdebugwrites the bootstrap;- jumps;
- sends
movie.binover the tap/interface; - waits for the bootstrap's final ACK;
- observes the screen (there is no back-channel after the jump).
Record a video of the 30 s run and note the actual dropped frames. Recovery is a power cycle; say so before running.
10. Milestones
- Measure the stylised stream (host only). Extract, vectorise, tile-diff, and report the numbers. Gate: ≤3.0 MB, p95 paint cost within 13.9 ms. Produce a preview PNG/MP4 of the vectorised look for approval.
- One exact frame in MAME. Build the payload with a single hardcoded
record, paint it, dump VRAM, compare bit-exact. This proves
FILLgeometry, the 2×2 scaling, the test harness and the marker page. - Five-second player. Pack a 5 s slice, play it, compare every sampled frame, and measure the paint-cost histogram. No loader yet; use the Lua path.
- Full 30 s. Pack 1,800 frames, play in MAME, tune simplification and frame drops until the data and motion gates pass.
- Delivery. Build the rdebug bootstrap + LANCE loader, run the same 30 s in MAME over the tap, then on the real unit.
- Report.
bad-apple/README.mdand a short report with the measured data size, paint-cost distribution, dropped frames, unit behaviour and the remaining gaps.
11. Risks
| risk | why it matters | mitigation |
|---|---|---|
| Potrace/RDP temporal jitter ("boiling") | looks like a glitch even when every frame is exact | 3-frame temporal median, spatial blur, minimum feature size; preview at 60 fps before committing to the full 30 s |
| Vectorised data exceeds the 3.03 MB data area | no room for the 30 s | measured 2.79 MB before vectorisation; gate at 3.0 MB; fallback is 4×4 blocks or stronger simplification, not a shorter clip without asking |
| Dirty tiles exceed the paint budget | frames hold or stretch | build-time ticks holds, adaptive simplification, per-frame cost estimate; report the distribution |
| Incremental updates show a wipe | accepted for the prototype, but may look worse than expected on the unit | baseline is incremental by human decision; if it reads as a glitch, milestone 2's VRAM measurement feeds a double-buffered/beam-chasing variant |
| The real unit is slower than the model | the movie stretches rather than drops | never skip a record; re-base the timeline; re-encode with stronger simplification from the measured unit cost |
| LANCE loader loses datagrams | partial movie, dead machine | offsets + CRC + ACK/retransmit; whole-file CRC before jump; error block and halt instead of jumping |
| Real C-LANCE high buffer addresses | loader cannot receive above 64 KB | baseline reuses vidstream.c's high block, documented as run on the unit; fallback is a low staging ring plus copy, measured in milestone 5 |
| 60 fps on 72 Hz | unavoidable judder | fixed-point 5:6 timeline; document it; do not call it a decoder bug |
| OS video mode not configured | no picture after takeover | inherit and publish the OS registers; boot path is "wait for the OS, then take over" |
| One demo per boot | a hang costs a power cycle | verify the image/checksum before jumping; README warning; no automatic retry after takeover |
| MAME timing is not silicon | false confidence in 60 fps | correctness in MAME, performance measured on the unit; report both |
12. Left to the builder
- Exact RDP epsilon, minimum feature size, and temporal-filter width: tune by
preview, then freeze in
params.json. - Whether the tile background choice is per-tile majority or a cost-minimising search; both are host-side and cheap.
- The precise
CONTROLmask and hoisting split forFILL L; copy the verifiedvram.asmdiscipline and measure once on the unit. - Whether a short slice uses the Lua/debugger load or the same bootstrap; the full 30 s uses raw LANCE because the OS is dead before the data lands.
- The marker page layout and the exact MAME dump procedure.
- Whether the first 30 s starts at t=0 (assumed) or at a musically chosen offset; t=0 is the default.
- Whether a stop/pause key is worth keeping. It would require one live ISR or polling the keyboard DUART; the default is no input, as the OS is dead.
13. Decisions from this session
- human: full screen, 1280×1024, first 30 s.
- human: as close to
hd60.mp4as the medium allows. - human: flat black-and-white polygons in the Another World spirit; drop dither and simplify.
- human: 60 fps target, drops allowed for complex scenes.
- human: the program may take the whole 4 MB; the OS may die; recovery is a hardware restart.
- human: bootstrap over
rdebug; the existing network loader may be used to speed the big load. - human: new top-level
bad-apple/folder. - human: incremental dirty-tile painting is acceptable; a brief wipe on a large change is not a failure for this prototype.
- human: stretch the 4:3 source to the 5:4 panel; keep all content.
- call: 640×512 logical grid (2×2 blocks), 32×32 logical tiles.
- call:
FILL-only runtime;LINEreserved for debug outlines. - call: 60 Hz timeline on a 72 Hz panel, with build-time
ticksholds. - call: 30 s from t=0.
- call: a
--fit cropswitch remains available for comparison, but stretch is the default.