How a race runs
2026-10-23, eleventh session.

The results of the stock reference race. Our reader printed every line of this screen again from the game's memory, digit for digit.
Un-POD, the reimplementation, needs an exact core: the rules, the clock, the physics, the opponents and the random numbers, with no drawing or sound. This session traced the first part of it, the race itself, and recorded races of the original as ground truth. The full reference is docs/systems/race.md. This entry is how we got there.
A third of the code was missing
We started from the physics functions the car-parameter session had named. They had no callers in the decompiled code. The physics step that should call them had no function at all.
A byte scan for call instructions found the callers at addresses that belonged to no function. Ghidra's analysis had skipped code that is reached only through pointers. Each car carries a pointer to its physics step and one to its driver (human, computer or network). The track's actions are found by name in a table. The code between those functions was missing too: 263 KB of the 857 KB of code lay outside every function.
The first repair went wrong twice:
- GameFrame's batch decompiler stopped on one bad address with a NullPointerException. The remaining addresses never ran.
- Given an address in the middle of an unknown function, it made a new function there. The physics step came out cut in two at a call.
The fix was a small Ghidra script of our own. It makes a function at every call target and after every ret in the gaps, skips the ones that fail, and decompiles the rest. That added 1,123 functions. A second pass over the action table's handlers added 55 more. The results are in a second decompile file next to the first. After that the trail ran from the physics step to the race loop.
The clock
Each frame, FUN_004ae894 reads timeGetTime() and multiplies the milliseconds by 65. That gives the frame's length in 16.16 seconds, but 65.536 would be exact. Observed in the code, this predicts a clock that runs 0.82% slow, at 0.99182 of real time. Measured over 490 seconds of a recorded race: 0.99181.
A frame is clamped between 10.7 and 458 ms of game time. A frame longer than three "step units" (43 or 50 ms) is split into equal smaller steps. There is no fixed tick: the physics steps as often as the frames come. A port that is to match the original bit for bit must replay the frame times the original saw.
The start

The countdown in the stock race, from the recording. Orange: the race clock. Green: how far Maverick's car has moved. The vertical lines mark the number on screen changing.
The code says the countdown runs on the wall clock: 5.5 seconds, a sound for each of 5, 4, 3, 2, 1, then "GO!" sets the race clock to zero. Before that the cars settle on their suspension for two seconds of game time. The recording shows the same (measured):
- the number changes about once a second;
- the race clock runs through the countdown, then falls to zero at "GO!";
- the cars stand still until "GO!", within a few 1/65536 of a unit, and then move.
The rules live in the tracks
The code that writes lap times had a single reference: a table that registers 52 actions by name. "P-Chrono Tour" is the lap line and "P-Chrono Inter" a checkpoint. "Troncon" is a section where a computer car picks its next road, and "Decision PIT" sends a damaged car to the pits. Others start sounds and animations, switch scenery on and off, or show "wrong way".
Each beacon in a track is a gate with two chains of these actions, one for each way through it. The car's physics step tests the gates within 100 units, and a car that crosses one runs its chain.
Our first decoding of the chains made no sense: Beltane's lap line seemed to switch scenery off. The chains number their actions in the track file's own list, which is alphabetical, not in the order the game registers them. Read that way, every track decodes (measured):
- On Beltane, beacon 35 is the lap line, and beacons 40, 32 and 0 are the checkpoints, in that order.
- Of its 43 beacons, 29 only switch the scenery.
- A checkpoint counts only if it is the car's next one.
Positions

The place of each car through the stock race, read from the game's order table. Places change only in steps.
The race order is a table of eight pointers. It is re-sorted only when a car passes a checkpoint or the lap line (observed in FUN_0042c264). Nothing compares distances along the track. So between two gates the order on screen can be stale. The chart shows it: every change of place is a step.
No dice for the opponents
The whole program has one random generator, the Watcom C library's rand(). The seeds were not visible in the decompile: Watcom passes the first argument in a register, so each srand call looked argument-free. The instruction before each call gave them.
During a race, rand() draws only for the tyre smoke, seeded with a fixed 1000 when the race loads. The opponents' drivers, their road choices and the lap logic never reach it (observed: no path from those functions to rand). One action, "? Proba Unique", is a random condition, but no track on the disc uses it.
So a race should be deterministic, given the frame times and the player's keys. That is the property an exact port needs. It is inferred until the same inputs are played twice.
Three races on tape

The first recording drawn: x and y of all eight cars, ten times a second. The white line is our scripted driver, weaving. One computer car took the branch at the top once.
A recorder in the emulated PC (run/vm/racerec.py) reads the race straight from memory ten times a second:
- every car's position;
- each racer's race record (place, laps, checkpoint, times);
- the race order;
- the race clock and the frame time.
The VM is shared, and the first two races were not ours:
- When we went to start a race, the game restarted under us. The mods session was loading its changed files. We asked it, and while it worked we recorded its race without touching it, since reading memory changes nothing. That recording starts 74 seconds in.
- A second script waited in the VM for the next countdown and started the recorder (
racewait.sh). It caught the mods session's next race from its first second. - Both had mods loaded: the lap counter read "MOD 1/3" instead of "Tour 1/3". The mods session told us its later runs also changed the car's grip. A reference race with changed physics would be useless. Our two runs turned out to have the disc's grip: the player's best laps, 2'33"38 and 2'37"39, are exactly the two grip-95 runs in its notes. Still, we wanted a clean one.
- When the mods session had cleared its files, we drove a race ourselves through the menus, with the game reading only the disc. That stock race,
beltane-3, is the reference.

The HUD agreed with the recording in every race: here the player's lap times and the track's lap record.
Times as the game prints them
The record layout was checked against four results screens. Our first reader rounded the times to hundredths, and one gap came out a hundredth off ("+23"21" against "+23"20" on the screen). The game's format_time works in x87 floating point and converts with the FPU's rounding mode. With the FPU set to truncate, every time and gap matches the screen. With rounding to the nearest, some seconds come out negative. So the game runs with the FPU truncating (measured through the output; where it is set is not traced). The reader now prints times the same way.
What we learned for GameFrame
- Check how much of the code is inside functions after every decompile.
- Record a whole run of the original from memory, with screenshots, as the reference for a port.
- Measure a game's clock against real time.
- Check that nothing modded is loaded before recording a reference.
- Let a script wait for the next race instead of driving the menus.
Each is now a know-how entry in docs/knowhow/, and the tools are proposed for GameFrame in docs/gameframe-lessons.md.
Next
- Record each frame's time and the keys from inside the game, then play one race twice to show it is deterministic.
- Trace how the computer cars follow the race paths, and what changes with difficulty.
- Port the lap, checkpoint and order rules, and check them against
beltane-3.