Files
retroDE_ps2/sim/tb/top/tb_gs_tile_spill_reload.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

197 lines
12 KiB
Systemverilog

// retroDE_ps2 — tb_gs_tile_spill_reload (Ch323 Brick 1c — two-batch DEPTH-survival proof)
//
// THE acceptance proof: tile-local color+Z can leave the chip and come back, and
// the reloaded Z affects a LATER render. Two TME+ABE+ZTE triangles (payload_tile_spill)
// render as two tile batches (single-prim mode); batch 2 reloads batch 1's flushed
// color+Z before rendering. The TB models the LPDDR backing (captured FROM the actual
// z_flush_* + color-flush streams — round-trip, not preloaded) and serves it on reload.
//
// Z scheme (CLEAR=0x4000, GEQUAL): P1 region A (top-left tri) Z=0x8000 color1(red);
// P2 region A+B (large tri) Z=0x6000 color2(blue). Region A pixel (1,1) overlaps both;
// region B pixel (10,2) is P2-only.
//
// NEGATIVE/POSITIVE flip (the real proof — Codex): two render passes via CORE_CTRL
// re-render. POSITIVE: batch-2 reload serves the captured backing → region A reloads
// P1's Z=0x8000 → P2(0x6000) FAILS → region A = color1. NEGATIVE: batch-2 reload serves
// CLEAR (0x4000) → P2 PASSES → region A = color2. The color1<->color2 flip in region A,
// with region B = color2 in BOTH, proves the result depends on Z reload, not stale state.
`timescale 1ns/1ps
module tb_gs_tile_spill_reload;
localparam int H_ACTIVE = 16, V_ACTIVE = 16;
localparam logic [2:0] TP_CLEAR=3'd1, TP_RENDER=3'd2, TP_FLUSH=3'd3, TP_RELOAD=3'd5, TP_ZFLUSH=3'd6;
localparam logic [31:0] CLEAR_Z = 32'h0000_4000, CLEAR_COLOR = 32'hFF00_8000;
localparam logic [31:0] COLOR1 = 32'hFF0000FF, COLOR2 = 32'hFFFF0000; // red / blue (ABGR)
logic clk=0; always #5 clk=~clk;
logic rst_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 reload_start; logic [7:0] tile_reload_raddr;
logic [31:0] tile_reload_color, tile_reload_z;
logic tile_reload_ready; // TB staging always warm (de25 adds fill latency in Brick 2)
// ---- behavioral LPDDR backing, captured FROM the flush streams (round-trip) ----
logic [31:0] z_backing [0:2047];
logic [31:0] c_backing [0:2047];
logic seen [0:2047];
logic neg_mode; // 1 = serve CLEAR (negative control)
// reload serve: register the raddr 1 cyc (gs_stub writes the tile RAM the cycle AFTER
// presenting raddr). map tile index -> screen index = y*64 + x (single tile @ origin, fbw=1).
// Procedural (not continuous `&&`) to avoid an iverilog vvp gate-elaboration quirk.
logic [7:0] sreg_raddr;
always_ff @(posedge clk) sreg_raddr <= tile_reload_raddr;
always_comb begin
logic [10:0] sidx;
logic ub;
tile_reload_ready = 1'b1;
sidx = (11'(sreg_raddr[7:4]) << 6) + 11'(sreg_raddr[3:0]); // screen index = y*64 + x
ub = (!neg_mode) && (seen[sidx] === 1'b1);
tile_reload_z = ub ? z_backing[sidx] : CLEAR_Z;
tile_reload_color = ub ? c_backing[sidx] : CLEAR_COLOR;
end
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),
.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),
.tile_reload_ready_i(tile_reload_ready), .tile_reload_color_i(tile_reload_color),
.tile_reload_z_i(tile_reload_z)
);
// ---- phase + counter snoops (per pass; reset by clr_counters) ----
int col_reload_w, z_reload_w, col_flush_e, z_flush_e, clear_z_w;
bit saw_reload, saw_flush, saw_zflush, backing_before_2nd_reload;
int z_flush_seen_count, reload_starts;
bit clr_counters;
always_ff @(posedge clk) begin
if (clr_counters) begin
col_reload_w<=0; z_reload_w<=0; col_flush_e<=0; z_flush_e<=0; clear_z_w<=0;
saw_reload<=0; saw_flush<=0; saw_zflush<=0; backing_before_2nd_reload<=0;
z_flush_seen_count<=0; reload_starts<=0;
end else if (rst_n) begin
// capture flush streams into the backing (round-trip).
if (z_flush_emit) begin z_backing[z_flush_addr[12:2]]<=z_flush_data; seen[z_flush_addr[12:2]]<=1'b1;
z_flush_e<=z_flush_e+1; z_flush_seen_count<=z_flush_seen_count+1; end
if (flush_emit) begin c_backing[flush_addr[12:2]]<=flush_color32; col_flush_e<=col_flush_e+1; end
// tile-RAM write attribution by phase.
if (dut.u_gs.tile_phase==TP_CLEAR && dut.u_gs.tile_z_we) clear_z_w<=clear_z_w+1;
if (dut.u_gs.tile_phase==TP_RELOAD) begin
saw_reload<=1'b1;
if (dut.u_gs.tile_color_we) col_reload_w<=col_reload_w+1;
if (dut.u_gs.tile_z_we) z_reload_w<=z_reload_w+1;
end
if (dut.u_gs.tile_phase==TP_FLUSH) saw_flush<=1'b1;
if (dut.u_gs.tile_phase==TP_ZFLUSH) saw_zflush<=1'b1;
// Ch323 clean-Z BOOTSTRAP: batch-1 (no valid backing) does NOT reload — it renders
// from the local CLEAR. Only batch-2 reloads, and by then batch-1's spill must have
// populated the backing. So there is exactly ONE reload_start, and z_flush has fired.
if (reload_start) begin
reload_starts <= reload_starts + 1;
if (z_flush_seen_count>0) backing_before_2nd_reload<=1'b1;
end
end
end
// ---- scanout capture ----
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
int errors;
task automatic chk(input bit c, input string m);
if (!c) begin errors++; $display("[spill] FAIL: %s", m); end endtask
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
// run one render pass: full DUT reset + fresh boot (the bootlet re-uploads the texture
// and re-renders both prims), then capture region A/B. seen[] is cleared so batch-1's
// reload serves clear; the backing is repopulated from THIS pass's batch-1 flush.
task automatic run_pass(output logic [23:0] rA, output logic [23:0] rB);
for (int i=0;i<2048;i++) seen[i]=1'b0;
rst_n<=1'b0; repeat(4) @(posedge clk); rst_n<=1'b1; repeat(8) @(posedge clk);
clr_counters<=1'b1; @(posedge clk); clr_counters<=1'b0;
@(negedge clk); core_go<=1'b1; @(negedge clk); core_go<=1'b0;
$display("[spill] @%0t booted, waiting core_halt", $time);
wait (core_halt==1'b1); repeat(4) @(posedge clk);
$display("[spill] @%0t core_halt, waiting dma_done", $time);
wait (dma_done_seen==1'b1); repeat(10) @(posedge clk);
$display("[spill] @%0t dma_done, xfer_busy=%0d", $time, dut.xfer_busy);
if (dut.xfer_busy==1'b1) wait (dut.xfer_busy==1'b0);
$display("[spill] @%0t waiting raster_active rise", $time);
wait (dut.u_gs.raster_active==1'b1); // level-sensitive (robust to an already-passed edge)
$display("[spill] @%0t raster_active high, waiting fall", $time);
wait (dut.u_gs.raster_active==1'b0); repeat(10) @(posedge clk);
$display("[spill] @%0t raster done, waiting end_of_frame", $time);
@(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;
$display("[spill] @%0t frame captured", $time);
rA = px(1,1); rB = px(10,2);
endtask
logic [23:0] posA, posB, negA, negB;
int p_creload, p_zreload, p_cflush, p_zflush;
initial begin
errors=0; rst_n=1; core_go=0; neg_mode=0; clr_counters=0;
// ---- POSITIVE pass: batch-2 reload serves the captured backing ----
$display("[spill] starting POSITIVE pass ...");
neg_mode<=1'b0;
run_pass(posA, posB);
$display("[spill] POSITIVE pass done: regionA=%06x regionB=%06x", posA, posB);
p_creload=col_reload_w; p_zreload=z_reload_w; p_cflush=col_flush_e; p_zflush=z_flush_e;
chk(saw_reload, "FSM never entered TP_RELOAD");
chk(saw_flush, "FSM never entered TP_FLUSH");
chk(saw_zflush, "FSM never entered TP_ZFLUSH");
chk(backing_before_2nd_reload, "backing not populated before batch-2 reload");
chk(clear_z_w >= 256, $sformatf("batch-2 CLEAR didn't clobber tile Z (clear_z_w=%0d)", clear_z_w));
// BOOTSTRAP: only batch-2 reloads (batch-1 renders from clear) -> 256 reload writes, not 512.
chk(p_creload==256, $sformatf("color reload writes=%0d exp 256 (batch-2 only; batch-1 boots from clear)", p_creload));
chk(p_zreload==256, $sformatf("Z reload writes=%0d exp 256", p_zreload));
chk(p_cflush ==512, $sformatf("color flush emits=%0d exp 512 (both batches flush)", p_cflush));
chk(p_zflush ==512, $sformatf("Z flush emits=%0d exp 512", p_zflush));
// ---- NEGATIVE pass: batch-2 reload serves CLEAR (control) ----
$display("[spill] starting NEGATIVE pass ...");
neg_mode<=1'b1;
run_pass(negA, negB);
$display("[spill] NEGATIVE pass done: regionA=%06x regionB=%06x", negA, negB);
// ---- the flip: region A must change with reload, region B stays P2 ----
chk(posA===COLOR1[23:0], $sformatf("POSITIVE region A=%06x exp color1 %06x (Z reload should make P2 fail)", posA, COLOR1[23:0]));
chk(negA===COLOR2[23:0], $sformatf("NEGATIVE region A=%06x exp color2 %06x (no reload -> P2 passes)", negA, COLOR2[23:0]));
chk(posB===COLOR2[23:0], $sformatf("POSITIVE region B=%06x exp color2 %06x (control)", posB, COLOR2[23:0]));
chk(negB===COLOR2[23:0], $sformatf("NEGATIVE region B=%06x exp color2 %06x (control)", negB, COLOR2[23:0]));
chk(posA!==negA, "region A did NOT flip between reload-on and reload-off (proof void)");
$display("=== Ch323 tile spill/reload two-batch proof ===");
$display(" counts(positive): color_reload=%0d Z_reload=%0d (exp 256, batch-2 only) color_flush=%0d Z_flush=%0d (exp 512)",
p_creload, p_zreload, p_cflush, p_zflush);
$display(" FSM: reload=%0d flush=%0d zflush=%0d backing_before_2nd_reload=%0d clear_clobber=%0d",
saw_reload, saw_flush, saw_zflush, backing_before_2nd_reload, clear_z_w);
$display(" region A: positive=%06x negative=%06x (exp color1=%06x / color2=%06x)", posA, negA, COLOR1[23:0], COLOR2[23:0]);
$display(" region B: positive=%06x negative=%06x (exp color2=%06x both)", posB, negB, COLOR2[23:0]);
if (errors==0) $display("[tb_gs_tile_spill_reload] PASS");
else $display("[tb_gs_tile_spill_reload] FAIL (%0d errors)", errors);
$finish;
end
initial begin #100000000; $display("[tb_gs_tile_spill_reload] TIMEOUT"); $finish; end
endmodule