The race rules in Rust
2026-10-24, first session.

Left: the original's results screen after three laps of Beltane. Right: pod-race after replaying the same race from its recorded positions (crates/pod-race/tests/replay.rs).
Un-POD is the plan to rebuild POD in Rust, exact where it decides what happens. This session started it with the part that needs no physics: the rules of a race. The crate pod-race counts laps and checkpoints, keeps the order, ends the race and lists the results. It does not move the cars. Until the physics is ported, it takes their moves from outside.
How POD counts a lap

Beltane's 43 gates, read from the track file, with one lap of the leader's path from the recording. Red: the lap line. Orange: the three checkpoints.
The rules of a lap live in the track, not in the code. Each gate (a "beacon") is a flat quad across the road with two chains of small actions, one for each way through it. After every physics step the game tests each car's move against the gates near it. When a move crosses one, the game runs the chain for that direction. On Beltane, "P-Chrono Tour" on the lap line counts laps and "P-Chrono Inter" on three gates counts checkpoints. A lap counts only after all three, in order. The other gates switch scenery and sounds on and off, and steer the computer cars.
Measured first
Three pieces of the rules do arithmetic that is easy to get almost right: the test for a car crossing a gate, the update of the order table, and the printing of times. Before porting them we ran each in the original under an emulator, on 20,000 inputs, with a new script measure/race_rules.py. The Rust code now gives the same answer for every one.
The gate test turned out to depend on the FPU's rounding mode. It takes a dot product in x87 code by adding 1.5 × 2^68 to the sum, and the mode decides how the last bit rounds. Run under both modes, the original gives different answers in 15 cases out of 20,000. Round to nearest, the mode the Watcom C library's start-up code sets, matches all of them. The printing of times looked like evidence for the other mode, but it is not. Its helper sets the mode to truncate for that one instruction and puts the old mode back after.
What went wrong on the way
- Ghidra's decompile of the gate test was garbled. The function and its dot-product helper pass their points in registers, and the decompile lost which point was which. We read the disassembly instead, about 120 instructions, and the helper's x87 code.
- The first model of the gate test was wrong. It rounded down, from the old reading that POD runs its FPU truncated. The emulator settled it, as above.
- The time formatter would not run under the emulator. It calls the C library's
sprintf, which needs data the emulator does not set up, and stopped with a memory fault. The script now stops at that call and reads the minutes, seconds and hundredths from its arguments. This also showed a carry from 60 seconds into the minutes that our readerracelog.pylacked. - The first replay disagreed with 1,038 samples. Every one was a sample early or late. The recorder reads the clock first and the records after it while the game runs, so a sample's records can be newer than its clock: once by 0.57 s of game time, while the VM was busy. The test now accepts a sample when the core passes through its state between that sample's clock and the end of the frame after the next sample.
- The race never ended in the replay. The end timer counts the real frame times, which the samples do not hold. The test now runs on after the last sample and checks the end against the rule's bounds instead of a frame.
- A car moved 54 units between two samples when the VM stalled. That is too far for the gate test, which only looks at gates within 100 units of the car. The replay cuts every sampled move into pieces of at most 4 units.
- Two of our own unit tests were wrong, not the code: one expected value sat exactly on a rounding tie, and one frame time was not the step boundary we thought.
Replaying a race
The reference race from the day before was recorded from the game's memory ten times a second: every car's position and race record. The test feeds those positions to the core and lets it decide which crossings count. The recording sees only about one frame in eight, so the frames are rebuilt around the clock values at which the game counted each gate.
At the first run the order went wrong right after the start. Two cars changed places while both were on the same lap at the same checkpoint. The order function we had traced cannot do that. A search of the raw code for the order table's address found the reason: a second function that swaps cars by how far they are along the race paths. Ghidra had not made a function there, so the trace missed it, and docs/systems/race.md said wrongly that only the gates change the order. It is corrected now.
This function needs each car's place on the paths, which the opponents' driving code keeps. It does not change the final order, which is the order the cars finish in. With the order left aside between gates, every one of the 4,489 samples matches. So do the eight final race records word for word, the order, and every line of the results screen. The next session ported the order between gates too (the order between gates).
To check the test can fail, we changed one split by a single unit: the test caught it. A change from "faster" to "as fast or faster" in the best-lap test was not caught, because no two laps in this race tie. The strict test is the original's, read in its code.
Where the port differs
Each difference is marked in the code and listed in docs/DEVIATIONS.md:
- The gate test's division cannot overflow here, where the original would stop with a divide error; the port truncates instead.
- The countdown reads the wall clock once per call, where the original reads it twice.
- Not ported: the physics, the best-time tables saved to
RECORDS/*.BRT, network and ghost races, the HUD and sounds, track actions that change other chains, and the run that drives unfinished cars to the line after the race. - In the test only: the frames are rebuilt around the recorded gate clocks, and the positions are the car's origin, not the chassis point the game tests.
What is known, and how
- Measured: the gate test, the order update and the time printing match the original on 20,000 inputs each.
- Measured: the replayed race matches the recording at every sample and at the end, but the clocks of the counted crossings come from the recording itself, so they are an input of the test, not a result.
- Observed: a second order update,
FUN_004afd3c, works on progress along the race paths. - Inferred: the game runs with the FPU rounding to nearest. The measurement fits only that mode, but the mode has not been read in the running game.
Next
- Record each frame's time and the point the gate test really uses, the chassis point at
car+0x360, so a replay needs no rebuilt frames. The recorder now reads the point; a run with it is still to do. - Then the physics step, so the core moves the cars itself.