# Vehicle physics runtime: mover update and substeps

This note recovers the frame-level schedule around the retail executable's
`PhysicsMover` and its registered simulation components. It establishes the
substep size and update order. It does not identify the force equations inside
those components, so it is not yet a complete vehicle or tire model.

## Reflected `PhysicsMover`

The executable contains the reflected type name `PhysicsMover`. Constructor
`0x005a4740` installs the vtable at `0x007a1d68`; serializer
`0x005a47b0` registers fields at offsets including `+0x20`, `+0x24`,
`+0x28..+0x44`, `+0x48`, `+0x4c`, `+0x50`, `+0x58`, `+0x64`, `+0x68`, and
`+0x6c`. The constructor and frame method use `+0x20` as a runtime state
pointer and `+0x58` as a physics-configuration/state pointer. The serializer
walks four pairs: each suspension pointer is at `+0x28 + 4*i` and its paired
wheel pointer is at `+0x38 + 4*i`, for `i = 0..3`. It invokes the
`Suspension` serializer `0x0059e020` and `Wheel` serializer `0x0059c980`
for these fields. The front/rear ordering is not recovered.

## Frame update and substep rule

Method `0x005b72d0` receives a frame delta `dt`. Before stepping, it clears
four byte flags at `physics_state +0x3c8..+0x3cb` and four floats at
`physics_state +0x330..+0x33c`, then calls a setup sequence:

```text
prepare(0x5ab990)
prepare_controls_or_state(0x5a49a0)
prepare(0x5abbf0)
update_delta_dependent_state(0x5a4d80, dt)
prepare_step_state(0x5b6300)
```

The names above describe call order only; except for the `dt` argument, the
helpers' complete semantics are not established.

One part of `0x005ab990` is now tied to the serialized corner objects. It
calls `0x0059e0c0` once on each Suspension at `mover +0x28 + 4*i`, passing a
per-corner float from `physics_state +0x3b8 + 4*i` and one shared float. The
helper stores the per-corner value at Suspension `+0x88`, computes a relative
three-float vector into `+0x90..+0x98` from two associated records, and
subtracts the shared float from the final component at `+0x98`. The same
stage writes the global pointer at `0x007fc6f0` into `+0x2c` of each paired
Wheel object. These are confirmed data operations; the values' units and the
global pointer's meaning remain unknown.

The frame method divides `dt` by the global float at `0x007fc070`, which is
`0.0044` seconds. Helper `0x0075bed4` converts a float to an integer by
truncation toward zero. The caller adds one, then divides `dt` by that count:

```c
int steps = trunc(dt / 0.0044f) + 1;
float step_dt = dt / steps;
for (int i = 0; i < steps; ++i)
    simulate_registered_components(step_dt);
```

For nonnegative frame deltas, this produces equal substeps no larger than
4.4 ms. At an exact multiple of 4.4 ms it deliberately adds one extra step,
so the resulting substeps are smaller than the cap. This is variable-length
substepping, not four hard-coded physics iterations.

The loop calls `0x005aa480` on the component owner at `mover +0x08`. That
helper walks the linked list rooted at owner `+0x0c`. Before the component
passes, it gathers float vectors into a contiguous stack buffer by calling
each component's vtable slot `+0x18` with the next output address; slot `+0x04`
returns the number of floats to reserve. It then revisits each component:
slot `+0x04` supplies the count, slot `+0x0c` returns the writable state
array, and slot `+0x20` is called with `step_dt`. After that callback, the
helper applies the gathered values to the component state as
`state[i] += step_dt * gathered[i]`. This is an explicit Euler-style update
over a flattened collection of per-component floats. The gathered values act
as per-step increments per unit of `dt`; the semantic names and units of the
component state fields remain unknown. After finishing the list, the helper
calls vtable slot `+0x04` once on the owner object; for this mover it resolves
to the post-component callback described below, and its return value is
ignored.

After each component pass, the frame loop reads two three-float vectors from
the physics-state pointer (`mover +0x20`) at `+0x130..+0x138` and
`+0x140..+0x148`, and adds them to the mover accumulators at `+0x100..+0x108`
and `+0x110..+0x118`. It invokes `0x005bb0d0` with `physics_state +0x30` as
both arguments. After the loop, it scales those six accumulated values by
`1 / steps` and calls `0x005a5540` with the original frame delta.

The owner's post-component callback is identified for this mover. Constructor
`0x005b5cc0` installs secondary vtable `0x007a25d4` on the embedded subobject
at `mover +0x08`. `0x005aa480` calls that subobject's vtable slot `+0x04`
after integrating the registered components; this resolves to `0x005b04d0`.
That method walks the linked list at subobject `+0x0c`, calling each entry's
vtable slot `+0x1c`, then invokes the four-corner Suspension accumulator
`0x005abb00` and tail-calls slot `+0x08`, which resolves to `0x005b0520` for
the installed vtable. Thus `0x005abb00` and `0x005b0520` run after each
`0x005aa480` state integration, inside the same `dt / steps` loop. The latter
updates the four corner response objects, calls `0x005abdf0` for Wheel and
DriveTrain response, then sends the transformed corner vectors and offsets to
the bounded accumulators. The force/torque arithmetic is visible, but the
corner object's physical meaning and response units remain unresolved.

In pseudocode, the directly visible integration shell is:

```c
clear_four_physics_flags_and_accumulators();
run_frame_setup_helpers(dt);
int n = trunc(dt / 0.0044f) + 1;
float h = dt / n;
for (int step = 0; step < n; ++step) {
    gather_component_vectors(component_owner, derivative_buffer); // mover+8
    int offset = 0;
    for each component:
        int count = component.float_count();             // virtual +0x04
        float *state = component.state();                // virtual +0x0c
        component.substep(h);                            // virtual +0x20
        for (int j = 0; j < count; ++j)
            state[j] += h * derivative_buffer[offset + j];
        offset += count;
    component_owner.virtual_0x04(); // this mover resolves to 0x005b04d0
    mover->accum_a += physics_state->vector_130;
    mover->accum_b += physics_state->vector_140;
    process_state_record(physics_state + 0x30, physics_state + 0x30);
}
scale_accumulators_by(1.0f / n);
finish_frame(dt);
```

`process_state_record` and the generic virtual slot descriptions are based on
the observed calls and data flow; they are not recovered original method
names.

## Frame finalization and per-corner outputs

After the substep loop, `0x005a5540` refreshes the physics-state block from the
owner/runtime records and the four Wheel objects. It first copies transform
and vector data, derives frame-level heading and vector values, mirrors the
current wheel outputs, and then rebuilds four corner transforms. This stage
makes several substep results available once per rendered frame.

The routine copies the owner record's transform data into physics-state
`+0x20` and passes the owner transform block at `+0x1f0` to `0x00592400` with
the physics-state pointer. It copies vectors from owner/runtime offsets
`+0xa0` and `+0xc0` into state `+0x30` and `+0xd0`. The length of the first
copied vector is stored at `+0x3f0`. State `+0x3ec` receives the dot product
of state vectors `+0x30` and `+0x50`. State `+0x3cc` receives the 16-bit
heading calculated from the x/y values at `+0x50/+0x54` by `0x0043c570`.
These are direct data-flow descriptions; coordinate axes and units are not
identified here.

The heading response at `+0x3ce` compares its previous 16-bit angle with the
owner heading word at owner `+0x1e0`. Both are converted to degrees using
`360 / 65536`, wrapped across the 0/360 boundary, and the previous angle moves
toward the owner heading at up to `120 degrees/second * frame_dt`. The result
is converted back to the 16-bit angle representation. This frame-level angular
slew is separate from the wheel transforms built later in the routine.

The four-Wheel loop refreshes one slot per corner. It mirrors Wheel `+0x24`,
`+0x28`, and `+0x164` into state banks beginning at `+0x300`, `+0x2f0`, and
`+0x310`, respectively. It also mirrors the byte at Wheel `+0x11c` to
`+0x350+i` and the profile index at `+0x114` to `+0x354+4*i`, maintaining the
enabled-wheel count at `+0x3e4`. For each corner, it follows the Suspension's
pointer at `+0x14`, compares the two records' `+0x1c` floats, selects one of
those values, subtracts the corresponding value from state `+0x3b8+4*i`, and
stores the result at `+0x340+4*i`. If mover byte `+0x84` is set, the
`+0x394+4*i` value is cleared and the corner transform source is replaced
with a shared fallback; otherwise Wheel `+0x170` is copied into state
`+0x394+4*i`, and its pointer at `+0x100` and float at `+0x104` supply the
transform inputs at `+0x374+8*i`. Wheel `+0x168` is copied into `+0x320+4*i`
when `+0x11c` is enabled, and zeroed otherwise. Semantic labels and units
for these per-corner values remain unknown.

The pass copies drivetrain mode data into state `+0x3f4`, sets `+0x408`
when the drivetrain's mode field at `+0x44` equals `3`, and copies its float
at `+0x70` to `+0x40c`. It computes state `+0x400` by selecting an entry from
the table at `0x007a1a2c` using a rounded combination of the object at mover
`+0x48` field `+0x14` and the engine-side value at `+0x2f4`; one path adds four
to the table index when the drivetrain mode and associated condition are
active. The first four table entries are `2.5`, and the following three are
`15.0`. That selected value,
divided by the scale reached through drivetrain `+0x18/+0x0c`, is used in a
frame-delta rate limit. The update also applies a 100-unit threshold and a
`0.55/0.45` blend when storing state `+0x400`. The table's semantic name and
the units of this response have not been recovered.

Finally, the routine takes the largest absolute Wheel `+0x118` value,
divides by three and clamps it to `[0, 1]`, storing the result at state
`+0x3e0` and `+0x3dc`. It averages the four Wheel `+0x170` values into
`+0x3d8` and calls `0x005921b0(state, i)` for each corner.

`0x005921b0` returns immediately when the corner flag at `+0x3b4+i` is clear.
For an active corner, it builds two 4x4 matrices in stack buffers. The base
rotation angle comes from state `+0x300+4*i`, scaled by the float at
`0x0078423c`, then converted to an integer angle for `0x005bada0`. For corner
indices 0 and 1 only, it additionally applies the 16-bit heading at state
`+0x3ce` to both matrices through `0x005bae60`. It then reads a geometry
descriptor through the pointer in state `+0x00`, at
`geometry + 0x10 + 0x30*i`; its first three floats and the scalar at `+0x14`
are copied into the matrix inputs. The corner displacement value at state
`+0x340+4*i` is added to that scalar and to a corresponding matrix input.

The helper writes the resulting matrices to the physics-state object's two
per-corner banks at `+0xf0+0x40*i` and `+0x1f0+0x40*i` via `0x004012b0`.
This is direct evidence that the frame finalizer turns Wheel angle, heading,
geometry descriptor, and suspension displacement into per-corner transforms.
The descriptor's axis convention and the purpose of the two matrix banks are
not recovered. I have not yet found the renderer's read of these exact banks,
so this establishes the runtime placement inputs and outputs, but not which
bank is applied to a particular tire mesh or how the mesh is oriented.

## Four corner records and controls

The `0x005a49a0` stage reads a count at `physics_state +0x3e4`. It checks four
enable bytes at `+0x350..+0x353`, uses corresponding indices at `+0x354`,
`+0x358`, `+0x35c`, and `+0x360` to sample a global array with a 0x98-byte
stride, accumulates values for enabled entries, divides by the count when it is
positive, and then branches on a state value reached through `mover +0x5c`.
This connects four per-corner lookups into a movement-surface table. The
frame-finish pass `0x005a5540` fills those exact enable and
index slots from each serialized Wheel: it copies Wheel `+0x11c` to physics
state `+0x350+i`, and Wheel `+0x114` to `+0x354+4*i`. When the enable byte
changes, it increments or decrements the active count at `+0x3e4`. The sampled
array is a separate static movement-surface profile table at `0x00800f00`, not
the Wheel pool. The Wheel's `+0x114` value is a profile index; the sampled
coefficient is profile `+0x10`.

The executable embeds the profile's primary surface label at `+0x78` and skid
label at `+0x88`. The IDs below are the values used by the `index * 0x98`
lookup. Their `+0x10` coefficient is `1.0` except for `SIDEWALK`, which is
`5.0`:

| ID | Surface label | Skid label |
|---:|---|---|
| 0 | `ROAD` | `SKID_ROAD` |
| 1 | `ROAD_WET` | `SKID_ROAD` |
| 2 | `ROAD_DRIFT` | `SKID_ROAD` |
| 3 | `ROAD_SMOKE_1` | `SKID_ROAD` |
| 4 | `ROAD_SMOKE_2` | `SKID_ROAD` |
| 5 | `ROAD_SMOKE_3` | `SKID_ROAD` |
| 6 | `ROAD_BRIDGE` | `SKID_ROAD` |
| 7 | `DIRT` | `SKID_DIRT` |
| 8 | `GRAVEL` | `SKID_DIRT` |
| 9 | `ROUGH_ROAD` | `SKID_ROAD` |
| 10 | `COBBLESTONE` | `SKID_ROAD` |
| 11 | `STAIRS` | `SKID_ROAD` |
| 12 | `PUDDLE` | `SKID_ROAD` |
| 13 | `DEEP_WATER` | none |
| 14 | `GRASS` | `SKID_GRASS` |
| 15 | `SIDEWALK` | `SKID_ROAD` |
| 16 | `WOOD` | `SKID_ROAD` |
| 17 | `PLASTIC` | `SKID_ROAD` |
| 18 | `GLASS` | `SKID_ROAD` |
| 19 | `SOLID_WALL` | none |
| 20 | `SEE_THROUGH_WALL` | none |
| 21 | `PLANT` | none |
| 22 | `POST` | none |
| 23 | `PILLAR` | none |
| 24 | `METAL_GRATE` | none |
| 25 | `METAL` | none |
| 26 | `CHAINLINK` | none |
| 27 | `CAR` | none |

