A phased plan for bringing the 1984 BBC Micro Elite (cassette version) to the PC3 in MMBasic, built on the firmware's Draw3D engine, the twelve ship meshes already proven in Bas/3ddemo.bas, and the annotated source at bbcelite.com.
Rendering below: the real Cobra Mk III mesh from 3ddemo.bas, drawn with the same backface rule Draw3D uses.
The original game never did hidden-line removal. Its rule was "draw an edge if either face it belongs to faces the viewer", and that is exactly what Draw3D produces when faces are created without a fill colour. Filled faces cost about the same (the triangle fill runs in C). True hidden-line mode (depthmode=2) is the expensive one: it rasterises a float depth buffer over the ship's screen box every frame, which for a close ship is more RAM than the whole MMBasic heap.
Draw3D CREATE with no fill array, Draw3D WRITE n,x,y,z,0,0 into a framebuffer, one FRAMEBUFFER COPY per frame. Overlapping ships simply overdraw, as on the BBC. Works on every RP2350 variant.
Same meshes with a fill array. Costs roughly one triangle fill per face on top of the edges. Ships must be drawn far-to-near (sort by z in BASIC, trivial) because the engine has no inter-object depth. Offer it as an in-game toggle; it is the Archimedes look, not the BBC look.
Use only where one large object fills the screen and the frame rate does not matter: the title-screen ship and the docking-bay view. Not for flight.
As in the original, a ship further than its blueprint's visibility distance is a single PIXEL. This is the main frame-rate lever: only ships close enough to have shape cost a Draw3D call.
MAX3D raised from 8 to 12 in configuration.h. The only array sized by it is the 13-entry pointer table, so the cost is 16 bytes of BSS. Needs a rebuild and flash before Phase 0 can create more than 8 objects.Draw3D CLOSE ALL off-by-one explained below; fixed in graphics/Draw3D.c (loop now 1..MAX3D), uncommitted alongside the MAX3D change.MM.INFO(PSRAM SIZE) is 0).CHAIN split stays a mechanical fallback if the tokenised size cap is reached.Objects are numbered 1 to MAX3D and live in struct3d[MAX3D + 1]; index 0 is never used. closeall3d() (Draw3D.c line 303) loops for (i = 0; i < MAX3D; i++), so it frees indices 0 to MAX3D - 1 and never visits the last object. HIDE ALL a few lines later loops 1 .. MAX3D correctly. closeall3d() is also what CloseAllFiles() calls when a program ends, so a program that finishes (or errors) with the last object open leaves its pointer dangling after the heap is reinitialised: the next run's Draw3D CREATE of that number fails with "Object already exists" and any SHOW of it reads freed memory. With MAX3D at 12 the victim becomes object 12, which Elite will use. Fixed: the loop is now for (i = 1; i <= MAX3D; i++). LIGHT, SET FLAGS and RESET now raise the same "object does not exist" error as SHOW instead of dereferencing a missing object, and SET FLAGS rejects a face number outside 0..nf-1 (a negative face used to pass the upper-bound test and write before the flags array).
The PicoComputer 3 is an RP2350B machine. Everything below is from the firmware source and the PC3 manual, not from the BBC's constraints.
| Item | PC3 / PicoMite fact | What it means for Elite |
|---|---|---|
| CPU | RP2350B, 252 MHz default, 315 or 378 MHz via OPTION CPUSPEED (tied to the HDMI pixel clock) | Run the game at 378 MHz. Both the interpreter and the SPI/HSTX paths scale. |
| RAM | MMBasic heap 152 KB (HDMIUSB), 144 KB (HDMIWEB), 180 KB (HDMIBTH). 8 MB PSRAM if OPTION PSRAM PIN is set; GetMemory spills to PSRAM for requests over half the heap. | Ship objects (about 5 KB each) and all game arrays fit the heap comfortably. PSRAM is the safety valve for tables and the z-buffer. |
| Framebuffer pool | 153600 B on HDMIUSB; 96000 B on HDMIWEB/HDMIBTH (cut-down build) | MODE 2 + F = 76800 B fits everywhere. MODE 5 + F = 153600 B fits HDMIUSB only. |
| Display | HDMI 640x480 timing; MODE 2 = 320x240 x 16 colours (palette via MAP), MODE 5 = 320x240 x RGB332, both pixel-doubled by hardware. FRAMEBUFFER COPY F,N,B copies on core 1. | Same 320x240 canvas Chuckie Egg used. Elite's 256x192 space view plus 56-row dashboard maps to 320x184 + 56 with the same proportions. |
| Keyboard | USB host. KEYDOWN(0) = keys held, KEYDOWN(1..6) = their codes; INKEY$ for typed characters. Arrow codes 128..131, F1..F12 = 145..156. | Held-key flight controls work (Chuckie's ReadKeys is the template). Function-key screens as on the BBC. |
| Gamepad | DEVICE(GAMEPAD n, LX|LY|RX|RY|B|H|T) for USB and BLE pads | Optional analogue pitch/roll; map later, not in the core. |
| Audio | I2S DAC (PCM5102). PLAY SOUND 4 channels (S/Q/T/N/O waveforms), PLAY MODFILE, PLAY MODSAMPLE, PLAY SAMPLE | BBC-style beeps from PLAY SOUND (the sfxdata.bas idiom); verify the sound generators run on the I2S path early in Phase 0. |
| Storage | A: flash drive and SD card, VAR SAVE (16 KB flash area), RUN "file",args with MM.CMDLINE$, CHAIN (clears variables), LIBRARY | Commander saves as a file. Data files (meshes, tokens, tables) on A: or SD. |
| Program size | MAX_PROG_SIZE = HEAP_MEMORY_SIZE (tokenised). Chuckie Egg is 53 KB of source, 1664 lines. | Elite will be 3 to 4 times Chuckie. Keep every table out of the program text from day one. |
| Maths in C | MATH Q_CREATE / Q_MULT / Q_INVERT / Q_ROTATE / Q_VECTOR / Q_EULER, V_CROSS / V_NORMALISE / V_ROTATE, M_MULT, DOTPRODUCT, SCALE, INTERPOLATE | Orientation and universe rotation stay in C. The interpreter only sequences them. |
The keyboard, sound and display facts above are for the PC3 (USB host, HDMI). They do not hold for the PicoCalc, whose I2C keyboard cannot report held keys.
Draw3D actually doesFrom graphics/Draw3D.c (1349 lines, compiled -Os, runs from flash). The published 3D manual is out of date in several places; the list below is what the parser and renderer do today, and every item matters for the port.
MAX3D, MAXCAM in configuration.h). The camera is bound at CREATE.sx = W/2 - panx + (x - camx) * viewplane / z, so viewplane is the focal length in pixels. Elite's own projection is a 256-pixel focal length on a 256-wide screen; Draw3D CAMERA 1, 320 reproduces that field of view on 320 pixels.Draw3D WRITE n, x, y, z.ROTATE is absolute and takes a 5-element float array [w, x, y, z, m]; m scales by m², so keep it 1.0. Feed each ship's orientation quaternion every frame. RESET bakes the current pose rather than restoring it; never call it.x, y, z on SHOW/WRITE are world offsets applied to every vertex. x, y are clamped to ±32766, z is any integer. A ship inside the field of view always satisfies |x| < 0.7 z, so the clamp only bites beyond z ≈ 46000, which is dot range anyway.N = (v1→v2) × (v1→v0), drawn when dot(v0 - cam, N) < 0. Winding therefore decides visibility; the converter must fix winding against Elite's stored face normals. SET FLAGS n, 4, face, count inverts individual faces as a repair tool.depthmode=1). Objects never depth-test against each other. Use depthmode=1 for solid ships: coplanar detail polygons (the Cobra's engine recesses) sort wrongly by centroid.DrawTriangle, quads as two triangles plus the four outer edges, 5+ vertices through the scanline polygon filler. Faces must have at most 20 vertices; larger ones overrun a stack array (the Cobra's 7-vertex rear and the Thargoid's 8-vertex ring are fine).depthmode=2, RP2350 only) needs a framebuffer write target, allocates a float per pixel of the object's on-screen bounding box (a 200x150 ship = 120 KB), rasterises every visible face into it, then depth-tests each edge pixel by pixel.WRITE for z below a small positive threshold; Elite treats those distances as collisions anyway.SHOW erases its previous box in colour 0; WRITE leaves it. With a cleared framebuffer every frame, use WRITE. After WRITE, DRAW3D(XMIN n) and HIDE stop working; the game computes screen positions itself.Draw3D LIGHT. Leave it off.CREATE makes 16 page-rounded allocations, so an object costs at least 4 KB whatever its size; a Cobra (28 vertices, 17 faces) is about 5 KB. Twelve objects is under 64 KB.sqrtf per face) and bubble-sorts the faces. For Elite meshes (4 to 17 faces) that is negligible against interpreter time. Phase 0 measures it.CREATE needs nine comma slots even when the fill array is omitted: Draw3D CREATE n,nv,nf,cam,v(),fc(),f(),col(),ec(),. The vertex array is FLOAT v(2, nv-1) indexed (coord, vertex).| Mode | Per-ship cost | Memory | Look | Fidelity to BBC Elite | Verdict |
|---|---|---|---|---|---|
Wireframe, backface culled (depthmode 0, no fill) | Normals + sort + one DrawLine per perimeter edge of visible faces | Object only | White lines on black, ships read as solid because rear faces are culled | Exact: same edge rule as LL9 | Default |
| Solid (fill index per face) | Above plus one triangle fill per face (two per quad) | Object only | Flat-shaded ships, one colour per face group | Archimedes/Amiga look; needs far-to-near draw order across ships | Toggle |
Hidden-line (depthmode 2) | Above plus per-pixel depth raster of every visible face and depth-tested edges | 4 bytes per bbox pixel, cached, up to 300 KB for a full-screen object | Clean wireframe with correct occlusion, including concave parts | Better than the original | Title and hangar only |
Dot (PIXEL) | One projection in BASIC | None | Single pixel | Exact: SHPPT | Beyond visibility distance |
Elite's ships are mostly convex, which is why the original looked right without occlusion. The exceptions (the Cobra's engine recesses, the Coriolis docking slot, the Python's rear) are handled in the original by winding and normals, and the converter carries those normals across, so mode 0 gives the same picture the BBC gave.
Per-face visibility distances in the blueprints (faces that are "always drawn when far") and per-vertex level-of-detail have no equivalent in Draw3D. Neither is visible at 320x240: the dot threshold covers the far case and the near case is drawn fully.
Plain DO loop, no SETTICK. Each iteration: read keys (INKEY$ then KEYDOWN), advance the simulation by the measured frame time, draw into F, FRAMEBUFFER COPY F,N,B. TIMER gives the delta. The BBC ran 5 to 12 frames a second and moved everything per frame; we scale the per-frame deltas by (our frame time / BBC frame time) so speed and turn rates feel the same at 25 fps as at 8.
Twelve slots as parallel arrays: type, position (3 floats, Elite units), orientation quaternion (5 floats), speed, accel, pitch and roll counters, energy, AI flags, missiles, target, explosion timer, and the Draw3D object number (0 when the ship is a dot or absent). Slot 0 is the planet, slot 1 the sun or station, as in FRIN.
Player pitch and roll rotate the universe: build r with MATH Q_CREATE, then for each slot MATH Q_ROTATE the position and MATH Q_MULT the ship's quaternion. A ship's own pitch and roll are a Q_MULT in its own frame; its nose vector is Q_ROTATE(q, [0,0,1]). Renormalise the quaternion every 16 frames, the same cadence as Elite's TIDY.
Python tools in Bas/elite_tools/ (the chuckie_tools pattern): parse the beebasm source into mesh, token, market and ship-stat files; generate reference test vectors for the galaxy; assemble the final .bas. Nothing hand-copied from the assembler listing.
| Consumer | KB | Notes |
|---|---|---|
| Framebuffer pool: MODE 2 display + F | 75 | Separate pool, not heap. Add L (37.5 KB) only on HDMIUSB. |
12 Draw3D objects | ~60 | Heap. Created on spawn, closed on kill. Coriolis is the largest (16 v, 15 f). |
| Universe arrays (12 slots) | ~4 | Heap. |
| Mesh source arrays (12 blueprints) | ~12 | Loaded once from file; copied into each CREATE. |
| Text tokens, names, market tables | ~8 | Pre-expanded strings; spill to PSRAM if present. |
| Stardust, scanner, temp strings, locals | ~6 | |
| Heap total | ~90 | of 152 KB (HDMIUSB) or 144 KB (HDMIWEB). Hidden-line z-buffer is extra and PSRAM-only. |
Space view 320x184 (Elite's 256x192 at 1.25x width, trimmed 8 rows), dashboard 320x56 below it. The dashboard is line art on the BBC and stays line art here: drawn once into a background copy and blitted, with the moving indicators redrawn each frame. The 3D scanner ellipse, its ship sticks and the compass live in the dashboard. Text uses an 8x8 DEFINEFONT of the BBC MOS character set so the docked screens keep the original 40-column feel at 320 pixels.
Bas/elite/
elite.bas the game (single program to start)
data/ships.dat 12 meshes: header stats, vertices, faces (winding fixed)
data/tokens.dat expanded text tokens, name digraphs, system descriptions
data/market.dat 17 commodities: base price, econ factor, base qty, mask, unit
data/dash.bmp dashboard bezel (optional; line art otherwise)
elite.cmd commander save (A: or SD)
Bas/elite_tools/
blueprints.py beebasm SHIP_* blocks -> data/ships.bas + ships.json (+ 3ddemo cross-check)
meshcheck.py scores meshview.bas auto output: engine verdicts vs stored normals
pc3.py COM3 console driver: probe, cmd, run (AUTOSAVE), grab (XMODEM screen capture)
tokens.py TKN1/QQ16 tables -> tokens.dat
galaxy_ref.py reference seeds/names/system data -> tests/galaxy_ref.txt
build.py assemble elite.bas from parts + DATA
Bas/elite/tests/
bench.bas Phase 0 frame-time harness
meshview.bas spin any ship in the three render modes; AUTOMODE runs the winding check
sheet.bas contact sheets of all ships for screen capture (captures/*.png)
galaxy_test.bas compares 256 systems x 8 galaxies against galaxy_ref.txt
| Elite (cassette source) | MMBasic on the PC3 | Notes |
|---|---|---|
| Ship blueprints (XX21): vertices, edges with two face ids, face normals, visibility distances | ships.dat with face polygons; Draw3D CREATE per live ship | Faces are rebuilt from edge adjacency; winding set so the computed normal agrees with the stored one. 3ddemo.bas already has all 12 and is the cross-check. |
| INWK / K% ship data block, FRIN slots, NOSH = 12 | Parallel arrays, 12 slots | Shuffle-down on KILLSHP as in the original; close the Draw3D object first. |
| Orientation vectors (sidev, roofv, nosev), TIDY | Unit quaternion + MATH Q_*; normalise every 16 frames | Quaternion is smaller and the engine wants one anyway. |
| MVEIT: apply player pitch/roll to all ships, move along nose, apply ship's own pitch/roll, damp | One SUB per slot per frame using Q_ROTATE/Q_MULT | Counters and damping tables copied from the source. |
| LL9 ship drawing, SHPPT dot | Draw3D WRITE when z ≤ visibility distance, else PIXEL | Engine culls and projects. Skip when z below near threshold or when the projected centre is far off screen. |
| Explosion cloud (DOEXP) | BASIC particle burst from the ship's projected centre, sized by distance and age | Engine does not expose projected vertices; the original's per-vertex clusters are approximated. |
| Planet (PLANET, PL9): circle plus crater (cassette); meridians are the disc version | CIRCLE outline; crater as one ellipse of LINE segments from the rotated axis vectors | 16 to 64 segments depending on radius. Meridians can be added later from the disc source. |
| Sun (SUN): filled disc with fuzzy edge | Per-scanline LINE with random edge jitter, as the original | Radius clamped; cache the last drawing to skip when unchanged. |
| Stardust (STARS front/side) | 18 particles, PIXEL/short LINE | Front view: radial with speed; side views: horizontal drift; rear: inward. |
| Space views and axis flipping | Flip x or z of every position and quaternion before drawing; camera unchanged | Four views, F1 to F4 as on the BBC. |
| Dashboard (DIALS), 3D scanner, compass | Line art into the dashboard strip each frame | Scanner: ellipse, stick per ship, dot colour by type. |
| Key logger (DOKEY) | KEYDOWN(1..6) latched into flags, INKEY$ for one-shot keys | Chuckie's ReadKeys pattern; BBC letters kept, views and screens on F1 to F10 (key table in B4). |
| Galaxy and system seeds, twisting, names, system data, market | Pure integer BASIC, bit-exact | Verified against the Python reference over all 2048 systems. |
| Text tokens (TT27, QQ18) | Pre-expanded strings in tokens.dat; only names and system descriptions generated at runtime | Removes the recursive tokeniser entirely. |
| Tactics (TACTICS), aggression, missiles, ECM, fleeing, docking traffic | One SUB per ship, run every 8th frame per slot as in the original's scheduling | Behaviour tables from the source. |
| Docking checks, station safe zone, docking computer | Angle and speed checks in the station's frame; docking computer = timed auto-dock | Cassette version has no docking animation. |
| Sound (NOISE, BEEP, EXNO) | PLAY SOUND ch, B, Q|N|T, f, v envelopes on a tick | Laser, missile launch, ECM, explosion, hyperspace, beep, boop. |
| Commander save (SVE/LOD) | Text file on A: or SD | No competition code. |
Each phase ends with something that runs on the PC3 and a measurable acceptance test. Sizes are relative to Chuckie Egg (about 1700 lines including data).
bench.bas: MODE 2 + F at 378 MHz; create 1, 4, 8 Cobras; measure ms per frame for wireframe, solid and hidden-line, plus the cost of CREATE/CLOSE, FRAMEBUFFER COPY, and a synthetic MVEIT-sized BASIC loop per ship.KEYDOWN reports up to six held keys; pick the flight keys so that pitch, roll, speed and fire do not ghost on a typical keyboard matrix (test with keytest.bas). PLAY SOUND on I2S is established.OPTION PSRAM PIN and MM.INFO(PSRAM SIZE).The harness, using the Cobra mesh from 3ddemo.bas (its DATA block is elided here):
' bench.bas - Phase 0 frame-time harness. Run at OPTION CPUSPEED 378000.
OPTION EXPLICIT
MODE 2 : FRAMEBUFFER CREATE : FRAMEBUFFER WRITE F
Draw3D CAMERA 1, 320 ' Elite's field of view on 320 px
DIM FLOAT v(2, 27), q(4) ' v(coord, vertex); q = w,x,y,z,m
DIM INTEGER fc(16), f(59), col(1), ec(16), fl(16)
DIM INTEGER n, ships, i, mode
DIM FLOAT t0
' ... READ the 28 vertices into v(0..2, i), the 17 face counts into fc(),
' ... the 60 face-vertex indices into f(); col(0)=RGB(WHITE): col(1)=RGB(GRAY)
' ... ec() all 0 (edge colour index), fl() all 1 (fill colour index)
q(4) = 1 ' m must be 1: the engine scales by m^2
FOR mode = 0 TO 2 ' 0 wireframe, 1 solid, 2 hidden-line
FOR ships = 1 TO 8
FOR n = 1 TO ships
IF mode = 1 THEN
Draw3D CREATE n, 28, 17, 1, v(), fc(), f(), col(), ec(), fl()
ELSE
Draw3D CREATE n, 28, 17, 1, v(), fc(), f(), col(), ec() ' 9 args, no fill = wireframe
ENDIF
NEXT n
t0 = TIMER
FOR i = 1 TO 100
CLS
FOR n = 1 TO ships
MATH Q_EULER RAD(i + n * 40), RAD(i * 2), 0, q()
Draw3D ROTATE q(), n
Draw3D WRITE n, (n - 4) * 60, 0, 900, 0, (mode = 2) * -2
NEXT n
FRAMEBUFFER COPY F, N, B
NEXT i
PRINT "mode"; mode; ships; " ships:"; (TIMER - t0) / 100; " ms/frame"
FOR n = 1 TO ships : Draw3D CLOSE n : NEXT n
NEXT ships
NEXT mode
Add a second run with a synthetic per-ship update (three MATH Q_ROTATE, one Q_MULT, twenty scalar statements) to stand in for MVEIT, and a third with the ships at z = 2500 to see how much of the cost is fill and line length rather than per-face overhead.
Accept when the numbers exist and the firmware changes (if any) are agreed.
Full listing in Bas/elite/tests/bench_results.txt; programs bench.bas, bench2.bas, keytest.bas alongside. Per-ship figures exclude the clear and the copy.
| Per Cobra, ms | z = 300 | z = 500 | z = 900 | z = 2500 |
|---|---|---|---|---|
| Wireframe (mode 0) | 1.02 | 0.73 | 0.50 | 0.35 |
| Solid (mode 1) | 2.03 | 1.51 | 1.14 | 0.84 |
| Hidden-line (mode 2) | 21.15 | 6.95 | 1.25 | 0.48 |
| Fixed cost | ms | Note |
|---|---|---|
| CLS of the framebuffer | 0.10 | |
FRAMEBUFFER COPY F,N | 0.08 | 38400 bytes |
FRAMEBUFFER COPY F,N,B | 16.55 | Paces to the 60 Hz frame: this is the game's vsync |
Draw3D CREATE + CLOSE | 0.19 | Spawn and kill per frame cost nothing |
| Synthetic MVEIT update, 8 ships | 2.07 | 3 Q_ROTATE, 1 Q_MULT, ~20 scalar statements each |
Budget decided: one 60 Hz frame (16.5 ms) per iteration, paced by the background copy. Eight wireframe ships at z = 900 plus the MVEIT-sized update come to about 6 ms, leaving 10 ms for the dashboard, scanner, stardust, planet and tactics. Solid ships fit 60 fps with few ships close and 30 fps otherwise. Hidden-line at z = 300 took 21 ms per ship and a 164 KB z-buffer that only existed because PSRAM is fitted; it stays title-screen only.
Closed by Peter: KEYDOWN works as documented, up to six simultaneous keys like any USB keyboard, subject to the keyboard's own matrix (which keys ghost depends on their positions), and PLAY SOUND on the I2S path is proven. Neither is a risk. The one remaining item is flashing the MAX3D = 12 build, which the PC3 does not yet run.
blueprints.py: parse elite-source.asm SHIP_* blocks; build face polygons from edge adjacency; orient winding against the stored normals; emit header stats (speed, energy, laser, missiles, bounty, canisters, visibility distance, targetable area, explosion count) and the mesh.3ddemo.bas (same vertex count, same face count, same edge set).meshview.bas: cycle ships, toggle the three modes, show the face count and frame time. This is where a wrong winding shows up as a missing or ghost face.Accept when all 12 ships display correctly in wireframe and solid at three distances, with no face flagged by hand.
elite_tools/blueprints.py reads the cassette elite-source.asm and writes Bas/elite/data/ships.bas. Every blueprint edge is drawn: boundary loops chained from the edge list, single-face detail edges as extra polygons, and bare lines (the Python's four fin lines, the Thargoid's two vents, the Cobra's nose spike, the Missile's eight fin lines) as sliver triangles with one extra vertex 0.5 units off the line. Nine ships came out identical to 3ddemo.bas; the demo's Python, Thargoid, Cobra and Missile were each missing lines and the Thargon was absent.
meshview.bas in auto mode asked the engine's DIAGNOSE for every polygon at three orientations and meshcheck.py compared each verdict with the stored normal: 444 verdicts, 0 mismatches on real faces, 3 deviations on sliver lines (inherent: a sliver's normal must be perpendicular to its line). Draw times per ship match Phase 0 (0.6 to 1.7 ms wireframe, 0.8 to 3.8 ms solid; the Coriolis is the dearest). Full table in tests/meshview_results.txt.
Solid mode must use depthmode 1 (vertex-based sort). With centroid sorting the coplanar detail quads on the Cobra's rear face can be painted over by the face itself; the demo already did this.
Captured from the PC3 (SAVE IMAGE, then XMODEM S into pc3.py grab):
Accept when flying around the station with 6 ships in the bubble holds the frame budget and the scanner agrees with the view.
Accept when Lave, Zaonce, Diso and Riedquat come out with the right names, economies and prices, and a 10-system tour spawns plausible traffic.
Accept when a Cobra and two Sidewinders fight back convincingly, the player can die, and the kill tally advances the rank.
Accept when manual docking succeeds and fails for the same reasons as on the BBC.
Accept when a trade loop Lave to Zaonce and back turns the expected profit and the save survives a power cycle.
PLAY SOUND channels; the original had no music.Accept when a fresh commander can reach Competent without hitting a bug and the manual page is written.
The cassette version has no missions (the Constrictor and Thargoid plans are disc-version features), no Trumbles, and no docking animation. They are out of scope here and could be borrowed from the disc source later.
| Risk | Likelihood | Mitigation |
|---|---|---|
| Interpreter time per ship blows the frame budget with 8+ ships | Medium | Phase 0 measures before design locks. Levers: run tactics on alternate frames per slot (the original already does), lower the dot threshold, fewer ships in the bubble, keep all vector maths in MATH. |
Tokenised program exceeds MAX_PROG_SIZE | Medium | All tables external from the start; LIBRARY for shared utilities; docked/flight CHAIN split with VAR SAVE state as the disc version did. |
Firmware with MAX3D = 12 not yet flashed on the target | Low | Phase 0 runs on the rebuilt HDMIUSB image; until then the bench caps at 8 objects. |
| Wrong winding on a concave face after conversion | Low | Converter checks each face's computed normal against the stored one; SET FLAGS 4 per face as the manual override. |
| Near-plane wrap when a ship is very close | Low | Skip WRITE below a z threshold; collision fires first in the original at those ranges. |
| Per-object minimum of 4 KB | Low | Twelve objects stay under 64 KB; the CLOSE ALL leak is fixed. |
| Hidden-line z-buffer exhausts the heap | Low | Only used on the title screen; falls back to wireframe if MM.INFO(PSRAM SIZE) is 0. |
| Copyright | Note | Elite is copyright Ian Bell and David Braben; the bbcelite.com repositories carry no licence. This is a personal port using the published data, like the existing Chuckie Egg port. Keep that in mind before distributing. |
The meshes in Bas/3ddemo.bas (already rendering on the engine) and the blueprint statistics from the cassette source.
| Ship | Vertices | Faces | Face-vertex entries | Demo z | Largest face |
|---|---|---|---|---|---|
| Viper | 15 | 9 | 34 | 600 | 6 (rear) |
| Thargoid | 20 | 9 | 40 | 1600 | 8 (ring) |
| Escape pod | 4 | 4 | 12 | 400 | 3 |
| Asp Mk II | 19 | 13 | 50 | 800 | 5 |
| Asteroid | 9 | 14 | 42 | 1000 | 3 |
| Canister | 10 | 7 | 30 | 300 | 5 |
| Cobra Mk III | 28 | 17 | 60 | 1000 | 7 (rear) |
| Mamba | 25 | 9 | 30 | 600 | 4 |
| Missile | 17 | 9 | 32 | 600 | 4 |
| Python | 11 | 9 | 28 | 1500 | 4 |
| Sidewinder | 10 | 8 | 26 | 600 | 4 |
| Coriolis station | 16 | 15 | 52 | 2500 | 4; docking slot is face 2 (vertices 12..15) |
The demo scales blueprint coordinates by 2 (MATH SCALE vertices(),2.0) and uses the demo z column above to frame each ship at viewplane 600. Two things to know before reusing these meshes:
Draw3D only strokes face perimeters, so the demo's converter turned each such detail group into an extra polygon that shares its host face's normal. That is the right technique and the converter in Phase 1 must do the same.Header fields decoded from each SHIP_* block. Bounty is the 16-bit word divided by 10. Laser and missiles come from byte 19 (%00lllmmm). Types 5 and 7 share the Cobra Mk III blueprint.
| Type | Ship | Vert | Edges | Faces | Canisters | Area (side) | Bounty Cr | Vis dist | Energy | Speed | Normal scale | Laser | Missiles | Max |coord| |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | Sidewinder | 10 | 15 | 7 | 0 | 65 | 5.0 | 20 | 70 | 37 | 2 | 2 | 0 | 64 |
| 2 | Viper (police) | 15 | 20 | 7 | 0 | 75 | 0 | 23 | 120 | 32 | 1 | 2 | 1 | 72 |
| 3 | Mamba | 25 | 28 | 5 | 1 | 70 | 15.0 | 25 | 90 | 30 | 2 | 2 | 2 | 64 |
| 4 | Python | 11 | 26 | 13 | 3 | 120 | 20.0 | 40 | 250 | 20 | 0 | 3 | 3 | 224 |
| 5 | Cobra Mk III (hunter) | 28 | 38 | 13 | 3 | 95 | 0 | 50 | 150 | 28 | 1 | 2 | 3 | 128 |
| 6 | Thargoid | 20 | 26 | 10 | 0 | 99 | 50.0 | 55 | 240 | 39 | 2 | 2 | 6 | 164 |
| 7 | Cobra Mk III (trader) | 28 | 38 | 13 | 3 | 95 | 0 | 50 | 150 | 28 | 1 | 2 | 3 | 128 |
| 8 | Coriolis station | 16 | 28 | 14 | 0 | 160 | 0 | 120 | 240 | 0 | 0 | 0 | 6 | 160 |
| 9 | Missile | 17 | 24 | 9 | 0 | 40 | 0 | 14 | 2 | 44 | 2 | 0 | 0 | 68 |
| 10 | Asteroid | 9 | 21 | 14 | 0 | 80 | 0.5 | 50 | 60 | 30 | 1 | 0 | 0 | 80 |
| 11 | Cargo canister | 10 | 15 | 7 | 0 | 20 | 0 | 12 | 17 | 15 | 2 | 0 | 0 | 24 |
| 12 | Thargon | 10 | 15 | 7 | 0 | 40 | 5.0 | 20 | 20 | 30 | 2 | 2 | 0 | 40 |
| 13 | Escape pod | 4 | 6 | 4 | 0 | 16 | 0 | 8 | 17 | 8 | 3 | 0 | 0 | 36 |
Other header bytes the game needs: the gun vertex (Cobra 21, Thargoid 15, all others 0), the explosion count (4n+6 with n from 1 for the missile to 12 for the station), and the edge heap size. Source constants: NOSH=12, NTY=13, COPS=2, THG=6, CYL=7, SST=8, MSL=9, AST=10, OIL=11, TGL=12, ESC=13, POW=15. The station's docking slot is the 20 x 60 rectangle of vertices 12 to 15 on the +z face.
Condensed from bbcelite.com so the implementer does not need to re-read the 6502 listing for the parts that must be bit-exact or that define the feel of the game.
Everything here was read from the cassette source pages, not the prose overviews, unless marked "verify". Formulas are written in MMBasic terms where that removes 8-bit trickery; the ones that must stay bit-exact (seeds, twist, names, market, RNG, checksum) are written as the 6502 does them.
A galaxy is three 16-bit seeds s0, s1, s2 stored little-endian (bytes s0_lo s0_hi s1_lo s1_hi s2_lo s2_hi). Galaxy 1: s0=&5A4A, s1=&0248, s2=&B753. System 0 of galaxy 1 is Tibedied; each further system is four twists on. Lave sits at chart position (20, 173).
' twist (TT54), 16-bit arithmetic, carry out of the high byte discarded
tmp = (s0 + s1) AND &HFFFF : s0 = s1 : s1 = s2 : s2 = (tmp + s1) AND &HFFFF
' system data (TT24) from the current seeds
gal_x = s1_hi
gal_y = s0_hi \ 2
government = (s1_lo \ 8) AND 7
economy = s0_hi AND 7 : IF government <= 1 THEN economy = economy OR 2
tech = (economy XOR 7) + (s1_hi AND 3) + (government + 1) \ 2 ' displayed as tech + 1
population = tech * 4 + economy + government + 1 ' tenths of a billion
productivity = ((economy XOR 7) + 3) * (government + 4) * population * 8 ' M Cr
radius_km = ((s2_hi AND 15) + 11) * 256 + s1_hi
' name (cpl): 3 or 4 digraphs, on a COPY of the seeds
n = 3 : IF s0_lo AND 64 THEN n = 4
FOR k = 1 TO n : i = s2_hi AND 31 : IF i <> 0 THEN name$ = name$ + digraph$(i) : twist : NEXT
Digraph tokens 128 to 159, index 0 prints nothing and index 15 ("A?") prints just "A":
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| AL | LE | XE | GE | ZA | CE | BI | SO | US | ES | AR | MA | IN | DI | RE | A? |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 0 |
| ER | AT | EN | BE | RA | LA | VE | TI | ED | OR | QU | AN | TE | IS | RI | ON |
Governments 0 to 7: Anarchy, Feudal, Multi-government, Dictatorship, Communist, Confederacy, Democracy, Corporate State. Economy 0 to 7: bit 2 clear is Industrial, set is Agricultural; 0 and 5 Rich, 1 and 6 Average, 2 and 7 Poor, 3 and 4 Mainly. Check value: Tibedied is Feudal, Poor Industrial, tech level 9 (stored 8), population 3.6 billion, productivity 11520 M Cr, radius 4610 km. Galactic hyperspace rotates each of the six seed bytes left by one bit, independently, and clears the legal record.
One random byte QQ26 is drawn on arrival in a system and saved with the commander; prices do not change until the next arrival. Economy is 0 to 7 as above.
price (0.1 Cr) = ((base + (QQ26 AND mask) + economy * factor) AND 255) * 4 ' 8-bit wrap is intentional
quantity = base_qty + (QQ26 AND mask) - economy * factor
IF quantity < 0 THEN quantity = 0 ELSE quantity = quantity AND 63
| # | Item | Base | Factor | Unit | Base qty | Mask |
|---|---|---|---|---|---|---|
| 0 | Food | 19 | -2 | t | 6 | 00000001 |
| 1 | Textiles | 20 | -1 | t | 10 | 00000011 |
| 2 | Radioactives | 65 | -3 | t | 2 | 00000111 |
| 3 | Slaves | 40 | -5 | t | 226 | 00011111 |
| 4 | Liquor/Wines | 83 | -5 | t | 251 | 00001111 |
| 5 | Luxuries | 196 | +8 | t | 54 | 00000011 |
| 6 | Narcotics | 235 | +29 | t | 8 | 01111000 |
| 7 | Computers | 154 | +14 | t | 56 | 00000011 |
| 8 | Machinery | 117 | +6 | t | 40 | 00000111 |
| 9 | Alloys | 78 | +1 | t | 17 | 00011111 |
| 10 | Firearms | 124 | +13 | t | 29 | 00000111 |
| 11 | Furs | 176 | -9 | t | 220 | 00111111 |
| 12 | Minerals | 32 | -1 | t | 53 | 00000011 |
| 13 | Gold | 97 | -1 | kg | 66 | 00000111 |
| 14 | Platinum | 171 | -2 | kg | 55 | 00011111 |
| 15 | Gem-Stones | 45 | -1 | g | 250 | 00001111 |
| 16 | Alien items | 53 | +15 | t | 192 | 00000111 |
Check: at Lave (economy 5) food is 3.6 to 4.0 Cr and computers 89.6 to 90.8 Cr with none available.
Each slot is 36 bytes (INWK). Positions are 24-bit sign-magnitude, ±8388607, relative to the player, z into the screen. Orientation is three vectors (nosev forward, roofv up, sidev right) of 16-bit sign-magnitude components with unit length 96 (&6000); nearly every calculation uses only the high bytes (±96). A fresh ship has sidev (96,0,0), roofv (0,96,0), nosev (0,0,-96), nose toward the viewer.
| Bytes | Field | Notes |
|---|---|---|
| 0-8 | x, y, z (lo, hi, sign each) | |
| 9-26 | nosev, roofv, sidev (x_lo, x_hi, y_lo, y_hi, z_lo, z_hi each) | |
| 27 | speed 1-40 | NPC moves 1.5 x speed units per frame; player DELTA 0-40 units per frame |
| 28 | acceleration, signed | applied then zeroed each frame |
| 29, 30 | roll counter, pitch counter | bits 0-6 magnitude, bit 7 direction; 127 = rotate forever (station, asteroids); otherwise decremented by 1 per frame |
| 31 | flags | bits 0-2 missiles, 3 on screen, 4 on scanner, 5 exploding, 6 drawn/firing, 7 killed |
| 32 | AI byte | bit 7 AI on, bits 1-6 aggression 0-63, bit 0 ECM fitted |
| 33-34 | line heap pointer | not needed: the engine redraws |
| 35 | energy | blueprint byte 14 is the maximum |
Scale: projection is 256 * value / z on a 256 x 192 view centred (128, 96), so 320 pixels wide means a 320-pixel focal length. The planet has radius 24576 (96 0) and is placed 65536 ahead on arrival, filling the view exactly; the sun is the same size, 1 to 7 planet-distances further. Ships are removed beyond 57344 on any axis (FAROF). The in-system jump moves 65536. Vertex coordinates in the blueprints are in the same units as positions (a Cobra is 256 units across), so the meshes go into Draw3D unscaled and WRITE takes the raw position. The blueprint visibility byte is compared with the distance reduced to 0..31 (LL9 part 2, which looks like the largest high byte divided by 8, so units of 2048: verify); Cobra 50 is never reached inside the bubble, Sidewinder 20 becomes a dot beyond about 40000, canister 12 beyond about 24000.
Roll JSTX and pitch JSTY are 1..255 centred on 128. A held key moves roll by 7 and pitch by 14 per frame (saturating; pushing the other way from beyond centre first snaps to 128). With damping on, the value moves 1 toward 128 per call, called twice per frame for roll and once for pitch. Then:
d = JSTX - 128 : ALP1 = ABS(d) \ 4 : IF ALP1 < 8 THEN ALP1 = ALP1 \ 2 ' 0..31, sign in ALP2
e = ABS(JSTY - 128) + 4 : BET1 = e \ 16 : IF BET1 < 3 THEN BET1 = BET1 \ 2 ' 0..8, sign in BET2
alpha = ALP1 / 256 : beta = BET1 / 256 ' radians per frame
Full roll builds in about 18 frames and decays at 2 per frame; full pitch builds in 9 and decays at 1. Speed DELTA is 0..40. These per-frame constants are what the frame-time scaling in the architecture section must preserve.
| BBC key | Action | BBC key | Action |
|---|---|---|---|
| S / X | pitch down / pull up | A | fire laser |
| < / > | roll left / right | T, U, M | target, unarm, fire missile |
| Space / ? | speed up / slow down | E | ECM |
| J | in-system jump | C | docking computer |
| H | hyperspace | TAB, ESC | energy bomb, escape pod |
| f0-f3 | front, rear, left, right view | f4-f9 | long chart, short chart, data, market, status, inventory |
On the PC3 keep the BBC letters (they are all plain ASCII in KEYDOWN) and put the views and screens on F1 to F10. Arrow keys can double S/X/</>.
(MCNT XOR slot) AND 15 = 0: TIDY the orientation (renormalise nosev, make roofv perpendicular, sidev = cross product). Each ship every 16 frames.(MCNT XOR slot) AND 7 = 0.pos += nosev_hi * speed / 64 per axis (unit 96, so 1.5 x speed units).speed += acceleration, clamp 1..max, zero the acceleration.z -= DELTA.X = X*(1 - 1/512) ± Y/16 : Y = Y*(1 - 1/512) ∓ X/16, about 3.6° per frame; decrement the counter unless it is 127.Universe rotation by the player (MVEIT part 5), high bytes only:
k = y - alpha * x
z = z + beta * k
y = k - beta * z ' uses the new z
x = x + alpha * y ' uses the new y
With quaternions: build r from the roll angle about Z and the pitch angle about X, invert it, and apply Q_ROTATE to positions and Q_MULT to orientations. The shear the original picks up from its Minsky rotation is what TIDY corrects; a normalised quaternion has none, so the feel is the same without the drift.
View flipping (PLUT), applied to positions and all three vectors before drawing: rear negates x and z; left swaps x and z then negates the new z; right swaps x and z then negates the new x.
energy += 1.(rnd AND 31) < m. A Thargoid launches a Thargon instead. "INCOMING MISSILE".laser_power \ 2 damage (blueprint byte 19 shifted right once) and the attacker's acceleration is decremented.(rnd OR 128) < AI_byte. Pitch counter = 3 with sign from roofv · XX15; roll counter = 5 (only if current roll < 16) with sign from sidev · XX15 XOR the pitch sign. CNT ≥ 22 means accelerate by 3; |CNT| ≥ 18 means decelerate by 1 (missiles 2).AI byte at spawn: traders rnd \ 2 with AI off (they never fight in the cassette version); pirates and bounty hunters rnd OR 192 (AI on, aggression 32-63) with ECM if a second random byte is 245 or more; Thargoids, Thargons, missiles and escape pods 63 with AI on; cops 32-63; station %00000001; asteroids and canisters 0.
| Condition | Task |
|---|---|
| every iteration | keys, essential dials, laser temperature -1, message timer |
| every 4 | the non-essential bars |
MCNT AND 7 = 0 | if energy ≥ 128 recharge fore and aft shields by 1 each (1 energy each); then energy = MIN(255, energy + 1 + energy_unit) |
(MCNT XOR slot) AND 7 = 0 | TACTICS for that slot |
(MCNT XOR slot) AND 15 = 0 | TIDY that slot |
| every 16 | dial flash phase (8 on, 8 off) |
MCNT AND 31 = 0 | station proximity check and spawn (B8) |
MCNT AND 31 = 10 | altitude = sqrt(x_hi² + y_hi² + z_hi² - 36); negative is a crash; "ENERGY LOW" below 50 |
MCNT AND 31 = 20 | cabin temperature from the sun distance, death past 255; scoops add speed/8 fuel when ≥ 224, cap 70 |
| MCNT wraps (every 256) | spawning: rnd < 35 gives a benign spawn at z_hi 38 (a Cobra trader 50% of the time, else an asteroid, or a canister with rnd < 5, skipped in the safe zone or with 3 asteroids present); cops with probability from contraband and legal status; pirates unless rnd ≥ 90 or (rnd AND 7) < government: rnd ≥ 200 gives a pack of (rnd AND 3) + 1 Sidewinders or Mambas, else one of Mamba, Python, Cobra hunter, Thargoid |
MCNT is set to 0 on launch, buying fuel and hyperspace, 1 after an in-system jump, 255 on death.
The station is placed at planet centre plus 2 x nosev(planet) x radius, so at altitude one radius, when MCNT AND 31 = 0, no station exists and the player is within 49152 (192 0) of that point on every axis. It spawns pointing at the planet with roll counter 255 (permanent 3.6° per frame) and AI byte %00000001. Inside the safe zone nothing hostile spawns, pirates do not attack, and the compass points at the station.
Docking checks in order: station not hostile; station nose within 26° of pointing at us (its nosev_z ≥ 86 of 96); station in front; unit vector to the station has z ≥ 89 of 96 (a 22° cone); our roll matches the slot (|roofv_x| ≥ 80 of 96, within 34°). A failed approach below speed 5 bounces without damage, otherwise it damages or kills.
Fire when the laser timer is 0, the fire key is held, laser temperature < 242 and the current view has a laser. Damage is power AND 127, 15 for both pulse and beam. Each shot adds 8 to the laser temperature, which cools by 1 per iteration. Beam refires every frame; pulse sets the timer to 10, decremented at 50 Hz, so 5 shots a second. Hit test: the target is in front, not the planet or sun, not exploding, both x_hi and y_hi are 0, and x_lo² + y_lo² < targetable area. On a hit energy drops by the damage; going negative kills (canisters spawn 50% of the time, up to blueprint byte 0). NPC lasers hit the player for laser_power \ 2.
| Rank | Kills | Rank | Kills |
|---|---|---|---|
| Harmless | 0-7 | Above Average | 64-127 |
| Mostly Harmless | 8-15 | Competent | 128-511 |
| Poor | 16-31 | Dangerous | 512-2559 |
| Average | 32-63 | Deadly | 2560-6399 |
| Elite | 6400 and up |
One kill point per kill in the cassette version; "RIGHT ON COMMANDER" each time the low byte wraps. Killing a Viper sets the fugitive bit.
z -= speed*64 : y += |y_hi|*q : x += |x_hi|*q; roll y += alpha*x/256 : x -= alpha*y/256; pitch y_hi -= BET1 (plus a small x term the original's author calls a mystery). Recycle when |x_hi| or |y_hi| ≥ 120 or z_hi < 16: x_hi = rnd OR 8, y_hi = rnd OR 4, z_hi = rnd OR 144, random signs. Side views drift x by 8*256*speed/z_hi; the rear view reverses the perspective.sqrt(K² - V²) + (rnd AND CNT), one horizontal line per row.' DORND: four seed bytes r0..r3; DORND2 clears the carry first, DORND does not
a = r0 : c = a \ 128 : a = ((a * 2) AND 255) OR carry_in : x = a
a = a + r2 + c : c = a \ 256 : a = a AND 255 : r0 = a : r2 = x
a = r1 : x = a
a = a + r3 + c : c = a \ 256 : a = a AND 255 : r1 = a : r3 = x
' result: a (random byte); carry c survives to the next call
Text tokens (TT27): 0-13 control codes (cash, galaxy number, current and selected system, commander name, fuel/cash line, sentence case, beep, all caps, tab to column 21, newlines), 14-31 recursive tokens 128-145, 32-95 ASCII, 96-127 recursive, 128-159 digraphs, 160-255 recursive tokens 0-95. Stored text is XORed with 35. The Python tool expands every fixed token to a plain string so only names, numbers and case handling remain at runtime.
Commander file: 76 data bytes. 0 mission flags; 1-2 chart position (20, 173 = Lave); 3-8 galaxy seeds; 9-12 cash in 0.1 Cr (1000.0 Cr = 10000; the source table shows the bytes as E8 03 00 00, so confirm the byte order against a real save); 13 fuel 70; 15 galaxy number; 16-19 lasers front/rear/left/right (15, 0, 0, 0); 22 hold size 22; 23-39 cargo x 17; 40-69 equipment flags, missiles, legal status, market availability x 17; 70 QQ26; 71-72 kills; 73 = 128; 74 CHK2 = &AA; 75 CHK = &03. Checksum: a = 73 : c = 0 : FOR x = 73 TO 1 STEP -1 : a = a + b(x-1) + c : c = a \ 256 : a = (a AND 255) XOR b(x) : NEXT gives CHK, and CHK2 = CHK XOR &A9. Our port only needs this if it wants to import real BBC saves; a plain text file is otherwise fine.
1-source-files/main-sources/elite-source.asm (ship blueprints, text tokens, tables) and elite-loader.asm. The same file is served raw from the elite-source-code-bbc-micro-cassette repository, which is what the converter should fetch. Per-ship annotated pages: /cassette/main/variable/ship_<name>.html; the blueprint table is xx21.html.graphics/Draw3D.c, graphics/Draw.h, configuration.h (MAX3D, heap and pool sizes), core/MATHS.c (quaternion and vector commands), graphics/FrameBuffer.c (COPY ... ,B), misc/External.c (gamepad).Bas/3ddemo.bas (12 Elite meshes on the engine), Bas/chuckie.bas and Bas/chuckie_tools/ (port and build pattern), PDF/3D_Graphics_User_Manual.pdf, PDF/Game_Development_Guide.pdf.