MARCIN RUSINOWSKI — ONLINE SINCE 90s MAC OS · C++ · UNREAL ENGINE EN · PL*
Demoscene — ← back to Demoscene

Inside Dead Air, fitting a moon into 4096 bytes

2026-08-30

The moon does not need an introduction. It is the one object in the sky that is not an abstraction: not a distant point of light like a star, not a smudge you need a telescope to place like most planets, but a shape everybody has actually watched, full and half and gone, since childhood. A whole generation learned to moonwalk from Michael Jackson's own video game before anyone taught them what an orbit was. That is not a technical reason to build something about the moon, it is just the honest one: the moon is close, it is familiar, and it has been part of ordinary culture in a way no other object in the sky has.

Dead Air is a 4K intro: a self-contained Windows executable, no assets, no external files. Everything from the terrain to the music is generated at runtime, and the whole thing has to fit inside 4096 bytes.

It started life under the working title "Surface," and the two names describe the same object from two angles. The visual is a grayscale flyover over a procedural lunar surface. The presentation frames it as a dead broadcast channel: a moon shot beamed in on a failing signal, VHS tracking errors and all. It was built for Xenium 2026's 4K Intro competition, under this year's "Television" theme.

Everything you see on screen is computed, not stored. There's no heightmap, no texture, no mesh. A single fragment shader receives a time value and a pixel coordinate and has to decide, for every pixel of every frame, what color that pixel is, starting from nothing but math.

The ground: noise, folded and flattened

The terrain is a heightfield built out of fractal Brownian motion: the usual trick of stacking several octaves of a simpler noise function at increasing frequency and decreasing amplitude, so you get large rolling shapes with progressively finer detail riding on top of them.

The base noise is value noise, a random value dropped at every lattice corner, then blended between corners with a smooth interpolation curve. The curve matters more than it sounds like it should. The first pass used the standard cubic smoothstep, and the terrain came out scarred with straight terrace lines running across every slope. That's an artifact of the curve's second derivative jumping at each cell border, which then shows up as a kink wherever the surface normal is computed from neighboring height samples. Switching to a quintic curve, continuous in its second derivative, removed the terracing entirely. It's a small change in the code and a large one in how convincing the ground looks.

The large landforms come from two of these fbm octaves at a low base frequency, each one ridge-folded. Instead of letting the noise go negative, it gets reflected back up using a smoothed absolute value, `1 - sqrt(x² + ε)`, rather than a hard `abs(x)`. The hard version paints another set of crease lines straight across the terrain, for the same reason as the smoothstep did: a discontinuous derivative, propagated into the lighting through the normal.

The smoothed version folds the noise into ridge-like shapes without leaving a seam. On top of that, a polynomial smooth-max function flattens everything below a certain height into flat lunar maria, blending the flat and the mountainous into each other with a rounded joint instead of a crease. That's the visual difference between "flat area next to mountain" and "flat area smoothly becoming mountain."

Four more detail octaves ride on top of those two base ones, sharing the same noise chain but pushed to higher frequencies. Their amplitude isn't constant. It's modulated twice: pulled down toward a floor near the flattened maria level, so the "water" reads as dead flat rather than subtly bumpy, and separately scaled by a very low frequency noise field so some regions of the moon come out jagged and others smooth, breaking up what would otherwise be a uniform texture repeated everywhere.

Craters are a separate layer stacked on top of all of that, at three different size classes. Each class is a jittered grid: the plane is divided into cells, and every cell rolls whether it spawns a crater, at what radius, and with what offset from the cell center. The profile itself is a bowl with a raised rim, blended smoothly back to flat ground past about 1.4 crater radii.

The jitter ranges needed an explicit cap. Early versions let a crater's rim reach past the edge of its own cell, so a crater's silhouette could get sliced by the neighboring cell's boundary, another straight-line artifact. Capping radius and offset so the two together never exceed half a cell width fixed it, at the cost of retuning the density and scale of all three crater classes to keep the same overall coverage.

Surface micro-detail (the fine-grained roughness that keeps close-up ground from reading as a smooth ridge) comes from a small three-octave fbm, rotated a fraction of a turn between octaves to avoid the lattice itself becoming visible as a repeating pattern. Its gradient perturbs the surface normal for a cheap bump-mapping effect, and the same noise field doubles as an albedo mottle, both fading out with distance so far terrain doesn't shimmer.

The camera doesn't touch a mesh, it walks a function

