Skip to content

The main loop and loading a race ​

How WINPOD.EXE goes from the menus to a race and runs it frame by frame, traced for Un-POD's game app (apps/unpod, docs/PORT.md, "Game app"). The rules inside the frame (the clock, the countdown, laps, the end) are in race.md; this page is the frame around them, and the interface Un-POD's core gives the app. 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.

From the menus to the race ​

StepFunctionWhat (observed unless marked)
Start-upFUN_004a05a8 → FUN_0049c800Once per run: the dictionary (load_dictionary), the circuit list (load_circuit_list), the car list (read_car_list), the macro actions (register_macro_actions), the menus (load_menus), and FUN_0049bcd0, which loads every car of the list (load_car_file, DAT_006174c0 of them, each into a slot at 0x6174ec + car × 0x63c)
Race chosenthe menu action at 0x436658 → FUN_004a0414(1, 1)Finds the track's row in CIRCUITS.BIN (0x61d5ec, 0x8c bytes each; Test_lev is a special name), then load_track_select and race_session
Load the trackload_track_select (FUN_0049b6a0)The view setting (FUN_0049cdbc, FUN_0049b3fc → FUN_0049cebc), then load_track_file (FUN_0046ef04, .BL3 or .BL5 by display mode), the HUD font (load_font_file), FUN_0049ba20 (the palette shades, then for each human view a call to FUN_004613dc), FUN_00493570, FUN_0042a4d8
RacersFUN_0049c13c, from race_setupFor each racer (humans first, DAT_0061d5bc + DAT_0061d5c0, plus DAT_0061d5c4 extra objects): car_to_racer copies the car slot into the race car (0x61aad0 + racer × 0x558); the driver (0x61f870 + racer × 0xb0) gets its car, object (0x61a6d0 + racer × 0x80) and a path block (0x5938b8 + racer × 0xb8); FUN_0045a150; then the controller by driver flags +0x38: bit 0 a human (driver_set_human, or driver_set_ai in a demo), bit 1 a computer car (driver_set_ai), bit 3 FUN_004b8b48, none fatal_error
The gridFUN_0049c13cEach car stands on its grid slot's start record (read_track_race, 0x24 bytes each); inferred from race.md ("The start") and the POD recording, which shows the eight cars in two rows before "4" (derived/race/pod-beltane-easy.jsonl, first sample)
Race setuprace_setup (FUN_0049f75c)FUN_00475820, FUN_0049c13c, race_reset (FUN_0042bec0), two seconds of settling, the countdown's phase 1, "Depart" (race.md, "The start")
The sessionrace_session (FUN_0049fba4)Reads the camera and view settings from Ubi.ini, runs the race loop FUN_0049d4c0, writes the settings back, runs "FinCourse", then the end of race FUN_00479114 (race.md, "The end")

The cars come from the race's entries in the menus (0x61eb8c, 0xdc each: the car at +0xc0, the kind at +0xd0, the grid slot at +0xcc); the opponents' names and car numbers are in the track (read_track_opponents, one list per difficulty; observed in the reader, which field the menus copy is inferred).

One frame ​

FUN_0049d4c0 first sets up the network cars (FUN_004b8d04, FUN_00487a9c), measures one frame (frame_clock), runs the keys and cameras once (FUN_00474dc0, FUN_0049de7c, FUN_00486868), picks the demo's cameras, and then loops while DAT_00502750 (race over), DAT_00620041 (quit) and DAT_0060b2e4 are all 0. Each pass, in this order (observed, 0x49d5e8 to 0x49d8f0):

#WhatFunction
1Keys typed as words (cheats, debug toggles)FUN_00471d5c
2Network messages (network race only)FUN_00486bd0
3The simulation steps with the frame time measured at the end of the last pass: one step, or n equal steps when the frame is longer than 3 × DAT_0060bdfc; none while a time trial counts down. Each step: every car's controller and physics, the zones, the collisions, the camerasrace_step (FUN_0049e63c)
4The race clock DAT_00502740 += DAT_0060be0c
5For each human racer: the wrong-way check, which sets bit 3 of the record's byte +4 (observed, 0x4d09bc; measured: set for the player left standing on the grid in pod-beltane-medium); next to the "Wrong-Way ON/OFF" macro handlers FUN_004d05b0, FUN_004d05e4FUN_004d09bc
6Sound: the engines and effects of the frameFUN_004c00f0
7Keys: camera changes, the pause and quit keys, restart (race_setup again)FUN_0049de7c, FUN_00486868
8The next frame time: timeGetTime since the last call, × 65, clampedframe_clock (FUN_004ae894)
9Drawing: begin (DAT_006924c0), the world for each view (FUN_0049ece8), end (DAT_006924c4)the renderer's pointers
10The HUD, per view: the countdown while the phase is not 0 (race_countdown), FUN_0047c470, then race_hud (FUN_0047f56c), which also runs the end timer
11Messages on screen, the frame shown, network, the pause menu, the joystick and keyboard state for the next frameFUN_00482a50, FUN_004747c8, FUN_004a4f28, FUN_0048774c, FUN_0042218c, FUN_00422af8, FUN_00422bb0, FUN_00434bf0