## Where wheel surface IDs come from

The per-wheel profile index is refreshed in the wheel-contact path. The
`0x005a2e40` loop calls `0x0059cb90` for a selected wheel/sample while iterating
the data reached through `edi +0x24`. That routine transforms the sample using
the wheel/vehicle frame, prepares contact-query inputs, and calls `0x005e7e70`
with the collision/query object reached through `[edi +4]`. `0x005e7e70` is a
tail jump to `0x005e72e0`. The query selects a collision record and, when its
third output pointer is non-null, writes the sign-extended byte at selected
record `+0x0b` through that pointer (`0x005e774a..0x005e774e`). If no hit is
selected, it writes profile index `1` instead (`0x005e7868`). The caller's
third output pointer aliases its local at `[esp+0x0c]`; after the query,
`0x0059d016` copies that dword to Wheel `+0x114`. This connects the collision
record's byte `+0x0b` directly to the movement-profile table index.

The caller also sets Wheel `+0x11c` to `1` on this path. A later branch can
force the profile index to `1` when any of the tested global conditions
`0x0089e7d8`, `0x00890108`, `0x0089e7e3`, or `0x0089010c` is active; those
conditions' gameplay labels are not established. Thus the normal selected
profile comes from hit-record `+0x0b`, with index `1` as the query miss default
and as a global-condition override.

The second update path, in `0x005a2eaa..0x005a2f21`, processes a selected
per-wheel record from a table reached through `edi +0x24`. It compares the
old Wheel `+0x11c` value with the corresponding table byte, updates the active
wheel count at physics state `+0x3e4` if the value changes, then copies the new
byte into Wheel `+0x11c`. It also copies neighboring record fields into Wheel
`+0x100`, `+0x104`, and `+0x118`. This is evidence that the contact/enabled
state can be refreshed from the sampled wheel data during the vehicle update.

The collision query's full geometric contract is broader than this mapping,
but the material-index transfer is explicit. The labels and coefficients in
the profile table still define the downstream gameplay-specific response.

The runtime polygon record is built from a compact track-collision entry by
`0x005d7f10`. It copies source-entry byte `+0x02` into runtime polygon byte
`+0x0b`, and source byte `+0x03` into runtime byte `+0x0a`; the routine then
decodes the entry's packed vertex selectors and builds the polygon's geometric
data. Runtime records are traversed at `0x50`-byte strides, with type `3` used
in the track-polygon path. This closes the source chain: the track collision
entry's byte `+0x02` is the value the wheel query returns as its surface-profile
index. The `TrackPolygonCollisionBody` reflected accessor at `0x00593d90`
independently reads `this+0x60`'s byte `+0x0b`, consistent with that nested
polygon representation. Byte `+0x03` has flag-like uses; bit `0x10` selects
runtime byte `+0x09` value `3` or `4`. The full flag meanings are unresolved.

The enabled-wheel profile-value average has two observed destinations. When
the state reached through `mover+0x5c` is `0` or `2`, it is written to
`*(mover+0x50)+0x34`. In state `1`, the setup path uses the same average as an
input to the command-source setter for `+0x20c`; a threshold branch can supply
the constant `0.4` instead. Other
states discard the average at this stage. The coefficient's gameplay meaning
is not established; preserve it as a surface-profile parameter rather than
naming it friction or grip. The same table's color at profile `+0x20` is used
by `0x005d7c4c` when it builds four debug draw colors for a selected profile.

The same finish pass also copies Wheel `+0x28` into physics-state
`+0x2f0+4*i` and Wheel `+0x24` into `+0x300+4*i`; it copies Wheel `+0x164`
into `+0x310+4*i`, and raises the value at `+0x364+4*i` when Wheel `+0x114`
is larger. These are direct state mirrors/max updates. Their physical labels
and units remain unknown.

The same stage writes several state-dependent values into `mover +0x20` and
its attached records. Examples include fields `+0x150`, `+0x154`, `+0x158`,
`+0x160`, `+0x200`, and `+0x204`. Some branches also read the float at
`+0x20c` on the object referenced by `mover +0x04`. In lifecycle mode 1 that
pointer aliases the embedded player `RealDriver`, as shown below.

The three command-slot setters are simple stores: `0x005eb910` writes its
float argument to `this+0x20c`, `0x005eb900` to `this+0x210`, and
`0x005eb8f0` to `this+0x214`. `0x005a49a0` calls `0x005eb910` with
`this = *(mover+0x04)` in two branches while preparing mover state. The
player's longitudinal update also calls these same setters, but with
`this = RealDriver`; at the end it copies `RealDriver+0x20c` to
`*(RealDriver+0x2a8)+0x438`. The lifecycle setup below resolves whether these
matching layouts are separate: in controller mode 1, `owner+0x30` points
directly to the embedded `RealDriver`, and the mover takes that same pointer
as its command source. So its `+0x20c/+0x210/+0x214` reads and writes address
the player's driver slots in that mode. The extra owner `+0x438` copy remains
a separate field.

The setup path narrows the mover-side owner: factory `0x005f46a0` passes its
`+0x30` pointer as the third constructor argument to `0x005b5cc0`, and that
constructor stores the argument at `PhysicsMover+0x04`. The factory's own
`this+0x40` is separately retained at `PhysicsMover+0x58`. So the object whose
command slots the mover reads is the factory owner's `+0x30` command-source
object.

There is a shared lifecycle for that owner type. At `0x005f6d50`, the owner
can create controller mode `1` through `0x005eba30` (which selects
`RealController`) and then calls `0x005f4400` followed by `0x005f46a0` to
create its selected component at owner `+0x34`. Factory component mode `1`
allocates `0x130` bytes and invokes the `PhysicsMover` constructor. This proves
that a single owner instance can hold both the `RealController` at `+0x2c` and
a `PhysicsMover` component at `+0x34`. In this mode, `RealController` passes
that owner to its embedded `RealDriver`; the driver stores it at `+0x2a8`, and
the embedded steering constructor stores it at steering `+0x00`. The mover
stores the same owner's `+0x30` pointer at mover `+0x04`. In controller mode 1,
setup assigns the embedded `RealDriver` pointer to that owner `+0x30` field.
Therefore the driver's slots, steering's input object, and the mover's command
source are the same object; the driver's host-side `+0x438` field is separate.

### Shared command-source updates during the mover tick

The `PhysicsMover` frame callback `0x005b72d0` calls `0x005a49a0` and then
`0x005abbf0` before its main state update. In the mover's state-1 branch,
`0x005a49a0` examines four enable bytes and associated indices in the physics
state, looks up enabled values in the global `0x98`-byte-stride table at
`0x00800f10`, and forms an average when at least one entry is enabled. It then
tests the existing `owner+0x30` float at `+0x20c` against a threshold and calls
`0x005eb910` with the mover's `+0x04` pointer as `this`, writing either the
fixed value `0.4` or the calculated average back to that same command slot.
The inputs identify this as mover-side state logic; the table's physical
meaning and units are unresolved.

The immediately following `0x005abbf0` pass reads all three values at
`owner+0x30 +0x20c/+0x210/+0x214` (throttle, e-brake, brake by setter
offset). It skips the conflict checks when owner `+0x434` is `1`, global byte
`0x0089e7e2` is set, or either mode dword `0x00890100/0x00890110` is nonzero.
Otherwise it applies this priority gate:

```text
if throttle <= brake or e_brake >= throttle:
    if throttle <= 0.5:
        throttle = 0
else:
    if brake < 1.0:
        brake = 0
    if e_brake < 1.0:
        e_brake = 0
```

The comparisons and writes are at `0x005abc33..0x005abca7`; setters
`0x005eb910`, `0x005eb8f0`, and `0x005eb900` perform the corresponding stores.
This is a direct mutual-priority rule for the shared command channels. The
function then updates a separate controller value and route/vehicle state.
These mover-side passes show that the shared steering/mover inputs are actively
adjusted and gated during the mover tick. With controller mode 1, their writes
modify the embedded `RealDriver` slots themselves. The player's separate
`owner+0x438` write is not the command handoff.

## Two-wheel Ackermann steering update (`0x0059def0`)

The method immediately following the serializer for
`TwoWheelAckermanSteering` (`0x0059dea0`) updates that object's two serialized
64-byte transform blocks at `+0x10` and `+0x50`. It stores its 16-bit steering
input at `+0x90`. The class identity is supported by the serializer's runtime
name and the matching `TwoWheelAckermanSteeringSlotPool` registration.

The input word is first converted to radians with the constant at
`0x00784240` (`2π / 65536`). Values above π are wrapped by subtracting `2π`,
giving a signed half-turn range. The method reads a geometry object through
steering-object `+0x04` and forms two deltas:

```text
longitudinal_delta = geometry[+0x10] - geometry[+0xa0]
lateral_delta      = geometry[+0x14] - geometry[+0x44]
adjusted_angle     = longitudinal_delta * input_angle
                     / (longitudinal_delta + lateral_delta * input_angle)
```

The x87 operations implement this ratio directly. A sign branch then prepares
opposite adjusted angles for the two transform blocks. Each block is reset to
an identity matrix by `0x0043c780`; the routine fills its planar rotation
entries using the executable's fixed-angle lookup helper `0x0043c350`, with a
π/2 phase offset for the cosine term. This is a concrete geometry-dependent
two-wheel steering transform, not merely a shared rotation copied to both
wheels. The data does not identify which block maps to left/right, how the two
geometry deltas are authored, or whether later code applies these matrices to
the front pair specifically. A recreation can implement the recovered angle
conversion and ratio while keeping wheel labeling and geometry-source mapping
configurable.

The serializer at `0x0059dea0` registers the two blocks at `+0x10/+0x50` and
the input word at `+0x90`; it does not register the geometry pointer at
`+0x04`. That pointer is runtime linkage supplied by another setup path. The
traffic mover also subtracts fields `+0xa0` from `+0x10`, but reads them
through owner `+0x20`, while this steering method reads through steering
object `+0x04`. The shared field offsets and subtraction are evidence for a
related geometry convention only. The inspected constructor, serializer, and
pose-sync paths do not establish pointer identity between those objects.

The Ackermann vtable and input accessor are explicit in this build. The
PhysicsMover constructor installs vtable `0x007a2084`; its entries are the
destructor `0x005a4770` at `+0x00`, update `0x0059def0` at `+0x04`, word
accessor `0x005a4760` at `+0x08`, and serializer `0x0059dea0` at `+0x0c`.
The accessor is only `return WORD [this+0x90]`; it is not a setter. In
`0x0059def0`, the incoming word is stored to `+0x90` before the two output
matrices are rebuilt. The Suspension post-integration callback `0x005a8790`
reads the Ackermann pointer at Suspension `+0xa8`, obtains its matrix base from
Ackermann `+0x08`, and uses the corner index at Suspension `+0xac` as a 64-byte
matrix offset before its Wheel transform update. This resolves a consumer of
the Ackermann output matrices. The writer/caller that supplies the update word
from the embedded RealDriver, and the runtime front/rear meaning of the corner
indices, remain unlocated.

The geometry pointer's construction source is mapped in r359 below. In brief,
mode-1 factory `0x005f46a0` passes `*(actor+0x10)+0x10` to
`0x005b5350`; its returned copy base is stored at `mover+0x54` and passed to
Ackermann `+0x04`. The Ackermann geometry fields map into the selected
GLOBALB row's wheel-anchor block at `+0x120`.

## Wheel rotation state (`0x0059d030`)

The `Wheel` serializer at `0x0059c980` exposes floats at `+0x24/+0x28` and a
state dword at `+0x110`. A nearby method at `0x0059d030` uses those fields
together: it builds a transform at `+0x30` from the angle at `+0x24`, adjusts
the stored angle when it reaches the large wrap thresholds, and gates the
signed scalar at `+0x28` using state `+0x110`.

The transform is initialized to identity with `0x0043c780`. The method then
calls the fixed-angle lookup `0x0043c350` for the stored angle and a π/2-offset
angle and writes the resulting rotation terms into the transform. The stored
angle wraps once by about `628.318` when it passes the positive or negative
threshold at `0x007a1ea8/0x007a1ea0`; if the once-wrapped value is still beyond
about `±6283.18` (`0x007a1ea4/0x007a1e9c`), the routine resets it to zero. These
unusually large limits are literal constants from the executable and should be
retained as such rather than replaced with a conventional one-turn wrap.

The scalar/state logic has this finite-value behavior:

```text
if state == 0 and scalar < 0: scalar = 0
if state == 2 and scalar > 0: scalar = 0

if scalar > +1e-5: state = 0
else if scalar < -1e-5: state = 2
else: state = 1
```

