# Traffic vehicle resource streaming

This note follows the resource-manager calls made when the traffic manager
creates and retires a streamed vehicle handle. It identifies resource paths
and class-dependent requests; it does not identify the final render-mesh
binding or prove that every requested asset is resident before the handle is
used.

## Manager handle creation

The ambient manager calls `0x0063bc70` with a pointer to the manager slot's
definition-index field at `slot +0x10`, followed by two zero arguments. The
wrapper calls `0x0063ae70` to obtain or create a resource-manager entry, then
links that entry into the global manager at `0x008a3288`. The returned entry is
stored in the slot at `+0x8f8`; the caller also stores the timer value at
`+0x900` and increments its active count. Retirement calls `0x0063ae00` with
that handle. This establishes a manager-owned handle lifecycle around the
selected definition, rather than direct creation of a visible car in the
traffic quota loop.

## Loaded-resource reconciliation

The traffic manager's state-7 callback `0x00420030` reconciles entries in the
global resource-record array rooted at `0x00890080` with its definition slots
under manager `+0x900`. For each resource record, it takes the associated
definition object from record `+0x14`, searches manager slots for a match to
the object's current `+0x908` value, copies the matched slot's `+0x10` value
to definition-object `+0x10` (or copies from the manager-list head if there is
no match), and stores the copied value back at definition-object `+0x908`.
It then calls `0x00601110` on the resource record with record `+0x10` as the
argument.

After initialization, the callback searches manager slots for a pair matching
the initialized definition index (`[resource_record+0x10]`) and variation byte
(`resource_record+0x10` byte `+7` versus slot byte `+0x17`). On a match it
passes slot handle `+0x8f8` to `0x0061ca50`, increments slot fields `+0x908`
and `+0x90c`, and stamps `+0x904` with global timer `0x0086518c`. These are
explicit counter updates in the matched definition slot; their precise
accounting semantics are not established by this callback alone. The same
definition/variation key is used by `0x00412910` for lookup and by
`0x0041ac40` during removal, where `+0x908` is decremented.

The initializer `0x00601110` copies the supplied definition-backed pointer
into the resource record's associated object at `+0x10`, mirrors its first
dword to object `+0x908`, saves the corresponding car-definition table record
at resource-record `+0x0c`, and calls the class-dependent resource setup
through `0x00639ce0`. For class 1 definitions whose object byte `+0x900` is
clear, it also publishes the definition index and a pointer to definition
offset `+0x2f0` in globals `0x008021b0` and `0x008900e8`. The routine creates
and replaces additional resource-backed fields at record `+0x20`, `+0x3c`,
and `+0x958`, then invokes object setup helpers including `0x005f5010`.
Traffic-object construction at `0x006046aa` calls this initializer directly
after initializing the large per-car object and its initial traffic-command
tuple fields. In state-7 reconciliation, the same initializer is called for
already-listed resource records. This links streamed definition state to
initialized per-car objects; the next section traces those records through the
shared collection used by traffic-context setup and the frame update.

## Streamed resource to live vehicle instance

The callback `0x006065b0` walks a six-entry local descriptor list and calls
`0x00606480` for entries whose leading type is 1 or 2. `0x00606480` passes a
type-2 predicate into `0x00639ce0`, allocates a `0x9e0`-byte runtime object,
and calls `0x00604260` to initialize it. That initializer writes the resulting
position to object `+0x60/+0x64/+0x68` and a 16-bit heading value to `+0x40c`.
The next subsection traces both owner-backed and descriptor-backed inputs,
including the six-slot builder and the position-related spatial helper calls.
The input units and heading convention remain unresolved.

After construction, `0x00606480` performs type-dependent setup through
`0x005eba30` and follow-up helpers, then inserts the new object into the
doubly linked collection rooted at `0x00890080`. That is the same collection
used by `0x00601860` to build traffic contexts and by frame updater
`0x00609ce0`. This closes part of the runtime path: streamed resource records
can become positioned runtime vehicle objects before the traffic manager
attaches context state and updates them.

The routine proves construction and collection membership. It does not prove
that every descriptor accepted by `0x006065b0` is ambient free-roam traffic,
or identify model-part assembly and transform units.

## Live traffic frame pass

The game-loop path at `0x00581407` calls `0x00609ce0` on the collection
manager rooted at `0x00890080`, passing the frame delta produced by the
surrounding time update. `0x00609ce0` is an update pass, not a render routine:
it prepares per-car position/history data, dispatches vehicle-object callbacks,
and performs manager-level route and state updates. The excerpt contains no
graphics draw submission or geometry selection.

At entry, the pass calls `0x005fa770`, `0x005fc0e0`, and a shared frame-state
helper. It then copies each active object's position (`+0x60/+0x64/+0x68`)
and scalar at `+0x8fc` into manager scratch arrays based at `+0x88` and
`+0xe0`. The first four entries use an unrolled copy; later entries use the
same 16-byte stride in a loop. Entries are filled with a sentinel through six
slots. This is a bounded snapshot table, not evidence of a six-car traffic
limit: the copy count comes from manager `+0x14` and the table is separately
used as scratch.

The central update iterates objects in the manager's current range
`[+0x14,+0x18)`. For each object it calls `0x005ebd80`, stores the returned
float at object `+0x4dc`, then calls the function in the child object's
vtable at `+0x0c` with the frame delta. It repeats a related pass for the
other range `[+0x18,+0x10)`, then calls the vtable function at child `+0x04`
with the delta for every object in the active `+0x10` range. These are
confirmed per-object callback edges; their full virtual method names and
all effects are not established here.

The manager then calls `0x005fbbe0` once for each object in the `+0x10` range.
When the frame delta passes its positive-time gate, this routine snapshots
the vehicle object's position (`+0x60..+0x68`), another vector (`+0x70..+0x78`),
and a third (`+0x110..+0x118`) into corresponding history fields at
`+0x510..+0x538`. It also has a branch using global `0x008900f8` and a
callback through object `+0x3c`; that branch computes a length from the
`+0x70` vector and scales it by a global constant. The exact meaning and units
of these vectors remain unknown. `0x00609ce0` also invokes `0x005f0f00`,
`0x005ffa50`, `0x00604740`, and other manager helpers in a fixed sequence,
and finally visits the context list at `0x008900b4` through `0x00602650`.

This gives a reproducible simulation skeleton for a WebGL reimplementation:
keep a live vehicle list, preserve previous position/vector samples, pass a
frame delta into per-vehicle behavior callbacks, and update manager contexts
afterward. It does not yet specify the callback algorithms or prove how a
live object's transform is bound to the geometry objects parsed by
`0x00493bb0`/`0x005cc280`; that render/model join remains open.

There is a separate confirmed pose preparation call. The resource initializer
`0x00601110` calls `0x005f5010` after setting up its resource-backed fields.
`0x005f5010` passes the vehicle's position at `+0x60`, heading word at
`+0x40c`, matrix-like state at `+0x90`, and output state at `+0x5c4` to
`0x005f49f0`. It then subtracts a reference position (object `+0x20`, or a
position obtained through the child at `+0x34`) from the vehicle position,
stores the resulting three floats at `+0x50/+0x54/+0x58`, and calls
`0x00592460` to synchronize the `+0x90` and `+0x50` blocks through a shared
matrix helper. This confirms a heading-and-position pose path inside the live
object. The destination is still object-side state; no examined instruction
here passes the `+0x958` resource handle or the parsed geometry-part list to a
draw call.

The `+0x958` field itself is not yet the missing mesh pointer. Initializer
`0x00601110` creates a `0x170`-byte helper object there through `0x0063a360`;
teardown `0x005f46a0` releases it through `0x0063a440`. Its constructor
initializes a nested helper at `+0x100`, six entries beginning at `+0x38`,
and associated state words. Update helper `0x00637c70` uses the nested object,
the live vehicle, and a derived index/count to update this helper state, then
calls `0x00633ef0`. A resource-binding pass at `0x00635016` resolves four
resource records by fixed hashes into owner fields `+0x2c04..+0x2c10`; when
the owner has a `+0x958` helper, it stores the `+0x10` payload pointers from
the last two records at helper `+0x108` and `+0x10c`. This identifies the
helper's two resource inputs, but does not prove that they are car mesh parts.
The examined helper update path does not issue a mesh draw. A separate traffic
render attachment is created at object `+0x18`; its constructor hashes the
resource names `MARKER_DIRECTION_AID` and `MARKER_GPS_AID` into handles at
`+0x10c` and `+0x110`. Those are marker resources, not car-body meshes. Keep
the `+0x958` helper separate: its exact gameplay/render role and the meaning
of its two resource handles remain open.

### Descriptor scan and spawn inputs

The six descriptors consumed by `0x006065b0` are built by `0x005eb5d0` in a
local array with `0x20`-byte entries. For each local entry, the routine scans
the global candidate array at `0x0089e810` (count `0x0089e814`, stride
`0x940`). A candidate is considered for that descriptor when its byte `+3`
matches the current one-based descriptor number and its leading byte is
non-positive. The descriptor's dword at `+4` then selects type 1 or 2; other
types are skipped. This records the actual selection key and filter, without
assuming the candidate array is itself the final live-car list.

`0x00606480` passes the source candidate and descriptor into
`0x00604260`. The initializer's four explicit inputs are: source record,
position vector, 16-bit heading, and a float parameter. With a non-null
owner/state pointer, `0x00606480` supplies the block at owner `+0x20` as the
position vector and the word at owner `+0x3cc` as heading. With no owner, it
passes the 0x20-byte local descriptor itself as the position vector and its
word at `+0x10` as heading; the float parameter comes from descriptor-derived
scratch state. `0x00604260` writes the three input floats to object
`+0x60/+0x64/+0x68` and the heading word to `+0x40c`; it also copies the
signed source-record byte `+1` into object `+0x0a`.

The local descriptor builder `0x005eb5d0` first scans the existing resource
records for each of its requested slots. A matching record's floats at
`+0x10/+0x14/+0x18` and heading word at `+0x20` are copied to descriptor
`+0/+4/+8` and `+0x10`, respectively. If no matching record exists, the
builder creates a fallback position and computes a heading through
`0x005da370` and `0x0043e6d0`. The exact fallback spacing constants and
heading convention remain unlabelled, but the important source distinction
is now clear: descriptors can preserve existing position/heading data or
generate a fallback placement.

The initializer's position handling is conditional: when the optional vector
is non-null, its three floats are copied to object `+0x510/+0x514/+0x518` and
serve as input to the spatial helper. When it is null, those three values
start at zero (neither call from `0x00606480` uses that branch). The helpers
do not visibly replace object `+0x60/+0x64/+0x68`;
those fields are copied from the initializer's local position vector. Instead,
`0x005da3d0` checks the vector against the scratch origin and may call
`0x005d9e30`, while `0x005da410` first calls `0x005d9cc0` and, if it returns a
record, also resolves it through `0x005d9e30`. These appear to select or
resolve spatial metadata around the position; the exact record type is still
unknown. The descriptor scratch is initialized by `0x005ded90`, which clears
a transform-like object, sets its diagonal scale to 1, and may call
`0x005d9e30` when the descriptor's `+4/+0` coordinates differ from the
scratch origin by at least the shared threshold. The disassembly proves the
input and destination fields, but not their units or heading convention.

## Resource records to traffic contexts

There is a second, explicit connection from the loaded-object collection into
traffic runtime state. Setup routine `0x00601860` walks the resource collection
rooted at `0x00890080` and builds manager-owned records at
`manager +0x30 + index*0x1f0`. It stores each context pointer in the manager's
table at `+0xbd0 + index*4` and advances the context count at `+0x24`.
It handles both the collection's leading segment and its current range, so
records already in either part are copied into the context table.

The helper `0x005fc2a0` performs that copy's initialization: it stores the
source car/resource record in context `+0`, writes the context address back to
source record `+0x1c`, and clears per-context state. It seeds the current and
previous route words at context `+0x04/+0x06` with `-1`, the route selector
byte at `+0x08` with `-1`, and byte `+0x09` with zero before route setup. In
the ordinary path, construction calls `0x005f7c30`, which clears route flags
`+0x0d/+0x0e`, scans route samples through `0x005ee150`, and updates route
fields and route-history arrays. Modes 7 and 8 bypass that scan and set
`+0x0d/+0x0e` to 1. The full scoring meaning is still open, but this identifies
the constructor's initial route-selection call and its sentinel values.

### Route sample construction and evaluation

The route pass consumes a manager-owned table: signed count at manager
`+0xbf4`, followed by single-precision values at `+0xbf8` with a four-byte
stride. `0x005ee620` resets the count and appends the values used for the
current setup. In its ordinary branch it calls `0x005ee4f0` several times
with fixed resource keys and offsets from earlier samples. It records the
returned sample indices in bytes `+0xbf6/+0xbf7` and stores the associated
three-component positions under manager `+0xc10` and `+0xc20`. A second branch
is selected by global `0x89e7a4`; it uses a different key/position sequence.
Modes 7 and 8 return before building the table. The key strings and selector
globals are still unnamed.

`0x005ee4f0` zeroes its output vector, searches the supplied resource record
list, and optionally matches a record by its `+0x0c` key. On a match it
transforms the record block at `+0x10` through `0x005ded90`, copies the three
result components to the output vector, and appends a computed float to the
manager table. If no record matches, a nonnegative input float is appended
directly; a negative input returns `-1` without adding a sample. The matched
and fallback paths use different scaling, so the table values should not yet
be described as world distances or angles. The shared wrap helper
`0x005d5d10` subtracts its two float arguments and, when its mode byte is set,
wrap-adjusts the result using global thresholds and the period constant at
`0x784258`.

For each table entry, `0x005f7c30` passes the source record's `+0x8f8` value
(scaled by `0x784258`) and the entry to `0x005ee150`. That evaluator compares
wrapped differences against the sample and context `+0x1c`, clears a
three-dword result, and filters using the magnitude of the first difference
and subsequent sign comparisons. Accepted entries receive a two-way side
indicator in result words `+0/+4` and a quantized value in `+8`, calculated
from source record `+0x42c`; exact units and semantic labels remain unknown.
The caller uses accepted entries to advance selector byte `+0x08`, update the
best route index at `+0x04/+0x06`, and write timing/history fields. This
establishes the sample filtering and selection path, while the meaning of its
side and output values remains unresolved.

