Implement the PS2 float model and the LightTfrags pass
Two pieces, and the first is the reason the second can be trusted. ps2.rs is the EE's FPU on raw u32 bit patterns. It is not f32: there are no denormals (exponent 0 *is* zero), no NaN and no infinity (exponent 255 is an ordinary value, so MAX reads back in Rust as NaN -- a test asserts exactly that, because it is the sharpest available statement of why this module exists), every result truncates toward zero, the adder discards the small operand's bits more than one below the large one's LSB and returns the large operand unchanged across a 25-bit exponent gap, and rsqrt is sqrt-then-divide with truncation between the steps rather than one rounded operation. The lighting pass accumulates all of these across every vertex of every patch, so this is a visible difference rather than a rounding curiosity. tfrag_light.rs is the pass itself: PEXT5 base colour, the select word picking one light set or blending two by bits 8..15 / 256, blended directions re-normalised where unblended ones are not, and base + A.rgb*max(dA, dA*wA) + B.rgb*max(dB, dB*wB). Lighting indexes positions, not vertex-info entries, since a common entry and a LOD entry can share a position. TfragLight gains a `select` field for the u16 at +6. It was briefly reconstructed with an unsafe pointer cast over the struct, which is exactly the sort of thing that breaks silently the moment a field is reordered; it is now a plainly parsed field. The pass does not run yet. The light bank framing the reference uses -- a u32 offset at +0x04, an i32 count there -- gives offset 0xa0d and a count of 698810368 on RAC2 level 0, so RAC2's gameplay extent is not laid out that way. The renderer now counts the vertices it actually lit and prints it, because falling back silently to the stored colours produces a plausible but wrong picture: lighting: 0 of 80274 vertices shaded by LightTfrags; the rest fall back to the tfrag's stored colours (no usable light bank) That line is the current honest state, and the frame has not changed as a result. Where RAC2 keeps its light bank is the open question.
This commit is contained in:
@@ -528,14 +528,72 @@ brightness changes did nothing at all. Faking it would also have hidden the
|
||||
gap. The colour attribute is picked up from the mesh automatically, so it is
|
||||
the *colour* data that is missing lighting, not a flag that was left off.
|
||||
|
||||
The real model is `LightTfrags`, which needs three things:
|
||||
The real model is `LightTfrags`. The PS2 float model and the pass itself are
|
||||
now written (`gc-engine::ps2`, `gc-engine::tfrag_light`); what is missing is the
|
||||
light bank. The three inputs were:
|
||||
|
||||
* the **light records** -- already parsed: azimuth and elevation as *indices*
|
||||
into the overlay's sine table, plus the base colour;
|
||||
* the **normal table** -- now decoded (`gc_formats::tfrag_light`), and normals
|
||||
are built and fed to the renderer;
|
||||
* the **light bank** from the level's `gameplay` extent -- framed and parsed,
|
||||
but the per-vertex evaluation is not written yet.
|
||||
* the **light bank** from the level's `gameplay` extent -- **not working**,
|
||||
see below.
|
||||
|
||||
### Why the frame is still dark: the light bank does not parse on RAC2
|
||||
|
||||
The reference frames the bank as "a u32 offset at +0x04, then an `i32` count
|
||||
there, then the sets". On RAC2 level 0 that offset is `0xa0d`, and the `i32`
|
||||
there is `698810368` -- not a count, and not a plausible one. `gameplay.bin`
|
||||
begins with `0x794e...`, not with a header of the shape the reference expects,
|
||||
so RAC2's gameplay extent is not laid out this way, or is compressed before
|
||||
this point.
|
||||
|
||||
The pass therefore never runs. Rather than let the renderer quietly draw the
|
||||
stored colours and look plausible, it counts what it lit and says so:
|
||||
|
||||
```text
|
||||
lighting: 0 of 80274 vertices shaded by LightTfrags; the rest fall back to the
|
||||
tfrag's stored colours (no usable light bank)
|
||||
```
|
||||
|
||||
That line is the honest state, and it is why the frame has not changed. What
|
||||
`gameplay.bin` actually is, and where RAC2 keeps its light bank, is the open
|
||||
question — `0x794e...` looks like it may be a compressed or encrypted
|
||||
container, in which case the bank is somewhere else entirely.
|
||||
|
||||
### The PS2 float model
|
||||
|
||||
`gc-engine::ps2` is the EE's FPU on raw `u32` bit patterns, and it is not
|
||||
`f32`:
|
||||
|
||||
* **no denormals** -- exponent 0 *is* zero, whatever the mantissa
|
||||
* **no NaN, no infinity** -- exponent 255 is an ordinary value, which means
|
||||
`MAX` reads back in Rust as NaN. A test asserts exactly that, because it is
|
||||
the clearest single statement of why this module exists.
|
||||
* **every result truncates toward zero**, and `1/3` comes out below `1/3`
|
||||
* **the adder aligns operands**: the smaller operand loses the bits more than
|
||||
one below the larger's LSB, and a 25-bit exponent gap returns the larger
|
||||
operand unchanged
|
||||
* **`rsqrt` is sqrt-then-divide**, with truncation between, not one rounded step
|
||||
|
||||
`mul`, `add`, `div`, `sqrt`, `itof12` and `max`/`min` all differ from `f32` in
|
||||
at least one case a lighting pass will hit. The pass accumulates them across
|
||||
every vertex of every patch, so this is a visible difference rather than a
|
||||
rounding curiosity.
|
||||
|
||||
### The lighting pass itself
|
||||
|
||||
Per vertex: `base + A.rgb * max(dA, dA*wA) + B.rgb * max(dB, dB*wB)`, where
|
||||
`base` is the record's 5:5:5:1 colour expanded by PEXT5, `dA`/`dB` are
|
||||
three-component dot products against the light directions, and `wA`/`wB` come
|
||||
out of the colour w lanes. `select` picks one light set or blends two by
|
||||
`bits 8..15 / 256`, and a blended direction is re-normalised where an unblended
|
||||
one is not.
|
||||
|
||||
One field had to be added to `TfragLight` to carry this: `select`, the `u16` at
|
||||
+6 of the record. It was briefly reconstructed with an `unsafe` pointer cast
|
||||
over the struct -- which is exactly the kind of thing that breaks silently the
|
||||
moment a field is reordered, and is now a plain parsed field.
|
||||
|
||||
### The normal table, and why it is found by searching
|
||||
|
||||
|
||||
+14
-6
@@ -42,12 +42,20 @@ early, while they are still cheap to retire.
|
||||
level 5 `0xcca30`); it is located by matching the whole 256-entry curve.
|
||||
Normals are negated as stored and get the same axis swap as positions.
|
||||
The pipeline now also writes `overlay.bin`.
|
||||
- [ ] **tfrag lighting pass.** The light bank from `gameplay` is framed and
|
||||
parsed; the per-vertex evaluation is not written yet. It is
|
||||
`base + A*max(dA, dA*wA) + B*max(dB, dB*wB)` and has to run in the PS2
|
||||
float model. Until then the renderer stays unlit by default;
|
||||
`GC_ENGINE_LIT=1` substitutes a host directional light for inspection,
|
||||
which is not the game's lighting.
|
||||
- [x] **The PS2 float model.** `gc-engine::ps2` is the EE's FPU on raw bits: no
|
||||
denormals, no NaN or infinity, truncation toward zero, the adder's
|
||||
operand alignment, and `rsqrt` as sqrt-then-divide. `MAX` reads back in
|
||||
Rust as NaN, which is the sharpest available statement of why this is
|
||||
not `f32`.
|
||||
- [x] **The `LightTfrags` pass** is written and unit-tested against the PS2
|
||||
model: PEXT5 base colour, `select` set blending, re-normalised blended
|
||||
directions, and `base + A*max(dA, dA*wA) + B*max(dB, dB*wB)`.
|
||||
- [ ] **The level light bank does not parse on RAC2.** The reference's framing
|
||||
(u32 offset at +0x04, i32 count there) yields offset `0xa0d` and a
|
||||
nonsense count on level 0. The renderer now reports how many vertices it
|
||||
actually lit, which is currently 0 of 80,274 -- so the frame falls back
|
||||
to the stored colours and says so rather than looking plausibly lit.
|
||||
Detail in `plan/level-assets.md`.
|
||||
- [ ] Then the GS register set and the guard-band clip test.
|
||||
|
||||
The disc being the exact build the reference projects pin is the single
|
||||
|
||||
Reference in New Issue
Block a user