So the frame time a pass steps with was measured during the pass before it, between its sound and its drawing (observed: frame_clock at 0x49d6d4, after the steps). The input the controllers read in step 3 was read at the end of the pass before (step 11, inferred from the names of the DirectInput calls in FUN_0042218c; the controllers' session traces it).

When the loop ends, FUN_0049d4c0 stops each car's sound (FUN_004bf090) and returns; the session goes on to "FinCourse" and the end of race.

What runs in a simulation step ​

From race.md ("The frame and its clock"): for each car the driver's +8 function (car_step, FUN_00428b98), which calls the controller at +0xc first (0x428c1f) and the beacon test after the physics; FUN_004235f8 and FUN_004231f4; the extra objects; the zones (race_zones); car-to-car collisions (FUN_0042ed74); a replay hook; the chase camera of each view (FUN_0042b6c0, camera at 0x60bba0). The cameras are part of the step, not the drawing.

Un-POD's frame ​

apps/unpod runs the same order with the core's crates. The core decides; the app only draws, plays and reads devices (AGENTS.md, working rule 5):

OriginalUn-POD
1, 7, 11: keysthe app reads the keyboard (winit) at the start of a pass and sends the key-map slots held as a Command::Input (pod_game::Controls, the slots pod_ai::human::control reads; docs/systems/controllers.md); the probe sends the same commands
3: the stepsGame::frame → for each step Cars::step (controllers and physics) then pod_race::Race::step (path trackers, beacons)
4, 8, 10: the clocks, countdown, end timerpod_race::Race::end_frame inside Game::frame
6: soundthe app, from Game::take_events and each car's pose (apps/unpod/src/audio.rs, pod-sound)
9, 10: drawing and HUDthe app, from Game::snapshot: the track and cars from the chase camera, the HUD, the results screen

The core's interface ​

crates/pod-game is the exact core assembled: no drawing, sound, devices or files. The app (or a test, or the probe) drives it with commands and reads it with queries. Agreed with the physics, controllers, front end and sound sessions on 2026-10-24 (shared notes, unpod-core-interface.md); changes go through this section.

rust
// pod_game::Setup is what the menus hand over: track, level, laps, flags, racers (car, name, human).
let mut game = pod_game::Game::new(race, cars);       // a pod_race::Race (Settings + Course), and the cars
game.command(Command::Input { racer: 0, controls }); // queued, applied at the next pass
game.frame(ms, wall_ms);                            // one pass of FUN_0049d4c0
let s = game.snapshot();                            // clock, phase, order, cars, records
for e in game.take_events() { /* countdown, GO, laps, finish: HUD and sound */ }
let results = game.results();                       // once the loop has ended

ms is the milliseconds since the last pass (what timeGetTime gives frame_clock) and wall_ms the timeGetTime value itself (the countdown counts on it). A test passes made-up values; the app passes the real clock; a replay passes the recorded frame times.

What moves the cars is a Cars (crates/pod-game/src/cars.rs), one call per car step:

MethodOriginal
settle(unit)race_setup's two seconds of settling
frame_time(&race)Some(dt) to impose a frame time (a replay); None for the measured one
step(dt, hold, inputs, &race) -> Vec<Option<Move>>for each car the controller and car_step; hold while the countdown runs (every car's +0x7c at 0); a Move is the chassis point before and after, and the +0x498 point for the path tracker
retrack(&mut race)after the cars' step: the trackers of the cars put back on the track placed again (FUN_004aa8a0)
section(racer, node, &mut race)a computer car's lane at a "Troncon" gate (FUN_004593fc), when pod_race reports Event::Section
pose(racer) -> Poseposition, heading, speed, gear and engine speed for drawing, the HUD and sound; each body part's damage set
props() -> Vec<PropPose>the props the physics moves, where it leaves them, for drawing

The cars come from a recording (pod_game::replay, POD's derived/race/pod-*.jsonl), or live (pod_game::live, unpod --live), started from the disc (pod_game::start, docs/systems/world.md). For each racer in order, as the car step FUN_0041b66c does:

  1. the time scale (FUN_00422be8, catch-up), which scales the step's time for this car;
  2. its controller with that time (pod_ai::human for the player, from the key map's slots; pod_ai::computer for the others);
  3. the car step (pod_physics::step, with the damage when it is on), whose chassis move goes to the race rules at once;
  4. with the frame's time, the upside-down timer and its put-back (FUN_004159d8) and the wheels' roll (FUN_004155b8).

Then the collision pass (FUN_0042196c): every pair of cars, then every car against the track's props and the props hit moving. The "Troncon" lanes go through pod_ai::computer::troncon. The countdown holds every car's +0x7c (the stuck timer) at 0, as race_countdown does.

The driver's +0x7c is one field. The physics counts it as the stuck timer (FUN_004227bc): after 2 s stuck it sets −2 s. The computer controller then reverses the car and counts the field back up to 0. The port keeps it in two structures, so it is copied both ways around the car step: before it into the physics' driver, after it into the controller's. Copied one way only, the physics' −2 s came back every step, and a stuck computer car reversed for ever (measured: two cars stuck on Beltane, 2026-10-24).

Additions (unpod): --autopilot drives the player's car with the computer controller. The race still counts it as the human, so the lane is chosen whenever its race tracker enters a link. --damage sets the damage option. A one-lap race on Beltane goes from the start to the results with every car finished (apps/unpod/tests/app.rs). With the trackers placed again after a put-back (retrack), the three-lap race finishes too. Maverick wins it in 5'36"16, against 5'34"96 for the winner of POD's own race on the same track and difficulty (measured, 2026-10-24). The autopilot is slow on the last stretch before the line (its third lap took 23 minutes).

The app around the core ​

apps/unpod hosts the core (apps/unpod/README.md):

  • Pacing. The window runs, at each redraw, the passes the race clock owes: real time × 65 (frame_clock's factor) for the window's elapsed time, each pass standing for 16 ms of timeGetTime. Pacing on the race clock, not on passes, matters for a replay: its rebuilt frames are about a tenth of a second each (one per recorded sample), so one pass a redraw would play the race six times too fast.
  • Drawing. The track, its props and the eight cars from a chase camera, in software with a depth buffer at 640 pixels across (the original's width), shown in the window as one texture and written as PNG headless. Measured in this session: egui meshes in painter's order (the viewer's way) put the big untextured walls of Beltane's tunnels in front of the road. The game's own renderer is pod-render's subject (docs/PORT.md).
  • The cars on screen. Racer 0 is the player in GAMMA (the reference race's car); racers 1 to 7 are the track's opponents for the level (read_track_opponents), each opponent's number a row of SCRIPTS/VOITURES.BIN (inferred: GAMMA is row 0 and Beltane's seven opponents use rows 1 to 7). A car's front is its +x axis: the steered wheels 0 and 2 (car_steering) stand there (observed in the car file).
  • The front end (--menus, natively): pod-ui's menus drawn at 640 × 480 and shown as the window's texture, keys passed as DirectInput scan codes and the mouse in its pixels. Its effects: StartRace becomes a pod_game::Setup and starts a race (the reference race with the chosen car until the physics lands, Deviation: in main.rs); Esc in a race opens the pause menu; Resume, Restart, Abandon and Quit as named.
  • Sound (pod-sound, natively): one engine for the whole run, as the game starts it once (FUN_004bf9b0, FUN_0045ee70: FIX.MEG, then the 17 track banks in CIRCUITS.BIN order), shared by the menus (their sounds) and each race (its track's bank enabled, FUN_0045ef0c); the CD music read from the disc image, started with the race (sound 20 with the countdown's first phase, as measured in the emulated PC; none in the menus). After each pass the countdown's events (object 1: 0x1f to 0x1c, 4, then 0x20 for "GO!") and, at the first pass, each car's engine (event 36 on object 0x10000000 | car, FUN_0045ef9c); then the frame's update (FUN_004c00f0) with the camera as listener; the blocks go to the machine's output a few ahead.
  • The HUD and the results screen in the glyphs of HABILAGE.BP3 (typo027 and typo025), with Un-POD's own layout until race_hud is traced.

The probe ​

An addition for agents and tests (Un-Banished has one): a command a line on stdin or a socket on 127.0.0.1, a JSON line back. state, cars, car N, results and events read the core's queries; input, pause, resume send its commands; run N and finish run passes; shot draws a frame. --vanilla switches it off. The commands are listed in apps/unpod/README.md.

Open ​

  • Where the grid slot's start record puts each car (FUN_0049c13c → FUN_0045a150 → FUN_00422fb8), and the settling's physics.
  • The keys in steps 1, 7 and 11 in detail (FUN_0049de7c, FUN_0042218c): the controllers' session.
  • FUN_0047c470 (no calls; inferred the HUD's per-view set-up), the HUD's fields (speed, gear, position, lap, times) and their fonts.
  • The order of the rest of a pass under the emulator (rows 5 to 11).

Measured (2026-10-24, crates/pod-game/tests/clock.rs, the step logs pod-beltane, pod-beltane-easy, pod-beltane-medium from inside POD's WINPOD.EXE): rows 3, 4 and 8. A pass is the run of steps that see one value of the frame-time sum 0x621b74; its frame time is the sum's change from the pass before. In all 344 whole passes the step count and each step's dt are pod_race::clock::steps(frame, unit). Every frame time lies in frame_clock's clamp (in the loaded VM most were the 30000 maximum, 11 steps a pass). The race clock gains the whole frame time over each pass (338 passes). At "GO!" it is 0 instead: the countdown (row 10) runs after the clock's add (row 4). The steps run during the countdown too (phase 2).

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