Skip to content

How a race runs ​

How WINPOD.EXE runs a race from the start lights to the results, traced for Un-POD's exact core (docs/PLAN.md, "The wrapper and Un-POD", layer 2). Addresses are WINPOD.EXE's. Each finding is observed (read in code or data), measured (seen in the running game, run/vm), inferred or unknown.

About a third of the program's code was not in Ghidra's first export: functions reached only through pointers (the per-car step and controllers, the track macro actions) and the code between them. The physics step FUN_00428b98, for example, had no function at all. The missed functions were made and decompiled into analysis/export/pod/WINPOD.EXE/decomp-extra.c (1,178 functions, ignored by git; docs/gameframe-lessons.md, "Tracing").

The parts ​

PartWhereWhat
Race sessionFUN_0049fba4Reads the camera and view settings from Ubi.ini, runs the race, writes the settings back, runs the track's "FinCourse" macro, then the end of race (FUN_00479114)
Race setupFUN_0049f75cPlaces the cars, settles them, resets the race state, starts the countdown, runs "Depart"
Race loopFUN_0049d4c0One pass per frame until DAT_00502750 (race over), DAT_00620041 (quit) or DAT_0060b2e4
Frame clockFUN_004ae894The frame time from timeGetTime
Simulation stepFUN_0049e63cEvery car's controller-and-physics function, the zones, the collisions, the cameras
CountdownFUN_004815e8The start lights and "GO!"
Track macrosFUN_00486004 registers the actions; beacons and events run themLaps, checkpoints, AI route choices, sounds, animations
End of raceFUN_00479114Finishes the cars still racing, saves best times, championship points

Who races ​

DAT_0061d5bc is the number of human racers and DAT_0061d5c0 the number of computer racers (observed: FUN_0042c120 counts humans against the first, and every loop over the cars runs to their sum; measured: 1 and 7 in a one-player race). Humans come first: racer n is a human when n < DAT_0061d5bc. DAT_0061d5c4 counts extra objects stepped after the cars (FUN_0049e63c; inferred: none in a normal race).

The race mode is in two flag bytes (observed in the branches; meanings inferred from what each branch does):

FlagMeaning
DAT_0061d5b4 & 3split screen (a second human, DAT_0061fec4)
DAT_0061d5b4 & 4network race (remote cars driven by FUN_00486c3c, message handling in FUN_00486bd0)
DAT_0061d5b4 & 8the attract demo (random track, random cameras: FUN_004361e0, FUN_0049d4c0)
DAT_0061d5b8 & 2championship (points and a sort at the end, FUN_00478568)
DAT_0061d5b8 & 4time trial: no grid order, each first lap-line crossing zeroes the race clock (FUN_0042c684), the countdown skips to phase 3
DAT_0061d5b9 & 2a mode that keeps a ghost lap (copies of the best lap's splits; FUN_0042c3a0, FUN_0042c684)

Measured in a one-player race on Beltane: DAT_0061d5b4 = 0, DAT_0061d5b8 = 9.

The per-car tables ​

Three tables, one row per racer (observed; strides from the loops):

TableStrideWhat
0x61aad00x558The race car: the car parameters and the physics state (docs/formats/cars.md); position at +0, +4, +8 (measured)
0x61f8700xb0The driver: +0 the race car, +8 the physics step, +0xc the controller, +0x38 flags (1 human, 2 computer, 4 network; measured), +4 the car's object, +0x34 the input word, +0x2c the path state; the computer's fields in docs/systems/controllers.md
0x5021400xbcThe race record: place, laps, checkpoints and times ("The race record" below)
0x61eb8c0xdcThe racer's entry from the menus: grid slot at +0xcc (0x61ec58), kind at +0xd0 (0x61ec5c: 1 human; bit 2 network); inferred: the name at +0

The driver's two function pointers are set by FUN_0042fb38 and FUN_0042fa60 (observed):

Controller (+0xc)Who
FUN_00423b3ca human: keyboard or joystick
FUN_0042f5f0a computer car following the race paths
FUN_0042f800a computer car with the network flag (4)
FUN_00486c3ca network car (set by FUN_00487a9c)

The physics step (+8) is FUN_00428b98 for every car: engine, brakes, tyres, suspension and the beacon test, with the fields of docs/formats/cars.md.

The frame and its clock ​

Each pass of the race loop FUN_0049d4c0 (observed):

  1. FUN_00471d5c: keys typed as words (cheat codes and debug toggles, compared against the strings at 0x5780bc and after).
  2. The frame time. FUN_004ae894 sets DAT_0060be0c to the milliseconds since the last frame (timeGetTime) times 65, clamped to 700 .. 30000, and adds it to DAT_0060be10. The unit is 16.16 seconds, so the factor should be 65.536: the game clock runs 0.82% slower than real time (observed: 65 = 0x41; inferred: a rounding by the programmers). The clamp keeps a frame between 10.7 ms and 458 ms of game time. When FUN_004d1b94 returns 0 (inferred: the first frame) the previous time is taken as 100 ms ago.
  3. The steps. Unless the countdown is in phase 3, the frame is stepped by FUN_0049e63c. With limit = 3 × DAT_0060bdfc: a frame shorter than limit is one step of DAT_0060be0c; a longer one is n = ceil(dt / limit) steps of dt / n each (integer division; the loop then puts the whole dt back for the clock, 0x49d708). The steps run before the clock moves on, so a lap or checkpoint counted in a step is stamped with the clock at the start of its frame (observed). DAT_0060bdfc is 0x444 (16.7 ms) or 0x3a8 (14.3 ms) by the track's view setting DAT_0061ffac (FUN_0049b6a0, FUN_0049ce70), so a step is at most 50 ms or 43 ms of game time.
  4. DAT_00502740 += DAT_0060be0c: the race clock, 16.16 seconds.
  5. FUN_004d09bc for each human racer (DAT_0061d5bc of them), not for network cars as first read: it moves the human's path tracker (driver +0x30) once per frame, outside the steps. When the car has moved 10 units (FUN_00430e30 ≥ 0xa0000) from the position kept at 0x7020f0 + 16 × racer, it runs FUN_004b0ec0, or FUN_004af830 at a fork on a countdown of 6 (observed by pod-ai in pod-physics' step log of beltane-3, 2026-10-24). So the human's tracker, and the order between gates, follows frames, not steps. Then the sound (FUN_004c00f0), the per-frame race logic (FUN_0049de7c), drawing (DAT_006924c0, FUN_0049ece8, DAT_006924c4), the HUD for each view (FUN_004815e8 while counting down, FUN_0047c470, FUN_0047f56c), FUN_0042218c and the pause menu (DAT_00608e74).

