Xenon 2

Behind the scenes

How the port works

Nothing here is a remake. The game you play is the 1989 Atari ST program, byte for byte, running on an emulated 68000. What changed is how it reaches your screen.

ST binaryThe original Xenon 2 program and its data, restored from a saved game state.
HatariThe 68000, the ST hardware and the sound chip are emulated as usual.
Draw hooksBreakpoints on the game's own draw routines record every sprite, tile and star it draws.
Sprite atlasEach sprite is looked up in a packed atlas at 1× — or in a 4× upscaled one.
GPUThe frame is rebuilt as a few hundred textured quads in WebGL, with optional motion interpolation.

Three passes per frame

Xenon 2 draws every frame in three separate 68000 code paths: the wall-tile and background pass, which redraws the whole visible tile grid from scratch; the starfield pass, which plots 48 single-pixel stars in four brightness bands that scroll at different speeds; and the object pass, which walks a linked list of game objects — the ship, enemies, bullets, particles, formations, boss segments — and calls each object's own draw routine through a function pointer.

That last call site is the key. At one exact program counter, the address register always points at a 98-byte object record whose fields are fully committed: position, sub-pixel fraction, current sprite, animation state, type. Hooking that instruction identifies every object in the frame and what it is. Hooks where the draw routines enter the game's blitters then record exactly what each object draws, including routines that draw only sometimes or assemble several pieces. The same trick, applied to the tile and star routines, captures the rest.

From a draw log to a picture

The recorded draw commands are replayed against a sprite atlas that was built offline by scanning the game's own sprite data in memory. A second atlas holds the same sprites upscaled 4×; since the renderer only needs a rectangle per draw, switching resolution is one global flag — the Hi-res sprites row in the in-game menu, or ?hires=1 on the play URL.

The game itself updates its world only every fourth display frame (12.5 times a second) and scrolls in whole pixels. Because the renderer knows each object's identity from frame to frame, it can interpolate positions between updates and present smooth motion at the display's refresh rate — the High FPS option. Effects the original computes on the CPU, such as the flash when something is hit, are reproduced in the fragment shader instead.

In the browser all of this runs as WebAssembly. The emulator returns to the browser's event loop at every vertical blank rather than unwinding its stack through Asyncify, which is what made the port fast enough for phones.

Interactive explorers

Self-contained pages generated from the reverse-engineering data. They open in the same tab; use the bar at the top to come back.

Write-ups

The working notes, rendered as-is. They were written for the developer, so expect addresses, register names and a lot of detail.

A gentle disclaimer: each document describes the project as it stood when that document was written, and later work has overtaken some of them. The autopilot is the clearest example: the early notes describe a Python controller talking to the emulator over a socket, which was later substantially reworked when it was ported to C inside the emulator. Where notes disagree, the more recent one (dates are in the headings and at the top of each page) is usually the one that matches the code.

Reverse engineering & GPU rendering notes

The main write-up: the game's three draw passes, the 98-byte object struct, sprite formats, the starfield, zoom-text screens, and how every draw is intercepted and rebuilt on the GPU.

XENON2.MD

Sound driver notes

How the game's own 68000 sound driver plays the Megablast theme and effects on the ST's PSG.

SOUND.MD

4x atlas upscale & frame interpolation

Where the hi-res mode comes from: a 4x upscaled sprite atlas and sprite-stream interpolation for smooth 60 fps motion between the game's 12.5 Hz updates.

plan-atlas-upscale-and-frame-interpolation.md

HD wall tiles: the seam problem

Why 4x-upscaled wall tiles show seams, what edge padding and map-context upscaling each fix and break, the numbers behind it, and the options still open.

HD_WALL_TILE_SEAMS.md

Scripted sine-motion bytecode

The tiny movement bytecode that drives many enemy flight paths, decoded from the 68000 code.

SCRIPT.MD

Shop item catalog

Every item Crispin sells, with names and prices read straight out of the ROM tables.

shop-items-catalog.md

Wall tiles on the GPU

Capturing the opaque and masked 16x16 wall tiles so the level geometry can be reconstructed.

plan-wall-tiles-render.md

The scrolling background mosaic

Rendering the parallax backdrop as one scrolling bitmap instead of tile by tile.

plan-background-scrolling-bitmap.md

Starfield capture & rendering

The 48-star routine, its four depth bands, and how it is reproduced on the GPU.

plan-starfield-gpu-rendering.md

The hit-flash tint effect

Reproducing the one-frame flash the game draws when something is hit.

plan-flash-tint-effect.md

Draw command stream → sprite atlas

The capture pipeline: recording every draw call and replaying it against a packed atlas.

plan-drawcommandstream-sprite-atlas-render.md

Shop screen palette

Why the shop needed its own palette snapshot and how the atlas handles palette switches.

shop-palette.md

Zoom-text interstitial screens

Capturing the GET READY / HIGH SCORE fly-through screens with their perspective starfield.

handoff-zoomtext-starfield-capture.md

Removing Asyncify from the WASM build

How the browser build returns to the event loop at VBL boundaries instead of unwinding the WASM stack, and what it did for performance.

remove-asyncify.md

Browser build performance

Measuring a slow browser session (the PERF line and A/B URL switches), why phones ran at 35 VBL/s, the fixes that brought them to full speed, and what is still open.

WASM_PERFORMANCE.MD

AVI recording architecture

Recording the authentic display and the reconstruction side by side for comparison.

AVIRECORDING.MD

Profiling autoplay and replay

The offline profiling workflow used to keep the autopilot and renderer fast.

PROFILING.MD

Building and running Hatari on Windows

The reproducible build/launch script and the DLL and asset pitfalls it works around.

HATARI_BUILD_RUN.MD

Save and load

Five save slots in the options menu, each with a screenshot of the game: how saves are stored on desktop and in the browser, and why the autopilot's object identities are saved too.

plan-save-load.md

Ship weapons, rendered and compared

Every purchasable weapon bought through the real shop and recorded next to the original display: 41 probes, what matches pixel for pixel and the differences that remain.

SHIP_ATTACHMENT_RENDER_TESTS.md

The missing Level 4 tongue and Drone

Conditional draw wrappers the capture skipped, and the move to capturing sprites where the game actually calls its blitter.

LEVEL4_SPRITE_RENDERING.md

The timed-power countdown

A digit drawn directly, outside the object list, that no object audit could find, and the hook that captures it.

POWER_COUNTDOWN_RENDERING.MD

Worms behind the terrain

Level 2 worm segments drawn with a terrain mask, and how the GPU view reproduces the tile occlusion.

SPRITE_STREAM_TERRAIN_MASK.md

Recording the GPU view with sound

Higher-resolution AVIs of the GPU view (1280x800 at scale 4) with the game's own sound, paired frame by frame despite late GPU downloads.

SPRITE_AVI_RESOLUTION_AUDIO_20261005.MD

AI errors: the shop-screen session

27 mistakes made while getting the shop screen to render, and the patterns behind them. The autopilot has its own, much longer log.

AIERRORS.MD
Other notes