Add the AKLABS megademo example

A six-scene demoscene production that pushes the interpreter to its
edges on purpose: an LCG where there is no RND, redrawn strokes where
the palette cannot be written, the sprite bank as the scrolltext, a
supernova and black hole out of SOLID/COLLISION/RSPPOS, wireframe 3D
from two rotations and a perspective divide, and a single-queue
beeper-style soundtrack with a noise drum break.

The same file runs in the standalone frontend and in the clockless
screenshot host, which gets each scene as a still. Three interpreter
lessons learned the hard way are recorded in the example's README and
TODO.md section 4: a scalar first assigned in a GOSUB dies with its
scope, predeclaring scratch runs the 128-slot variable pool dry, and
a TI# busy-wait starves the PLAY queue of its one release per frame.

Co-Authored-By: Claude Code (Fable 5) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
2026-08-02 15:58:47 -04:00
parent e2a3f08613
commit 96cfd21de8
3 changed files with 1535 additions and 0 deletions

67
TODO.md
View File

@@ -727,6 +727,73 @@ Also on the queue, from the reference's own defect list:
so moving one operand out of the way only shifted the collision along. Covered by
`tests/language/arrays_in_parameter_lists.bas` and `tests/runtime_evaluate.c`.
### What a demoscene program wanted and could not have
`examples/megademo/megademo.bas` was written to push the interpreter to its edges, and these
are the edges it hit. None of them blocked the demo — every one has a workaround, and the
program carries all six — but each workaround is a routine every future game will carry too,
and that is the argument for the verb. Ordered by how much BASIC each one would delete.
- **`RND` is still missing.** Chapter 17 teaches the LCG workaround and the demo's `RANDOM`
label carries it: nine tokens, two convention globals (`RMAX#` in, `RND#` out), and a seed
the caller must remember to take from `TI#`. That is the right *teaching* example and the
wrong thing to make every program transcribe. Closing it is the standard function shape —
dispatch-table row, exec handler, tests, a Chapter 12 row — and Chapter 17's section becomes
a history lesson instead of a requirement. `ASC` is in the same boat with a worse workaround
(`INSTR` into a 41-character alphabet string); `INT` and `SQR` are not, because integer
assignment and `^ 0.5` already are those functions.
- **The palette cannot be written.** `PALETTE[17]` in `src/graphics_tables.c` is `static
const`, and `COLOR` can only choose among its sixteen entries. Palette rotation was the
cheapest animation of the whole era — one register write cycled every pixel of a colour —
and here the same effect is a full redraw: the demo's title "colour cycle" re-strokes the
logo every frame (`DRAWTEXT` with `CM# = 1`), and its raster bars erase and redraw five
ramp lines apiece per frame. Closing it means palette state moves from a static table to
the graphics backend, plus a verb form to set an index (`COLOR` grew nothing new on a C128,
so this would be ours — Chapter 13 material), plus the docs figures that assume Pepto's
values stay put.
- **`GSHAPE` stamps in one mode.** BASIC 7.0's takes a mode argument — replace, OR, AND,
XOR — and XOR is the one that mattered: stamp to draw, stamp again to erase, no damage to
what was underneath. That is software sprites beyond the eight real ones, and it is the
difference between the demo's raster bars chewing holes in the starfield (they erase in
black) and leaving it intact. Needs a blend argument through `SSHAPE`/`GSHAPE`'s render
path in `src/graphics_akgl.c`, and libakgl may need to offer the blend mode first — check
before filing there.
- **Nothing reports whether `PLAY` has drained.** The queue depth exists in
`akbasic_AudioState` (`count - head`); no function reads it. The demo's `TUNE` label
requeues each batch on a wall-clock guess — it stamps `MT#` with the batch's computed
length plus a breath, and the sums drift against `TEMPO` and had better stay under the
queue's 128-note cap when a requeue lands early. An `RPLAY(v)` returning notes pending —
same shape as `RSPPOS` — deletes the arithmetic. It also fixes the quiet failure Chapter 7
warns about, where a program `QUIT`s before its music finishes and never knows.
- **`PLAY` is monophonic: one queue, one clock, one instrument per batch.** An earlier
version of this entry claimed there was no noise waveform, and that was wrong —
`ENVELOPE`'s sixth argument selects triangle, sawtooth, square or noise
(`src/runtime_audio.c`), which Chapter 7 does not document and should. The real gap is in
`akbasic_play_service()` (`src/play.c`): every voice's notes go into **one** queue with one
release clock, and the envelope applied when a note sounds is whatever the last `T`
*parsed* selected — so a program that writes three `V` tracks gets them end to end, on one
instrument, and `M` is a no-op precisely because there is nothing to synchronise. The
backend already has three independent voices and `tone()` takes a voice number; what is
wanted is a queue head and clock per voice, and the envelope resolved at parse time into
the note rather than read from shared state at release. The demo works around it the way
1985 did on one channel: an interleaved kickbassarpeggio line (the `TUNE` label), which
is authentic and should not be mandatory.
- **The only vsync is spinning on `TI#`, and the spin is worse than it looks.** Chapter 13
documents the polling; the demo's first cut had the loop — read `TI#`, branch to yourself
until it moves — and it did not just waste CPU, it **starved the `PLAY` queue dry**. The
chain is measurable: the host calls `settime()` once per `runtime_run()` batch, and
`akbasic_play_service()` can therefore release at most one note per batch (after a release,
`nextms` sits ahead of the frozen clock). A batch of 256 *held* `SLEEP` steps is nearly
free, but a batch of 256 *executed* spin statements is slow enough to drop the release rate
below a sixteenth-note tune's fifteen notes a second — and the queue never reclaims slots
until it drains completely, so the backlog compounded across scenes and overflowed the
128-note queue with `PLAY queue is full`. Reproduced both ways in isolation: the same tune
requeued eight times over a `SLEEP` loop survives, over a `TI#` spin it dies on the third
batch. The demo's `VSYNC` label now sleeps a frame instead. A verb that parks the program
until the next batch boundary (`SLEEP 0` is an available spelling) is the same
hold-without-blocking shape `SLEEP` and `GETKEY` already have in `src/runtime_console.c`,
and it would make the honest spelling also the safe one.
---
## 5. Deliberate deviations from the reference