How Exile works
03 · How Exile works

How Exile draws the world

Every graphic in the game, the spacesuit, thirty-odd creatures, the spaceship hull and all 64 landscape tiles lives in just 3.3KB. Expanded by some fantastic 6502 technique.

The previous article looked at how Exile (BBC Micro, 1988) generates its 256×256-tile world from about 500 bytes of code. This one is about what you actually see: how the graphics are stored, how another handful of bytes becomes a red rock face here and a green bush there, and how the sprite plotter gets it all onto the screen fast enough to scroll on a 2MHz 6502.

very piece of art in the game — the player’s spacesuit, thirty-odd creatures, bullets, the spaceship hull, and all 64 landscape tiles lives in one 2,592-byte spritesheet. Add the geometry tables, the tile tables, and the palettes, and Exile’s entire visual identity is about 3.3KB. The rest is, again, technique.

The interactive demos on this page are the reconstruction’s real display pipeline: the validated sprite plotter, tile plotter and scroller writing actual BBC screen memory, presented at the true 2:1 pixel aspect. You can fly the viewpoint around the world with the arrow (or WASD) keys and watch the original’s scrolling machinery work.

First, the screen

▶ demo: fly the real screen ⌗ 6502 source

The BBC Micro’s display modes trade resolution against colour within fixed memory budgets. The mode everyone remembers for colourful games is MODE 2: 160×256 pixels, 16 colours (kind of), and each pixel twice as wide as it is tall — chunky, but vivid.

Exile doesn’t quite use MODE 2. It programs the 6845 video controller directly (&01e3: eight bytes per character, 64 characters per line) to create a custom 128×256 sixteen-colour screen occupying 16KB at &4000&7fff instead of MODE 2’s usual 20KB. Trimming the width from 160 to 128 pixels buys back 4KB of RAM and, not coincidentally, makes the screen exactly as wide as the spritesheet. (As an aside I absolutely love these kind of tricks with the 6845).

Two properties of this screen shape everything downstream:

Remember I said kind of 16 colours. There’s a third trick hiding in here: the BBC’s palette makes colours 8–15 flashing versions of 0–7. Exile reprograms them to look identical to 0–7 — and uses the spare top bit as a per-pixel priority flag. When the plotter is drawing a background sprite it tests the screen byte’s top bit (&1064: EOR (screen),Y / BMI skip) and refuses to overwrite a pixel that a foreground sprite has claimed. Depth sorting, implemented as a colour number. This is how sprites can seamlessly move behind certain features in the game.

And the screen scrolls without moving a byte: the 16KB is treated as a circular buffer, the 6845’s start address register is nudged to pan the window (set_crtc_start_address, &1f9f), and the plotter wraps addresses off the end of screen memory back to the start with an ORA #&40 / EOR #&3f. Only the newly exposed strip at the screen edge is ever redrawn.

You can feel the machinery in the demo: the original scrolls in 2-pixel horizontal and 8-scanline vertical sections (watch the status line’s origin values as you fly), and the reconstruction will authentically scroll in this way in the standard version.

With the extra memory and horsepower of a modern PC, essentially unbounded in Exile terms, the enhanced version also supports smooth scrolling and a larger viewport, you can try these with the demo below and they’re available in the reconstructed game.

▶ demo: smooth scrolling (non-authentic) · ▶ demo: wide viewport

Interlude: what a wider screen breaks

⌗ 6502 source

That wide viewport turned out to be much more interesting than a rendering exercise, and the reason is worth a detour because it says a lot about how Exile really works: the world only lives on camera.

We’ll discuss this in more detail in a later article and so in some ways this is the teaser.

The game has just sixteen object slots. Everything you meet — creatures, doors, dropped items, your own bullets — is one of sixteen live records, and everything else is parked in compact side tables. Objects near the visible screen get promoted into a slot; drift far enough off it and check_if_this_object_is_far_away (&111d) measures you against the plotted window and demotes you back to a table entry (demote_primary_object_to_secondary, &0c6e). The window is only about eight tiles across — exactly the authentic screen — so on original hardware the live world is a bubble barely bigger than what you can see.

