# Driving-feel boundary and next capture (r308, 2026-10-16)

## Decision

No handling coefficient was changed in this pass. The available executable
analysis does not yet justify replacing the viewer's direct heading update
with a retail steering response, or changing the viewer's speed-to-yaw curve.
Changing those numbers from the static tables alone would make the car feel
different, but would not be a source-grounded match.

## What is already source-backed in the viewer

`updatePlayer()` in `webgl-city/viewer.js` uses the retail physics substep
ceiling (`0.0044` seconds), limits the digital throttle command to `+1.0/s`
and `-1.5/s`, quantizes wheel/heading angles to a 16-bit full-turn grid, and
maps gamepad input through the recovered controller profile at `0x7e9e88`.
The keyboard endpoints also match the `-1/+1` endpoints in both ordinary
RealDriver profile modes. These are input and representation matches; none
defines the resulting chassis yaw.

## Exact unresolved links

The ordinary RealDriver path samples a mode-selected profile, then uses the
owner's runtime scalar at `*(*(owner+0x20)+0x270)`, owner speed at `+0x42c`,
three command values at `owner[+0x30]+0x20c/+0x210/+0x214`, steering state at
`+0x20/+0x54/+0x7c/+0x84`, static speed/rate/state curves, and a stateful
smoother. Its packed result is stored at `RealDriver+0x1e0`. The current static
trace does not establish which dispatcher branch the free-roam player takes,
the live values of those fields, or that the player's packed word reaches its
`PhysicsMover` and Ackermann object. The AI `DragDriver` path is not evidence
for the player's path.

The viewer's `0.22 rad/s` yaw cap, `3.5 m/s²` lateral estimate, direct
`heading += yaw * dt`, `0.55 rad` visual wheel-angle ceiling, and fixed
acceleration/braking baselines are therefore viewer choices. Source
wheelbase/Ackermann geometry only determines visible wheel angles; the traced
RealDriver response has no direct wheelbase/track/radius read. The exported
GLOBALB `+0x380` value is copied into a setup block, but no consumer linking it
to this steering response has been found.

## Diagnostic plan when XP reaches free roam

The existing `tools/capture_nfsu2_player_steering.gdb` is prepared for the
local QEMU GDB stub. With a legitimate running gameplay session, capture a
stationary, slow, and moderate-speed left/right sweep while recording:

1. The primary player actor, selected controller mode, component list, and
   whether that actor owns the candidate `PhysicsMover`.
2. Which `0x0041dfc0` branch executes; steering `+0x08/+0x10/+0x20/+0x48/
   +0x54/+0x7c/+0x80/+0x84`; request/profile, `dt`, owner speed/commands, and
   the nested `+0x270` scalar.
3. The packed result at `RealDriver+0x1e0`, matching mover owner/command-source,
   Ackermann `+0x90`, wheel matrices, and body heading/yaw over the same sweep.
4. A straight throttle/release/brake run recording RealDriver command outputs,
   drivetrain state, mover speed, and body acceleration. This is needed before
   replacing the current acceleration baseline with engine-curve-derived
   force, whose physical output units remain unresolved.

Only after those pointer and response correlations are observed should the
viewer implement the matching branch/state update. The XP disk was found at
`/home/jhylands/vms/winxp/winxp.qcow2`. A temporary QCOW2 overlay boots to the
Windows account login screen; submitting a blank password did not log in. The
original disk was left untouched. Until the account can be opened and free
roam reached, no player gameplay runtime trace is available; do not treat the
static AI or component traces as a substitute.

## E-brake input-path mismatch found in static code

Disassembly of `0x0041e060` confirms that events `0x99` and `0x9a` are
registered to callback `0x00407f40`. That callback stores the digital and
analog samples at `RealDriver+0x2e8` and `+0x2e9`, selects the digital sample
when nonzero (otherwise the analog sample), and writes its normalized value
to `+0x2ec`. Longitudinal update `0x00407a30` routes that channel through the
`+0x210` setter; mode-1 `PhysicsMover` reads `+0x210` alongside throttle and
brake. These addresses establish a separate e-brake command channel.

The viewer's Space key / gamepad A currently affects only its approximate
coast drag, low-speed stop, and yaw pivot. It does not model a separate
e-brake command or a calibrated braking effect for it. That is a structural
control-path mismatch, but static code does not identify the default key
binding or e-brake force/yaw response. No physics value was changed.

The prepared `tools/capture_nfsu2_player_steering.gdb` now also logs the raw
e-brake input bytes `+0x2e8/+0x2e9` when they change, beside normalized
`+0x2ec` and mover command `+0x210`. With the other existing log fields, a
free-roam capture can distinguish e-brake from regular brake and reverse and
observe their downstream command output. It still needs a brake-only,
e-brake-only, and reverse-only hold, first from rest and then while rolling,
plus an active player-to-mover correlation before any force behavior is
transferred to the viewer.