Measured over a whole race (derived/race/beltane-2.jsonl, 490 s): the race clock gains 0.99181 s per second of wall time, against 65 / 65.536 = 0.99182 predicted.

So the physics runs at a variable step that follows the frame rate: there is no fixed tick. Measured: DAT_0060bdfc = 0x3a8 on Beltane; frames of 12 ms (DAT_0060be0c = 780). A port that is to match the original bit for bit must replay the recorded sequence of frame times (re-port, "inputs").

One simulation step, FUN_0049e63c (observed):

  1. For each car: the driver's +8 function with the driver in EAX (FUN_00428b98, which calls the controller at +0xc first: call [edx+0xc] at 0x428c1f), FUN_004235f8, then FUN_004231f4 on the car.
  2. The extra objects (DAT_0061d5c4).
  3. FUN_0045a7f0: for each car, the first zone (.lev, DAT_005392d4, 0x44 bytes each) that FUN_0045a29c says holds it, acted on by FUN_0045a524.
  4. FUN_0042ed74: car-to-car collisions (skipped while DAT_0061d5ac is set, a debug key).
  5. A replay hook (DAT_00608e7c) when FUN_004b45f4 returns 2.
  6. The camera of each view (FUN_0042b6c0 with the chase camera at 0x60bba0).

The physics step itself scales DAT_0060be0c by FUN_0042ffd4 for its duration and splits the suspension into smaller steps (FUN_00426fb4) (observed; the physics is docs/formats/cars.md's subject).

The start ​

FUN_0049f75c (observed):

  1. Runs FUN_00475820, clears two words of each car's +0xbc/+0xc0, and lists the human racers' indices at 0x61fdf0 (stride 0xd4, one per view).
  2. FUN_0049c13c puts the cars on the grid (the track's start records, read_track_race).
  3. FUN_0042bec0 resets the race state: clock, counters, records, and the order table from each racer's grid slot (0x61ec58); then loads the track's best times (load_records, FUN_004b2300).
  4. Settling: two seconds of game time (0x20000) in steps of 3 × DAT_0060bdfc, each car's input +0x34 set to 0x10 and +0x7c to 0, the physics step run and the camera placed. The cars drop onto their suspension before the start.
  5. DAT_00502740 is zero and phase DAT_005920a4 is one (zero in time trial or when DAT_0050720a is set), FUN_00459c8c, a sound, then the track's "Depart" macro (DAT_00693a30).

The countdown, FUN_004815e8, runs every frame while the phase is not 0 (observed):

  • Phase 1: remember timeGetTime() in DAT_005920a8, go to phase 2 (3 in time trial).
  • Each frame: the number shown is (DAT_005920a8 + 5500) / 1000 - now / 1000 (the first division unsigned, the second signed), whole seconds of the wall clock, not the game clock. A new number plays a sound: 5 0x1f, 4 0x1e, 3 0x1d, 2 0x1c, 1 0x04. Every car's input +0x7c is held at 0.
  • When now > DAT_005920a8 + 5500 (unsigned, 0x481656): phase 0, "GO!" (0x4f308d), sound 0x20, the number -1, and the race clock is set to 0 again. Every frame of the countdown, "GO!" included, writes the grid places into each record's byte +4 and the order table again (not in a time trial).

Measured (beltane-2.jsonl): the numbers 5, 4, 3, 2, 1 and a brief 0 follow about one second apart; the race clock runs through the countdown (1.3 s to 5.9 s of game time) and is zeroed at "GO!"; the cars stand still within a few 1/65536 units until "GO!", then move.

Because the countdown runs on timeGetTime while the physics runs on frame times, the number of physics steps before "GO!" depends on the frame rate (inferred). The cars are held in place by the zero input, and the clock restarts at "GO!", so the race itself starts from the same state (inferred; to be measured).

Beacons, checkpoints and laps ​

The rules of a lap are not in the code but in the track: each beacon (.bal, read_track_beacons) is a gate with two macro chains, one for each way through it.

The beacon test (observed). In the physics step, for each car that is not a network car, FUN_0042a3ac walks the DAT_00502138 beacons (0x44 bytes each in memory at DAT_0050213c: the 0x3c bytes of the file and two macro pointers at +0x3c and +0x40). A beacon is tested when the car is within 100 units (0x640000, as |dx| + |dy|) of either end; the first test is strict, the second not. The point is the chassis point at car+0x360, copied before the step (0x428caa) and compared with its place after it. FUN_0042a1f0 decides whether the move crossed the gate: the move's two ends are on opposite sides of the gate's plane, and the crossing point lies between the gate's ends in x and y (with 0x20 of slack, tested only for an end pair more than 0x20 apart) and between its lowest (+8) and highest (+0x20) z. From the front (the old point on the plane's positive side) it runs the macro at +0x3c, from the back the one at +0x40.

The beacon's 15 words (observed in FUN_0042a1f0, measured on Beltane): corner A at +0, corner B at +0xc, B raised at +0x18 (its z at +0x20 is the top), A raised at +0x24, and the plane's normal (16.16, length 1) at +0x30. The side of the plane is the dot product FUN_004db530 of (A − point) with the normal: x87 code that adds 1.5 × 2^68 (0x4fa000) to the first product, then the others, and keeps the low word of the double, so the result is the sum / 65536 rounded by the FPU's mode. The crossing point is the old point plus (new − old) × t >> 16 with t = (d1 << 16) / (d1 − d2) (idiv).

The game races with the control word 0x027f, round to nearest at 53-bit precision (measured: every step of pod-physics' step log), not the 0x037f that Watcom's start-up fninit (0x4d74e0) leaves; what sets it is unknown (it may be Wine). At 53 bits each sum with 1.5 × 2^68 rounds straight to a step of 65536. Measured under Unicorn on POD's WINPOD.EXE (measure/race_rules.py, 20,000 moves across Beltane's gates, POD FUN_0041cd84 and FUN_004d41a0): the port matches every result under 0x027f; 11 come out differently under 0x037f and 20 under truncation. An earlier model of the first edition assumed 0x037f (a step of 32 first); it matched the first edition's measurement under that word, which was the wrong word for the running game.

On Beltane the checkpoints 32 and 0 count on their gates' back chains and the lap line 35 and checkpoint 40 on the front ones (measured: the chains, and the crossings in the reference race).

Macros (observed). A macro is a chain of nodes (handler, parameter, next) built by read_macro_refs (FUN_0046cbf0) from three words each: an index into the track's macro table (read_track_macros: 52 action names, the same in all 18 tracks), the parameter, and the next node's index or -1. The handlers are registered by name in FUN_00486004:

ActionHandlerDoes
P-Chrono TourFUN_0042c684The lap line, driven forward
N-Chrono TourFUN_0042ce80The lap line, driven backward
P-Chrono InterFUN_0042c3a0A checkpoint, forward: a split time
N-Chrono InterFUN_0042ce7cA checkpoint, backward (returns at once)
TronconFUN_00459680A section of the race paths: a computer car picks its next path link
Decision PITFUN_0045ab14A computer car with enough damage and laps to go turns into the pit lane
Fin Course, Fin Dep LFUN_004595e0, FUN_00459590End of a path, end of a start lane
Contrainte, Pistes cacheesFUN_0045963c, FUN_00459df0Constraint planes; hidden tracks
Wrong-Way ON/OFFFUN_004d05e4, FUN_004d05b0The wrong-way warning
Tunnel Entree/Sortie, Aff Env ON/OFF, Reverb VoitureFUN_004758b8, FUN_00475960, FUN_0047585c, FUN_00475becPicture and sound only
? Tour, ? Joueur, ? Concurrent, ? Proba Unique, DEBUT-COND, ...Sinon..., FIN-CONDFUN_0046ff30 .. FUN_004756ccConditions: on the lap, on human or computer, on a random draw
Macro, Active/Desactive/Init/Remplace/Echange Macro, All Macros ON/OFFFUN_00475710 .. FUN_004757b0Macros that change macros
Lance/Arret/Fige/Deplace/Relance/Reinit/Stop/Bloque AnimFUN_00475478 .. FUN_004753a4Track animations
Son Source/Statique/Localisé, Tue Sons ...FUN_00475a20 .. FUN_00475bbcSounds

The track's four named events (read_track_events) are macros run by the game: "Depart" (DAT_00693a30) at the start, "FinCourse" (DAT_00693a3c) at the end, "Arrivee" (DAT_00693a38) and "Charge" (DAT_00693a34).

What the tracks use (measured by decoding every .BL3; decoders/track_race.py and the chain walk): every track but TEST has one P-Chrono Tour and one N-Chrono Tour, one to three P-Chrono Inter, and Troncon on most gates. No track uses "? Proba Unique", "? Tour" or any condition. Beltane has 43 beacons: beacon 35 is the lap line (forward: P-Chrono Tour, Troncon), beacons 40, 32 and 0 the checkpoints, 29 gates switch the scenery on and off, two pairs mark tunnels, and "Depart" starts four localised sounds that "FinCourse" stops.

Checkpoints. The track's k checkpoint records (0x20 bytes each at 0x502778, k in DAT_0061d5d4, at most 4: FUN_0042bec0 stops the game with more) start with a beacon number that read_track_beacons turns into a pointer. Record 0 is the lap line, records 1 to k-1 the checkpoints in order (measured: Beltane's are beacons 35, 40, 32, 0). FUN_0042c3a0 counts a checkpoint only when the beacon is the racer's next one (0x502798 + 0x20 × checkpoint), the racer has crossed the lap line at least once and has not finished: it stores the split (clock minus the time at the last checkpoint), keeps the best split per checkpoint (0x502760), compares it with the track records (FUN_004b24fc against 0x693e78) for the HUD, and moves the racer up the order.