Evidence: retail disassembly at `0x005ee150`, `0x005ee4f0`, `0x005ee620`, and
`0x005f7c30` (working dump `/tmp/nfsu2-disassembly.txt`, around lines
693016, 693360, 693480, and 705698 respectively).
Setup routine `0x00607180` uses the same helper and table layout, then invokes
vehicle setup helper `0x005eba30`. Loaded car records therefore acquire
traffic contexts and an initial route-state pass, beyond merely receiving a
streamed model handle.

The callback `0x00606cb0(manager, slotIndex, ...)` resolves its context from
`manager+0xbd0+4*slotIndex` and obtains the source record through context `+0`.
The same first stack argument is the selector for its `4..7` branch and is
written as a 16-bit value to context `+0x10`. The `5` branch has a separate
calculation for context `+0x198`; `4`, `6`, and `7` share another calculation.
Both paths call timer helper `0x00577400`; the values depend on manager/global
time fields and constants. The callback also copies context `+0x14` to `+0x20`
and updates context byte `+0x0c` through helper predicates.

The earlier context-setup description conflated this callback with vehicle
setup: `0x00607180` calls `0x005eba30` with literal code `3`, but it does not
call `0x00606cb0`. Direct calls to `0x00606cb0` occur in manager processing
routine `0x006076d0` and notification handler `0x006090f0`. The notification
handler passes the matched context table index first, the source context byte
`+0x0a` second, then its two callback arguments. So the `4..7` switch is
selected by the manager table slot index; the notification callback arguments
are separate values. Timer units, field meanings, and gameplay labels remain
unresolved.

The link written at source-record `+0x1c` is consumed by notification handler
`0x006090f0`, so it is not just setup bookkeeping. The handler's lookup helper
`0x005f0e30` scans the collection rooted at `0x00890080` for a record whose
signed 16-bit key at `+0x0a` matches the notification's signed byte key at
`+0x04`. If the record has a context at `+0x1c`, the handler follows that
context's source pointer at context `+0`, reads the source record's `+0x1c`
link, and searches for that context pointer in the manager's table at
`+0xbd0`. Once found, it calls `0x00606cb0` with the located context index and
the context byte at `+0x0a`, along with both arguments supplied to the
notification callback. The callback's first argument is applied to the
notification object by `0x005efa30` and is forwarded as the third stack
argument to `0x00606cb0`; it does not select the latter's `4..7` branch.
Several notification types can be routed through this path, but the type
values and notification-object state values are not yet mapped to named
gameplay events. This proves the record/context link participates in runtime
state changes; it does not show the serialized traffic tuple being read here.
Keep the numeric fields separate: the handler's filter reads the notification
object's dword at `+0`, while the `4..7` branch uses the context table index.

The inspected direct dispatch sites supply these callback arguments to
`0x006090f0`:

| Call site | First callback argument | Second callback argument | Receiver at call site |
|---:|---|---:|---|
| `0x0060210e` | `0` | `12` | each other non-null entry in a six-slot group |
| `0x006093b6` | `6` | `12` | the primary entry at caller `+0x04` |
| `0x006093f5` | `0` | `12` | each eligible entry in a six-slot group |
| `0x0060977c` | `0` | `12` | each non-null entry in a six-slot group |
| `0x00609a93` | `0`, `4`, or `6` | `12` | one indexed entry; depends on its type and a flag |
| `0x00609abe` | `0` | `12` | each other non-null entry in a six-slot group |
| `0x0060b034` | `0` | `12` | the entry at caller `+0x04` |
| `0x0060b0b0` | selected entry's dword `+0x00` | `12` | each entry in a six-slot group |
| `0x0060b26b` | `3` | `11` | the current entry |

These are observed callback arguments, not decoded event names. The receivers
and state values show the handler is used both for group fan-out and for
single-entry updates; they do not identify what `11` or `12` mean.

The handler also filters before dispatch: it skips helper `0x005efa30` for
notification dword types `0`, `4`, `5`, and `6`, while other types pass one of
the caller's arguments to that helper. It requires both global manager
`0x00890118` and collection `0x00890080`, and refuses the context update when
the context's 16-bit state at `+0x10` is nonzero while global mode
`0x0089e7b0` is active. Callers such as `0x006097d0` iterate six notification
slots and route populated entries through this handler, which places it in a
bulk lifecycle path. The slot meanings and broader purpose of the bulk pass
remain unknown.

The helper `0x005efa30` is a state setter on the notification object: it
compares the supplied dword with object `+0`, writes a changed value, and on a
changed transition to `2`, initializes fields `+0x274/+0x278` from fixed
constants and clears `+0x280`. This makes the handler's first stack argument a
state code, not an event duration. Inspected call sites send `0`, `3`, computed
`4`/`6`, or a dword copied from the notification object; values such as
`0x0b` and `0x0c` seen at these sites are the handler's other argument, not
the value passed to `0x005efa30`. The helper supports a transition to state
`2`, though that value was not present in the inspected calls. These numeric
states remain unlabeled. The reset and initialization side effects are enough
to recreate the observed transitions without assigning gameplay names.

The frame updater at `0x00609ce0` (including its per-car pass at
`0x0060a011..0x0060a076`) receives the same collection root,
`0x00890080`. It iterates the root's count at `+0x10` and source car-record
pointers beginning at root `+0x1c`. The context builder `0x00601860` also
reads car-record pointers from this root array (using its `+0x14/+0x18`
range fields), then stores each generated context pointer in the individual
source record at record `+0x1c`. Thus the frame updater and context setup use
car-record pointers from the same backing collection. Each record processed
by context setup carries a link to its manager-owned traffic context. The
array and context table are different structures, joined through those shared
car-record objects.

The related `0x0061ca50` helper walks the resource manager's linked list
rooted at manager `+0x58`, searches entries by their `+0x123c` key, and calls
`0x00610910` on matching entries with a non-null `+0x08` field. It can repeat
this for matching entries and invoke `0x0057ef60` while the manager has a
pending `+0x14` state. Although the reconciliation caller increments slot
counters around this helper, `0x0061ca50` itself is not a simple direct
reference-count increment.

## Resource requests made by `0x0063ae70`

The factory separates requests by resource pool. Startup setup registers
`0x008a1cf8` as `CarLoadedSolidPackSlotPool` and `0x008a1cf4` as
`CarLoadedTexturePackSlotPool`. Helper `0x00633b00` uses the solid-pack pool;
the factory passes it definition-record `+0x40` and stores the returned handle
in its solid-pack subobject. This field is a null-terminated path; for example,
the `PEUGOT` record contains `CARS\\PEUGOT\\GEOMETRY.BIN`. This establishes
the solid-pack request as the car geometry bundle. Helper `0x00633b90` uses
the texture-pack pool and handles the explicit texture and vinyl paths below.

The factory obtains the per-car resource object and formats file paths using
the embedded strings `CARS\\%s\\TEXTURES.BIN` and
`CARS\\%s\\VINYLS.BIN`. It always sends the texture path through request
helper `0x00633b90` with argument `0x8000`. It resolves the definition index
through the global table at `0x008a1ccc` and reads the class byte at record
`+0x844`:

```text
resource = get_or_create_streamed_car(slot.definition_index)
request("CARS\\<car-name>\\TEXTURES.BIN", 0x8000)

if definition.type != 2:
    if vinyls_path_exists("CARS\\<car-name>\\VINYLS.BIN"):
        request("CARS\\<car-name>\\VINYLS.BIN", 0x19000)
```

The type-2 branch jumps past the vinyl lookup and request. Since the 15
type-2 records are the definitions admitted by the ambient candidate scan,
ambient traffic loads the named car's texture bundle but does not request its
vinyl bundle through this path. Non-type-2 cars conditionally request vinyls
after a path-existence check. The numeric values are the arguments passed to
`0x00633b90`; their allocator or priority meaning is not decoded here.

After these requests, the factory attaches resource-backed fields to the car
object and returns the manager entry. This executable trace does not itself
show how those handles are joined to the render mesh. The installed PC bundle's
texture hashes and mesh material runs are decoded below; shader behavior and
traffic-spawn transforms remain separate questions.

## Parts-animation sidecar

The separate routine `0x0061c600` formats the path
`CARS\\%s\\PARTS_ANIMATIONS.BIN`, checks whether it exists, and if present
queues it through `0x0057cfb0` with callback `0x0063bd10`. Before constructing
the path it searches the 46-record definition table for a matching car key.
This identifies a conditional per-car animation sidecar, but the inspected
routine does not prove that this request is part of every ambient traffic
handle or show how the loaded animation data drives visible parts.

## Geometry chunk structure

The geometry parser reached from the retail chunk readers treats each child as
an eight-byte `(tag, payload_bytes)` header followed by that many payload
bytes. A parent chunk advances by `8 + payload_bytes`. Root tag `0x80134000`
contains object tag `0x80134010`; parser `0x00493bb0` walks the object's
children and builds a runtime descriptor. The observed child tags populate
these descriptor fields:

| Child tag | Observed parser action | Runtime descriptor field |
|---:|---|---:|
| `0x00134011` | Aligns after header/payload start to 16 bytes; initializes a linked-list head | `+0x88` |
| `0x00134012` | Saves payload pointer | `+0x2c` |
| `0x00134013` | Saves payload pointer | `+0x3c` |
| `0x00134017` | Saves payload pointer and adds its byte length to a global total | `+0x84` |
| `0x00134018` | Stores payload pointer at the selected `+0x84` block; records `size / 8` | block `+0`, `+8` |
| `0x00134019` | Stores payload pointer at the selected block; records `size / 24` | block `+4`, `+0xa` |
| `0x0013401a` | Saves a 16-byte-aligned payload pointer and derives a count using 100-byte units | `+0x80`, `+0x1b` |
| `0x0013401b` | Saves payload pointer and adds its byte length to another global total | `+0x90` |

The field relationships for `0x134018`, `0x134019`, and `0x13401a` are visible
in the parser, but their semantic labels are not established. Preserve the
recorded pointers, counts, and alignments without assigning them names such as
normal, material, or collision data.

The object finalizer `0x00493f20` scans the same children and calls a second
reader, `0x005cc280`, for nested tag `0x80134100`. That reader recognizes
subtags `0x00134b01`, `0x00134b02`, and `0x00134b03`. It aligns their payloads
to 16-byte boundaries, stores the `0x134b02` pointer at descriptor `+0x14`,
and stores the `0x134b03` pointer/byte length at `+0x18/+0x1c`. The
`0x134b01` branch calls graphics-interface vtable slot `+0x68`, which is the
vertex-buffer creation method; `0x134b03` calls slot `+0x6c`, the
index-buffer creation method, with format `0x65` (`D3DFMT_INDEX16`). Both
branches lock the returned resource, copy the payload bytes, and unlock it.
This identifies `0x134b01` as vertex data and `0x134b03` as 16-bit indices.
The loader retains the `0x134b02` payload at descriptor `+0x14`. Both the city
stream and the installed 240SX use this chunk for range data, but their record
layouts differ. The 240SX's 36-byte position/normal/color/UV layout and its
range fields are detailed below. Its bundle-level material-to-texture mapping
is now decoded; the executable's shader implementation is not.

This recovers the executable's container walk, descriptor offsets, alignment,
and graphics upload path for the geometry chunks. The installed 240SX check
below confirms its vertex stride, range-record layout, and bundle-level
texture binding. The transform applied to a spawned car remains open.

### Geometry descriptor post-pass

The root parser runs a second pass after `0x00493bb0` builds each object
descriptor. `0x004940e0` walks root children, keeps the object returned while
reading `0x80134001`, and passes it with each `0x80134010` object to
`0x00493f20`. That routine revisits the object children and:

- subtracts the byte lengths for `0x134017`, `0x134018`, and `0x134019` from
  the aggregate initialized by the first pass; `0x13401b` similarly updates a
  second aggregate;
- on a nested `0x80134100`, calls `0x005ce7d0` with the runtime descriptor and
  nested chunk, then uses `0x00488bc0` to search the companion context's
  eight-byte keyed records (key at record offset 0, value at offset 4);
- clears a matched record's value and updates a byte counter indexed by the
  low 13 bits of the object key; and
- links the descriptor into one of two global lists, depending on the lookup
  result, and updates global descriptor and byte totals.

The binary-search helper's argument pattern and the eight-byte stepping are
directly visible in `0x00493f20`/`0x00488bc0`. The wider meaning of the
companion record values and the two global lists is not established. This pass
does not call the graphics draw interface. It confirms descriptor/resource
registration after buffer upload; the separate runtime submission path is
described below.

### Indexed geometry draw pass

The executable does contain a separate indexed-geometry pass at `0x005cf420`
(main body begins `0x005cf425`). It walks a global render queue and nested
linked records, prepares a mesh's vertex buffer and index buffer when needed,
sets render state through the graphics interface, and calls device vtable slot
`+0x148` (the D3D9 `DrawIndexedPrimitive` slot). The call uses triangle-list
primitive type `4`; the remaining arguments are assembled from the record at
`ebx+0x18`, `ebx+0x34`, and the range record's byte length at `edi+0x0c`.
Before that draw, the pass calls `0x005bcc30` on the outer queue item's
`+0x10` value and binds per-range state through the nested object's resource
handles.

There is a concrete producer for that queue. `0x005c5930` walks a range array
using a count at input `+0x10`, a pointer at `+0x14`, and a 0x3c-byte step,
matching the installed 240SX range-record size. It resolves per-range state
and calls `0x005ceb30`; that routine creates or updates a linked render entry
and inserts it into a bucket in the `0x00870d30` queue family. The renderer's
`0x005cf420` pass walks those buckets and draws the queued records. City object
setup calls `0x005c5930` for its object range blocks (for example, from
`0x004a741a` and `0x004a7489`), so this queue path is confirmed for streamed
scene geometry.

This closes the generic range-record-to-indexed-draw path. The disassembly
also contains a traffic-marker submission path into that producer:
`0x006316d0` walks the live object list rooted at `0x00890080`, finds an object
through each list node's `+0x18`, and calls `0x005f3ae0` on matching objects.
`0x005f3ae0` is gated by global state `0x0089e7b0 == 7`. In that branch it
checks manager `0x00890118` (including its type value `4`) and its context
pointer at `+0xbd0`, then reads the context's source object. It builds a
transform from that source object's position at `+0x60..+0x68` and heading
word at `+0x40c`, raises the transform by a fixed Z offset, and calls
`0x005c5930` on attachment resource `+0x10c`, passing the transform and three
zero options. The range producer then creates entries consumed by
`0x005cf420`, which submits indexed triangles to D3D9.

