gpu: textured polygons update draw-mode state from their texpage word

Real GPU copies the texpage attribute word carried inside every textured
polygon (GP0 0x24-0x3F, texture bit set) into the draw-mode state
(GPUSTAT bits 0-8), exactly like GP0(E1) bits 0-8 — Beetle runs
SetTPage(CB[4 + ((cc>>4)&1)] >> 16) on every such command
(beetle-psx mednafen/psx/gpu.cpp:1070). So the polygon's OWN word (not
the last E1) selects its semi-transparency mode, and later rect/sprite
prims (which carry no texpage word) consume the state the polygon left
behind. We fetched texels from the prim's page but blended with the
stale E1 global, and never updated the page state rects depend on.

Effect: additive glows blended with whatever mode the last E1 happened
to set, flipping bright light halos into dark/black blobs intermittently
(OT bucket order decides which prim precedes, hence camera/distance
dependence). Proven on Ape Escape attract frame 60052: nine op-0x2E
firework quads at the artifact coordinates carry prim mode 1 (B+F) while
the last E1 set mode 0 (B/2+F/2) — rendered as dark maroon blobs;
Beetle renders the same beat as bright fireworks. Post-fix sweep shows
siren-hat glow halos, MONKEY PARK marquee bloom, and lava glow restored
across two full attract cycles.

Poly words do not touch dither/draw-to-display (E1-only bits 9-10) nor
texture-disable (bit 11 latches only under GP1(09h) allow-change).
All backends inherit the fix via gr_set_semi_transparency /
current_texpage(). Faithful-core fix for all titles; documented as D12
in accuracy/axis5_gpu.md (the audit had marked semi-transparency
FAITHFUL — the modes were, their state source was not).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-07-05 12:44:29 -07:00
parent 9b7e806656
commit 85876ec7cb
2 changed files with 45 additions and 4 deletions
+19
View File
@@ -216,6 +216,25 @@ vs `beetle-psx/mednafen/psx/<file>:line` (oracle).
and can scribble across VRAM where hardware would have dropped it).
- **Effect**: rare; mainly a robustness/timing divergence. Low priority.
### D12 — Textured-polygon texpage attribute did not update GPU draw-mode state — FIXED 2026-07-05
- **Oracle**: every textured polygon's texpage word is copied into the GPU
draw-mode state before drawing — `SetTPage(CB[4 + ((cc>>4)&1)] >> 16)` runs
for all GP0 0x20-0x3F with the texture bit (gpu.cpp:1070-1072). The poly's
own word (not the last E1) selects its semi-transparency mode, and later
rect/sprite prims (no texpage word) consume the state the poly left behind.
Poly words cannot alter dither/draw-to-display (E1-only bits 9-10) nor
texture-disable (bit 11 latches only under GP1(09h) TexDisableAllowChange).
- **Ours (pre-fix)**: textured polys fetched texels from their own tpage but
blended with the stale E1-set global `semi_transparency`, and never updated
`texpage_x/y/colors` for subsequent rects.
- **Effect**: additive glows (mode 1 in the prim word) blended with whatever
mode the last E1 set — Ape Escape's headlamp halos / siren flares / firework
bursts rendered as dark blobs or black starbursts, intermittently (depends
on OT bucket order, hence camera/distance). Proven on Ape attract frame
60052: nine op-0x2E quads at the artifact coords carried prim mode 1 vs E1
mode 0. Fixed in `gpu.c set_tpage_from_poly()`, called by all four textured
poly handlers; all backends inherit via `gr_set_semi_transparency`.
### Items that are FAITHFUL (no action)
- Semi-transparency 4-mode selection + STP-bit gating (SW and GL).
- Mask set/check including in copies (SW and GL).
+26 -4
View File
@@ -1680,6 +1680,21 @@ static uint16_t current_texpage(void) {
(semi_transparency << 5) | (texpage_colors << 7));
}
/* Hardware: the texpage attribute word carried inside every textured polygon
* (GP0 0x24-0x3F with the texture bit) is copied into the GPU draw-mode state
* (GPUSTAT bits 0-8) exactly like GP0(E1) bits 0-8 — the poly's own word, not
* the last E1, decides its semi-transparency mode, and later rectangle/sprite
* prims (which carry no texpage word) consume the state the poly left behind.
* Beetle: SetTPage(CB[4 + ((cc>>4)&1)] >> 16) on every textured poly.
* Bits 9-10 (dither / draw-to-display) exist only in E1; bit 11 (texture
* disable) only latches when GP1(09h) allowed it, which polys can't grant. */
static void set_tpage_from_poly(uint16_t tpage_word) {
texpage_x = tpage_word & 0xF;
texpage_y = (tpage_word >> 4) & 1;
semi_transparency = (tpage_word >> 5) & 3;
texpage_colors = (tpage_word >> 7) & 3;
}
/* Helper: set up SW renderer state before a textured draw.
* semi_trans: whether the primitive has semi-transparency enabled (from opcode bit)
* raw_texture: 1 if this is a raw-texture opcode (bit 0 of GP0 opcode set) */
@@ -1708,9 +1723,10 @@ static void gp0_exec_textured_tri(void) {
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint16_t clut_x = (clut & 0x3F) * 16;
uint16_t clut_y = (clut >> 6) & 0x1FF;
/* Texpage from word 4 bits 16-31, or use current state */
/* Texpage from word 4 bits 16-31 */
uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[4] >> 16);
uint16_t tpage = tpage_word & 0x1FF;
set_tpage_from_poly(tpage_word);
for (int i = 0; i < 3; i++) {
vx[i] += draw_offset_x;
@@ -1743,7 +1759,9 @@ static void gp0_exec_textured_quad(void) {
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint16_t clut_x = (clut & 0x3F) * 16;
uint16_t clut_y = (clut >> 6) & 0x1FF;
uint16_t tpage = (uint16_t)(gp0_cmd_buf[4] >> 16) & 0x1FF;
uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[4] >> 16);
uint16_t tpage = tpage_word & 0x1FF;
set_tpage_from_poly(tpage_word);
/* Widescreen: tagged billboard quads carry CPU-computed pixel offsets the
* GTE squash never saw — re-squash every X around the prim's anchor. */
@@ -1815,7 +1833,9 @@ static void gp0_exec_shaded_textured_tri(void) {
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint16_t clut_x = (clut & 0x3F) * 16;
uint16_t clut_y = (clut >> 6) & 0x1FF;
uint16_t tpage = (uint16_t)(gp0_cmd_buf[5] >> 16) & 0x1FF;
uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[5] >> 16);
uint16_t tpage = tpage_word & 0x1FF;
set_tpage_from_poly(tpage_word);
for (int i = 0; i < 3; i++) {
vx[i] += draw_offset_x;
@@ -1852,7 +1872,9 @@ static void gp0_exec_shaded_textured_quad(void) {
uint16_t clut = (uint16_t)(gp0_cmd_buf[2] >> 16);
uint16_t clut_x = (clut & 0x3F) * 16;
uint16_t clut_y = (clut >> 6) & 0x1FF;
uint16_t tpage = (uint16_t)(gp0_cmd_buf[5] >> 16) & 0x1FF;
uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[5] >> 16);
uint16_t tpage = tpage_word & 0x1FF;
set_tpage_from_poly(tpage_word);
for (int i = 0; i < 4; i++) {
vx[i] += draw_offset_x;