Seventeen demos in forty-nine pixels
Rusty Nail's launcher is a little console OS on an STM32H753: a purple header bar, a grid of carts, a footer of button hints, the whole thing rendered into a 128x128 PICO-8 framebuffer and pushed out over HDMI. In the corner of that header bar there used to be a blinking sparkle glyph. It blinked because the screen needed something alive in it, and that was the cheapest heartbeat I could think of at the time.
This week the sparkle became a game, and then the game became a demoscene.
That is a pixel-exact re-render of the square, not a photo - the demos are pure functions of the frame counter, so I can replay them off-target and capture every frame. On the console it sits in the top-left of the header on every screen, 9x9 pixels: a 1px white frame around a 7x7 black court.
A ball in a box
The idea was small: replace the sparkle with a tiny breakout court. One pixel ricochets around, and every time it hits a wall it changes to a random PICO-8 colour. Two constraints came out of actually watching it run. Black is banned from the dice, because a black ball on a black court is not a minimalist statement, it is an empty box. And the new colour must differ from the old one, because a "colour change" you cannot see is not a colour change.
The sizing maths has a small trap. A 1px border costs two pixels per axis, not one, so "about 10 pixels" of badge gives you 9x9 outside and 7x7 inside - and 9 is exactly the tallest box that clears both edges of the 11px header bar by a pixel. Anything bigger kisses chrome.
Closed orbits, and putting english on the ball
The first version moved the ball with two triangle waves - x bouncing 0..6, y the same but phase-shifted. It looked right for about thirty seconds. Then I noticed it never deviated: the same diamond, lap after lap after lap.
That is not a bug in the waves, it is the actual physics. A ball moving diagonally at equal speeds in a discrete box always rides a closed orbit; in a 7x7 court the orbit is a fixed 12-cell ring, and a random starting phase only picks where on the ring you begin. Real breakout does not have this problem because real breakout has a paddle. Something has to perturb the ball, or geometry wins.
So the walls put english on it. The ball is a real simulation now - position and heading carried across frames - and roughly one wall hit in three shoves it one cell sideways along the wall it just hit:
if hx || hy {
let c = 1 + ((r >> 4) % 15) as u8; // random colour, never black
col = if c == col { 1 + c % 15 } else { c }; // never a repeat
BALL_COL.store(col as u32, Relaxed);
if r % 3 == 0 && !(hx && hy) {
// english: shove the ball one cell along the wall it hit
let n = if r & 8 != 0 { 1 } else { -1 };
if hx { py = (py + n).clamp(1, 5) } else { px = (px + n).clamp(1, 5) }
}
}
Each nudge kicks the ball onto a different lap, and over a few seconds it wanders the whole court. There is one orbit you have to stay off: the pure corner-to-corner diagonal, where every bounce is a corner hit. It turns out the nudge can never put you on it - a nudge happens on a wall, so one coordinate is pinned at 0 or 6 while the other lands in 1..5, and the diagonal orbit needs both at the same corner. Geometry giveth, geometry taketh away.
Then I wanted more
A single pixel with good physics is fun for a day. But the square is a 7x7 canvas refreshing at ~56fps with a 16-colour palette, which is to say: it is a tiny Amiga, and I grew up on the demoscene. So the badge became a jukebox, and the jukebox filled up fast. Seventeen demos, in deck order:

Breakout, copper bars, a three-layer parallax starfield, plasma, the Boing ball, fire, shadebobs, a rotozoomer, matrix rain, Game of Life on a torus, moire rings, a lissajous spark with a cooling trail, a tunnel pulse, an oscilloscope, vector balls, metaballs, and unlimited bobs - the old Amiga flex where sprites stamp into a framebuffer that never clears, so the court slowly fills with copies.
Porting demoscene effects to 49 pixels is a brutal editor. Plasma, fire, copper bars and the rotozoomer shrink beautifully, because they are colour fields - every pixel carries signal. Anything built from projected geometry dies: a wireframe cube at 7x7 is eight vertices of noise, and a 216-point shape morph is fog. I also wrote a sine-wave text scroller, watched it, and cut it the same hour: at this size text reads as smearing, not as greetings. Pixel demos only.
The one that surprised me was metaballs. I had written it off - blobby isosurfaces need resolution, surely - but the maths disagreed. Two blobs on mirrored orbits, an inverse-square field, two thresholds:
let v = 48 / (1 + d1) + 48 / (1 + d2);
if v >= 28 {
fb.set_pixel(ox + xx, oy + yy, p8::GREEN); // core
} else if v >= 13 {
fb.set_pixel(ox + xx, oy + yy, p8::DARK_GREEN); // halo
}
When the blobs cross, the halos genuinely merge into a peanut before the cores touch. The signature move survives at seven pixels wide. I was wrong and I am glad.
The deck, or: how randomness starved my favourite demo
The first jukebox picked the next demo uniformly at random every 6..10 seconds. Then I sat down to admire breakout and could not find it. For minutes. I went looking for the bug - a broken match arm, a colour rolling invisible, an id off by one - and found nothing, because there was nothing. Five other demos and an unweighted die is all it takes; the chance of breakout not appearing in ten deals is about 16%, and I had simply lost the lottery while staring at the screen.
Uniform random selection does not owe you coverage. So the jukebox deals from a deck now: every demo exactly once per shuffled hand, 6..10 seconds a slot, and the deck only reshuffles when it runs dry. Nothing can starve for longer than one full hand, and a reshuffle is not allowed to deal the card that just played:
let mut deck = DECK.load(Relaxed);
if deck == 0 {
deck = (1 << DEMOS) - 1; // reshuffle all seventeen
}
let mut hand = deck;
if hand.count_ones() > 1 {
hand &= !(1u32 << id); // never repeat the demo that just played
}
let mut n = (r >> 12) % hand.count_ones();
Boot always opens on breakout. It earned that.
The dice deserve one more confession. While rendering the montage for this post I picked a seed and waited for the breakout ball to wear something bright, and it rolled dark colours for twenty consecutive bounces before handing me a pink. Random means random; if you want a guarantee, you build a deck.
What it costs
Almost nothing, which is the point. Every demo is a function of the overlay tick plus, for the four that smear across frames (fire, Life, shadebobs, unlimited bobs), one shared 49-byte scratch grid that is zeroed on every deal. Randomness is a single 32-bit LCG seeded from the boot timer. There is no allocation, no task, no state machine entanglement - the launcher's state machine does not know the square exists. Drawing 49 pixels per frame does not move the needle on a firmware that is already pushing a full 128x128 desktop out over the RP2040 HDMI pipe.
The cleanup the square forced was worth it on its own: the firmware had PICO-8 colour indices hardcoded as bare numbers in a dozen places (the header bar was colour 2, the progress fill colour 10, and so on). There is now a named constant for all sixteen -
pub mod p8 {
pub const BLACK: u8 = 0;
pub const DARK_BLUE: u8 = 1;
// ...
pub const PINK: u8 = 14;
pub const PEACH: u8 = 15;
}
- and a rule to match: nothing that draws into a PICO-8 framebuffer gets to write a bare colour number again.
Ten seconds of the original, above, with the english doing its work - watch how the laps stop repeating. Seventeen tiny machines taking turns in a box the size of a fingernail, on every screen of the console, forever. The sparkle never stood a chance.