The render frame routine calls `0x006316d0` after a `0x005cf420` queue draw
(for example, at `0x005cae6e` and `0x005cb32a`), then draws the queue again.
Marker attachment nodes are made by `0x005f437b`, which matches entries in
the live object collection against manager-owned class/index data before
calling `0x005f3e10`. That constructor links the node to its parent at
`parent+0x18` and initializes its `+0x10c` and `+0x110` resource handles from
the hashed names at `0x007a62c4` and `0x007a62b4`.

The companion callback `0x005f3cb0` runs under the state-zero gate. It uses
the source object's position, queries a route/spatial helper through
`0x00419b20`, and passes the resulting candidate and timing value through
`0x00419d20`. On success it updates a position through the attachment's
`+0x110` GPS-aid resource with `0x005eac90`, caches the three-float result at
attachment `+0x120..+0x128`, and sets validity byte `+0x114`. On a later miss,
it reuses the cached position while the validity byte is set. The route
helper's candidate semantics and `0x005eac90`'s exact transform contract are
not yet labeled.

During the render pass, `0x006316d0` follows the attachment link, matches it
to the current manager entry, and calls these callbacks. The state-seven
callback `0x005f3ae0` submits the `MARKER_DIRECTION_AID` resource through the
generic range builder.

The same pass then dispatches the current live object to a second renderer.
When its nested type is `3`, it calls `0x00620ae0`; otherwise it calls
`0x006162e0` with the live object as `this`, the matched manager entry, and
render-state arguments. The `0x006162e0` path prepares object transforms and
scans 63 records at render-owner offset `+0x17a8`, stepping by `0x20` bytes.
Each record is split into two `0x10`-byte groups of four handles. The renderer
chooses one of four levels for each group: it compares a computed size value
with thresholds `40`, `20`, and `10`, yielding level 0–3, and accepts an
override through global selectors at `0x00802728` and `0x0080272c`. The two
groups' semantic distinction remains unknown. For eligible records it
resolves three keyed state values through
`0x0048d860`, builds a transformed range submission, and calls
`0x005c5930` on the matched render context with flag `0x8000` included in the
state mask. This establishes a live traffic model-part draw path into the same
indexed geometry queue. The exact owner/slot field meanings, the asset names
behind these runtime resources, the separate type-3 renderer, and wheel
placement remain open.

The renderer's LOD selector starts with `0x0048acb0`, called with the current
render context, a position, and `3.0`. That helper measures weighted 3D
distance from the position to a stored center. `context+0x40` points to a
bounds/scale record whose center is at `+0x40..+0x48` and axis weights at
`+0x50..+0x58`. If `distance - 3.0 <= 1.5`, it
returns twice the stored radius (`context[+0x0c]`); otherwise it returns
`3.0 * radius / (distance - 3.0)`. It converts the result to an integer.
Starting from an incoming render-level parameter, the selector compares that
metric against `40`, `20`, and `10` and advances the four-way level while
thresholds are crossed. The metric decreases with distance, matching a
projected-size style LOD policy. Exact world units and the identity of the
sampled position are not yet labeled. Two globals, `0x00802728` and
`0x0080272c`, can independently override the selected level for the two
handle groups.

The caller at `0x006162e0` supplies the helper's `3.0` parameter at
`0x006162f7`. It then rejects helper results below 4 at `0x00616341`. In the
ordinary post-helper branch, it multiplies the integer result by the float at
`0x007a6700` (`0.7`) and converts it back to an integer before the same cull;
the branch skips this adjustment when either render-state check selects value
3. The WebGL proxy currently applies the helper equation but not this
state-dependent wrapper adjustment, because the live render state is not yet
resolved. Its visible traffic still reports a zero metric with BODY_A's
normalized bounds at the tested freeway view, which means the retail
`context[+0x0c]` radius and weighted units cannot be replaced by that GLB sphere
without losing all traffic to the `< 4` cull.

```text
draw_live_vehicle_parts(context, renderOwner, position, state):
    metric = boundedSizeMetric(context, position, parameter = 3.0)
    level = incomingLodLevel
    while level < 3 and metric >= [40, 20, 10][level]:
        level += 1
    levelA = overrideA if overrideA != -1 else level
    levelB = overrideB if overrideB != -1 else level

    for partIndex in 0 .. 62:
        record = renderOwner.parts[partIndex]             // +0x17a8 + index*0x20
        for group, selectedLevel in [(0, levelA), (1, levelB)]:
            handle = record[group][selectedLevel]        // 4 handles per group
            if record[group][0] == null or handle == null:
                continue
            if not categoryStateAllows(renderOwner, partIndex, state):
                continue

            resolveRenderState(handle, 0xd6d6080a, renderOwner.state384)
            resolveRenderState(handle, 0x471a1dca, renderOwner.state38c)
            resolveRenderState(handle, 0xa7366ae6, renderOwner.state384)
            submitTransformedRange(context, handle, state | 0x8000)
```

`categoryStateAllows` above names the remaining branch rather than asserting
its meaning: the executable uses a model-definition byte at `+0x838+partIndex`
and special per-category cases before it reaches the generic bit test. The
three constants are exact lookup keys passed to `0x0048d860`; their symbolic
labels are not recovered yet. The submission call is `0x005c5930`.

### Vehicle-part resource table

The `+0x17a8` table is populated by the geometry-resource builder at
`0x00634800`. Its store at `0x00634a79` writes a resolved geometry handle at
an indexed address derived from the part category, bank, and detail. Each
`0x20`-byte record contains two groups of four handles; the live renderer
selects one of four levels in each group using thresholds `40`, `20`, and
`10`. Cleanup at `0x00634630` walks 63 `0x20`-byte records and releases up to
eight handles per record; the initializer at `0x0063841c` clears the same
`0x7e0`-byte span. The traffic
renderer reads from the same base offset and stride, joining these loaded car
resources to its live draw path. Its loop runs from index 0 while the index is
less than `0x3f`, so it visits exactly the same 63 entries.

The resource-key builder at `0x00631e20` creates hashed names from a car base
name using executable templates. The visible templates include
`%s_RIM_BLUR`, `%s_RIM`, `%s_TIRE`, `%s_NEON`, `%s_SHADOW`, `%s_LOGO`,
`%s_DOOR_HANDLE`, `%s_SIDELIGHT`, `%s_LICENSE_PLATE`, `%s_BADGING_EU`,
`%s_BADGING`, `%s_INTERIOR`, `%s_MISC`, and `%s_CENTRE_BRAKELIGHT`.
This confirms the table is part of the authored car-part resource system.
The exact mapping from those templates to the eight words in each record, and
which categories traffic submits, remain unresolved.

The table arithmetic is now exact. In the loader at `0x00634800`, category
`c` comes from the 63-entry pointer array at definition `+0x590+c*4`; for each
category it walks two name banks (`g = 0,1`) and four detail slots (`d = 0..3`).
The handle destination is:

```c
parts[c][g][d] = owner + 0x17a8 + c*0x20 + g*0x10 + d*4;
```

The address calculation is visible at `0x00634a5a..0x00634a80`. For each bank,
the loader forms a base name with the literal suffix `_B` and then tries four
detail names with suffixes `_A`, `_B`, `_C`, and `_D` as the detail index runs
from zero to three (`0x00634943..0x00634a40`). A separate trailing lookup at
`0x0063515e..0x006351a0` forms a key ending in `_A`; it is outside the indexed
table loop, so it should not be conflated with bank zero. The purpose of these
name suffixes remains unresolved. The four detail slots are attempted in
order. If a higher detail slot has no resolved handle, the loader copies the
previous slot's handle; a missing slot zero becomes null. This fallback is
the branch at `0x00634a8a..0x00634abd`. Categories `0x23..0x2e` are skipped by
the outer loop. Categories `0x34..0x3e` also register an eight-byte auxiliary
entry under owner `+0x13e8`, with indices `c-0x34` before category `0x3b` and
`c-0x38` from `0x3b` onward (`0x00634ac3..0x00634af6`). The remaining
category-specific resource-state branches are retained as numeric cases in
the disassembly; their authored labels are still unknown.

Readable loader pseudocode, with the unresolved key formatter kept explicit:

```text
for category in 0..62:
    descriptor = definition[0x590 + category*4]
    if descriptor is null or category in 0x23..0x2e:
        continue

    for bank in 0..1:
        baseKey = formatAuthoredPartKey(descriptor, bank, suffix="_B")
        for detail in 0..3:
            key = addDetailSuffix(baseKey, char('A' + detail))
            handle = resolveGeometry(key)
            if handle is null and detail > 0:
                handle = parts[category][bank][detail - 1]
            parts[category][bank][detail] = handle
            if category in 0x34..0x3e:
                initializeAuxiliaryEntry(owner + 0x13e8,
                                         auxiliaryIndex(category), 8)
```

The key routines combine descriptor-selected strings and indexed global name
tables before hashing. This pseudocode captures the observed loops and
fallback but leaves the string composition behind
`formatAuthoredPartKey`/`addDetailSuffix` until its input fields are named.

This fixes the runtime record layout and the exact suffixes used to build its
candidate keys. It still does not identify which category numbers are rims,
tires, or other named parts, nor how the live renderer chooses between the
two banks for a particular car or traffic class.

The resource keys passed by the attachment constructor to `0x0043db50` are
NUL-terminated names at `0x007a62c4` and `0x007a62b4`; the hash helper resolves
them as `MARKER_DIRECTION_AID` and `MARKER_GPS_AID`. The state-7 callback
submits the directional-aid resource to the indexed draw queue, while its
companion callback uses the GPS-aid resource in a separate positional update.
The adjacent live-object dispatch also reaches the model-part renderer. The
queue builder's 60-byte records match the installed 240SX's range records.
The executable's part-name templates establish that live traffic rendering
uses the authored car-part resource system. The exact record-to-template
mapping and which 240SX `BODY_A`, alternate-body, and tire objects appear in
traffic remain unresolved.

## Installed 240SX geometry check

The read-only XP NTFS image contains the installed PC file
`CARS/240SX/GEOMETRY.BIN` (7,675,600 bytes), alongside a 940,480-byte
`TEXTURES.BIN`, a 13,971,328-byte `VINYLS.BIN`, and
`PARTS_ANIMATIONS.bin`. I extracted only the geometry and texture pack to
`/tmp` for format analysis; the original car bundle is not copied into the
workspace. The geometry bundle confirms the executable's `0x80134010` object
and nested `0x80134100` mesh chunks, and supplies concrete car-specific layouts:

- Each of the 609 object chunks has a `0x134900` descriptor followed by
  `0x134b01` vertex bytes, `0x134b02` range records, and `0x134b03` 16-bit
  indices.
- The common vertex stride is 36 bytes. Offsets `+0x00..+0x0b` are three
  position floats; `+0x0c..+0x17` are three floats whose vector lengths are
  approximately one across the ordinary vertex set; `+0x18..+0x1b` are four
  packed bytes; and `+0x1c/+0x20` are UV floats. The packed byte channel
  meanings are not inferred.
- After leading `0x11` filler, each range record is 60 bytes. Its dword at
  `+0x0c` is an index count and its dword at `+0x1c` selects a slot from the
  object's `0x134012` eight-byte entries. Slice starts are cumulative from
  prior counts. The index buffer can carry a trailing `0x11` or zero word
  outside those ranges.

`tools/export_nfsu2_car.py` applies those layouts and writes the derived
inspection mesh `car-240sx-inspection.glb`. The export contains 2,140
material-sliced primitives and 106,331 triangles with no invalid triangle
references. Its bounds are about 4.6 units long, 1.9 wide, and 1.9 high, which
is consistent with an assembled car scale. It retains every authored part
object, including alternative body and accessory pieces, so it is an asset
inventory rather than one selected retail customization. That geometry-only
preview still uses fallback colors; the stock selection with decoded texture
images is exported separately as `car-240sx-stock-textured.glb` below.

The companion `TEXTURES.BIN` exposes a stable index structure. Its
`0x33310002` has 73 eight-byte catalog entries: a texture key and a zero dword.
The adjacent `0x33310003` has 73 24-byte records in the same key order. Each
record contains key, whole-file payload offset, stored size, decompressed size,
header distance, and an unknown dword. The fifth field is `0x100` for this
pack; it is not a pixel-format code. The tool
`tools/export_nfsu2_car_texture_index.py` writes this inventory to
`car-240sx-texture-index.json`.

