# Programmable car shader recovery, r267

## Result and evidence

The `car` technique in the installed PC executable's `IDI_CAR_FX` is now
disassembled, including its constant tables. This recovers the paint/glass
calculation, but does **not** supply a faithful set of inputs for the WebGL
player car. No viewer material changed. No tests, VM changes or server changes
were made during this audit.

[`car-effect-states-r267.json`](car-effect-states-r267.json) identifies the
executable/resource hashes, effect offsets, techniques and pass states.
[`car-programmable-shaders-r267.asm`](car-programmable-shaders-r267.asm) contains
the four decoded programs; its instruction offsets are relative to each shader
blob. [`car-programmable-shaders-r267.json`](car-programmable-shaders-r267.json)
also retains instruction tokens, individual shader hashes, constant-table
metadata and effect defaults. These programs are shader model 1.1, not a PBR
roughness/clearcoat model. The fixed-function path remains documented in
[`car-material-stages-r267.md`](car-material-stages-r267.md).

## Pass 0: register and texture correspondence

| Stage | Registers | Named effect input |
| --- | --- | --- |
| Vertex | `c0`–`c9` | `HarmonicCoeff`, ten float4 values |
| Vertex | `c10`–`c13` | `WorldViewProj` |
| Vertex | `c14`–`c16` | `WorldView`, three rows used |
| Vertex | `c17` | `LocalEyePos` |
| Vertex | `c18`, `c19` | `DiffuseMin`, `DiffuseRange` |
| Vertex | `c20`, `c21` | `SpecularMin`, `SpecularRange` |
| Vertex | `c22`, `c23` | `SpecularPower`, `EnvmapPower` |
| Pixel | `c0`, `c1` | `EnvmapMin`, `EnvmapRange` |
| Pixel | sampler/stage 0 | `envmap_sampler`, cube sampler bound to `EnvMapTex` |
| Pixel | sampler/stage 1 | `samp`, bound to `diffusemap` |

The constant tables prove the register mapping. Effect object assignments prove
the two sampler-to-texture names; they do not identify the live texture objects.
Vertex inputs are position `v0`, normal `v1`, UV `v2`. There is no vertex-color
input to this program. The shaders do not consume D3DRS_TEXTUREFACTOR or the
fixed-function SetMaterial fields.

Let `P` be local position, `N` the supplied normal, and `h0`…`h9` the float4
harmonic coefficients. The shader does not renormalize `N`. Its polynomial is:

```
H(X) = h0 + h1*x*x + h2*y*y + h3*z*z
          + h4*x*y + h5*x*z + h6*y*z + h7*x + h8*y + h9*z
E = normalize(LocalEyePos.xyz - P.xyz)
q = max(dot(E, N), 0.00999999978)
R = 2*q*N - E
```

`q` has a lower bound, not an explicit upper bound. The reflected vector uses
that bounded `q`; replacing it with an unrestricted reflection changes the
back-facing case. The raw vertex color outputs are:

```
d.rgb = 0.5 * H(N).rgb * (DiffuseMin.rgb + q*DiffuseRange.rgb)
d.a   = 0.5 * (DiffuseMin.a + q*DiffuseRange.a)
s.rgb = 0.5 * H(R).rgb * pow(saturate(H(R).a), SpecularPower)
                      * (SpecularMin.rgb + pow(q, SpecularPower)*SpecularRange.rgb)
s.a   = pow(q, EnvmapPower)
```

The environment lookup coordinate is `WorldView[0..2].xyz * R`, evaluated as
three dot products; the base UV is copied from `v2`. Lighting and powers are
calculated **per vertex**, then interpolated. D3D clamps the color inputs to
the pixel shader to `[0,1]`; denote the resulting interpolated colors by `D`
and `S`. A port must retain the color-input range behavior, rather than merely
evaluate all these expressions per pixel. See Microsoft's
[pixel shader 1.x register rules](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx9-graphics-reference-asm-ps-registers-ps-1-x).

Let `T` be the base texture sample and `C` the cube texture sample. The pixel
instructions (`tex`, `mad`, `mul_x2`, `add`, co-issued alpha `mov`) give:

```
B = 2*T*D                                      // RGBA
J = 2*C.rgb*(EnvmapMin.rgb + S.a*EnvmapRange.rgb) + S.rgb
out.rgb = B.rgb + 2*B.a*J
out.a = B.a
```

This is the instruction-level algebra, before final output range/precision
behavior. Shader model 1.x register precision and edge cases such as `log(0)`
must also be respected by a faithful port. It is not equivalent to selecting a
Three.js metalness or clearcoat weight.

For WINDSHIELD, the existing helper's `s50=0.3`, `s54=1` becomes
`DiffuseMin.a=0.3`, `DiffuseRange.a=0.7`. Before color clamping/interpolation,
the resulting alpha is base-texture alpha times `(0.3 + 0.7*q)`; the exact
pixel calculation is `2*T.a*D.a`. Thus the programmable glass has a
view-dependent alpha input, not a constant opacity of 0.3. Whether it blends
also depends on the live texture/layer descriptor and BlendState.

