Game archaeology · DOS · 1987

Test Drive, Unpacked

Distinctive Software's Test Drive (Accolade, 1987): how the EGA release is put together, which assets and data tables come out of it cleanly, and what an SDL3 port would take. Every claim below is checked against the unpacked executable or against byte-exact, CRC-checked decoding of the shipped files. Anything that is only inferred is labelled as such.

How is it built? Microsoft C plus hand-written EGA assembly, EXEPACK-compressed

A small launcher (TD.EXE) runs either TDCGA.EXE or TDEGA.EXE. The game itself is about 52 KB of code in roughly 170 C functions, with planar blitters written in assembly.

What can be extracted? All art, all five stages of road data, car physics, music and scores

The graphics archives use ARC compression. The road layout and sign placement are byte tables in the data segment. Gearboxes, torque curves and gauge geometry sit in each .BIN file.

Is an SDL3 port feasible? Yes. The hardware footprint is small and well understood

It uses one video mode, a timer hook, the PC speaker, BIOS keyboard and joystick, and DOS file I/O. Everything else is plain game logic that can be translated function by function.

Containers

From file on disk to sprite

The EGA files reuse ARC's compression methods unchanged: "crunched" (RLE90 + 12-bit LZW, method 8) and, for the title archive only, "squeezed" (static Huffman, method 4). The CGA files use a simpler escape-byte RLE. Both decode to the same resource archive.

EGA · .PES

"Pckd" header

Packed and unpacked sizes, the ARC method number (8 or 4) and an ARC CRC-16.

50 63 6B 64 · 44 07 00 00 · 6E 10 00 00 · 08 00 · 37 16
Decompress

LZW → RLE90

A Unix compress 4.0 decoder at image 0x9E40 feeds an RLE90 expander at 0x9CF9 (90 nn repeats the previous byte).

CGA · .CMP

Escape RLE

A raw u32 size, then 83 vv nn writes byte vv nn times.

83 AA 06 → AA AA AA AA AA AA
Archive

Named resources

u32 total size, u16 count, 4-character names, u32 offsets.

dash dot dota gbo0 gbox …
Sprite

16-byte header + planes

Width (in bytes), height, hotspot, x, y and a plane map. The low nibble of each plane-map byte says which colour planes that stored plane is written to; the high nibble of the first byte clears planes and of the second sets them. Masks are separate sprites, AND-blitted before the image.

Result: all 18 .PES files decode to exactly their stated size and pass their CRC-16. All 17 .CMP files match their size field. Every resource in every archive parses as a sprite.

Extracted art

The garage, the road and the traffic

Contact sheets straight from the decoder, shown at 2× with the 1:1.2 pixel aspect of a 200-line display. Colours use the game's own remapped palette (DS:00CC), which main loads through int 10h AX=1002h right after switching to mode 0Dh; index 6 is light grey, 10 is brown and 14 is light blue rather than the stock order.

Contact sheet of TESTDRV.PES: title logo, Accolade key fob, side-view car, wheel frames and passenger window animation
TESTDRV.PES: the only squeezed (Huffman) archive. Title logo, key fob, the intro car with wheel frames, and 19 frames of the passenger-window driver animation.
Lotus Esprit cockpit sprites, EGA 16 colours
LOTUS.PES: the cockpit, cut into dashboard, instrument cluster, roof, steering-wheel frames, gear gate, mirror and radar-detector lights.
Lotus Esprit cockpit sprites, CGA 4 colours
LOTUS.CMP: the same resources redrawn for CGA palette 1. The pixel data is packed at 2 bits per pixel in rows.
Lamborghini Countach showroom sprites with specification sheet
COUNTASB.PES: the showroom. The spec sheet, including its acceleration graph, is a single bitmap; nothing is typeset at runtime.
Road-side object sprites: dealership, game over, signs, poles, rocks, hazards, speeding ticket
XROADA.PES: the road itself. Cliff edges, lane lines, poles, rocks, and speed, turn, two-way and gas signs at five distance scales. Also oil slicks, potholes, gravel, the speeding ticket, GAME OVER, THE END MOTORS and the glove-box note.
Traffic vehicle sprites at five scales with masks
XROADB.PES: traffic at five scales. Each vehicle comes with a separate mask sprite (rig0 / rg0m). XROADC holds the rest.
Gas station screen and four station name signs
GAS.PES: the stop between stages, with signs for four stations named after the team: Don's Gas-O-Rama, JB's Pitstop, Kevin's, and Mr. Tony's Fuel King.
Road data

Five stages, one byte per road unit

There is no road file on disk; the string ROADDATA.SHP is left over and never referenced. The road is a byte stream inside the data segment (DS:2D28–6360). Each byte indexes a 4-byte record [flag, curve, pitch, object] in a 110-entry table at DS:2B70. The renderer reads 40 units ahead, bending each row by the curve value, and spawns signs, traffic, police and hazards from the object codes.

