# 0012 — Ch347: CLUT (PSMT8) textured-alpha sprites Status: planned (synthetic brick buildable now; authentic acceptance gated on a real capture) Date: 2026-06-23 ## Goal Extend the Ch344/Ch345a textured-alpha SPRITE path from PSMCT32-only to **PSMT8 indexed (CLUT) textures**: `TEX0.PSM=0x13` → fetch 8-bit index from VRAM → CLUT → ABGR texel → MODULATE → source-over alpha. This is the first "real game" GS feature beyond the homebrew corpus (which is anomalously all-PSMCT32); PS2 titles lean on palettized textures to fit VRAM, so a richer free corpus (Ch347 target: a ScummVM-freeware capture, Beneath a Steel Sky) forces CLUT. Scope is **PSMT8 only** — PSMT4 (nibble/RMW) deferred unless census forces it. ## Key finding: the CLUT machinery is ~95% already built (search-before-reimplement) The platform already has, and PROVES for textured TRI/SPRITE **DECAL** (Ch296/297/299/314): - `clut_stub.sv` — 256×32 CLUT RAM, **two** combinational read ports; one is already dedicated to the texture sampler (`tex_read_idx`→`tex_read_data`). - `clut_loader_stub.sv` — VRAM→CLUT load FSM, CLD-mode policy, PSMCT32/PSMCT16 unpack, `load_busy` guards read2. - `gs_texel_addr.sv` PSMT8 path — 1 byte/texel linear byte address; `gs_swizzle_psmt8_stub.sv` for swizzle. - `gs_texture_unit.sv` (Ch296) — byte-lane extract from the 32-bit word + CLUT lookup; output is `.tex_color`. - gs_stub already decodes TEX0 CLUT fields (CBP/CPSM/CSM/CSA/CLD) and the textured-DECAL gate already admits PSM 0x13/0x14. Critically: the Ch344 half-rate sprite datapath captures **`s1_tex_color`**, and `s1_tex_color` IS the `gs_texture_unit` output (gs_stub.sv:4352) — i.e. already CLUT-decoded for PSMT8. So the CLUT decode happens upstream of the half-rate capture. ## What actually needs doing 1. **Relax the textured-alpha SPRITE eligibility gate** (`new_tex_abe_active`, gs_stub.sv ~:5114): `(tex0_psm==6'h00)` → `(tex0_psm==6'h00 || tex0_psm==6'h13)` (PSMT8). PSMT4 (0x14) left out for v1. 2. **Validate the timing** — the one real risk. PSMT8 adds a byte-lane SELECT; under `TEX_RD_REGISTERED=1` (the board config) the selector is realigned (`SEL_DELAY`). The Ch344 half-rate capture (ta_tex_q/ta_tex_q1, the 1-deep texel delay) was tuned to PSMCT32's registered-read latency. We must prove the CLUT-decoded texel is still valid at the frozen-beat capture for PSMT8 — a COMBINATIONAL-read TB would be a FALSE GREEN (this exact trap bit Ch344). Use a **registered-read** TB. 3. **CLUT precondition**: a TEX0_1 write with CLD≠0 must fire (loading clut_stub) before the sprite draws — same precondition as the proven indexed-DECAL path; declared, asserted in the TB. ## Pre-fit synthetic TB (buildable NOW — no capture needed), proving Codex's 5 points `tb_gs_psmt8_alpha_sprite` (registered-read model, SPRITE_TEX_ALPHA=1, TEX_RD_REGISTERED=1): 1. index fetch hits the right byte (PSMT8 linear address → correct VRAM byte lane); 2. CLUT maps index → ABGR (program clut_stub via a CLD≠0 TEX0 / loader); 3. the **texel's** alpha (from the CLUT entry) drives source-over against the dest; 4. **no read2 collision** regression (texel read on primary beat, dest on frozen beat, CLUT lookup is combinational — assert no overlap, incl. vs `load_busy`); 5. the **PSMCT32** sprite path stays green (cross-check the existing tb_gs_textured_alpha_sprite + regression). Acceptance for the synthetic brick: TB passes + full regression + quartus_syn 0-err. This banks the hardware without claiming authentic content. ## Synthetic ≠ authentic — two separate labels (Codex) The datapath proof (`tb_gs_psmt8_alpha_sprite`) proves index→CLUT→ABGR→source-over works. It is NOT authentic CLUT *ingestion*. Authentic PSMT8 additionally requires the emitted TEX0's CLUT-side fields to select a CLUT that is actually loaded and resident: - **Screening (DONE, Ch346):** `gs_texture_residency.py` now decodes CBP/CPSM/CSM/CSA/CLD and, for indexed-PSM (0x13/0x14) candidates, REQUIRES a resident CLUT upload at CBP before the draw (epoch-tracked, same as the texture) — else REJECT. It also flags CLD=0 (no load trigger -> possibly-stale palette). So `residency_ok()` won't green-light a PSMT8 candidate whose palette isn't resident. - **Emission (capture-step TODO):** the feeder/translator must carry the CLUT-side TEX0 fields. Today `ps2_feeder.c`'s `tex0 TBP TBW TW TH TFX` grammar packs ONLY texture-side fields — it needs CBP/CPSM/CSM/CSA/ CLD added (and the fixture must upload the palette to CBP + a CLD!=0 TEX0 so clut_loader_stub fires). Build this around the exact Ch346-selected candidate, not speculatively. ## Board-fit guardrail (Codex guardrail 1) — RESOLVED The "missing HDMI IO_STANDARD" the synth smoke reported was a FALSE alarm: the assignments are present + correct in the QSF (with an `-entity` qualifier); the scaffold check's regexes were EOL-anchored and didn't tolerate the qualifier. Fixed 3 checks in sim/Makefile (VIRTUAL_PIN + HDMI/ADV7513 IO_STANDARD). The QSF carries the full 77-source list (incl. osd/qsys platform modules under USE_QSYS_TOP) so the owner's board fit is unaffected. NOTE: `quartus_syn_only` itself is a reduced smoke (files.f, 115 entries) that OMITS the platform modules, so it can't fully elaborate the de25 top — a pre-existing smoke-scope limitation, not a board-fit blocker. Quartus analyzed the Ch347 gs_stub change clean (the 7 elaboration errors are all unrelated platform entities). ## Authentic acceptance (gated on the capture — do NOT commit the target until it exists) 1. Capture a Beneath a Steel Sky (ScummVM-freeware) GS dump. 2. `gs_texture_residency.py` (Ch346) picks a RESIDENT, plausible PSMT8 candidate WITH a resident CLUT — **prefer a no-wrap footprint** so we don't repeat the Ch345b wrap-mode ambiguity. 3. Extend `ps2_feeder.c`/translator with CLUT-side TEX0 fields + palette upload; emit the scene; software reference pixel-diffs; then board fit (after confirming the board profile's clut_load_busy wiring). Provenance: all dump-derived content stays LOCAL/gitignored, same discipline as the cube/sprite fixtures.