This is consistent with a wheel angle, signed rolling input, and a three-state
direction/near-zero encoding, but the code does not name the fields or prove
their physical units. It establishes transform generation and sign-state
handling; it does not recover tire friction, slip, contact force, or how the
scalar is generated upstream.

## Curve-backed four-wheel update (`0x005abdf0`)

The setup/update routine at `0x005abdf0` first calls `0x00594cb0` for the
owner's object at `+0x4c`, then reads a signed-byte setting through the owner
state pointer at `+0x5c`. It calls `0x005a8280` on each of the four Wheel
pointers stored at owner `+0x38..+0x44`, passing zero and that signed byte.
This is the runtime consumer of the four curve objects built during
vehicle-definition setup (see [traffic-vehicle-definitions.md](traffic-vehicle-definitions.md)).

The Wheel update reads its `+0x114` movement-surface index and uses profile
fields at both `profile+0x08` and `profile+0x10`; these are separate profile
values. It also reads a curve/input object through `+0x1c`, state through
`+0x20`, and calls `0x0059d400` on its active path. The routine writes a pair
of response values at Wheel `+0x124/+0x128`, caches input-related values at
`+0x100/+0x104`, and marks the path at `+0x160`. A fallback path clears
`+0x160` and derives the response pair from other Wheel fields. The code
establishes the data flow but does not name the pair's physical units.

After `0x005a8280`, `0x005abdf0` performs five passes over the four Wheels.
For each Wheel it forms two weighted values from the Wheel fields:

```text
u = wheel[+0xb0] * wheel[+0x124] + wheel[+0xc0] * wheel[+0x128]
v = wheel[+0xb4] * wheel[+0x124] + wheel[+0xc4] * wheel[+0x128]
```

Those results are combined with owner transforms and associated records at
owner `+0x28..+0x34`. Each pass also calls `0x00594da0` on the shared object
at owner `+0x4c`; that routine's mode-3 branch advances the drivetrain. The
five iterations are explicit in this function, but the disassembly does not
establish their duration or name them as fixed-time physics substeps. After
the loop, the method calls `0x0059d600(wheel, index)` for each Wheel, then
`0x005aa830` on the shared DriveTrain. That final method is mode-gated: in
one branch (`+0x3c == 1`, `+0x44 == 3`) it evaluates the Engine and may add a
scaled value to `Engine+0x7c`; another branch (`+0x3c == 2`) evaluates the
Engine and conditionally adds to the same accumulator. The routine consumes
no Wheel pointer directly, so its placement after the four Wheel calls does
not establish a direct Wheel-to-Engine data dependency.

`0x0059d600` first checks Wheel `+0x108`. If it is positive and Wheel
`+0x10c` is zero, the routine temporarily multiplies response component
`+0x124` by `1.5`. It then computes the magnitude of the response pair and a
surface- and state-dependent bound. When the weighted pair exceeds that bound
and passes the routine's positive magnitude guard, it scales both components
by the same ratio, preserving their direction, and stores the ratio at Wheel
`+0x170`. If the limiter does not run, it divides `+0x124` by `1.5` to undo
the temporary weighting. It then uses the bounded pair with Wheel vector and
matrix fields to build a transformed three-vector at `+0x130`. Helper
`0x0043c9c0` applies the 3x4 affine matrix at Wheel `+0x70..+0xac` to a
response vector, computing each output component as a matrix row dot the
input plus that row's translation. The path also handles several global
transform modes, so the written vector depends on more than the response
pair. Profile `+0x04` contributes to the bound in this stage, distinct from
the `+0x08/+0x10` profile reads in `0x005a8280`.

This closes a concrete part of the drivetrain-to-wheel chain: `0x00594a30`
adds drive-dependent terms into Wheel `+0x108`, and `0x0059d600` uses a
positive value there to weight the first response component before the
surface/state response cap. During the five passes in `0x005abdf0`, each
Wheel's `+0x108` also participates in a response correction that adds to
`+0x124`, when the Wheel's `+0x160` flag is clear and `+0x10c` is zero. That
correction combines paired-wheel response terms, the Wheel curve sample at
`+0x10`, the Wheel response at `+0x124`, and Wheel `+0x2c`. The accumulator
therefore changes the response pair before `0x0059d600` bounds it. The
accumulator's physical units are still unresolved; the resulting transformed
vector feeds the four-corner accumulator described below.

The response cap's core operation can be represented as:

```c
float x = wheel->field_124;
float y = wheel->field_128;
bool drive_weight = wheel->field_108 > 0.0f && wheel->field_10c == 0;
if (drive_weight)
    x *= 1.5f;

float magnitude = sqrtf(x * x + y * y);
float bound = wheel_response_bound(wheel); // surface/state expression below
if (magnitude > bound && magnitude > 0.001f) {
    float scale = bound / magnitude;
    x *= scale;
    y *= scale;
} else if (drive_weight) {
    x /= 1.5f;
}
wheel->field_124 = x;
wheel->field_128 = y;
```

This is the cap core only; the routine also updates `+0x170` and writes the
transformed vector at `+0x130` under additional state and global-mode gates.

The three profile reads have separate observed roles. In the active branch of
`0x005a8280`, profile `+0x08` is multiplied by Wheel `+0x180`, `+0x184`, and
the sampled value at Wheel `+0x18`'s `+0x10`; state `+0x424 == 1` adds a
further multiplier from `0x0059d490`. That product scales an input to
`0x0059d400`. This helper computes

```text
threshold = 1.2 * (curve[+0x08] * wheel[+0x17c] * 10
                 + curve[+0x04] * wheel[+0x178] * 4)
demand = abs(wheel[+0x28]) * 0.1 + curve[+0x10] * argument

if threshold <= demand:
    wheel[+0x10c] = 0
    # +0x28 is left as it was
else:
    wheel[+0x10c] = (threshold <= 1.0) ? 1 : 0
    wheel[+0x28] = 0
```

The branch comparisons are x87 ordered comparisons; unordered values also take
the first branch. The active caller then writes its two response components
to `+0x124/+0x128`. The fallback branch clears
`+0x160` and uses profile `+0x10` with Wheel `+0x108/+0x190` to form its first
response component; a helper supplies the second. In `0x0059d600`, profile
`+0x04` multiplies Wheel `+0x18`'s `+0x0c`, `+0x188`, `+0x184`, `+0x180`,
and `+0x164`, plus a global scale, to form the radial response bound. A
conditional `0x0059d490` factor also applies there. Thus these profile fields
change Wheel response and limiting; their semantic units remain unknown.

The shared multiplier `0x0059d490` normally returns `1.0`. It only derives a
smaller value when Wheel `+0x10c` is zero, Wheel `+0x28` is negative, and the
dword reached through Wheel `+0x14` at `+0x18` is at least `2`. In that case it
reads `+0x3ec` from the object at Wheel `+0x20`, computes
`clamp((value - 13.410818) * 0.11185, 0, 1)`, then maps the result into
`[0.85, 1.0]` as `0.85 + 0.15 * result`. This makes the optional profile
multiplier a state- and value-dependent reduction in the active Wheel response
and its radial bound. The source fields' gameplay names and units remain
unresolved.

The static table's `+0x04/+0x08` pairs are both `1.0` for most IDs. The
non-default pairs are:

| ID | Surface | `+0x04` | `+0x08` |
|---:|---|---:|---:|
| 8 | `GRAVEL` | 0.97 | 0.957875 |
| 9 | `ROUGH_ROAD` | 0.99 | 0.9813 |
| 13 | `DEEP_WATER` | 0.96 | 0.9513 |
| 14 | `GRASS` | 0.96 | 0.948 |
| 15 | `SIDEWALK` | 0.96 | 0.948 |
| 22 | `POST` | 0.99 | 0.981 |
| 23 | `PILLAR` | 0.99 | 0.981 |
| 24 | `METAL_GRATE` | 0.99 | 0.981 |
| 25 | `METAL` | 0.99 | 0.9813 |
| 26 | `CHAINLINK` | 0.99 | 0.981 |
| 27 | `CAR` | 0.99 | 0.981 |

The existing `+0x10` values remain `1.0` except `SIDEWALK`, which is `5.0`.
These are literal profile values and runtime multipliers, not enough evidence
to rename them as friction, grip, or restitution.

This trace connects the setup-time definition curve to four Wheel response
pairs, a repeated combined Wheel/Drivetrain update, and a later per-Wheel
limiting/transform stage. It does not yet recover the units of those response
values or the precise meaning of the five passes.

## Four-wheel response stage (`0x005b0520` → `0x0059d160`)

The `0x005b0520` callback runs on the PhysicsMover's embedded subobject. Its
four entries at subobject `+0x20` alias the four Suspension pointers serialized
at `mover +0x28..+0x34`. For each Suspension entry it passes the low byte at
Suspension `+0x80` and float at `+0x24` to `0x0059d160`, using the pointer at
Suspension `+0xa0` (the Wheel) as `this`; the returned float is stored at
Suspension `+0xb0`. The callback then calls the shared four-Wheel/DriveTrain
update at `0x005abdf0` before making its second pass over the Suspensions. The
Wheel response object's class is identified; the physical units of its response
remain unknown.

Inside `0x0059d160`, the byte is copied to child `+0x11c`. When the float input
is not positive, the routine clears child `+0x164` and returns zero. For a
positive input, it takes two three-vectors at child `+0x80` and `+0x150`, forms
their cross product with `0x0043d1e0`, crosses that result with the first
vector, then dots the result with the second vector. The helpers confirm these
operations directly: `0x0043d1e0` is a three-dimensional cross product and
`0x0043d0c0` writes `point + scale * vector`. The scalar path is:

```text
orientation_factor = max(4 * dot(cross(v80, cross(v150, v80)), v150) - 3, 0.3)
child[+0x164] = orientation_factor * positive_input
```

When the byte input is set, the method also constructs a point at child
`+0x140` from child `+0xa0`, vector `+0x150`, and the negated value reached
through child `+0x14` at `+0x10`. The caller's `+0xb0` receives the routine's
zero return; the nonzero computed response is child `+0x164`. This links a
four-entry physics stage to a concrete orientation-weighted response, but the
vectors' physical roles and the response's units are not named. It does not
yet establish that this scalar is tire friction or contact force.

The second pass in `0x005b0520` checks whether the value at each Suspension
`+0x80` is nonzero. The five-argument constructor call at `0x005b5ee0` pushes
index, RigidBody, Wheel, optional Ackermann Steering, and corner-config pointer
in reverse cdecl order; `0x005a86c0` stores these at Suspension `+0xac`,
`+0xa4`, `+0xa0`, `+0xa8`, and `+0x14`. Thus `entry+0xa0` is Wheel and
`entry+0xa4` is RigidBody. The callback forms
`delta = wheel[+0x140..+0x148] - body[+0x20..+0x28]`. Unless global flag
`0x0086b36c` is set, it calls `0x0059bb00` with `this=body`, Wheel's transformed
vector at `+0x130`, and that delta. In compact form, the callback's confirmed
ordering is:

```text
for each of four entries:
    result = update_child(entry[+0xa0], low_byte(entry[+0x80]), entry[+0x24])
    entry[+0xb0] = result

update_four_wheels_and_drivetrain()

for each of four entries:
    if entry[+0x80] != 0 and global[0x0086b36c] == 0:
        wheel = entry[+0xa0]
        body = entry[+0xa4]
        delta = wheel[+0x140..+0x148] - body[+0x20..+0x28]
        accumulate_response(this=body, wheel[+0x130..+0x138], delta)
```

`0x0059bb00` caps the Wheel vector's length at about `111.76`, adds it to the
RigidBody's `+0x110..+0x118`, forms its cross product with `delta`, caps that
length at `1200`, and adds the result to the RigidBody's `+0x120..+0x128`.
This establishes that the Wheel response goes directly into the same RigidBody
allocated at `mover+0x20`; it is not accumulated in the corner-config record.
The helper is also used by the separate `Smokeable` update at `0x005b1dd0`.
The caps and accumulator offsets are known, but their physical units and the
conversion to chassis linear/angular motion remain unresolved. This three-vector is
separate from the per-corner 4x4 matrices written later by `0x005921b0` into
physics-state `+0xf0..` and `+0x1f0..`; the renderer reads of those matrix
banks remain unlocated.

The body-state helpers around this work have a separate, now-decoded role.
`0x00592400(state, point)` transforms the first three floats at `point` through
the affine matrix at `state+0x50`, subtracts that transformed result from the
three-float position at `state+0x20`, and stores the difference at
`state+0x10..+0x18`. With a null point argument, it uses the pointer stored at
`state+0x00` as the input. `0x00592460` copies the 64-byte matrix at
`state+0x50` to `state+0x90`. The frame finalizer calls `0x00592400` with the
owner's transform record at `owner+0x1f0`, then copies the matrix to `+0x90`.
These helpers maintain a relative-position vector and a cached matrix; their
`+0x50` matrix is not either of the per-corner banks at `+0xf0` and `+0x1f0`.

## Engine output curve (`0x00594300`)

