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
+44
View File
@@ -0,0 +1,44 @@
# rtl/debug
Trace taps, observability modules, and first-class debug infrastructure.
This directory is intentionally first-class per the debug/validation strategy
in `docs/contracts/validation.md`. Nothing here is ornamental; stubs and real
blocks alike depend on it.
## Wave 1 contents
- `trace_pkg.sv` — shared types (`subsys_e`, `event_e`) and string renderers
used by all trace producers and `trace_sink_stub`.
- `trace_sink_stub.sv` — simulation-only text trace writer. One instance per
output file; each Wave 1 stub wires its event port to its own sink. See
`docs/decisions/0000-trace-format.md` for format rationale.
## Usage pattern for Wave 1 testbenches
```systemverilog
trace_sink_stub #(
.FILENAME ("ee.trace"),
.SINK_LABEL("ee_fetch")
) u_trace_ee (
.clk (clk),
.rst_n (rst_n),
.ev_valid (dut_ev_valid),
.ev_subsys(dut_ev_subsys),
.ev_event (dut_ev_event),
.ev_arg0 (dut_ev_arg0),
.ev_arg1 (dut_ev_arg1),
.ev_arg2 (dut_ev_arg2),
.ev_arg3 (dut_ev_arg3),
.ev_flags (dut_ev_flags)
);
```
## Notes
- Cycle counter is internal to the sink and advances on `clk` while `rst_n`
is high. Cross-clock correlation is a later-wave concern.
- `ev_flags == 0` renders as `-` in the trace line; any non-zero value is
printed as an 8-hex-digit field.
- Event/subsystem codes are globally unique in Wave 1 (not per-subsystem).
Revisit if the namespace gets crowded.
+166
View File
@@ -0,0 +1,166 @@
// retroDE_ps2 — trace package
//
// Defines the shared trace vocabulary used by Wave 1 stubs. Kept small on
// purpose: subsystem IDs, event codes, and string renderers only. See
// docs/decisions/0000-trace-format.md and docs/stub_module_plan.md.
//
// Contract:
// trace line = cycle subsystem event arg0 arg1 arg2 arg3 flags
//
// Event codes are globally unique (not per-subsystem) in Wave 1 to keep the
// renderer trivial. Revisit if the namespace gets crowded.
`ifndef RETRODE_PS2_TRACE_PKG_SV
`define RETRODE_PS2_TRACE_PKG_SV
`timescale 1ns/1ps
package trace_pkg;
typedef enum logic [3:0] {
SUBSYS_EE = 4'h0,
SUBSYS_MEM = 4'h1,
SUBSYS_GS = 4'h2,
SUBSYS_INTC = 4'h3,
SUBSYS_SIF = 4'h4,
SUBSYS_DMAC = 4'h5,
SUBSYS_IOP = 4'h6,
SUBSYS_GIF = 4'h7,
SUBSYS_PLAT = 4'h8,
SUBSYS_OTHER = 4'hF
} subsys_e;
typedef enum logic [7:0] {
EV_RESET = 8'h00,
EV_IFETCH = 8'h01,
EV_READ = 8'h02,
EV_WRITE = 8'h03,
EV_UNMAPPED = 8'h04,
EV_IRQ = 8'h05,
EV_MODE = 8'h06,
EV_BGCOLOR = 8'h07,
// Wave 2 additions — DMAC / GIF / GS write-path visibility
// (see docs/wave2_dma_gif_plan.md).
EV_DMA_CFG = 8'h08,
EV_DMA_START = 8'h09,
EV_DMA_BEAT = 8'h0A,
EV_DMA_DONE = 8'h0B,
EV_GIFTAG = 8'h0C,
EV_GS_WRITE = 8'h0D,
// Ch76 — GS primitive-observer "primitive complete" pulse.
// Fired by gs_stub when an XYZ2/XYZF2 vertex commit closes a
// discrete primitive (POINT / LINE / TRI / SPRITE) per the
// currently-latched PRIM[2:0]. arg0=prim_type, arg1=vert
// threshold, arg2=cumulative prim count after this draw,
// arg3=closing vertex data. No rasterization yet.
EV_PRIM_DRAW = 8'h0E,
EV_OTHER = 8'hFF
} event_e;
// ------------------------------------------------------------------
// Ch81 — structured GIF/GS field decoders
//
// The raw 64-bit XYZ2 / XYZF2 / RGBAQ payloads are an awkward
// contract for the next layer of the pipeline (rasterizer or
// pixel emit). These struct types carry the same data already
// unpacked into channel components so a consumer doesn't have
// to re-derive the bit slices.
//
// XYZ2 (PCSX2 GSRegs.h: bits[15:0]=X, [31:16]=Y, [63:32]=Z 32-bit)
// XYZF2 (PCSX2 GSRegs.h: bits[15:0]=X, [31:16]=Y, [55:32]=Z 24-bit,
// [63:56]=F fog byte)
// RGBAQ (bits[7:0]=R, [15:8]=G, [23:16]=B, [31:24]=A, [63:32]=Q float)
//
// X and Y are PS2 12.4 fixed-point screen coordinates (top 12
// bits = integer pixel, low 4 bits = sub-pixel). Z is treated
// as opaque in this package — depth interpretation depends on
// the GS framebuffer/zbuffer config, which the recognition
// layer doesn't model. Q is the texture-coordinate divisor
// (IEEE single-precision float); we carry it verbatim.
//
// is_xyzf2 records the source format so a consumer can
// disambiguate the 24-bit-Z + 8-bit-fog packing from the full
// 32-bit-Z packing without re-reading the original reg#.
// ------------------------------------------------------------------
typedef struct packed {
logic is_xyzf2; // 1 = XYZF2 source, 0 = XYZ2
logic [7:0] fog; // valid iff is_xyzf2; else 0
logic [31:0] z; // 32-bit (XYZ2) or zero-extended 24-bit (XYZF2)
logic [15:0] y; // 12.4 fixed-point screen Y
logic [15:0] x; // 12.4 fixed-point screen X
} vertex_t;
typedef struct packed {
logic [31:0] q; // texture-coord divisor (IEEE float)
logic [7:0] a;
logic [7:0] b;
logic [7:0] g;
logic [7:0] r;
} color_t;
function automatic vertex_t decode_vertex(input logic [63:0] data,
input logic is_xyzf2);
vertex_t v;
v.x = data[15:0];
v.y = data[31:16];
v.is_xyzf2 = is_xyzf2;
if (is_xyzf2) begin
v.z = {8'd0, data[55:32]}; // zero-extend 24 bits
v.fog = data[63:56];
end else begin
v.z = data[63:32];
v.fog = 8'd0;
end
return v;
endfunction
function automatic color_t decode_color(input logic [63:0] data);
color_t c;
c.r = data[7:0];
c.g = data[15:8];
c.b = data[23:16];
c.a = data[31:24];
c.q = data[63:32];
return c;
endfunction
function automatic string subsys_str(input subsys_e s);
case (s)
SUBSYS_EE: return "EE";
SUBSYS_MEM: return "MEM";
SUBSYS_GS: return "GS";
SUBSYS_INTC: return "INTC";
SUBSYS_SIF: return "SIF";
SUBSYS_DMAC: return "DMAC";
SUBSYS_IOP: return "IOP";
SUBSYS_GIF: return "GIF";
SUBSYS_PLAT: return "PLAT";
default: return "OTHER";
endcase
endfunction
function automatic string event_str(input event_e e);
case (e)
EV_RESET: return "RESET";
EV_IFETCH: return "IFETCH";
EV_READ: return "READ";
EV_WRITE: return "WRITE";
EV_UNMAPPED: return "UNMAPPED";
EV_IRQ: return "IRQ";
EV_MODE: return "MODE";
EV_BGCOLOR: return "BGCOLOR";
EV_DMA_CFG: return "DMA_CFG";
EV_DMA_START: return "DMA_START";
EV_DMA_BEAT: return "DMA_BEAT";
EV_DMA_DONE: return "DMA_DONE";
EV_GIFTAG: return "GIFTAG";
EV_GS_WRITE: return "GS_WRITE";
EV_PRIM_DRAW: return "PRIM_DRAW";
default: return "OTHER";
endcase
endfunction
endpackage : trace_pkg
`endif // RETRODE_PS2_TRACE_PKG_SV
+88
View File
@@ -0,0 +1,88 @@
// retroDE_ps2 — trace_sink_stub
//
// Simulation-only text trace writer for Wave 1 stubs.
//
// Purpose, owns, success condition, replacement path: see
// docs/stub_module_plan.md (Wave 1, item 1)
// docs/contracts/validation.md
// docs/decisions/0000-trace-format.md
//
// Interface shape:
// - One sink instance per output file. A testbench instantiates multiple
// sinks (one per stub under test) and wires each stub's event port to
// its own sink. Offline tooling merges files by cycle when needed.
// - The cycle counter is internal and advances on clk while rst_n is high.
// Multi-clock-domain correlation is a later-wave concern.
//
// Line format (docs/decisions/0000):
// cycle subsys event arg0 arg1 arg2 arg3 flags
// where flags is rendered as `-` when zero.
`timescale 1ns/1ps
module trace_sink_stub
import trace_pkg::*;
#(
parameter string FILENAME = "trace.txt",
parameter int SCHEMA_VERSION = 1,
parameter string SINK_LABEL = "trace"
) (
input logic clk,
input logic rst_n,
input logic ev_valid,
input subsys_e ev_subsys,
input event_e ev_event,
input logic [63:0] ev_arg0,
input logic [63:0] ev_arg1,
input logic [63:0] ev_arg2,
input logic [63:0] ev_arg3,
input logic [31:0] ev_flags
);
integer fd;
longint unsigned cycle_count;
initial begin
fd = $fopen(FILENAME, "w");
if (fd == 0) begin
$fatal(1, "[trace_sink_stub %0s] cannot open %0s", SINK_LABEL, FILENAME);
end
$fdisplay(fd, "# retroDE_ps2 trace, schema v%0d, sink=%0s",
SCHEMA_VERSION, SINK_LABEL);
$fdisplay(fd, "# columns: cycle subsystem event arg0 arg1 arg2 arg3 flags");
cycle_count = 64'd0;
end
always_ff @(posedge clk) begin
if (!rst_n) begin
cycle_count <= 64'd0;
end else begin
cycle_count <= cycle_count + 64'd1;
if (ev_valid) begin
if (ev_flags == 32'd0) begin
$fdisplay(fd,
"%0d %0s %0s 0x%016h 0x%016h 0x%016h 0x%016h -",
cycle_count,
subsys_str(ev_subsys),
event_str(ev_event),
ev_arg0, ev_arg1, ev_arg2, ev_arg3);
end else begin
$fdisplay(fd,
"%0d %0s %0s 0x%016h 0x%016h 0x%016h 0x%016h 0x%08h",
cycle_count,
subsys_str(ev_subsys),
event_str(ev_event),
ev_arg0, ev_arg1, ev_arg2, ev_arg3,
ev_flags);
end
end
end
end
final begin
if (fd != 0) $fclose(fd);
end
endmodule : trace_sink_stub