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:
67
TODO.md
67
TODO.md
@@ -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 kick–bass–arpeggio 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
|
||||
|
||||
99
examples/megademo/README.md
Normal file
99
examples/megademo/README.md
Normal 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.
|
||||
1369
examples/megademo/megademo.bas
Normal file
1369
examples/megademo/megademo.bas
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user