Stage strips

Right curve Left curve Sign Hazard Police

Curve colour strength follows the total turn in each run. Stages end at a gas station, and the last one at THE END MOTORS.

Schematic top-down route map integrated from curve values
Route map, schematic. The curve values are integrated at ¼° per unit and the stages are joined end to end. The game only ever turns the road within a 40-row window around the car, so the whole route was never designed to close up: stage 3 nets +1022° of turning and draws loops. Read this as a chart of rhythm and sequence, not geography.
Car files

Five cars, 1 238 bytes each

Each .BIN is read raw into DS:268F. It holds the gear ratios, a shift-gate graph that joystick movements walk through, an 80-point torque curve (one entry per 128 rpm), and pixel lists for the speedometer and tachometer needle tips. Drag, braking and idle rpm are constants in the executable, not in the car files.

Torque curves from the car files

The y axis shows raw table values (the game's own units). The marker on each line is that car's rev limit.

CarGearsRev limitGrip limitPeak torqueGauges
OffsetFieldStatus
+000Number of gearsverified
+004 / +008Rev limit (going over damages the engine) · grip limit (going over starts a skid and tyre squeal)verified
+0127 gear ratios; rpm = ratio × speed ÷ 65536verified
+03C / +07C / +10CShift-gate nodes, a joystick-direction × node transition table, and node → gearverified
+11CTorque curve, 80 bytes, one entry per 128 rpmverified
+16C … +328Gauge type, needle pivots, speedometer and tachometer needle-tip tablesverified
+00CSteering-wheel sprite positioninferred
+002 +006 +010Not read anywhere in TDEGA; possibly used by TD.EXE or the CGA buildunknown
Sound & scores

A 100 Hz speaker sequencer and a checksummed hall of fame

TDSND.SND

Four songs stored as bytecode in the usual resource archive. Each event is a note number (55 ≈ A440) followed by a u16 duration in ticks. FE sets how early notes cut off, FD–FB/FA–F8 open and close three levels of loops, and FF ends the song or chains to the next. The player runs from the timer interrupt and plays through the PC speaker using a divisor table at DS:6452. tools/sndplay.py renders the songs to square-wave WAVs, which have not been listened to yet.

SCORES

Eight entries, each written with %-20.20s%-20.20s%ld. The line after each is a CRC-8 checksum (reflected polynomial 0xB8), seeded with the row number. Entries whose checksum doesn't match are dropped on load, so hand-edited scores disappear.

SDL3 port

What has to be replaced

Only a handful of places touch hardware. The rest is ordinary 16-bit C logic working on tables that are now decoded. The practical route is a function-by-function reimplementation using a Ghidra decompilation of TDEGA_unp.exe as reference, loading the original data files through the decoders above.

int 10h · mode 0Dh

Draw into a 320×200 indexed framebuffer, convert it through the 16-colour palette, and present it as a streaming SDL_Texture with 4:3 logical presentation.

~190 × out dx,al

These are EGA plane-select and write-mode calls inside the planar blitters. In software they become a sprite blit using the plane map and mask sprites.

int 8 hook · PIT

The game tick and the 100 Hz music tick. Run them as a fixed-step loop with SDL_GetTicksNS. The physics steps are tick-based, so the tick rate has to match the original.

port 61h/42h speaker

Generate a square wave into an SDL_AudioStream from the same divisor table. Tyre squeal and engine sounds come from the same routines.

int 16h · joystick

Use SDL keyboard events and SDL_Gamepad. The shift-gate graph maps directly onto a stick.

int 21h files

Replace with plain SDL_IOStream reads. Keep the SCORES CRC so existing score files still load.

int 13h key disk

Leave it out. It checks for fake-numbered sectors on track 39 that return CRC errors.

Open risks

  • Traffic AI, collisions, police pursuit and the radar logic have been located but not fully traced.
  • Speed units: the speedometer table is indexed by the high byte of speed, and top speeds computed without drag come out too high for plain mph.
  • The Corvette's digital dash drawing code hasn't been found.
  • Timing: EGA-era frame pacing and tick-based physics have to be reproduced exactly for the game to feel right.
Toolkit

Reproduce everything

ScriptWhat it does
tools/unexepack.pyUnpacks the EXEPACK-compressed executables into plain MZ files
tools/x86dis.pyByte search and 16-bit disassembly addressed by image offset
tools/tdres.pyDecodes PES/CMP archives and exports every sprite as .bin and .png
tools/sheet.pyBuilds a labelled contact sheet per archive
tools/roadmap.pyWrites the road stream to JSON and draws the schematic route map
tools/cardata.pyAnnotated per-car JSON plus a comparison table
tools/sndplay.pyRenders TDSND songs to square-wave WAVs

The format reference lives next to the game files in FORMATS.md.