The capture also now saves the candidate player `PhysicsMover` when its
existing finalizer correlation succeeds. A change-only breakpoint after the
per-corner command writes checks `ESI == tracked mover` and
`EBX == mover+0x38+4*index` before reading the Wheel. It logs raw index, Wheel
`+0x178/+0x17c`, profile pointer and `+0x04/+0x08/+0x10`, response
`+0x124/+0x128`, and `+0x10c/+0x28`. Each index logs initially and then only
when an address/word changes or a float differs by at least `0.01`; front/rear
labels remain unknown. This adds observations of the response chain without
claiming physical meaning for those fields.

### E-brake command through Wheel response fields

Static disassembly extends the path through `PhysicsMover` update `0x005a4d80`.
It reads the command source's `+0x210`, updates mover state `+0x70` with
different `dt`-scaled rates across the `0.5` command threshold, and writes a
per-corner value into each child Wheel's `+0x17c`. The raw write is the
four-entry data table at `0x7fc074` multiplied by command `+0x210`, followed
by branches on physics state (mover/owner `+0x424`), state value `+0x3ec`,
and curve/helper `0x00591d90`. The state update uses globals `0x7a1a04=1.0`
and `0x7a1a08=20.0` as its two `dt` rates, threshold `0.5`, and lower clamp
`0x7a1a0c=0.5`. Later gates compare against `0x7a1a10=0.3` and
`0x7a1a14=80.0`; a branch adds `0x784260=0.5` to the wheel value before
storing. The table's four IEEE-754 values are
`0.0044, 0, 0, 1`; this establishes only data values and loop order, not which
physical corner each index represents. Some state branches add the constant
at `0x784260` before storing. No unit or force meaning is established for
Wheel `+0x17c` by these stores.

Two Wheel consumers are identified. `0x005a8280` reads Wheel `+0x178/+0x17c`
and multiplies them by profile fields `Wheel+0x1c` `+0x4/+0x8` and globals
`0x7fc708/0x7fc70c`, respectively; it then uses the resulting terms in
Wheel-response calculations and writes response fields including
`Wheel+0x124/+0x128`. `0x0059d400` uses the same weighted sum, multiplied by
`0x7fc710`, in a comparison with a speed-dependent term using `abs(Wheel+0x28)`
and profile `+0x10`; it can set Wheel `+0x10c` and clear `+0x28`. The static
global values are `0x7fc708=4.0`, `0x7fc70c=10.0`, `0x7fc710=1.2`,
`0x7fc714=0.1`, and the nearby `0x7fc718=1.0`. These are multipliers and
comparison constants only; the profile values and runtime states needed to
evaluate the response are not captured.

This is a source-backed e-brake-to-Wheel response path, but it does not expose
brake-force units, tire-force distribution, wheel-corner identity, or the
resulting rendered yaw. Static follow-through does establish the structural
angular path: Wheel `+0x130` enters `0x0059bb00`, which adds a capped vector and
its `cross(response, delta)` moment to the RigidBody accumulators at
`mover+0x20`. RigidBody integration carries that moment into angular state;
`0x005a5540` then copies the integrated orientation and derives a heading word.
The trace does not establish the live wheel values or physical corner mapping.
The static render audit below resolves the yaw basis but not the final drawable
transform. No browser handling change is justified from the force-path trace. The
narrow runtime capture still needs the four Wheel `+0x17c/+0x178` values,
their profile pointer and `+0x4/+0x8/+0x10` values, `+0x124/+0x128` response,
`+0x10c/+0x28`, mover/owner `+0x424/+0x3ec`, and body velocity/yaw while
holding brake and e-brake separately. Correlating those fields to active
player Wheels is required before interpreting the response as force or yaw.


The static angular chain is now followed one step further. `0x0059bb00` adds
the capped Wheel response vector to RigidBody `+0x110..+0x118`; it computes a
cross product of that response with `wheel[+0x140..+0x148] - body[+0x20..+0x28]`,
caps that product, and adds it to `+0x120..+0x128`. The RigidBody derivative
uses the accumulated moment in its angular-momentum-like state at `+0x80..+0x88`;
the post-step conversion through `+0x90` produces angular-rate values at
`+0xc0..+0xc8`, which feed the orientation-matrix derivative. These are the
operations and field links proven by code. Calling them force, torque, or yaw
would require units, inertia interpretation, and live axis data that are not
available here.

