# Ch355 audit log — MULTI-TEXTURE composition (different-texture draws accumulate into one LPDDR framebuffer)

Codex rung after Ch354. Brick 1 (narrow): TWO authentic SH3 draws with DIFFERENT TEX0/CLUT composited into one FB via
SCENE-LEVEL texture rebind + staged-list retriggering. Dump order (authentic): A=idx19562 (tbp=11264/cbp=14080) THEN
B=idx89761 (tbp=9216/cbp=13952). NOT Brick 1: per-primitive TEX0, multi-resident cache, 640x480, cross-draw Z.

## Preflight (gs_make_sh3_multitex_fixture.py) — fail-CLOSED, PASS
- authentic dump order (A idx < B idx); same frame f1; DIFFERENT texture keys (11264 vs 9216).
- on-screen overlap: A[203..340]x[10..282] ∩ B[245..490]x[57..390] = [245..340]x[57..282].
- each texture+CLUT INDEPENDENTLY RESIDENT at its draw-time epoch (per-draw local-mem reconstruction); the two
  textures differ (real multi-texture, not aliasing); the two CLUTs differ.
- CLUTs RELOCATED to distinct, non-overlapping, in-BRAM CBPs: A->CBP=480 (0x1E000), B->CBP=484 (0x1E400).

## Emit — union geometry (Codex's numbers) + two of everything + independent A->B reference
- union origin (202,9), content 289x381 -> stored FB 320x381 (FBW=5), stride 1280 B, size 0x77100, scanout 15240
  beats/frame; HDMI shows all 320 cols (31 right cols precleared-black beyond the 289px content).
- two feeder lists (dump order): A (TEX0 CBP_A) 68 tris/619 words; B (TEX0 CBP_B) 68 tris/619 words; max staging addr
  618 < 12-bit 4096.
