GS alpha test and blend: all four rules the item names are now present

Finishes the rule layer. Two traps, both of which produce a picture that looks
slightly wrong rather than obviously broken:

AFAIL READS BACKWARDS. The PS2's alpha-test register holds an enable, a 7-bit
reference, and a bit that looks like "fail if alpha is above the reference". On
the hardware it means the opposite -- AFAIL = 0 discards where alpha is BELOW the
reference. passes_alpha_test takes the side explicitly rather than encoding the
register, and both directions are tested so the convention is pinned in code
rather than in a comment.

THE OPAQUE BIT MUST STAY OUT OF THE COMPARISON. 0x80 is fully opaque, but it is
also numerically 128, so a test that compares the raw byte against a 0x40
reference would have every fully-opaque fragment pass every threshold, and nothing
would ever be discarded. passes_alpha_test clamps the opaque bit off first, and
there is a test that asserts exactly that -- the failure mode here is a scene that
renders correctly with the wrong shading, which is not something anyone notices
by looking.

Blend is the four GS primitive types, decoded from the primitive-type byte and
named by what they do rather than by the documentation's IIIB/IIIA/IIIC labels,
which name two different modes identically. Out-of-range types decode to Off
rather than panicking, because the byte comes from asset data. Addition clamps at
255 rather than rescaling, matching the GS's 7-bit clamp; a caller wanting a true
average blends before widening, and the doc says so.

That completes the four rules the roadmap names for this item:
  alpha 0x80 == 1.0      alpha_to_8
  CLAMP wrap             clamp01
  mip selection          mip_level
  blend + alpha test     BlendMode, passes_alpha_test

Nine unit tests, no wgpu work, and the roadmap item stays [~] rather than [x] --
the rules are the input to the renderer, not the renderer.

Worth noting the pattern in both traps: each one produces output that is plausible
and slightly-off, with no error anywhere. That is the same shape as the sign
inversion in mip_level and the byte-order bug in the anchor search. The tests are
the only thing that catches this class, which is the argument for having them even
though nothing here needs a GPU.
This commit is contained in:
2026-10-14 04:13:19 +02:00
parent 81ec17857a
commit 31f4ba3f7c
2 changed files with 133 additions and 4 deletions
+9 -4
View File
@@ -711,10 +711,15 @@ lookup, and it is the main thing inherited from ReRAC.
`0x80` is opaque, and `0x80 & 0x7F == 0` is exactly why it reads as half to
an 8-bit reader), `clamp01` (CLAMP wrap, with `1.0` kept at the far edge),
and `mip_level` -- `round(log2(image / on_screen))` clamped to the chain,
where a *smaller* on-screen size selects a *higher* level. **Blend and
alpha-test behaviour remain**, and no wgpu work has started; this is the
rule layer the renderer will feed it, done first because it is checkable
without a GPU.
where a *smaller* on-screen size selects a *higher* level. **All four rules
are now there**, plus `passes_alpha_test` and `BlendMode` for the
alpha-test and blend halves of the item. Two traps worth naming:
the GS's `AFAIL` bit reads as "fail if above the reference" and on the
hardware means the opposite -- it discards where alpha is **below**; and the
opaque bit must be kept out of the comparison, or `0x80` satisfies every
threshold and nothing is ever discarded. **No wgpu work has started** --
this is the rule layer the renderer will feed it, done first because it is
checkable without a GPU.
- [ ] VU0/VU1 skinning and lighting on the PS2 float model
- [ ] LOD/morph/cull per `TfragProc` and `TieProc`
- [ ] Occlusion culling, fog, shadows