The lap line, FUN_0042c684, counts only after the last checkpoint (or on the first crossing) and for a racer that has not finished. It stores the lap time (clock minus the clock at the last lap line), the best lap (DAT_0050275c, racer DAT_00502758) and the best of each lap (0x502760), checks the track's lap records (FUN_004b26f8, FUN_004b24fc with 0x693e78), adds one to the lap count and prints the lap number or "LASTLAP" (AFFICH, FUN_00474910). When the count passes the race's laps (DAT_0061d5c8, from CIRCUITS.BIN +0x84): FUN_00459e58, DAT_00502748 += 1 (cars finished), the finished flag, the race time checked against the race records (FUN_004b24fc with 0x693a84, splits through FUN_004b2a34), and for a human FUN_0042c120.

Measured: the first crossing, a few seconds after "GO!", only starts the count. Lap 1 is timed from "GO!" (clock 0), and the race time is the sum of the laps.

Positions ​

The order is the table at 0x502720: place p (from 0) holds a pointer to a race record. Each record also keeps its place, from 1, in the high nibble of byte +4 (observed, measured). FUN_0042bec0 fills both from the grid. Two things change it in a race:

  • At a gate: when a car's checkpoint or lap line counts, FUN_0042c264 moves it up past every car ahead of it that has not finished and has fewer lap-line crossings, or as many and an earlier checkpoint (observed; measured under Unicorn on 20,000 random tables, measure/race_rules.py). A car that finishes passes every car still racing, so the final order is the finishing order.
  • Between gates: each computer car's path tracker calls FUN_004afd3c when it moves on ("The race paths and the order between them" below).

