ec82764bef
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>
197 lines
12 KiB
Systemverilog
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
|