Show more world, then, and you’re showing tiles whose objects are dormant: doors that won’t open, creatures frozen mid-air at the flanks. So the enhanced fit-to-screen mode widens the activation bubble to match the viewport — but only there. This is gated hard, because plenty of genuine gameplay technique depends on exactly when things activate; every validator and the authentic 4:3 mode run with the widening at zero, where the code is bit-for-bit the original’s. With it on, the far-away window slides left and grows right by the extra visible tiles (the X axis only), and the columns flanking the authentic window get the same promotion pass the scroller runs for newly exposed strips.

Shipping that promptly broke the game in a way I didn’t recall from 1988: while fixing a bug with the hovering balls under the under the Pericles I noticed that the fix didn’t resolve the game when played in wide screen. The cause was fascinating. Ambient spawners — nests, pipes, hives — don’t just need a free slot: spawn_object (&9aec) demands five of them (&9aba), a deliberate reserve that keeps doors, explosions and your own bullets from ever being starved by wildlife. In the authentic window that budget works because flank scenery constantly churns out of the slots as you move. With the widened bubble, the flanking doors, bushes and collectables were pinned live permanently: measured at the nest, the original view idles at about seven free slots, the widescreen view at three and the five-slot bar is never met. Resulting in no spawns, ever. The wider camera had spent the world’s entire object budget on scenery.

With the enhanced mode fix the widening now yields under slot pressure. Each frame the enhanced mode counts free slots; below five (the spawners’ own threshold) the effective overscan shrinks a tile per frame, and the original’s far-away machinery, running unmodified, demotes the flanks and reclaims the slots, state preserved. At six or more it regrows slowly, a tile every 32 frames. Busy areas settle into a partial widening that always leaves the spawners’ budget reachable; quiet areas get the full viewport. In the nest scenario the spawn timing under pressure now matches the authentic 4:3 run exactly, while the wide view keeps working everywhere the world can afford it.

I found the symmetry really satisfying: you can’t widen Exile’s camera without widening its economy. Sixteen slots isn’t an implementation detail, its pivotal. The game’s pacing, its spawn rhythms, even the little reserve that guarantees your gun always fires, are all balanced against it.

Storage: one sheet, two bits per pixel

▶ demo: browse the spritesheet ⌗ 6502 source

The spritesheet at &b3ec is 128×81 pixels at two bits per pixel — four pixels per byte, in the BBC’s native four-colour byte layout, 32 bytes per row, 2,592 bytes total. Into it are packed 125 sprites, defined by four parallel tables (&be0c, &be89, &bf06, &bf83) holding each sprite’s width, height and sheet position squeezed into four bytes, with the spare low bits used for baseline flip flags. Sprites are any size from a bullet to a 16×32 tile.

The tiles are not a separate system, rather a tile is just a sprite with three bytes of metadata. Table &04ab maps each of the 64 tile types to a sprite; &04eb gives a vertical offset within the 16×32 cell (this is how a quarter-height floor strip sits at the bottom of its cell rather than floating at the top); &052b assigns a palette class. Sharing is aggressive — several tile types point at the same sprite, and the WATER tile has no art of its own at all: it’s the STONE sprite wearing a blue palette.

Two bits per pixel means each sprite pixel is only ever colour 0, 1, 2 or 3. Which raises the obvious question: how does a four-colour sprite format produce a sixteen-colour screen full of red caves, green bushes, yellow columns and white spacesuits?

Colour: a palette byte per sprite

▶ demo: tile colouring across the world ⌗ 6502 source

Every sprite and tile is drawn through a one-byte palette. The top nibble picks the physical colour for logical colour 3; the bottom nibble indexes a sixteen-entry table of colour pairs (&0b79) for logical colours 1 and 2; colour 0 is transparent-over-background. The disassembly annotates these bytes with a compact notation — &81 is “rgk”: colour 1 red, colour 2 green, colour 3 black.

So the format is four colours, but the screen is sixteen, because every sprite brings its own mapping. And the mapping is dynamic. As covered in the previous article, tile palette classes are resolved per-tile at draw time (&23cf): stone recolours every 16 rows and earth every 32 from the strata table at &117d (the geological banding), bushes hash their position into one of four colourways, spaceship parts are red-and-white in the top half of the world and Triax’s green-and-yellow in the bottom, mushrooms are red on floors and blue on ceilings. One set of sprites, many worlds of colour.

▶ demo: stone strata · ▶ the bush hash · ▶ leaf removal

Almost nothing touches the hardware palette at runtime — the flash when you’re hurt, the endgame explosion, and the water. The flooded caverns aren’t drawn at all: a raster interrupt reprograms background colour 0 to cyan for one scanline at the waterline and blue below it, every frame (&12cb). Fly below the waterline in the demo and you’re seeing the same trick: the screen memory holds stone shaped tiles and the blue is a per-scanline trick.