There's no geometry to intersect. The renderer is a heightfield ray march: starting from the camera, step along each ray, sample the terrain function's height at the current horizontal position, and compare it to the ray's current altitude. When the ray dips below the ground height, you've crossed the surface.

This is deliberately not a true signed-distance field. The height-above-ground value doesn't tell you the real distance to a steep wall standing off to the side, only how far you are from the ground directly beneath you. That makes the march cheaper (one height sample instead of a correctly bounded distance estimate), but it means the step size has to stay conservative.

Each step is a fraction of the current height gap, with a floor proportional to distance already traveled. Without that minimum, a ray grazing nearly parallel to a flat plain can take steps so small it never actually reaches the horizon, and you get hard, wrong edges where flat maria should fade into the distance instead.

Downward-facing rays get a shortcut. Since the terrain's maximum height is known, a ray already pointed down can jump analytically straight to the point where it enters the terrain's height range, skipping the empty air above the mountains instead of stepping through it one sample at a time.

Once a step lands below ground, the ray has overshot: the true surface is somewhere between the last point above ground and this one below it. Four steps of bisection narrow that bracket down and land the hit almost exactly on the surface. Skipping this and just using the overshot point produces the same kind of terracing the noise curve fix addressed, this time from the raymarch itself rather than the height function.

The world also isn't flat. A small quadratic term subtracts height as a function of squared distance from the camera, which is a cheap stand-in for planetary curvature. It bows the horizon down convincingly enough that a ray pointed dead level from camera height still eventually intersects the ground, rather than sailing off to infinity.

Lighting is a single hard sun, with no sky fill beyond a tiny ambient term, which fits the vacuum setting. Shadows are a second ray march, from the hit point toward the sun, with a growing step size and a soft-shadow trick common in raymarched scenes: track the minimum ratio of height-above-ground to distance traveled along the shadow ray. That produces a penumbra, partial shadow near the edge of an occluder, full shadow well inside it, for roughly the cost of the regular march.

The starting offset for that second ray has to grow with distance from the camera too. Too small an offset and far-away shadow rays start out already slightly underground, from the same floating-point precision limits that make anti-aliasing hard at a distance, and you get random black specks scattered across otherwise lit terrain.

The sky, for rays that never hit anything, is its own small piece of math: a star field built by projecting the ray direction onto a grid and hashing each cell to decide whether it holds a star, with a second hash for size and a simple time-based twinkle, plus a horizon glow that's a true exponential falloff with altitude rather than a flat-colored band. That's the difference between a glow that looks like scattered light and a hard-edged colored shape sitting behind the terrain.

The other four kilobytes: music, and making it small

None of this would fit in 4096 bytes as ordinary compiled code. The raymarch, the noise stack, the crater layers, the shadow logic, and the audio synthesis all have to survive a specific compression-oriented build pipeline built around a handful of well-known size-coding tools.

The shader source is written as ordinary, readable GLSL, then run through a minifier that strips whitespace, shortens identifiers, and generally turns it into the smallest text that still compiles. This step happens before the final compression pass, and it matters because the compressor downstream works on text redundancy, so a shorter, denser source compresses better than a verbose one, even accounting for the loss of naturally repeating variable names.

The resulting shader source is embedded as a string inside a small C program, compiled with the Microsoft compiler in a mode that strips out the C runtime entirely: no libc, no startup code beyond what's strictly needed to open a window, get an OpenGL context, and hand the shader to the GPU. That executable is then run through Crinkler, a linker built specifically for the demoscene that replaces the normal linking step with an aggressive compressor tuned for small executables. It doesn't just zip the file, it models the actual entropy of x86 machine code and shader text well enough to routinely beat general purpose compressors by a wide margin on this kind of program.

Every visual effect beyond the base terrain render (the VHS tracking-error tear, the analog "flagging" warp at the top of frame, color-static bursts, a datamosh-style blocky freeze, chromatic aberration and ghost-trail bursts, a vignette, scanlines, film grain) is written as an independent, individually toggleable feature behind its own compile-time flag. A small custom preprocessing step runs before minification and physically deletes the code for any disabled feature, rather than leaving it in as dead branches the compiler might or might not remove.

That made it possible to actually measure what each effect costs in the final executable. The bare terrain, no sky, no glitches, compiles to a bit under 2 kilobytes. Stars and horizon haze add well under two hundred bytes. The full stack of eight glitch effects together costs under five hundred more. Watching that number per feature is most of the actual craft in a project like this: the render itself is "free," in the sense that the GPU does the heavy lifting every frame regardless of scene complexity, but every line of GLSL source is rent, paid once, against a hard 4096-byte ceiling.

