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