// ============================================================================
// tb_gs_z_flush_writer.sv  (Ch323 Brick 2)
//
// Focused TB for the tile Z-flush LPDDR writer (Codex: stride + beat packing need
// their own test). Drives a STRIDED Z emit stream (a couple of 16-word tile rows at
// 256-byte stride) on the gs clock into a behavioral 256-bit EMIF write target on the
// (faster, async) emif clock, and checks:
//   (1) every Z lands at Z_BASE + addr in the correct 32-bit lane (addr[4:2]) with the
//       right WSTRB nibble — strided addressing + single-lane packing are correct
//   (2) z_write_beats == #emits, z_wr_errs == 0, fifo_overflow == 0
//   (3) a forced non-OKAY bresp increments z_wr_errs
// ============================================================================
`timescale 1ns/1ps

module tb_gs_z_flush_writer;
    localparam [29:0] Z_BASE = 30'h0010_0000;
    localparam int    STRIDE = 256;

    logic gs_clk=0;  always #7 gs_clk = ~gs_clk;     // design (slower)
    logic axi_clk=0; always #3 axi_clk = ~axi_clk;   // emif (faster), async
    logic gs_rst_n, axi_rst_n;

    logic        enable, z_flush_emit; logic [31:0] z_flush_addr, z_flush_data;
    logic [31:0] z_write_beats, z_wr_errs; logic fifo_overflow;
    logic [29:0] awaddr; logic [1:0] awburst; logic [6:0] awid; logic [7:0] awlen;
    logic [2:0]  awsize; logic awvalid, awready;
    logic [255:0] wdata; logic [31:0] wstrb; logic wlast, wvalid, wready;
    logic [1:0]  bresp; logic bvalid, bready;

    gs_z_flush_writer #(.Z_BASE(Z_BASE), .FB_BASE(30'd0), .FB_BYTES(32'h1000),
                        .TEX_BASE(30'h0020_0000), .TEX_BYTES(32'h8000), .FIFO_DEPTH(16)) dut (
        .gs_clk(gs_clk), .gs_rst_n(gs_rst_n), .enable(enable), .trace_clear(1'b0),
        .z_flush_emit(z_flush_emit), .z_flush_addr(z_flush_addr), .z_flush_data(z_flush_data),
        .z_write_beats(z_write_beats), .z_wr_errs(z_wr_errs), .fifo_overflow(fifo_overflow),
        .axi_clk(axi_clk), .axi_rst_n(axi_rst_n),
        .awaddr(awaddr), .awburst(awburst), .awid(awid), .awlen(awlen), .awsize(awsize),
        .awvalid(awvalid), .awready(awready), .wdata(wdata), .wstrb(wstrb), .wlast(wlast),
        .wvalid(wvalid), .wready(wready), .bresp(bresp), .bvalid(bvalid), .bready(bready)
    );

    // behavioral 256-bit EMIF write target.
    logic [255:0] mem [0:65535];
    int force_err = 0;
    typedef enum logic [1:0] { W_IDLE,W_DATA,W_B } wst_t; wst_t wst; logic [29:0] beat;
    always_ff @(posedge axi_clk or negedge axi_rst_n) begin
        if (!axi_rst_n) begin wst<=W_IDLE; awready<=0; wready<=0; bvalid<=0; bresp<=0; end
        else begin
            awready<=0; wready<=0; bvalid<=0;
            case (wst)
                W_IDLE: if (awvalid) begin awready<=1; beat<=(awaddr>>5); wst<=W_DATA; end
                W_DATA: if (wvalid) begin
                    wready<=1;
                    for (int by=0; by<32; by++) if (wstrb[by]) mem[beat[15:0]][by*8 +: 8] <= wdata[by*8 +: 8];
                    wst<=W_B;
                end
                W_B: begin bvalid<=1; bresp<=(force_err>0)?2'b10:2'b00; if (bready) wst<=W_IDLE; end
            endcase
        end
    end

    int errors=0;
    task automatic chk(input bit c, input string m); if (!c) begin errors++; $display("[zwr] FAIL: %s", m); end endtask
    function automatic [31:0] memword(input [29:0] byteaddr); memword = mem[byteaddr[20:5]][ byteaddr[4:2]*32 +: 32 ]; endfunction
    task automatic emit(input [31:0] a, input [31:0] d);
        @(posedge gs_clk); z_flush_addr<=a; z_flush_data<=d; z_flush_emit<=1'b1;
        @(posedge gs_clk); z_flush_emit<=1'b0; repeat(2) @(posedge gs_clk);
    endtask

    // expected: stream of (offset,data) covering 2 tile rows (16 words each, 256B stride).
    localparam int NW = 32;
    logic [31:0] exp_off [0:NW-1]; logic [31:0] exp_dat [0:NW-1];
    initial begin
        for (int row=0; row<2; row++) for (int col=0; col<16; col++) begin
            int k; k = row*16+col;
            exp_off[k] = row*STRIDE + col*4;
            exp_dat[k] = 32'hABCD_0000 | (row << 12) | (k+1);  // distinct per (row,col)
        end
    end

    initial begin
        enable=0; z_flush_emit=0; z_flush_addr=0; z_flush_data=0; force_err=0;
        for (int i=0;i<65536;i++) mem[i]=256'd0;
        gs_rst_n=0; axi_rst_n=0; repeat(4) @(posedge axi_clk); gs_rst_n=1; axi_rst_n=1; repeat(2) @(posedge gs_clk);
        enable<=1'b1;

        for (int k=0; k<NW; k++) emit(exp_off[k], exp_dat[k]);
        // let the writer drain.
        begin int g=0; while ((z_write_beats < NW) && g<5000) begin @(posedge axi_clk); g++; end end

        for (int k=0; k<NW; k++)
            chk(memword(Z_BASE + exp_off[k][29:0]) === exp_dat[k],
                $sformatf("Z[off=0x%0x] mem=0x%08x exp 0x%08x", exp_off[k], memword(Z_BASE+exp_off[k][29:0]), exp_dat[k]));
        // PACKED: NW=32 consecutive-within-row 32-bit words = 4 full 256-bit beats
        // (2 rows x 2 blocks/row of 8 lanes). One AXI write per beat, not per word.
        chk(z_write_beats==(NW/8), $sformatf("z_write_beats=%0d exp %0d (packed: NW/8)", z_write_beats, NW/8));
        chk(z_wr_errs==0,      $sformatf("z_wr_errs=%0d exp 0", z_wr_errs));
        chk(!fifo_overflow,    "fifo_overflow set");

        // forced-error write: emit a FULL 8-lane block at a fresh address so it flushes
        // (a single stray emit would stay in the partial-beat accumulator and never write).
        force_err=1;
        for (int k=0; k<8; k++) emit(32'h0000_2000 + k*4, 32'hDEAD_BEE0 | k);
        begin int g=0; while ((z_wr_errs==0) && g<5000) begin @(posedge axi_clk); g++; end end
        chk(z_wr_errs==1, $sformatf("forced-err z_wr_errs=%0d exp 1", z_wr_errs));

        // Ch323 asymmetric-reset regression (THE board bug): pulse gs_rst_n ONLY (mimics a
        // CORE_CTRL re-render) WITHOUT axi_rst_n. If the FIFO write reset followed gs_rst_n, the
        // write pointer resets to 0 while the read pointer stays put → gray-code DESYNC → the
        // read side sees PHANTOM entries and drains ~DEPTH spurious beats. The fix ties the FIFO
        // write reset to axi_rst_n, so a gs-only reset can't desync the pointers: no phantom beats.
        force_err=0; repeat(20) @(posedge axi_clk);          // let any real traffic settle
        begin logic [31:0] beats_b4;
            gs_rst_n=1'b0; repeat(3) @(posedge gs_clk); gs_rst_n=1'b1; repeat(2) @(posedge gs_clk);
            beats_b4 = z_write_beats;
            repeat(200) @(posedge axi_clk);                  // a desync would drain phantoms here
            chk(z_write_beats===beats_b4,
                $sformatf("PHANTOM beats after gs-only reset: %0d->%0d (FIFO pointer desync)", beats_b4, z_write_beats));
        end
        // and a fresh block must still write correctly afterward.
        for (int k=0; k<8; k++) emit(32'h0000_3000 + k*4, 32'h600D_0000 | k);
        begin int g=0; while ((memword(Z_BASE+32'h3000)!==32'h600D_0000) && g<5000) begin @(posedge axi_clk); g++; end end
        for (int k=0; k<8; k++)
            chk(memword(Z_BASE + 32'h3000 + k*4) === (32'h600D_0000 | k),
                $sformatf("post-gs-reset Z[0x%0x]=0x%08x exp 0x%08x", 32'h3000+k*4,
                          memword(Z_BASE+32'h3000+k*4), 32'h600D_0000|k));

        $display("[zwr] beats=%0d wr_errs=%0d overflow=%0d errors=%0d", z_write_beats, z_wr_errs, fifo_overflow, errors);
        if (errors==0) $display("[tb_gs_z_flush_writer] PASS"); else $display("[tb_gs_z_flush_writer] FAIL");
        $finish;
    end
    initial begin #500000; $display("[tb_gs_z_flush_writer] TIMEOUT"); $finish; end
endmodule