FUN_0042ceac is a variant of FUN_0042c264 with a tie-break on +0x14 (the clock at the last gate), used by the network messages (FUN_004870e8).

The race paths and the order between them ​

The race paths (read_race_sections FUN_0046bab0, read_race_graph FUN_0046bdec; docs/formats/tracks.md) give each car a place on the lap between gates (observed, and measured under Unicorn: measure/race_paths.py, 20,000 cases each):

  • The graph at run time. A node (0x24 bytes) holds its position (+0), its links out (+0xc count, +0x14 list: the first list in the file) and a value (+0x1c). Each node has a list of section entries, one per lane: four words in the file, at run time a 16-bit lane number (+0), a 16-bit point count (+2), a pointer to the lane's first point (+4, from the third word) and flags (+8: bit 0x80, and a type in the low four bits). A link (0x1c bytes) holds the start node's entries it carries (+0, +4), its end node (+8), its start node (+0xc), its points (+0x10: the lane its word names, a run of the sections' points), and one value per point (+0x14, +0x18). The values fall along the lap from the lap line round to it again: the distance to go, 16.16 (inferred). DAT_0058e080, the graph's first word, is the lap's length on that scale (inferred: on Beltane it equals the highest node value, 514,358,392 or 7,848 units). On Beltane, nodes 3, 5 and 8 (from 0) fork two, three and two ways, and each node has about ten lanes, some flagged or of type 4 to 6 (pit and special routes, inferred).
  • The tracker FUN_004b0ec0 keeps a car's link and point at driver+0x2c and +0x30. Along a link it steps on while the next point is nearer (|dx| + |dy| + |dz|). At a link's last point it walks the end node's links out and takes each whose first point is nearer than the one chosen so far, or lies behind the car (the dot product FUN_004db530 of the link's first step and the car's offset is not negative; FUN_004b0b98 gives the next point). The move holds only if it lowers the value, so the tracker never passes the lap line by itself. It does nothing while DAT_00539280 (the first link's points) is 0. A computer car's path follower FUN_0042f440 runs it from the car's controller each physics step, on the car's position at +0x498.
  • "Troncon" (FUN_00459680) is how trackers pass the line and the forks. At load, FUN_004592b4 gives the gates' "Troncon" actions the nodes in turn, beacon by beacon, front chain before back. For a human the tracker goes to the start of the node's first link out, unless it is more than 500 units (0x1f40000) of value from the node and from the lap's end. For a computer car with driver flag 2 (+0x38), FUN_004593fc picks the lane whose number is nearest the driver's lane at +0x44 (skipping flagged ones and types 4 to 6; type 1 depends on a distance), the car's steering takes it (+0x28, +0x4c, +0x78), FUN_004594d8 finds the link out that carries it, and the tracker starts there and runs.
  • The order update FUN_004afd3c runs when a tracker moves on (and, without a move, when a human in a view is the car or the one ahead). From the car just ahead upwards, while those cars have no more lap-line crossings, the car passes each one with fewer crossings or an earlier next checkpoint, or else, for two computer cars, a higher path value. It stops above the finished cars (DAT_0050274a). Two quirks: a car it does not pass does not stop it, so a pass can jump a car; and after the first car the test to go on compares the crossings twice (0x4b0435, 0x4b0437). With a human in a view, the comparison uses the cars' distance (FUN_00430e30), their links and nodes (FUN_0047bf60) and a dot product instead (0x4aff64 to 0x4b0305; observed, not traced in detail).