## Material record and blend inputs

Use the `D0/D1`, `S0/S1`, `E0/E1` products defined in the material-stage note.
Subject to the already documented flag/global branches that can zero terms,
the programmable branch at `0x5bd989`–`0x5bda96` uploads:

| Effect input | Value | Upload call |
| --- | --- | --- |
| DiffuseMin | `(D0, s50)` | `0x5bd99e` |
| DiffuseRange | `(D1-D0, s54-s50)` | `0x5bd9bf` |
| SpecularMin | `(S0, 0)` | `0x5bd9ea` |
| SpecularRange | `(S1-S0, 0)` | `0x5bda0b` |
| EnvmapMin | `(E0, 0)` | `0x5bda2c` |
| EnvmapRange | `(E1-E0, 0)` | `0x5bda4d` |
| SpecularPower | `1.10000002384 * s58` | `0x5bda76` |
| EnvmapPower | `s7c` | `0x5bda96` |

The power multiplier comes from the executable float at `0x800b94`. It is
different from the fixed branch's `4*s58`; those paths cannot be interchanged.

BlendState is not a shader color constant. The five effect-state assignments
reference array elements in this order: alpha-test enable, alpha reference,
alpha-blend enable, source blend, destination blend. The one-operation FXLC
records select successive four-byte offsets 0, 4, 8, 12, 16. Helpers
`0x5bcb70`/`0x5bcbc0` populate this array at `0x86ead8`, and the upload is at
`0x5bcda6`. Its live source remains texture/layer bytes `+0x55` and `+0x56`,
not the named material row.

## Pass 1: framebuffer alpha is a separate output

Pass 0 enables RGB writes only (`COLORWRITEENABLE=7`), with Z comparison
LESSEQUAL (`ZFUNC=4`). Its calculated alpha can still control framebuffer
blending even though that channel is not written.

Pass 1 disables alpha testing and blending, enables only alpha writes
(`COLORWRITEENABLE=8`), and uses depth EQUAL (`ZFUNC=3`). Its vertex shader
computes `saturate(dot(P, WorldView[2]) * FocalRange)`. Its pixel shader
replicates the interpolated value to the output. This pass writes a
depth/focal-range signal to framebuffer alpha; it does not write glass
opacity. The downstream use of that framebuffer channel is outside this
shader trace.

## Why this does not yet justify a viewer change

The effect defaults for all six minimum/range float4 values, both powers,
all **40 HarmonicCoeff floats**, FocalRange and all five BlendState integers
are zero. These are placeholders. With zero harmonic coefficients the
diffuse and specular RGB terms vanish; the zero environment defaults cannot
restore retail lighting. Material rows alone do not contain the missing
harmonic coefficients.

In the programmable lighting branch, `0x5bd445`–`0x5bd461` uploads ten
float4 values from lighting-packet offset `+0x120` to HarmonicCoeff through
SetVectorArray. This is a separate queued pointer (`queue +0x1c`) from the
named material (`queue +0x20`). The coefficient values and their upstream
generation/coordinate convention for the current city/player draw are not
recovered by the shader audit. Their producer is traced in
[`car-harmonic-producer-r267.md`](car-harmonic-producer-r267.md):
three configured directional lights, position-selected streamed lights and a
live dynamic-light list are projected into the packet, then rotated to the
draw frame. The live profile and accepted light records remain unavailable.
Using invented coefficients or a guessed tint would not be a source-backed
improvement.

The following runtime correlation is still required:

1. Confirm pipeline type 5's selected technique/index/flag. The exact selector
   and capability gates remain documented in the material-stage note; the
   static preference for `car` is not proof of a particular live selection.
2. For the same primary-player body and WINDSHIELD submissions, retain both
   queue pointers, the selected/replaced 0xa0 material row, and any flag/global
   branches affecting the uniforms. The named paint database does not prove
   the player's active row.
3. Capture the ten float4 values at lighting-packet `+0x120` when uploaded at
   `0x5bd461`, alongside the final material uniform uploads listed above.
   Trace the packet producer to recover a general city-lighting mapping,
   rather than applying one captured location's coefficients everywhere.
4. Record the base and cube texture identities/content/alpha, sampler state,
   LocalEyePos and matrix conventions, plus the five BlendState values and
   descriptor `+0x55/+0x56`. Capture FocalRange if reproducing the alpha pass.

The next useful XP capture is this single correlated body/window draw after
legitimate gameplay is available. The existing paint-row replacement capture
does not yet log the lighting packet, technique and texture/blend handoff.
No activation bypass or unavailable gameplay telemetry is assumed here.
The recovered formulas explain which lighting and alpha behavior is missing
from a generic material, but the full inputs remain insufficient to implement
a faithful player-car shader from static defaults and material rows alone.
