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:
- Pixels are 2:1 wide. A 16×32-pixel tile is a square on a real monitor. (This is why a naive rendering of extracted tile art looks vertically stretched.)
- Sixteen colours means four bits per pixel. A full byte holds just two pixels. Storing art this way would be ruinous, that would take 5184 bytes - space Exile just doesn’t have, so it doesn’t.
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
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:
get_tile(x, y)computes a byte: two flip bits + a tile type (procedural algorithm + 1KB overlay — see the previous article).- Types
&00–&08are resolved through the tertiary object tables: a hand-placed door/switch/nest at this x, or a per-type default feature. - The palette class for the resulting type is resolved against position: strata band, world half, position hash, flip.
- The tile’s sprite, cell y-offset and flips are looked up.
- 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:
- Green earth. The extractor’s colour-pair table had colours 1 and 2
swapped relative to the game (the annotation says
&81= red-green-black, not green-red-black). One transposition, and every rock in the world grows moss. - A chaotic ship. Rendering placeholder types
&00–&08literally (doors and switches everywhere) instead of resolving tertiary objects, and drawing every sprite at the top of its cell instead of at its&04eboffset — floor strips floated, hull sections corrupted. - Rocks with holes. This was just a silly mistake but squashing 16×32 tiles to 16×16 by dropping alternate rows destroys exactly half of a dithered texture The fix: keep cells at native 16×32 and let the 2:1 squash happen in the final draw, the way the real hardware’s wide pixels did.
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.