The render handoff narrows the remaining orientation question. The actor update
at `0x004562cf` reads owner position (`+0x20`), angle word (`+0x3cc`), and a
float (`+0x3ec`), then calls `0x005fbd60`. Its pose path reaches
`0x005f52c0`, stores position in the render object at `+0x60..+0x68` and the
angle at `+0x40c`, and calls `0x005f5010` to rebuild the matrix at `+0x90`.
`0x005f49f0` calls the yaw-matrix helper `0x005bac90`, which builds the same
column-major yaw basis as the viewer: columns `(cos θ, sin θ, 0)` and
`(-sin θ, cos θ, 0)`, with Z unchanged. Its trig lookup uses
`0x0043c370` (input offset by a quarter turn) and `0x0043c2b0`. This supports
the retail map-plane basis `(cos θ, sin θ)` for local +X and agrees with the
viewer fallback: exported local -Z maps through `GLTF_TO_CITY_UP` to city +Y,
then `heading - π/2` maps it to `(cos heading, sin heading)`. No static
axis/sign mismatch is shown.

The render trace establishes a pose-to-render-object matrix rebuild, but does
not yet prove that this matrix is the final visible vehicle mesh transform or
fully decode the base-matrix composition at render object `+0x20`. The yaw
basis itself agrees, so no viewer orientation change is supported by this
static comparison. Confirming the remaining drawable edge requires an XP
runtime capture correlating the owner and render-object matrices with a visible
vehicle.

To resume the isolated VM after the account password is available, run
`tools/start_nfsu2_xp_capture.sh`. It reuses the temporary overlay, disables
guest networking and host GPU passthrough, and exposes the existing capture
scripts' QEMU GDB stub on localhost port 1234. The guest's original disk stays
the read-only backing image.


## Handling-data inventory and ranked next capture (r321)

The extracted stock rows in GLOBALB chunk `0x34600` provide per-car mass
(record `+0x220`), three engine RPM thresholds (`+0x300..+0x308`), nine
primary and nine secondary engine samples (`+0x310/+0x334`), a three-sample
subtractive engine curve (`+0x358`), four stock/upgrade gearbox records
(starting `+0x2c0/+0x460/+0x4a0/+0x4e0`), and wheel radii/widths and axle
anchors (`+0x120..+0x1df`). The viewer loads these through
`garage-cars/manifest.json`: `loadGarageCar()` attaches `spec.powertrain`; RPM
and gear display plus the preview curve sample the stock ratios, final drive,
wheel radius, thresholds, curves, and mass. Wheelbase for steering animation
comes from exported car geometry batches. The source `+0x380` scalar is
exported but has no proven steering consumer, and the shared suspension
records at `+0x280/+0x2a0` have unresolved units. Contact-profile fields are
surface-wide, not per-car grip values. The ISO is an installer image; the
installed extraction checked here contains the 29 car geometry/texture pairs
and GLOBALB, but no separate named handling or `PC_*_Config.fng` table.

No safe handling correction emerged. The viewer's source-loaded values are
being used where their consumers are understood, while its absolute
acceleration, shift policy, braking, yaw and lateral limits still lack a
retail-calibrated output. Capture these in this order when free roam is
reachable:

1. **Player steering sweep:** identify the active controller/PhysicsMover and
   selected RealDriver branch; log steering request/profile, `dt`, speed,
   commands, the nested `+0x270` scalar, steering state `+0x20/+0x48/+0x54/
   +0x7c/+0x80/+0x84`, packed output `RealDriver+0x1e0`, Ackermann `+0x90`,
   Wheel transforms, and body heading/velocity through stationary, slow and
   moderate left/right sweeps. This resolves the selected stateful steering
   response and its downstream handoff; static tables do not yield its live
   output.
2. **Straight pull and shifts:** record DriveTrain `+0x44/+0x3c/+0x14/+0x58/
   +0xec`, Engine `+0x14/+0x18/+0x7c/+0x74/+0x84/+0x88/+0x8c/+0x90`, each
   driven Wheel `+0x108/+0x10c/+0x11c/+0x124/+0x128/+0x130`, Suspension force
   output, RigidBody force accumulators and before/after velocity. These show
   which engine/drivetrain branches are active and provide the missing scale
   from exported curves/ratios to body acceleration.
3. **Surface response:** correlate each Wheel's live contact/profile ID and
   profile `+0x04/+0x08/+0x10` with Wheel response, Suspension output, body
   acceleration and yaw on pavement and a second surface. This is required
   before calling the shared profile coefficients grip or applying them as
   per-car tire parameters.

The prepared steering GDB capture covers the player/Wheel correlation and
response fields; the straight-pull and surface runs should extend the same
capture session rather than infer output from a dashboard-only RPM trace.

## Current XP capture availability (r331)

