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
This commit is contained in:
2026-08-02 15:58:47 -04:00
parent f8fc086df3
commit 6536658c92
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

View File

@@ -0,0 +1,99 @@
# AKLABS MEGADEMO '85
A demoscene production for akbasic, written as if it were 1985 and this dialect were
the machine under the tree. Code and FX by Tachikoma, dedicated to GameCube,
megagreetz to starfort.
```sh
./build-akgl/basic examples/megademo/megademo.bas
```
Q or ESC quits. Six scenes: a title card, a Lissajous braid, a spinning wireframe
cube and octahedron — which drift together, ignite a pulsing origin point strobing
the next scene's colours, swallow the camera as the perspective scale outgrows the
window, and split that origin into the first two raster bars — then the raster
bars proper, the sprite bank going supernova — the letters fall into the middle, a
fireball throws them at the walls, and a black hole drags everything back into the
centre as the transition out — and a finale that composes all of it. A sawtooth
chiptune in A minor throughout, with a noise drum break, if a SID answers.
## What it leans on
This is a tour of the interpreter's edges, on purpose:
- **No `RND`** — it carries Chapter 17's LCG, seeded from `TI#`.
- **No palette writes** — the "colour cycle" on the logo is an honest redraw of the
same strokes in the next colour, and the raster bars are erased and redrawn two
scanlines at a time. The palette is sixteen fixed colours and the demo says so on
screen.
- **Eight sprites** — so the lettering *is* the sprite bank. Each letter of GAMECUBE
is drawn with the stroke font from `examples/breakout`, captured with `SSHAPE`,
installed with `SPRSAV`, and tinted with `SPRITE`.
- **`SOLID` walls and `COLLISION 2`** — the supernova registers four rectangles and
reverses a bearing per `RCOLLISION(n, 7)`. The interpreter does the physics;
BASIC does one subtraction. The letters leave the blast on `MOVSPR`'s continuous
form, eight bearings fanned evenly around the compass.
- **The black hole is `RSPPOS` plus one rotation** — read each letter's position
back, rotate the offset from centre, shrink it by 0.86, put it there. Thirty-four
frames later everything is in the middle. The screen goes with it: an iris of
collapsing black `CIRCLE`s, which works because the drawing verbs clip silently
at the window edge — a fireball and an iris can both start bigger than the
screen.
- **The camera is a multiplier** — the wireframe zoom-through is nothing but the
perspective scale growing fourteen percent a frame until the solids' edges leave
the window, which the silent clipping permits. The pulsing origin point strobes
through the bar ramp table because it *is* the next scene: it splits, rides the
Y axis to the band edges, and stretches into the first two raster bars in place.
- **3D with no matrices** — the wireframe scene rotates fourteen pooled vertices
through two composed axis rotations and a perspective divide, all of it `SIN`,
`COS` and one division, with the float kept on the left of every product because
the left operand decides the arithmetic. Erase is the raster bars' trick: redraw
the previous frame's twenty-four edges in black.
- **`PLAY` is one queue** — every note holds it for its length and the whole batch
plays on one instrument, so three `V` tracks come out end to end, not layered. The
soundtrack is therefore beeper music, the way single-channel tracker tunes were
written: sixteenths that take turns being the kick (an `O1` root), the pumping bass
(`O2`/`O3` octaves) and the rolling arpeggio (`O4` and up), two alternating
eight-beat batches — Am F C G, then Am F Dm E with the G sharp — and every third
batch a bar of noise drums. Noise is real: `ENVELOPE`'s sixth argument is a
waveform, 0 to 3, which Chapter 7 forgot to mention. Voice 1 carries the tune so
the collision blips (voice 2) and scene sweeps (voice 3) never steal its channel.
It also probes for its hardware like a proper boot loader, with one `TRAP` per
device: no graphics refuses by name and exits, no audio mutes the soundtrack, no
text grid drops the status row, and a host that never sets the clock — which is
exactly what `tools/screenshot.c` is — renders each scene as a still and leaves the
finale as the picture. The same file runs everywhere; nothing hangs.
## The ones that bit
**A scalar first assigned inside a `GOSUB` dies with that call's scope.** The first
cut computed text centring in a subroutine and read the result after `RETURN`; every
line of text rendered flush left, because the variable read back as a fresh zero.
Reads climb the scope chain, so a nested subroutine can *use* your globals — but a
write to a name that exists nowhere creates it in the current scope, and the scope
is not coming back. That is why Chapter 18's listings predeclare.
**Then the predeclaring bit back.** The obvious lesson — predeclare everything — ran
the 128-slot variable pool dry the moment the wireframes arrived: `Maximum runtime
variables reached`. A predeclared scratch variable occupies a slot forever; a scoped
one gives its slot back at `RETURN`. So the rule is narrower than it first looks:
predeclare a subroutine's *outputs* and nothing else. This program predeclares four.
**And the raster wait strangled the SID.** The `VSYNC` label used to spin on `TI#`
until the jiffy turned — romantic, and fatal. The host releases at most one queued
`PLAY` note per frame, and a frame of 256 *executed* spin statements is slow enough
to drop the release rate below the tune's fifteen notes a second. The queue never
reclaims slots until it drains completely, so the backlog compounded quietly for two
scenes and then `PLAY queue is full` killed the show. `VSYNC` now sleeps a frame —
`SLEEP` holds at no per-step cost — and `TUNE` traps the overflow on hosts slower
than this one, dropping a bar instead of the whole demo. `TODO.md` §4 carries the
measurement.
## What it wanted and could not have
Recorded properly in `TODO.md` §4, "What a demoscene program wanted and could not
have": `RND` as a real function, palette writes, `GSHAPE` XOR stamping, a way to ask
whether `PLAY` has drained, per-voice `PLAY` queues so three voices can actually
sound together, and a frame-edge wait that is not a busy loop. None of them blocked
the demo; all of them left a routine behind that every future game will carry too.

File diff suppressed because it is too large Load Diff