- two textures de-swizzled -> LPDDR + per-texture CRC: A sum32=0x95a83cbc, B sum32=0xfbdeaa32 (=Ch354's tbp=9216).
- two relocated CLUTs; bootlet uploads BOTH (A->CBP_A, B->CBP_B); each list's scene-level TEX0 (CLD=1) selects its CBP.
- independent A->B composed reference (dump order, DECAL paint-order, B over A): 27455 covered, 9849 A&B overlap px.

## Integration sim (tb_top_psmct32_sh3_multitex) — PASS on ALL Codex gates
- TWO cache fills, each a FRESH fill_done low->high (rearm), correct beats(8192)/bytes(262144)/0 rd_errs, texture-
  specific CRC: A=0x95a83cbc THEN B=0xfbdeaa32.
- NO stale-frame_drained race: scene A = fresh low->high; scene B = high->low->high (an observed FALL proves B's beats
  flowed and cleared A's stale high before the fresh rise). Enforced by a fd_before-aware run_scene task.
- preclear EXACTLY once; scanout (video_src) disabled until the SECOND fresh drain.
- records_emitted = 68 per list; writer ovf/BRESP = 0; scanout 15240 beats/frame EXACT; underflow=0.
- final FB scored vs the independent A->B reference: OVERLAP(A&B) COMPOSITE = 96.7% <=1-texel (gated >=95%) — proves B
  (tex B + relocated CLUT B) correctly overwrote A. clut_bad=0 (every covered px in palette A or B).

## Two SEPARATE claims (as Ch354)
- CLAIM 1 multi-texture accumulation/composite: PROVEN — overlap composite 96.7% >=95%, clut_bad=0, cache rearmed with
  correct per-texture CRCs, anti-stale drains, exact scanout beats.
- CLAIM 2 fidelity: ALL=69.3% (REPORTED, not gated) — dragged by the A-only region = draw 19562's OWN per-draw
  perspective-sampling fidelity (the parked limit; B/89761 renders fine, hence the 96.7% B-dominated overlap). Whether
  to independently verify 19562-alone is an open question for Codex.

## Remaining before board fit (Codex: no fit until runtime staging also passes)
- Host two-group RUNTIME-STAGING flow: preclear once; upload+fill+CRC tex A; STREAM list A (report words/records, 12-bit
  bound); GO A + observe fresh drain; REFILL+CRC tex B; STREAM list B; GO B + observe high->low->high; video_src after
  the 2nd drain. Then board fit (owner-gated).

## Codex round 2 — NEGATIVE-Q root cause + exact canonicalization (RESOLVED)
The combined ALL=69.3% was masking a real defect, NOT the parked perspective limit. Diagnostic (ONLY=A/B/AB +
FBDUMP + compose_mt.py):
- A→B RTL composition == combined AB, 0/121920 px BIT-EXACT (accumulation/order correct).
- CLUT relocation CORRECT: A rendered 100% in palette A, B 100% in palette B (palA ∩ palB = ∅).
- ONLY=B (89761, positive Q) = 95.6%; ONLY=A (19562) = 27.4% with a CORRECT CLUT.
- ROOT CAUSE: idx19562 has NEGATIVE q/s/t (q[-0.00039..-0.00024]); 89761 positive. Codex confirmed the RTL
  perspective reciprocal input/products are UNSIGNED -> negative Q is unsupported (not merely imprecise).

FIX (Codex) — exact host-side homogeneous-coordinate canonicalization in the fixture tool, BEFORE fixed-point pack:
per triangle, all-Q-positive unchanged; all-Q-negative -> negate every vertex's S,T,Q (EXACT: (-s)/(-q)=s/q, and the
negation of a linear attribute interpolates identically); mixed-sign/zero Q -> FAIL CLOSED. Overflow-checked.
- SYNTHETIC GATE: a positive-Q triangle and its all-negated twin pack BIT-IDENTICALLY (self-test PASS).
- 19562-alone re-rendered vs the UNCHANGED reference: 27.4% -> 93.0%, clut_bad=0 (oracle fixed to not count
  RTL-unwritten coverage-edge pixels as clut_bad).
- A→B composition re-run: still 0/121920 BIT-EXACT.
- Default AB acceptance: ALL 69.3% -> 95.1%, OVERLAP composite 96.4% (>=95%), clut_bad=0, 15240 beats, PASS.

RECORDED: the feeder CANONICALIZES homogeneous coordinates to positive Q. This is NOT native signed-Q hardware
support; a true signed reciprocal path remains a later platform extension for mixed-sign triangles.

## Ch355 Brick 1 status: integration PROVEN. Remaining before fit = host two-group runtime-staging flow.

## Board prep (Ch355 multi-texture) — verified in sim, owner-gated for the fit
- Board top: `GS_SH3_LPDDR_FB_MULTITEX` overrides SCANOUT_LB to STRIDE=1280/ROW_BEATS=40/NROWS=381 (320x381, FBW=5) and
  FEEDER_STG_WORDS->2048; everything else shared with GS_SH3_LPDDR_FB. Elaborates clean (exit 0) under
  GS_SH3_LPDDR_FB + GS_SH3_LPDDR_FB_MULTITEX + GS_LPDDR4B_FB + GS_LPDDR_TEX.
- FB 320x381, stride 1280, size 0x77100, scanout 15240 beats/frame.
- Bootlet (bios/payload_sh3_mt) uploads BOTH relocated CLUTs (CBP_A=480, CBP_B=484). RUNTIME staging -> NO
  FEEDER_STG_INIT_FILE (feeder starts empty; the host streams each per-scene list).
- Host `tools/ps2_sh3_multitex.c` (the two-group runtime flow): wait feeder ready -> FB base 0 -> preclear ONCE
  (121920 words, 0..0x77100) -> fill+CRC tex A (fresh fill_done, crc 0x95a83cbc, 8192 beats/262144 bytes/0 rd_errs) ->
  STREAM list A (reset 0x0D8, per-word LO 0x0DC/HI 0x0E4; report words/records + 12-bit bound) -> arm writer, GO A,
  fresh drain (0x02C[6]) -> REFILL+CRC tex B (0xfbdeaa32) -> STREAM list B -> GO B, frame_drained HIGH->LOW->HIGH ->
  video_src after the 2nd fresh drain. Compiles -Wall clean; dry-run parses texA/texB CRCs + 68 tris/619 words each.
- Build profile `sh3_lpddr_fb_multitex` (select_de25_profile.sh + QSF): SCENE GS_SH3_LPDDR_FB + GS_SH3_LPDDR_FB_MULTITEX
  + GS_LPDDR_TEX (no bitstream feeder init); FIXTURE bios/payload_sh3_mt. Ran profile-select + verified the QSF.
- BOARD RUN: `sudo ./ps2_sh3_multitex sh3_mtA_tex_lpddr.mem sh3_mtB_tex_lpddr.mem feeder_sh3_mtA.mem feeder_sh3_mtB.mem`.

## Codex round 3 — bridge-streaming sim test (closes the gap that hid the board bug)
BOARD RUN #1 rendered 0 beats: the host wrote 0x0D8=1 (SET staging addr) instead of 0, so word0/header landed at
slot 1 -> feeder read ntris=0. Root cause is RTL: ps2_hps_bridge.sv:1171 `feeder_addr_q <= wdata_lane` (0x0D8 W SETS
the address to the written value). Host fixed to write 0 (matches ps2_feeder.c); BOARD RUN #2 = full success.
The gap: BOTH the multi-texture TB and tb_top_psmct32_feeder_bridge_demo drove feeder_stg via $readmemh / direct
waddr — neither exercised the real 0x0D8/0x0DC/0x0E4 protocol.

Closed in tb_ps2_hps_bridge (joins the full regression): a model_stg captures the bridge's commit strobe
(feeder_stg_we_tgl_o + waddr/wdata), and the test stages TWO complete lists EXCLUSIVELY via bridge register writes:
- Scene A (10 words): 0x0D8=0, per-word 0x0DC(lo)/0x0E4(hi) -> every word checked at its slot, ESPECIALLY word0;
  final 0x0DC readback == nwords.
- NEGATIVE test: 0x0D8=1 -> word0 mis-slots to slot 1 (reproduces the exact board failure; pins the addr-set semantics).
- Scene B (6 words, different): re-stage after 0x0D8=0 (A->B, no $readmemh) -> every word + word0 + final addr.
- records_emitted readback (0x0E4 R) == 68.
tb_ps2_hps_bridge PASS. This exercises the exact host<->bridge address path that failed on silicon; the render/drain
per scene is silicon-verified (BOARD RUN #2) + covered by tb_top_psmct32_sh3_multitex.

## Ch355 Brick 1: BANKED — multi-texture composition on silicon + the runtime-staging gap closed.
