Car physics
How POD 2.8's WINPOD.EXE moves a car, traced for Un-POD's exact core (docs/PORT.md, "Car physics") and ported in crates/pod-physics. Addresses are POD's (labels/pod/). The first edition, where this work began, differs as docs/versions.md records. Each finding is observed (read in code or data), measured (seen in the running game or under the emulator), inferred or unknown.
State (2026-10-24). The physics step FUN_0041b66c and every function it calls are ported. Each function matches the original's own calls bit for bit, captured under Unicorn on states recorded from the running game. The whole step matches every car step of a recorded race: 8,328 of 8,328 steps on Beltane, every word of every car, the step where the game put the player's car back on the track included (measured, tests/steps.rs). Followed on its own from its first recorded state, a car matches the recording until another car touches it: the player and one computer car for the whole race (1,041 steps), the others for 173 to 218. With car-to-car collisions, the port follows the whole race on its own: all eight cars, all 1,041 steps and the race's 21 contacts, word for word (race_followed_with_collisions). Damage is ported too, measured with the option forced on ("Damage"). On a second race, Canyon (damage on, jumps), all 18,600 car steps match and the port follows the whole race on its own: 2,325 steps, the jumps and 60 car contacts. The track's props are ported and measured on Canyon's props placed in the cars' path ("Props").
Recording: the step log
Ground truth for the physics needs every step, and the state at fixed points inside each step. A reader outside the process (run/vm/racerec.py, memrec) sees about one frame in eight, and its reads straddle frames. ptrace breakpoints at the step left every thread of the Wine process in tracing stops (measured: a deadlock within the first few steps, then a crashed game; docs/knowhow/ptrace-breakpoints-wine.md).
The step log instead adds a small code cave to the VM's copy of WINPOD.EXE (run/vm/steplog.s, patched in by run/vm/patch_steplog.py, started and stopped by run/vm/steplog.py). It writes three kinds of record to Z:\tmp\podsteps.bin:
| Record | Where | What |
|---|---|---|
| STEP | entry of the simulation step FUN_0049a89c | a self-describing table of memory blocks: the frame time, the racer counts and race modes, the countdown, the race block 0x4f8d44..0x4f9854 (beacons, race records, order, clock, catch-up records), the track's sector table, every race car and driver, the camera, the views, the AI's summed step time |
| CTRL | in FUN_0041b66c right after the controller returns (0x41b6f6) | that car (0x558), its driver (0xb0), the frame time block (its dt already scaled by FUN_00422be8), the race block, the chassis search's counter (0x4f3310) |
| PHYS | after the physics step returns to FUN_0049a89c (0x49a8d8) | the same five, after the physics |
The hooks save every register and redo the instructions they replace. The game runs at full speed. measure/steplog.py reads a log on the host. The patch goes on top of run/wine/patch_winpod.py's POD list, which also stops POD quitting on QEMU's CPU (an AMD family 6 it does not know). The hook code and the helpers sit in .reloc's slack from 0x74c240 (made executable), the block table in .idata's zero tail at 0x4e0900, the data at 0x4e0e00. In the VM, with the game in a race: sudo python3 steplog.py start, a whole-process snapshot (memscan.py snapall NAME), then steplog.py stop. STEPLOG_PID and MEMSCAN_PID pick the game when several run.
Measured in every log so far: the game thread's x87 control word is 0x027f. That is 53-bit precision and round to nearest, the Windows default for a new thread. It is not Watcom's 0x037f from fninit. So every x87 operation in the physics rounds exactly as an IEEE f64 operation does.
Measuring
measure/physics_emu.py loads a whole-process snapshot of the running game (memscan.py snapall, taken during the same race) into Unicorn. It then puts a recorded step's blocks over it and runs the original's code:
physics(s, racer):FUN_0041b66cfrom the CTRL record, the controller replaced by aretand the time scale replaced by the recorded scaled dt. Compared with the PHYS record: 8,328 of 8,328 car steps match onpod-beltane.time_scale(s, racer):FUN_00422be8with the race block as the racer before left it.sim_step(s): the whole simulation stepFUN_0049a89c, each controller replaced by a hook that loads its CTRL record. These two are not yet re-run on a POD recording.
Four things had to be stubbed or set up. Sound effects (FUN_004b8d64) are replaced by a ret. Watcom's memmove reloads ES from the stack, so the emulator needs a flat GDT. Racers before the one stepped have already moved this step, so time_scale takes their PHYS state. rand() (0x4c7f37) finds its seed in the game thread's C library data through the getter at [0x4ed728] (TlsGetValue), which the snapshot does not hold, so the emulator gives it a block of its own. Inside the step only smoke_emit (0x45d44c) draws from it. Every car step still matches, so the car does not depend on that draw (measured).
measure/physics_capture.py records every call of chosen functions inside those runs: the registers in and out, the bytes the call read before writing them, and the bytes it wrote. crates/pod-physics/tests/ rebuild each call's inputs from those bytes, run the port, and compare every byte the original wrote. The port must also leave untouched every byte the original left alone. measure/physics_cases.py exports the recorded steps and the snapshot for tests/steps.rs. All of it lives in derived/physics/, which git ignores.
./gameframe python measure/physics_capture.py target/pod/disc/WINPOD.EXE derived/physics/pod-beltane.mem.gz derived/physics/pod-beltane.steps.gz derived/physics/cases-pod --every 3 --max 3 41b66c 41af80./gameframe python measure/physics_cases.py derived/physics/pod-beltane.mem.gz derived/physics/pod-beltane.steps.gz derived/physics/pod-beltanecargo test --release -p pod-physicsArithmetic
Integer code multiplies 16.16 numbers with imul and keeps bits 16..47 (shrd eax, edx, 16). It divides with (a << 16) / b through idiv, or through a reciprocal table where |b| ≤ 32. Vector work goes through x87 routines at 0x4d41a0..0x4d4769. Each one loads integers, multiplies and adds in the FPU, then adds 1.5 × 2^68 (0x4ee0c0) and keeps the low word of the stored double. That word is the sum / 65536, rounded to nearest (observed).
| Routine | Port | What |
|---|---|---|
FUN_004d41a0 | math::dot16 | dot product, ((a0·b0 + M) + a1·b1) + a2·b2 |
FUN_004d41df | math::cross16 | cross product; z through × 1/65536 and fistp |
FUN_004d465d, FUN_004d4606 | math::scale16 | vector × scalar |
FUN_004d43ad | Tables::length16 | length through a square-root table, at a shift that keeps 13 to 15 bits |
0x4c5e6c, 0x4c5e64 | Tables::sin16, cos16 | sine table, angle brought into (−π, π] |
FUN_004c5ed8 | body::exp16 | e^x ≈ 1 + x + x²/2 |
The game builds three tables at start-up. Each matches a formula exactly in the running game (measured, tests/tables.rs):
- square roots at
[0x4ed434]: ⌊√i × 65536⌋, read for i in 0x2000..0x7fff; - reciprocals at
[0x4ed42c]: 2^24 / i truncated, i from −16384 to 16383; - sines at
0x72c704: round(sin(i × 8/65536) × 65536) for 0 to π/2.
The race car
0x558 bytes per racer at 0x630838 (docs/formats/cars.md for the parameters). The port keeps it as the original lays it out (car::Car, a byte image with named offsets). Five point records of 0xd8 bytes come first: wheels 0 to 3, then the chassis point at 0x360. Fields the physics uses, beyond those in cars.md (observed in the functions named; labels/fields.tsv):
| Offset | Field | Function |
|---|---|---|
| point +0x00 | position (world) | FUN_00419680 |
| point +0x0c | velocity | FUN_00419680 |
| point +0x18 | the surface under the point (0x62bc0c + 12 × n; its first word is the grip) | FUN_004174f8 |
| point +0x1c, +0x20 | its sector, and last step's (for skid marks) | FUN_00417988, FUN_0041a714 |
| point +0x24 | contact normal | FUN_004174f8 |
| point +0x30, +0x34, +0x3b | the touched face: mesh, number, quad (1) or triangle (0) | FUN_004174f8 |
| point +0x38, +0x39 | sliding, skidding | FUN_0041af80 |
| point +0x3a | touching | FUN_00417988 |
| point +0x3c | a lift factor for floor contact (0 in every car) | FUN_004168dc |
| point +0x5c | wheel roll angle | FUN_00415424 |
| point +0x60, +0x6c, +0x78 | the point's axes: rolling direction, axle, up | FUN_0041b3d4 |
| point +0x84 | impulse taken this step | FUN_00416284 |
| point +0x90 | rolling resistance | FUN_0041b66c |
| point +0xc0, +0xc4, +0xd0 | wall penetration depth, direction, bounce | FUN_004167ec |
| point +0xd4 | touching count: 1..16 down, −1..−16 off | FUN_0041b66c |
0x404 | cleared by the wheel spin (unknown use) | FUN_00418bd4 |
0x438 | wheel 1's sector as a number | FUN_00417988 |
0x444 | engine speed the wheels imply | FUN_004185ac |
0x448 | bit 2: automatic gearbox | FUN_0041b66c |
0x449 | gear change asked: 0x20 up, 0x40 down | FUN_00418f0c |
0x470 | gear change timer (15000/65536 s) | FUN_00418f0c |
0x474 .. 0x480 | shift points | FUN_004191c4 |
0x498 | the body's centre (the track point the AI and the order use) | FUN_00419ad0 |
0x4a4, 0x4b0, 0x4bc | forward, left and up axes | FUN_00419ad0 |
0x4c8 | velocity | FUN_00419ad0 |
0x4d4, 0x4e0 | force and torque carried into the next step (from collisions) | FUN_00419ad0 |
0x4ec | written by the controller (unknown) | controller |
0x4f0 | upside-down timer, two 16-bit halves | FUN_004159d8 |
0x4f4 | the car's sound table (cars.md, "After the objects") | sounds |
0x4f8, 0x4f9 | skid sound counter; sound state bits (1 skid, 2/4 engine rising/falling, 8 airborne, 0x10 scraping, 0x20 car contact, 0x40 crash) | FUN_0041b66c |
0x4fc | angular velocity | FUN_00419ad0 |
0x508 | inertia about the forward, left and up axes | FUN_00419ad0 |
0x52c | brake hold time | FUN_004189c4 |
0x530, 0x534 | air drag and downforce | FUN_00417c2c |
The step
FUN_0049a89c runs, for each racer in turn, the driver's step (FUN_0041b66c), then FUN_004159d8 and FUN_004155b8. After all cars come the zones and the car-to-car collisions. FUN_0041b66c runs in this order (observed):
| # | Function | Port | What |
|---|---|---|---|
| 1 | FUN_00422be8 | step::catch_up | time scale for a computer car; multiplies the frame time for this car's step |
| 2 | call [driver+0xc] | caller | the controller (pod-ai, pod-input) |
| 3 | 0x41b6f6 | step::step | input bits: driver +0x34's low 16 bits − 8; bit 4 forced during a held countdown |
| 4 | FUN_004191c4 | drive::automatic | automatic gearbox (car+0x448 bit 2) |
| 5 | FUN_00418f0c | drive::gears | gear changes |
| 6 | FUN_0041b3d4, FUN_00424704 | frames::wheel_axes, turn | the wheels' axes, front ones turned by the steering |
| 7 | FUN_00418010 | drive::engine | engine speed out of gear, or from an analog throttle |
| 8 | FUN_004185ac | drive::torque | drive torque from the torque curve |
| 9 | in step | step::step | torque shared by car+0x54c: 1 all wheels by radius, 2 front, else rear |
| 10 | FUN_004189c4 | drive::brakes | brake torques |
| 11 | in step (0x417be8) | step::step | rolling resistance −(+0x90 × spin) >> 16 |
| 12 | FUN_00417c2c | forces::air | drag and downforce ∝ (forward speed / 256)² |
| 13 | FUN_00417cec | forces::grip | each touching point's sideways grip |
| 14 | FUN_00417f48 | forces::wheel_drive | torque / radius along each wheel's rolling direction |
| 15 | in step | step::step | under 10 units/s, hold the car on a slope |
| 16 | FUN_004194c8 | suspension::springs | spring and damper, ±7.63 |
| 17 | in step | step::step | normal forces along each contact normal |
| 18 | FUN_0041af80 | tyres::tyres | tyre limits: slip curve, friction circle, skid flags |
| 19 | in step | step::step | forces scaled by how long the wheels have been down |
| 20 | FUN_00419ad0 | body::integrate | the body: velocity, position, spin, orientation; air behaviour |
| 21 | FUN_00416284 | body::impacts | impulses from walls (inside 20) |
| 22 | FUN_00419564 | suspension::substeps | wheel heights in sub-steps of at most 0x444 |
| 23 | FUN_00419680 | suspension::place_points | points placed from the body |
| 24 | FUN_00417988 | collide::track | the track: sectors, floor faces, walls |
| 25 | FUN_00418bd4 | drive::wheel_spin | wheel spin |
| 26 | FUN_0041a714 | step::step | skid marks and smoke (only each wheel's last sector is car state) |
| 27 | FUN_0041988c | suspension::travel | wheel travel limits, the body pushed when a touching wheel bottoms out |
| 28 | FUN_00416030 | body::push_out | out of the walls |
| 29 | in step | step::step | touching counters |
| 30 | FUN_0041cf2c | pod_race | beacon test (the step returns the chassis point's move) |
| 31 | FUN_004227bc | step::stuck | the stuck timer at driver +0x7c, for computer drivers |
| 32 | end of step | step::sound_state | sound state in +0x4f8, +0x4f9; sound slots as events |
After the step, in FUN_0049a89c: FUN_004159d8 (step::upside_down) and the wheels' roll in FUN_004155b8 → FUN_00415424 (step::wheel_roll).
Engine and gears
The engine speed (car+0x440, rad/s) rises by rev_up × dt with the throttle out of gear, and falls by rev_down × dt / 2 without it. It stays between 0 and engine_max − 1. In gear it follows the wheels: FUN_004185ac sets +0x444 to the front wheels' mean spin × the gear's ratio. The torque is the curve at that speed in rpm / 256 (× 0x98c9e, 60/2π, then >> 24). It is scaled by a smooth function of how far the engine runs ahead of the wheels, by the ratio, by 0.70 (0xb3b0) for a human driver, and by 4 against the gear (observed). A gear change takes 15000/65536 s. Up from neutral re-maps the engine speed through a two-piece line (in FUN_00418f0c). Down may skip gears while the engine stays under +0x478 (observed).
Tyres
FUN_0041af80 splits each touching point's force into a rolling part and a side part (contact normal × rolling direction). The side part is held to a limit. Under 0.92 units/s the limit is the load × traction grip × the surface's grip. Above that it comes from the slip ratio (velocity · +0x6c / speed) through a curve at 0x4e9a78: peak 0.342, offset 0.523, curvature 8.55 below the peak and 2.37 above, the same for both axles. That curve is scaled by a quarter of the car's weight. The rolling part is held to a quarter of the weight × side grip × surface grip, less once (that limit / 256) × the side limit passes 0x1dcd65 (observed). Past the side limit the point slides: +0x39 when |slip| > 0x4240, +0x38 when > 0x9470.
At low speed the function tests the skid flags against a slip ratio left on the stack by an earlier point. For the first such point, that stack slot holds what FUN_00417cec stored there, the address of the step's force array (inferred from the stack frames). The port uses a value above both thresholds (tyres::STALE_RATIO). No recorded call has reached this path yet.
The body
FUN_00419ad0 sums the touching points' forces with the outside force (air, plus weight on z) and the force carried from collisions. Each point's force adds a torque r × F about the centre, r reaching to the contact (radius below the point's up axis). Wall impacts (FUN_00416284) add impulses along the walls' directions. Their size is (−v·n − 0.264) × mass / dt × the point's bounce, shared among the points hit (halved for two). A single point also gets friction up to 0.15 of the impulse. Then 15/16 of all the yaw torque is taken away again (the original subtracts 0xf000/65536 of it). Velocity integrates by force × dt / mass. Position moves by the mean of the old and new velocity × dt × step_scale (1.1942). Spin integrates per axis by the inertia at +0x508 and is held to 2.29 rad/s on its largest axis.
When every wheel has been off the ground for more than two steps, the spin dies away (e^(−3.5 dt)). After four steps at speed the nose turns into the velocity (e^(−1.03 dt)). Above 30 units/s the front wheels' steering moves the car sideways in the air (0x5a0000 × dt × steer). Nearly stopped with three wheels down and pushed into a wall, the steering turns the car on the spot (observed; the airborne branches are not yet exercised by a recording).
The track
The physics reads the track as the game keeps it in memory, at the original's addresses, through World (the snapshot in the tests):
- sectors:
[0x57bf74]records of0x3cbytes from[0x57bf78]; +0 the mesh, +0x10 and +0x1c the bounds, +0x28 the neighbour count (−1 out of play), +0x2c the neighbour list; - a mesh: triangle count (i16 +2), quad count (i16 +4), vertices (+0xc), triangles (+0x14,
0x20bytes: normal, corners at +0x14, flags +0x1c, surface +0x1d), quads (+0x18,0x3cbytes: normal, the two halves' normals at +0xc and +0x18, corners at +0x2c, flags +0x36, surface +0x37); - face flags: bit 3 ground, bit 5 wall;
- surfaces: 12-byte records at
0x62bc0c, the grip first.
Each step, a point that touched a face first tests the same face again. Otherwise it searches its sector and the sector's neighbours. Every fourth call of the racer's counter it searches all sectors instead. It looks there for the first ground face it is over (FUN_00424ba4 for quads, by the side of the A–C diagonal, and FUN_00424ee4 for triangles). It counts as on the face between 2.29 below and its radius above. A floor more than 0.195 off the car's up axis lifts the point into the suspension. A wall, or anything under the chassis point, pushes it out with a depth and a bounce. The chassis point searches only when FUN_00417884 says so: every 17th call (the counter 0x4f3310), with fewer than four wheels down, or when a wheel's normal points away from the car's up axis. Then come the walls of every sector found (FUN_00416be0). With two or three wheels down, the free wheels are also tested where they would sit on the touching wheels' planes.
Putting a car back
FUN_00415730 (putback::put_back) puts a car back on the track. POD calls it from the track pass FUN_00417988 when no point found anything and the car is 10 below wheel 0's sector, just before the pass returns (0x417bd8). It also calls it from FUN_004159d8 after 2.5 s upside down (0x415a82). Observed in the disassembly, measured on pod-beltane step 870, where the player's car drove up a wall and fell through:
- Where. The middle of the face wheel 0 last touched (
+0x30mesh,+0x34face,+0x3bquad). For a triangle, the three corners are summed and scaled by0x5555(FUN_004d4606); for a quad, the four are summed and shifted right by 2. - Heading. Along the race path when the track has one (
[0x543254]not 0), from the driver's tracker (+0x2clink,+0x30point): the next point minus the tracker's (FUN_004abd38). Past a link's last point, the next point (FUN_004abae8) is the end node's position when more than one link leaves it, the first point of its only link, or 0 (only the middle of a link was reached). Without paths, the car's forward axis. - The car. The upside-down timer cleared,
car_reset(FUN_0041c814): velocity, spin, carried force and torque, engine and gearbox,+0x4ec, the brake hold, each point's skid flags, steering, roll, velocity, spin and impulse, the wheels at rest height; engine limit 1000 rad/s, shift points at 0.92, 0.46, 0.10 and 0.20 of it. ThenFUN_00423538places it (afterFUN_004231b4clears the pose and callscar_resetagain): the body at the point raised by 1, forward the heading made level and of length 1, up the world's z, left up × forward, the points placed (FUN_00419680), each point's sector the first near it (FUN_0045ca78). - Left to the caller (
step::Out::put_back): the beacon test with the chassis point's move (unless the driver's flag bit 2), the driver's tracker found again (FUN_004aa8a0), a computer driver's lane target (FUN_00455ed0), the racer's record at the race block's+0x40 + 0xbc × racercleared when not negative, and the chase camera reset (FUN_0049b984) for a car a human view shows.
Car against car
After every car's step, the simulation step runs FUN_0042196c (cars::collisions): each pair of racers once, in order (FUN_0042127c, cars::pair). Observed in the disassembly, measured under Unicorn over the whole of pod-beltane (physics_capture.py --sim):
- The hulls. Each car's drawn object (the driver's
+4) has a hull at+0x44: half sizes+0, radius+0xc, centre+0x10. The object's frame is the car's:FUN_004155b8sets its matrix (+0x48) to the forward, left and up axes as columns and its position (+0x6c) to the chassis point less up × the hull's z half size (cars::pose). - The test
FUN_00420548(cars::hull_test). The positions must lie within the summed radii on each axis, and the centres (FUN_00423fd8) within that distance. Then, along the normal between the centres (FUN_00423c64, a unit vector through a table of squares), the two boxes' reach Σ |n · axis| × half size must pass the distance. The overlap is reach − distance. The contact is the midpoint of the boxes' facing corners, with half their x, y and z distances as its spread. - The parts.
FUN_004209c8marks the object's parts near the contact (the part list at+4, offsets+0x98, hulls+0x34). Wheels 0 to 3 are parts 6 to 9, with offsets from the car. A contact counts only when both cars have a part near it. - The impulse.
FUN_00420adc: closing speed along the normal × ((2 ma mb / (ma + mb)) >> 8) / (dt << 8), masses at+0x520, 0 when the cars part.FUN_00420b4cadds it to the carried force along the car's own axis nearest the normal, and the matching torque about the centre. - The push. The second car is pushed by half the overlap along the normal and the first back, each with its points placed again. Placing the drawn objects again turns the wheels by one more frame of spin (
FUN_00415424). - Sound. An impulse over 0x2000 sets the contact bit
+0x4f90x20 in both cars when neither has it, and the first plays its slot 7. A weaker one clears it when both have it. - Left to the caller: the beacon test after each push, the damage record, and the moving objects' collisions (
FUN_004215a8,FUN_00421750,FUN_00454a98; none on Beltane, not ported).
Damage
After each car's impulse in a contact, and after each wall impact in the car's step (FUN_00416284 at 0x4164a4 and 0x4166de: the impact's parts and a quarter of its impulse, body::Impact), FUN_00420e54 (damage::hit) damages that car when the option [0x63334c] is on and [0x633314] is 0. The port applies a step's impact damage after the body's motion (step::step_damaged): nothing in between reads what the damage changes. Measured: all ten wall-impact damage calls in the re-run came from the one-point branch (0x4166de, return 0x4166e3), with exactly the parts and strengths the port's impacts report (impact_damage_calls_match_body_impacts). The several-point branch (0x4164a4) ran 8 times on Canyon, beside 146 one-point calls, all matching; all 274 damage calls of that race (damage on, mode 1) match. It was off in pod-beltane, so it is measured by running that race's contacts under Unicorn with the option set to 1 and to 2, and with the parts' health set low (physics_capture.py --poke). Observed in the disassembly:
- Health. The first six parts of the drawn object carry a health (
+0x3c, 2,000,000 at the start). When half the impulse reaches 50,000, it is shared byidivamong the parts near the contact: those of the six in the contact's mask (mode 2), or all six (mode 1). - A part that runs out. Without a macro chain (
+0x38) it stays at 0. With one, the chain runs. Each element is (macro, argument, next), and the only macro used isFUN_004545a8, which turns the part into its template, keeping its offsets and placement. So the part takes the damaged part's health, its next chain and its hull (+0x34, which the parts test reads next time). Its health becomes the new part's plus how far it fell below zero, at least 0. - The wheels. A broken part hurts wheels (
FUN_00415318, by part bit: 0x10 wheel 0, 4 wheels 0 and 1, 1 wheel 1, 0x20 wheel 2, 8 wheels 2 and 3, 2 wheel 3). One wheel (FUN_00415270) gets grips × 0.95 and rolling resistance + radius² × 15.26. An axle (FUN_004152ac) gets both wheels' grips × 0.97 and each wheel's rolling resistance + radius² × 7.63. - The damage record for the race display (
FUN_0047ed1c,damage::Record), per racer at0x5a8e34 + 0xb8 × racer: an 8-byte entry per part hit (FUN_0047ea74: its bit, the icon from the table at[0x5a8e30]by level and by front, middle or rear, the level up to 2 each time it breaks, its screen place from0x5a9c98). After a break, the front (bits 0x30) and rear (bits 3) levels are summed (0x5a9cf4,0x5a9cf8). Past 3, that half of the display's car picture swaps to its damaged image (FUN_0047ec6c, throughFUN_004545a8). Drawing them is the display's. - Left to the caller: the rest of a replaced part (its mesh), which is the renderer's.
Props
The props of a track's .env (cones, drums, boxes, signs; 25 of POD's 33 tracks have some, Beltane none) are 0xd0-byte object records at [0x62129c], [0x621298] of them, with a hit flag each at 0x6212a0 (props.rs). Fields: +0x1c mass, +0x20 friction, +0x24 inertia per axis, +0x30 contact points (records of 0x40 bytes at +0xb8: radius, offset, place, mass, force, push, sector, flags), +0x34 hull, +0x38 macro chain, +0x3c health, +0x40 matrix, +0x64 and +0x70 the point and face it rests on, +0x74 force, +0x80 torque, +0x8c velocity, +0x98 position, +0xa4 spin. Observed in the disassembly:
- A car against a prop (
FUN_004215a8, after the car pairs inFUN_0042196c). The hull test runs with the prop as the second hull, then the car's parts test and the impulse. The prop is pushed by half the overlap and given the impulse (FUN_004214b4: force, torque, half the impulse off its health, its macro chain when that runs out). The car is pushed back, then given its impulse, its damage, and its drawn object placed again. - A hit prop then pushes each other prop it touches (
FUN_00421750, a quarter of the impulse). The two hand on what they rest on: the other's face, or, at a slant of more than 0.885, the contact's normal kept in a record at0x4f9440. Then the prop moves (FUN_00454a98,props::motion). - Its motion. It takes the faces its hull touches in the sectors near it (
FUN_004547a4): a ground plane and a wall plane. Gravity pulls each point down (its mass × 0x2d). A point below a plane is pushed out by depth × mass / dt². The pushes give force and torque, and on the ground the force along it is cancelled while it stays small. Then come the velocity, the position (by the half step's velocity), the spin per axis by the inertia, and the axes turned and squared again. On the ground the spin dies (× 0.916 a step), and a slow prop resting on three points or more settles: its hit flag is cleared. A prop that falls 8 below its sector with no neighbour holding it is removed (FUN_004adab0).
Measured: no prop was hit in the step-logged Canyon race, so four of its props were placed on racer 0's path under Unicorn (physics_capture.py --poke), in the air and on the road, at eight points of the race. Every call matched (tests/props.rs): 33,280 car-against-prop tests (77 hits), 77 impulses, 1,463 prop pairs, 77 motions (55 on the ground), and 208 whole collision passes with cars, props and damage together. The rest of the props' code was reached by setting props' hit flags on Canyon: the stack of boxes knocked into each other (78 touching pairs, handing on what they rest on), a box set against a wall (12 points touching it), and a box set 20 units below the track, removed from the list each step (FUN_004adab0; the list closes up behind it and the pass skips the next prop, as in the original). All of it matches: 143 motions, 2,687 prop pairs, 214 whole passes. One deviation: with no ground face, the original copies a rest point its plane search never set (stack left from earlier calls). The port leaves it 0; it is never used without a face.
Catch-up
FUN_00422be8 scales a computer car's frame time so it keeps near the racer its record targets. The record is four words in the block at 0x4f97c8, the driver's +0x90. The target comes through the function at 0x4f984c, FUN_00422918, the racer by number. The distance is measured along the race paths, the link values at the driver's +0x2c/+0x30, and taken straight when within 50. The scale moves by 800 / weight towards the target's place. Within 50 it averages in the speed ratio that would close the gap. It stays between base + 0.90 and base + 1.008, where base (0x4f9848) is 0, 3000 or 6000 by the difficulty (0x633334) and is set during the countdown. The bottom also drops by 400 per place behind first (observed; measured under Unicorn, 2,008 calls).
Ported, and how it is checked
| Item | Check | Cases (pod-beltane) |
|---|---|---|
| x87 helpers, tables | tests/helpers.rs, tests/tables.rs | 8,314 .. 8,328 calls each; every table entry |
| drivetrain (5 functions), wheel spin | tests/drive.rs | 2,776 calls each |
| forces, vector helpers | tests/forces.rs | 642 .. 8,037 |
| suspension (4) | tests/suspension.rs | 2,776 .. 8,328 |
| tyres | tests/tyres.rs | 2,776 (187 sliding points) |
| wheel axes, steering turn | tests/frames.rs | 2,776, 1,826 |
| body, impacts, push-out | tests/body.rs | 2,776 each (3 with impulses) |
| track contact (8 functions) | tests/collide.rs | 10 .. 8,307; 8 wall passes with pushes; one put back |
| putting a car back (5 functions) | tests/putback.rs | 1 or 2 calls each (step 870) |
| car against car (7 functions) | tests/cars.rs | hull test 29,148 calls (24 overlaps); 21 contacts; 1,041 collision passes; object frames 8,370 |
| damage (5 functions) | tests/damage.rs | 52 calls in each mode, 12 parts broken; 29,148 pairs and 8,328 body steps with damage on |
| the whole step | tests/steps.rs | 8,328 of 8,328 car steps, one put back |
| the whole race | tests/steps.rs | Beltane: 8 cars × 1,041 steps, 21 contacts; Canyon: 8 × 2,325 steps, jumps, 60 contacts; carried by the port alone |
| props (7 functions) | tests/props.rs | 33,280 car-prop tests (77 hits), 77 motions, 208 passes, on Canyon's props placed in the cars' path |
| a car on its own | tests/steps.rs | player 1,041 of 1,041 steps; computer cars 173 .. 1,041 |
Captured with --every 3 --max 3 from the race's 1,041 steps (derived/physics/cases-pod/, keyed by POD's addresses). POD no longer calls the first edition's triple-product helper (its face tests use the dot and cross products), so the port has none.
Not ported yet
- A replaced part's mesh (the renderer's), and drawing the damage record.
- Putting a car back, its parts outside the car ("Putting a car back" above), and a point with no sector searching every sector (
FUN_0045ccd8), which no recording has reached. - The zones
FUN_00456dd4: they changed no car in the race recorded. - Skid marks and smoke
FUN_0041a714(drawing; theirrand()draws reach nothing else), playing soundsFUN_004b8d64(the step reports the slots), the drawn objects inFUN_004155b8. - A world from the disc. Done for the track's sectors, meshes, faces and surfaces (
crates/pod-world,docs/systems/world.md): every field the physics reads matches a snapshot of POD's Beltane. The race paths for putting a car back are not laid out yet.
Open questions
- Which branches the recordings have not exercised: airborne behaviour, analog throttle and brake, the low-speed stale slip ratio. A track with jumps (Canyon) and a joystick recording would cover them.
car+0x404andcar+0x4ec.- Driver
+0x30and+0x7cchange between the physics and the next step, outside the simulation step. Observed by pod-ai: the countdownFUN_0047b98csets every car's+0x7cto 0 each frame until "GO!", andFUN_004c688cmoves the human's path point+0x30once a frame. The stuck timer+0x7cis shared with the computer controllerFUN_004221d8, which counts a negative value up in reverse and puts the car back at 0 (docs/systems/controllers.md). - pod-race measured the gate test (
FUN_004d41a0) under0x037f; the game runs0x027f(above). The results agree on 20,000 cases there; a case where 53 and 64 bits round differently would show it.