Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)

RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression
(272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps,
and all dump-derived textures/traces) is excluded via .gitignore and stays local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 20:10:50 -04:00
commit ec82764bef
2462 changed files with 2174303 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
# Decision 0000: Trace Format
Status: `Locked`
## Context
Trace format is a required Phase 0 decision because every subsystem contract
already depends on debug visibility, and those traces are much more useful if
they share a known structure.
The project needs one format that is:
- simple enough to emit from early RTL stubs,
- simple enough to generate from emulator-side instrumentation,
- human-readable during bring-up,
- structured enough for automated diffs later.
## Options considered
1. One shared tabular text format for everything.
2. Subsystem-specific text formats with no shared envelope.
3. Binary trace format from day one.
4. Common text envelope plus subsystem-specific payload fields.
## Decision
Adopt a `common text envelope plus subsystem-specific payload fields` format for
Phase 0 and Phase 1.
The common envelope should include:
- cycle or monotonic timestamp,
- subsystem id,
- event type,
- schema/version id,
- payload fields encoded as self-describing key/value pairs or a stable
column layout documented per subsystem.
Golden-reference traces should be normalized into the same envelope before
comparison. The trace files under `sim/traces/` remain text during early bring-up.
Binary traces are deferred unless text traces become a demonstrated bottleneck.
## Consequences
- Early traces stay readable in code review and terminal workflows.
- RTL stubs can emit useful traces before any heavy tooling exists.
- Emulator-side tooling has a clear normalization target.
- Different subsystems may still define different payload fields, but they must
fit inside the same outer structure.
- If performance later requires binary traces, the project can add them behind
the same logical schema rather than reinventing the event model.
## Inputs to use when locking
- `docs/contracts/validation.md`
- per-subsystem "required debug visibility" sections in `docs/contracts/`
- `sim/traces/README.md`
## Follow-up
- The stub-module plan should name the initial envelope fields explicitly.
- Subsystem contracts may later gain a short "trace payload schema" section once
the first stubs are specified.
+48
View File
@@ -0,0 +1,48 @@
# Decision 0001: Project Posture
Status: `Locked`
## Context
The project needed to choose between:
- native full-system on current hardware,
- staged subset on current hardware,
- hybrid architecture,
- future-hardware target.
This decision sets the planning posture for all early contracts and milestones.
## Options considered
1. Native full-system on current hardware.
2. Staged subset on current hardware.
3. Hybrid architecture with significant host-side execution.
4. Future-hardware target.
## Decision
Adopt `staged subset on current hardware`.
This means:
- the project targets the current retroDE platform,
- the architectural path remains that of a real PS2 core,
- coverage will be incomplete for an extended period,
- early phases prioritize observable bring-up over broad software
compatibility.
The intended interpretation is:
`real PS2 architectural path, incomplete coverage`.
## Consequences
- Early milestones may validate only parts of the machine.
- Some subsystems can remain stubbed or reduced while others become real.
- The project preserves continuity with the rest of the retroDE family by
staying on current hardware.
- The project explicitly does not promise full-title compatibility on the
current platform.
- Contracts and milestones should optimize for progressive integration instead
of "all-or-nothing" completeness.
+46
View File
@@ -0,0 +1,46 @@
# Decision 0002: BIOS Policy
Status: `Locked`
## Context
The project needed a firmware strategy that balanced authenticity with
bring-up practicality.
Main approaches considered:
- real BIOS only,
- HLE-only BIOS behavior,
- real BIOS with narrowly-scoped debug stubs.
## Options considered
1. Real BIOS only.
2. HLE-only BIOS strategy.
3. Real BIOS plus narrow debug stubs.
## Decision
Adopt `real BIOS plus narrow debug stubs`.
Policy details:
- Real user-supplied BIOS images remain the primary firmware path.
- Debug stubs are allowed only where they materially shorten early bring-up.
- Stubs must be narrow, explicit, and temporary.
Every stub must be tracked in a decision record or equivalent design note with:
- owner,
- purpose,
- scope boundary,
- removal condition.
## Consequences
- The project stays anchored to real PS2 boot behavior.
- Early bring-up may proceed without waiting for every subsystem to be complete.
- There is a maintenance cost: stub behavior must not silently become the
architecture.
- The repository must never include Sony BIOS images.
- The stub-module plan must call out which stubs are in play for each phase.
+47
View File
@@ -0,0 +1,47 @@
# Decision 0003: Golden Reference Strategy
Status: `Locked`
## Context
The project needs software references for:
- early boot and subsystem trace comparison,
- broader behavior cross-checking,
- dispute resolution when implementations differ.
The main candidates were DobieStation, PCSX2, Play!, or a custom-purpose
reference.
## Options considered
1. DobieStation as sole primary reference.
2. PCSX2 as sole primary reference.
3. Multiple references with role separation.
4. Play! as primary reference.
5. Purpose-built minimal reference.
## Decision
Adopt `multiple references with role separation`.
Role split:
- DobieStation: early boot and subsystem-oriented trace comparison.
- PCSX2: breadth and behavior cross-checking.
- Sony documentation and authoritative hardware references: tiebreak when
available.
This project does not treat PCSX2 as a mere fallback; it is a peer reference
used for a different purpose.
## Consequences
- Validation infrastructure must be able to normalize traces from more than one
reference source.
- Some disagreements will require manual triage rather than simple "reference
wins" logic.
- The project gets a smaller, more coherent early trace oracle while retaining
access to broader real-world emulator behavior.
- The validation contract and stub-module plan should name which reference is
used for each milestone.
@@ -0,0 +1,46 @@
# Decision 0004: First Visible Milestone
Status: `Locked`
## Context
The project needed a first milestone that would produce fast, meaningful signal
without conflating platform-video integration with EE/BIOS bring-up.
## Options considered
1. BIOS fetch only.
2. GS-stub test pattern only.
3. Minimal homebrew EE graphics.
4. Split first milestone into two parallel proofs.
## Decision
Adopt a `split first milestone` with two parallel proofs.
### Milestone A: platform/video proof
- GS-stub test pattern through the platform video path.
Purpose:
- validate display plumbing,
- validate retroDE integration,
- isolate platform-output bugs from CPU/boot bugs.
### Milestone B: core/boot proof
- EE BIOS fetch and early trace match against DobieStation.
Purpose:
- validate memory visibility, reset vectors, and EE-side early execution,
- isolate core bring-up bugs from display-path bugs.
## Consequences
- Early work can proceed in parallel on platform-video and core-boot tracks.
- "First success" is no longer overloaded into one giant milestone.
- Minimal homebrew EE graphics is deferred to a later integration milestone.
- The stub-module plan should explicitly map which stubs and traces are needed
for Milestone A versus Milestone B.
@@ -0,0 +1,25 @@
# Decision 0005: Phase 0 Source of Truth
Status: `Locked`
## Context
`docs/phase0_checklist.md` began as an options menu. Once the Phase 0 decisions
were locked, the project needed one clear rule for which document is
authoritative.
## Decision
Decision records under `docs/decisions/` are the source of truth for locked
Phase 0 choices.
`docs/phase0_checklist.md` remains useful, but only as a progress and navigation
document. When a checklist item is locked, it should point to the corresponding
decision record.
## Consequences
- The checklist can stay concise.
- Locked decisions are not duplicated as prose in multiple places.
- Future changes to posture, BIOS policy, milestone definition, or validation
policy should update the decision records first.
+113
View File
@@ -0,0 +1,113 @@
# Decision 0006: VRAM Roadmap
Status: `In progress` — Ch251.4 near-term rescue applied, longer-term work
queued.
## Context
The Ch251 hardware demo build (`de25_nano_psmct32_raster_demo_top`) failed the
Quartus Fitter on Agilex 5 with **516 / 358 M20K** (144%). The Fitter resource
report attributed ~410 M20Ks to two replicated `vram_bram_stub` banks:
```
u_demo|u_vram|mem_rtl_0 Logical Size: 4194304 bits M20K blocks: 204.800
u_demo|u_vram|mem_rtl_1 Logical Size: 4194304 bits M20K blocks: 204.800
```
Root cause: `vram_bram_stub` exposes **1 write + 2 independent read ports**.
An M20K block has at most two physical ports total (and at most one write
port). To honour 1W + 2R, Quartus replicates the entire storage so each read
port gets its own simple-dual-port BRAM, with the write fanned to both copies.
True dual-port would not have rescued this — TDP still gives only 2 physical
ports, not 3.
The two read ports serve distinct clients:
- **read** — PCRTC scanout (every pixel)
- **read2** — PSMT4 RMW old-byte read on the rasterizer write path
The Ch251 build draws PSMCT32 sprites only. The PSMT4 RMW pipe is wired but
never fires (`is_t4_emit` stays low), so read2 is dead weight on hardware.
## Decision (Near-Term — Ch251.4)
Add a parameter `ENABLE_READ2` to `vram_bram_stub`:
- Default `1` keeps every simulation TB and every PSMT4-exercising path
byte-identical.
- Hardware top (`de25_nano_psmct32_raster_demo_top`) overrides to `0`. When
disabled, the read2 always_ff branch contains **no reference** to `mem`,
so Quartus infers a single 1W+1R simple-dual-port BRAM (~205 M20Ks at
512 KiB) instead of two replicas (~410 M20Ks).
This is a **scoped hardware-demo build profile**, not a general fix. It is
correct only as long as the hardware build is PSMCT32 (or any non-PSMT4
format). Any future hardware build that exercises PSMT4 RMW must either
re-enable read2 (and accept the M20K cost) or first land the long-term
architecture below.
## Decision (Long-Term)
Before the GS path expands beyond PSMCT32 on hardware (PSMT4 RMW, broader
format coverage, or a larger framebuffer), replace the replicated-multi-read
VRAM with one of:
1. **Arbitrated TDP VRAM scheduler** — one TDP backing memory. Port A serves
PCRTC reads with priority; port B serves the writer / RMW path. PSMT4 RMW
becomes multi-cycle and may stall raster writes. This is the most correct
long-term FPGA shape.
2. **Line-buffer scanout** — PCRTC reads short bursts into a small line
FIFO/line-buffer once per scanline, freeing the VRAM ports for writes for
the rest of the line. More complex but closer to a scalable video
architecture.
3. **Bank/tile partitioning** — split VRAM by banks so different clients
typically hit different banks. Still needs conflict handling. Useful as a
later optimization, not as the first replacement.
Eventually larger memory surfaces (≥ a few MiB of true PS2 VRAM, or the
32 MiB main RAM) will need SDRAM/HPS/DDR-backed storage with tiled BRAM
caches; the all-M20K convenience model does not scale.
## Triggers — when to revisit (Ch252)
Re-open this decision and land one of the long-term options above when
**any** of the following becomes true on a hardware build:
1. **PSMT4 RMW returns to the rasterizer write path on hardware.** Any
GS draw flow that consults `is_t4_emit` needs the second VRAM read
port live, which re-introduces the replication cost.
2. **More than one VRAM read client during scanout.** The current
profile is one read client (PCRTC). A second simultaneous read
consumer — texture cache fetch, CLUT sampler from VRAM, secondary
display window, anything that races PCRTC for read bandwidth —
recreates the 1W+nR shape that forced Quartus replication in the
first place.
3. **VRAM_BYTES grows beyond the current 512 KiB profile.** 512 KiB
already costs ~205 M20Ks per replica at Agilex 5 packing. Any
expansion (larger framebuffer, multi-format scratch space, texture
storage) at the current replicated shape exceeds the device budget.
A simulation/elaboration tripwire in `vram_bram_stub.sv` fires
(`$display` + `$fatal`) when `ENABLE_READ2 = 1` **and**
`BYTES >= 262_144` (256 KiB). 256 KiB is not magical — it is the
threshold above which replicated VRAM becomes a board-level
architectural decision rather than a casual parameter flip. The
tripwire is a loud canary in lint / sim; the **real protection is the
board-top parameter profile**.
## Consequences
- Ch251 ships on hardware with the read2-strip build profile. The
bring-up runbook documents the override so anyone reading it later sees
the explicit trade-off.
- Simulation regressions stay byte-identical (default `ENABLE_READ2 = 1`).
- Any chapter that re-enables PSMT4 on hardware **must** land an arbitrated
/ line-buffered VRAM design first. Surfacing this as a decision record
keeps it from quietly slipping when scope expands.
- The Ch251 failure was a warning shot about VRAM strategy, not a fundamental
blocker on the PS2 core. Actual 512 KiB framebuffer storage is ~205 M20Ks;
the over-budget portion was the second full copy.
@@ -0,0 +1,271 @@
# 0007 — EE Core Reality Checkpoint (Ch306)
Status: Accepted
Date: 2026-05-28
Chapter: Ch306 (strategic recon / design — no RTL)
Supersedes: nothing. Companion to 0006-vram-roadmap.md.
Authors: lead architect, with Codex co-review.
---
## 1. Executive Summary
`rtl/ee/ee_core_stub.sv` (2155 lines) is **a behavioral compatibility oracle, not a CPU.**
It is an interpreter-style multicycle FSM that has been grown chapter-by-chapter (Ch67 → Ch305) to boot `qbert.elf` ~1.49M instructions deep by adding, one blocker at a time, exactly the opcodes, syscall HLE cases, MMIO stubs, and testbench-side pokes that the next blocker demanded. It has been extraordinarily productive *as a discovery instrument*: it told us precisely which 67 instruction behaviors a real PS2 game touches during boot, which syscalls the EE kernel must service, and which MMIO regions matter. That is its value, and that value is real.
But it is now load-bearing in a way it was never designed to be. The owner and Codex have called the key question correctly: **we are about to confuse the oracle for the deliverable.** The stub mixes three layers that real hardware keeps strictly separate (CPU / BIOS-kernel / async-hardware), and several of the things that make qbert "boot" are fabrications — a `$v0=1` longjmp fib (Ch215) that *created* the BIOS treadmill we then chased for 50 chapters, an `$a0`-aware bit-17 syscall return (Ch294/0x7A) that fakes an interrupt that never fired, and a testbench poke (Ch299) that writes `1` into qbert's private global from inside the TB.
**Go / No-Go on a synthesizable R5900 subset: GO, with caveats.**
A deliberately-scoped multicycle R5900 subset (fetch/decode, 32-bit ALU, load/store, branches + delay slots, HI/LO, and the existing gpr128/MMI subset) is **straightforwardly synthesizable on Agilex 5 (DE25-Nano)**. There are no language-level blockers in the current RTL, the microarchitecture is a clean synchronous `always_ff` FSM with handshaked memory ports, and ~63 of the 67 decoded behaviors graduate essentially as-is. The path is **bounded and validatable**. The danger is not technical intractability — it is *layer confusion*: letting oracle hacks leak into the real core.
This document splits the work into two explicit, permanently-separate tracks and defines the graduation path.
- **Track A — EE Behavioral Oracle**: keep `ee_core_stub` as a *discovery-only* instrument. Its output is a living opcode/syscall/MMIO checklist. It is never the CPU.
- **Track B — Synthesizable EE Core**: a new, clean core built to the checklist Track A produces, validated against the existing ~50 focused EE TBs (re-pointed for full-width semantics).
---
## 2. The Three-Layer Separation
Real PS2 hardware keeps three things in three places: the **CPU** executes instructions; the **BIOS/kernel ROM** services syscalls and implements `longjmp`/`_ReturnFromException`; **async hardware** (INTC / DMAC / GS / VBLANK / SIF) produces the events and flags that kernel code polls. The stub collapses all three into one FSM. The table below re-classifies every feature in the inventories by where it actually belongs.
| Stub feature | Layer | Graduates to Track B CPU? | Where it really belongs |
|---|---|---|---|
| SPECIAL ALU/shift/HILO set (SLL…SRAV, ADD…SLTU, MFHI/MFLO, MULTU, DIVU) | (a) CPU-architectural | **Yes** | CPU core |
| Immediate ALU (ADDI…LUI), branches (BEQ/BNE/BLEZ/BGTZ + REGIMM BLTZ/BGEZ + BEQL/BNEL), jumps (J/JAL/JR/JALR) | (a) CPU-architectural | **Yes** | CPU core |
| Loads/stores (LB/LH/LW/LBU/LHU + multi-beat LD/LQ/SD/SQ), SB/SH/SW | (a) CPU-architectural | **Yes** | CPU core |
| MMI subset (PCPYLD/PSUBB/PNOR/PAND/PCPYUD/PCPYH) + gpr128 shadow | (a) CPU-architectural | **Yes** (if MMI in scope) | CPU core |
| COP0 MFC0/MTC0/RFE/EI, SYNC, CACHE | (a) CPU-architectural (partial) | **Yes** (needs widening) | CPU core; RFE↔ERET to reconcile |
| SYSCALL **exception-entry mechanism** (EPC / Cause.ExcCode=Sys / vector) | (a) CPU-architectural | **Yes** (the *mechanism* only) | CPU core |
| SYSCALL **$v1 case table** (0x3C EndOfHeap, 0x3D InitMainThread, 0x40, 0x64 FlushCache, 0x6B, 0x77, 0x78, 0x79, 0x13, 0x17, 0x16, 0x12) | (b) BIOS/kernel HLE | **No** | PS2 BIOS ROM, or a dedicated EE-kernel HLE companion module between CPU and memory map |
| Ch199 `_ReturnFromException(2)` RFE-on-syscall-8 shortcut | (b) BIOS/kernel HLE | **No** | BIOS kernel exception-return path (ROM). The status-stack pop is architectural; *selecting it by syscall number* is kernel behavior |
| Ch215 `jmp_buf` restore FSM (hardcoded base `0xA000B1E0`, 12-slot libc layout, forced `$v0=1`) | (b) BIOS/kernel HLE | **No** | BIOS ROM `longjmp()`. **This `$v0=1` fib is the documented source of the Ch215 treadmill (Ch269).** It is a workaround, not behavior |
| Syscall 0x7A `$a0`-aware bit-17 readiness return | (c) async-hardware stand-in | **No** | INTC/DMAC-completion/event delivery (real interrupt fires the flag). Labeled "Not architectural truth" |
| Ch299 TB-side library-ready poke (`useg_shadow_mem[0x4CA70]=1` on qbert-specific arg guard) | (c) async-hardware stand-in | **No** | Memory side effect of the RegisterLibraryEntries (0x77) kernel callback. **Most fragile, ship-blocking hack in the inventory** |
| Syscall 0x12/0x16 (Add/EnableDmacHandler) registration | (b) BIOS/kernel HLE → (c) | **No** | Kernel handler table; the *enable* arms real INTC/DMAC dispatch (unbuilt hardware) |
| Syscall default-case halt (`retired_flag_halt` → S_HALT, expose $v1/$a0-$a3) | (c) TB-only scaffolding | **No** | Diagnostic only; real CPU vectors to kernel |
| Trace port cluster (`ev_*` + `retired_*` shadows) | (c) TB-only scaffolding | **No** (strip) | Test instrumentation; no hardware counterpart |
| Per-syscall runner observers (snapshots, tuple tables, $a0 counters) | (c) TB-only scaffolding | **No** | Passive measurement; correct to live in the TB |
| BIOS reset-vector LUI/ORI/JR trampoline + ELF `$readmemh` loader | (c) TB-only scaffolding | **No** | Real BIOS boot + program loader |
**The crisp rule:** the CPU core contains *faithful instructions and the exception-entry mechanism, and nothing else.* Every syscall service moves to a BIOS/HLE companion. Every fabricated flag moves to the async-hardware layer (and until that hardware exists, it stays in the oracle/TB — never in the real core).
---
## 3. Track A — EE Behavioral Oracle
**Role: discovery only. This is `ee_core_stub` as it exists today, plus the ELF runner harness.**
Track A continues exactly as Ch67→Ch305 did: when a new game/BIOS path blocks, Track A finds out *why* and *what is missing*, cheaply, by adding the minimum stub behavior to push past the blocker. It is allowed to lie (the `$v0=1` fib, the bit-17 fake, the TB poke) because its job is to *map the territory*, not to be the territory.
**Output: a living checklist.** Track A's deliverable is not silicon — it is three growing lists:
1. **Opcode checklist** — every instruction a real workload touches, with required fidelity (see §6).
2. **Syscall checklist** — every EE kernel service number, its observed arg shape, and its required return contract.
3. **MMIO checklist** — every device region touched (DMAC global/per-channel, INTC, timers, GIF, SIF), with the access pattern.
These lists are the *specification* Track B builds to. Every entry on them is evidence-backed by a real boot trace, which is worth more than any datasheet table because it tells us what *actually matters* for the games we run.
**The one inviolable rule:** Track A output must **never be mistaken for the CPU.** Specifically:
- An oracle hack (`$v0=1`, bit-17, TB poke) is a *flag that hardware is missing*, not a feature to copy. When Track B implements the real mechanism, the corresponding oracle hack must be **backed out**, and a TB must prove the real mechanism produces the same observable result the hack faked.
- Any conclusion drawn "after the Ch215 shim fires" must be labeled "under jmp_buf fallback semantics" (per the Ch269 finding). Track A conclusions downstream of a known fib are suspect by construction.
---
## 4. Track B — Synthesizable EE Core
**A new, clean RTL core (`rtl/ee/ee_core.sv`, distinct from `ee_core_stub.sv`), built deliberately to the Track A checklist.**
### 4.1 The first synthesizable subset (concrete)
Scope the first Track B core to exactly what qbert boot proves is needed, and no more:
- **Fetch / decode / retire**: handshaked instruction fetch over the existing BIU/memory-map ports; fully combinational decode (the `is_*` assign pile is fine).
- **32-bit integer ALU**: SLL/SRL/SRA/SLLV/SRLV/SRAV, ADD/ADDU/SUB/SUBU, AND/OR/XOR/NOR, SLT/SLTU, all immediate forms (ADDI/ADDIU/SLTI/SLTIU/ANDI/ORI/LUI). **Add the Arithmetic Overflow trap** for ADD/SUB/ADDI (the stub defers it; a real core must trap, Cause.ExcCode=12).
- **HI/LO**: MFHI/MFLO/MTHI/MTLO, MULTU (infers DSP), and DIVU **as a multi-cycle iterative divider FSM** (not the combinational `/`+`%` — see §4.3).
- **Load/store**: LB/LH/LW/LBU/LHU/SB/SH/SW with AdEL/AdES alignment exceptions, plus multi-beat LD/LQ/SD/SQ via the proven `sq_beat` counter pattern.
- **Branches + delay slots**: BEQ/BNE/BLEZ/BGTZ, REGIMM BLTZ/BGEZ, branch-likely BEQL/BNEL (squash semantics), jumps J/JAL/JR/JALR. Keep the `branch_pending` latch model.
- **128-bit GPR + MMI subset**: `gpr128[0:31]` and PCPYLD/PSUBB/PNOR/PAND/PCPYUD/PCPYH. **Gate this behind a parameter** (`EE_ENABLE_MMI`) so a minimal build can fall back to a 32×32 regfile and save ~4096 FFs.
- **COP0**: MFC0/MTC0 for the 5 modeled regs + the proper **exception-entry mechanism** (EPC save, Cause.ExcCode, BEV vectoring) and **ERET** (reconciled against the stub's R3000-style RFE — R5900 uses EXL/ERL/EPC). SYNC and CACHE are faithful no-ops on a cacheless in-order core.
**Explicitly out of the first subset:** the syscall $v1 table (moves to a BIOS/HLE companion fed by the real SYSCALL exception), COP0 64-bit upper lanes beyond what MMI needs, FPU/COP1, VU0/VU1 macro-mode, and full TLB. These are later chapters or separate tracks.
### 4.2 Recommended microarchitecture: **start multicycle/interpreter-style, pipeline later**
Keep the current 8-state FSM shape (S_IFETCH_REQ → S_IFETCH_WAIT → S_EXECUTE → optional S_MEM_*; drop the two Ch215 shim states). **Reasons:**
1. **It already synthesizes cleanly.** The synthesizability assessment is unambiguous: clean synchronous `always_ff`, handshaked ports, no latches (both `unique case` blocks carry defaults), constant-bound loops. There are *no language-level blockers*.
2. **It is the correct altitude for first-silicon correctness.** A multicycle core has no hazards, no forwarding, no branch prediction — delay slots are a single `branch_pending` latch. This is the smallest correct design, and correctness-first is the only sane order when the goal is "prove a real R5900 RTL works."
3. **Pipelining is a pure-performance follow-on**, addable once the multicycle core passes the full TB suite and boots qbert. The R5900 is a dual-issue in-order pipeline; that is a *known, bounded* later effort, not a prerequisite for graduation.
4. **It matches the proven `iop_core_stub` shape**, so the platform integration patterns already exist.
Minimum ~4 cycles/instruction is acceptable for bring-up. The DE25-Nano has the headroom.
### 4.3 What must be stripped / gated for synthesis
From the synthesizability assessment, ranked:
- **STRIP_HW_DIVIDER=1 is mandatory** for any fit. The inferred combinational divider is the documented ~32 ns STA critical path (Ch162). Track B must replace it with a **multi-cycle sequential divider FSM** if DIVU semantics are needed (they are — qbert uses it).
- **Strip the trace port cluster** (`ev_valid/ev_subsys/ev_event/ev_arg0-3/ev_flags` + the `retired_*` shadow registers + the divu/multu trace arms). These are pure observability (~4×64 + 32 + several 32-bit FFs of dead weight) that force the synthesizer to keep otherwise-dead arg-computation logic. Replace with a thin, optional debug-readout port if needed.
- **Gate the gpr128 shadow** (`EE_ENABLE_MMI`). 32×128 = 4096 FFs is the dominant flop cost and Quartus will build it in ALMs (async multi-port read), not M20K. Keep only if MMI/quadword is in scope.
- **The CH215 jmp_buf FSM and the EE_SYSCALL_HLE dispatcher do not enter Track B at all.** In the stub they are param-gated OFF; in Track B they are simply absent — they move to the BIOS/HLE companion.
- `unique case`, constant-bound for-loops: **keep** (not blockers; defaults prevent latches).
---
## 5. Validation Strategy
**The existing ~50 focused `tb_ee_core_*` benches + the qbert boot path ARE Track B's compliance harness.** This is the single strongest asset we have, and it directly answers the owner's worry.
### 5.1 Why the existing suite transfers
The compliance inventory confirms **all 50 focused TBs are reusable** with only mechanical adaptation. The uniform pattern is port-driven: each TB hand-assembles a tiny program into the BIOS/bootstrap slots, lets the DUT fetch/decode/execute *through the public memory-map ports* to a PASS-syscall halt, then checks results. **Step 2 (execution) is already fully port-driven — there are no internal pokes to make the core run.** Many TBs embed an in-program BNE/BEQ-to-FAIL self-check, so the expected architectural behavior is encoded in the program itself and is checkable purely from observable halt-PC/RAM. There is a strict 1:1 opcode→TB discipline (Ch271Ch293), so **there are no implemented-but-untested opcodes.**
### 5.2 The two required adaptations (both mechanical, both bounded)
1. **Hierarchical-peek → architectural readout.** Most TBs read the *post-halt* result via `u_core.regfile[...]` (and `u_ee_ram.mem[]`/`u_bios.mem[]` for stores). Against a renamed/synthesized core these peeks break. Fix: change each test program to **store its result register to a known RAM/MMIO address** and read it back through the map port. This is a per-TB swap that does not change the encoded expected behavior. Store-class TBs (memops, sb, sh, sd, sq, lq, ld) already verify partly through `u_ee_ram.mem[]` and are closest to a real memory boundary.
2. **Stub-accurate golden values → architecture-accurate golden values.** Several TBs deliberately encode *simplified* semantics: DADDU/DSUBU/DSLL as low-32 only, and (per stale comments — actually now full-128 via gpr128) the SQ/SD/LQ/LD width expectations. Against a true 64/128-bit Track B core, the low-32 expectations would FAIL and **must be upgraded to full-width**. The TBs are reusable as scaffolding and as behavior encodings; their golden values need a width pass.
### 5.3 Known coverage gaps to close (new TBs for Track B)
- **gpr128 invariant**: add a dedicated TB asserting `gpr128[i][31:0] === regfile[i]` directly (today only transitive via PCPYUD/etc.).
- **COP0 exception state**: EPC save/restore, ERET, Cause.ExcCode encoding — no focused TB today beyond BEV and Count. This is the *most important* new TB, because the SYSCALL exception-entry mechanism is the CPU's only legitimate connection to the kernel.
- **Arithmetic Overflow trap** for ADD/SUB/ADDI (stub defers it; Track B implements it).
- **DI positive semantics** (today only a negative/still-trapping companion in tb_ee_core_ei).
### 5.4 Directly addressing the owner's worry
> *"Are we even able to verify a real R5900 RTL would work / model the hardware to finalize?"*
**Yes — and we are unusually well-positioned to, for three concrete reasons:**
1. **We have a behavioral golden model.** Track A (the stub) is, for the scoped subset, a working executable specification. Track B can be **co-simulated against Track A instruction-by-instruction**: run the same program through both, compare retire-by-retire (PC, GPR writeback, memory effects). Divergence is an immediate, localized bug report. This is the gold-standard CPU-verification methodology (lockstep against a reference model), and we already own the reference model.
2. **We have an evidence-backed requirements list.** We are not guessing what an R5900 needs — qbert's 1.49M-instruction boot trace *tells us* exactly the opcode/syscall/MMIO surface that matters. Track B's "done" is defined by a real workload, not a datasheet wishlist.
3. **We have a port-driven, near-complete compliance suite** (§5.1) that runs entirely through the public bus interface — i.e., it validates the core the same way the rest of the system will use it.
**The honest qualifier:** "verify a *real R5900*" means verify the *scoped subset we implement*, in lockstep against the oracle and the TB suite, booting the workloads we target. It does **not** mean bit-exact cycle-accuracy against Sony silicon (multiply/divide latency, dual-issue timing, cache timing are not modeled and are out of scope for first-silicon). For a "boots and runs the game correctly" goal — which is the project goal — that scope is sufficient and verifiable. For a "cycle-perfect deterministic netplay" goal it is not, and we should not pretend otherwise.
---
## 6. Master Opcode / Feature Checklist
This is the deliverable Codex asked for: every decoded behavior, its fidelity, whether it is synthesizable, and whether it graduates to the Track B CPU core.
| Mnemonic | Encoding | Fidelity | Synth | Graduates |
|---|---|---|---|---|
| SLL | SPECIAL 0x00 | faithful | yes | **Yes** |
| SRL | SPECIAL 0x02 | faithful | yes | **Yes** |
| SRA | SPECIAL 0x03 | faithful | yes | **Yes** |
| SLLV | SPECIAL 0x04 | faithful | yes | **Yes** |
| SRLV | SPECIAL 0x06 | faithful | yes | **Yes** |
| SRAV | SPECIAL 0x07 | faithful | yes | **Yes** |
| JR | SPECIAL 0x08 | faithful | yes | **Yes** |
| JALR | SPECIAL 0x09 | faithful | yes | **Yes** |
| SYSCALL | SPECIAL 0x0C | hle_or_shim | needs_work | **No** (only the exception-entry mechanism graduates; the $v1 table is kernel HLE) |
| SYNC | SPECIAL 0x0F | faithful | yes | **Yes** |
| MFHI | SPECIAL 0x10 | faithful | yes | **Yes** |
| MFLO | SPECIAL 0x12 | faithful | yes | **Yes** |
| MULTU | SPECIAL 0x19 | faithful | yes | **Yes** (infers DSP; latency not modeled) |
| DIVU | SPECIAL 0x1B | faithful | needs_work | **Yes** (needs multi-cycle iterative divider; STRIP_HW_DIVIDER for fit) |
| DSLL | SPECIAL 0x38 | low32_approx | yes | **Yes** (needs full 64-bit shifter + DSLL32) |
| ADD | SPECIAL 0x20 | faithful | yes | **Yes** (needs overflow trap, ExcCode 12) |
| ADDU | SPECIAL 0x21 | faithful | yes | **Yes** |
| DADDU | SPECIAL 0x2D | low32_approx | yes | **Yes** (needs full 64-bit adder) |
| SUB | SPECIAL 0x22 | faithful | yes | **Yes** (needs overflow trap) |
| SUBU | SPECIAL 0x23 | faithful | yes | **Yes** |
| DSUBU | SPECIAL 0x2F | low32_approx | yes | **Yes** (needs full 64-bit subtract) |
| AND | SPECIAL 0x24 | faithful | yes | **Yes** |
| OR | SPECIAL 0x25 | faithful | yes | **Yes** |
| XOR | SPECIAL 0x26 | faithful | yes | **Yes** |
| NOR | SPECIAL 0x27 | faithful | yes | **Yes** |
| SLT | SPECIAL 0x2A | faithful | yes | **Yes** |
| SLTU | SPECIAL 0x2B | faithful | yes | **Yes** |
| BLTZ | REGIMM rt=0x00 | faithful | yes | **Yes** (BLTZAL link variant not modeled) |
| BGEZ | REGIMM rt=0x01 | faithful | yes | **Yes** (BGEZAL link variant not modeled) |
| J | 0x02 | faithful | yes | **Yes** |
| JAL | 0x03 | faithful | yes | **Yes** |
| BEQ | 0x04 | faithful | yes | **Yes** |
| BNE | 0x05 | faithful | yes | **Yes** |
| BLEZ | 0x06 | faithful | yes | **Yes** |
| BGTZ | 0x07 | faithful | yes | **Yes** |
| ADDI | 0x08 | faithful | yes | **Yes** (needs overflow trap) |
| ADDIU | 0x09 | faithful | yes | **Yes** |
| SLTI | 0x0A | faithful | yes | **Yes** |
| SLTIU | 0x0B | faithful | yes | **Yes** |
| ANDI | 0x0C | faithful | yes | **Yes** |
| ORI | 0x0D | faithful | yes | **Yes** |
| LUI | 0x0F | faithful | yes | **Yes** |
| MFC0 | COP0 rs=0x00 | low32_approx | yes | **Yes** (only 5 regs modeled; Count at full clock vs half) |
| MTC0 | COP0 rs=0x04 | low32_approx | yes | **Yes** (partial Status/Cause fields; Count write dropped) |
| RFE | COP0/CO funct 0x10 | faithful | yes | **Yes** (reconcile vs R5900 ERET) |
| EI | COP0/CO 0x42000038 | low32_approx | yes | **Yes** (should set Status.EIE; companion DI still traps) |
| LB | 0x20 | faithful | yes | **Yes** |
| LH | 0x21 | faithful | yes | **Yes** |
| LW | 0x23 | faithful | yes | **Yes** |
| LBU | 0x24 | faithful | yes | **Yes** |
| LHU | 0x25 | faithful | yes | **Yes** |
| LD | 0x37 | faithful | yes | **Yes** (full 64-bit via gpr128) |
| LQ | 0x1E | faithful | yes | **Yes** (full 128-bit via gpr128) |
| SB | 0x28 | faithful | yes | **Yes** |
| SH | 0x29 | faithful | yes | **Yes** |
| SW | 0x2B | faithful | yes | **Yes** |
| SD | 0x3F | faithful | yes | **Yes** (full 64-bit via gpr128; stale "beat1=0" comments) |
| SQ | 0x1F | faithful | yes | **Yes** (full 128-bit via gpr128; stale "beats 1-3=0" comments) |
| CACHE | 0x2F | hle_or_shim | yes | **Yes** (no-op correct for cacheless model) |
| PCPYLD | MMI2 sa 0x0E | faithful | yes | **Yes** (full-128) |
| PSUBB | MMI0 sa 0x09 | faithful | yes | **Yes** (full-128, no cross-byte borrow) |
| PNOR | MMI3 sa 0x13 | faithful | yes | **Yes** (full-128) |
| PAND | MMI2 sa 0x12 | faithful | yes | **Yes** (full-128) |
| PCPYUD | MMI3 sa 0x0E | faithful | yes | **Yes** (reads upper 64; drove gpr128) |
| PCPYH | MMI3 sa 0x1B | faithful | yes | **Yes** (full-128 halfword broadcast) |
| BEQL | 0x14 | faithful | yes | **Yes** (branch-likely squash) |
| BNEL | 0x15 | faithful | yes | **Yes** (branch-likely squash) |
| NOP | 0x00000000 | faithful | yes | **Yes** |
**Tally: 63 of 67 decoded behaviors graduate to the Track B CPU core.** The 4 that do not: SYSCALL (only its exception-entry mechanism graduates; the $v1 table is kernel HLE) — and that is the only true non-graduate, since CACHE graduates as an accepted no-op. The genuinely-approximate-but-graduating ops are DADDU/DSUBU/DSLL (need full 64-bit datapath) and MFC0/MTC0/EI (need fuller COP0 coverage). **The MMI/128-bit infrastructure is the strongest, most faithful part of the stub and is genuinely synthesizable.**
---
## 7. Go / No-Go + Recommended Next Chapters
**Does a scoped R5900 subset fit Agilex 5 and pass the TBs? — YES, with the §4.3 caveats honored.**
- **Fit**: Agilex 5 has hundreds of K ALMs. The dominant cost is gpr128 (~4096 FFs) — wasteful but not fatal, and gateable. MULTU infers DSP (fine). The divider must be stripped/replaced. The trace cluster should be stripped. No structural blocker.
- **TBs**: passes in simulation against the stub as-is; a stripped/gated synthesis config (no trace, divider replaced, HLE absent) needs the §5.2 adaptations (architectural readout + full-width golden values) and the §5.3 new TBs. Hence **go_with_caveats**, not unqualified go.
### Track A — next chapters (discovery only)
- **Ch307**: Autopsy the next qbert wait loop (post-Ch294/0x7A unblock; the steady-state hot-PC, e.g. the suspected `0x00106154` region). Classify the gate (memory flag? MMIO poll? handler-fire?) the same way Ch294 did. **No RTL** — produce the checklist entry, not a hack, unless a one-shot stub is the cheapest way to see the *next* blocker.
- **Ch308 (A)**: Begin backing out fabrications into the async-hardware layer: replace the Ch299 TB poke with the real RegisterLibraryEntries (0x77) memory side effect, modeled in the HLE companion, and prove qbert still progresses. This *de-risks* Track B by validating the real mechanism in the cheap environment first.
- **Ch309 (A)**: Capture a full lockstep retire-trace export from the oracle for a fixed qbert prefix, to serve as Track B's co-sim golden reference (§5.4.1).
### Track B — next chapters (the real core)
- **Ch308 (B)**: Scaffold `rtl/ee/ee_core.sv` — a clean multicycle skeleton: fetch/decode/retire FSM + 32-bit ALU (SLL…SLTU, ADDI…LUI) + HI/LO + branches/delay slots. **Validate immediately against the existing ALU/shift/branch TBs** (tb_ee_core_shift, _varshift, _rtype_logic, _rtype_addu, _add_sub, _slt, _slti, _branch_zero, _jal, _jalr) re-pointed to architectural readout. No MMI, no MMIO, no syscalls yet.
- **Ch309 (B)**: Add load/store (LB…SW + multi-beat LD/LQ/SD/SQ) with AdEL/AdES, and the multi-cycle DIVU FSM (replacing the combinational divider). Validate against _memops, _lb/_lbu/_lh/_lhu/_sb/_sh, _ld/_lq/_sd/_sq, _align/_align_exc, _divu_mflo, _multu_mflo.
- **Ch310 (B)**: Add the COP0 exception-entry mechanism (EPC/Cause.ExcCode/BEV vectoring) + ERET, plus the gated gpr128/MMI subset. Add the **new** TBs: gpr128 invariant, COP0 exception state, overflow trap. Wire the SYSCALL exception to vector into the BIOS/HLE companion (not an internal $v1 switch). First lockstep co-sim run against the Ch309(A) golden trace.
---
## 8. Risks / Rabbit-Holes to Avoid
**Be honest about what could make this unrecoverable — and what is merely hard.**
1. **THE primary risk: conflating oracle hacks into the real core.** This is the single thing that turns a bounded project into an unrecoverable one. If the `$v0=1` fib, the bit-17 fake, or a syscall stub leaks into `ee_core.sv`, Track B becomes a second oracle wearing a CPU costume — and we will chase phantom "blockers" (the Ch264Ch268 thunk-chain hunt is the cautionary tale: 5 chapters chasing a treadmill that was *our own shim*, per Ch269). **Mitigation: the §2 rule is non-negotiable — the CPU core contains faithful instructions + exception entry, full stop. Every backed-out hack gets a TB proving the real mechanism reproduces the faked result.**
2. **GS fillrate and VU0/VU1 parallelism are separate mountains — do not let them contaminate the EE-core decision.** The EE *integer/MMI core* is tractable and is what this document scopes. The GS (rasterizer fillrate, VRAM bandwidth — see 0006-vram-roadmap) and the VUs (two SIMD vector coprocessors with their own microcode, macro/micro mode, and tight EE coupling) are each *larger* than the EE core and have their own roadmaps. **Risk: scope creep that bundles "boot qbert's CPU code" with "render qbert's graphics." Keep them separate; the EE core graduating does NOT imply the frame renders.** FPU/COP1 is a smaller but real adjacent piece, also deferred.
3. **Cycle-accuracy ambition.** If the goal silently drifts from "boots and runs correctly" to "cycle-perfect," the project becomes unbounded (multiply/divide latency, dual-issue scheduling, cache timing, bus contention). **Mitigation: §5.4 names the scope explicitly. First-silicon is behavioral correctness, not timing fidelity.**
4. **The divider critical path.** Known, measured (~32 ns, Ch162), and already gated. The only risk is *forgetting* to replace it with a sequential FSM when DIVU semantics are required. Tracked as a Ch309(B) deliverable.
5. **TB golden-value drift.** Several TBs encode stub-accurate (low-32 / truncated) golden values. If Track B is validated against *unmodified* stub TBs, a correct full-width core will FAIL spuriously, or worse, a buggy core will PASS against a too-lax expectation. **Mitigation: the §5.2 width pass is a prerequisite, not an afterthought.**
6. **Hierarchical-peek brittleness.** Not unrecoverable, but if ignored it blocks the entire compliance suite from running against the new core. Mechanical (§5.2.1) but must be budgeted.
**Bottom line: the EE core itself is tractable, bounded, and validatable.** We have a golden behavioral model, an evidence-backed requirements list, and a near-complete port-driven compliance suite — three assets most from-scratch CPU projects never have. The path is not a rabbit hole *provided* we hold the layer separation. The unrecoverable scenarios all share one root cause — letting the oracle and the CPU be the same artifact. This document exists to make sure they never are.
@@ -0,0 +1,201 @@
# 0008 — GS Tiled-VRAM Feasibility Baseline + Test #2 Spec
Status: Accepted (baseline); Test #2 = spec only, not implemented
Date: 2026-05-28
Chapter: Phase-3 hardware de-risk (LPDDR4B bandwidth spike) → GS architecture pivot
Supersedes: nothing. Companion to 0006-vram-roadmap.md and 0007-ee-core-reality-checkpoint.md.
Authors: lead architect, with Codex co-review and a parallel outside-perspective review.
---
## 1. Executive Summary
The single missing number that gated the whole "is a faithful-enough PS2 GS
physically possible on this board?" question has been **measured on real
silicon**, and it clears the first gate cleanly.
A standalone HPS-coexistent diagnostic core (`de25_lpddr4_bw`, an ao486-cloned
shell + a saturating AXI4 traffic generator on the FPGA-side LPDDR4B EMIF)
sustained, over a 256 MiB sequential stream at the EMIF user clock (310 MHz
exactly):
| phase | cycles | sustained |
|------|------|------|
| write | 9,786,835 | **8.50 GB/s** |
| read | 9,913,927 | **8.39 GB/s** |
- **~86%** of the 256-bit fabric port (≈27.4 of 32 bytes/cycle).
- **~79%** of the ~10.7 GB/s LPDDR4 PHY peak. (Both ceilings, one consistent result.)
- **Read ≈ write** at `MAX_OUTSTANDING=16` → the bus is **bandwidth-bound, not
latency-bound**. Nothing to sweep; the number is trustworthy as-is.
**Verdict on the bandwidth gate: GREEN.** Before measuring, the working
assumption was "probably impossible." 8.4 GB/s sustained changes the tone to
**"feasible *if* the GS is architected around tiling from day one."** The board
is not killed by LPDDR bandwidth. Full-4 MB-VRAM-in-M20K remains off the table
(0006); the tiled-VRAM path is no longer fantasy.
**The gate still standing is texture + locality**, not raw sequential
bandwidth. Test #1 measured framebuffer-shaped sequential traffic. It cannot
see random texture reads, CLUT indirection, or the tile-reload churn that
primitive disorder and alpha-overdraw produce. **Test #2** — a tiled-raster
microbenchmark driven by *real game traffic* — is the measurement that finally
answers "faithful-enough GS on this board: yes or no." This document specs it;
it does not implement it.
---
## 2. Test #1 — the measured baseline (authoritative)
- **Memory under test:** FPGA-side LPDDR4B, 1 GB, 32-bit, 2666 MT/s (DE25-Nano
Rev B), via the same `EMIF_Qsys` hard-IP ao486 ships. User port: **256-bit
AXI4 @ 310 MHz** (IOPLL ×62/10 off the 50 MHz reference — exact).
- **Theoretical ceilings:** ~9.92 GB/s if you count the 256-bit (32-byte) port
at 310 MHz; ~10.7 GB/s from the DRAM PHY (32-bit × 2666 MT/s). These are the
same physical limit viewed two ways. (Historical note: an earlier "78 GB/s"
was a bits-treated-as-bytes error — do not resurrect it.)
- **Method:** saturating sequential write phase then read phase over 256 MiB,
4 KiB AXI-legal bursts (128 beats × 32 B; see note), up to 16 in flight, raw
emif_clk cycle counts exposed — GB/s computed off-chip so no Fmax assumption
is baked in.
- **Conclusion:** sequential tile-stream bandwidth is **viable**; no need to
sweep outstanding-count; result internally consistent against both ceilings.
- **Caveat (explicit):** does **not** model random texture reads, CLUT, Z, or
alpha-blend / framebuffer RMW behavior. That is Test #2.
> Bring-up footnote (durable lesson): the first board run flagged a bresp
> error because the bursts were 8 KiB (AWLEN=255), which violates the AXI4
> 4 KiB-boundary rule; the EMIF NAK'd them with SLVERR. Fixed to 4 KiB bursts
> (AWLEN=127). **Any future AXI master in this family — including the GS
> tiled-VRAM DMA — must cap bursts at 4 KiB.** See [[reference-lpddr4-bw-spike]].
---
## 3. What LPDDR4 actually carries per frame in a tiled design
Tiling moves framebuffer/Z **read-modify-write** on-chip (M20K), so the three
things crossing the DDR boundary per frame are wildly unequal:
1. **Framebuffer writeout (tile flush to DDR): trivial.** 640×480×4 B ≈ 1.2
MB/frame → ~70 MB/s @ 60 fps. Noise against 8.4 GB/s. Ignore it.
2. **Texture fetch: the dominant unknown.** Textures are too large to sit in
M20K beside the framebuffer tile, so they stream from DDR. Locality-driven.
3. **Tile reload from primitive disorder.** When primitives don't arrive in
tile order, a tile gets evicted and re-fetched. Also locality-driven.
Items 2 and 3 are why a synthetic test would *lie* and the emulation traces are
the only honest source of truth: both depend on real access patterns and
working-set shape, not on raw throughput.
---
## 4. The PS2-specific tilt: palettized textures
PS2 textures were overwhelmingly **palettized — 4-bit and 8-bit indexed through
CLUT**, not 32-bit RGBA. That is a **quarter to an eighth** the per-texel DRAM
traffic of the naive 32-bit assumption. Budgeting texture bandwidth as if every
texel were 32-bit would massively overestimate the wall.
**Prerequisite measurement before any Test #2 RTL:** a **texture-format
histogram** — what fraction of texel fetches are 4-bit / 8-bit / 16-bit /
32-bit, plus the **overdraw factor** on busy scenes. That histogram sizes the
entire texture-bandwidth question before a line of RTL is written.
> **REALITY CHECK (2026-05-28, post-review):** an outside review assumed this
> histogram could be "extracted from the trace library / the 301 chapters."
> **It cannot — no real-game GS trace corpus exists in-repo.** A full-disk
> search confirmed every GS/texture artifact here is synthetic (hand-authored
> testbench sprites, `bake.py` test cards), and the two live-emulator capture
> harnesses (DobieStation, PCSX2) are parked/blocked (`sim/golden/README.md`,
> `third_party/*/NOTES.md`). The 301 chapters are EE-opcode/BIOS work, not GS
> captures. So this number must be **captured fresh, not extracted.** Building
> Test #2 against an assumed distribution would be the GS-side repeat of the
> Ch215 oracle-confusion trap. The realistic source is **PCSX2 GS dumps**
> (`.gs`/`.gs.xz` — a built-in PCSX2 GS-debugger feature that records real
> GIF + privileged-register traffic, incl. TEX0/CLUT, replayable offline);
> a prebuilt PCSX2 binary sidesteps the in-repo CMake-deps block. The
> prerequisite to the prerequisite is therefore **acquiring real GS dumps**
> (needs PCSX2 + games the owner owns), then a software `.gs` parser (no RTL).
---
## 5. Test #2 — Tiled-Raster Microbenchmark Spec
Goal: measure **sustained DDR bandwidth and tile-reload rate under real PS2
workloads**, in a tiled rasterizer fragment (tile color/Z resident in M20K, RMW
on-chip, tile + texture streamed to/from LPDDR4B).
### 5.1 Two trace-data prerequisites (do these FIRST — they scope the build)
1. **Texture-format histogram** (§4): texel-fetch distribution by bit-depth +
overdraw factor, from real game traffic.
2. **Worst-case stimulus selection** (§7): identify the single most
alpha-blended / overdraw-punishing in-game scene in the trace library — the
design must clear *peak*, not mean.
### 5.2 Workload knobs (sweep matrix)
- **Tile size:** 32×32 and 64×32 pixels (start).
- **Color format:** PSMCT32 first; later PSMCT16 / PSMT8.
- **Z buffer:** on / off.
- **Alpha blend:** on / off.
- **Texture mode:** solid color · small cached texture · streaming texture ·
CLUT texture.
- **Primitive mix:** fullscreen sprites · many small sprites · triangles.
### 5.3 Metrics (per configuration)
- tiles/sec, pixels/sec
- **bytes/pixel external** (the locality number that matters)
- LPDDR4 read GB/s and write GB/s (reuse the Test #1 counter approach)
- M20K footprint (tile color + Z + any texture cache)
- tile-reload rate (evictions/frame under the real primitive order)
### 5.4 Stimulus
Driven by **representative GS primitive + texture traffic pulled from the
emulation history** — specifically the worst-case scene from §5.1(2). **Not** a
boot screen or menu: those are bandwidth-trivial and will hand back a gorgeous
green result that collapses in-game.
---
## 6. Permanent architecture this implies
The GS that survives this board almost certainly is:
- **On-chip tile color + Z buffers** (M20K), RMW resolved on-chip.
- **LPDDR4B as backing VRAM** (no full 4 MB VRAM in M20K — consistent with 0006).
- **Texture cache or texture-tile streamer** feeding the rasterizer from DDR.
- **Scanout** either from the tiled framebuffer cache or a resolved linear buffer.
DSP budget is not the constraint (the shipped raster demo used 4/376 DSP).
Bandwidth and on-chip working-set are.
---
## 7. Methodological guardrails
- **Traces are truth.** Texture/locality numbers cannot be synthesized honestly;
pull them from real game traffic.
- **Test the peak, not the mean.** The torture case is alpha-blended overdraw
(smoke, fog, transparency, particles) — simultaneously worst for tile RMW and
often texture-heavy. Find the worst frame and make *that* the stimulus.
- **Don't over-trust the green.** Test #1 green ≠ faithful-GS feasible. Only
Test #2 under real game traffic produces the integer that answers the question.
---
## 8. Status / Next
- **Bandwidth gate: GREEN** (this doc, §12). New feasibility baseline.
- **Strategic pivot endorsed by Codex + outside review:** the next serious work
moves from qbert opcode-growth (Track A oracle, 0007) toward **GS tiled-VRAM
architecture feasibility** — because that path now has a plausible physical
foundation.
- **Immediate next step (no RTL): ACQUIRE real GS traffic first** — the trace
corpus does not exist (see §4 reality check). Capture PCSX2 GS dumps from
real games (owner-supplied, prebuilt PCSX2), then write a software `.gs`
parser to produce the texture-format histogram + locate the worst-case
alpha-overdraw frame (§5.1). Only then is the Test #2 stimulus honest.
- **Then:** build the Test #2 microbenchmark to this spec; its sustained number
under real game traffic is the final yes/no on faithful-enough GS on this board.
- **Chapter numbering note:** "Ch306" is already this repo's EE-core reality
checkpoint (0007). This GS line is a later chapter (Ch307+); the label, not
the substance, is what differs from Codex's framing.
@@ -0,0 +1,63 @@
# 0009 — Combined textured + alpha + depth: per-pixel memory-op schedule
**Status:** proven in sim (Ch302), board-pending. Local BRAM probe; NOT yet tiled VRAM.
## Why this exists
Before designing tiled/LPDDR-backed VRAM we need the exact per-pixel read/write
schedule a primitive that is simultaneously **textured + alpha-blended +
depth-tested** demands. Until Ch302 those three GS features were *mutually
exclusive* (each the sole `read2` consumer for its primitive). Ch302 lifts that —
behind the default-off `COMBINED_TAZ` param — with an explicit walker-stalling
multi-beat FSM in `gs_stub`, so the schedule is observable and asserted.
Speed was explicitly NOT a goal; the correct, observable schedule is.
## The per-pixel schedule (single read2 port, single write port)
Z-test is issued FIRST so a hidden pixel costs one read and nothing else:
| Beat | read2 (1-cyc registered) | compute | write port |
|------|--------------------------|---------|------------|
| 0 `CB_Z` | issue **stored-Z** read (`z_rd_en`) | — | — |
| 1 `CB_ZW` | (issue **texel** read iff Z passes) | Z-test (GEQUAL): frag_z vs stored_z. **FAIL → stop** (no texel/dest read, no write; advance) | — |
| 2 `CB_T` | issue **dest-color** read (`fb_rd_en`) | latch texel as Cs + As (=texel α) | — |
| 3 `CB_FB` | — | blend `Cv=((CsCd)·As)>>7+Cd` | **write color** (blended) → FB |
| 4 `CB_ZWR` | — | — | **write Z** → Z-buffer (skip if ZMSK); then advance walker |
The three reads land on the single read2 port in **separate cycles**, so the
existing read2 priority mux + its mutual-exclusion `$error` asserts are untouched
(one consumer per cycle). The two writes serialize on the single write port
(color beat 3, Z beat 4). The walker does not advance to the next candidate
pixel until BOTH writes complete.
## The concrete requirement for tiled VRAM
- **hidden pixel: 1 read, 0 writes** (stored-Z only).
- **visible pixel: 3 reads + 2 writes** — stored-Z, texel, dest-color reads;
color + Z writes.
So tile-local memory must serve **up to 3 reads + 2 writes per pixel**. The
options this makes concrete (no longer hand-wavy):
- a **2-read-port** tile RAM (e.g. texel + Z in parallel, dest folded in) + a
write path, OR
- a **3-phase read schedule** on fewer ports (what this probe does, serialized),
trading throughput for ports, OR
- tile-local banking that absorbs the dest read-modify-write locally.
Z-first ordering means the texel/dest bandwidth is only spent on visible pixels —
a real saving the tiled design should preserve.
## Verification (tb_top_psmct32_combined_demo)
A green Z-writing background + one TME+ABE+ZTE triangle whose interpolated Z
crosses the background Z (top half passes, bottom fails). A **memory-op tracer**
records, per pixel, the read enables + write addresses and asserts the SEQUENCE
(not just final pixels):
- depth-FAIL: z-read=1, texel-read=0, dest-read=0, color-write=0, Z-write=0 → pixel stays background green.
- depth-PASS: z-read=1, texel-read=1, dest-read=1, color-write=1, Z-write=1 → blend(texel, green); texel RGB and green dest both present.
Result: 35 PASS / 7 FAIL / 160 outside, errors=0. Param=0 keeps all prior demos byte-identical.
## Out of scope (deliberately)
Perspective (affine only — perspective proven separately, Ch301), alpha-test /
texture-alpha discard, non-PSMCT32 dest, and throughput (multi-beat is fine here).
@@ -0,0 +1,454 @@
# 0010 — On-chip tile-local renderer core (first tiled-VRAM rung)
**Status:** proven in sim (Ch303), board-pending. One 16×16 tile, on-chip color+Z,
flush to VRAM. Texture still BRAM-VRAM. NO LPDDR, NO multi-tile binning yet.
## Why
doc 0009 established the per-pixel requirement for a combined textured+alpha+depth
primitive: **hidden = 1 read / 0 writes; visible = 3 reads / 2 writes.** doc 0008 §6
sets the target architecture: on-chip tile color+Z (RMW resolved on-chip), LPDDR as
backing VRAM, texture streamed/cached. This rung builds the **first piece**: the
on-chip tile color+Z scratchpad with flush, so the combined RMW happens on-chip and
only the texture fetch + the tile flush cross to VRAM. (Codex framing: build the
tile-local core first; stage LPDDR integration later.)
## What was built
- **`gs_tile_ram`** (rtl/gif_gs/gs_tile_ram.sv): generic 1W1R on-chip tile RAM,
registered read (1-cycle, matching the VRAM read2 contract). Instantiated twice
in gs_stub (gated by `TILE_LOCAL`): `u_tile_color` (256×32) + `u_tile_z` (256×32)
— one 16×16 tile, ~2 KiB total.
- **gs_stub `TILE_LOCAL` mode** (default 0 → byte-identical): a combined TME+ABE+ZTE
triangle renders into the tile via a CLEAR → RENDER → FLUSH sequence overlaid on
the existing R_IDLE/R_SCAN/R_DRAIN FSM.
## The tile memory schedule (the deliverable)
```
CLEAR : 256 cycles → write tile_color = TILE_CLEAR_COLOR, tile_z = TILE_CLEAR_Z
(every entry initialized; the "background")
RENDER (per inside pixel, tile index = {y[3:0], x[3:0]}):
beat0 read tile_z
beat1 Z-test (GEQUAL: frag_z vs tile_z). FAIL → STOP (no texture read,
no tile_color read, no tile_color/tile_z write). PASS → read texture (VRAM)
beat2 texel ready (Cs/As) ; read tile_color (dest)
beat3 blend ; WRITE tile_color
beat4 WRITE tile_z (skip on ZMSK)
FLUSH : 256 cycles → read tile_color[idx] (registered) → framebuffer write
(raster_pixel_emit → VRAM at the linear FB address). ~70 MB/s class
per doc 0008 — trivial.
```
In tile terms:
- **hidden pixel:** tile_z read only. No texture, no tile_color read/write, no tile_z write.
- **visible pixel:** tile_z read + texture read + tile_color read + tile_color write + tile_z write.
- **flush:** tile_color read → framebuffer write, ×256.
Texture stays on the VRAM read2 path (unchanged). Only color/Z moved on-chip.
## Verification (tb_top_psmct32_tile_demo)
Combined triangle (interpolated Z crossing the clear Z) over a CLEAR'd green tile.
A tracer on the tile-RAM ports + the emit port asserts the schedule:
- CLEAR wrote 256 color + 256 Z entries.
- hidden (depth-fail) pixels: no texture read, no tile_color write, no tile_z write.
- visible (depth-pass) pixels: texture read + tile_color write + tile_z write; rendered
color = blend(texel, clear-green); occluded/outside = clear green.
- FLUSH emitted 256 framebuffer writes; final scanout matches the Ch302 image.
Result: clear 256/256, flush 256, 35 visible / 7 hidden, errors=0. `TILE_LOCAL=0`
keeps every prior demo byte-identical.
## External LPDDR bandwidth model (documented, not yet exercised)
Per doc 0008: framebuffer flush is **trivial** (640×480×4 B ≈ 1.2 MB/frame ≈ 70 MB/s
@60fps, noise vs the measured 8.4 GB/s). Texture fetch + tile-reload from primitive
disorder are the real DDR consumers and are **locality-driven** — to be measured
against real GS traces (doc 0008 §45), not synthesized. This rung does NOT touch
LPDDR: texture is BRAM-VRAM, one tile, no eviction.
## Ch304 — 2×2 multi-tile grid (extension)
The single-tile core generalizes to a `TILE_COLS×TILE_ROWS` grid with minimal
change, because (a) `tile_idx = {y[3:0],x[3:0]}` is already the tile-local address
for any 16-aligned tile, and (b) attribute interpolation is screen-space → seams
are continuous by construction. Added: an outer tile loop (the popped primitive +
solved gradients persist across all tiles), per-tile walker-bbox clip to
`primitive_bbox ∩ tile` (skip render if no overlap → tile shows clear color), and
a flush FB-address offset by the tile origin. `TILE_COLS=TILE_ROWS=1` is byte-
identical to the single-tile path. Codex scope: fixed primitive list (one
primitive re-rendered per tile), re-test-against-each-tile, NO external bin memory.
Proven (tb_top_psmct32_tile2x2_demo): one triangle spanning a 2×2 grid (32×32,
crossing x=16 & y=16) — all 4 tiles clear independently (256 each), 1024 flush
emits, and the **whole 32×32 scanout matches a single screen-space reference**
(718/718, 67 seam-region pixels continuous) → no visible seams. This is the
re-test-each-primitive-against-each-tile architecture; a real binning engine /
command buffer is a later optimization, not needed for the architectural proof.
## Ch305 — MULTI-PRIMITIVE tiled scene (extension)
Generalizes the grid from re-rendering ONE primitive per tile to compositing a
LIST of primitives per tile, in order, so later primitives depth-test/alpha-blend
over earlier ones within each tile. Gated by `TILE_MULTIPRIM` (default 0 →
byte-identical) + `TILE_PRIM_COUNT` (batch size). The primitive FIFO IS the list
store (its slots already hold each primitive's pre-solved gradients), so re-reading
a slot is free. Per tile: CLEAR → load+render prim 0 → (pipeline-flush) → load+render
prim 1 → … → FLUSH. The grid starts only once the whole batch is buffered
(`fifo_count >= TILE_PRIM_COUNT && all_grad_done`) — the demo-honest stand-in for a
future GIF-EOP/kick. Empty-clip primitives are skipped per tile; the whole FIFO is
drained at grid end (streaming/partial-drain is future work). The inter-primitive
advance waits for the per-pixel pipeline to fully flush (`comb_pipe_empty`), not just
the walker reaching R_DRAIN, so a primitive's in-flight color/Z writes commit before
the next primitive's `ras_*` load.
Proven (tb_top_psmct32_tile_multiprim_demo): 3 combined prims over the 2×2 grid —
opaque blue bg (Z=0x5000), opaque red (Z=0x6000, in front), translucent white
(Z=0x5800, blends but is OCCLUDED by red where 0x5800 < 0x6000). The whole 32×32
scanout matches a software integer-Z-buffer + source-over replay (514/514), with all
interaction regions exercised (blue 24 / red 48 / light-blue 26 / occlusion 19 /
green 416) and seam continuity (50 seam matches). This is the architecture a real
command-stream replay needs; a per-tile bin buffer is a later optimization.
DEBUG NOTES (two non-obvious bugs surfaced): (1) the per-primitive clip wires indexed
a FIFO array through a function inside continuous `assign`s — iverilog-12 mis-reads
that as 0 (silent hang, sim-time frozen); fixed by computing them in `always_comb`
(legal SV, Quartus-clean; sim-only workaround). (2) The first failing image was a
FIXTURE bug, not RTL: three solid 4×4 textures placed 0x100 apart aliased, because a
PSMCT32 texture with TBW=1 has a 0x100-byte row stride so a 4-tall texture spans
0x400 bytes; spacing them 0x400 apart (TBP0 32/36/40) fixed it. The depth/RMW path
was correct all along.
## Ch306 — GS SCISSOR clipping (extension)
Bakes the GS SCISSOR_1 rectangle into the tile-traversal walker bounds (param
`SCISSOR_ENABLE`, default 0 → byte-identical). Because the scissor is a rectangle and
the walker scans a rectangle, the effective draw region = primitive bbox ∩ tile bbox ∩
scissor rect is itself rectangular — so the scissor is just intersected into the
walker bbox at all clip sites (single-prim `clip_*`, multiprim `always_comb`, and the
`mp_next_nonempty` empty-test). NO per-pixel scissor test: pixels outside the scissor
are never visited, so color and Z writes are both suppressed for free. SCISSOR_1 (GIF
reg 0x40) is parsed into a GLOBAL `scissor_1_q` (reset full-range); decoded fields →
12-bit `eff_sc*` gated by SCISSOR_ENABLE (0/0xFFF when off → max/min no-op). Per-
primitive (FIFO-snapshot) scissor is a future extension if a command stream varies it.
Proven (tb_top_psmct32_tile_scissor_demo): the Ch305 3-prim scene + SCISSOR_1
[9..22]×[6..20] (crossing both seams) — 514/514 match, clipped=39 (would-be-scene
pixels outside the rect are clear green), inside=59 (kept scene matches the unclipped
ref), exact boundary (edgePairs=6), seam=50. Regression 209→210, byte-identical.
## Ch307 — texture WRAP modes (REPEAT + CLAMP) (extension)
Adds GS texture wrap (CLAMP_1 WMS/WMT: REPEAT/CLAMP) for u/v, inside `gs_texture_unit`
(param `TEX_WRAP_ENABLE`, default 0 → pass-through byte-identical). Applied to u/v
BEFORE texel-address gen, so it covers the linear and swizzle paths and all callers at
one point. REPEAT = `u & (2^TW - 1)`; CLAMP = `min(u, 2^TW-1)`. gs_stub parses CLAMP_1
(reg 0x48) and snapshots wrap mode + TW/TH per primitive (FIFO, like ras_tbw), so
REPEAT and CLAMP primitives coexist in one scene. Codex sequencing: wrap/clamp before
bilinear, because it determines which edge neighbours a future bilinear filter samples.
Proven: a standalone sampler TB (tb_gs_texture_wrap) covers PSMCT32 + PSMT8 +
PSMT4-swizzle (wrap happens before swizzle); the board TB (tb_top_psmct32_tile_wrap_demo,
557/557) renders two textured tris sampling a striped 4×4 texture with UV 0..8 — REPEAT
tiles 2× (two white stripes), CLAMP sticks (one white stripe + edge-stretched blue).
Regression 210→212, byte-identical. (Fixture lesson: the first NON-solid tile texture
exposed an upload-giftag REGS nibble-count bug that solid textures had masked.)
## Ch308 — PSMCT16 tile color buffer (extension)
The on-chip tile COLOR RAM can be PSMCT16 (RGB5A1, 16-bit) instead of PSMCT32, via
param `TILE_COLOR_PSMCT16` (default 0 → byte-identical). It HALVES the color tile RAM
(`TILE_COLOR_W` = 16; Z RAM stays 32-bit) — the first answer to "can tile color be
narrower than the 32-bit blend width when the frame format allows it?" (yes). The RMW
packs ABGR8888→pix16 on write/clear, unpacks pix16→ABGR (bit-replicate) for the blend
dest, and the FLUSH emits PSMCT16 framebuffer writes (mirroring the proven S2 PSMCT16
emit; vram_normalize keys the halfword off byte_addr[1]). Scanout reads PSMCT16 via
DISPFB1.PSM=0x02.
CONSTRAINT discovered: the combined tile path's primitive eligibility requires
FRAME.PSM==PSMCT32 (the combined RMW was built PSMCT32-only). So the PSMCT16-ness lives
in the tile RAM + flush + DISPFB, NOT in FRAME.PSM — the demo keeps FRAME PSMCT32 (so
the prims classify as combined) while DISPFB + tile + flush are PSMCT16. A fully-PSMCT16
FRAME would need the combined gate relaxed to accept PSMCT16 dest (future work).
Proven (tb_top_psmct32_tile_psmct16_demo, 514/514): the Ch305 scene in PSMCT16, matched
against a software reference that applies the SAME per-step 5-bit quantization
(q(c)=(c&0xF8)|(c>>5)) the on-chip RMW does — each primitive blends over the quantized
dest. Clear green 0x80→0x84, light-blue 0x7F→0x7B, pure blue/red unchanged, red
occlusion intact. Regression 212→213, byte-identical.
## Ch309 — generic GS ALPHA blend modes (extension)
Generalizes the combined blender from the single hardcoded source-over to the GS
selector machinery `Cv = clamp(((A-B)*C)>>7 + D)` (A/B/C/D from ALPHA_1, FIX=[39:32]),
param `ALPHA_MODES_ENABLE` (default 0 → source-over, byte-identical). gs_alpha_blend
gains a_sel/b_sel/c_sel/d_sel/ad/fix inputs + a generic datapath; gs_stub FIFO-snapshots
the per-primitive selectors+FIX and wires them to u_comb_blend. The combined-eligibility
gate `close_combined` (which hardcoded source-over) is relaxed to accept any ABE
primitive when ALPHA_MODES_ENABLE — the generic blender handles any config. (Same class
of "eligibility gate too strict" as Ch308's PSMCT32-FRAME requirement: when you add a
per-pixel mode to the combined path, check the datapath AND close_combined.)
Proven (tb_top_psmct32_tile_alpha_demo, 514/514): the Ch305 scene with P1 ADDITIVE
(A=Cs,B=0,C=FIX=0x80,D=Cd → Cs+Cd) → magenta over the blue bg (glow/particle add), while
P0/P2 stay source-over (light-blue intact) and P2 is still depth-occluded by P1. Two
blend modes coexist. Regression 213→214, byte-identical.
## Ch310 — bilinear texture filtering (extension, 2-phase)
4-tap bilinear (PSMCT32), staged per Codex. PHASE 1: a multi-beat bilinear sampler in
gs_texture_unit (param BILINEAR_ENABLE, default 0): reads the 4 neighbours (each via the
Ch307 wrap), lerps by fractional U/V; schedule = 4·(1+RD_LATENCY)+1 ≈ 9 cyc/sample (the
architectural number for the future texture cache). Proven by a standalone TB
(tb_gs_texture_bilinear) — all 6 cases exact (center=nearest, halfway=4-tap avg, clamp
edge no-OOB, repeat edge wraps, nearest unchanged). PHASE 2: integrated into the COMBINED
tile path — TEX1.MMAG (GIF 0x14 bit5) per-primitive selects nearest vs linear; a runtime
`filter_lin` input gates the 4-tap; the affine interp gains a frac sibling
(interp_affine_uv_frac → step[15:12]); a new CB_TWAIT beat stalls the per-pixel FSM on the
LEVEL !tex_busy until the ~9-cycle sample completes (the FSM steps half-rate on z_advance,
so a level wait can't miss the 1-cycle out_valid), then CB_T latches the HELD filtered texel.
Depth/Z/blend/tile-RMW unchanged; bilinear did NOT touch close_combined (the prim is still
source-over ABE). Proven (tb_top_psmct32_tile_bilinear_demo): a magnified 4×4 blue/white
checker, nearest tri blocky (0 midtones) vs bilinear tri smoothed (all midtones), same
coverage (stall dropped nothing). Regression 215→216, byte-identical.
## Ch311 — per-tile BIN BUFFER (extension)
Replaces Ch305's render-time re-test (mp_next_nonempty: each tile re-scans all prims) with
a real precomputed bin buffer (param BIN_BUFFER_ENABLE, default 0). A new TP_BIN phase runs
a (prim,tile) double-loop counter FSM (prim_count×NTILES cycles) that tests each prim's
bbox∩tile∩scissor (the same overlap math) and appends the prim index to bin_prim[tile][] /
bin_n[tile], in ascending draw order. The render then walks each tile's bin (CLEAR-done loads
bin slot 0; RENDER-drain steps through bin_n; FLUSH at end) — no re-scan. Equivalent image
to the re-test path (same overlap test + order). This is the primitive-ROUTING machinery for
command-stream replay; the grid stays 2×2 (prove the mechanism, scale later). Proven
(tb_top_psmct32_tile_bin_demo): bins read back exactly (t0={0,1} t1={0,1} t2={0} t3={0,2} for
an all-tiles/2-tiles/1-tile prim trio) + image 594/594 vs the re-test reference. Regression
216→217, byte-identical.
## Next (staged, per Codex)
1. Multiple tiles / tile grid (primitive→tile binning). [DONE: Ch304 grid, Ch305 list, Ch311 bin buffer]
Scissor/window clipping. [DONE: Ch306]
Texture clamp/repeat. [DONE: Ch307]
PSMCT16 tile color. [DONE: Ch308]
ALPHA mode expansion. [DONE: Ch309]
Bilinear filtering. [DONE: Ch310 — sampler + combined-path integration]
Larger grid sweep. [DONE: Ch312 — 2x2→4x4 (16 tiles, 64x64) via the bin buffer; no new RTL logic, COLS/ROWS/NTILES already parameterized]
## Ch312 — 4x4 grid (extension)
Scales the tiled renderer to a 4×4 grid (16 tiles, 16×16 each = 64×64) by setting
TILE_COLS=TILE_ROWS=4 — NO new RTL logic, since the grid loop + bin buffer (NTILES,
CUR_T_W/BIN_T_W via $clog2) were already parameterized. 64×64 PSMCT32 FB fills 16 KiB so
the demo uses VRAM 32 KiB (textures @ 0x4000). Proven (tb_top_psmct32_tile_bin4x4_demo):
3 prims (P0 4-tile / P1 6-tile cross-seam / P2 1-tile, + 6 empty tiles), all 16 bin_n
read back exactly (1100 1211 0111 0001, empty=0), t5={0,1} order preserved, image 3240/3240
vs the re-test reference, seam continuity across x=16/32/48 + y=16/32. Regression 217→218,
byte-identical. The fit (owner) gives the resource-scaling answer: bin storage grows 4×
(60→240 register bits, still tiny) — a hard ALM/register jump would signal the register-bins
should go BRAM/MLAB-backed before larger scenes.
## Ch313 — full PSMCT16 framebuffer mode (extension)
Relaxes the `close_combined` eligibility gate so the combined/tiled path accepts a
PSMCT16 dest (`frame_1_q[29:24]==6'h02`) — but ONLY when `TILE_COLOR_PSMCT16=1`, so a
PSMCT16 FRAME never pairs with a PSMCT32 flush. This was the LAST place forcing a
PSMCT32 FRAME: the tile color RAM, the dest-color unpack for blending, and the flush
emit (be=`4'b0011`, psm=`0x02`, `<<1` byte addr) were ALL already PSMCT16 from Ch308,
keyed off `TILE_COLOR_PSMCT16` and independent of `FRAME.PSM`. So Ch308's PSMCT32-FRAME
workaround is gone — render/flush/scanout are now consistently RGB5A1. One-term RTL
change; at `TILE_COLOR_PSMCT16=0` (default) the new disjunct is constant-0 and the gate
collapses to the original PSMCT32-only test (byte-identical). Demo = the Ch312 4×4
(64×64) scene with `FRAME.PSM=PSMCT16` + DISPFB PSMCT16. A 64×64 PSMCT16 FB is 8 KiB —
HALF the 16 KiB PSMCT32 FB — so the demo runs in **16 KiB VRAM vs Ch312's 32 KiB**: the
direct framebuffer-memory saving that motivates the LPDDR-backed FB phase. Proven
(tb_top_psmct32_tile_psmct16fb_demo): flush 4096/4096 carry psm=0x02 + be=`0011`, ZERO
PSMCT32 flushes (whole FB is 16-bit); image 3240/3240 vs a re-test reference replayed
with per-step RGB5A1 quantization `q5(c)=(c&0xF8)|(c>>5)` (EXACT); 2875 matched pixels
differ from the would-be PSMCT32 value (proves the FB is genuinely RGB5A1, not PSMCT32);
bin_n/scissor/depth identical to Ch312 (1100 1211 0111 0001, t5={0,1}, seam 464).
FIT-CLEARED + VISUALLY VERIFIED on Agilex 5 (2026-06-01): vs Ch312 (4×4 PSMCT32, 32 KiB),
RAM blocks 45→29 (16), block-mem 688,128→421,888 (256 Kbit), ALMs 159, regs 555 — the
PSMCT16 FB recovered ALL of Ch312's 4×-scale-up memory cost, landing back on the Ch311 2×2
PSMCT32 baseline of 29 RAM blocks. A 4× grid in PSMCT16 costs ZERO extra framebuffer memory
vs the 2×2 PSMCT32 grid: hard proof the framebuffer (not bins/logic) is the on-chip memory
consumer and pixel format trades directly against it. Board image matches (blue/red/teal
tris + green, RGB5A1-quantized, no seams).
## Ch314 — bilinear for palettized (PSMT8/PSMT4) textures (extension)
Extends bilinear to INDEXED textures with the CLUT-BEFORE-INTERPOLATE rule: each of the 4
taps fetches an index, CLUTs it to RGBA, then the 4 COLORS interpolate (NOT the indices —
that would round to one palette entry). The sampler core is ~6 lines: the bilinear FSM tap
capture changes from `tap[beat] <= tex_rd_data` to `tap[beat] <= near_color` (already
`(PSMT8||PSMT4)?clut_rd_data:tex_rd_data`), so PSMCT32 is byte-identical and indexed taps
capture the CLUT'd color. New param PALETTE_BILINEAR (default 0) widens `do_lin` to admit
PSMT8(0x13)/PSMT4(0x14). The per-tap addr-gen (linear/swizzle + wrap/clamp) already runs
BEFORE the CLUT lookup, so "swizzle-before-CLUT" + edge wrap/clamp are free. For the BOARD
demo (bilinear lives only in the combined path), `close_combined`'s texture-PSM gate also
widens to admit PSMT8/PSMT4 when PALETTE_BILINEAR; the shared gs_texture_unit already had
the CLUT port wired and CLUT is a combinational 3rd port (no read2-arbitration change).
Proven: tb_gs_texture_bilinear (unit) CASE7 PSMT8 red↔blue halfway → 0xFF7F007F (purple,
neither endpoint = colors interpolated), CASE10 PSMT4 nibble across a byte boundary, CASE11
repeat / CASE12 clamp edges + no OOB, CASE1-6 PSMCT32 byte-identical;
tb_top_psmct32_tile_palbilinear_demo (board, combined path + CLUT load) nearMid=0 /
bilMid=58 — the on-board CLUT-before-interp proof. Regression 219→220 byte-identical, board
elab EXIT 0. FIT-CLEARED + VISUALLY VERIFIED on Agilex 5 (2026-06-01): LEFT tri blocky
blue/white indexed checker, RIGHT tri smoothed blue↔white midtones (CLUT-before-interp on
silicon). RESOURCE DELTA vs Ch310 (2×2 PSMCT32 bilinear, 16KB): ALMs 30,229→30,101 (flat),
DSP 122→122 (0), block-mem 425,984→425,984 (0), RAM blocks 29→29 (0) — palettized bilinear
is essentially FREE: zero extra DSP (reuses the same lerp multipliers), zero extra memory
(reuses the CLUT port from Ch296), the CLUT-before-interp restructure is just a mux on the
tap-capture path.
## Ch315 — primitive/bin capacity scaling (extension)
Parameterizes the primitive FIFO depth (was a hardcoded `FIFO_DEPTH=4`) as `TILE_FIFO_DEPTH`
(default 4 → byte-identical; power-of-2). In the bin-buffer renderer this depth sizes BOTH
the prim-list capacity N AND the per-tile bin depth M (bins are `[NTILES][FIFO_DEPTH]`),
so they're coupled (M=N: a tile's bin can hold every queued prim). Adds sim-visible
diagnostics (`raster_overflow_count_r`, `bin_occ_max_r`, defensive `bin_overflow_r`).
ARCHITECTURAL ANSWER to "where do register bins stop being reasonable": the dominant cost
is the ~40 `fifo_*` per-prim attribute arrays (hundreds of register bits/slot); the bins
add only `NTILES*FIFO_CNT_W` index bits per depth (~48 bits/depth at 4×4 — negligible), so
register bins stay cheap far past the FIFO's practical limit. OVERFLOW nuance: the batched
tile path triggers at `TILE_PRIM_COUNT` and drains the FIFO, so excess prims are CLAMPED
(visible as capped bin occupancy), not push-dropped — `raster_overflow` (the streaming
push-while-full flag, now counted) doesn't fire in the batched path; and `TILE_PRIM_COUNT`
must be `<= FIFO_DEPTH`. Proven: tb_top_psmct32_tile_cap_demo (depth 8, 7 prims) — bin t0
holds 6 (occ_max=6 > old 4), draw order {0..5}, image 3873/3873, no overflow;
tb_top_psmct32_tile_cap_overflow_demo (depth 4, same payload) — occ_max CLAMPS to 4
(capacity ceiling) and still renders all 16 tiles gracefully. Regression 220→222
byte-identical, board elab EXIT 0. (Demo puts the deep bin in t0 to dodge an orthogonal
latent bug: empty tiles preceding the first non-empty tile flush black — to be fixed
separately.) FIT-CLEARED + VISUALLY VERIFIED on Agilex 5 (2026-06-01). RESOURCE SLOPE
(depth-8 vs Ch312 depth-4): ALMs 29,682→32,072 (+2,390), regs 33,356→37,486 (+4,130),
block-mem + RAM-blocks UNCHANGED (688,128 / 45). So +4 FIFO slots = ~1,033 regs + ~600 ALMs
PER primitive slot, ZERO block RAM — dominated by the ~40 fifo_* attribute arrays; the bins
add ~80 regs/slot (negligible). The bins never stop being reasonable; the per-prim attribute
FIFO is the ALM-bound scaling wall (~16-prim headroom at this grid). Beyond that, move the
per-prim attribute storage (not the bins) to block RAM.
## Ch316 — leading-empty-tile traversal fix (correctness)
Fixes the latent bug found in Ch315: tiles that are EMPTY and PRECEDE the first non-empty
tile flushed BLACK instead of the clear colour. ROOT CAUSE: the per-tile flush row-stride
is `flush_pixel_index_w = flush_y*(ras_fbw<<6)+flush_x` (gs_stub ~line 3408), and `ras_fbw`
(FRAME width) was loaded ONLY by `mp_load_prim` (on primitive load). A leading-empty tile
loads no prim, so it used the reset `ras_fbw=0` → stride 0 → every row collapsed onto row
0's FB addresses → the tile's real screen rows kept the FB-init value (black). Empties AFTER
a render inherited that render's ras_fbw, hence were fine — the exact asymmetry observed.
FIX: in the `mp_grid_start` branch (~line 5588) load `ras_fbp/ras_fbw/ras_psm/ras_bpp_shift`
from the batch's oldest FIFO entry (`fifo_*[fifo_rptr]`) at GRID-RENDER START, so the flush
address is valid for EVERY tile. A batch shares one FRAME, so this equals what `mp_load_prim`
sets at render → byte-identical for any batch whose first tile is non-empty. Proven:
tb_top_psmct32_tile_late_demo (1 prim only in t15, t0..t14 empty) — ZERO black pixels, all
empty tiles green-cleared, bin_n[15]=1 (renderer reached the last tile, no premature done),
image 3990/3990; Ch315 cap_demo still 3873/3873. Regression 222→223 byte-identical, board
elab EXIT 0 (GS_TILE_LATE_DEMO). Root-caused with a direct VRAM probe (FB at empty tiles
0x0 → 0xFF008000 after the fix). FIT-CLEARED + VISUALLY VERIFIED on Agilex 5 (2026-06-01):
whole 64×64 green (all 15 leading empties clear correctly) + one blue triangle in t15;
resources on the Ch312 baseline (ALMs 29,801, regs 32,442, block-mem/RAM unchanged) — the
fix adds zero storage (loads 4 existing ras_* regs at grid start), pure control-flow.
## Ch317 — LPDDR-backed framebuffer, tile-flush only (sim write/readback proof)
First external-framebuffer step, deliberately tight: ONLY the PSMCT16 tile FLUSH is
redirected to an LPDDR framebuffer; tile color/Z + texture stay on-chip. The proven LPDDR
path (doc 0008, 8.4 GB/s, 256-bit AXI4 → EMIF hard-IP) lives in a SEPARATE diagnostic core,
not the GS top — so this rung proves the write/readback path against a behavioral LPDDR
MODEL (no board fit; wiring the real EMIF master + LPDDR scanout into the GS top is the next
rung). New module `gs_lpddr_fb_writer.sv`: a staging FIFO + burst engine (coalesces a
contiguous +2 run into one burst, 4 KiB cap per the doc 0008 AXI lesson) + byte-addressed
backing FB + bandwidth/over-underflow counters. Consumes the existing flush stream
(`raster_pixel_fb_addr_q` is already the linear `fb_base+(y*pitch+x)*2`). Integrated into
the bram top generate-guarded by `LPDDR_FB_ENABLE` (default 0 → not instantiated,
byte-identical), as a transitional ADDITIVE mirror (BRAM FB still feeds scanout; LPDDR is
the readback-proof target). Proven: tb_gs_lpddr_fb_writer (256-px tile → 512 B / 16 bursts;
2049-px run → 2 bursts via the 4 KiB cap; enable=0 inert) and tb_top_psmct32_tile_lpddrfb_demo
(Ch313 PSMCT16 scene → LPDDR FB == BRAM FB for all 4096 px; 8192 bytes; 256× 32-B bursts; no
over/underflow; ~0.20 GB/s @100 MHz model). Regression 223→225 byte-identical, board elab
EXIT 0 (writer pruned at default). FIX worth noting: a `PTR_W'(FIFO_DEPTH)` truncation read
the FIFO empty-as-full; use `count[PTR_W]`.
## Ch318 — LPDDR framebuffer write path on hardware (RTL sim-proven + fit-ready; board gated)
Connects the Ch317 write path to the real fabric→LPDDR port. qsys_top exposes an
`f2sdram` AXI4-256 port (was tied off); the GS runs on design_clk, f2sdram on CLOCK2_50 —
genuinely async. New `gs_async_fifo` (gray-code CDC) + `gs_lpddr_axi_master` (thin wrapper,
per Codex — does NOT touch the proven writer): GS-domain packer (16 px → one 256-bit
tile-row beat {addr,data,strb}) → async FIFO → f2sdram AXI burst FSM (single-beat INCR,
AWSIZE=5, AWLEN=0, full WSTRB). A HARD `write_enable` gate (packer + awvalid/wvalid + FSM)
makes an LPDDR write impossible unless explicitly enabled — Linux-safety. de25 top exposes
the PSMCT16 flush stream and, under `ifdef GS_LPDDR_FB`, drives the f2sdram write channel
(default = legacy inert tie-off → byte-identical; with the macro, write_enable=0 + FB_BASE=0
placeholder, so the fitted core boots inert). Proven: tb_gs_lpddr_axi_master (gate-off →
zero AXI activity; gate-on → 16 INCR beats, 0 protocol/bresp/FIFO errors, slave readback ==
source, under AW/W/B backpressure + async clocks); de25 elaborates EXIT 0 both ways;
regression byte-identical. fifo_full gotcha: use `count[PTR_W]` (a PTR_W-wide literal
truncates DEPTH→0). The BOARD run is GATED on a Linux-safe LPDDR address (owner: /proc/iomem
→ reserved region → FB_BASE → raise write_enable → write 8 KiB → HPS devmem readback/hash).
HW acceptance = write/readback + fitter snapshot. Ch319 = LPDDR scanout.
## Ch319 — LPDDR4B framebuffer write + HPS-bridge readback (SILICON-VERIFIED)
The f2sdram/HPS-DRAM path of Ch318 was CLOSED as platform-rejected (BRESP 256/256 on the
secure reserved region — /dev/mem reads of 0x80000000 crash the board). The GS framebuffer
pivots to **FPGA-private LPDDR4B** via the EMIF_Qsys IP (cloned from de25_lpddr4_bw/ao486,
same device): emif_clk ~310 MHz, emif_reset_n = cal-ready. Reuse the Ch318 writer chain
(`gs_lpddr_axi_master` + `gs_async_fifo` + counters), just retargeted onto the EMIF AXI
write port instead of f2sdram. New `gs_lpddr_rd_probe.sv` lets the HPS read any FB word back
over the bridge (`LPDDR_RDADDR` @ 0x03C: write byte-addr → poll `LPDDR_STATUS[3]` rd_pending
→ read the 32-bit word); the `lpddr_dump` tool walks this to pull a whole frame to a PPM.
**Crucially the FB is FPGA-private, NOT Linux RAM** — so verification is via the bridge probe
+ bridge COUNTERS (bytes/bursts/bresp_err), never /dev/mem. SILICON-VERIFIED: write 8 KiB →
bridge readback hash matches the source (md5 3b12baffc00bb6419fa66272c75b2cc7), BRESP_ERRS=0.
## Ch320 — LPDDR4B scanout to HDMI (SILICON-VERIFIED)
Display the LPDDR4B framebuffer on HDMI. `gs_lpddr_scanout.sv`: a whole-frame cache (an 8 KiB
M20K copy of the frame, NOT an ao486-style line buffer) filled from LPDDR via single-beat
reads (arlen=0 — the ONLY AXI read pattern proven on this EMIF; multi-beat bursts garble),
indexed by the PCRTC `vram_read_addr`. `gs_lpddr_rd_arb.sv`: a 2:1 read arbiter sharing the
EMIF read channel (port0 scanout = priority, port1 Ch319 probe). de25 top muxes the video
source (BRAM default / LPDDR scanout) on `LPDDR_CTRL[2]` video_src, gated by the PCRTC
display-window (`pix_window_o`). SILICON-VERIFIED at 64×64: scanout pixel-identical to BRAM.
**Bug found+fixed on silicon:** the scanout ignored the PCRTC display window → 10 sheared
tiles; fixed by exposing `pix_window_o` and gating the scanout mux. The whole-frame cache
DOES NOT SCALE — see Ch321: at 1024 beats (32 KiB) it never finishes loading on this EMIF.
## Ch321 — larger FB (128×128 PSMCT16) + LINE-BUFFER scanout (SILICON-VERIFIED) — ACCEPTED ARCHITECTURE
Two bricks. **Brick 1 (render):** new 128×128 PSMCT16 fixture (32 KiB frame) +
`GS_TILE_LPDDR128_DEMO` profile (VRAM grown 8→64 KiB so a 32 KiB frame fits, TILE grid 8×8 of
16×16 tiles). **Brick 2 (scanout) — the real deliverable:** `gs_lpddr_scanout_lb.sv`, a
double-buffered LINE-BUFFER reader that holds just TWO scanlines (displays row L from buf[L&1]
while prefetching the next row), O(width) on-chip not O(width×height). **DECISION: the
whole-frame cache is REFERENCE/FALLBACK only, NOT the architecture** — a cache that "fits"
still MIRRORS the FB in M20K, defeating the move to LPDDR, and empirically it won't even load
a 32 KiB frame on this EMIF (frame-cache `0x4` → cache_valid never sets, blank). The
line-buffer is THE scanout path going forward. SILICON-VERIFIED: render BURSTS=0x400/BRESP=0;
line-buffer `LPDDR_CTRL=0xC` → STATUS line_valid=1/rd_errs=0, HDMI matches the lpddr_dump PPM
pixel-for-pixel (no col-1 band — the sim TB's residual 1px/line was confirmed a checker
leading-edge artifact, not hardware). **Three real HARDWARE bugs fixed** (the first board
attempt garbled): (1) multi-beat burst → single-beat reads (arlen=0); (2) miss-prone request
toggle → free-running sequential prefetcher; (3) vsync-mid-read AXI abort → deferred reset
(`fs_pending`, never abort an in-flight read). Fit clean: 31,683 ALMs (68%), 117 RAM (33%).
## Next (per Codex)
The framebuffer now lives off-chip (write + line-buffer scanout, silicon-proven). Make
TEXTURE storage external next, correctness-first, before any performance sizing:
1. **Ch322 — LPDDR-backed texture fetch/cache (correctness-first).** One known texture in
LPDDR4B; a small read-only texture cache behind the sampler; BRAM texture path stays as
fallback. Acceptance: LPDDR-textured image == BRAM-textured image, rd_errs=0, counters
prove LPDDR fills happened. NOTE (prereq-check finding): the nearest-path sampler assumes
FIXED 1-cycle texel latency (no stall on the default path — CB_TWAIT only exists for
bilinear/combined), so a naive demand-miss stall would corrupt output. Resolve via
prefetch-warm (fill the cache fully before raster → every read a 1-cycle hit) OR add a
sampler/walker stall — see the Ch322 framing.
2. Framebuffer/Z backing to LPDDR with tile flush/reload.
3. Command-stream ingestion (defer until both FB and texture memory are off-chip).
Only after a real-trace texture-format histogram (doc 0008 §4) is performance LPDDR sizing
honest; Ch322 is correctness-only and does NOT pretend to know real-game cache sizing.
+101
View File
@@ -0,0 +1,101 @@
# 0011 — GS dump ingestion (Ch340): parse, census, translate a supported subset
Status: **ACCEPTED — Ch340 CLOSED (2026-06-21)** as a parser + census / fail-closed victory. Brick 5
(authentic on-silicon render) is **explicitly waived** because the authentic dump contains zero
supported segments — that is the census doing its job, not a failure.
## Closeout (honest framing, per Codex)
- Authentic `cubes_demo` GS dump (MIT homebrew, content-clean) parsed **deterministically, 0 malformed**.
- Container format pinned from PCSX2 source and validated byte-exact; byte-exact synthetic parser test
passes (`tools/test_gs.sh`).
- Primitive reconstruction (GS vertex-kick model) works: **648 triangles + 540 sprites**.
- Support census classified **every** primitive; histograms + reasons emitted to
`captures/gs/reports/cubes.census.txt` (aggregate only).
- Translator **failed closed with no scene**: every authentic triangle is textured (`TME=1`, no real-
texture path) and sprites are unsupported. Nothing was approximated.
- Core trust-boundary goal achieved: **authentic GS traffic enters the pipeline and unsupported
content is reported, not faked.** The translator→`ps2_feeder`→staging path is proven on the
supported synthetic fixture; authentic silicon render is deferred to the census-derived blocker.
- Mechanical top blocker → **Ch341: textured-triangle ingestion** (real texture state/upload/bind).
Do NOT hunt another dump for a convenient flat segment; do NOT substitute synthetic silicon for
authentic Brick 5.
Original design follows (the boundary it set still holds).
## Goal & trust boundary
Authentic GS traffic enters the proven host pipeline, is decoded **deterministically**, and a
**strictly-supported subset** reaches pixels with **no hidden approximation**. Ch340's honest victory
is that property — NOT a full game frame rendering. Real captures will expose unsupported textures,
transfers, blend/state, and primitive modes; those are **reported**, never approximated.
## Pipeline
```
.gs[.xz] ──(container parser)──► raw packets ──(GIF/GS decoder)──► normalized event stream
│ │
│ (local, gitignored) ▼
└────────────────────────────────────────────────► support census + histograms (reports/)
supported subset ──(translator)─┴─► ps2_feeder scene file
(Ch339 encoder streams it)
```
The translator emits the Ch339 text scene grammar (`tri`/`trig`/`tritile`/`rect`/`go`); it does NOT
re-implement the staging format. `ps2_feeder -f scene.txt` renders it on the existing bitstream.
## Normalized event stream (schema v1, versioned)
A flat, ordered list; every event carries `byte_off`, `frame_idx`, `event_idx`. Event kinds:
- `FRAME_BOUNDARY {field}` — VSync packet (frame delimiter).
- `GIFTAG {path, nloop, eop, pre, prim, flg, nreg, regs}` — decoded GIF tag header.
- `GSREG {addr, name, value}` — a GS register write (from A+D, REGLIST, or PACKED expansion).
- `IMAGE {qwc, dst_fmt}` — an IMAGE-mode (HWREG/texture/FB upload) transfer; payload bytes summarized,
NOT inlined into committable output.
- `READFIFO {qwc}` — local→host transfer.
- `MALFORMED {reason}` — structural decode failure at this offset.
`GSREG.name` covers the register set we already encode in `bake.py`/`ps2_feeder`: PRIM, RGBAQ, ST, UV,
XYZ2/XYZ3, XYZF2/XYZF3, TEX0_1/2, CLAMP, FOG, TEX1/2, FRAME_1/2, ZBUF_1/2, TEST_1/2, ALPHA_1/2,
SCISSOR, PRMODE/PRMODECONT, BITBLTBUF, TRXPOS, TRXREG, TRXDIR, etc. Unknown addrs → `GSREG` with
`name="UNKNOWN_0xNN"` (reported, not dropped).
## Support census (every event classified)
- **translated** — emitted into the ps2_feeder scene (the supported subset, below).
- **ignored (justified)** — safely skippable with a stated reason (e.g. FOG with FGE off; a redundant
state write; a NOP). The justification is explicit per category.
- **unsupported** — a real effect we cannot faithfully reproduce yet (textured prim, sprite with a
real texture, blend mode ≠ the proven source-over, PSM we don't render, Z format, dest-alpha test,
scissor we don't honor, TRIANGLE_FAN/STRIP we haven't reduced, lines/points, IMAGE texture upload).
Recorded with frame/event/byte offset + the exact reason. **Never approximated.**
- **malformed** — structural failure (bad GIF tag, truncated payload, length mismatch).
Reports (committable, no game content): per-dump JSON/text with frame count, a GS-register-write
histogram, a primitive-mode histogram, and the full unsupported/malformed list with offsets+reasons.
## Supported subset that reaches pixels (Ch340 v1)
Matches what the feeder + `ps2_feeder` already render faithfully on silicon:
- `PRIM` = TRIANGLE (prim type 3), with `IIP` flat or gouraud (per-vertex RGBAQ).
- Vertices via `XYZ2`/`XYZ3` (Z honored — Ch338 cross-batch Z is correct).
- Color via `RGBAQ` (MODULATE through the unity texel — matches the proven path).
- `FRAME_1`/`ZBUF_1`/`TEST_1`(GEQUAL)/`ALPHA_1`(the proven source-over) within the supported envelope.
A draw segment qualifies only if EVERY primitive in it is in this subset and the state matches the
proven envelope. Sprites→rect and triangle-strip→triangle reduction are candidate Ch341 work, decided
from the census, not pre-built.
## Acceptance (Codex)
1. Byte-exact parser tests on a small **synthetic** `.gs` fixture (`captures/gs/synthetic/`, authored
once the real container format is confirmed).
2. One authentic dump parses **deterministically** (same events every run).
3. Frame / register / primitive histograms emitted.
4. Unsupported events carry frame/event/byte offset + reason.
5. ≥1 carefully chosen supported frame/segment translates to a ps2_feeder scene and **renders on
silicon**.
6. Translation failures **stop before board access**.
## Bricks (gated on the dump)
1. Inspect the real dump bytes; confirm the container framing (header, compression, packet types).
Build the container parser + a matching synthetic fixture; byte-exact parser test.
2. GIF/GS decoder → normalized events (GIF tag + A+D/REGLIST/PACKED register expansion). Unit-tested
against hand-built GIF packets (the encode side already exists in `bake.py`).
3. Census + histograms over the normalized stream; emit reports.
4. Translator: supported-subset events → ps2_feeder scene file; everything else → census. Fail closed.
5. Pick a supported segment from the census; render via `ps2_feeder -f`; confirm on silicon.
Ch341 is then chosen from the census's highest-impact blocker, not guessed now.
@@ -0,0 +1,86 @@
# 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.
+22
View File
@@ -0,0 +1,22 @@
# Design Decisions
This directory is for short decision records once the team starts locking items.
Suggested format:
- title
- date
- status
- context
- options considered
- decision
- consequences
Locked so far:
- `0000-trace-format.md`
- `0001-posture.md`
- `0002-bios-policy.md`
- `0003-golden-reference.md`
- `0004-first-visible-milestone.md`
- `0005-phase0-source-of-truth.md`