The `Engine` serializer at `0x00594220` registers three sampled curves at
`+0x24`, `+0x38`, and `+0x60`, alongside scalar fields including `+0x14`,
`+0x78`, `+0x80`, `+0x84`, `+0x88`, `+0x8c`, and `+0x90`. The runtime method
at `0x00594300` returns a float from these curves. The code does not identify
that return value's physical units, so it is left as `engine_output` here.

All three curves are sampled at `engine[+0x14] * 9.549304`. This constant is
approximately `60/(2π)`, converting an angular rate in radians per second to
revolutions per minute if `+0x14` uses radians per second. The curve helper at
`0x005764d0` uses a descriptor containing sample count at `+0`, input origin
at `+4`, reciprocal sample spacing at `+0xc`, and float samples at `+0x10`.
It clamps to the first or last sample outside the descriptor range and linearly
interpolates between adjacent samples inside it.

The evaluator's offset-level behavior is:

```text
rpm_key = engine[+0x14] * 9.549304
bias = engine[+0x88] ? 0 : engine[+0x80] * engine[+0x78]
base = sample(engine_curve_24, rpm_key) * engine[+0x90]
if engine[+0x74] != 0:
    base *= 1.05

if engine[+0x84] > 0:
    base += sample(engine_curve_60, rpm_key)
         * parent[+0x2ac]
         * float_table_7a1798[engine[+0x84]]
         * bias

if engine[+0x8c] > 0.6:
    base *= 1 - 0.75 * (min(engine[+0x8c], 1) - 0.6) / 0.4

engine_output = base - sample(engine_curve_38, rpm_key) * (1 - bias)
if engine[+0x88] != 0:
    engine_output -= sample(engine_curve_38, rpm_key)  # additional 1x
elif engine_output < 0:
    engine_output *= parent[+0x3c] == 1 ? 0.65 : 0.85
```

The `+0x88` branch therefore zeroes `bias`, disables the positive curve-60
contribution, and doubles the curve-38 subtraction; it also bypasses the
negative-output scaling branch. The table at `0x007a1798` is indexed by
`+0x84`; its initial values include `1.7`, `1.0`, and `1.4`, but its complete
extent and semantic labels have not been established. Direct callers include
`0x00594cd2`, `0x00594db4`, `0x005a1293`, `0x005aa8ab`, and `0x005aa8f0`.
Tracing those callers is needed before assigning this returned float a
gameplay name such as torque or engine braking.

### GLOBALB engine sample data handoff (2026-10-15, resolved statically)

The garage exporter reads the three relevant source layouts from the matched
GLOBALB vehicle row: nine `primary_curve` floats at record `+0x310`, nine
`secondary_curve` floats at `+0x334`, and three floats at `+0x358`. The record
header at `+0x300/+0x304/+0x308` contains the three RPM values used by the
constructor. These are source layouts; the distinctions between adjacent
runtime descriptors matter.

The definition copy path (`0x005b4dc0 → 0x005ac4f0`) copies the record region
starting at `+0x110` into the owner block at `+0x30`, so record `+0x300` maps
to owner `+0x230`, and the sample arrays at `+0x310/+0x334/+0x358` map to
owner `+0x240/+0x264/+0x288`. The factory at `0x005f47b4` calls
`0x005b5350`; that helper returns the selected base object at `x+0x40`, which
is retained by the PhysicsMover as `mover+0x54`. In the mover constructor
`0x005b5cc0`, `0x005aa5a0` is called with `mover+0x54+0x1f0`, which is exactly
the copied owner header at `x+0x230`. Thus the Engine constructor receives the
vehicle-specific block containing these fields.

`0x005aa5a0` initializes four adjacent sampled layouts:

| Engine offset | Count | Domain source | Samples source | Evaluator use |
| --- | ---: | --- | --- | --- |
| `+0x24` | 9 | owner `+0x230` and `+0x238` (record `+0x300/+0x308`) | owner `+0x240` (record `+0x310`, `primary_curve`) | sampled by `0x00594300`, multiplied by Engine `+0x90` |
| `+0x38` | 3 | the same owner `+0x230/+0x238` endpoints | owner `+0x288` (record `+0x358`) | sampled by `0x00594300` and subtracted |
| `+0x4c` | 9 | the same owner `+0x230/+0x238` endpoints | owner `+0x264` (record `+0x334`, `secondary_curve`) | not read by `0x00594300` |
| `+0x60` | 9 | owner `+0x230` and `+0x234` (record `+0x300/+0x304`) | initially shared pointer `0x007fbf38` | sampled conditionally by `0x00594300` |

The direct Engine constructor and the offset-level evaluator therefore resolve
the previously uncertain handoff. The car's primary samples feed the
evaluator's positive base curve at `Engine+0x24`; the 3-sample `+0x358` data
feeds its subtractive curve at `Engine+0x38`. The other car-specific nine
sample array is indeed at `Engine+0x4c`, not `+0x60`, and the evaluator does
not read `+0x4c`. Do not describe `secondary_curve` as the conditional
positive curve. The `+0x60` pointer starts at a shared static table; later
rebindings, if any, have not been established.

This proves descriptor layout and sampling, not output semantics. The Engine
scalar modifiers, loaded Engine `+0x14/+0x90` state, mode selection, and
conversion of `engine_output` into vehicle force remain unresolved. The
primary curve may support a relative, normalized viewer comparison, but not
retail torque values or absolute acceleration without those missing links and
runtime observations.

The neighboring `DriveTrain` serializer begins at `0x005944a0` and identifies
the component name as `ADriveTrain` (the serializer skips its leading `A`).
Its runtime update at `0x00594cb0` branches on the integer at `DriveTrain+0x44`.
For value `1`, this callback returns without an engine or wheel update. Values
`0` and `2` share its engine-speed response path; value `3` enters a separate
engine-evaluation path. The additional callback at `0x00594da0` also handles
value `3`, adding `Engine+0x7c`, combining the result with ratio data, calling
`0x00594840`, then passing the result to `0x00594a30`. That helper updates
drivetrain and linked wheel state, including a float at `DriveTrain+0x70`,
engine `+0x7c`, and wheel `+0x108` accumulators. These are confirmed consumer
chains from engine and ratio state into drivetrain and wheel state; the mode
labels and units of the accumulated values are still unknown.

The player-side `RealDriver` update at `0x0041e2c0` supplies a further link.
When the host actor's state at `host+0x28` equals `1`, it obtains the component
pointer from `host+0x34`, then the drivetrain pointer from that component's
`+0x4c`, and calls `0x00594910` with `RealDriver+0x2f4`. The setter compares
that float with `DriveTrain+0x14`, global float thresholds `0.95` and `0.05`,
and `1.0`, then selects one of modes `0` through `3` at `DriveTrain+0x44`.
Some branches also replace `DriveTrain+0x14` with `1.0` or `0.0`; others
store the supplied float. This establishes a player-driver-to-drivetrain
state path, but the semantic meaning of `RealDriver+0x2f4` is not established,
so it is not yet safe to call this a gear, clutch, or shift signal.

This explains the otherwise distinct mode-3 path in `0x00594da0`: it also
evaluates the engine curve, adds the engine accumulator at `Engine+0x7c`,
divides through ratio data, applies the `0x00594840` ratio helper, and feeds
the result to `0x00594a30`. The setter call confirms that mode 3 is reachable
from the player driver's update; the numeric mode still has no recovered
gameplay name. The same `RealDriver` tick calls `0x00407400` afterward, but
that routine maintains the driver's own state and timer/output fields; no
additional direct engine input was identified in this pass.

### DriveTrain mode dispatch and wheel update

The setter at `0x00594910` writes `DriveTrain+0x60 = 3` on every call, then
uses the supplied float and global values to choose `DriveTrain+0x44` and
update `DriveTrain+0x14`. The threshold constants decode to `0.95`, `0.05`,
`1.0`, and `0.0`. For ordinary finite inputs, the observed behavior is:

| Supplied input | `+0x44` | `+0x14` |
| --- | ---: | --- |
| Greater than `0.95` | 3 | `1.0` |
| Less than `0.05` | 1 | `0.0` |
| Between `0.05` and `0.95`, greater than previous `+0x14` | 2 | supplied input |
| Between `0.05` and `0.95`, less than or equal to previous `+0x14` | 0 | supplied input |

The endpoints follow the actual x87 comparisons: exactly `0.95` and `0.05`
continue into the middle path. Equality with the previous `+0x14` selects mode
0. The raw branch structure also routes unordered/NaN inputs through special
destinations not represented in the table. The globals decode to ordinary
floats in the retail executable, but their gameplay names and `+0x14`'s role
are not identified. The driver caller supplies `RealDriver+0x2f4`; no evidence
yet ties it to a named control such as gear selection or clutch input.

`0x00594cb0` clears `DriveTrain+0x70` at entry and dispatches on `+0x44`.
Mode 1 returns immediately. Modes 0 and 2 compare Engine `+0x14` with
DriveTrain `+0x4c`, update the state at `+0x58` through a clamped response,
then pass `DriveTrain+0x14 * DriveTrain+0x58` to `0x00594a30`. Mode 3 enters
the other path in this callback: it evaluates the Engine curve and passes the
result to `0x00594a30`. A second callback, `0x00594da0`, also returns unless
the mode is 3; then it evaluates the curve, adds `Engine+0x7c`, scales through
the Engine ratio descriptor, applies `0x00594840`, and passes the combined
result to `0x00594a30`. The meaning of the fields and mode numbers remains
unknown.

The common helper has an early return when `DriveTrain+0x3c == 1`. Otherwise,
it adds its float argument to `DriveTrain+0x70` and subtracts a bounded
contribution from `Engine+0x7c`. The bound is the greater of a global minimum
of `0.25` and the integer at the parent object's `+0x3e4` multiplied by
`0.25`; the result is multiplied by the helper argument. The helper then
updates the four linked Wheel objects. It writes Wheel `+0x108` only when
Wheel `+0x10c` is zero. The per-wheel terms depend on Wheel `+0x11c`, paired
wheel response values at `+0x28`, ratio samples, parent `+0x42c`, and
DriveTrain `+0xec`; some branches use `0.5` or `0.35` to split the response
between paired wheels. This confirms an engine/drivetrain-to-wheel accumulator
path. It does not identify the units of `+0x70`, `+0x7c`, or `+0x108`, nor
does it establish that the last value is torque.

## Per-corner suspension response accumulation

Callback `0x005abb00` provides another four-corner path. It is called from
`0x005b04d0` after a linked-list pass over component callbacks. If its initial
state gate passes, it loops over the four Suspension objects at `mover +0x28`
through `+0x34`. For each object, it reads a referenced record through
Suspension `+0x14`, compares Suspension `+0x1c` with that record's `+0x14`,
and conditionally calls `0x0059e1a0`. The helper's returned scalar is added to
the matching float at `physics_state +0x330 + 4*i`. Those four floats are
cleared at the beginning of `0x005b72d0`'s frame update.

The branch uses x87 comparison flags: less-than, greater-than, and unordered
outcomes skip the helper, while equality reaches it. Treat this as the exact
observed gate rather than assuming a simple positive/negative contact test.
Inside `0x0059e1a0`, `0x0043c9c0` applies the referenced record's affine
transform, whose basis occupies `+0x30..+0x58` and translation occupies
`+0x60..+0x68`, to the Suspension vector at `+0x30..+0x38`.
`0x0043d1e0` then forms a cross product with the referenced record's vector
at `+0xc0..+0xc8`. The helper combines that cross product with
the record vector at `+0xa0..+0xa8`, dots the result with the vector at
`physics_state+0x150..+0x158`, and negates the scalar. A nonpositive scalar
returns zero. Otherwise, the scalar is multiplied by `0.225` and the float at
referenced record `+0x164`; that product is returned and its scaled vector is
passed to `0x0059bc60`, unless global flag `0x0086b36c` is set.

`0x0059bc60` caps the input vector's length at `1000.0` and adds it to the
referenced record's accumulator at `+0x70..+0x78`. It forms a cross product
using the capped vector and the supplied companion vector, caps that result's
length at `10.0`, then adds it to the record accumulator at `+0x80..+0x88`.
These are accumulator limits, not evidence of world units. The accumulator
updates are skipped when `0x0086b36c` is set, but the scalar return remains
the same. The source establishes the arithmetic and destinations; it does
not establish whether the returned scalar is spring force, contact impulse,
or another response.

The confirmed accumulator shell is:

```c
for (int i = 0; i < 4; ++i) {
    Suspension *s = mover->suspension[i];
    Record *r = *(Record **)((byte *)s + 0x14);
    if (x87_gate(s->field_1c, r->field_14))
        physics_state->corner_response[i] += evaluate_suspension(s); // 0x59e1a0
}
```

This callback is distinct from the `0.0044`-second substep loop. Its outputs
are frame-cleared per-corner inputs to later physics processing.

### Consumers of the four accumulated values

The main vehicle update at `0x006012b0` calls `0x005ecac0` with its `dt`
argument. That helper processes each response slot in order. For slot `i`,
with `i = 0..3`, its arithmetic is:

```c
const float gain = 1.0f * 0.0035f / 0.0225f; // about 0.15556 per second
float smooth = state[0x4f8 + 4*i];
smooth += (state[0x330 + 4*i] - smooth) * gain * dt;
state[0x4f8 + 4*i] = smooth;
state[0x4e8 + 4*i] += smooth * dt;
```