Measured in the reference race: the cars swap places between gates 47 times; the player is last throughout, so only two computer cars ever compare.

The race record ​

0xBC bytes per racer at 0x502140 + racer × 0xbc, zeroed and set up by FUN_0042bec0. Measured against the results screen of a three-lap Beltane race (Maverick first in 5'31"03, best lap 1'47"75; the player eighth, 3'59"51 behind):

OffsetTypeWhat
+0x00i32-1 once a human has finished (FUN_0042c684), else 0
+0x04u8place (high nibble, from 1); bit 0 finished, bit 1 out (network), bit 2 driven to the end (FUN_0047831c)
+0x08u8low nibble: lap-line crossings (laps + 1 at the finish); high nibble: the next checkpoint
+0x0cptrwhere the current split goes (+0x3c + 0x10 × lap + 4 × checkpoint)
+0x1016.16 sclock at the last lap line: the race time at the finish
+0x1416.16 sclock at the last checkpoint or lap line
+0x1816.16 s"GO!" to the first lap-line crossing
+0x1c16.16 s × 8lap times, lap 1 first
+0x3c16.16 s × 4 per lapcheckpoint splits; the four of a lap add up to the lap
...best-lap copies for the ghost mode (+0x4c, +0x5c, +0x7c)

The end ​

  • A human finishing (FUN_0042c120) increments DAT_00502749 by one; when every human has finished (one-player and split screen), DAT_0050274c is one and DAT_00502754 starts at 3 seconds (0x30000). In a network race the first finisher starts a 60-second timer (0x3c0000), shown on the HUD (FUN_0047f56c).
  • FUN_0047f56c counts DAT_00502754 down by the frame time just measured, after setting DAT_00502750 records whether DAT_00502754 is below one, so the race loop ends after the frame that finds the timer run out (observed; measured: the clock stood 3.04 s after the player's finish). The end clock is the first frame end at or past the finish + the finishing frame's time + 3 s; in the reference race that puts the finishing frame between 700 and 1,187 units.
  • The computer cars do not have to finish first: the race goes on until the humans have.
  • FUN_0049fba4 then runs "FinCourse" and FUN_00479114:
    1. lists the human racers at 0x57883c;
    2. FUN_0047831c drives every car still racing to the line: the controller, then the physics step (which calls the controller again, so it runs twice a step here; 0x47849f, 0x4784a7), with steps of 3 × DAT_0060bdfc, up to 300 seconds of game time more (60 in a network race), with no drawing. So the results of cars that had not finished come from this fast run, at a coarser step than the race itself;
    3. the network results (FUN_0048e51c);
    4. saves the best times: save_records (FUN_004b23f4) writes the record tables at 0x693a84 to RECORDS/<track>.BRT (docs/formats/records.md), after FUN_004b24fc has inserted each new time during the race;
    5. a championship: points (FUN_00478568) and two sorts of the racers (FUN_004d2925, Watcom qsort, with FUN_004787dc or FUN_0047875c).
  • Printing times: format_time (FUN_0047e4d0) works in x87 floating point with the game's constants: minutes = v × 2.5431e-7 (1 / (65536 × 60)) and seconds = rem / 65536, each truncated by FUN_004d18ac, which loads a control word with rounding set to chop around frndint and puts the old one back, whatever mode the game runs in; then hundredths = frac × 0.0015259 + 0.5, truncated, with a carry into the seconds when it passes 99 and from the seconds into the minutes when they pass 59 (0x47e543, 0x47e560). The constant for minutes is a little above 1 / 3,932,160, so whole minutes never come out one short, and the rest is exact: the whole function is integer arithmetic (pod_race::time). Measured under Unicorn on 20,000 values (measure/race_rules.py). An earlier reading took the truncation for the FPU's mode; it is FUN_004d18ac's own.
  • The results screen ("CLASSEMENT") is drawn by the menus from the order table and the records (inferred; its function is not traced yet).

Random numbers ​

One generator for the whole program: Watcom's C library rand() at FUN_004d1877, state = state × 0x41c64e6d + 0x3039, returning bits 16 to 30 (observed). Its state is per thread (FUN_004d186d returns the thread's data + 0xc). srand is FUN_004d189b, the seed in EAX (Watcom's register convention).

Seeds (observed, from the instruction before each call):

CallSeedWhen
0x460e90 in FUN_00460bbc1000Loading a race (from FUN_0049ba20): then 255 draws fill the table at 0x57205c
0x4b1e52 in default_recordstimeGetTime()Built-in best times when no .BRT opens
FUN_0043dbaf, FUN_0043dd20, FUN_0043fc6c, FUN_0044f5d3, FUN_0044f794, FUN_00450838 (3), FUN_00451cf0, FUN_00452240time(NULL) (FUN_004d1d5a)Menus: random names and choices
0x41b25ea function's resultThe network layer

Draws during a race (observed; the closure of each function in the decompiled code):

  • The tyre smoke and skid marks: FUN_00460f08 draws once per call (rand() % 255 into the table at 0x57205c), from the physics step (FUN_00428b98 → FUN_00428114 → FUN_004611ac). The value sizes a particle; it does not reach the cars' state (inferred). It does advance the shared state.
  • "? Proba Unique" (FUN_0046ff00): rand() % 100 <= parameter. No track uses it.
  • The attract demo: random track and split screen (FUN_004361e0), random cameras every 5 to 10 seconds (FUN_0049d4c0).
  • None in the opponents' AI: the controllers FUN_0042f5f0 and FUN_0042f800, the path choices (Troncon, Decision PIT), the race step FUN_0049e63c, the lap and checkpoint handlers and the network driver reach no rand call. The opponents' behaviour rules are chosen by the summed step time DAT_00572458 modulo 100 instead (FUN_00462f40, docs/systems/controllers.md): no random number, but it follows the frame times.

So a one-player race is deterministic given the frame times and the human's inputs. The only randomness is the smoke, which changes only the picture (inferred; to be checked by replaying one race twice).

Ported ​

crates/pod-race (Un-POD's exact core, first part) ports the rules above, each item naming its function: the frame clock and its steps (clock), the countdown and the reset (start), the beacon test (gate), the track's beacons and chains (course), the lap line, checkpoints, order at gates and a human's finish (rules), the end timer and the results (end), format_time (time), the race record as its 47 words (record), and the race paths, the tracker, "Troncon" for humans and the order between gates (paths, order). Every difference from the original is marked Deviation: or not ported (docs/DEVIATIONS.md). The physics is not ported: the caller moves the cars.

Evidence:

WhatHow it is known
Beacon test, order at gates, format_time (POD FUN_0041cd84, FUN_0041ed00, FUN_00478f7c)Measured: POD 2.8's WINPOD.EXE under Unicorn, control word 0x027f, 20,000 inputs each (measure/race_rules.py into derived/measure/race-pod/); tests/measured.rs matches every one
Path tracker, order between gates (POD FUN_004abec4, FUN_004aad68)Measured: the same, on POD's Beltane path graph, 20,000 cases each (measure/race_paths.py); every one matches
The rules over a whole raceMeasured: tests/replay.rs drives the core with the positions of POD's three reference races (pod-beltane-easy, pod-beltane-medium, pod-azthec-medium; 11,372, 3,596 and 3,620 samples after "GO!"). In every sample each car's record (finished bit, crossings, next checkpoint, the clocks at +0x10 and +0x14) is one the core passes through; at the end every word of the eight race records and the order are the original's (but bit 3 of a human's byte +4, below). A fourth, pod-factory-full (FACTORY, five laps, EASY; 14,450 samples), was stopped before the player finished and has no final records; every sample matches, the seven computer cars' five laps included. A fifth, pod-beltane-full (Beltane, three laps, EASY; 23,509 samples), stopped by the recorder's time limit with the player still on lap 2, matches in every sample and in every word of the final records and the order. A sixth, pod-beltane-e2e (Beltane, three laps, 6,666 samples after "GO!"), reaches the results screen; each sampled car record and the final records and order match
Backward tracker, order for a car moving back (POD FUN_004ac298, FUN_004ab418)Measured: the same, 20,000 cases each; every one matches
The order between gatesMeasured: tests/replay.rs order_between_gates: for every pair of samples with no gate between them, the core starts from the game's records, order and trackers (the recordings hold each driver's path state), applies the trackers' moves through the order update, and gets the game's next order: 11,265, 3,499 and 3,501 pairs, every one (and 14,285 of 14,285 on pod-factory-full and 23,407 of 23,407 on pod-beltane-full), including all 21, 26 and 59 where the order changed (one shows a sample late: the recorder reads the order before the trackers). Counted apart: 37 pairs where several cars moved within one sample (which moved first decides) and 2 where the player's place changed (the human branch, not ported)

