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
+269
View File
@@ -0,0 +1,269 @@
// retroDE_ps2 — ee_bootstrap_mmio_stub
//
// Latched-register-file stub for the EE "bootstrap MMIO" window at
// physical `0x1F80_0000 - 0x1F80_FFFF` (64 KiB). Covers the real
// PS2 MCH (memory controller), SBUS gateway, and RDRAM init
// registers the BIOS touches very early in boot. This is the
// narrowest thing that closes the poisoned-dataflow hole found by
// chapter 7.99: before this module existed, the EE map returned
// `0xDEADBEEF` for every CPU read in this window, and the BIOS
// laundered that poison into a data structure whose later
// traversal wedged the core forever.
//
// Semantics (deliberately simple, not architecturally accurate):
// - Full window is a 16 KiB word-addressed register file; all
// registers reset/init to 0.
// - Writes latch per-byte: for each `wr_be[i]` that is asserted,
// `regs[addr[15:2]][8*i +: 8] <= wr_data[8*i +: 8]`. Untouched
// byte lanes preserve their existing value. This makes SB/SH
// write-through-this-window safe — prior chapters added SB/SH
// for BIOS progress, and without be-aware latching a sub-word
// store here would clobber the other three (or two) bytes.
// - Reads return the currently-latched value, one-cycle latency,
// matching the rest of the stub ecosystem.
// - No side effects, no per-register behavior (no ready-bit
// auto-set, no interrupt generation, no state machines).
//
// That keeps BIOS read/modify/write sequences self-consistent:
// if the BIOS reads reg X, ORs a bit, writes back, it sees the
// merged value on the next read. It does NOT emulate real
// hardware semantics (e.g. status bits that flip on their own,
// interrupt latches, FIFO behavior). If the BIOS tripwire-depends
// on any of that, it will reveal itself the same way the 0x14B4
// linked-list wedge did — via a new diagnostic signal, handled
// in a future chapter.
//
// Trace:
// Per-access event on SUBSYS_MEM with the region tag
// `REGION_EE_MISC_MMIO = 9`. arg0 is the 16-bit offset within
// the window (not the full 32-bit address — the map's own
// trace already carries the full address; the stub's finer
// trace carries the offset so downstream analysis can see
// which register was touched without having to mask). arg1 is
// the data (write data, or the value being returned on read).
// arg3 is the region constant. flags bit 0 = write.
//
// Size cost: 16384 × 32 bits ≈ 64 KiB of sim memory. Negligible.
`timescale 1ns/1ps
module ee_bootstrap_mmio_stub
import trace_pkg::*;
#(
// Ch202 — narrow "ready" return for offset 0x1814. Pre-Ch201 the
// window returned the latched register value (which initialises to
// 0); the BIOS at PC=0xBFC4FB04..FB30 polls this address waiting
// for ($read & $mask) != 0 and our zero return left it spinning.
// Default = 32'hFFFFFFFF satisfies any non-zero mask the BIOS may
// hold in $a0 — wider than a real PS2 GPUSTAT (typical idle =
// 0x1C00_0000), but the BIOS has not been observed to USE the
// value beyond the bit-test so the wider satisfaction is safe.
// A future chapter can narrow this if a side-effect is observed.
parameter logic [31:0] MMIO_1814_RDY_VALUE = 32'hFFFF_FFFF,
// Ch258 — IOP DMAC PCR realism stub. The IOP DMAC Priority Control
// Register lives at phys 0x1F8010F0 (= EE kseg1 0xBF8010F0). Real
// PS1/IOP hardware resets this to 0x07654321 (priority 1 for ch0,
// 2 for ch1, ... 7 for ch6, with bit[31:24]=0x07 as the enable
// mask). Ch218 observer captured BIOS reading this address three
// times during the Ch215 longjmp treadmill (PC=0xbfc4d2cc /
// 0xbfc4d2dc / 0xbfc4d350), all returning 0 from our latched-zero
// stub. Whether the zero return is the cause of the treadmill or
// an incidental noise read is open — Ch258's job is to flip the
// PCR to its real reset value and re-observe.
//
// This is a REALISM STUB, not a fix. We are not modelling the
// IOP DMA channel priority semantics; we are just declining to
// return poison-zero for a named hardware register with a known
// reset value. If BIOS escapes the Ch215 treadmill after this
// change, great. If it does not, Ch258 closes with "PCR was not
// the gate" and we name the next observed blocker.
parameter logic [31:0] MMIO_10F0_PCR_VALUE = 32'h0765_4321
)
(
input logic clk,
input logic rst_n,
// Write port
input logic reg_wr_en,
input logic [15:0] reg_wr_addr,
input logic [31:0] reg_wr_data,
input logic [3:0] reg_wr_be,
// Read port — 1-cycle latency, matches rest of stub ecosystem
input logic reg_rd_en,
input logic [15:0] reg_rd_addr,
output logic [31:0] reg_rd_data,
output logic reg_rd_valid,
// Ch259 / Ch260 — DIAGNOSTIC source-injection port for the named
// IOP INTC view at 0x1F801070/0x1F801074. DEFAULT IS ZERO in every
// existing instantiation (tb_ee_bootstrap_mmio.sv and
// tb_ee_core_bios_smoke.sv both tie this to 16'd0 unless the
// BIOS-long TB's +IOP_INTC_BOOT_SRC plusarg overrides it).
//
// When non-zero, each set bit is ORed into I_STAT every cycle so
// the assertion survives W1C clears (matches the "real device
// asserts the line until serviced" shape, not a one-shot pulse).
//
// This port exists ONLY as a controlled diagnostic knob. Ch259
// closed the BIOS-mmio-probe arc with the finding that single
// synthetic source bits do not break the Ch215 treadmill — the
// multi-state IOP/SBUS/kernel activity is needed instead. Any
// future use of this port should be similarly scoped (TB-driven,
// documented intent, default-zero on instantiation).
input logic [15:0] iop_intc_inject_src_i,
// Trace
output logic ev_valid,
output subsys_e ev_subsys,
output event_e ev_event,
output logic [63:0] ev_arg0,
output logic [63:0] ev_arg1,
output logic [63:0] ev_arg2,
output logic [63:0] ev_arg3,
output logic [31:0] ev_flags
);
localparam int WORDS = 16384; // 64 KiB / 4
localparam logic [63:0] REGION_EE_MISC_MMIO = 64'd9;
logic [31:0] regs [0:WORDS-1];
initial begin
for (int i = 0; i < WORDS; i++) regs[i] = 32'd0;
end
logic [13:0] wr_idx;
logic [13:0] rd_idx;
assign wr_idx = reg_wr_addr[15:2];
assign rd_idx = reg_rd_addr[15:2];
// Per-byte write latch — honors reg_wr_be so SB/SH through this
// window preserves the untouched byte lanes instead of clobbering
// the whole 32-bit register.
always_ff @(posedge clk) begin
if (rst_n && reg_wr_en) begin
if (reg_wr_be[0]) regs[wr_idx][ 7: 0] <= reg_wr_data[ 7: 0];
if (reg_wr_be[1]) regs[wr_idx][15: 8] <= reg_wr_data[15: 8];
if (reg_wr_be[2]) regs[wr_idx][23:16] <= reg_wr_data[23:16];
if (reg_wr_be[3]) regs[wr_idx][31:24] <= reg_wr_data[31:24];
end
end
// Read — 1-cycle latency. Ch202: offset 0x1814 ignores the latched
// register and returns MMIO_1814_RDY_VALUE so the BIOS bit-test
// poll satisfies (read & mask) != 0 on the first read. Writes to
// 0x1814 still latch into regs[]; a future chapter can promote
// 0x1814 to a true read-write register if BIOS-write semantics
// matter, but the current observed behavior is read-only-status.
// Ch258 adds the same shape for offset 0x10F0 (IOP DMAC PCR).
// Ch259 promotes 0x1070 (IOP INTC I_STAT) and 0x1074 (I_MASK)
// OUT of the anonymous regfile into named INTC behavior — W1C
// on STAT writes, plain-write on MASK writes, sticky source
// injection from `iop_intc_inject_src_i`. Matches the existing
// `rtl/intc/intc_stub.sv` shape exactly so the EE-side view of
// the IOP INTC behaves like the IOP-side view does.
localparam logic [13:0] OFFSET_1814_WIDX = 14'h0605; // 0x1814 >> 2 (1541)
localparam logic [13:0] OFFSET_10F0_WIDX = 14'h043C; // 0x10F0 >> 2 (1084)
localparam logic [13:0] OFFSET_1070_WIDX = 14'h041C; // 0x1070 >> 2 (1052)
localparam logic [13:0] OFFSET_1074_WIDX = 14'h041D; // 0x1074 >> 2 (1053)
// Ch259 — named IOP INTC state. Independent of the anonymous
// regs[] (writes to 0x1070/0x1074 still update regs[] via the
// generic per-byte latch above, but reads bypass it for these
// offsets, matching the Ch202/Ch258 override pattern).
logic [15:0] iop_intc_stat_q;
logic [15:0] iop_intc_mask_q;
wire [15:0] iop_intc_stat_w1c_mask =
(reg_wr_en && wr_idx == OFFSET_1070_WIDX && (&reg_wr_be))
? reg_wr_data[15:0] : 16'd0;
wire iop_intc_mask_wr_en =
reg_wr_en && wr_idx == OFFSET_1074_WIDX && (&reg_wr_be);
always_ff @(posedge clk) begin
if (!rst_n) begin
iop_intc_stat_q <= 16'd0;
iop_intc_mask_q <= 16'd0;
end else begin
// I_STAT: W1C of cleared bits, OR'd with sticky injection.
// Assertion-wins on same-cycle W1C+source collision —
// matches `intc_stub.sv` lines ~102-110 so we don't
// swallow an interrupt that's still held.
iop_intc_stat_q <= (iop_intc_stat_q & ~iop_intc_stat_w1c_mask)
| iop_intc_inject_src_i;
if (iop_intc_mask_wr_en)
iop_intc_mask_q <= reg_wr_data[15:0];
end
end
wire [31:0] iop_intc_stat_read = {16'd0, iop_intc_stat_q | iop_intc_inject_src_i};
wire [31:0] iop_intc_mask_read = {16'd0, iop_intc_mask_q};
always_ff @(posedge clk) begin
if (!rst_n) begin
reg_rd_data <= 32'd0;
reg_rd_valid <= 1'b0;
end else begin
reg_rd_valid <= reg_rd_en;
if (reg_rd_en) begin
if (rd_idx == OFFSET_1814_WIDX)
reg_rd_data <= MMIO_1814_RDY_VALUE;
else if (rd_idx == OFFSET_10F0_WIDX)
reg_rd_data <= MMIO_10F0_PCR_VALUE;
else if (rd_idx == OFFSET_1070_WIDX)
reg_rd_data <= iop_intc_stat_read;
else if (rd_idx == OFFSET_1074_WIDX)
reg_rd_data <= iop_intc_mask_read;
else
reg_rd_data <= regs[rd_idx];
end
end
end
// Trace emission — one event per cycle, write wins on same-cycle
// collision (mirrors the rd/wr_en mutual-exclusion at the map level;
// this is defensive for mechanical safety).
always_ff @(posedge clk) begin
if (!rst_n) begin
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_MEM;
ev_event <= EV_WRITE;
ev_arg0 <= 64'd0;
ev_arg1 <= 64'd0;
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else if (reg_wr_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_MEM;
ev_event <= EV_WRITE;
ev_arg0 <= {48'd0, reg_wr_addr};
ev_arg1 <= {32'd0, reg_wr_data};
ev_arg2 <= 64'd0;
ev_arg3 <= REGION_EE_MISC_MMIO;
ev_flags <= 32'h0000_0001;
end else if (reg_rd_en) begin
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_MEM;
ev_event <= EV_READ;
ev_arg0 <= {48'd0, reg_rd_addr};
ev_arg1 <= (rd_idx == OFFSET_1814_WIDX)
? {32'd0, MMIO_1814_RDY_VALUE}
: (rd_idx == OFFSET_10F0_WIDX)
? {32'd0, MMIO_10F0_PCR_VALUE}
: (rd_idx == OFFSET_1070_WIDX)
? {32'd0, iop_intc_stat_read}
: (rd_idx == OFFSET_1074_WIDX)
? {32'd0, iop_intc_mask_read}
: {32'd0, regs[rd_idx]};
ev_arg2 <= 64'd0;
ev_arg3 <= REGION_EE_MISC_MMIO;
ev_flags <= 32'd0;
end else begin
ev_valid <= 1'b0;
end
end
endmodule : ee_bootstrap_mmio_stub