The globals decode to `0x784250 = 1.0`, `0x7a5658 = 0.0035`, and
`0x7a5654 = 0.0225`, giving a gain of about `0.15556` per second. The code
applies an explicit first-order response-following step at `+0x4f8..+0x504`,
then integrates that updated value into `+0x4e8..+0x4f4`. It does not clamp
the gain-times-delta-time term in this helper. Other downstream reads are
described below.

A separate stateful component update, `0x0056e6e0` (the response application
is in its `0x0056e820` path), reads those same four values from its owner
pointer at `[this]`. If its local state at `this+4` is nonzero, it subtracts
each value times the float argument at `[esp+0x0c]` from four fields in the
component's records:

| Response field | Destination field |
|---:|---:|
| `owner +0x330` | `this +0x100` |
| `owner +0x334` | `this +0x1d0` |
| `owner +0x338` | `this +0x2a0` |
| `owner +0x33c` | `this +0x370` |

The four destinations are spaced by `0xd0`, matching the repeated record
stride allocated by the constructor at `0x0056e610`. The method also advances
through a small integer state machine and performs owner callbacks; the exact
record field meaning and the time/weight meaning of its float argument remain
unknown. This trace confirms a second consumer, but does not justify naming
the values as force or impulse.

Another routine around `0x005998a0` also reads indexed values from the
`+0x2d0..+0x324` region and accumulates them into a separate record. Its
relationship to the four response slots is not yet established, so it is not
treated as a confirmed consumer here.

The substep routine `0x005b1dd0` has a second, conditional substep loop. It
first calls `0x00593210` with the incoming delta, then enters the loop only
when byte `this+0x506` is set. The count is `trunc(dt / 0.0166667) + 1`, using
the float at `0x007fc858`, and each inner step is `dt/count`. Each pass checks
the integrated slot at `this+0x4f0`; when positive, it reads the three
velocity components at state `+0xa0..+0xa8`, computes their magnitude, and
constructs response vectors using `-state[0x4f0] * speed`. It sends those
vectors to `0x0059bb00`, then invokes `0x005aa480` on the linked component
list rooted at `this+0x130` with the inner delta, followed by `0x005bb0d0` on
the state record at `this+0x68`.

`0x0059bb00` limits the first input magnitude against `0x007a1ad8` (about
`111.76`) and accumulates it into state `+0x110..+0x118`. It computes the
cross product of its two inputs, limits that result against `0x007a1adc`
(`1200`), and accumulates the result into `+0x120..+0x128`. The first response
vector is proportional to velocity in the opposite direction. That resembles
a speed-dependent damping term, but the code does not establish the gameplay
name or units of this response. Helper `0x00593210` refreshes cached vectors
through virtual slots `+0x14`, `+0x08`, and `+0x0c` before the optional loop.

This method has a conditional integration schedule with a 16.67 ms interval.
It is the `Smokeable` vtable's `+0x30` update method, separate from the
`PhysicsMover` component callback at vtable `+0x20`. Its caller and delta
source have not been identified, so the relationship between this 16.67 ms
schedule and the mover's 4.4 ms schedule remains open.

Factory `0x005b41b0` requests an object from the allocator using global
`0x0086b374`; when allocation succeeds, it forwards three arguments to
constructor `0x005b3240` and returns the initialized object. The constructor
initializes the embedded list sentinel at `+0x13c`, installs primary vtable
`0x007a2698` and an embedded list-owner vtable at `this+0x130` pointing to
`0x007a268c`. It then calls initializer `0x005b1540` with a descriptor.
That initializer allocates a child state through global pool handle
`0x0086b2e0`, stores the pointer at `this+0x68`, and copies descriptor
`+0xb0..+0xb8` into child-state `+0x1f0..+0x1f8`. It stores the descriptor at
`this+0x148`, initializes `this+0x150` from descriptor `+0xc4`, and zeros
bytes `+0x504..+0x507`, floats `+0x154` and `+0x168`. It also calls
`0x0059b940` on the child state and conditionally links records from
descriptor lists at `+0x84` and `+0x88` into `this+0x4f4` and `+0x4fc`.
Thus a newly initialized instance has its inner-update flag `+0x506` and
output gate `+0x507` clear. `0x005adf90` is the activation routine: it sets
`+0x506` and byte `+0x19`, increments the active-instance count at
`0x0086b378`, initializes the resource held at `+0x16c`, and initializes the
records in list `+0x4fc` once (tracked by `+0x505`). It also fills `+0x15c`
from the global clock at `0x0086518c` when the field is zero or `-1`.
`Smokeable::Update` calls this routine from response branches at
`0x005b21ac` and `0x005b21d7`.

The deactivation check at `0x005a8d90` clears `+0x506` and `+0x19`, decrements
the active count, and releases the resource at `+0x16c` when the linked object
at `this+0x60`, field `+8`, is null. Otherwise teardown proceeds only when the
global-clock delta and the `0x005a0820` time query exceed two thresholds,
byte `+0x18` is set, and the magnitudes of child-state vectors at `+0xa0` and
`+0xc0` are below their respective thresholds. It then calls `0x0059e400`,
which also clears `+0x506` and releases the resource. The constants and units
behind these age/time and vector checks have not been identified. The
serializer binds `+0x507` as a state
byte, and its getter returns that byte sign-extended; no runtime writer to
`+0x507` has been located yet.

Routine `0x005ae000` provisions a descriptor's child instances. It reads the
count at descriptor `+0x8c`, forms per-item identifiers from the count index
and a table handle at `0x0086b2bc`, resolves each corresponding descriptor
entry, and invokes a factory callback through `0x0086af90`. Each returned
object is initialized by `0x005adb20` and activated by `0x005adf90`, then
stored in a caller-provided pointer array. Two calls from the scheduler at
`0x005ffc02` and `0x005ffc42` show this path is used for paired objects.
Startup code at `0x005b42bb` assigns `0x005b41b0`, the Smokeable factory, to
that same callback slot. Thus these descriptor entries create Smokeable
instances. The scheduler and factory path are confirmed; the remaining open
edge is how the owner descriptor becomes associated with a particular mover
or vehicle.

Destructor `0x005b19b0` reinstalls those vtables at entry while cleaning up,
then resets the primary pointer to `0x007a2168` before returning. Serializer
`0x005b1d00` uses the reflected type string
`Smokeable` (`0x007a272c`). The nested list is passed to `0x005aa480` from the
`Smokeable` update. After its loop, `0x005b1dd0` transforms the state vector
at `+0x30` by
the 4x4 matrix at state `+0x1f0`. The helper `0x00441000` calculates the
result's x/y/z as the matrix's first three rows, including translation from
matrix `+0x30/+0x34/+0x38`. The method subtracts that transformed point from
the position vector at state `+0x20..+0x28`, and subtracts state `+0x168` from
the resulting z component in a four-float local record. It then calls virtual
slot `+0x6c` on `this` (with no stack arguments). The primary vtable at
`0x007a2698` contains the update method at `+0x30` and getter `0x005b1c00`
at `+0x6c`. The getter returns the sign-extended byte at `this+0x507`. The
caller stores whether that result is nonzero at `this+0x490`, unless global
bit `0x007fb724:0` forces the positive branch. On the positive branch, the
method copies a separate 16-byte local record to the object at
`this+0x4e0` offset `+0x50` (and calls `0x006147e0` when that object's `+0xe4`
field is zero), or to `this+0x4a0` when the pointer is null. It then calls
`0x005a8c80` with a local vector record. Finally, while byte `this+0x506` is
set and `this+0x150` is positive, it reduces `this+0x168` by `0.2 * dt` and
clamps it to zero. The virtual call is a gate over the `+0x507` state byte,
not a collision query; the local record's downstream meaning remains
unresolved.

One routine previously grouped here, `0x005a71c0`, is not a frame-time
consumer of the runtime `+0x4e8..+0x504` response bank. Its caller
`0x005ac4f0` passes a vehicle-definition subobject and a blended output block;
the routine reads source tuning fields at the same numeric offsets while
composing that block. The setup-time key-7 blend and its source data are
documented in [traffic-vehicle-definitions.md](traffic-vehicle-definitions.md).

## Preallocated physics component pools

Initializer `0x00598f10` registers named pools through `0x00440df0`. That
helper aligns each record size to four bytes and delegates to allocator
`0x00440b40`; the allocator multiplies record size by the supplied capacity
and stores both values in the pool header. Let `N` be the integer read from
global `0x0089e814` at initializer entry. The observed registrations are:

| Pool name | Record bytes | Capacity argument |
|---|---:|---:|
| `RigidBodySlotPool` | `0x210` | `N + 0x20` |
| `TwoWheelAckermanSteeringSlotPool` | `0xa0` | `N` |
| `WheelSlotPool` | `0x1a0` | `4 * N` |
| `EngineSlotPool` | `0xb0` | `N` |
| `DriveTrainSlotPool` | `0xf4` | `N` |
| `AerodynamicsSlotPool` | `0x40` | `N` |
| `SuspensionSlotPool` | `0xc0` | `4 * N` |
| `MellovatorSlotPool` | `0x250` | `N` |
| `NullMoverSlotPool` | `0x0c` | `N` |

The names establish that the executable models rigid bodies, a two-wheel
Ackermann steering component, wheels, engines, drivetrains, aerodynamics, and
suspension as distinct pooled object types. Wheel and suspension storage is
allocated at four times the common capacity, matching the four Wheel and
Suspension fields serialized by each PhysicsMover. The exact ownership path
from a mover to pool allocation is still open.
The global `N` is passed as the capacity value here, but its source-setting
and broader gameplay meaning are not yet recovered.

## Recreation implications

A WebGL recreation can reproduce the observed frame scheduling with
`n = trunc(dt / 0.0044) + 1` and equal `dt/n` integration intervals. The two
accumulator vectors should be averaged over substeps. The frame-finalization
pass also supplies a verified 120-degree-per-second heading slew, the four
wheel profile slots, a bounded drivetrain/engine table response, a normalized
maximum wheel scalar, and per-corner transform rebuild calls. These frame
outputs give a concrete integration boundary for a renderer, while the
component force equations, collision response, meanings and units of several
state fields, and final mesh placement path still require tracing.

## Evidence

- `0x005a4740` installs the vtable at `0x007a1d68`; the serializer at
  `0x005a47b0` registers `PhysicsMover` fields.
- `0x005a47b0` serializes four Wheel/Suspension pairs via `0x0059c980`
  (`Wheel`) and `0x0059e020` (`Suspension`).
- Factory `0x005f46a0` passes its `+0x30` and `this` pointers to constructor
  `0x005b5cc0`; the constructor stores them at `PhysicsMover+0x04` and
  `PhysicsMover+0x58` (with `+0x40` applied to the latter).
- Lifecycle `0x005f6d50` has a branch that creates a mode-1 controller, then
  invokes `0x005f4400`/`0x005f46a0` on the same owner; mode 1 in the latter
  factory selects the `PhysicsMover` allocation/constructor path.
- `0x0059dea0` serializes the two transform blocks and steering word used by
  `0x0059def0`; that method reads the geometry deltas and writes opposite
  adjusted-angle transforms.
- `0x0059c980` serializes Wheel fields used by `0x0059d030`; the latter rebuilds
  the rotation transform and applies the signed scalar/state gate.
- `0x005b0520` iterates four component entries and calls `0x0059d160`; that
  method gates on its scalar input, computes the cross/dot response, and writes
  the result at child `+0x164`. The callback then calls `0x005abdf0` before
  its second pass sends each transformed child vector and position delta to
  `0x0059bb00` for accumulation at the RigidBody's `+0x110` and `+0x120`.
- `0x0059d600` caps the Wheel response pair and calls `0x0043c9c0` to apply the
  Wheel's affine matrix at `+0x70` to a vector, writing the three-float result
  at `+0x130`. The later pass in `0x005b0520` sends that vector and a
  per-corner position delta to `0x0059bb00`, which accumulates capped vectors
  directly into RigidBody `+0x110` and `+0x120`.
- `0x005b72d0` clears four flag bytes and four floats, invokes the listed
  preparation helpers, computes the substep count from `0x007fc070`, and
  loops through `0x005aa480`.
- `0x0075bed4` performs float-to-integer truncation; `0x007fc070` is the
  float `0.0044`.
- `0x005a5540` performs the post-substep state copies, vector magnitude and
  dot-product calculations, 16-bit heading conversion/slew, four-Wheel field
  refresh, drivetrain/engine table response, wheel-scalar normalization, and
  four calls to `0x005921b0`.
- Constants used by the heading path include `360/65536` at `0x00784248`,
  `65536` at `0x00784238`, `360` at `0x0078450c`, `180` at `0x00784510`, and
  `120` at `0x007a1a28`; the frame update multiplies the last value by `dt`.
- `0x005921b0` checks the per-corner byte at `state+0x3b4+i` and builds the
  corresponding transform matrices for enabled corners. `0x00592400` is
  called during frame finalization with the runtime transform record and
  physics-state output block.
