Files
retroDE_ps2/sim/tb/top/tb_gs_tile_spill_lpddr.sv
thejayman77 ec82764bef 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>
2026-06-29 20:10:50 -04:00

260 lines
16 KiB
Systemverilog

// retroDE_ps2 — tb_gs_tile_spill_lpddr (Ch323 Brick 2 — REAL LPDDR-path integration proof)
//
// The gap that let the board bugs through: tb_gs_tile_spill_reload (Brick 1) forced
// tile_reload_ready=1 and served an on-chip backing — it NEVER exercised the real
// gs_z_flush_writer / gs_tile_reload / EMIF round-trip, and gs_stub PULSES reload_start
// while gs_tile_reload edge-detects it (pulse-vs-toggle). This TB wires the SAME modules
// the de25 wires — gs_stub -> 2x gs_z_flush_writer (color/Z) -> behavioral 256-bit EMIF
// -> gs_tile_reload -> gs_stub serve port — across an ASYNC emif clock, and proves the
// two-batch depth result (region A = red color1, region B = blue color2) THROUGH THE REAL
// DATA PATH. Small LPDDR bases (C=0, Z=0x1000) keep the behavioral mem tiny.
`timescale 1ns/1ps
module tb_gs_tile_spill_lpddr;
localparam int H_ACTIVE = 16, V_ACTIVE = 16;
localparam logic [31:0] COLOR1 = 32'hFF0000FF, COLOR2 = 32'hFFFF0000; // red / blue (ABGR)
localparam [29:0] C_BASE = 30'h0000_0000, Z_BASE = 30'h0000_1000;
logic clk=0; always #5 clk = ~clk; // design clock
logic eclk=0; always #3 eclk = ~eclk; // emif clock (faster, async)
logic rst_n, erst_n, core_go;
logic [7:0] r,g,b; logic hsync,vsync,de;
logic core_halt, dma_done_seen, frame_seen, raster_overflow, frame_toggle, dma_done_toggle;
logic z_flush_emit; logic [31:0] z_flush_addr, z_flush_data;
logic flush_emit; logic [31:0] flush_addr, flush_color32;
logic cflush_emit; logic [31:0] cflush_addr, cflush_data;
logic reload_start; logic [7:0] tile_reload_raddr; logic [29:0] reload_base;
logic [31:0] tile_reload_color, tile_reload_z; logic tile_reload_ready;
logic [2:0] tile_phase_o;
top_psmct32_raster_demo_bram #(
.H_ACTIVE(H_ACTIVE), .V_ACTIVE(V_ACTIVE),
.VRAM_BYTES(8*1024), .PSMCT32_SWIZZLE(1'b0),
.COMBINED_TAZ(1'b1), .TILE_LOCAL(1'b1), .TILE_SPILL_ENABLE(1'b1)
) dut (
.clk(clk), .rst_n(rst_n), .core_go(core_go),
.r(r), .g(g), .b(b), .hsync(hsync), .vsync(vsync), .de(de),
.core_halt(core_halt), .dma_done_seen(dma_done_seen),
.frame_seen(frame_seen), .raster_overflow(raster_overflow),
.frame_toggle(frame_toggle), .dma_done_toggle(dma_done_toggle),
.joy_a_pressed_i(1'b0), .joy_b_pressed_i(1'b0),
.flush_emit_o(flush_emit), .flush_addr_o(flush_addr), .flush_color32_o(flush_color32),
.tile_color_flush_emit_o(cflush_emit), .tile_color_flush_addr_o(cflush_addr), .tile_color_flush_data_o(cflush_data),
.z_flush_emit_o(z_flush_emit), .z_flush_addr_o(z_flush_addr), .z_flush_data_o(z_flush_data),
.reload_start_o(reload_start), .tile_reload_raddr_o(tile_reload_raddr), .reload_base_o(reload_base),
.tile_reload_ready_i(tile_reload_ready), .tile_reload_color_i(tile_reload_color),
.tile_reload_z_i(tile_reload_z), .tile_phase_o(tile_phase_o)
);
// ================= REAL spill writers (color + Z) =================
wire [29:0] cw_aa, zw_aa; wire [1:0] cw_ab, zw_ab; wire [6:0] cw_id, zw_id;
wire [7:0] cw_al, zw_al; wire [2:0] cw_as, zw_as; wire cw_av, zw_av, cw_ar, zw_ar;
wire [255:0] cw_wd, zw_wd; wire [31:0] cw_ws, zw_ws; wire cw_wl, zw_wl, cw_wv, zw_wv, cw_wr, zw_wr;
wire [1:0] cw_br, zw_br; wire cw_bv, zw_bv, cw_brd, zw_brd;
wire [31:0] c_beats, c_errs, z_beats, z_errs; wire c_ovf, z_ovf;
gs_z_flush_writer #(.Z_BASE(C_BASE), .FB_BASE(30'h8000), .FB_BYTES(32'h1000),
.TEX_BASE(30'h9000), .TEX_BYTES(32'h1000)) u_color (
.gs_clk(clk), .gs_rst_n(rst_n), .enable(1'b1), .trace_clear(1'b0),
.z_flush_emit(cflush_emit), .z_flush_addr(cflush_addr), .z_flush_data(cflush_data),
.z_write_beats(c_beats), .z_wr_errs(c_errs), .fifo_overflow(c_ovf),
.axi_clk(eclk), .axi_rst_n(erst_n),
.awaddr(cw_aa), .awburst(cw_ab), .awid(cw_id), .awlen(cw_al), .awsize(cw_as),
.awvalid(cw_av), .awready(cw_ar), .wdata(cw_wd), .wstrb(cw_ws), .wlast(cw_wl),
.wvalid(cw_wv), .wready(cw_wr), .bresp(cw_br), .bvalid(cw_bv), .bready(cw_brd)
);
gs_z_flush_writer #(.Z_BASE(Z_BASE), .FB_BASE(30'h8000), .FB_BYTES(32'h1000),
.TEX_BASE(30'h9000), .TEX_BYTES(32'h1000)) u_z (
.gs_clk(clk), .gs_rst_n(rst_n), .enable(1'b1), .trace_clear(1'b0),
.z_flush_emit(z_flush_emit), .z_flush_addr(z_flush_addr), .z_flush_data(z_flush_data),
.z_write_beats(z_beats), .z_wr_errs(z_errs), .fifo_overflow(z_ovf),
.axi_clk(eclk), .axi_rst_n(erst_n),
.awaddr(zw_aa), .awburst(zw_ab), .awid(zw_id), .awlen(zw_al), .awsize(zw_as),
.awvalid(zw_av), .awready(zw_ar), .wdata(zw_wd), .wstrb(zw_ws), .wlast(zw_wl),
.wvalid(zw_wv), .wready(zw_wr), .bresp(zw_br), .bvalid(zw_bv), .bready(zw_brd)
);
// ================= REAL reload engine =================
wire [29:0] rl_aa; wire [1:0] rl_ab; wire [6:0] rl_id; wire [7:0] rl_al; wire [2:0] rl_as;
wire rl_av, rl_ar; wire [255:0] rl_rd; wire [1:0] rl_rr; wire rl_rl, rl_rv, rl_rrdy;
wire [31:0] rl_cbeats, rl_zbeats, rl_rderrs;
gs_tile_reload #(.COLOR_BASE(C_BASE), .Z_BASE(Z_BASE), .TILE_W(16), .TILE_H(16),
.STRIDE_BYTES(256), .ROW_BEATS(2), .COLOR_W(32)) u_reload (
.axi_clk(eclk), .axi_rst_n(erst_n),
.reload_start(reload_start), .reload_base(reload_base), .reload_done(),
.color_beats(rl_cbeats), .z_beats(rl_zbeats), .rd_errs(rl_rderrs),
.araddr(rl_aa), .arburst(rl_ab), .arid(rl_id), .arlen(rl_al), .arsize(rl_as),
.arvalid(rl_av), .arready(rl_ar), .rdata(rl_rd), .rresp(rl_rr), .rlast(rl_rl),
.rvalid(rl_rv), .rready(rl_rrdy),
.serve_clk(clk), .raddr(tile_reload_raddr),
.color_o(tile_reload_color), .z_o(tile_reload_z), .reload_ready(tile_reload_ready)
);
// ================= behavioral 256-bit EMIF (shared mem; eclk) =================
// Two independent write slaves (color/Z regions disjoint) + one read slave (reload).
logic [255:0] mem [0:1023];
// ---- color write slave ----
typedef enum logic [1:0] {WA,WD,WB} ws_t;
ws_t cws; logic [29:0] cbeat;
assign cw_ar = (cws==WA);
assign cw_wr = (cws==WD);
assign cw_bv = (cws==WB); assign cw_br = 2'b00;
always_ff @(posedge eclk or negedge erst_n) begin
if (!erst_n) cws<=WA;
else case (cws)
WA: if (cw_av) begin cbeat<=cw_aa[29:5]; cws<=WD; end
WD: if (cw_wv) begin for (int by=0;by<32;by++) if (cw_ws[by]) mem[cbeat[9:0]][by*8+:8]<=cw_wd[by*8+:8]; cws<=WB; end
WB: if (cw_brd) cws<=WA;
endcase
end
// ---- Z write slave ----
ws_t zws; logic [29:0] zbeat;
assign zw_ar = (zws==WA);
assign zw_wr = (zws==WD);
assign zw_bv = (zws==WB); assign zw_br = 2'b00;
always_ff @(posedge eclk or negedge erst_n) begin
if (!erst_n) zws<=WA;
else case (zws)
WA: if (zw_av) begin zbeat<=zw_aa[29:5]; zws<=WD; end
WD: if (zw_wv) begin for (int by=0;by<32;by++) if (zw_ws[by]) mem[zbeat[9:0]][by*8+:8]<=zw_wd[by*8+:8]; zws<=WB; end
WB: if (zw_brd) zws<=WA;
endcase
end
// ---- reload read slave (a few-cycle latency to exercise async timing) ----
typedef enum logic [1:0] {RA,RL,RV} rs_t;
rs_t rs; logic [29:0] rbeat; logic [2:0] rlat;
assign rl_ar = (rs==RA);
assign rl_rv = (rs==RV); assign rl_rl = (rs==RV); assign rl_rr = 2'b00;
assign rl_rd = mem[rbeat[9:0]];
always_ff @(posedge eclk or negedge erst_n) begin
if (!erst_n) begin rs<=RA; rlat<=0; end
else case (rs)
RA: if (rl_av) begin rbeat<=rl_aa[29:5]; rlat<=3'd4; rs<=RL; end
RL: if (rlat==0) rs<=RV; else rlat<=rlat-1'b1;
RV: if (rl_rrdy) rs<=RA;
endcase
end
// ================= scanout capture (from Brick-1 TB) =================
logic [7:0] cap_r[0:V_ACTIVE-1][0:H_ACTIVE-1], cap_g[0:V_ACTIVE-1][0:H_ACTIVE-1], cap_b[0:V_ACTIVE-1][0:H_ACTIVE-1];
bit capture_armed; logic [31:0] hcnt_d, vcnt_d;
always_ff @(posedge clk) begin
if (!rst_n) begin hcnt_d<=0; vcnt_d<=0; end
else begin hcnt_d<=32'(dut.u_pcrtc.hcnt); vcnt_d<=32'(dut.u_pcrtc.vcnt); end
end
always_ff @(posedge clk)
if (rst_n && capture_armed && de && (vcnt_d<V_ACTIVE) && (hcnt_d<H_ACTIVE)) begin
cap_r[vcnt_d][hcnt_d]<=r; cap_g[vcnt_d][hcnt_d]<=g; cap_b[vcnt_d][hcnt_d]<=b; end
function automatic logic [23:0] px(input int y, input int x); px={cap_b[y][x],cap_g[y][x],cap_r[y][x]}; endfunction
// reload handshake witnesses
bit saw_reload_ready, saw_reload_start;
always_ff @(posedge clk) begin
if (!rst_n) begin saw_reload_ready<=0; saw_reload_start<=0; end
else begin
if (reload_start) saw_reload_start<=1'b1;
if (tile_reload_ready) saw_reload_ready<=1'b1;
end
end
// ---- DEBUG: trace region-A pixel (1,1) Z through the chain ----
// tile-local index for (lx=1,ly=1) = ly*16+lx = 17. screen Z byte = (1*64+1)*4=0x104.
// Z block in mem = (Z_BASE+0x104)>>5 = 0x88, lane = 0x104[4:2] = 1 (bits 63:32).
int reload_pass, zflush_evict;
logic [31:0] zA_spilled_data [0:3]; // z_flush_data emitted for region-A screen addr 0x104, per eviction
logic zA_spilled_seen [0:3];
logic [31:0] zA_reload_seen [0:3]; // tile_reload_z_i written into tile RAM idx 17, per reload pass
logic [31:0] zA_mem_at_reload [0:3];
bit mem_has_8000; // did 0x8000 ever appear anywhere in the Z region after batch1?
// emif-domain: each time the reload fill completes a read of the region-A Z block (0x88),
// capture what rdata word1 it got + how many fills ran.
logic [31:0] fillZ_blk88 [0:7]; int fill_blk88_n; int fill_done_edges; logic rdone_d;
always_ff @(posedge eclk) begin
if (!erst_n) begin fill_blk88_n<=0; fill_done_edges<=0; rdone_d<=0; end
else begin
if (rl_rv && rl_rrdy && rl_aa==30'h0000_1100 && fill_blk88_n<8) begin
fillZ_blk88[fill_blk88_n[2:0]]<=rl_rd[63:32]; fill_blk88_n<=fill_blk88_n+1;
end
rdone_d <= u_reload.reload_done;
if (u_reload.reload_done && !rdone_d) fill_done_edges<=fill_done_edges+1;
end
end
always_ff @(posedge clk) begin
if (!rst_n) begin reload_pass<=0; zflush_evict<=0;
zA_spilled_seen[0]<=0; zA_spilled_seen[1]<=0; zA_spilled_seen[2]<=0; zA_spilled_seen[3]<=0; end
else begin
if (reload_start) reload_pass<=reload_pass+1;
// region A screen Z byte addr = (1*64+1)*4 = 0x104. Capture what's emitted for it.
if (z_flush_emit && z_flush_addr[15:0]==16'h0104 && zflush_evict<4) begin
zA_spilled_data[zflush_evict[1:0]]<=z_flush_data; zA_spilled_seen[zflush_evict[1:0]]<=1'b1;
end
// count evictions: a rising edge of TP_ZFLUSH entry (phase 6)
if (tile_phase_o==3'd6 && dut.u_gs.tile_sweep_r==9'd0) zflush_evict<=zflush_evict+1;
if (dut.u_gs.reload_wr_we && dut.u_gs.reload_wr_addr==8'd17 && reload_pass<4) begin
zA_reload_seen[reload_pass[1:0]] <= tile_reload_z;
zA_mem_at_reload[reload_pass[1:0]] <= mem[10'h088][63:32];
end
end
end
int errors;
task automatic chk(input bit c, input string m); if (!c) begin errors++; $display("[lpddr] FAIL: %s", m); end endtask
logic [23:0] rA, rB;
initial begin
errors=0; core_go=0;
// Ch323 — simulate a CLEAN BOOT from SCRUB LPDDR: fill the backing with GARBAGE (high Z).
// The clean-Z bootstrap must still produce red-on-blue: batch-1 renders from the local
// CLEAR (it does NOT reload this garbage), spills, and batch-2 reloads batch-1's spill.
for (int i=0;i<1024;i++) mem[i]=256'h7EED7EED_7EED7EED_7EED7EED_7EED7EED_7EED7EED_7EED7EED_7EED7EED_7EED7EED;
rst_n=0; erst_n=0; repeat(8) @(posedge eclk); erst_n=1; repeat(4) @(posedge clk); rst_n=1; repeat(8) @(posedge clk);
@(negedge clk); core_go<=1'b1; @(negedge clk); core_go<=1'b0;
$display("[lpddr] @%0t booted, waiting core_halt", $time);
wait (core_halt==1'b1); repeat(4) @(posedge clk);
wait (dma_done_seen==1'b1); repeat(10) @(posedge clk);
if (dut.xfer_busy==1'b1) wait (dut.xfer_busy==1'b0);
$display("[lpddr] @%0t waiting raster_active", $time);
wait (dut.u_gs.raster_active==1'b1);
wait (dut.u_gs.raster_active==1'b0); repeat(10) @(posedge clk);
@(posedge dut.u_pcrtc.end_of_frame); @(posedge clk); capture_armed<=1'b1;
@(posedge dut.u_pcrtc.end_of_frame); @(posedge clk); capture_armed<=1'b0;
rA = px(1,1); rB = px(10,2);
$display("[lpddr] regionA(1,1)=%06x regionB(10,2)=%06x", rA, rB);
$display("[lpddr] writers: c_beats=%0d z_beats=%0d c_ovf=%0d z_ovf=%0d reload: cbeats=%0d zbeats=%0d rderrs=%0d",
c_beats, z_beats, c_ovf, z_ovf, rl_cbeats, rl_zbeats, rl_rderrs);
$display("[lpddr] handshake: saw_reload_start=%0d saw_reload_ready=%0d reload_passes=%0d", saw_reload_start, saw_reload_ready, reload_pass);
$display("[lpddr] DEBUG region-A(idx17,screen 0x104) Z trace (evicts=%0d, reload passes=%0d):", zflush_evict, reload_pass);
for (int p=0;p<4;p++) if (zA_spilled_seen[p]) $display("[lpddr] evict %0d: z_flush emitted Z=0x%08x for region-A addr", p, zA_spilled_data[p]);
for (int p=0;p<4;p++) $display("[lpddr] reload slot %0d: idx17 served Z=0x%08x (mem lane1 was 0x%08x)", p, zA_reload_seen[p], zA_mem_at_reload[p]);
// scan the whole Z region in mem for any 0x8000 word
mem_has_8000 = 1'b0;
for (int bk=10'h080; bk<10'h0F0; bk++) for (int wd=0; wd<8; wd++)
if (mem[bk][wd*32+:32]==32'h0000_8000) mem_has_8000 = 1'b1;
$display("[lpddr] mem Z-region contains a 0x8000 word anywhere: %0d", mem_has_8000);
$display("[lpddr] fills completed (reload_done edges)=%0d, region-A Z-block reads=%0d:", fill_done_edges, fill_blk88_n);
for (int p=0;p<fill_blk88_n && p<8;p++) $display("[lpddr] fill read#%0d of block 0x88 word1 = 0x%08x", p, fillZ_blk88[p]);
$display("[lpddr] u_reload.z_ram[16..19] = %08x %08x %08x %08x (idx17 should be 0x8000)",
u_reload.z_ram[16], u_reload.z_ram[17], u_reload.z_ram[18], u_reload.z_ram[19]);
$display("[lpddr] u_reload final fill state rst_q=%0d row=%0d beat=%0d", u_reload.rst_q, u_reload.row, u_reload.beat);
// The integration proof THROUGH the real LPDDR path:
chk(saw_reload_start, "gs_stub never pulsed reload_start");
chk(saw_reload_ready, "gs_tile_reload never asserted reload_ready (pulse-vs-toggle / fill hang)");
chk(!c_ovf && !z_ovf, "spill FIFO overflow");
chk(c_beats>0 && z_beats>0, "spill writers wrote 0 beats");
// Color MUST match Z (both are 256 tile px / 8 = 32 beats per eviction). A color>Z mismatch
// means the color writer is over-fed (e.g. from generic raster_pixel_emit, not the dedicated
// TP_FLUSH color stream) — the Ch323 board "108 vs 64 + overflow" bug.
chk(c_beats==z_beats, $sformatf("color beats (%0d) != Z beats (%0d) — color writer over-fed (wrong flush stream)", c_beats, z_beats));
chk(rl_cbeats>0 && rl_zbeats>0, "reload read 0 beats");
chk(rA==COLOR1[23:0], $sformatf("region A = %06x, expected red color1 %06x (reload failed depth)", rA, COLOR1[23:0]));
chk(rB==COLOR2[23:0], $sformatf("region B = %06x, expected blue color2 %06x", rB, COLOR2[23:0]));
if (errors==0) $display("[tb_gs_tile_spill_lpddr] PASS");
else $display("[tb_gs_tile_spill_lpddr] FAIL (errors=%0d)", errors);
$finish;
end
initial begin #5000000; $display("[tb_gs_tile_spill_lpddr] TIMEOUT"); $display("[tb_gs_tile_spill_lpddr] FAIL"); $finish; end
endmodule