All 73 payloads decode: 44 use JDLZ v2 and 29 use HUFF v1. The decompressed
buffer begins with image pixels; its `OldTextureInfo` header is at
`decoded_size - header_distance + 0x88`. That header self-checks against the
descriptor key, stores width/height at `+0x20/+0x22`, and the pixel-format tag
at `+0x26`. The observed 240SX pack uses DXT1 (`0x22`), DXT3 (`0x24`), and
BGRA8888 (`0x20`). The 16-byte HUFF header declares both decompressed and
compressed lengths; the image stream follows it. These layouts and the HUFF
decoder are described in the [PryHUB texture parser](https://github.com/bdrtr/PryHUB/blob/main/crates/gizmo-nfs/src/texture/mod.rs),
and the decode path is in its [texture decoder](https://github.com/bdrtr/PryHUB/blob/main/crates/gizmo-nfs/src/texture/decode.rs).
I checked the results against this installed pack: all 73 records decode and
their embedded hashes match their directory keys.

The geometry-side relation is now recovered too. Each 60-byte range record's
slot index selects an eight-byte `0x134012` entry; its first dword is the
material/texture hash. When that hash is present in `TEXTURES.BIN`, the run
uses that image. The all-parts geometry contains 2,155 material runs, of which
six distinct texture hashes resolve to images; the stock-selection export
resolves three images across its 30 materials. The complete decoded record
inventory and per-run mapping are in
[`car-240sx-textures-decoded.json`](car-240sx-textures-decoded.json).
Unresolved hashes are shader-only runs such as body paint, glass, and chrome;
their visual shader behavior is still an approximation in a WebGL recreation.

The stock export, [`car-240sx-stock-textured.glb`](car-240sx-stock-textured.glb),
contains 27 selected parts, 5,200 triangles, 30 material runs, and three
embedded textures. [`car-240sx-textured.glb`](car-240sx-textured.glb) keeps all
609 authored alternatives and accessories, with 106,665 triangles, 52
materials, and six embedded textures. The latter is an inventory, not a single
assembled configuration. Both files are derived from the installed user's
assets; the original executable and source bundles are not checked into the
workspace.

The GLBs can be regenerated from the installed `CARS/240SX` directory with a
local build of the open-source parser CLI:

```sh
ug2 export /path/to/CARS/240SX --out /tmp/240SX-stock --format glb
ug2 export /path/to/CARS/240SX --out /tmp/240SX-all --all --format glb
```

`ug2 textures /path/to/CARS/240SX` lists every decoded texture and prints the
material-run-to-texture resolution.

## Ambient vehicle body previews

The installed type-2 `TAXI` definition points to `CARS\\TAXI\\GEOMETRY.BIN`.
Its 64,688-byte geometry bundle contains authored objects `TAXI_BODY_A`,
`TAXI_BODY_B`, `TAXI_BODY_C`, `TAXI_TIRE_FRONT_A`, and `TAXI_TIRE_FRONT_B`.
The three body objects are alternatives with nearly identical bounds, not
three parts to combine. The two tire objects also overlap at the origin in the
export, which means their attachment transforms are not recovered here.

The exporter accepts repeated `--include-part` and `--texture-pack` options.
The derived `car-taxi-body-a.glb` keeps `TAXI_BODY_A` and the authored
`TAXI_TIRE_FRONT_A` mesh (two objects, 475 triangles). The viewer omits the
source tire object at the origin and places four copies using estimated
wheelbase and track from body bounds; exact retail attachment transforms are
not recovered. The per-car `TEXTURES.BIN` is empty, but
three shared material maps (`CARBOTTOM`, `DRIVER`, and `WINDOW_TRAFFIC`) resolve
from `GLOBALB.BUN`; the per-car body-paint hash remains a fallback material.
The city viewer binds these embedded shared maps for this model and the other
fourteen type-2 `BODY_A` exports. The model-to-definition map, quota vectors,
per-body counts, and texture inventory are in
[`traffic-ambient-models.json`](traffic-ambient-models.json).

The source bundles contain 73 authored objects across the fifteen models:
most have three body alternatives (`BODY_A/B/C`) and two meshes named
`TIRE_FRONT_A/B`; BUS and FIRETRUCK have two body alternatives. The per-object
vertex/index counts, material texture hashes, object matrices, and transformed
bounds are recorded in [`traffic-ambient-parts.json`](traffic-ambient-parts.json).
The tire meshes' local bounds are centered close to their own origins, while
the recovered object matrices contain no meaningful wheel placement. Their
exact chassis attachment and mirroring are not recovered. The viewer's four
wheel positions are estimates from body bounds, not the game's runtime transforms.

The XP image's `CARS/TAXI` directory contains only `GEOMETRY.BIN` (64,688
bytes), zero-byte `TEXTURES.BIN`, and zero-byte `VINYLS.BIN`; it has no
`PARTS_ANIMATIONS.bin`. The geometry parser uploads each nested `0x134b01`
vertex buffer and `0x134b03` index buffer as a render object. This recovers the
mesh upload path, but the analyzed traffic bundle does not provide chassis
placements for its two tire objects.

Keep this separate from the player car physics path. A player `PhysicsMover`
serializes four Suspension/Wheel pairs, and frame finalization calls
`0x005921b0` for each enabled corner to build transform matrices from runtime
per-corner state. The available call trace does not connect those player
physics transforms to type-2 streamed traffic objects. The ambient preview
uses the authored front-tire mesh at estimated four-corner positions instead
of assigning the player wheel transforms to it. The remaining assembly
question is the traffic object's
render/model update path from its streamed handle to its geometry objects.

Regenerate the inventory from the extracted XP files with:

```sh
python3 tools/export_nfsu2_car_parts.py /path/to/extracted-traffic-geometries \
  reverse-engineering/traffic-ambient-parts.json
```

The preview maps each baked body's signed longitudinal axis onto its directed
route, its height axis onto city Z, and its lateral axis across the route. The
source BODY_A transforms are baked into the GLBs, so the output component axes
can differ from the source semantic axes; the manifest records the measured
permutation and signs for each model. Each body is scaled to 4.5 units long. The traffic manifest keeps
per-model axis signs, while `CARBOTTOM` face normals and bounds establish the
up axis. Tire meshes are independently baked, so the viewer maps their thinnest
axis to the body lateral axis and the radial plane to the body's long/up axes.
Scale, four-corner positions, and model selection remain preview assumptions;
the other body alternatives and exact tire attachment transforms are not
recovered.
Shared maps are bound where their hashes resolve in the
supplied TPK; the model-specific body-paint hashes remain fallback colors.

```sh
ntfscat /path/to/nfsu2-xp-part.img \
  '/Program Files/EA GAMES/Need for Speed Underground 2/CARS/TAXI/GEOMETRY.BIN' \
  > /tmp/nfsu2-taxi-geometry.bin
python3 tools/export_nfsu2_car.py /tmp/nfsu2-taxi-geometry.bin \
  reverse-engineering/car-taxi-body-a.glb --include-part TAXI_BODY_A \
  --include-part TAXI_TIRE_FRONT_A \
  --texture-pack /path/to/GLOBALB.BUN
```

The extraction and export are reproducible from the NTFS partition image:

```sh
ntfscat /path/to/nfsu2-xp-part.img \
  '/Program Files/EA GAMES/Need for Speed Underground 2/CARS/240SX/GEOMETRY.BIN' \
  > /tmp/nfsu2-240sx-geometry.bin
python3 tools/export_nfsu2_car.py /tmp/nfsu2-240sx-geometry.bin \
  reverse-engineering/car-240sx-inspection.glb
```

## Evidence

- `0x00412b90..0x00412ba2` passes the selected slot's definition-index field
  to `0x0063bc70`, saves the returned handle at slot `+0x8f8`, and timestamps
  the slot.
- `0x0063bc70` calls `0x0063ae70` and inserts the returned entry in the list
  rooted at manager `+0x58`; it records the wrapper's additional arguments on
  that entry.
- `0x0063ae70` formats the texture path at `0x007a8d78`, requests it through
  `0x00633b90`, then checks the selected definition's type before formatting
  the vinyl path at `0x007a8d64`.
- `0x0063ae70` passes definition-record `+0x40` to `0x00633b00`; the first
  extracted record stores `CARS\\PEUGOT\\GEOMETRY.BIN` at that offset.
- `0x00633b00` uses the registered solid-pack pool at `0x008a1cf8`;
  `0x00633b90` uses the texture-pack pool at `0x008a1cf4`.
- `0x0061c600` references `0x007a8674`, the `PARTS_ANIMATIONS.BIN` path
  pattern, and queues the optional request after an existence check.
- `0x00494060` dispatches root `0x80134000` children; `0x00493bb0` parses
  nested `0x80134010` chunks, and `0x00493f20` finalizes them.
- `0x00493bb0` handles child tags `0x134011..0x13401b`; `0x005cc280` handles
  nested `0x80134100` and upload subtags `0x134b01..0x134b03`.
- In `0x005cc280`, the `0x134b01` branch calls graphics vtable slot `+0x68`
  with the aligned vertex payload length; the `0x134b03` branch calls slot
  `+0x6c` with its length and format `0x65`, then copies both payloads into
  locked buffers. The latter call is the 16-bit index-buffer path.
- The 240SX `TEXTURES.BIN` has 73 matched catalog/descriptor entries; each
  compressed blob decodes, its embedded image header hash matches the key, and
  the mesh range's selected slot hash resolves directly against the texture
  directory when an image is used. See the decoded field meanings and limits
  above.
- `0x00412a8c..0x00412a9d` retires an eligible slot through
  `0x0063ae00`; manager destruction uses the same release helper.
- `0x00420030` loads resource records from `0x00890080`, calls
  `0x00601110`, and reconciles definition/variation pairs against manager
  slots. `0x006046aa` calls the same initializer while constructing a
  per-car object.
- `0x0061ca50` searches linked resource-manager entries by key and calls
  `0x00610910` on matching entries; it does not itself directly increment a
  reference counter.
- `0x006065b0` calls `0x00606480` for descriptor types 1/2;
  `0x00606480` allocates `0x9e0` bytes and initializes world position/heading
  through `0x00604260`, then links the object into `0x00890080`, used by
  traffic-context setup and frame updates.
- `0x00601860` and `0x00607180` populate the `+0xbd0` table of
  0x1f0-byte traffic contexts from records in the global collection.
  `0x005fc2a0` stores the source record pointer and the reverse context
  pointer at source `+0x1c`.
- `0x006316d0` is called inside the render frame, walks the live collection,
  and dispatches non-type-3 entries to `0x006162e0`; that renderer scans 63
  `0x20`-byte records beginning at render-owner `+0x17a8` and submits eligible
  resources through `0x005c5930`. `0x00631e20` builds car-part resource hashes
  from templates including `%s_RIM`, `%s_TIRE`, `%s_NEON`, `%s_SHADOW`,
  `%s_LOGO`, `%s_DOOR_HANDLE`, `%s_LICENSE_PLATE`, `%s_INTERIOR`, and
  `%s_MISC`; the loader at `0x00634800` stores resolved geometry handles into the indexed
  table at `+0x17a8`. Init and cleanup cover 63 `0x20`-byte records with two
  groups of four handle positions each; the renderer also visits exactly 63
  entries and chooses one of four levels in each group using computed-size
  thresholds `40`, `20`, and `10` (unless overridden by global selectors).
  The loader address formula is `+0x17a8 + category*0x20 + bank*0x10 +
  detail*4`; key construction uses a `_B` base suffix followed by `_A` through
  `_D` detail suffixes, and absent higher-detail resources inherit the
  preceding detail handle.
- `0x005f3e10` hashes the strings `MARKER_DIRECTION_AID` and
  `MARKER_GPS_AID` into attachment resources `+0x10c/+0x110`;
  `0x005f3ae0` submits the directional aid and `0x005f3cb0` updates/caches
  the GPS-aid position.

These string paths are from the installed retail executable. The bundle names
and type-specific branch are direct observations. The state-7 traffic callback
connects the directional-aid marker resource to the range queue and indexed
draw pass. The live-object dispatch reaches a renderer over the table populated
from authored car-part resource keys. Its final-entry boundary and exact
part-category mapping remain open.


## Follow-up: retail render slots and current WebGL audit

The routine containing `0x00638400` clears a `0x7e0`-byte region at owner
`+0x17a8` from `0x0063841c`, stores its input resource descriptor at owner
`+0x8`, copies render-state hashes from the descriptor, and calls
`0x00636840`. The observed call into the per-car part loader at
`0x00634800` is at `0x006387c9`. The render path at `0x006162e0`
selects among the already populated category/detail handles according to
computed size. This confirms that the retail render object has
distance-selected slots, but does not yet prove which traffic geometry objects
populate them. The descriptor-to-mesh category map and wheel attachment
transforms still need recovery.

The current viewer has no generated lane-line pass (`syntheticMarkings: 0`).
The `RDP_LANEA/B/C` and intersection maps extracted from `STREAML4RA.BUN` are
narrow asphalt images; their material blend flags are `0`, so texture alpha is
not used as blend coverage. The viewer swaps red/blue for P8 (`0x08`)
textures before sampling: the raw `RDP_LANEA_YS_AA_KT` PNG looks cyan at its
edge, but the runtime-sampled texture has its yellow edge stripe. The DXT3
`RDP_LANEB_WD_AA_KT` map contains a white edge stripe, and
`RDP_PARKING_WL_AA_KT` contains parking stripes. These marks come from their
sampled textures. The `RDP_LANEA_YS` and `RDP_LANEB_WD` mesh slices also carry
warm and bright-neutral vertex colors. An r85 shader experiment preserved
that vertex chroma, adding false yellow/white tint to the texture. Revision
r86 removes vertex chroma from this opaque road path; sampled texture color
supplies the road markings, while vertex luminance supplies only the prelight
adjustment.
Parsing the full installed `STREAML4RA.BUN` TPK finds 24 records named `RDP_*`;
the lane maps are 64x512 and no separate line/dash/crosswalk map appears among
those road records. The exported L4RA material images resolve to these same
TPK keys. This confirms the yellow/white edge paint is texture-authored and
the r85 tint was wrong. The correct texture coordinates and material placement
still need route-level comparison with retail gameplay; the texture inventory
alone does not establish which side of each road tile carries a center or lane
divider. Revision r92 also corrected `RDP_ROADSTRIP_MC`: it is an opaque source
slice, not a sparse alpha overlay over a second estimated road texture.

A fresh SwiftShader WebGL2 capture at route 450, distance 300, loaded four
traffic bodies (PANELVAN, SUV, HATCHBACK, PANELVAN) on routes 790/791. Their
runtime route-direction dot values remain about `-0.999` relative to the
source file order because the viewer reverses those waypoint arrays to follow
the executable's retail direction basis. Their model-base elevations match
sampled road elevations within 0.001 world units. Those bodies were upright in
that capture, but this does not cover every traffic route or prove the
retail part transforms. All current previews still estimate tire placement
from body bounds, and the disassembly has not yet identified the category to
mesh attachment data needed for a faithful assembly.

The r81 live-route capture also shows why the vehicles can read as inverted:
the body shells stay upright, but the estimated black tire disks project above
some roofs. In the baked source bounds, `COUPE_TIRE_FRONT_A` diameter is 1.01
times the BODY_A vertical extent and `SUV_TIRE_FRONT_A` is 0.85 times that
extent. Their corresponding B tire diameters are 0.59 and 0.53 times the body
height. This makes the A meshes visibly suspect for those bodies, but does not
prove B is the retail selection or reveal mounting positions. Revision r87
lets `?trafficModel=NAME` load one source model at a time for route inspection;
the normal build still loads the full quota-eligible traffic roster.

The r87 source-viewer route diagnostic isolated this assembly. Chrome's CDP
session reached the rendered city state with the full 96-texture set uploaded,
`trafficModel=COUPE`, and no page exceptions. After a 4.5-second W hold the
HUD read 38 mph; the live debug object reported four rendered COUPEs on route
450, directed-route dot values near `-0.988`, and model bases 0.0043 world
units above their sampled road surfaces. That A-tire view showed upright
bodies but tire disks sticking above the roofs.

Revision r88 exports both authored tire objects into each traffic preview GLB
and adds `trafficTireVariant=A|B` for direct comparison. A live route capture
after the texture set reached 96/96 shows the black A disks above the coupe
roofs; the B version places its smaller authored disks below the body line.
The default preview now uses B, with A retained as a diagnostic. In source
bounds, COUPE A's tire diameter is 1.01 times BODY_A height while B is 0.59;
SUV's ratios are 0.85 and 0.53. These measurements explain the visual failure
with estimated mounts, but do not identify which bank/detail slot the retail
loader binds to TIRE_FRONT_A/B. Four tire positions, wheelbase, and track also
remain estimates. Next, map the retail resource-key records to these authored
object names and recover the four wheel anchors.

## Retail tire-name evidence

The installed `GLOBALB.BUN` contains the outer `0x80034602` catalog chunk at
file offset `0x6e0a94`. Its `0x34606` child begins at `0x6e0ae0`; the first
catalog labels include `HATCHBACK02 TIRE`, `HATCHBACK TIRE`, `MINIVAN TIRE`,
`SUV TIRE`, `BUS TIRE`, `COUPE TIRE`, and `TAXI TIRE`, followed by `Stock Front
Tire`. These are catalog labels from the retail data. They do not name the
`TIRE_FRONT_A/B` geometry objects, so they cannot establish which mesh the
traffic renderer binds.

The retail executable separately contains the resource template `%s_TIRE`
and cullable-part labels `CULLABLE_CAR_PART_TIRE_RL`, `_RR`, `_FR`, and `_FL`.
Together with the `0x00634800` category/detail loader, this confirms a generic
tire resource category and four named wheel positions exist in the retail
system. The missing link is the model resource record that maps those keys to
the geometry bundle's front tire variants and four transforms. The WebGL B-tire
default remains a measured visual fit correction; that mapping is still open.

## Road material slices and current route capture

The clean L4RA export preserves the source material slices as separate
primitives. For `TRN_SH_ROADA_CHOP_A1_R1_0`, the intersection, yellow lane,
neutral lane, and shoulder materials use 8, 37, 18, and 30 triangles. Their
index sets have no overlapping triangles, while they share the same source
vertex and UV arrays. This confirms that these markings are assigned by the
retail material slices and their texture coordinates; the viewer does not
need to stack generated paint over the road for this mesh. The browser reports
`syntheticMarkings: 0`.

A fresh route 450 capture at revision r88 shows PANELVAN, SUV, and HATCHBACK
traffic bodies. The live diagnostics put their body bases within 0.001 world
units of the sampled road elevation. The bodies look upright in this one
frame, but that check does not cover every model or route. The screenshot was
taken at 92/96 nearby city textures; road textures had already loaded. It
shows that source materials are being sampled, but does not settle whether
their UV placement matches the retail game's appearance. The XP VM still does
not reach a visible game desktop, so that direct comparison remains open.

## Retail cullable wheel-part keys

The executable's static cullable-part records begin at `0x00803ae0` with a
`0x50`-byte stride. Their string keys map the first ten category ids as:

| Category id | Retail key |
| ---: | --- |
| 0–3 | `CULLABLE_CAR_PART_TIRE_FL`, `_FR`, `_RR`, `_RL` |
| 4–7 | `CULLABLE_CAR_PART_BRAKE_FL`, `_FR`, `_RR`, `_RL` |
| 8–10 | `CULLABLE_CAR_PART_SIDE_FRONT`, `_SIDE_REAR`, `_UNDERNEATH` |

The tire and brake names therefore have an exact retail corner order. Function
`0x0060c280` indexes these records and loops over their probe vectors; this is
cullable-part data, not proof of the geometry attachment positions. The
separate four wheel transforms remain unresolved, so the WebGL viewer still
uses estimated tire mounts.

A fresh packaged-build audit forced each of the twelve positive-quota traffic
body meshes onto the same recovered route. With `TIRE_FRONT_B` selected, all
twelve bodies appeared upright in the capture and produced no browser errors.
This is a one-view body orientation check; it does not validate their driving
directions across routes or substitute for comparison with retail gameplay.

## Road-texture pass, revision r90

The opaque `RDP_LANE*`, `RDP_INTERSECT*`, parking, and grill materials now use
sampled texture RGB after palette decoding, without a display gain or vertex
color. This avoids modifying the source pattern in the road shader. It does not
prove that each road piece has the same material/UV assignment as the retail
runtime.

An RGB inspection confirms that `RDP_LANEA_YS_AA_KT` contains a yellow edge
stripe and `RDP_LANEB_WD_AA_KT` a white edge stripe. The inspected
`RDP_INTERSECT_AA_WH` looks like asphalt grain when viewed alone at native
resolution. Rendering the `RDP_INTERSECT_AA_WH` material slice of
`TRN_CS_ROADA_CHOP_M1_R3` with its exported source UVs and the P8 channel swap
reproduces the broad crossing bars in the route capture. The line marks are
therefore texture-authored and placed by the retail mesh UVs; there is no
generated marking geometry. The per-material projection is shown in
[`176-intersection-texture-material-layers.png`](../webgl-city/review/images/176-intersection-texture-material-layers.png).
The r89/r90 captures still do not establish that every road tile or visible
mark in the complete city is placed correctly.

## Route 852 direction edge case, revision r91

The waypoint-direction audit covers all 1,154 L4RA route records. Their
length-weighted mean dot products against increasing file order are negative;
1,153 are below `-0.25`, while short route 852 (`TrackRoutesA32`) is `-0.125`.
That four-waypoint spline has segment agreements `-1.00`, `-0.47`, and `+0.13`,
so its mean is weakly negative because its final short curve turns against the
prior basis. The old `-0.25` confidence threshold left that route in file order
and sent traffic against the dominant retail direction. Revision r91 reverses
any route with a negative mean, retaining source waypoint IDs for junction
links. This follows the decoded basis, but route 852 still needs a live view
against the retail game.

## Road-strip alpha interpretation, revision r92

The clean GLB preserves the stream's per-material `usage`, `blend`, and
texture-format fields in `material.extras`. In L4RA, the lane, intersection,
parking, grill, and `RDP_ROADSTRIP_MC` records use `usage=2`, `blend=0`.
The viewer had treated the road-strip RGB as a translucent detail mask and
composited it over the `RDP_ROADSTRIP_FREEWAYCE` texture. That was an inferred
blend which contradicted the recovered material flag. Revision r92 now samples
the full source RGB for every such opaque RDP slice and ignores alpha for
coverage. No route-derived lane paint is drawn. The `roadSourceMode=legacy`
query retains the previous composite for visual comparison; the corrected
default still needs validation against a retail-gameplay capture.

## Traffic heading follows waypoint basis, revision r93

The route export preserves each waypoint's decoded `route_point_vector` from
the `0x34121` record. The retail lane controller reads the route basis when
resolving forward lookahead; the WebGL preview had used only `atan2` of the
straight chord between adjacent sparse waypoints to orient each body. Revision
r93 linearly interpolates the decoded XY basis for the traffic yaw and
sign-aligns each basis with the traversed segment. That preserves the route's
current direction choice and uses authored heading at curves. If a local basis
opposes its segment, the viewer follows that segment to avoid turning a car
backward at that point. This improves the heading source, but the complete
Bezier/traffic-mover path and all route transitions still need comparison with
the retail executable.

## Traffic body frame audit, revision r94

The manifest records the transform-derived long/up/side axis for each exported
BODY_A/B/C mesh. Checking all 43 available body variants finds a unique
permutation of the three output axes for every mesh; there are no long/up/side
axis collisions. The 12 BODY_A models with positive normal-density quotas all
have valid frames. The WebGL traffic matrix maps each mesh's measured up axis
and sign to world +Z, and corrects triangle winding when that mapping reflects
the mesh. A close moving COUPE capture on route 791 shows the body upright and
following its decoded route heading; its base is within 0.005 world units of
sampled pavement in that frame. This narrows the reported inversion to other
models/routes or remaining body/tire assembly errors. It does not prove every
moving fleet instance or route transition matches the game.

## Retail traffic wheel anchors, revision r95

The traffic definitions in `GLOBALB.BUN` also contain four axle coordinates at
record `+0x120`, with a `0x30`-byte stride. The same decoder already used these
values for player-car stock wheels. Applying the corresponding traffic record
to each model gives authored front-left, front-right, rear-right, and rear-left
positions in the original car frame (`+X` forward, `+Y` lateral). The exporter
now transforms each position through the selected `BODY_A/B/C` object's linear
and translation terms, matching the coordinate frame of its baked GLB body.
The WebGL traffic assembly uses these positions instead of estimating axle
spacing and track from body bounds. The tire mesh (`A` versus `B`), runtime
detail-bank mapping, and tire orientation/steering animation remain unconfirmed
in the retail draw path.


## Traffic tire object transforms, revision r96

The geometry exporter records the authored 4x4 transform for every `BODY_A/B/C`
and `TIRE_FRONT_A/B` object. Traffic preview meshes are exported with those
object transforms baked independently. The viewer now applies the relative
transform `bodyTransform * inverse(tireTransform)` before placing each tire at
the `GLOBALB.BUN` axle coordinate. This expresses the tire mesh in the selected
body's baked coordinate frame while retaining the source axle spacing and
track. The traffic route capture shows level COUPE bodies on the source pavement
with this transform path active. The source tire meshes remain very low-poly,
and their runtime A/B detail selection and steering state are still unresolved.


## Shared traffic textures restored, revision r97

The 15 traffic GLBs embed the shared `CARBOTTOM`, `DRIVER`, `WINDOW_TRAFFIC`,
and `TIREBACK` textures from the installed `GLOBALB.BUN` TPK when those hashes
resolve. The model-specific paint hash still has no matching image in that
shared pack and stays a fallback paint color. A previous re-export in the
anchor work omitted the texture-pack argument, which dropped all images from
the traffic GLBs. Revision r97 regenerated the exports with the original TPK;
the exporter now automatically includes the same `GLOBALB.BUN` when it is
provided for axle anchors. A fresh Chrome capture shows the source glass and
tire maps again. This still needs comparison against the XP game for paint and
complete model assembly.


## Vehicle vertex prelight, revision r99

PC car vertices use a 36-byte stride: position at `+0x00`, normal at `+0x0c`,
packed BGRA color at `+0x18`, and UV at `+0x1c`. The exporter had replaced
those authored colors with white. It now converts the BGRA bytes to glTF
`COLOR_0` for traffic and player-car geometry, preserving the source's per-vertex
prelight and panel shading in the WebGL material path. The updated exports cover
all 15 traffic models and all 29 stock player cars. Chrome captures show more
shape shading on the 3000GT and COUPE; the traffic body-paint hash still has no
matching texture in the shared TPK and remains a fallback paint color. This is
source-data preservation, not yet an XP screenshot comparison.


## Authored marking sample path, revision r100

`RDP_LANE*`, `RDP_INTERSECT*`, `RDP_PARKING*`, and `RDP_ROAD_GRILL*` use the
sampled material RGB directly after the retail P8 channel swap. The viewer no
longer interprets their alpha as a coverage mask or applies prelight and an
estimated asphalt underlay to those marking materials. There is no generated
lane-line geometry pass. Traffic GLBs also receive a new cache version so the
browser reloads the source-axis exports instead of keeping older cached
transforms. This corrects the rendering path from the extracted source data;
the traffic fleet still needs a fresh browser capture after full asset upload.


## Source-axis fleet audit and runtime paint, revision r101

The renderer audit was repeated with all 12 positive-quota traffic `BODY_A`
meshes selected in turn at one route-791 pose. Each body appears upright, the
viewer reports no browser errors, and the `GLOBALB.BUN` axle-anchor height is
within about 5 cm of the sampled pavement. This is one controlled pose and
does not prove that every route transition is correct.

The previous WebGL viewer assigned a hand-made nine-color palette to traffic
cars. The executable's live vehicle draw instead resolves the material hash
`0xd6d6080a` through render-owner state `+0x384` (along with other paint and
render-state hashes). The palette is removed until that runtime state is
recovered; source vertex prelight and the neutral material fallback remain.
The body-paint hash has no matching texture in `GLOBALB.BUN`. The source tire
meshes are still low detail and their visual/material match needs further work.


## Optional vehicle paint-state lookup, revision r102

The constructor at `0x00638d7f` receives a parent/configuration object and
checks its pointer at `+0x68c`. If present, it calls `0x0061f700`. That helper
reads a 16-bit selector at descriptor `+0x0a`, uses the table manager at
`0x008a1cc8` to find the selected entry whose key is `0x6ba02c05`, and returns
the entry's state handle. The constructor resolves that handle through
`0x0048f6a0` and stores the result at render-owner `+0x384`. If the optional
descriptor is absent, the constructor passes null to `0x0048f6a0`; if the
descriptor exists but its selector is `0xffff`, `0x0061f700` returns null.
Both paths reach the default state object from `0x0048f6a0`.

The live draw later resolves material hash `0xd6d6080a` against this
`+0x384` state. This shows that paint variation depends on a runtime
parent/configuration selector, rather than a color list in the extracted
traffic geometry or quota fields. The key `0x6ba02c05` identifies an entry in
the state-handle map (documented below); the descriptor selector used by
ambient traffic and the final color/texture/finish operation inside the
resolved state object remain unknown. The WebGL renderer therefore keeps the
neutral fallback and does not invent a color palette.

## Ambient paint descriptor source and wheel shading, revision r155

The installed `GLOBALB.BUN` definition export now preserves the two words at
definition-record `+0x84c` and `+0x850`, under `key_3f_selector_words` in
[`traffic-vehicle-definitions.json`](traffic-vehicle-definitions.json). In the
type-2 class-2 setup path, `0x0041ab55` calls `0x00637040` with argument zero.
That helper selects the word at `+0x84c` and requests descriptor key `0x3f`
through manager `0x008a2b68`; it stores a successful lookup at live owner
`+0x68c`. All 15 type-2 definitions contain `0x253570af` at both `+0x84c` and
`+0x850`. This narrows the missing edge: class-2 actors share the same
definition-side descriptor key, and `0x00638d7f` later reads the selector
word at `+0x0a` from the returned descriptor. The extracted `0x34600`
definition table does not contain that `+0x0a` value. The `0x6ba02c05`
lookup rows are present in the `0x34605` state-handle map, but without the
missing descriptor there is no evidence for which selector ambient traffic
uses. The resolved state's appearance also remains unknown.

The current close capture
[`244-r153-traffic-texture-frame-check.png`](../webgl-city/review/images/244-r153-traffic-texture-frame-check.png)
shows the limitation directly: an untextured gray body, brown translucent
windows, and nearly black wheels. In the exported taxi model, `rubber_vehicle`
is the no-texture material on the main 90/48-index tire slices; the separate
`TIREBACK` map is applied only to a six-index back-face slice per tire and is
nearly all black with transparent pixels. The exporter assigns the rubber
fallback factor `[0.12, 0.13, 0.14, 1]`; the viewer multiplies this by source
vertex color/prelight and directional lighting and adds no rubber specular
finish. This explains the dark appearance in the current renderer, but the
retail rubber shader/material default has not been recovered. There is no
source-backed replacement color, tire texture binding, or paint texture to
apply, so the viewer materials remain unchanged.

## Class-`0x3f` selector lookup boundary, revision r156

The retail lookup at `0x0061b980` sends the supplied descriptor class and key
through the manager at `0x008a2b68`; its helper `0x0061b670` scans the loaded
resource-key entries and, after finding the descriptor, reads its selector at
`+0x0a`. A non-`0xffff` selector indexes the signed-word list at manager
`+0x14`; each list entry selects an eight-byte state row from manager `+0x1c`,
where the first word is compared with the requested state hash. In the traffic
setup, that requested hash is `0x6ba02c05`. The manager setup at `0x00636ccb`
builds these table pointers from the loaded `0x80034602` resource catalog.

The installed `GLOBALB.BUN` does contain state-table entries for
`0x6ba02c05`: the `0x00034605` child at file offset `0x6f54f0` has payload
size `0x90e0` and includes 99 little-endian occurrences of that key. This
child is a flat sequence of 8-byte `(key, value)` pairs. That layout is
confirmed by the lookup: `0x0061f700` indexes manager `+0x1c` with
`index * 8`, compares the first dword to `0x6ba02c05`, and returns the second
dword. The 99 matching values are distinct 32-bit resource hashes; the first
is `0x471a1dca`, and the first 31 matches are interleaved with pairs keyed by
`0x4732da07`.

The hashes are no longer opaque: 98 resolve to named state resources in the
same bundle. Each is an outer `0x00135200` chunk with a `0xa0`-byte payload,
containing an inner `0x0041440f` record, the state hash, a fixed-width name,
and a 0x60-byte state payload. Names include `REGPAINTGRAY` through
`REGPAINTPINK`, the metallic paint choices, `PEARL`/`PEARL1` through
`PEARL9`, `CUSTOMPAINT_1` through `CUSTOMPAINT_20`, and windshield tint and
rim states. The remaining value, `0xd6640dff`, occurs inside the state-map
chunk itself and does not have the named-resource chunk structure; it is
treated as a hash collision rather than a resolved resource. The exporter
[`export_nfsu2_paint_states.py`](../tools/export_nfsu2_paint_states.py)
records this mapping and preserves each payload as raw bytes in
[`traffic-paint-states.json`](traffic-paint-states.json). The payload's
individual fields and rendering semantics are still unknown, so the names
identify available state resources but do not by themselves provide their
final color, texture, or finish.

The class-`0x3f` descriptor that would select one of these entries is still
missing. A raw scan of the installed game directory (about 1.7 GB) finds the
descriptor key `0x253570af` exactly 30 times, all in the 15 type-2 definition
records at `0x34600` offsets `+0x84c/+0x850`; it is absent as a serialized
resource key elsewhere. Those definition fields request the missing
descriptor, but do not contain its `+0x0a` selector. Without that selector
and the resolved state object's payload, the 99 table values cannot identify
which state ambient traffic uses or reveal its color, texture, or finish. A
runtime snapshot of manager `0x008a2b68` after traffic setup remains the next
evidence needed; the viewer keeps its identity fallback and explicitly
unresolved paint state.

## XP runtime snapshot attempt, revision r157

I verified the remaining runtime step with a throwaway QEMU boot of
`/tmp/nfsu2-xp-review-overlay.qcow2`, which is a COW child of the installed XP
disk, and an additional QEMU `-snapshot` layer. The base qcow2 and named
overlay retained their original sizes and modification times. XP reached the
James logon screen; blank-password login failed, and the normal built-in
Administrator account with a blank password also failed. I stopped there
without resetting credentials or changing the base disk. QEMU can expose its
GDB stub on localhost for live inspection, but the game must first be launched
inside XP so the manager tables are populated. The exact remaining dependency
is valid XP logon credentials (or a separately authorized way to launch the
game in that guest); without them there is no live class-`0x3f` state to
capture. The viewer remains unchanged.

I also checked the installed vehicle texture candidates before treating that
runtime table as the only remaining source. All 15 ambient traffic model
folders have zero-byte `TEXTURES.BIN` files. The nonempty installed car
`TEXTURES.BIN` and `VINYLS.BIN` catalogs decode successfully (31 and 34 files,
respectively), but none contains the paint material key `0xd6d6080a` or the
ambient selector key `0x253570af`. The shared `CARS/TEXTURES.BIN` and
`CARS/WHEELS/TEXTURES.BIN` likewise do not supply either key. They therefore
cannot replace the missing class-`0x3f` descriptor/state rows; they provide no
source-backed body-paint image or selector value for the WebGL export.

The needed capture is the runtime manager at `0x008a2b68` after traffic setup:
the resource entry selected by class `0x3f` and key `0x253570af`, including its
`+0x0a` selector, the selected signed-word list at manager `+0x14`, and the
`0x6ba02c05` state row in manager `+0x1c`. To reproduce the final appearance,
also capture the resolved state object passed to the vehicle draw for owner
`+0x384` (and the draw-time value/binding used for material `0xd6d6080a`).
Until that table or an equivalent retail screenshot/material-state trace is
available, changing the neutral fallback would invent a paint color or finish.


## Road marking and fleet complaint follow-up, revision r103

The current live viewer reports zero generated marking segments. Its visible
crosswalk, yellow hatch, lane stripes, and pavement detail are sampled from the
source `RDP_*` textures through their exported source material slices and UVs.
The current route-450 capture is available as
[`187-r102-live-road-textures.png`](../webgl-city/review/images/187-r102-live-road-textures.png);
the isolated material-layer capture is
[`176-intersection-texture-material-layers.png`](../webgl-city/review/images/176-intersection-texture-material-layers.png).
The live capture reached 92 of 96 nearby textures. These captures confirm the
render path and source assignments, but do not establish that every stripe is
placed as in retail gameplay; a usable XP gameplay reference is still absent.

The latest close fleet capture shows the BODY_A shells upright in the checked
pose. This does not disprove reports of overturned-looking traffic: the preview
still lacks the retail detail-slot binding and wheel attachment transforms.
The smaller authored `TIRE_FRONT_B` meshes are active, and the four longitudinal
and lateral axle coordinates come from `GLOBALB.BUN`, but tire scale/orientation
and full route-by-route traffic transforms remain unverified. The source data
and this preview are therefore not yet a faithful traffic assembly.

The retail car-part renderer has a four-level detail path. Function
`0x006316d0` walks the live vehicle collection, tests the definition type at
`[object+0x14]+4`, and dispatches non-type-3 objects to `0x006162e0`; the
type-3 branch uses `0x00620ae0`. Immediately before the non-type-3 call it
loads the render owner from `object+0x554` and passes it as `this`. This joins
the live type-2 object path to the same renderer that reads the 63 category
records at owner `+0x17a8`. The render-owner setup calls `0x00634800` at
`0x006387c9`; the loader builds `_A`, `_B`, and onward resource keys in
four-level groups. `0x006162e0` chooses a level from projected size at
thresholds 40, 20, and 10. The exact resource keys populated for each type-2
model and how their BODY_A/B/C files map to those detail slots still need to
be matched against the loaded resource records. In `0x34600`, player-car
definitions have 51–52 nonzero values in `+0x590`, while quota-bearing
traffic definitions have only 10. Reading the extracted traffic words shows
they are five repeated float parameters at indices 48–52 and 56–60 (12.0, 1.0,
1.0, 2.0, and 1.5), not pointers to the player-part descriptor array.
Therefore this renderer link does not identify the retail body variant or
tire assembly; the runtime render-owner resource binding must be traced
separately from the `0x34600` traffic-definition layout.

## Traffic definition and render-owner boundary, revision r104

I rechecked the `0x34600` records in the extracted `GLOBALB.BUN` against the
retail loader. The type-2 traffic records' `+0x590` words are identical float
bands, not geometry pointers. A separate pointer path explains why this does
not settle the part binding: `0x00634800` indexes `+0x590` through render-owner
`+0x4`, while the `0x34600` global definition row is resolved separately and
stored at owner `+0x8`.

The address `0x00638400` was previously described as a constructor entry. The
disassembly there starts mid-routine; the confirmed table clear is at
`0x0063841c`, and the observed call into the part loader is at `0x006387c9`.
This correction narrows the next trace to the runtime render owner passed
from live traffic object `+0x554` into `0x006162e0`. It does not establish the
type-2 resource-name-to-mesh binding.

The owner construction now has a confirmed input boundary. `0x0063a5a0`
allocates a `0x2ec0`-byte owner and calls `0x00638190` with the live object
and its `+0x10` pointer; the returned owner is stored at live-object
`+0x554`. In `0x00638190`, owner `+0x0` receives the live object, owner `+0x4`
receives that `+0x10` pointer, and the first dword of the pointer selects a
`0x890`-byte global definition row which is stored separately at owner
`+0x8`. The part loader at `0x00634800` uses owner `+0x4`, not owner `+0x8`,
for its indexed category data.

The producer path for live-object `+0x10` is partly resolved. `0x00604260`
stores its source-record argument at live-object `+0x14`. The initializer
`0x00601110` then copies the descriptor beginning at source-record `+0x10`
into the live object's `+0x14` record using `0x00413440`, and sets live-object
`+0x10` to the copied descriptor at `+0x14+0x10`. It separately resolves the
descriptor's first dword to the global definition row at live-object `+0x0c`.
That cloned descriptor—not the `0x34600` definition row—is therefore the
input to the render owner's 63 category slots. Its runtime source record and
the names/handles in its category entries remain to be identified.

Road materials remain source driven in the current WebGL build:
`syntheticMarkings` is zero, and the corrected default draws each opaque
`RDP_*` material slice once using its exported UVs and source texture. The
captures confirm that source images are sampled, but the misplaced-looking
paint in some views still needs comparison against a visible XP gameplay
reference. This trace provides no basis for generating replacement line
geometry.

The current WebGL diagnostic still renders the 12 positive-quota BODY_A
models upright at one route-791 pose, while that is not a route-wide proof.
The visible traffic stays crude and the user's report of inverted or
backward cars remains open pending the retail resource-key-to-mesh mapping and
a close live-route comparison.

The viewer's traffic chooser also fell back to the full loaded roster when no
model remained eligible in the selected quota column. The normal loader already
filters out definitions with no quota in column 0, so that fallback did not
introduce the excluded bus, ambulance, or fire truck models; my first
interpretation of this bug was wrong. Revision r102 now restricts fallback to
models with a positive quota in the active column and returns no model when
that column has none. The recovered L4RA areas use columns 0–3, each with 12
positive-quota models and a quota total of at least 56, above the seven-car
active limit. This makes the chooser's fallback honor the source table, but is
not a fix for the reported orientation or tire-assembly problem and is not
expected to change ordinary L4RA traffic under the active-car limit.

## Opaque road-slice replay removed, revision r103

The main pass already samples the source `RDP_*` road material image for each
exported material slice. The `usage=2`, `blend=0` records in the extracted
L4RA GLB are opaque. A separate tail pass nevertheless replayed every
`RDP_ROADSTRIP_*` primitive classified as composite 2 with alpha blending,
which applied a second, guessed prelight to the same road strip. That replay
is now limited to the explicit `roadSourceMode=legacy` comparison path. The
normal retail-material path draws each authored road slice once. This removes
an incorrect renderer operation; it does not change the source UV layout or
prove the road stripes are positioned like the XP game.

The fresh r103 capture reached “City scene ready” with no browser errors and
reports `syntheticMarkings: 0`. The yellow hatched strip remains visible from
its source road texture, so its placement and the missing-looking center line
still need comparison against an XP gameplay frame. Capture:
[`191-r103-road-material-single-pass.png`](../webgl-city/review/images/191-r103-road-material-single-pass.png).

## Runtime descriptor copy and source road markings, revision r105

I followed the descriptor copy helper called by `0x00601110` rather than
inferring the layout from the traffic definition table. `0x00413440` copies
the runtime descriptor from the source geometry record into the live object:
it copies the first 0x570 bytes of the payload in 32-bit words, delegates the
range at `+0x45c` to `0x004133d0`, copies selected state at `+0x580..+0x58c`,
then copies 0xaa dwords from `+0x590` and 0xaa bytes from `+0x838`, plus the
tail fields at `+0x8e4/+0x8e8`. This confirms that the live object's category
array comes from the per-geometry descriptor. It is separate from the
`0x34600` global type-2 definition row, whose repeated values at `+0x590` are
floats. The part loader reads the first 63 descriptor dwords; what each index
resolves to in the installed resource catalog is still open.

The source texture audit identifies which visible road paint is authored in
texture pixels. `RDP_LANEA_YS_AA_KT` contains the edge stripe,
`RDP_LANEB_WD_AA_KT` contains its white counterpart, and
`RDP_ROADSTRIP_MC` contains repeated dashed paint. In the viewer's default
pass, these opaque material slices sample their source images with the
exported UVs, and `syntheticMarkings` is zero. The r103 WebGL frame therefore
does not have a code-generated lane line. It still does not verify that the
exported road slice placement matches an XP gameplay frame; a stripe that
appears in the wrong place must be traced to the source slice, UVs, or
material binding rather than patched with another line overlay. Capture 191
shows the current placement for that follow-up.

The captured live traffic bodies in review images 161, 172, and 193 appear
upright in those tested poses, but their wheel meshes are rough estimates and
the source part-to-detail-slot binding has not been recovered. The matrix
maps each exported signed local forward/up/side axis onto the directed route
basis; that one-pose shell check is not enough to dismiss reports of cars
appearing inverted or moving backwards. Route-wide body and wheel inspection
and the retail resource-key mapping remain required before claiming the
traffic assembly matches the game.

## Exported GLB axis-frame correction, revision r106

A second coordinate conversion was missing from the traffic preview metadata. The
retail vehicle exporter transforms source car coordinates from X-forward,
Y-lateral, Z-up into glTF coordinates `(Y, Z, -X)`, but `viewer_axes()` had
measured the transformed retail axes before applying that final permutation.
The manifest therefore called the wrong GLB component the forward/up/side
axis. Most cars were tagged as X-forward even though their exported forward
axis is GLB Z; using those records in the route matrix could turn a correctly
exported car onto its side or upside down.

Revision r106 measures the semantic axes after `(Y, Z, -X)`, and records model
bounds and GLOBALB axle anchors in that same GLB frame. The body/tire relative
transform is also conjugated through the coordinate conversion before it is
applied to exported tire vertices. All traffic GLBs were regenerated from the
installed XP `GEOMETRY.BIN` files and `GLOBALB.BUN`; the XP sources were not
modified. A packaged SwiftShader route-450 capture selects PANELVAN, SUV, and
HATCHBACK from the recovered positive-quota roster. Their model-base height
matches sampled pavement within 0.001 world units, with no page errors. This
checks the corrected live transform on one intersection; a close all-model,
all-route audit and retail gameplay comparison remain open.

Road markings continue to come from the source `RDP_*` material textures and
exported UVs. The route diagnostic reports `syntheticMarkings: 0`, and the
normal renderer draws opaque source road slices once. The route capture still
does not prove that every source slice was assigned to the correct road tile in
the exported scene or that the tile placement matches the XP game.

## Traffic geometry export frame completed, revision r107

The first r106 capture exposed a further pipeline mismatch: the traffic script
called `car.export()` with its default `gltf_vehicle_axes=False`. That meant the
vertices were still in the transformed retail frame even though r106's new
metadata described glTF `(Y, Z, -X)` axes. The matrix metadata alone could not
fix the scene. Revision r107 now enables `gltf_vehicle_axes=True` for BODY_A,
BODY_B, BODY_C, and tire exports, matching the already-converted axes, bounds,
and GLOBALB wheel anchors. The corresponding winding correction is applied by
the exporter.

The new packaged COUPE-only live-route capture shows two cars in the same
intersection; both bodies are upright and point along the traffic routes. The
first car's base is within 0.005 world units of sampled pavement, with no
browser errors. This is a close one-model check, not a full-fleet or route-wide
verification. The visible crosswalk and lane paint are still sampled from the
source material textures; the renderer reports zero generated markings.
Capture: [`196-r107-coupe-gltf-frame.png`](../webgl-city/review/images/196-r107-coupe-gltf-frame.png).

The r107 fleet review applies the corrected export to all 12 definitions with a
positive quota in the selected free-roam traffic column. The close side-on
captures show upright bodies with the wheel meshes below the body line on the
sampled route-450 intersection. They do not prove route-wide orientation or
match the retail traffic paint and LOD selection. Three other type-2 definitions
have no positive quota in this column and do not appear in ordinary traffic.
See [`fleet-r107.html`](../webgl-city/review/fleet-r107.html).

## Pavement starts for regions without free-roam routes, revision r108

The package includes all eight installed STREAM regions. The two small regions
L4RH and L4RR have no `RoutesFreeRoam` records, despite containing `RDP_*`
pavement triangles. Previously `choosePlayerStartPose()` returned `null` in
these regions and disabled the player drive. The viewer now chooses a valid
long road-width pavement triangle and aligns the initial car heading to its
longest edge when a region has no retail waypoint routes. The other six regions
continue to use their recovered free-roam paths.

A packaged SwiftShader check reached `City scene ready` in all eight regions
with no browser errors. L4RA/L4RB/L4RC/L4RD/L4RF/L4RG use route starts; L4RH
and L4RR use the pavement-only fallback and have no ambient traffic. L4RR's
capture shows the race circuit; L4RH remains a sparse gray pavement scene, so
these starts only establish that the player can move on source pavement and do
not establish full scene coverage in those regions. See review captures 197 and
198.

## Live Bayview drive capture, revision r108

A packaged SwiftShader run started at L4RA route 450, distance 525, then held W
for 3.5 seconds. The player moved about 29 world units from the start while the
sampled road height changed by less than 0.006 units. No road-failure diagnostic,
building/traffic collision diagnostic, or browser error was recorded. The frame
shows the 240SX on the authored pavement, nearby quota-selected traffic, and
lane markings sampled from the source road materials. This validates a short
stretch of the main arterial, not the full road network or the XP scene match.
Capture: [`199-r108-bayview-short-drive.png`](../webgl-city/review/images/199-r108-bayview-short-drive.png).

## Traffic stays on exported waypoint positions, revision r113

The viewer previously searched laterally for `RDP_LANEB/C/D` material
triangles and shifted a traffic body to the first match on its computed right
side. That was a WebGL placement guess, not a recovered retail rule. The
decompiled `AIActionDriveTrafficLane` path chooses route phases and generates
`FuturePoint` samples using waypoint basis and corridor fields; the current
notes establish that this selector applies position transforms but do not
recover the selected lane phase or lateral sign for every traffic actor. The
viewer now uses the exported waypoint position for both rendering and traffic
collision checks, while continuing to sample pavement height at that position.
This removes an unsupported material-name-based offset. It does not claim that
the waypoint coordinate alone reproduces the retail lane path; phase selection
and the full FuturePoint transform remain open. Road paint continues to come
from source RDP textures and UVs, with no generated marking geometry.


## Live body-orientation check on source routes, revision r116

A close Chrome capture on the packaged route-450 scene forced each of the 12
positive-quota `BODY_A` models through the moving traffic renderer and centered
the view on the live cars. The shells are upright in this sample. The coarse
black tire disks still look detached at this scale, so this is not evidence that
the entire car assembly matches the game, and it does not check every route
transition. The prior fixed-pose checks had the same limitation; the user
reported inversion remains open until the affected route/view is reproduced.

The road review confirms the yellow hatch, intersection bars, and edge paint
come from the decoded `RDP_*` textures and source UVs; there is no synthetic
marking pass. The r116 issue is material-slice placement across the exported
road meshes, which still needs a direct comparison with the XP game.
The close 12-body capture is
[`209-r116-live-traffic-body-audit.png`](../webgl-city/review/images/209-r116-live-traffic-body-audit.png).

## Route-450 texture and body-axis recheck, revision r117

A fresh packaged Chrome capture from the live L4RA viewer shows the arterial
from above. The center stripe and white lane dashes are sampled from the
exported `RDP_*` material textures and UVs; the renderer reports zero synthetic
markings and does not emit lane-line geometry. The package also embeds the
source lane and intersection images. This confirms their source, not the
citywide attachment/UV placement that the user reports as incorrect. The new
capture is [`210-r117-route450-source-markings.png`](../webgl-city/review/images/210-r117-route450-source-markings.png).

The road-focused capture does not establish traffic orientation. A previous
close BODY_A sample showed upright shells on this route, but the reported
inverted cars and backward travel on other routes remain unexplained. The viewer
maps each selected model's measured signed long/up/side axes to the route basis,
with the up basis fixed to world Z. A route-transition audit against retail
waypoint directions and fresh per-route captures is still required; neither
sample establishes completion.

### Directed route-basis audit

I compared each route segment with the decoded waypoint `route_point_vector`
after applying the viewer's whole-route endpoint reversal. In all six populated
regions, no retained usable route had more than 10% of its length opposed to
that source basis (using a per-segment threshold of dot < -0.25). This makes a
wholesale route-file-order reversal an unlikely explanation for the reported
backward cars. It does not validate the target-route direction chosen at each
junction or prove that the moving model's front axis matches retail.

A separate reproduction of the viewer's node-link transition selection found
1,132 links in L4RA and 77/87/17/43/30 in L4RB/L4RC/L4RD/L4RF/L4RG. L4RA's
weakest accepted turn has heading alignment 0.451 (about 63 degrees); it still
points forward by the current geometric filter, but it is a prime candidate to
inspect against the executable's runtime route-phase choice. I have not changed
the transition threshold without retail evidence. The reported route-wide
inversion/backward motion therefore remains open.


## Live weak-link transition audit, revision r118

The viewer's debug pose now distinguishes the assigned spawn route from the
route actually being traversed after a junction. On the weakest accepted L4RA
link, route 1016 joins route 1203 at its source waypoint 27. The live pose then
advances toward source waypoint 26 with heading about -0.12 radians, matching
the retail-decoded basis (approximately east with a slight south component).
This establishes that this particular 0.451 alignment is a sharp forward join,
not a backward turn. Two traffic bodies appear upright in this route sample,
but body/tire fit is still crude and the broader inversion report remains
unresolved. Capture: [`211-r118-route1016-transition.png`](../webgl-city/review/images/211-r118-route1016-transition.png).

## Route-450 source-object audit, revision r119

I inspected the current packaged Chrome scene at the intersection around
world XY `(-917, -366)`. The street mesh is `TRN_CS_ROADA_CHOP_F4_R1`; its
exported material slices include `RDP_INTERSECT_AA_WH`, `RDP_LANEA_YS_AA_KT`,
and `RDP_LANEB_WD_AA_KT`. The crossing bars and lane paint in this view are
from those source texture images and UVs. The viewer still reports zero
synthetic marking geometry. This identifies their source but does not show
that the road piece is placed or oriented as it is in retail gameplay; the
user's report that the marks look wrong remains open.

The live pose snapshot included 4DR_SEDAN02 on route 744, PICKUP on route 741,
HATCHBACK02 on route 739, and PANELVAN on route 737. Their model-base Z values
were between 0.001 and 0.052 world units from the sampled road surface. The
visible shells in this intersection capture were upright, so this sample does
not reproduce the reported inverted traffic. The route-wide inversion/backward
motion issue and the retail body-part/detail-slot mapping remain unresolved.
Capture: [`212-r119-sceneaudit.png`](../webgl-city/review/images/212-r119-sceneaudit.png).

## Route-744 live traffic and material recheck, revision r120

I centered the packaged Chrome view on the moving route-744 traffic sample and
read the exact material records from the exported L4RA GLB. The intersection
street mesh still binds `RDP_INTERSECT`, `RDP_LANEA`, and `RDP_LANEB` source
images; all three are `usage=2`, `blend=0`, so the viewer samples their RGB
directly. Its live diagnostic still reports `syntheticMarkings: 0`. This rules
out generated lane-line geometry in the current package, but the source mesh,
material slice, or UV placement can still be wrong for the user's intended
street; that needs a same-location XP gameplay reference.

Four cars rendered on routes 744, 741, 739, and 737 in this close traffic view.
The sampled 4DR_SEDAN02, PICKUP, HATCHBACK02, and PANELVAN shells are upright,
with model-base heights within 0.052 world units of the sampled pavement. The
route-744 body matrix places its measured up axis on world Z, so route heading
cannot roll this BODY_A mesh upside down. This only narrows the search: it does
not reproduce the reported inversion or backwards travel on other routes, and
the BODY_A/B/C retail detail-slot mapping remains unresolved. Capture:
[`217-r120-live-traffic-and-road-materials.png`](../webgl-city/review/images/217-r120-live-traffic-and-road-materials.png).

## Unmapped detail slots held out of normal traffic, revision r122

The decompiled retail renderer's projected-size thresholds (40/20/10) are
known, but the executable's resource-slot-to-exported `BODY_A/B/C` mapping is
not. The r121 viewer guessed that suffix order matched retail detail order and
selected B/C as cars moved farther away. That inference was not supported by
the recovered resource table; the B/C meshes can have different baked forward
and up signs, and normal traffic could therefore change orientation at a LOD
boundary. Revision r122 removes that guess: normal traffic uses the measured
`BODY_A` transform, while B/C load only with an explicit debug
`trafficBodyVariant` query. This is a conservative correction, not a recovered
retail LOD implementation. The user-reported upside-down traffic still needs
to be reproduced on the affected routes and checked against the live transform.

The road renderer still emits no generated marking geometry. The crosswalk,
lane lines, and hatching are sourced from `RDP_*` texture images with exported
UVs. Their placement is still reported wrong; identifying a source texture
does not prove the right road object, material slice, or UV transform was
recovered. That requires matching the same road location to the XP game view.

## P8 road-palette channels corrected, revision r123

**Superseded by revision r136 below.** The r123 byte-order assumption was
incorrect; it treated the PNG export as proof of the retail Direct3D palette
order.

The r122 fragment shader swapped red and blue whenever a material's TPK format
was P8 (`0x08`). This was incorrect: the palette decoder exports bytes in the
source RGBA order, and P8 palette entries are already RGBA. At the route-450
intersection, a sampled `RDP_ROADSTRIP_MC` texel `(76,131,151)` was changed by
the shader to `(151,131,76)`, which made the blue/cyan hatch yellow/ochre.
Revision r123 removes that channel swap. The source image, mesh triangles, and
UVs are unchanged; the renderer now displays the decoded retail palette RGB.

The same-location reference image is from the clean-room OpenUG2 renderer, which
loads the extracted retail STREAM data and renders at `(-675.9,-548.5)`. It
shows the blue/cyan source hatch, consistent with the unmodified palette bytes.
This is a reference renderer, not a capture from the XP game. It confirms the
P8 color discrepancy but does not establish correct road-piece placement or
the behavior of traffic on every route. The viewer's live WebGL tab remained
on its loading screen during the attempted r123 capture, so a fresh in-browser
visual confirmation is still outstanding.

Reference implementation notes: [OpenUG2 format documentation](https://github.com/whoismept/OpenUG2/blob/main/docs/FORMATS.md), [OpenUG2 source](https://github.com/whoismept/OpenUG2).

## Initial ambient-traffic refresh runs at scene start, revision r125

In the controlled headless capture, Chrome's page clock was still `0` when the
first city frame was drawn. The traffic refresher had an initial timestamp of
`0` and returned whenever `now - lastRefresh < 1800`, so it produced no traffic
at all until the clock advanced. The model roster and routes had loaded, but the
first frame was empty. The refresher now starts at negative infinity and resets
to that sentinel when BODY_A assets become ready; the first draw assigns traffic
immediately, and later draws retain the 1.8-second refresh cadence.

At route 450, distance 300, the next live capture has four visible BODY_A cars
on routes 790 and 791. Their decoded route direction dots are `-0.9988` and
`-0.9990`; the route points are reversed to follow the retail basis and both
cars report `journeyDirection=1`. Their bases are 0.0000–0.0147 world units
from the sampled pavement and the four shells look upright in this view. This
confirms only these two local route fragments and these BODY_A models. It does
not rule out a bad BODY_A transform elsewhere, explain earlier inverted cars,
or identify the retail resource table's BODY_A/B/C detail-slot mapping.

The same capture uses the r123 P8 channel correction above. The hatch now
matches the raw blue/cyan source palette. The large intersection surface and
scene composition still need a same-location XP gameplay comparison.


## Opaque road texture prelight restored, revision r127

OpenUG2's default world pass enables source vertex color and uses a 2×
modulation for opaque world materials. The WebGL RDP lane/intersection path had
been sampling the decoded RGB without that `COLOR_0` multiplier, leaving road
textures substantially brighter than the same-location reference capture.
Revision r127 applies the source vertex prelight to opaque RDP colors. The
route-450 capture is now darker, and its P8 hatch remains cyan after the r123
palette-channel correction. The current viewer still generates zero lane-mark
geometry: markings come from source `RDP_*` textures and exported UVs. Correct
road-piece placement remains unresolved. Capture: [`221-r127-road-prelight.png`](../webgl-city/review/images/221-r127-road-prelight.png).

## Traffic up-axis matrix invariant

A recheck of `tools/export_nfsu2_traffic_cars.py` and the exported BODY_A
metadata shows that axis signs are measured from each retail BODY object matrix
after the exporter conversion `(x,y,z) -> (y,z,-x)`. All 15 BODY_A records have
three unique cardinal semantic axes with alignment 1.000. The WebGL instance
matrix maps each signed semantic `up` axis onto world `+Z` exactly; route heading
only changes the horizontal forward and lateral columns. Therefore the current
route-following matrix cannot produce a rolled or upside-down BODY_A car by
turning at a junction. This check validates the exported transform metadata's
internal consistency, not that its source semantic up-axis is correct in the XP
game. If an upside-down shell is still visible, likely remaining faults include
an incorrect retail object-frame interpretation, a different unverified body
or attachment path, or a car mesh whose visible top disagrees with its bounds
and wheel anchors. A citywide live capture and a visual source-to-export frame
audit are still required before the reported inversions can be called fixed.


## Close live sample of the reported inverted traffic, revision r128

I moved the packaged WebGL debug camera to the currently rendered traffic at
route 450. The four bodies in the live frame are PANELVAN, SUV, HATCHBACK, and
PANELVAN, all `BODY_A` on routes 790/791. Browser telemetry reports each body
base within 0.001 world units of the sampled pavement, and the shells appear
upright in this close view. Capture: [`223-r127-live-traffic-close-up.png`](../webgl-city/review/images/223-r127-live-traffic-close-up.png).

This does not reproduce the reported upside-down/backwards traffic. It
confirms the current body-frame path on these two local routes only. The other
regions, junction transitions, and exact retail body-resource selection still
need inspection against the XP game.

## Road texture RGB preserved, revision r131

The user flagged the visible road markings as incorrect and asked that they come
from the game texture. The prior WebGL path had no generated line geometry
(`syntheticMarkings: 0`), but it still multiplied opaque `RDP_*` texture RGB by
exported vertex colors. Revision r131 removes that custom tint for source
records with `usage=2, blend=0`: their decoded source RGB is sampled at the
exported UVs, alpha is ignored as required by the material record, and no lane
paint is synthesized. This follows the recovered TPK record flags and
preserves the source marking pixels. The cyan hatch on `RDP_ROADSTRIP_MC` is
already present in the extracted texture; this does not establish that the
original game places that strip at the same location in the full road layout.

A fresh packaged Chrome capture after all 96 city textures uploaded shows the
source-textured crossing and lane slices with four nearby traffic bodies.
Telemetry reports the bodies on routes 790/791 and each body base within
0.001 world units of the road sample. They look upright in this one L4RA view;
this does not reproduce the reported inverted cars elsewhere. The actual
retail traffic mesh/attachment selection and remaining route transitions are
still open. Capture: [`227-r131-source-rgb-road-and-traffic.png`](../webgl-city/review/images/227-r131-source-rgb-road-and-traffic.png).

A second Chrome check at the extracted L4RA route 852 start follows route 853
onto the elevated ramp. The two visible traffic bodies face along the ramp and
sit upright in this capture; their decoded route bases do not reproduce the
reported inversion. Nearby route 693 cars occupy lower road levels, which is
why they are not visible in the same camera view. Capture:
[`228-r131-route852-traffic.png`](../webgl-city/review/images/228-r131-route852-traffic.png).

## Body forward axis checked against retail axle anchors

The source body-frame interpretation can be cross-checked independently against
`GLOBALB.BUN`'s four axle coordinates at definition `+0x120`. The first two
anchors are the front axle and the last two the rear axle. For each exported
`BODY_A`, I projected their baked coordinates onto its measured long axis, then
multiplied the front-minus-rear delta by `long_sign`, the sign used by the
WebGL route matrix. All 15 type-2 definitions point forward after that mapping:

| Definition | Baked long axis/sign | Front minus rear in baked axis | After route sign |
|---|---:|---:|---:|
| TAXI | 2 / -1 | -4.31 | +4.31 |
| 4DR_SEDAN02 | 2 / +1 | +3.34 | +3.34 |
| AMBULANCE | 2 / +1 | +3.29 | +3.29 |
| TAXI02 | 2 / +1 | +3.34 | +3.34 |
| PANELVAN | 2 / +1 | +5.03 | +5.03 |
| COUPE | 2 / -1 | -2.69 | +2.69 |
| FIRETRUCK | 2 / +1 | +10.05 | +10.05 |
| PARCELVAN | 2 / +1 | +3.06 | +3.06 |
| PICKUP | 2 / +1 | +4.19 | +4.19 |
| 4DR_SEDAN | 2 / -1 | -4.31 | +4.31 |
| BUS | 0 / +1 | +23.76 | +23.76 |
| SUV | 2 / +1 | +3.15 | +3.15 |
| MINIVAN | 2 / +1 | +3.84 | +3.84 |
| HATCHBACK | 2 / -1 | -2.64 | +2.64 |
| HATCHBACK02 | 2 / +1 | +2.51 | +2.51 |

This independently supports the viewer's forward-axis signs; it makes a simple
per-model body-axis sign error less likely as the cause of backwards cars. It
does not validate each transition heading, the runtime `_A`/`_B`/`_C` resource
selection, or the source `+Z`-up interpretation. Those remain the next traffic
orientation checks rather than changing axis signs by eye.

## Route-center versus drivable-lane check, revision r132

I temporarily removed the material-based lateral traffic shift to test the
decoded waypoint XY directly. A Chrome capture at L4RD route 106 showed four
cars following the waypoint center down `RDP_ROADSTRIP_MC`, the painted median
hatch, instead of the neighboring carriageway. The route record therefore
does not by itself give the rendered vehicle center.

The viewer retains its limited median correction: when a waypoint lies on
`RDP_ROADSTRIP_MC`, it samples the original exported road triangles across the
route and moves the car to the neighboring `RDP_LANEA` span. The marking pixels
remain the source `RDP_*` textures; the viewer does not draw lane paint. This
is still a geometry/material-based approximation, not the retail algorithm.
The decompilation shows that `AIActionDriveTrafficLane` builds positions from
selector-specific `FuturePoint` candidates using waypoint basis, signed
corridor adjustments, and runtime candidate weights. Recovering the active
candidate and its two weights is required to replace this approximation with
the exact retail lateral position.

## Player spawn moved off the median, revision r134

The same route 106 check showed that its initial player position was still
only 1.5 m from the route center and straddled the median. When no explicit
`startLateral` URL override is supplied, `placePlayerOnRoute` now applies the
same source-lane correction when the route center intersects
`RDP_ROADSTRIP_MC`. In the fresh packaged Chrome capture, both the player and
four nearby traffic bodies sit on the adjacent `RDP_LANEA` pavement after all
112 city textures uploaded. The player moves 4.25 m for this route sample; its
body base and the four traffic bases are within 0.05 world units of the sampled
road. An explicit `startLateral` value still bypasses the automatic correction.
This improves the drivable start on this median-centered route, but it remains
a geometry/material approximation until the retail candidate weights are
recovered. Capture: [`231-r134-l4rd-player-and-traffic-lane.png`](../webgl-city/review/images/231-r134-l4rd-player-and-traffic-lane.png).

## Traffic follows the decompiled 3D waypoint frame, revision r135

The route export already preserves the two source vectors used by the
`0x004122c0` FuturePoint resolver. Its forward vector is
`(-waypoint[+0x18]/32767, -waypoint[+0x1a]/32767,
-previous[+0x21]/128)`. Its lateral vector is
`(-waypoint[+0x1a]/32767, waypoint[+0x18]/32767,
-waypoint[+0x20]/128)`. The viewer previously reduced the first vector to an XY
heading and placed every traffic body on a flat world-up frame. Revision r135
interpolates both exported vectors, aligns them with the directed route, and
builds the vehicle up axis from their cross product. Reversed route travel
negates forward and lateral together, retaining an upright frame.

This uses the decompiled road slope and side basis for vehicle pose. It does
not reconstruct the selector-dependent `FuturePoint` displacement: runtime
candidate values at `+0x54/+0x58`, the selected phase byte, and actor-specific
offsets are still needed for exact lane placement. The current median shift
therefore remains a pavement-geometry correction. Road markings remain
sampled from the texture and material slice bound to each exported road
primitive; the viewer creates no marking geometry. The blue hatch on
`RDP_ROADSTRIP_MC` is present in the decoded source image, so its color and
placement must be resolved through source texture/material evidence rather
than a hand-painted replacement.

## P8 road palette correction, revision r136

The r135 screenshot showed a cyan center stripe. Its source material is
`RDP_LANEA_YS_AA_KT`, whose clean GLB metadata records `format=0x08` (P8),
while neighboring `RDP_LANEB_WD_AA_KT` is DXT3 (`0x24`). The TPK decoder had
copied palette bytes directly into RGBA images. Direct3D palette entries are
`D3DCOLOR` DWORDs (ARGB values), stored as BGRA bytes on this little-endian
platform. That made the yellow stripe appear cyan. The offline decoder
preserves those source bytes in the PNG; revision r136 applies a
format-specific red/blue swap at draw time using the recovered material
metadata. This keeps existing and future GLBs on the same runtime path. DXT
materials are unchanged. The stripe is still sampled through the original
road mesh UVs; there is no generated line geometry.

The experimental pixel-color lane avoidance has also been removed from player
and traffic placement. Texture color is visual material data, not a recovered
lane-controller signal. Traffic lateral placement still has the explicit
median/material correction on route 106 until the decompiled
selector-dependent `FuturePoint` offsets are recovered. A fresh route-450
Chrome capture shows the yellow stripe with no browser errors, but complete
mesh/UV placement against an XP gameplay capture remains outstanding:
[`233-r136-p8-road-marking-channel-fix.png`](../webgl-city/review/images/233-r136-p8-road-marking-channel-fix.png).


## L4RD route-106 traffic spawn correction, revision r138

A live L4RD route-106 viewer snapshot had zero traffic candidates even though
route records 106 and 116 pass through the player's start. The spawn preview
was placed 10 route units ahead, but the candidate filter rejected it unless
it was at least 12 units ahead. This disagreed with the per-frame view-corridor
check, whose lower bound is 8 units. Revision r138 uses that same 8-unit lower
bound for candidate creation.

The packaged Chrome page now reports two viable route records and renders four
nearby cars at the first update, then seven as the remaining models finish
loading. In the first snapshot, their headings are 1.91–1.93 radians against
the player's 1.93-radian heading, they follow route 106 in its decoded forward
direction, and their body bases are within 0.05 world units of sampled road
height. They use the current median correction (about 4 m) because the retail
FuturePoint lane selector is still not reconstructed. The capture is
[`237-r138-l4rd-traffic-spawn.png`](../webgl-city/review/images/237-r138-l4rd-traffic-spawn.png); it was taken during model loading and does not prove
citywide traffic orientation or retail paint/part assembly.

## BODY_A up-axis checked against extracted wheel anchors, revision r139

I compared the exported BODY_A bounds and signed up axes with the four wheel
anchors extracted from the same traffic definitions. For all 15 traffic
models, the recorded up sign points from the wheel plane into the body. The
measured body extent above that plane ranges from 1.23 m (COUPE) to 7.02 m
(BUS); the lowest bound is within 0.07 m of the wheel plane for every model.
This independently checks the exporter axis signs against authored vehicle
data. It rules out a BODY_A sign inversion in the exported metadata, but does
not explain the reported inversions on other routes or establish that the
viewer is selecting the same resource and transform as retail.

A close Chrome view of route 106 shows three cars following the same narrow
stream, all upright in this particular frame. The duplication is a visible
placement problem: route 106 and 116 share waypoint coordinates, while the
viewer still applies nearly the same material-based lane correction to both.
The retail controller's selector bank and runtime FuturePoint weights remain
necessary before these can be placed on distinct source lanes. Capture:
[`238-r139-l4rd-traffic-frame.png`](../webgl-city/review/images/238-r139-l4rd-traffic-frame.png).

## Preserve the 3D traffic frame through junction gaps, revision r141

`sampleTrafficJourney` interpolated position and yaw across short, recovered
route transitions, but the transition pose omitted the source waypoint's 3D
forward and lateral vectors. `trafficVehicleMatrix` therefore fell back to a
flat heading-only frame for the gap. The transition pose now samples both
route endpoint frames and interpolates their 3D forward/lateral axes using the
source and target travel directions. This carries the decompiled pitch and
side-slope information across the junction handoff. The browser scene loads
without errors, but the reported upside-down traffic has not yet been
reproduced at a transition.

The packaged L4RD route-106 audit reports `syntheticMarkings: 0` and
`markingsSource: authored road materials and textures`. The current close
capture ([`239-r141-l4rd-road-and-traffic-source-textures.png`](../webgl-city/review/images/239-r141-l4rd-road-and-traffic-source-textures.png))
shows the yellow median hatch and white lane lines sampled from source RDP
materials. This confirms the pixels come from extracted textures; it does not
confirm that every road piece and UV is positioned as in retail. Traffic still
uses a material-based lane shift where the waypoint falls on the median, and
that remains an approximation until the runtime FuturePoint weights are
recovered. The nearby Coupe is upright in this one view, while its body and
wheels remain visibly unlike the game.

## Named default render-state fallback (r266)

The installed `GLOBALB.BUN` contains an outer `0x00135200` resource at offset
`0x1ae8` with handle `0x2c420d64`, name `DEFAULT`, and the usual 0x60-byte
state payload. Loader `0x0048e070` compares the parsed state's handle at
`+0x0c` with `0x2c420d64` and stores its record pointer in global
`0x007f399c`. The state-map helper `0x0048f6a0` returns that global when its
requested handle is not found in the loaded state list. This pins down the
name and source bytes of the missing-handle fallback; it does not assign
semantics to the 24 payload floats or recover the paint descriptor selector.
The exporter records this separate fallback alongside the 99
`0x6ba02c05` map rows in [`traffic-paint-states.json`](traffic-paint-states.json).