- `0x005aa480` gathers per-component float vectors through virtual slot
  `+0x18`, calls the per-step slot `+0x20`, then integrates those values into
  the component state array returned by `+0x0c` using `state += h * vector`.
  It calls embedded-owner slot `+0x04` after the list; constructor
  `0x005b5cc0` installs vtable `0x007a25d4`, resolving that call to
  `0x005b04d0`. This method calls child slot `+0x1c`, `0x005abb00`, and then
  `0x005b0520` through owner slot `+0x08`, placing those Suspension and
  four-corner response callbacks inside the same substep loop. The frame loop
  separately accumulates the two three-float groups from physics state
  `mover+0x20` at `+0x130/+0x140` before `0x005a5540`.
- `0x005a49a0` reads four enabled/index pairs and averages the selected
  values from the global 0x98-byte-stride record array; in state `1` that
  average feeds the shared command-source `+0x20c` setter, with a `0.4`
  threshold override.
- `0x005a5540` copies each Wheel's `+0x11c` flag and `+0x114` index into those
  four physics-state slots, updates the active count at `+0x3e4`, and mirrors
  Wheel fields `+0x24/+0x28/+0x164` into per-corner state arrays.
- `0x005ab990` updates all four serialized Suspension objects through
  `0x0059e0c0` and writes the shared `0x007fc6f0` pointer into each paired
  Wheel object's `+0x2c` field.
- `0x005b04d0` calls `0x005abb00`; that routine conditionally accumulates the
  four `0x0059e1a0` return values into `physics_state +0x330..+0x33c`.
- `0x00594220` serializes `Engine`; `0x00594300` samples its three curves and
  returns the offset-level output described above. `0x005944a0` serializes
  `DriveTrain`; `0x00594cb0` forwards that engine result to `0x00594a30` in
  mode 3. Mode 1 returns immediately; modes 0 and 2 use the separate
  engine-speed response path.
- `0x006012b0` passes its frame delta to `0x005ecac0`, which filters all four
  response fields into `+0x4f8..+0x504` and integrates the filtered values
  into `+0x4e8..+0x4f4`.
- `0x0056e6e0` conditionally applies the four response fields to four
  `0xd0`-spaced component records, subtracting each scaled value from the
  corresponding field at offsets `+0x100`, `+0x1d0`, `+0x2a0`, and `+0x370`.
- `0x005b1dd0` uses `this+0x4f0` as a positive-only gate and scale for a
  body-velocity-dependent vector passed to `0x0059bb00`; the routine has a
  conditional `dt / 0.0166667` inner schedule and updates a linked component
  list. `0x0059bb00` accumulates one bounded vector and a bounded cross product.
- Factory `0x005b41b0` allocates from the pool/allocator handle at `0x0086b374`
  and calls constructor `0x005b3240` with three forwarded arguments. The
  constructor initializes list sentinel `+0x13c`, installs primary vtable
  `0x007a2698` plus list-owner vtable `0x007a268c` at `this+0x130`, then calls
  initializer `0x005b1540`. The initializer allocates the associated
  state block via handle `0x0086b2e0`, saves the descriptor, copies three
  floats into the state matrix block, and clears the update/emit flags.
  Destructor `0x005b19b0`
  resets the primary pointer to `0x007a2168` at exit. Serializer `0x005b1d00`
  uses the
  reflected type string `Smokeable` at `0x007a272c`; the primary vtable holds
  update method `0x005b1dd0` at `+0x30` and getter `0x005b1c00` at `+0x6c`.
- `0x00441000` transforms a three-float point by the first three rows and
  translation column of a 4x4 matrix. `0x005b1dd0` uses it with state `+0x30`
  and matrix `+0x1f0`. The primary vtable `0x007a2698` maps slot `+0x6c` to
  `0x005b1c00`, which returns the sign-extended byte at `this+0x507`; the
  caller stores its nonzero state at `this+0x490` and branches to write one of
  its transform records.
- `0x005a71c0` folds the response bank into weighted fields in a separate
  record during setup, not frame-time response processing.
- `0x00598f10` registers the named component pools. `0x00440df0` forwards the
  record size and capacity, and `0x00440b40` multiplies them for allocation;
  wheel and suspension capacities are computed as `4 * N`.

## Mode-1 component-list type resolution and accumulator edge (r286, 2026-10-16)

The mode-1 `PhysicsMover` list can now be associated with its concrete pooled
objects. Constructor `0x005b5cc0` first appends its RigidBody (`mover+0x20`),
then four Wheels and four Suspensions, then Engine at `mover+0x48` and DriveTrain
at `mover+0x4c`. Pool registration `0x00598f10` maps the RigidBody to
`RigidBodySlotPool` (`0x0086b2e0`) and the remaining allocations to
`WheelSlotPool` (`0x0086b2e8`), `SuspensionSlotPool`
(`0x0086b2f8`), `EngineSlotPool` (`0x0086b2ec`), and `DriveTrainSlotPool`
(`0x0086b2f0`). The `TwoWheelAckermanSteering` at `mover+0x24` is constructed
separately and is not one of these linked-list entries. The `Aerodynamics`
pool object at `mover+0x50` is also separately attached by this constructor.

The vtables resolve the callbacks used around `0x005aa480` and
`0x005b04d0`:

| List object | Constructor / vtable | `+0x18` gathered values | `+0x1c` post-integration callback | `+0x20` substep callback |
| --- | --- | --- | --- | --- |
| RigidBody (one) | `0x005a7f30` / `0x007a20c4` | `0x0059c1e0` | `0x0059c5f0` | `0x005a80d0` |
| Wheel (four) | `0x005a8160` / `0x007a20e8` | `0x0059d350` | `0x0059d030` | `0x0050d510` |
| Suspension (four) | `0x005a86c0` / `0x007a210c` | `0x0059e130` | `0x005a8790` | `0x0050d510` |
| Engine | `0x005aa5a0` / `0x007a235c` | `0x005a1290` | `0x005aa760` | `0x0050d510` |
| DriveTrain | `0x005af9d0` / `0x007a2508` | `0x005949b0` | `0x005a1400` | `0x0050d510` |

For Wheel, Suspension, Engine, and DriveTrain, `+0x20` resolves to
`0x0050d510`, which returns zero. The RigidBody's `+0x20` resolves instead to
`0x005a80d0` (it returns the shared pointer `0x007fc694`). The `0x005aa480`
loop gathers each object's `+0x18` vector, calls `+0x20`, and integrates the
vector through the object's `+0x0c` state pointer. The owner's `0x005b04d0`
callback then calls each type's `+0x1c` method before its explicit four-corner
and drivetrain passes. This identifies the virtual list entries and their
concrete work boundaries; it still does not label every flattened state
component with physical units.

The force-accumulator owner is resolved: Suspension `+0xa4` is the RigidBody
at `mover+0x20`, and `+0xa0` is its Wheel. The capped Wheel vector and moment
accumulate directly into RigidBody `+0x110..+0x128`. RigidBody's generic list
`+0x18` callback `0x0059c1e0` reads those accumulators while gathering its
0x20-float derivative vector; `0x005aa480` integrates that vector into the
RigidBody state array at `+0x20` with the current substep `h`. After integration,
RigidBody `+0x1c` callback `0x0059c5f0` copies the force/moment accumulators to
`+0x130..+0x148`, clears `+0x110..+0x128`, and conditionally adds a damping
term back through `0x0059bb00`. Then the owner callback runs
`0x005b0520`, which adds the next Wheel response. This establishes a real
source-backed force-to-integrated-state path and its substep scaling, but the
0x20 state elements, mass/inertia factors, and relationship to rendered chassis
position/yaw are not fully identified; no numeric browser force or steering
parameter is justified by this trace alone.

## RigidBody state fields, mass conversion, and mover transform handoff (r290, 2026-10-16)

Further tracing gives a partial semantic map for the 32-float state returned by
RigidBody vtable slot `+0x0c` (`this+0x20`):

| RigidBody offset | Evidence-backed role | Evidence |
| --- | --- | --- |
| `+0x20..+0x28` | Position vector | `0x0059c1e0` emits `+0xa0..+0xa8` as its derivative; `0x005a5540` copies these fields to the mover's runtime transform block at `mover+0x58`, offset `+0x20`. |
| `+0x30..+0x6c` | 4x4 orientation/transform matrix | Constructor initializes this block to identity. `0x0059c1e0` gathers its derivative through `0x0059c120`; `0x005a5540` copies it to the runtime transform block at offset `+0x50`. |
| `+0x70..+0x78` | Linear momentum-like state vector | `0x0059c1e0` places integrated force terms into derivative slots `+0x50..+0x58`, which integrate into these fields. `0x0059c5f0` multiplies this vector by the inverse-mass field at `+0x168` to produce `+0xa0..+0xa8`, the position derivative. |
| `+0x80..+0x88` | Angular momentum-like state vector | `0x0059c1e0` places integrated moment terms into derivative slots `+0x60..+0x68`, which integrate into these fields. The post-step callback converts this state using `+0x90` to produce angular-rate fields at `+0xc0..+0xc8`. |
| `+0xa0..+0xa8` | Linear velocity / position derivative | Produced from `+0x70..+0x78` and inverse mass by `0x0059c5f0`; copied to derivative slots 0–2 by `0x0059c1e0`. |
| `+0xc0..+0xc8` | Angular-rate vector used by the matrix derivative | Produced by `0x0059c5f0`; consumed by `0x0059c120` while differentiating the orientation matrix. |

The descriptor supplied to the RigidBody constructor is retained at `+0x14`.
`0x0059b940` reads its first float into `RigidBody+0x164` and stores
`1.0 / descriptor[0]` at `+0x168`. The latter is the inverse-mass coefficient
used above. The same setup routine derives 4x4 blocks at `+0x170` and `+0x1b0`
from descriptor data beginning at `+0x10` and `+0x50`; their precise inertia
tensor meaning and coefficient units are not established by these callsites.
The global vehicle export's candidate mass field has not yet been traced to
this constructor descriptor, so it cannot yet provide a per-car browser value.

At frame finalization, `0x005a5540` copies the integrated position and matrix
from the RigidBody into the runtime transform block referenced by
`PhysicsMover+0x58`: destination offsets `+0x20` and `+0x50`, respectively.
It also forwards the derived linear and angular-rate vectors to destination
offsets `+0x30` and `+0xd0`. This proves transfer into the mover's instance
transform record. The downstream consumer that turns this record into the
visible chassis/world render matrix, including its yaw extraction convention,
is still missing. No numeric WebGL handling change is justified until the
descriptor-to-export mapping and the remaining inertia/transform consumers are
resolved.

## Steering-to-wheel and heading trace boundary (r291, 2026-10-16)

The mode-1 `PhysicsMover` constructor `0x005b5cc0` allocates the Ackermann
object, installs vtable `0x007a2084`, stores the runtime geometry pointer at
Ackermann `+0x04`, and sets `+0x08` to the matrix block at `+0x10`. It invokes
the object's virtual `+0x04` update once during construction. The Ackermann
object is stored separately at `mover+0x24`; the generic list walked by
`0x005b04d0` is the Wheel/Suspension/Engine/DriveTrain list, so that walk does
not prove a per-frame Ackermann update or a write to its input word `+0x90`.

The wheel side of the chain is concrete after matrices exist: Suspension
post-update `0x005a8790` reads Ackermann `+0x08`, selects one 64-byte transform
using its corner index `+0xac`, and updates its Wheel. In the mover force step
`0x005b0520`, each wheel is evaluated and then its active force vector is
passed to `0x0059bb00` with the RigidBody as owner. Static setup does not map
the four corner indices to front/rear or left/right, and neither this force
step nor the linked component callbacks identify the writer that changes
Ackermann `+0x90` from RealDriver steering. Thus Ackermann geometry and its
wheel-transform consumer are known, but normalized steering-to-tire force is
not recoverable from these edges alone.

The body-to-heading transfer is now narrower than the earlier unknown-render
edge. `0x005a5540` copies the integrated RigidBody orientation block at `+0x30`
to the mover instance transform block at `mover+0x58` offset `+0x50`; it then
passes two values from that block (`+0x50/+0x54`) to fixed-angle helper
`0x0043c570` and stores the returned word at instance offset `+0x3cc`. The
helper selects a 16-bit angle through quadrant/sign handling and the retail
angle lookup table. This identifies where an instance heading word is derived,
but not the final drawable transform consumer, the world-axis sign mapping, or
whether that heading word is the same convention as the browser's map-plane
heading. No steering/yaw coefficient follows from this transfer.

## Ackermann input writer xref audit (r292, 2026-10-16)

An instruction scan of the complete `.text` disassembly found one word-width
write to object offset `+0x90` in the vehicle-physics address range:
`0x0059defe`, inside `TwoWheelAckermanSteering::Update` at `0x0059def0`. It
loads the incoming 16-bit stack argument (`[esp+0x10]` after its prologue) and
stores it at `[this+0x90]`. Thus this function is the setter as well as the
matrix rebuild; there is no separate direct field store in `.text` to trace
back to RealDriver.