The live QEMU capture copy is running from `/tmp/nfsu2-winxp-capture.qcow2`,
with guest networking disabled and a GDB stub at `127.0.0.1:1234`. Its VNC
display is bound to `127.0.0.1:5901`. The visible screen is the Windows XP
account selector for `James`, prompting for a password. An Escape key was sent
only to wake the bouncing-logo screen saver; no password was entered and no
login bypass was attempted. The existing GDB traces still require the user to
reach a logged-in desktop and launch NFSU2 before attaching at the documented
game-code breakpoints.


## Command-priority mismatch candidate (r322)

The browser currently forwards requested throttle, regular brake, and a boolean
handbrake independently into its approximate speed update (`updatePlayer()`;
keyboard throttle/brake and gamepad triggers may overlap). Retail mover update
`0x005abbf0` has a shared-command priority gate before physics: unless owner
`+0x434 == 1`, global byte `0x0089e7e2` is set, or either mode dword
`0x00890100/0x00890110` is nonzero, it compares
`RealDriver+0x20c` (throttle), `+0x210` (e-brake), and `+0x214` (brake). If
throttle is no greater than brake or e-brake, it clears throttle only when the
value is at most `0.5`; otherwise it clears brake and e-brake individually
when each is below `1.0`. This is a concrete input-path difference for
partial analog overlap, but whether the active free-roam player uses this gate
or one of its bypasses is not known. The prepared steering capture now pairs
breakpoints before and after the gate, filtered to the tracked player mover,
and records the owner flag, all bypass globals, command source, and throttle /
e-brake / brake values. The viewer input priority stays unchanged until that
capture observes the player's active branch.


### Priority-gate bypass xref audit (r323)

The bypass cannot be selected from the static player-construction path. The
only direct `.text` caller of `0x005abbf0` is `0x005b72d0`, the generic
`PhysicsMover` frame callback. At the gate, `this+0x5c` supplies the factory
owner whose `+0x434` is compared with `1`; `this+0x04` supplies the command
source whose `+0x20c/+0x210/+0x214` slots are changed. Static lifecycle proves
a controller-mode-1 owner can hold a RealDriver and this mover, but does not
identify which owner instance is the free-roam player or establish its
`+0x434` value at this callback.

The global bypass is also dynamic. `0x0053fff0` derives byte `0x0089e7e2` from
configuration/context values, `0x00526720` sets it during a mode-setup branch,
and `0x005269b7`/`0x005407fd` clear it. The other bypass dwords
`0x00890100/0x00890110` are read by this gate and several neighboring
controller/steering paths; a whole-`.text` immediate-reference scan found no
writes or address-taking sites for them, so their active values are not
recoverable from this executable alone. Similar `+0x434` accesses do not
resolve the owner field: e.g. `0x005b6300` writes a computed value to the
separate physics-state block at `mover+0x58 +0x434`, while `0x005f527d` writes
an index-like value through a different object pointer. Those are not evidence
for `factoryOwner+0x434`.

The prepared `tools/capture_nfsu2_player_steering.gdb` now installs paired
breakpoints at `0x005abc00` (after `ESI=this` and owner resolution, before
bypass tests) and `0x005abcac` (the common post-gate continuation), conditioned
on `ESI` matching the already tracked player mover. It logs factory-owner
pointer and `+0x434`, all three bypass globals, command-source pointer and the
throttle/e-brake/brake command values at both points. Output is change-triggered
with 0.05 command-value buckets to avoid per-frame spam. The breakpoint
numbering accounts for the earlier Python Wheel breakpoint (7), so the regular
command breakpoints are 8-14 and this pair is 15-16. A live free-roam capture
will show whether the player's normal frame applies the priority writes and
whether partial gamepad overlap should be filtered. The viewer gate remains
unchanged until that branch is observed.

## Source references

- [`player-controls.md`](player-controls.md#webgl-driving-reconstruction-boundary)
- [`webgl-driving-gap.md`](webgl-driving-gap.md#ordinary-and-active-controller-response-lift)
- [`vehicle-physics-runtime.md`](vehicle-physics-runtime.md#frame-update-and-substep-rule)
- [`tools/capture_nfsu2_player_steering.gdb`](../tools/capture_nfsu2_player_steering.gdb)

## Force-chain follow-up (r322)

The executable-backed Engine → DriveTrain → Wheel → Suspension → RigidBody
follow-up is recorded in
[`vehicle-physics-runtime.md`](vehicle-physics-runtime.md#engine-to-rigidbody-longitudinal-force-boundary-r322-2026-10-16).
It shows why static GLOBALB curves, gear ratios, and constants do not yield
exact acceleration: live Engine modifiers and operating point, DriveTrain
mode/ratio state, contact-gated Wheel accumulators, and the per-substep
RigidBody response must be captured. The required fields and exact straight-
pull capture are listed there; no viewer acceleration change is supported
without it.