What the replay takes from the recording, and so does not test: the clock at which each counted crossing happened (the frames are rebuilt around them, tests/common/frames.rs), for the first crossing the final record's +0x18, each car's place on the paths before "GO!", and at a "Troncon" gate the link a computer car goes on (the recording shows it; the lane choice itself is pod-ai's). It tests which crossings count, the lap and split times as differences, where each lands in the record, the best lap and splits, places, flags and counters, the split pointer and the final order. The positions are the chassis point (car+0x360) and the trackers' point (+0x498), as the game tests them. None of POD's three recordings reaches the race's end (the recorder's time ran out first), so the end and the results text are not checked on POD; the first edition's beltane-3 checked them (docs/versions.md).

The full replay also reports how many of the game's orders and trackers it passes through in sequence, without a bar: a slow frame of the game holds many samples and steps, which the rebuilt frames cannot separate (Beltane: 26 of 28 and 31 of 33 orders; Azthec, with many more swaps, 36 of 85). order_between_gates checks the order update itself against the game's own trackers.

Going the wrong way (POD, observed; measured under Unicorn on 20,000 cases each, measure/race_paths.py). FUN_004ac298 is the tracker run backwards: along a link it steps back while the point before is nearer; at a link's first point it takes, of the start node's links in (the node's second list, +0x10/+0x18), each one whose last point is nearer than the point chosen so far or lies ahead of the car (the dot product of the step into it, FUN_004abdb4, and the car's offset from it is not positive). The move holds only if it raises the value; then, for a car still racing, FUN_004ab418 moves it down the order: from the car just behind, while those cars have no fewer crossings, the car drops behind each one with more crossings or a later next checkpoint, or else a lower path value, with the same two quirks as FUN_004aad68. POD's point after a link's last (FUN_004abae8) is the first point of the end node's link out when it has one, the node's place when it has several (the code works out the mean of their first points and then overwrites it with the node's place, 0x4abbf5; measured by the fold search below), and 0 when it has none. FUN_004abc1c is the point before a link's first: the last point of the start node's link in, or the mean of several (truncating), 0 for none. Ported: Paths::track_back, Race::follow_path_back, Race::order_back.

Where the paths fold back (POD FUN_004c6854, measured under Unicorn: 20,000 cases on Beltane, 849 of them finding a place, and 2,000 on Azthec, 248 finding one; every one matches). It searches one way, then the other (FUN_004c665c with ECX 1, then 0). Way 1 walks forward (FUN_004c64e0) and measures along the step into the tracker's point (FUN_004abdb4); way 0 walks back (FUN_004c65a4) along the step out (FUN_004abd38). Each walk takes up to 10 points (FUN_004abe3c forward, FUN_004abe88 back) while the path value keeps changing its way, stopping after 30 units of value; at a fork it takes each branch in turn. Walks repeat (up to 10) while the point reached lies past the tracker's point along the step and within 100 units (|dx| + |dy|) of the car. A walk that ends on the near side with |dx| + |dy| + 2 |dz| under half the reach given puts the tracker there. Ported: Paths::fold.

Putting a tracker back on the paths (POD FUN_004aa8a0, measured under Unicorn: 300 cases on Beltane and 2,000 on Azthec, every one matches; measure/race_paths.py). Two passes from the tracker's point; the distance is the larger of |dx| and |dy|, or |4 dz| when as large. Each pass searches, stopping at the first answer: the start node's links (within 60 units) when the node has no extra entries, else each extra entry's walk through the graph (FUN_004aa7a4, depth first, skipping the nodes searched, within 100 units); then the end node, the nodes whose links lead into the start node, and every node (FUN_004aa5ac, each node's links out by FUN_004aa44c; 100 units). A point within 10 units ends a search. In the first pass a point must lie within 1,000 units of the tracker's path value and any answer is taken; in the second an answer must be nearer than the first pass's best shifted right by 3 on Beltane or by 1 on any other track (a name test against "beltane", 0x4aab39), else the first pass's best stays. Ported: Paths::place.

The human's tracker, every frame (POD FUN_004c688c, first edition FUN_004d09bc; measured under Unicorn with the start-up tables built: 20,000 cases on Beltane and 5,000 on Azthec, every one matches in the tracker, the follower's state, the record's word 0 and byte +4; measure/race_paths.py, the human set). The race loop runs it once a frame for each human racer, with the car (EAX). Its state is 16 bytes at 0x73932c + racer × 16: the position it last ran at, +0xc 1 after a move forward and 0 after a move back, and +0xe a wait (frames until the tracker may be put back again, set to 6 each time it is). It does nothing without paths (DAT_00543254) or until the car has moved 10 units (FUN_00423ac0) since it last ran; then it keeps the position and counts the wait down.

  1. When the tracker's link starts at a fork (a node with several links out, or extra entries), the tracker is put back on the paths (FUN_004aa8a0) once the wait runs out, at once after a move back. Elsewhere, it is put back when the car is more than 8 units above or below the tracker's point with a wheel on the ground (car +0x3a, +0x112, +0x1ea, +0x2c2) and the wait has run out; if that moved it, the follower stops there.
  2. Bit 3 of the record's byte +4 is cleared with bits 0 to 2 (and 0xf0).
  3. The fold search (FUN_004c6854) with a reach of 2 |dz| + |dx| + |dy| from the car to the point. If it finds nothing, the link does not start at a fork, and the car is more than 60 units (|dx| + |dy|) from the point, the car is measured along the flat step out of the point (or, when it is behind a link's last point, the step into it). A car past the step by more than the step's length and 30 units gets bit 3 (if it has not finished) and the tracker put back once the wait has run out; the follower stops. A further test of the squared distances against the double at 0x4e8ff5 (10,737,418,240,000) can never pass: both sides are 32-bit integers.
  4. Four votes on the car's way: its movement along each of the two steps into the tracker's point and the two out of it (FUN_004abdb4, FUN_004abd38; a step that does not exist counts two). A vote forward after a move forward moves the tracker forward (FUN_004abec4) at once. Four votes and a move forward along the sum of the steps also move it forward, putting it back first unless the wait was just set. Moving forward sets the record's word 0 to 0 (unless it is -1, a finished human) and +0xc to 1.
  5. Otherwise, a car not in reverse (gear +0x44a not 8) facing along the steps sets word 0 to 0 and runs the tracker backwards (FUN_004ac298). A car in reverse or facing back is going the wrong way: after a move forward the tracker is put back (unless the wait was just set); then word 0 counts up one, the tracker runs backwards, and +0xc becomes 0.

Ported in pod-ai (pod_ai::human_path::follow, with Tables::normalize, POD FUN_00423bc0), since it needs pod-ai's tables. The reference replays still mask bit 3 in a human's final record: pod-race cannot call pod-ai, and the follower runs every frame where the recordings hold ten positions a second (pod-beltane-medium, where the player's driver was stuck, has the bit set).

Not ported yet: where a car is put on the paths at the start (FUN_004af830, FUN_004af230), the order update's branch for a human in a view, the cars still racing driven to the line (FUN_0047831c, physics), the best-time tables, network and ghost modes, and the HUD. The computer cars' lane choice at "Troncon" is ported in pod-ai (pod_ai::lanes, POD FUN_004559dc), which pod-game calls on the core's Event::Section.

Open ​

  • The zone actions (FUN_0045a524), and FUN_004235f8, FUN_004231f4 after each step.
  • The results screen's function, and how the menus read the order table.
  • Measured: the number of physics steps before "GO!" (needs each frame, from inside the game), and two races from the same inputs.
  • The order update's branch for a human in a view; where the race loop calls the human's follower, and who reads the record's word 0 (the wrong-way frames) and bit 3.
  • A POD recording that reaches the results screen, to check the end and the results text on POD.
  • Each frame's dt and the order after each step, from inside the game (POD's recordings hold the chassis point, the tracker's point and each driver's path state ten times a second), so the full replay needs no rebuilt frames.

Prose CC0 1.0, code Apache 2.0. POD: Planet of Death is © Ubi Soft; none of its files are on this site.