Rendering: the plotter

▶ demo: teleport shimmer ⌗ 6502 source

The sprite plotter (&0ec0&1147) is where the 2bpp-storage / 4bpp-screen bargain gets settled, at speed, and it’s a great piece of 6502 code just full of tricks. Really marvelous.

For each sprite it first expands the palette byte into seven zero-page locations — the Mode-2 bit patterns for each logical colour, in “left pixel” and “right pixel” alignment (convert_palette_to_sixteen_colour_pixel_values, &0ee1). The locations aren’t arbitrary, they’re addresses: &00, &01, &02, &10, &11, &20, &22.

Those numbers are the heart of the whole routine. When the plotter reads a byte of sprite data, it masks out one pixel’s two bits — which, in the interleaved byte layout, produces exactly the values &00, &01, &10 or &11 (or their left-pixel twins &00, &02, &20, &22) — and writes that value into the address field of the next instruction (&1028). The masked pixel is the zero-page address of its own colour data. Colour translation costs one self-modified load; there is no lookup table, no branching, no shifting.

The translated pixels are then pushed onto the 6502 stack — the plotter uses the processor stack as its row buffer, because PHA is the cheapest possible “append byte”. When the row is assembled, a second loop reads the buffer back and writes merged pairs of pixels to the screen. Reading the buffer with DEX produces the row one way; a self-modified INX produces it mirrored — horizontal flip costs nothing. Vertical flip is the same idea one level up: the address step between sprite-sheet rows is a self-modified operand, +32 or −32 (&0f36). This is why Exile can afford to use flipping everywhere: the earth-texture table in the landscape generator is mostly the same two sprites in four orientations, and slopes get their rotation for free.

It’s worth pausing on how many of the tricks in this one routine would horrify a modern code reviewer. It rewrites its own operands (five different instructions), repurposes the CPU stack as a pixel buffer, encodes data in instruction addresses, and encodes depth in a colour bit. Every one of those buys cycles or bytes, and on this machine doing this much both were the whole game.

The demo’s creatures are also drawn by this exact routine — the reconstruction’s plotter is validated byte-identical against the original for every screen byte it touches. The “teleport shimmer” button engages the plot wrapper’s teleport shrink (&0d4d), the effect that squeezes a sprite into scanline slivers as it dematerialises.

From tile byte to pixels: the full pipeline

▶ demo: the pipeline, live ⌗ 6502 source

Pulling the two articles together, here’s the complete journey for one map cell — run for every cell in the strip a scroll exposes, plus every tile a moving object might touch:

  1. get_tile(x, y) computes a byte: two flip bits + a tile type (procedural algorithm + 1KB overlay — see the previous article).
  2. Types &00&08 are resolved through the tertiary object tables: a hand-placed door/switch/nest at this x, or a per-type default feature.
  3. The palette class for the resulting type is resolved against position: strata band, world half, position hash, flip.
  4. The tile’s sprite, cell y-offset and flips are looked up.
  5. The plotter expands 2bpp sheet data through the palette to 4bpp screen bytes — self-modified translation, stack row buffer, priority-bit test — into a circular screen buffer that the video chip is panning.

Rebuilding it, and the revealing mistakes

The interactive map demo from the previous article reimplements steps 1–4 and a canvas approximation of step 5 — and the demo on this page replaces that approximation with the real thing. Almost every visible bug along the way was one of these systems half-understood. In roughly the order I hit them:

Each mistake produced a world that was recognisably Exile and obviously wrong which is its own testament to how much of the game’s look lives in these few kilobytes of technique rather than in the art bytes themselves. I should have captured a screenshot of the chaotic ship as it was a truly bizarre scene.

The renderer demo on this page runs the reconstruction’s display pipeline: sprite plotter, replot wrapper with clipping and the teleport shrink, tile plotter, and the scroller’s strip-wipe/plot machinery — all held byte-identical to the original 6502 code in screen memory across five emulator checks and 85,000 fuzz cases. The next article covers how things move: the physics engine that made Exile famous.

Exile © 1988 Peter Irvin and Jeremy Smith · published by Superior Software
Reconstructed by James Randall from the enhanced version disassembly created by level7
Presented as a tribute to its authors.