// 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= 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