The music runs on 4klang, a well-known demoscene synthesizer that generates its entire song from a compact, hand-editable table of instrument and pattern data rather than storing any audio samples. The runtime cost is a small assembly-language playback engine plus that data table, and the actual sound is synthesized live from oscillators, filters and a shared reverb tank the moment the intro starts.

Composing for it in practice meant writing the piece normally, in a regular DAW, exporting it as MIDI, and converting each note's onset and duration into the synth's own timing grid and instrument parameter format. That's a translation step, since 4klang has no piano-roll interface of its own, only patterns of raw parameter bytes. Getting the loop to repeat cleanly, a bass line right, and a reverb tail that doesn't clip the loop point, all turned out to be more about correctly reading MIDI export quirks than about the DSP itself.

On the smallest end of the budget, even single bytes matter: an unused synthesizer feature left enabled in the engine, a duplicated table entry, a pattern list one slot too long. All of it is code the compressor still has to store, whether or not anything on screen or in the mix ever uses it, and finding that kind of dead weight was worth real, measurable space back.

The build pipeline end to end looks like this: shader source, through the toggle stripper, through the minifier, into a C program compiled without a runtime, linked with the assembled synth engine, and the whole executable handed to Crinkler for final compression. Every stage in that chain exists purely to buy back bytes for the next one.

The numbers

Source code, before any minification, is not small. The shader is 680 lines, 33537 bytes. Stripping out code behind disabled feature toggles brings it to 616 lines, 31530 bytes. After minification it becomes a single 684-line, 37814-byte C string: minified GLSL is denser in identifiers but loses the line breaks and comments that made the raw count smaller, so the line count alone is not a good size signal, only the final compressed output is.

The rest of the source: `main.c`, the entry point and setup code, is 510 lines, 27922 bytes. 4klang's own synth engine, third-party assembly, is 1767 lines, 51298 bytes. The instrument and pattern data, the part actually edited by hand for this track, is 925 lines, 35588 bytes.

None of these source sizes map directly to the final executable. What matters is what Crinkler does with them, and that was tracked build by build over the course of the project.

Early in development, with no audio at all and every visual toggle on, the bare terrain (no sky, no glitch effects) compiled to 1991 bytes. Adding stars and horizon haze brought that to 2133 bytes, a cost of 142 bytes. Turning on the full stack of eight glitch effects brought the total to 2616 bytes, 483 bytes for all of them together.

Once the 4klang audio engine was wired in, the size jumped, as expected: the first working build with real audio came in at 4514 bytes, over budget. From there it went down in a series of concrete steps: removing nine unused instrument slots that were still present as dead data saved 187 bytes, down to 4327. Turning on Crinkler's more aggressive compression settings (`/COMPMODE:VERYSLOW`, higher hash size and hash tries, `/TRANSFORM:CALLS`, `/SATURATE`) saved another 128 bytes, down to 4199. A pass trimming unused pattern slots and a dead-code cleanup across the shader and `main.c` brought it to 3984 bytes, the first audio-enabled build under the 4096-byte limit.

From there the size moved up and down in smaller amounts as the piece was finished: a build with the tempo changed from 100 to 25 BPM came in at 3993 bytes, and later checkpoints in the project's own build log range between roughly 3786 and 4083 bytes, depending on what was being tuned that day (reverb settings, pattern edits, shader changes).

The final competition build, `deadair.exe`, is exactly 4032 bytes, confirmed by direct byte count on disk.

There is no clean number for how those 4032 bytes split between shader code and music data in that specific build. Crinkler's compression report exists, but its per-symbol breakdown reflects references and compressed entropy sharing between symbols, not a "this many bytes are shader, this many are music" split, so no such number is given here.

What the project's own build log does say plainly, from a size-optimization pass partway through development: the shader was, at that point, the single largest compressed payload in the executable, larger than the audio engine and its data combined.

Dead Air placed second in the 4K PC Intro category at Xenium 2026, according to the intro's own information file.

A personal note

I build renderers and work on graphics optimization for a living, in the game industry, day in and day out. Dead Air was a rediscovery for me: different tools, a different pipeline, a different target than anything I touch at work. No engine, no asset pipeline, no production constraints, just a shader, a linker, and a hard byte limit to fight against.

It gave back something that daily production work rarely does: plain fun, the kind that comes from building something as small and complete as it can possibly be, start to finish, on my own terms. That is not marketing language for this project. It is just true.

Watch it

Dead Air on Pouet · Dead Air on Demozoo