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
+229
View File
@@ -0,0 +1,229 @@
# Ch259 outcome + Ch260 brief for Codex
**Status:** Ch259 landed exactly as scoped. Both phases ran. Treadmill
unchanged through both phases. Pausing for Codex's Ch260 call. No
further code changes until your directive.
## Ch259 implementation (per Codex spec)
### A. Tightened Ch218 observer
`tb_ee_core_bios_smoke.sv` — the observer is now a **compact INTC
transaction log** scoped to exactly `0x1F801070` (`I_STAT`) and
`0x1F801074` (`I_MASK`):
- Captures both **reads and writes** in order, tagged with pass
index (from `ch217_count`).
- Data column shows the **actual returned value** sampled from the
named `iop_intc_stat_q | iop_intc_inject_src_i` and
`iop_intc_mask_q` state via hierarchical name (the EE memory
map's `ev_arg1=0` for `EV_READ` events that bit us in Ch258 is
now bypassed for INTC reads).
- Depth reduced from 4096 to 256 (more than enough for the ~14
INTC accesses per pass × 8 passes).
- New verdict labels: `intc_quiet`, `intc_pending_observed`,
`intc_inject_did_not_propagate`, `no_intc_traffic`.
- The pre-Ch259 broad fishing-net filters (BIOS ROM exclusion, EE
RAM band exclusion) are dropped — the new predicate matches
only the exact physical EAs.
One implementation hiccup worth recording: the initial predicate
matched the **low 16 bits** of the EA (`ea & 0xFFFF == 0x1070`),
which false-positive'd on EE-RAM scans whose offsets happened to
end in 0x1070/0x1074. Fixed to match the exact physical EAs after
one rerun. Documented inline.
### B. Named IOP INTC behavior in `ee_bootstrap_mmio_stub`
Promoted `0x1F801070`/`0x1F801074` out of the anonymous regfile
into named INTC state. Mirrors `rtl/intc/intc_stub.sv` semantics
exactly:
- **I_STAT (`0x1F801070`)**:
- reset: `16'd0`
- read: returns `iop_intc_stat_q | iop_intc_inject_src_i` (sticky
injection)
- write: W1C — `stat_q <= (stat_q & ~wdata) | inject_src` per
cycle. Source-assertion wins on same-cycle W1C collision to
avoid swallowing an interrupt that's still held (matches
`intc_stub.sv:102-110`).
- **I_MASK (`0x1F801074`)**:
- reset: `16'd0`
- read: returns `iop_intc_mask_q`
- write: plain write (full-word `&reg_wr_be`). Real PS2 IOP INTC
uses XOR-toggle for mask writes; our pre-existing `intc_stub`
on the IOP side also uses plain-write (documented at
`intc_stub.sv:19`). Ch260 can extend if BIOS demonstrably
requires the toggle.
- **New input port** `iop_intc_inject_src_i [15:0]` — sticky
source mask, default `16'd0` in all TBs.
Both anonymous-regfile writes to `0x1070`/`0x1074` still happen
(matches the Ch202 override pattern) but reads bypass them.
### Plusarg-controlled experiment
`tb_ee_core_bios_smoke.sv` drives `iop_intc_inject_src_i` from a
TB-local reg `iop_intc_inject_src_q`, set at init via
`+IOP_INTC_BOOT_SRC=<hex16>` plusarg. Default `16'd0` so every
other TB stays byte-identical. To inject one source bit:
```
vvp .../tb_ee_core_bios_long.vvp +BIOS=... +CH55_INSTALL +IOP_INTC_BOOT_SRC=0001
```
## Verification
Full sim regression: **155 PASS / 0 FAIL** with Ch259 changes in
place.
## Phase 1 — baseline, no synthetic source
`make tb_ee_core_bios_long BIOS=...` (default
`IOP_INTC_BOOT_SRC = 0x0000`):
```
[ch218] CH259_INTC_TRANSACTIONS captured=98 (cap=256)
[ch218] summary: reads=56 writes=42 I_STAT(R=21 W=7) I_MASK(R=35 W=35) injected_src=0x0000
[ch218] verdict=intc_quiet
[ch217] CALLER_PASSES total=8 (treadmill persists)
retired_events: 24,029,051
```
BIOS executes the **same ~14-instruction INTC sequence every pass**
from a code region at `0x8003E370..0x8003E700` (BIOS-installed
runtime in EE RAM). Per pass:
```
pc=0x8003e370 LHU R+W ea=0x1F801074 (probe I_MASK)
pc=0x8003e37c LUI WR ea=0x1F801070 d=0 (W1C I_STAT no-op)
pc=0x8003e44c LHU RD ea=0x1F801070 d=0 (read I_STAT)
pc=0x8003e480 LHU RD ea=0x1F801070 d=0
pc=0x8003e484 LHU RD ea=0x1F801074 d=0
pc=0x8003e53c LHU RD ea=0x1F801070 d=0
pc=0x8003e540 LHU RD ea=0x1F801074 d=0
pc=0x8003e63c LHU RD ea=0x1F801074 d=0
pc=0x8003e644 BEQ WR ea=0x1F801074 d=0 (clear I_MASK)
pc=0x8003e700 ADDU WR ea=0x1F801074 d=1 (set mask bit 0)
pc=0x8003e63c LHU RD ea=0x1F801074 d=0 (?? — still 0)
pc=0x8003e644 BEQ WR ea=0x1F801074 d=0 (clear again)
pc=0x8003e700 ADDU WR ea=0x1F801074 d=8 (set mask bit 3)
```
**Notes:**
- I_STAT always reads 0 — no source asserted, no pending.
- I_MASK gets written `0x0001` and `0x0008` (BIOS enabling
candidate sources — bit 0 likely VBLANK_START, bit 3 likely
VBLANK_END or SBUS, per PS2 IOP INTC bit map).
- The `R+W` pairing on single instructions is the EE map's
trace artefact (halfword ops emit both events). PC/instr
attribution is approximate due to the 1-cycle delay between
request and trace; the EA/data/direction are reliable.
**Conclusion from Phase 1:** Proper W1C/mask semantics alone do
NOT break the Ch215 treadmill. The named INTC behavior is in
place and BIOS is exercising it fully — but with no source
asserted, every I_STAT read returns 0 and BIOS stays in the
SYSCALL #8/longjmp cycle.
## Phase 2 — `+IOP_INTC_BOOT_SRC=0001` (sticky bit 0)
Same binary, plusarg flipped:
```
[tb_ee_core_bios_smoke] Ch259 IOP_INTC_BOOT_SRC = 0x0001 (synthetic sticky source on I_STAT)
[ch218] CH259_INTC_TRANSACTIONS captured=98 (cap=256)
[ch218] summary: reads=56 writes=42 I_STAT(R=21 W=7) I_MASK(R=35 W=35) injected_src=0x0001
[ch218] verdict=intc_pending_observed
[ch217] CALLER_PASSES total=8 (treadmill PERSISTS)
retired_events: 24,029,051 (byte-identical to Phase 1)
```
**The sticky source IS propagating**`intc_pending_observed`
fires (verdict logic confirms at least one I_STAT read returned
non-zero with the inject mask). BUT:
- Ch217 verdict unchanged (`longjmp_return_repeats_due_to_static_state`)
- Pass count unchanged (8)
- Retire count unchanged to the cycle (24,029,051)
This matches the **fake-handler-path** outcome Codex flagged as
the risk of A-style hardcoding. **A pending I_STAT bit alone is
necessary but not sufficient.** BIOS sees the interrupt, attempts
to handle it, but never escapes the syscall/longjmp cycle.
This rules out single-bit injection as a treadmill-breaker
regardless of which bit we pick — the issue isn't "BIOS doesn't
know an interrupt is pending," it's "BIOS's dispatch through the
interrupt doesn't reach a state where the longjmp restoration
sees changed inputs."
## What we learned from Ch259
1. **Named INTC behavior is in place** at the EE-side view of the
IOP INTC pair. Future chapters can rely on it.
2. **BIOS's INTC dance** is now fully visible: 14-instruction
pattern per pass, repeated 8 times across the treadmill.
3. **The static state isn't INTC alone.** Even with a pending
bit asserted, the treadmill persists. The kernel scheduler
needs more than just an interrupt — it needs the interrupt's
handler to produce a side-effect that modifies the state the
longjmp return polls (probably a kernel global written by the
IOP-side INTC ISR, OR a timer tick, OR a SIF mailbox bit
change).
4. **Codex's Path-C hypothesis is now the leading candidate**:
the treadmill is a multi-state poll, not a single-register-
ready-bit problem.
## Three candidate Ch260 paths
**A. Observe the post-pending-bit code path.** Phase 2 has BIOS
seeing a pending bit but still looping. Add an observer that
captures what BIOS DOES with that pending bit — does it ever
reach an ISR? Does it write somewhere? Does it then poll a
*different* address whose value also needs to change? This is
another diagnostic chapter, not a fix.
**B. Model IOP-side activity.** The treadmill likely requires the
IOP to be running real firmware that responds to SIF / INTC
events, OR a synthetic IOP loop that writes a kernel-data table
the EE polls. Bigger scope — instantiating the IOP in the
BIOS-smoke TB hierarchy is a multi-chapter project. But this is
the most "correct" path.
**C. Defer and pivot.** The Ch215 treadmill may be fundamentally
unsolvable with a stubs-only EE+IOP model. Consider whether the
project is better served by accepting that real BIOS won't boot
in this configuration and focusing on:
- Continuing the BIOS-less synthetic demo line (Ch251+ visible
on silicon, already shipping)
- Building the IOP-side execution scaffolding as a separate
arc with its own minimal-firmware target
- Returning to BIOS bring-up after the IOP arc has produced a
"real-enough" IOP responder
## What changed in the tree
- `rtl/ee/ee_bootstrap_mmio_stub.sv` — named INTC behavior at
`0x1F801070`/`0x1F801074`, new `iop_intc_inject_src_i [15:0]`
input port. Ch202 (0x1814) and Ch258 (0x10F0) hardcodes intact.
- `sim/tb/ee/tb_ee_bootstrap_mmio.sv` — wires the new port to 0.
- `sim/tb/integration/tb_ee_core_bios_smoke.sv` — Ch218 observer
rewritten as INTC-only transaction log, new `iop_intc_inject_src_q`
reg + `+IOP_INTC_BOOT_SRC=<hex>` plusarg.
- This file — Ch259 outcome.
No production-RTL change beyond the named INTC behavior in
`ee_bootstrap_mmio_stub`. Hardware demo path untouched.
## Decision needed from Codex
1. Ch260 path? (A / B / C / something else)
2. Trim or remove the Ch218 observer now? Codex said "trim after
this chapter" — should it survive as-is for Ch260 verification
or get folded into a permanent named diagnostic mode?
3. Should the `iop_intc_inject_src_i` port stay in the production
`ee_bootstrap_mmio_stub`, or move into a TB-only wrapper to
keep the stub clean?
Pausing all code changes until your call.