Files
Tachikoma 13f1df03ef
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m46s
akbasic CI Build / coverage (push) Successful in 4m16s
akbasic CI Build / sanitizers (push) Successful in 8m10s
akbasic CI Build / akgl_build (push) Successful in 8m10s
akbasic CI Build / mutation_test (push) Successful in 17m55s
Unbreak the three example programs the RND merge left behind
Adding native RND and ASC (ae2c702) made RND a function name, and a suffixed
identifier that collides with one is refused -- "SYNTAX ERROR Reserved word in
variable name". Three example programs held their PRNG output in a variable
called RND#, or a host field called RND%, and none of them had run since:

  - examples/galaga/ bound RND% as a host field on both ENEMY and GAME. The
    BASIC-visible name is ROLL% now; the C member stays `rnd`. This one was
    caught by example_galaga and example_galaga_interop, which have been
    failing.
  - examples/breakout/characters/breakout.bas and examples/megademo/
    megademo.bas both use RND# for their LCG output, renamed to ROLL#. Neither
    is in any test, so neither failure was visible.

examples/breakout/sprites/breakout.bas was broken a second way: seven REM lines
the reader refuses. Worth recording that the ceiling is not the one the message
names -- src/sink_stdio.c fails when the read filled the buffer without seeing
a terminator, so with AKBASIC_MAX_LINE_LENGTH at 80 the message says "79
character limit" and the real maximum is 78, because a 79-character line leaves
no room for the newline. The sweeps that fixed the corpus and the megademo for
this did not reach this file. The seven comments are reflowed.

The prose went stale with the code. Chapter 21 said "there is no RND verb in
this dialect; issue #16 tracks adding one", chapter 17's historical aside
offered an LCG that no longer parses, and four REM blocks across the two games
said the same thing. All of them now say RND exists, and say why these programs
keep their own generator anyway: the sequence has to be reproducible for a
headless run to be the same game on every machine, which is what lets
interop_test.c assert exact counts.

Chapter 21 also gains the rule that bit them, since a reader writing a host
type will hit it: a host field name is a bare word and shares a namespace with
every verb and function.

None of this came from the submodule bump -- all three were already broken on
main. It was found by running the tutorial games, which nothing else does;
that gap is akbasic issue #58.

Verified: all three run clean under the dummy drivers, and 114/114 default,
116/116 with akgl.

Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
Co-Authored-By: Claude Code (Claude Opus 5, claude-opus-5[1m]) <noreply@anthropic.com>
2026-08-05 23:26:33 -04:00
..

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.

./build-akgl/basic examples/megademo/megademo.bas

Q or ESC quits. Seven 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 — the raster bars proper, a full-screen vaporwave sunset decompressed out of the listing itself, 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 CIRCLEs, 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.
  • The picture is strings, because DATA is a straw — the pool is 512 items and the font owns ~350, so the full-screen sunset rides in string literals: 160x120 cells, sixteen colours, run-length encoded two characters a run and decoded with INSTR into 5x5 blocks — about 2 KB for the whole base frame. vaporwave.py composes the image in palette space — dithered by row, not pixel, so the gradients band like a CRT instead of shattering the RLE — and owns the generated block between the PICTURE-BEGIN/PICTURE-END markers. Do not hand-edit it; rerun the script. The bird gliding over it is the counterpoint: a genuine 24x21 SPRSAV type-in sprite, 63 bytes, which is everything DATA has room to say. The paint is the reveal, and the exit is a stride-63 column dissolve — 63 is coprime to 160, so the walk hits every column once and looks random while carrying no state. The strings come in 64-character chunks because a source line is 80 columns, like the machines this pretends to be — the decoder carries its cursor from one IM$ entry to the next, and the generator refuses to cut inside a run.
  • And then the picture is video. Six delta frames loop the floor grid toward you and crawl the sun's slice pattern — full motion video, 1.3 KB total, about 220 bytes a frame. A delta re-encodes only the rows that changed: an R record jumps the decoder's cursor (two base-36 digits of row), Q skips what stayed, and black draws, which is how a moved grid line is erased. The rays are not in the encoding at all; they are struck live over every frame, which is the difference between a thirty-run grid row and a five-run one. The loop is mathematically exact: line k sits at B*(r^(k+t)-1) so six frames advance t by one and land the set on itself, and the slice period divides the frame count. The generator simulates the decoder and asserts base-plus-deltas reproduces every frame before it will emit a byte.
  • 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. A batch is four PLAY statements, one bar each — the 80-column line limit will not hold four bars in one string, and it does not need to: the parser's voice, envelope and duration state persists across statements and every PLAY appends to the same queue, so four bars queue exactly as one line would.

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.