The vtable bytes at `0x007a2084` are destructor `0x005a4770`, update/setter
`0x0059def0`, accessor `0x005a4760`, and serializer `0x0059dea0`. In the code,
the only direct installation of that vtable is at `0x005b5e0d`; the constructor
at `0x005b5cc0` then stores the object at `mover+0x24` and makes an indirect
`+0x04` call at `0x005b5d9d`. This is the only identified callsite, and the
disassembly at that call has no nearby explicit stack-argument push. The
remaining invocation path may be indirect through runtime dispatch, but no
identified site supplies a RealDriver-derived word. The generic callback walk
at `0x005b04d0` calls linked components' `+0x1c` callbacks; Ackermann is
separately attached, so that walk does not close the writer edge.

The RealDriver output is independently known: `0x0041dfc0` packs its result to
`RealDriver+0x1e0`. Current traces show that value being used as a heading
target by mover finalization and read in the mover's mode gate, but no
instruction-level transfer from it to Ackermann update's stack argument.
Likewise the GLOBALB row `+0x380` value is copied into setup-object `+0x2a0`,
but its steering consumer remains unproven. The direct-writer scan therefore
narrows the missing edge to the caller/dispatcher supplying the Ackermann
setter's 16-bit argument; it does not justify mapping either RealDriver
`+0x1e0` or GLOBALB `+0x380` to that argument.

## Ackermann per-frame input handoff (r293, 2026-10-16)

The setter caller gap described in r292 is closed by the recurring mover update
path. The PhysicsMover vtable is installed as `0x007a25e0` in constructors
`0x005b039a` and `0x005b5d04`; its update entry is `0x005b72d0`. That method
clears per-frame state, calls `0x005ab990`, `0x005a49a0`, then calls
`0x005abbf0` with the same mover in ECX at `0x005b732d`. It continues with
`0x005a4d80`, `0x005b6300`, and the fixed-step update loop. The `dt` argument
is at `[ebp+8]`, so this is the per-frame mover path before its substeps.

In `0x005abbf0`, `this+0x04` is loaded as the owner at `0x005abc30` and
`0x005abcac`. Several control/mode branches gate preceding float-command
handling, but the Ackermann handoff is reached after those branches converge.
At `0x005abd1f`, the function executes `movzx edx, WORD PTR [edx+0x1e0]`;
`edx` still holds the owner pointer, so the source is the low 16-bit word at
`owner+0x1e0`. At `0x005abd26`, it loads Ackermann from `mover+0x24`, then at
`0x005abd2c` dispatches its vtable `+0x04` with that zero-extended word as the
stack argument. The vtable installed at `0x007a2084` maps that slot to
`0x0059def0`, which stores the word at Ackermann `+0x90` and rebuilds its wheel
transforms. Separately, `0x0041dfc0` is the known producer of RealDriver
`+0x1e0`; this establishes the full static data path from the RealDriver output
field through the mover update to Ackermann `+0x90`.

This proves the field handoff, but does not establish the numerical encoding
or units of the word, whether all RealDriver modes use the same convention, or
how resulting wheel transforms scale into suspension/tire forces and chassis
yaw. The exact conversion of GLOBALB row `+0x380` into its setup field remains
unconnected to this path. Do not infer a WebGL steering coefficient from the
handoff alone. A runtime capture should compare RealDriver `+0x1e0`, Ackermann
`+0x90`, Ackermann wheel matrices, suspension forces, and body yaw over one
controlled steering sweep.

## RealDriver steering word response and angular units (r294, 2026-10-16)

`0x0041dfc0` does not pack a normalized input directly into an angle. Its
branch order is: if global dword `0x0081cb8c` is nonzero, call alternate
`0x00406a40`; otherwise call `0x005c1000`, dispatching to active-controller
`0x004061c0` when true and ordinary `0x00416bb0` when false. In the ordinary
routine, the profile mode is 1 only when globals/flags
`0x89e7d8`, `0x890108`, `0x890114`, `0x89e7d9`, `0x890104`, `0x89e7e2`,
`0x890100`, and `0x890110` take the all-clear branch; otherwise mode 4 is
selected. The two tables have distinct nonlinear signed samples over input
`[-1,+1]`.

For that ordinary branch, the sampled profile is combined with owner
`+0x270`, speed, throttle/e-brake/brake commands and steering-state curves.
`0x00405dc0` and `0x004060e0` produce baseline and rate responses. The
`[-45,+45]` degree clamp at `0x00416cf0..0x00416d24` applies only to the first
intermediate. The routine then combines that value with prior steering angle,
rate response times `dt`, and caller-derived bounds. Only after this stateful
combination does it convert the selected angle to packed turns: multiply by
`0x784238` at `0x00416d90`, convert to integer, and store the low word at
`steering+0x7c` (`0x00416db4`). The caller returns this packed word and stores
it at `RealDriver+0x1e0`.

The packed representation is established exactly: Ackermann `0x0059def0`
converts a word unit with `0x784240 = 2*pi/65536`, or about
`0.0000958738 rad` (`0.00549316 degrees`) per unit. Before packing, the ordinary
routine decodes the previous word with `360/65536` degrees per unit and wraps
values at 180 degrees by subtracting a full turn. Thus the unit scale is known,
but the final angle for a requested input cannot be calculated without the
selected branch, timestep, speed, owner scalar, state curves, and prior state.
The intermediate ±45-degree clamp is not a proven final steering lock.

The WebGL player uses a viewer-defined `±0.55 rad` wheel-angle/yaw model and
quantizes it to the same 16-bit turn grid. Its gamepad path applies the
controller profile from `0x7e9e88`, matching that input curve in the
`0x004061c0` branch; keyboard endpoints are also `±1`. Neither the wheel-angle
cap nor the speed-to-yaw equation follows from the retail word units. No
numeric handling update is justified by this source trace alone.

## Per-car RigidBody mass mapped to GLOBALB (r298, 2026-10-16)

The candidate mass field at GLOBALB stock row `+0x220` is now linked to the
mode-1 RigidBody constructor. Definition selection at `0x005b4dc0` passes row
`+0x110` to copy helper `0x005ac4f0`, which copies `0xac` dwords into the
selected definition block at `+0x30`. The selected base returned by
`0x005b5350` is that copied `+0x30` block and is retained by PhysicsMover at
`mover+0x54`. Constructor `0x005b5cc0` passes `mover+0x54+0x110` to RigidBody
constructor `0x005a7f30` at `0x005b5d61`. Under the copy offset, this pointer
is exactly GLOBALB row `+0x220`. RigidBody initializer `0x0059b940` reads
`descriptor[0]` into `RigidBody+0x164` and stores its reciprocal at `+0x168`,
which is used by the integrated-state update. This proves GLOBALB `+0x220` is
the RigidBody mass parameter, although the mass unit scale is not named by the
binary.

The garage exporter already includes the 29 stock values as
`powertrain.mass_tonnes`; no extraction change is required. `updatePlayer`
now uses `referenceMass/sourceMass` as a relative inverse-mass factor on top
of its existing engine-curve ratio, preserving the 240SX reference baseline.
This follows the RigidBody mass/inverse-mass path but remains a viewer
approximation: Engine curve output units, torque-to-wheel force, drivetrain
ratio effect on tractive force, and the absolute 240SX acceleration baseline
are not resolved. A short Chrome drive can check the integration and car-to-car
relative effect, not retail-equivalent acceleration.

Other per-car fields have different evidence limits. An audit against the
installed `GLOBALB.BUN` (SHA-256
`07e1c37a2e5ba4b3e6a17b257e653409c31d5f0b3f9df3464aec36adbbced9d0`) corrects
the earlier descriptor count: the four Suspension constructors do not receive
four unique 0x20-byte records. At `0x005b5ec2..0x005b5eca`, the corner index is
divided by two and multiplied by `0x20`; the pointer passed at
`0x005b5ee0` is `mover+0x54+0x170 + 0x20*floor(index/2)`. Constructor
`0x005a86c0` stores that pointer at Suspension `+0x14`. Thus corners 0 and 1
share GLOBALB row `+0x280`, and corners 2 and 3 share row `+0x2a0`; the next
record at `+0x2c0` is the stock gearbox block, not a suspension descriptor.

The two actual stock 240SX blocks at absolute offsets `0x6c7940` and `0x6c7960`
decode as eight little-endian floats each:

| Record | `+0x00` | `+0x04` | `+0x08` | `+0x0c` | `+0x10` | `+0x14` | `+0x18` | `+0x1c` |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| GLOBALB `+0x280` (corners 0/1) | 5.3 | 37 | 5.4 | 5.6 | 36 | 0.16 | -0.16 | 0.15 |
| GLOBALB `+0x2a0` (corners 2/3) | 5.2 | 30 | 5.35 | 5.65 | 35 | 0.16 | -0.16 | 0.15 |

Across all 31 type-0 stock rows, the corresponding first/second-block ranges
are: `+0x10` 20–45 / 23–45, `+0x14` 0.12–0.20 / 0.12–0.20, `+0x18`
-0.22–-0.12 / -0.22–-0.12, and `+0x1c` 0.045–0.15 / 0.05–0.15. These
are verified source values, but their units and physical labels are not
recovered.

The update callsites expose only a partial mapping. `0x005a8790` uses descriptor
`+0x14` and `+0x18` as the upper/lower clamp values for Suspension state
`+0x1c`; it also uses descriptor `+0x10` in the fallback calculation
`Suspension+0x124 = Suspension+0x190 * Suspension+0x108 / descriptor+0x10`.
At `0x005abb00`, the per-corner response helper `0x0059e1a0` is called only
when Suspension `+0x1c` compares equal to descriptor `+0x14`; its scalar is
then added to physics-state `+0x330+4*i`. This gate and scale path do not give
the values physical units, a spring/damper interpretation, or a force
coefficient that can be applied to the viewer's simplified acceleration/yaw
model. The other descriptor words, the semantics of Suspension `+0x1c` and
`+0x108/+0x190`, and the downstream force units remain unresolved. Therefore
the stock data can be recorded for future reverse engineering, but cannot yet
support handling changes. No per-car brake or drag coefficient has been
connected to the driver brake path or separate Aerodynamics component.
The existing wheel-anchor/radius/width records are exported from row
`+0x120..+0x1df`. No GLOBALB field has been connected to a per-car brake
coefficient or a drag coefficient consumed by the separate Aerodynamics
component. The exported `+0x380` value remains unverified as a steering
parameter, and runtime surface profile coefficients are not a per-car tire
friction field. These records do not support further `updatePlayer` tuning
without consumer and unit traces.

## Engine-to-wheel acceleration trace follow-up (r299, 2026-10-16)

The static Engine/DriveTrain path now has a clearer boundary, but it still
does not yield a retail acceleration coefficient. Engine constructor
`0x005aa5a0` receives the selected GLOBALB block at `mover+0x54+0x1f0` and
builds the sample tables described above. It initializes Engine `+0x74` and
`+0x84` to zero, `+0x88` to false, `+0x8c` to `-1`, `+0x90` to `1.0`, and
`+0x98` from helper `0x00457b00`. Thus `0x00594300`'s multiplication by
`Engine+0x90` starts at unity; this is not a recovered per-car torque
multiplier. `0x005aa760` updates `Engine+0x14` against limits in the runtime
descriptor at `Engine+0x18`, changes the `+0x88` state at its thresholds, and
clears `Engine+0x7c`. Evaluator `0x00594300` samples the primary and
subtractive curves using `Engine+0x14`, then applies state, shared-table, and
mode factors. The GLOBALB curves therefore specify part of a stateful engine
response, not a standalone force or torque curve.

The transmission handoff remains state-dependent. `0x00594cb0` uses
`DriveTrain+0x44` to select a no-op mode, two engine-speed response modes, or
the direct Engine-evaluation mode; `0x00594da0` advances the mode-3 branch and
combines Engine output and `Engine+0x7c` through ratio helpers. Common helper
`0x00594a30` accumulates its input at `DriveTrain+0x70`, changes `Engine+0x7c`,
and apportions response to linked Wheel `+0x108` values using DriveTrain
state, per-wheel response values, and differential/ratio data. The resulting
Wheel state passes through the Wheel response limiter and Suspension callback
before a vector is accumulated on the RigidBody. The arithmetic establishes
the handoffs, but none of these scalar accumulators has a proven physical unit.

Consequently, GLOBALB primary samples plus exported gear ratios and wheel
radii still cannot produce source-backed acceleration. The minimum runtime
capture needed to continue is one straight player pull recording the active
Engine/DriveTrain mode (`DriveTrain+0x44`, `+0x3c`), ratio/control state
(`DriveTrain+0x14`, `+0x58`, `+0xec`), Engine operating point and modifiers
(`Engine+0x14`, `+0x7c`, `+0x74`, `+0x84`, `+0x88`, `+0x8c`, `+0x90`, and
descriptor `+0x18`), each driven Wheel's `+0x108`, `+0x10c`, `+0x11c`,
`+0x124/+0x128`, `+0x130`, Suspension force output, RigidBody force
accumulators, and before/after RigidBody velocity. That capture must establish
which mode and wheel response branch the free-roam player uses and correlate
the final body velocity change to the exported per-car values. Until then,
`updatePlayer`'s acceleration baseline and curve-to-acceleration conversion
remain approximate; no additional numeric viewer change is justified.

## Engine-to-RigidBody longitudinal force boundary (r322, 2026-10-16)

A second pass over the executable confirms that the extracted GLOBALB tables
and constants do not determine longitudinal acceleration. The execution path
has dynamic branch/state inputs at every handoff:

