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.
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.
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.
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.
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.
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 16A Unix compress 4.0 decoder at image 0x9E40 feeds an RLE90 expander at 0x9CF9 (90 nn repeats the previous byte).
A raw u32 size, then 83 vv nn writes byte vv nn times.
u32 total size, u16 count, 4-character names, u32 offsets.
dash dot dota gbo0 gbox …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.
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.






rig0 / rg0m). XROADC holds the rest.
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.
Curve colour strength follows the total turn in each run. Stages end at a gas station, and the last one at THE END MOTORS.

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.
The y axis shows raw table values (the game's own units). The marker on each line is that car's rev limit.
| Car | Gears | Rev limit | Grip limit | Peak torque | Gauges |
|---|
| Offset | Field | Status |
|---|---|---|
| +000 | Number of gears | verified |
| +004 / +008 | Rev limit (going over damages the engine) · grip limit (going over starts a skid and tyre squeal) | verified |
| +012 | 7 gear ratios; rpm = ratio × speed ÷ 65536 | verified |
| +03C / +07C / +10C | Shift-gate nodes, a joystick-direction × node transition table, and node → gear | verified |
| +11C | Torque curve, 80 bytes, one entry per 128 rpm | verified |
| +16C … +328 | Gauge type, needle pivots, speedometer and tachometer needle-tip tables | verified |
| +00C | Steering-wheel sprite position | inferred |
| +002 +006 +010 | Not read anywhere in TDEGA; possibly used by TD.EXE or the CGA build | unknown |
TDSND.SNDFour 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.
SCORESEight 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.
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.
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.
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.
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.
Generate a square wave into an SDL_AudioStream from the same divisor table. Tyre squeal and engine sounds come from the same routines.
Use SDL keyboard events and SDL_Gamepad. The shift-gate graph maps directly onto a stick.
Replace with plain SDL_IOStream reads. Keep the SCORES CRC so existing score files still load.
Leave it out. It checks for fake-numbered sectors on track 39 that return CRC errors.
| Script | What it does |
|---|---|
| tools/unexepack.py | Unpacks the EXEPACK-compressed executables into plain MZ files |
| tools/x86dis.py | Byte search and 16-bit disassembly addressed by image offset |
| tools/tdres.py | Decodes PES/CMP archives and exports every sprite as .bin and .png |
| tools/sheet.py | Builds a labelled contact sheet per archive |
| tools/roadmap.py | Writes the road stream to JSON and draws the schematic route map |
| tools/cardata.py | Annotated per-car JSON plus a comparison table |
| tools/sndplay.py | Renders TDSND songs to square-wave WAVs |
The format reference lives next to the game files in FORMATS.md.