// retroDE_ps2 — tb_top_psmct32_feeder_scene_retrigger_demo (Ch337)
//
// CLEAN SCENE-LEVEL RETRIGGER for >FIFO_DEPTH (multi-batch) scenes. Ch336 proved a >8 scene composes
// into one framebuffer; Ch337 proves the host can retrigger a NEW >8 scene without racing the last
// batch's render/flush. The control FSM now gates its ready on gs_stub.raster_scene_busy (which stays
// high through the inter-batch raster_active dips) instead of raster_active alone.
//
// Sequence (each scene is 14 prims = 2 batches): boot renders scene A, then on EACH feeder_ready edge
// we retrigger the next scene IMMEDIATELY (minimal gap — a premature ready would let the new scene
// race the old one and corrupt the FB):
//   A (tiles 0-13 RED)  ->  B (tiles 2-15 BLUE)  ->  A (tiles 0-13 RED)
// Each scene's first (full-flush) batch wipes the WHOLE framebuffer, so after each retrigger the FB
// must be EXACTLY that scene with ZERO residue from the other:
//   after B: BLUE in tiles 2-15, and NO RED anywhere (scene A fully gone).
//   after A: RED in tiles 0-13, and NO BLUE anywhere (scene B fully gone).

`timescale 1ns/1ps

module tb_top_psmct32_feeder_scene_retrigger_demo;

    localparam int H_ACTIVE = 64, V_ACTIVE = 64;

    logic clk, rst_n;
    initial clk = 1'b0;
    always #5 clk = ~clk;

    logic        core_go;
    logic [7:0]  r, g, b;
    logic        hsync, vsync, de;
    logic        core_halt, dma_done_seen, frame_seen, raster_overflow;
    logic        frame_toggle, dma_done_toggle;
    logic        feeder_go_tb, feeder_ready_tb;

    top_psmct32_raster_demo_bram #(
        .H_ACTIVE(H_ACTIVE), .V_ACTIVE(V_ACTIVE),
        .VRAM_BYTES(32*1024), .PSMCT32_SWIZZLE(1'b0),
        .COMBINED_TAZ(1'b1), .TILE_LOCAL(1'b1), .TILE_COLS(4), .TILE_ROWS(4),
        .TILE_MULTIPRIM(1'b1), .TILE_PRIM_COUNT(4), .TILE_FIFO_DEPTH(8),
        .BIN_BUFFER_ENABLE(1'b1), .HEARTBEAT_SPLICE_ENABLE(1'b0),
        .FEEDER_ENABLE(1'b1), .FEEDER_STG_WORDS(256)
    ) 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),
        .feeder_stg_we_i(1'b0), .feeder_stg_waddr_i(12'd0), .feeder_stg_wdata_i(64'd0),
        .feeder_go_i(feeder_go_tb),
        .feeder_ready_o(feeder_ready_tb), .feeder_records_o(), .feeder_waits_o()
    );

    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];
    logic       cap_de[0:V_ACTIVE-1][0:H_ACTIVE-1];
    bit capture_armed;
    task automatic clear_cap;
        for (int y=0;y<V_ACTIVE;y++) for (int x=0;x<H_ACTIVE;x++) begin
            cap_r[y][x]=0; cap_g[y][x]=0; cap_b[y][x]=0; cap_de[y][x]=0; end
    endtask
    initial begin clear_cap(); capture_armed=1'b0; end
    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; cap_de[vcnt_d][hcnt_d]<=1'b1; end

    function automatic int color_in_box(input int bx0, input int by0, input logic [7:0] er, eg, eb);
        int n; begin n=0;
            for (int y=by0;y<by0+16;y++) for (int x=bx0;x<bx0+16;x++)
                if (cap_de[y][x] && cap_r[y][x]===er && cap_g[y][x]===eg && cap_b[y][x]===eb) n++;
            color_in_box = n;
        end
    endfunction
    // Whole-frame pixel count of a color — the leftover detector.
    function automatic int color_in_frame(input logic [7:0] er, eg, eb);
        int n; begin n=0;
            for (int y=0;y<V_ACTIVE;y++) for (int x=0;x<H_ACTIVE;x++)
                if (cap_de[y][x] && cap_r[y][x]===er && cap_g[y][x]===eg && cap_b[y][x]===eb) n++;
            color_in_frame = n;
        end
    endfunction
    function automatic int tile_x(input int t); tile_x = (t%4)*16; endfunction
    function automatic int tile_y(input int t); tile_y = (t/4)*16; endfunction

    int errors;
    localparam logic [7:0] FF=8'hFF, ZZ=8'h00;

    // CONTRACT MONITOR (the real Ch337 acceptance): feeder_ready must NOT rise until the WHOLE scene
    // has drained. Sampled from gs_stub's RAW drain state (not the derived scene_busy wire) so it is
    // independent of the fix's own signal. With the pre-Ch337 gate (raster_active alone) ready rises
    // in an inter-batch dip while the next batch's prims are still queued (fifo_count != 0) -> fires.
    // The FB check alone can't catch this: a premature retrigger self-heals after the long settle +
    // per-scene full-flush. This is what makes the TB bite.
    int ready_violations;
    logic ready_d;
    always_ff @(posedge clk) begin
        if (!rst_n) begin ready_d <= 1'b0; ready_violations <= 0; end
        else begin
            ready_d <= feeder_ready_tb;
            if (feeder_ready_tb && !ready_d) begin    // feeder_ready rising edge
                if (dut.u_gs.fifo_count != 0 || dut.u_gs.raster_active ||
                    dut.u_gs.mp_flush_pending || (dut.u_gs.tile_phase_r != 3'd0)) begin
                    $error("[CONTRACT] feeder_ready rose mid-scene: fifo=%0d raster_active=%b mp_flush_pending=%b tile_phase=%0d",
                           dut.u_gs.fifo_count, dut.u_gs.raster_active, dut.u_gs.mp_flush_pending, dut.u_gs.tile_phase_r);
                    ready_violations <= ready_violations + 1;
                end
            end
        end
    end

    task automatic recapture;
        repeat(200000) @(posedge clk);
        @(posedge dut.u_pcrtc.end_of_frame); @(posedge clk); clear_cap(); capture_armed=1'b1;
        @(posedge dut.u_pcrtc.end_of_frame); @(posedge clk); capture_armed=1'b0;
    endtask

    // Retrigger the staged scene the INSTANT ready asserts (minimal gap stresses the race).
    task automatic go_now;
        wait (feeder_ready_tb==1'b1);
        @(negedge clk); feeder_go_tb=1'b1; @(negedge clk); feeder_go_tb=1'b0;
    endtask

    initial begin
        errors=0; feeder_go_tb=1'b0;
        // Boot scene = A (bitstream-init equivalent).
        $readmemh(`FEEDER_SCENE_A_FILE, dut.g_feeder.feeder_stg);

        rst_n=1'b0; core_go=1'b0;
        repeat(4) @(posedge clk); rst_n=1'b1; repeat(8) @(posedge clk);
        @(negedge clk); core_go=1'b1; @(negedge clk); core_go=1'b0;
        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);
        wait (feeder_ready_tb==1'b1);                       // boot render of A complete

        // ---- scene A (boot) : tiles 0-13 RED, NO blue, nothing in tiles 14-15 ----
        recapture();
        for (int t=0;t<14;t++)
            if (color_in_box(tile_x(t),tile_y(t),FF,ZZ,ZZ) < 15) begin
                $error("[A boot] tile %0d not RED (%0d px)", t, color_in_box(tile_x(t),tile_y(t),FF,ZZ,ZZ)); errors++; end
        if (color_in_frame(ZZ,ZZ,FF) != 0) begin $error("[A boot] BLUE present (%0d px)", color_in_frame(ZZ,ZZ,FF)); errors++; end
        $display("[retrig A boot] RED=%0d BLUE=%0d (t0=%0d t13=%0d)",
                 color_in_frame(FF,ZZ,ZZ), color_in_frame(ZZ,ZZ,FF), color_in_box(0,0,FF,ZZ,ZZ), color_in_box(tile_x(13),tile_y(13),FF,ZZ,ZZ));

        // ---- retrigger scene B : tiles 2-15 BLUE, and scene A must be FULLY GONE (zero RED) ----
        $readmemh(`FEEDER_SCENE_B_FILE, dut.g_feeder.feeder_stg);
        go_now();
        recapture();
        for (int t=2;t<16;t++)
            if (color_in_box(tile_x(t),tile_y(t),ZZ,ZZ,FF) < 15) begin
                $error("[B] tile %0d not BLUE (%0d px)", t, color_in_box(tile_x(t),tile_y(t),ZZ,ZZ,FF)); errors++; end
        if (color_in_frame(FF,ZZ,ZZ) != 0) begin $error("[B] RED leftover from scene A (%0d px) — premature ready / batch race!", color_in_frame(FF,ZZ,ZZ)); errors++; end
        if (int'(dut.feeder_records) != 14) begin $error("[B] records=%0d (want 14)", int'(dut.feeder_records)); errors++; end
        $display("[retrig B] BLUE=%0d RED-leftover=%0d records=%0d", color_in_frame(ZZ,ZZ,FF), color_in_frame(FF,ZZ,ZZ), int'(dut.feeder_records));

        // ---- retrigger scene A again : tiles 0-13 RED, and scene B must be FULLY GONE (zero BLUE) ----
        $readmemh(`FEEDER_SCENE_A_FILE, dut.g_feeder.feeder_stg);
        go_now();
        recapture();
        for (int t=0;t<14;t++)
            if (color_in_box(tile_x(t),tile_y(t),FF,ZZ,ZZ) < 15) begin
                $error("[A2] tile %0d not RED (%0d px)", t, color_in_box(tile_x(t),tile_y(t),FF,ZZ,ZZ)); errors++; end
        if (color_in_frame(ZZ,ZZ,FF) != 0) begin $error("[A2] BLUE leftover from scene B (%0d px) — premature ready / batch race!", color_in_frame(ZZ,ZZ,FF)); errors++; end
        if (int'(dut.feeder_records) != 14) begin $error("[A2] records=%0d (want 14)", int'(dut.feeder_records)); errors++; end
        if (raster_overflow) begin $error("[A2] raster_overflow set"); errors++; end
        $display("[retrig A2] RED=%0d BLUE-leftover=%0d records=%0d", color_in_frame(FF,ZZ,ZZ), color_in_frame(ZZ,ZZ,FF), int'(dut.feeder_records));

        if (ready_violations != 0) begin $error("[retrig] %0d premature-ready (mid-scene) violations", ready_violations); errors += ready_violations; end
        $display("[retrig] ready-contract violations=%0d", ready_violations);
        $display("[tb_top_psmct32_feeder_scene_retrigger_demo] errors=%0d", errors);
        if (errors==0) $display("[tb_top_psmct32_feeder_scene_retrigger_demo] PASS");
        else           $display("[tb_top_psmct32_feeder_scene_retrigger_demo] FAIL");
        $finish;
    end
    initial begin #2500000000; $error("[tb_top_psmct32_feeder_scene_retrigger_demo] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_feeder_scene_retrigger_demo
