Cars (VOITURES/*.BV3, *.BV4)
Nine cars, each in two files: .BV3 with 8-bit textures and .BV4 with 16-bit ones. The models are the same in both (measured: the objects end at stream offsets exactly 0x10300 apart, the difference in texture size).
Reader: crates/pod-formats/src/car.rs, with carparams.rs for the parameters (prototypes decoders/car.py, decoders/carparams.py). Viewer: the 3D car view in apps/viewer, with the parameter table beside the model; pod-viewer info NAME prints the same table, and pod-viewer render NAME OUT.png draws the car. State: decoded in all 18 files (measured, 2026-10-23): the walk follows load_car_file to its last read, every parameter byte is a named field or zero, and the parameters write back byte-identical through the container. Modded (2026-10-23, docs/modding.md): a changed grip setting (the third garage setting) shows on the car screen and changes the race car's grip. A changed side_grip/traction_grip loads into the car slot but does not reach the race: the garage setup replaces it (setup_defaults, FUN_004436c8; car_from_setup, FUN_00442f04).
The game's reader is load_car_file (FUN_0046ae1c). It opens the file with key 0x0F2E (container.md) and reads, in order (observed in the disassembly; the 0x24-byte and light reads are not in Ghidra's pseudocode, which drops the register arguments):
| Step | Read | Function |
|---|---|---|
| 1 | name: the car's name (arc) | load_car_file |
| 2 | 0x558 bytes: the parameters (below) | load_car_file |
| 3 | u32 n (64 in every car), n × u32: the torque curve | load_car_file |
| 4 | name, texture block (textures.md), u32 | load_car_parts (FUN_0046aabc) |
| 5 | 18 objects, each followed by 0x1C bytes; 4 objects, each followed by 0x1C bytes; 4 objects | load_car_parts |
| 6 | two animated objects: name, u32, object, 0x34 bytes, u32 frames (0 in every car), frames × 64 bytes | read_animated_object (FUN_0046a780) |
| 7 | 0x24 bytes: a word the game overwrites, then 16 × u16 | load_car_file |
| 8 | 5 × u32: the garage settings | load_car_file |
| 9 | a light: u32 kind, 0x30 bytes, 4 × u32 | load_car_file, FUN_00434e50 |
After step 9 the stream holds 2,048 to 13,741 more bytes, mostly not zero. The game never reads them (observed: load_car_file closes the file next): inferred, leftover memory the original tool wrote out as block padding. A writer must keep them to stay byte-identical.
Unlike the tracks, the car's animated objects have no texture block (measured: the object starts right after the u32; observed: FUN_0046a780 reads a block only on one branch of a test of ECX after the name). What the two objects are is open.
The .BV3 texture block carries the car's palette in its palette slot (measured, 2026-10-24, in all of POD's and the first edition's car files; an earlier note here said the slot was zero). The 16-bit software build remaps the pages to CHROME.CH3's palette at load (docs/systems/render.md, "Palettes and texture blocks").
Objects
Read by FUN_00466898. Coordinates are signed 16.16 fixed point.
| Read | Content |
|---|---|
| u32 | Vertex count n |
n × 12 | Vertices: x, y, z |
| u32, u32, u32 | Face count, and two counts that size run-time arrays of 0x1C and 0x38 bytes |
| faces | One after another (below) |
n × 12 | Vertex normals |
| u32 | Not used by the viewer |
Face
Read by read_face (FUN_00465d54), which branches on the corner count (observed).
| Read | Content |
|---|---|
| name | Material (T_G_CHUB3_A) |
| u32 | Corners: 3 or 4 (any other count stops the game with an error) |
| 4 × u32 | Vertex indices; unused slots hold leftovers |
| 3 × i32 | Face normal, 16.16 |
| name | Render type: TEXGOU (textured, Gouraud), GOURAUD, FLAT |
0x28 bytes | u32 texture page, four (u, v) pairs on the 256 grid, u32 leftover |
| 12 bytes | Quads only (read at 0x46b25d); meaning unknown |
| u32 | Face flags. The reader copies its low three bytes into runtime face offsets +0x36, +0x35, and +0x37 respectively (observed at FUN_00465d54); the render loop uses these bytes for visibility, face sidedness, and other face behavior. |
The bytes after an object (0x1C)
Seven words, 16.16: half-extents in x, y and z, a radius, and the centre x, y, z (inferred from the values: a bounding box and sphere; the wheels have centre 0).
What the objects are
Inferred from drawing them:
| Objects | What they look like |
|---|---|
| 18 (step 3) | The body, as three sets of six pieces; sets 1 and 2 differ from set 0 in their panels (damage states or detail levels: open) |
| 4 (step 4) | Wheels, each modelled around its own centre |
| 4 (step 5) | A flat outline of the body at height 0, flat-shaded: a shadow or collision hull |
Axis 0 runs along the car, axis 1 across, axis 2 up (measured by drawing the vertices from three sides).
Parameters
The 0x558 bytes go straight into the car's slot (0x6174ec + car × 0x63c, block at +0x90). At the start of a race FUN_00476aa0 copies the fields below into the race car (0x61aad0 + racer × 0x558), which has the same layout; the physics reads them there at the same offsets (observed). Everything else in the block is the car's run-time state (engine speed, current gear, velocities, the curve pointer at +0x43c), and it is zero in all nine cars (measured).
Not scrambled. An earlier reading took the wheels from stream offset 0x58. That fits only Arc and AV1, whose names have 3 letters: the block starts after the name, so its offset moves with the name's length (measured: with the name read first, all nine cars read as numbers). The .BV3 and .BV4 of a car hold the same block (measured).
Values are signed 16.16 fixed point unless marked. The units are inferred from the formulas: the game multiplies weight by 9.81 (0x9cf5c) and turns engine speed into rpm with 60/2π (0x98c9e), so lengths are metres, masses kilograms, angles radians and speeds per second. Each value below is observed in the function named.
Point records
Five records of 0xD8 bytes: wheels 0 to 3 at 0x000, 0x0D8, 0x1B0, 0x288, then the chassis point at 0x360. Wheels 0 and 2 are the front pair (positive x), 1 and 3 the rear; 0 and 1 are on the negative-y side. Offsets are within a record.
| Offset | Field | Records | Read by | What the game does with it |
|---|---|---|---|---|
+0x40 | mass, kg | all | FUN_00429bf0 | Inertia m·r²/2 into +0xa0; weight −m·g·9.81 into +0x44. 100 for wheels, 50 for the chassis point in every car |
+0x48 | x, m | all | FUN_004231f4 | Position along the car |
+0x4c | y, m | all | FUN_004231f4 | Position across the car |
+0x50 | z, m | all | FUN_004231f4 | Height. Zero in the files; for wheels FUN_00429cb4 sets it to the rest height at reset. The chassis point's x, y, z is subtracted from every wheel's position: it is the centre of mass |
+0x54 | radius, m | all | FUN_00429bf0 | Wheel radius (inertia; the garage averages the four, FUN_00442dd8). For the chassis point: the radius of its contact with the ground (FUN_00424478) |
+0x94 | side grip | wheels | FUN_004284cc | Times the surface's grip and a quarter of the car's weight: the most sideways force the tyre holds before it slides |
+0x98 | traction grip | wheels | FUN_004284cc | Times the surface's grip and a quarter of the weight: the force along the wheel before it spins or locks |
+0xac | spring | wheels | FUN_00426f18 | Suspension force −(z − rest)·spring − speed·damper, limited to ±7.63 |
+0xb0 | damper | wheels | FUN_00426f18 | The speed term of the same force |
+0xb4 | rest, m | wheels | FUN_00429cb4 | Height the wheel starts at |
+0xb8 | travel max, m | wheels | FUN_004272dc | Highest z; the wheel stops there |
+0xbc | travel min, m | wheels | FUN_004272dc | Lowest z |
FUN_00429cb4 overwrites the chassis point's +0x94 and +0x98 with 0x9c40 (0.61) at reset; they are zero in the files.
The car
| Offset | Field | Read by | What the game does with it |
|---|---|---|---|
0x44b | u8: forward gears (6 in every car) | FUN_0042672c | Top gear; the garage copies this many ratios (FUN_00442dd8) |
0x44c | 9 × gear ratio | FUN_00426184 | Engine speed per wheel speed, indexed by the current gear (u8 at 0x44a): 0 neutral (0), 1–6 forward, 7 unused (0), 8 reverse (negative). In every car gear n is gear 1 ÷ n |
0x484 | steering: largest angle, rad | FUN_0042386c | Limit of the front wheels' angle (+0x58 of wheels 0 and 2) |
0x488 | steering speed, rad/s | FUN_0042386c | Added per second while left or right is held |
0x48c | steering return, rad/s | FUN_0042386c | Speed back to centre, times the steering speed, when no key is held |
0x518 | rev up, rad/s² | FUN_00425c50, FUN_00426a40 | Engine speed gained per second with the throttle out of gear |
0x51c | rev down, rad/s² | FUN_00425c50 | Engine speed lost per second, out of gear |
0x520 | body mass, kg | FUN_00429bf0, FUN_0042a07c | Weight into 0x524; the body's inertia from its bounding box (m/12 · (a² + b²)) |
0x528 | gravity scale | FUN_00429bf0 | Weight = mass × this × 9.81 (4.36359 to 4.36362 in the nine cars) |
0x538 | front brake share | FUN_0042653c | Wheels 0 and 2 brake with this share, 1 and 3 with 1 − share (0.5 in every car) |
0x53c | step scale | FUN_00427514 | Multiplies the time step when the car's velocity moves its position (1.1942 in every car); inferred: a world-units-per-metre factor |
0x544 | brake rate, /s | FUN_0042653c | Braking grows by this per second while the key is held |
0x548 | brake max | FUN_0042653c | Braking stops growing here; a joystick sets a share of it |
0x54c | u32: drive | code at 0x428d4b | Which wheels the engine drives: 1 all four (front and rear shares by wheel radius), 2 the front pair, any other value the rear pair. 1 in every car |
At reset FUN_00429cb4 sets the largest engine speed (0x514) to 1000 rad/s (9,549 rpm) and the shift points from it, whatever the file holds there.
Torque curve
64 u32 values, one per 256 rpm of engine speed (observed: FUN_00426184 converts the engine speed in rad/s to rpm and indexes the curve by rpm ÷ 256). The value times the throttle and the gear ratio is the torque on the driven wheels. Every car's curve rises to a flat top and falls to 0 at entry 40 (10,240 rpm). The garage rebuilds the curve from its settings (FUN_00443048), so a changed curve may last only until the player changes the setup.
After the objects
| Read | Content |
|---|---|
0x24 bytes | A u32 the game replaces with a handle (load_car_file stores the result of FUN_004c1e48 there; the files hold leftover text such as rog\), then 16 u16 numbers, 0xFFFF for none. Inferred: the car's sound effects, by number. The 16th differs between a car's .BV3 and .BV4, so it is leftover too |
| 5 × u32 | The garage's five settings, 0 to 100 (Arc: 86, 37, 61, 55, 61). FUN_004436c8 copies them into the setup, and FUN_00443048 builds the torque curve from them. In order: acceleration, brakes, grip ("adhérence"), steering, speed (measured: the car screen shows GAMMA's 49, 25, 95, 82, 49; a changed third value shows as the grip). Grip in the race = 1.2 + 0.026 × setting on all four wheels (measured at 95, 50 and 5) |
| light | The record the track lights use (tracks.md). FUN_00434e50 picks a shading routine from the kind (2 in every car) and the second tail word. The race car points at it (+0x554). The meaning of the 48 bytes is open, as for the tracks |
Wheel positions
The viewer places each wheel at (x, y, rest height) from its point record. Measured: in all nine cars the wheels land at the body's corners.
Car lists (SCRIPTS/VOITURES.BIN, VOITURE2.BIN, VOIRESET.BIN)
Reader: crates/pod-formats/src/cartable.rs (prototype decoders/cartable.py). State: decoded and round-trip for all three files (measured, 2026-10-23).
FUN_00477400 reads a list: a plain u32 count, then count records of 0x80 bytes, each byte XORed with 0x68. FUN_0047747c writes one the same way (observed).
POD's VOITURE2.BIN holds 23 records with a count of 8 (measured). POD's reader (FUN_00472020) reads the counted 8: its seven new cars and ALIEN. The 15 after them name cars that are not on the disc (Breton, IRX, Akir7, Nocte, Kaizer, Pirate, Eagle, Razor, Xira four times, DarkSmil, Bat, Witch); inferred: the cars released on POD's web site, as the README says. They are never read; cartable::read_all keeps them for writing back.
| Record offset | Size | Content |
|---|---|---|
0x00 | char[0x40] | Car file without extension (GAMMA → VOITURES/GAMMA.BV3) |
0x40 | char[0x14] | Name shown in the menus (gamma); FUN_0048d110 and FUN_00436658 look cars up by it |
0x54 | char[13] | Picture name (24manta.tga) |
0x61 | char[13] | Picture name (Pmanta_E.tga) |
0x6e | char[13] | Picture name (Pmanta_A.tga) |
0x7b | 5 bytes | Zero |
No shipped program reads the picture names (measured: the strings appear in no executable or DLL, and no .tga files ship). POD2_0.DLL, the online client, names the three lists.
| File | Count | Holds | Where it goes |
|---|---|---|---|
VOITURES.BIN | 8 | The race list: Gamma, Saber, Scorp, Shivan, AV1, Dopple, Bakar, Arc | count 0x6174c0, records 0x61f424 |
VOITURE2.BIN | 1 | Spare cars: Alien | count 0x505c50, records 0x505c54 |
VOIRESET.BIN | 8 | The race list as shipped (equal to VOITURES.BIN on the disc) | read into the stack |
The menu code around 0x43ee1c–0x43f6be (inside FUN_0043e9cc as Ghidra cuts it) reads the spares, and writes both VOITURES.BIN and VOITURE2.BIN back (observed). The routine at 0x43f404 replaces the race list with VOIRESET.BIN's eight records and moves every car not in it to the spares (observed). Inferred: the menu swaps a spare car, such as the Alien, into the eight-car race list, and "reset" undoes it. These two files are save state: the game writes them. When a race-list entry changes, the game clears that car's "setup ready" flag (0x50911c + car).
Open questions
- How
FUN_00443048turns the five settings into the curve. - What the two animated objects after the parts are.
- The sound numbers: which effect each slot plays.
- The light record's 48 bytes (shared with the tracks).
- The units of grip, springs, brakes and torque: they are ratios inside the game's own scale.
- What the three body sets are, and why Bakar's front piece sits apart from its body.
- The quad-only 12 bytes and the face flags.
- Checking a changed value by driving under Wine (
docs/running.md).