- `0x00594cb0` first branches on DriveTrain `+0x44`. In the mode-1 branch it
  calls Engine evaluator `0x00594300` on the object at DriveTrain `+0x20`, then
  passes the returned float to `0x00594a30`. Modes 0 and 2 instead read
  Engine `+0x14`, DriveTrain `+0x4c`, `+0x58`, `+0x14`, and `+0x18`, clamp
  the operating-point difference using global `0x007fbf34`, and also call
  `0x00594a30`. Thus mode and current runtime state choose which output enters
  the drive update.
- `0x00594300` samples the primary engine curve using Engine `+0x14` scaled
  by `0x007a1d28` (the RPM conversion factor), then multiplies by Engine
  `+0x90`; optional terms depend on `+0x74`, `+0x84`, owner `+0x2ac`,
  `+0x88`, `+0x80/+0x78`, `+0x8c`, and the subtractive curve at `+0x38`.
  The output is therefore not the static primary sample alone. Even if every
  GLOBALB sample and shared table constant is known, Engine operating point
  and modifiers are required to evaluate this call exactly.
- `0x00594da0` runs only when DriveTrain `+0x44 == 3`. It combines the same
  Engine evaluator output with Engine `+0x7c`, accesses the current indexed
  ratio using DriveTrain `+0x3c` and the ratio block at `+0x18`, calls ratio
  helper `0x00594840`, and combines additional terms using DriveTrain `+0xec`
  and related ratio/config pointers before forwarding the result to
  `0x00594a30`. The two mode-3 tests also gate optional contributions on
  configuration values. The executable proves this is stateful ratio and
  accumulator logic; it does not equate `+0x3c` with a browser gear number
  without observing the active object at runtime.
- `0x00594cb0` clears DriveTrain `+0x70` before mode dispatch. For modes
  other than `+0x3c == 1`, `0x00594a30` adds its incoming value there, then
  apportions an update to linked Wheels' `+0x108`. That calculation reads
  DriveTrain `+0x14/+0x18/+0x24..+0x34`, `+0x3c/+0xec`, the parent object
  reached through DriveTrain `+0x34` at `+0x42c`, Wheel `+0x11c/+0x10c/+0x28`,
  and paired-Wheel state.
  Contact flags gate the contribution, and Wheel `+0x10c` gates the write to
  `+0x108`. The directly visible result is a Wheel accumulator, not body
  acceleration.
- `0x005abdf0` combines each Wheel's `+0x108` with its response pair
  `+0x124/+0x128`, surface/profile values, paired Wheel terms, and transforms.
  `0x0059d600` bounds the result using Wheel contact and surface state and
  writes transformed response at `+0x130`. `0x005b0520` sends that vector and
  its application-point offset to `0x0059bb00`, which adds bounded linear and
  cross-product accumulators to the RigidBody. Only the subsequent RigidBody
  integration and mover finalization produce body velocity/pose.

The narrow missing edge is the live mapping from player controls to the active
DriveTrain mode/gear/clutch state and its runtime Engine modifiers, then from
Wheel response/contact state through Suspension/RigidBody to the measured
velocity change. The best next capture is one tagged player-car straight pull
through launch and at least one shift, recording DriveTrain `+0x44/+0x3c`,
`+0x14/+0x18/+0x24..+0x34/+0x58/+0xec/+0x70`, Engine `+0x14/+0x18/+0x74/
+0x78/+0x80/+0x84/+0x88/+0x8c/+0x90/+0x7c`, four Wheels' `+0x108/+0x10c/
+0x11c/+0x124/+0x128/+0x130`, the paired Suspension outputs, RigidBody
accumulators `+0x110..+0x128`, momentum `+0x70..+0x78`, and derived linear
velocity `+0xa0..+0xa8` before/after each 4.4 ms substep.
This establishes which branches execute and supplies the only missing values
needed to test whether exported GLOBALB data is sufficient to reproduce each
observed acceleration increment. Without that trace, static values cannot
produce exact retail acceleration; no viewer acceleration change is warranted.

## Wheel accumulator to rigid-body velocity and heading (r357)

The four-corner path reaches the body through concrete state slots. In the
second pass of `0x005b0520`, each active Suspension supplies its Wheel's
transformed response vector (`Wheel+0x130..+0x138`) and the offset from body
position to application point (`Wheel+0x140..+0x148 - RigidBody+0x20..+0x28`)
to `0x0059bb00`. That helper caps response magnitude at `0x7a1ad8` (111.76)
and adds it to the RigidBody vector accumulator `+0x110..+0x118`. It computes
the cross product of response with application-point offset, caps its
magnitude at `0x7a1adc` (1200), and adds it to the moment accumulator
`+0x120..+0x128`. Thus the statically recoverable moment relation is
`M += cap(cross(F, r), 1200)` after response capping. The executable fixes
these operands and caps; it does not label their physical units or provide
the live per-wheel response.

The RigidBody `+0x18` callback `0x0059c1e0` gathers the state derivative:
`+0xa0..+0xa8` (linear velocity) goes into position derivative `+0x00..+0x08`;
the orientation-matrix derivative from `0x0059c120` goes into `+0x10..+0x4c`;
linear accumulator `+0x110..+0x118` goes into momentum derivative
`+0x50..+0x58`; and moment accumulator `+0x120..+0x128` goes into angular
momentum derivative `+0x60..+0x68`. `0x005aa480` integrates these 32 floats
with its current substep, so the force and moment derivatives update the
stored linear momentum at `+0x70..+0x78` and angular momentum at
`+0x80..+0x88`, respectively. The following RigidBody `+0x1c` callback
`0x0059c5f0` multiplies momentum `+0x70..+0x78` by inverse mass `+0x168` to
produce linear velocity `+0xa0..+0xa8`.

Angular state is similarly concrete, but is not a direct steering-to-yaw
formula. The RigidBody constructor `0x0059b940` derives 3x3 matrix blocks at
`+0x170` and `+0x1b0` from the RigidBody descriptor; helper `0x43c7c0`
computes an inverse from descriptor matrix data. During `0x0059c5f0`, the
state flag at `+0x158` selects
one matrix, which is combined with the current orientation matrix and
angular-momentum vector `+0x80..+0x88` to produce angular-rate vector
`+0xc0..+0xc8`. The angular-rate magnitude is bounded by the global value at
`0x7a1b1c` (250). `0x0059c120` uses that rate to build the orientation
derivative. Finally, `0x005a5540` copies the integrated orientation into the
mover transform and extracts its heading word at instance `+0x3cc`.

This confirms the chain `wheel response → bounded force/moment accumulators →
linear/angular momentum → velocity/angular rate → integrated orientation →
heading`. It still cannot yield a numeric player yaw from steering alone:
the active player's four Wheel responses, contact/profile state, application
points, selected inertia branch, and driver-to-mover identity are runtime
values. Although the generic mode-1 constructor maps the RigidBody mass to
GLOBALB, the descriptor's inertia matrices and their player instance have not
been connected to the live free-roam car. No steering or yaw coefficient was
changed. The next useful extraction remains a pointer-linked player sweep
through `tools/capture_nfsu2_player_steering.gdb`, augmented with the four
Wheel `+0x130/+0x140`, RigidBody `+0x110..+0x128`, `+0x70..+0xc8`, selected
inertia matrix, mover orientation, and extracted heading.

## Ackermann geometry resolves to per-car GLOBALB anchors (r359, 2026-10-16)

The mode-1 factory geometry pointer resolves to the selected car's copied
GLOBALB row. Factory `0x005f46a0` loads `resource = *(actor+0x10)` and calls
`0x005b5350` with `this = resource+0x10`. If lazy initialization is needed,
`0x005b5350` calls `0x005b4fc0`, which dispatches to `0x005b4dc0`. That
method uses the dword at `resource+0x10` as the row index to address the
`0x890`-byte GLOBALB record, copies `record+0x110` to `resource+0x40`, and
returns that copied base. The factory passes the same base to
`0x005b5cc0`, which stores it at `mover+0x54`; Ackermann setup stores it
unchanged at `steering+0x04`.

This makes the Ackermann geometry reads exact source offsets, because its
geometry pointer is the copied row base:

| Ackermann read | GLOBALB row field | Exported anchor meaning |
|---|---|---|
| `geometry+0x10` | `record+0x120` | anchor 0 forward/X |
| `geometry+0x14` | `record+0x124` | anchor 0 lateral/Y |
| `geometry+0x44` | `record+0x154` | anchor 1 lateral/Y |
| `geometry+0xa0` | `record+0x1b0` | anchor 3 forward/X |

Accordingly, the disassembled deltas are `anchor0.X - anchor3.X` and
`anchor0.Y - anchor1.Y`; they are the source's first-left-to-fourth-left
longitudinal span and first-to-second lateral span. The garage exporter reads
these same four anchor records from `record+0x120` with `0x30`-byte spacing.
For the stock 240SX, these inputs are approximately `2.43878` and `1.46000`
GLOBALB coordinate units. The exported per-car anchors can therefore ground
the Ackermann geometry used by the viewer; its current max/min wheelbase and
track summaries coincide for symmetric anchor sets but are not the most
literal source fields for asymmetric sets.

The r360 follow-up maps both Ackermann matrices to front anchor indices and
the exported wheel placements, and decodes their signed-angle assignment.
`steering+0x10` is anchor 0 (positive lateral); `steering+0x50` is anchor 1
(negative lateral). Positive input assigns the adjusted angle to anchor 0 and
the raw angle to anchor 1; negative input assigns negative raw to anchor 0 and
negative adjusted to anchor 1. The remaining viewer-specific uncertainties are
the exported tire mesh's local rotation axis/handedness and whether the viewer's
steering angle has the same semantic meaning and sign as the retail Ackermann
word. The r361 geometry audit confirms the viewer's wheel axes and documents the
remaining signed-spin comparison limit. See
`ackermann-corner-matrix-side-sign-r360.md` and
`player-wheel-axis-handedness-r361.md`. Body yaw still depends on the runtime
response path described above.

The stock wheel mesh geometry and exporter axis conversion are checked in
`player-wheel-axis-handedness-r361.md`: the tire axle is GLB local X and the
viewer correctly spins around X and steers around Y. The exporter axis map has
negative determinant, so retail Wheel angle `+0x24` and a right-handed GLB X
rotation have opposite signs if treated as the same scalar. The viewer spin is
currently integrated from path distance rather than copied from retail; its
sign cannot be compared to `+0x24` without a runtime sample.

## Ackermann and contact inputs to Wheel response (r358, 2026-10-16)

The preceding callbacks show why the Wheel output cannot be calculated from
the steering word and static car geometry alone. Each Suspension post-update
`0x005a8790` builds the corner pose using the current RigidBody transform and
Suspension state; when Ackermann is present, it selects the matrix at
`steering+0x08 + 64*corner_index`. That pose is passed through the Wheel
transform update. The wheel-contact path `0x005a2e40` / `0x0059cb90` queries
the live collision world; a hit's runtime polygon byte `+0x0b` selects the
Wheel movement profile at `+0x114`, and a miss defaults that index to `1`.

Ackermann itself converts the packed input with `2π/65536` radians per word
unit, wraps it to signed radians, then computes the adjusted angle from its
absolute value:
`adjusted = longitudinal_delta * abs(input_angle) /
(longitudinal_delta + lateral_delta * abs(input_angle))`. The r359/r360 notes
resolve the pointer-to-row, anchor offsets, matrix block, front Wheel index,
and lateral-side mapping. The exported tire mesh's rotation axis and the
viewer-steering-to-Ackermann input semantics remain open.

The response pair at Wheel `+0x124/+0x128` is then updated in
`0x005abdf0`, using the Wheel's curve/state and distinct movement-profile
fields `+0x08/+0x10`, with drive correction through `+0x108`. In
`0x0059d600`, when `+0x108 > 0` and `+0x10c == 0`, the first component is
weighted by `1.5` before the pair is magnitude-limited. The bound uses the
selected profile's `+0x04`, Wheel `+0x18/+0x164/+0x180/+0x184/+0x188`, global
scale `0x007fc718` (1.0), and conditional state multiplier `0x0059d490`:
`bound = profile[+0x04] * profile_object[+0x0c] * wheel[+0x164] *
wheel[+0x180] * wheel[+0x184] * wheel[+0x188] * optional_multiplier`.
The limited pair is transformed using Wheel matrix `+0x70..+0xac` plus global
transform-mode gates and written to `+0x130..+0x138`. These are concrete
inputs and operations, but several response fields are live state and the
profile values have no proven physical units.

Offline assets provide stock wheel anchors and render-city triangles, and
GLOBALB contains some per-car suspension/curve data plus static surface-profile
values. They do not include the runtime collision polygon selected under each
wheel (including its surface byte), suspension pose/compression and response
state, or active player component pointers. A geometric support approximation
could be sampled from exported meshes, but it cannot reproduce the retail
profile selection and resulting force vector. To continue with numbers, a
runtime capture must sample `0x0059d600` inputs/output alongside each
Suspension's state, Ackermann corner matrix, collision-query hit record and
surface byte, and the active player's Wheel/DriveTrain linkage. No viewer
coefficient is justified by the extracted static assets alone.
