// retroDE_ps2 — tb_top_psmct32_sh3_zint640c12 (Ch360 — four-epoch real-raster persistent-Z integration)
//
// Codex's pre-fit gate: prove the persistent-Z ROP through the SAME path the de25 top wires (GS_SH3_LPDDR_FB_Z):
//   REAL demo (feeder + raster, authz sh3_zs640c12 NATIVE-640x480 strong-reject scene) -> gs_lpddr_zc_emit -> REAL de25 arbiters
//   (gs_lpddr_wr_arb s0=color / s2=Z-write ; gs_lpddr_rd_arb s3=Z-read) -> ONE shared behavioral LPDDR
//   with randomized backpressure. Unlike tb_top_psmct32_sh3_zarb (which fed a handshake-throttled trace),
//   the raster CANNOT honor g_ready, so this is the gate that catches request-FIFO fragment DROPS.
//
// Asserts (Codex list):
//   * final LPDDR color + Z == independent clamp16 persistent-Z scoreboard over the ACTUAL emitted fragments;
//   * zero fragment drops (g_valid && !g_ready) — the whole reason this sim exists;
//   * exactly one ordered scene marker (sh3_fb_flush) per accepted GO; no fragment after a marker within a scene;
//   * Z cache invalidated (clear_done) after preclear and before the first GO; Z persists across epochs;
//   * a FRESH ordered frame_drained per epoch; scanout never referenced during render (rd_arb s0/s1/s2 idle);
//   * merged AW only in color/Z ranges, merged AR only in Z range (disjoint-map monitor).
// LOCAL/gitignored fixtures; skip-guard if absent.
`timescale 1ns/1ps

module tb_top_psmct32_sh3_zint640c12;
`include "sh3_zs640c12_params.vh"                  // FBPXW=640, FBH=480, N_EPOCHS=4, EPk_CRC/EPk_NTRIS, TEX_*, ...
    localparam int W = FBPXW, H = FBH;
    localparam int NPX = W*H;
    localparam int NEP = N_EPOCHS;
    localparam [31:0] COLBASE = 32'h0000_0000, ZBASE = 32'h0014_0000;
    localparam int COL_TOP = NPX*4;              // color region byte-size (disjoint below ZBASE)
    localparam int Z_TOP   = NPX*2;              // Z region byte-size
    localparam int MEMBEATS = 65536;             // shared LPDDR: covers ZBASE + Z_TOP

    // per-epoch expected CRC / records
    // Ch360 — four epochs share one texture: EPk_REUSE=1 means the epoch runs on the resident cache without a fill.
    logic [31:0] EP_CRC [0:3]; int EP_REC [0:3]; bit EP_REUSE [0:3]; int fills;
    initial begin
        EP_CRC[0]=EP0_CRC; EP_CRC[1]=EP1_CRC; EP_CRC[2]=EP2_CRC; EP_CRC[3]=EP3_CRC;
        EP_REC[0]=EP0_NTRIS; EP_REC[1]=EP1_NTRIS; EP_REC[2]=EP2_NTRIS; EP_REC[3]=EP3_NTRIS;
        EP_REUSE[0]=EP0_REUSE; EP_REUSE[1]=EP1_REUSE; EP_REUSE[2]=EP2_REUSE; EP_REUSE[3]=EP3_REUSE;
        fills=0;
    end

`ifdef C12_REQ_DEPTH
    localparam int TB_REQ_DEPTH = `C12_REQ_DEPTH;   // Ch359 sim-only depth-sweep variant (Codex-authorized)
`else
    localparam int TB_REQ_DEPTH = 1024;            // Ch359 production depth (Codex: cold-Z burst peaks at 478)
`endif
    logic clk=0;       always #5 clk=~clk;
    logic emif_clk=0;  always #2 emif_clk=~emif_clk;
    logic rst_n; logic fb_commit=0; always #13 fb_commit=~fb_commit;
    int errors; initial errors=0;

    // clamp16 (vendored PCSX2 PSMZ16S source-Z saturation)
    function automatic logic [15:0] cl16(input logic [31:0] z); cl16=(|z[31:16])?16'hFFFF:z[15:0]; endfunction

    // ===== bram-top (FB_LPDDR_ONLY, AUTOSTART off), 384x381 =====
    logic        core_go; logic [7:0] r,g,b; logic hsync,vsync_o,de;
    logic        core_halt,dma_done_seen,frame_seen,raster_overflow,frame_toggle,dma_done_toggle;
    logic        feeder_go_tb, feeder_ready_tb; logic [15:0] feeder_records_w; logic [31:0] feeder_waits_w;
    logic        gs_tex_rd_en_o; logic [31:0] gs_tex_rd_addr_o; logic [31:0] tex_cache_data; logic tex_cache_ready;
    logic [31:0] tex_cache_hits, tex_bram_hits;
    logic        flush_emit_w; logic [31:0] flush_addr_w; logic [31:0] flush_color32_w; logic [5:0] flush_psm_w;
    logic [11:0] flush_x_w, flush_y_w; logic [31:0] flush_z_w;
    logic        stg_we; logic [11:0] stg_waddr; logic [63:0] stg_wdata;

    top_psmct32_raster_demo_bram #(
        .H_ACTIVE(FBPXW), .V_ACTIVE(FBH),
        .VRAM_BYTES(VRAM_BYTES_P), .VRAM_ENABLE_READ2(1'b0), .PSMCT32_SWIZZLE(1'b0),
        .COMBINED_TAZ(1'b0), .TILE_LOCAL(1'b0), .TILE_COLS(1), .TILE_ROWS(1),
        .TILE_MULTIPRIM(1'b0), .TILE_PRIM_COUNT(1), .TILE_FIFO_DEPTH(8),
        .BIN_BUFFER_ENABLE(1'b0), .HEARTBEAT_SPLICE_ENABLE(1'b0),
        .FEEDER_ENABLE(1'b1), .FEEDER_STG_WORDS(STG_WORDS), .FEEDER_AUTOSTART(1'b0),
        .PERSPECTIVE_CORRECT(1'b1), .PERSP_RECIP_IDX_BITS(11),
        .GRAD_SEQ_DIVIDER(1'b1), .GRAD_DIV_CYCLES(4),
        .GS_LPDDR_TEX(1'b1), .TEX_VRAM_BASE(TEX_VRAM_BASE), .TEX_CACHE_BYTES(TEX_BYTES),
        .CLUT_CSM1_ENABLE(1'b1), .FB_LPDDR_ONLY(1'b1)
    ) dut (
        .clk(clk), .rst_n(rst_n), .core_go(core_go),
        .r(r), .g(g), .b(b), .hsync(hsync), .vsync(vsync_o), .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(stg_we), .feeder_stg_waddr_i(stg_waddr), .feeder_stg_wdata_i(stg_wdata),
        .feeder_go_i(feeder_go_tb), .feeder_ready_o(feeder_ready_tb),
        .feeder_records_o(feeder_records_w), .feeder_waits_o(feeder_waits_w),
        .flush_emit_o(flush_emit_w), .flush_addr_o(flush_addr_w),
        .flush_pix16_o(), .flush_color32_o(flush_color32_w), .flush_psm_o(flush_psm_w),
        .flush_x_o(flush_x_w), .flush_y_o(flush_y_w), .flush_z_o(flush_z_w),
        .gs_tex_rd_en_o(gs_tex_rd_en_o), .gs_tex_rd_addr_o(gs_tex_rd_addr_o),
        .tex_cache_data_i(tex_cache_data), .tex_cache_ready_i(tex_cache_ready),
        .tex_cache_hits_o(tex_cache_hits), .tex_bram_hits_o(tex_bram_hits)
    );

    // texture cache + behavioral texture LPDDR (reloaded per epoch)
    logic [29:0] araddr; logic [1:0] arburst; logic [6:0] arid; logic [7:0] arlen; logic [2:0] arsize;
    logic        arvalid, arready; logic [255:0] rdata; logic [1:0] rresp; logic rlast, rvalid, rready;
    logic        fill_start, fill_done; logic [31:0] fill_beats, fill_bytes, tex_rd_errs, fill_crc_w;
    gs_texture_cache #(.LPDDR_TEX_BASE(LPDDR_TEX_BASE), .TEX_VRAM_BASE(TEX_VRAM_BASE),
                       .TEX_BYTES(TEX_BYTES), .N_BEATS(N_BEATS)) u_cache (
        .axi_clk(clk), .axi_rst_n(rst_n), .fill_start(fill_start), .fill_done(fill_done),
        .fill_beats(fill_beats), .fill_bytes(fill_bytes), .rd_errs(tex_rd_errs), .fill_crc(fill_crc_w),
        .araddr(araddr), .arburst(arburst), .arid(arid), .arlen(arlen), .arsize(arsize), .arvalid(arvalid), .arready(arready),
        .rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready),
        .sample_clk(clk), .tex_rd_en(gs_tex_rd_en_o), .tex_rd_addr(gs_tex_rd_addr_o),
        .tex_rd_data(tex_cache_data), .tex_ready(tex_cache_ready)
    );
    logic [31:0] lpddr_mem [0:(TEX_BYTES/4)-1];
    typedef enum logic [1:0] { S_IDLE, S_WAIT, S_DATA } sstate_t; sstate_t sst; logic [3:0] tdly; int tbeat;
    always_ff @(posedge clk) begin
        if (!rst_n) begin sst<=S_IDLE; arready<=0; rvalid<=0; rlast<=0; rresp<=0; rdata<=0; tdly<=0; end
        else begin arready<=0; rvalid<=0; rlast<=0;
            case (sst)
                S_IDLE: if (arvalid) begin arready<=1; tbeat<=(araddr-LPDDR_TEX_BASE)>>5; tdly<=4'd2; sst<=S_WAIT; end
                S_WAIT: if (tdly==0) sst<=S_DATA; else tdly<=tdly-1'b1;
                S_DATA: if (rready) begin for (int w=0;w<8;w++) rdata[w*32 +: 32]<=lpddr_mem[tbeat*8+w];
                        rresp<=2'b00; rvalid<=1; rlast<=1; sst<=S_IDLE; end
            endcase
        end
    end

    // ===== render epoch: one ordered scene marker (sh3_fb_flush) per accepted GO =====
    logic fb_flush=0, render_inflight=0, feeder_ready_q=0; int eof_count;
    wire feeder_ready_rise = feeder_ready_tb && !feeder_ready_q;
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin render_inflight<=0; feeder_ready_q<=0; fb_flush<=0; eof_count<=0; end
        else begin feeder_ready_q<=feeder_ready_tb; fb_flush<=1'b0;
            if (feeder_go_tb && feeder_ready_tb) render_inflight<=1'b1;
            if (render_inflight && feeder_ready_rise) begin fb_flush<=1'b1; render_inflight<=1'b0; eof_count<=eof_count+1; end
        end
    end

    // ===== gs_lpddr_zc_emit — Z-then-color emit, fed by the REAL raster (NO handshake) =====
    logic        zc_enable, clear_start, clear_done, frame_drained;
    wire  [15:0] g_zq   = cl16(flush_z_w);
    wire         frag_v = flush_emit_w && (flush_psm_w==6'h00);
    wire         g_valid = frag_v || fb_flush;
    wire         g_scene = fb_flush;
    wire  [11:0] g_x = flush_x_w, g_y = flush_y_w;
    wire  [31:0] g_color = flush_color32_w;
    wire         g_ready;
    // Z AXI
    logic [31:0] z_araddr; logic [7:0] z_arlen; logic [2:0] z_arsize; logic [1:0] z_arburst; logic z_arvalid, z_arready;
    logic [255:0] z_rdata; logic [1:0] z_rresp; logic z_rlast, z_rvalid, z_rready;
    logic [31:0] z_awaddr; logic [7:0] z_awlen; logic [2:0] z_awsize; logic [1:0] z_awburst; logic z_awvalid, z_awready;
    logic [255:0] z_wdata; logic [31:0] z_wstrb; logic z_wlast, z_wvalid, z_wready; logic z_bvalid, z_bready; logic [1:0] z_bresp;
    // Color AXI
    logic [31:0] c_awaddr; logic [7:0] c_awlen; logic [2:0] c_awsize; logic [1:0] c_awburst; logic c_awvalid, c_awready;
    logic [255:0] c_wdata; logic [31:0] c_wstrb; logic c_wlast, c_wvalid, c_wready; logic c_bvalid, c_bready; logic [1:0] c_bresp;
    logic [31:0] z_beats_read, z_beats_written, c_beats_written, col_ovf, bresp_err; logic zc_idle;
    gs_lpddr_zc_emit #(.COLBASE(COLBASE), .ZBASE(ZBASE), .FB_PXW(W), .FB_H(H), .REQ_DEPTH(TB_REQ_DEPTH), .COL_DEPTH(128)) u_zc (
        .gs_clk(clk), .gs_rst_n(rst_n), .enable(zc_enable),
        .g_valid(g_valid), .g_ready(g_ready), .g_x(g_x), .g_y(g_y), .g_zq(g_zq), .g_zmsk(1'b0),
        .g_ztest(1'b1), .g_ztst(2'd2), .g_color(g_color), .g_be(4'hF), .g_scene(g_scene),
        .axi_clk(emif_clk), .axi_rst_n(rst_n), .clear_start(clear_start), .clear_done(clear_done), .frame_drained(frame_drained),
        .z_araddr(z_araddr), .z_arlen(z_arlen), .z_arsize(z_arsize), .z_arburst(z_arburst), .z_arvalid(z_arvalid), .z_arready(z_arready),
        .z_rdata(z_rdata), .z_rresp(z_rresp), .z_rlast(z_rlast), .z_rvalid(z_rvalid), .z_rready(z_rready),
        .z_awaddr(z_awaddr), .z_awlen(z_awlen), .z_awsize(z_awsize), .z_awburst(z_awburst), .z_awvalid(z_awvalid), .z_awready(z_awready),
        .z_wdata(z_wdata), .z_wstrb(z_wstrb), .z_wlast(z_wlast), .z_wvalid(z_wvalid), .z_wready(z_wready),
        .z_bvalid(z_bvalid), .z_bready(z_bready), .z_bresp(z_bresp),
        .c_awaddr(c_awaddr), .c_awlen(c_awlen), .c_awsize(c_awsize), .c_awburst(c_awburst), .c_awvalid(c_awvalid), .c_awready(c_awready),
        .c_wdata(c_wdata), .c_wstrb(c_wstrb), .c_wlast(c_wlast), .c_wvalid(c_wvalid), .c_wready(c_wready),
        .c_bvalid(c_bvalid), .c_bready(c_bready), .c_bresp(c_bresp),
        .z_beats_read(z_beats_read), .z_beats_written(z_beats_written), .c_beats_written(c_beats_written),
        .col_ovf(col_ovf), .bresp_err(bresp_err), .idle(zc_idle)
    );

    // Ch359 DIAGNOSTIC — request-FIFO occupancy high-water (write-domain binary pointers, hierarchical):
    // sizes the burst that overflows REQ_DEPTH=256 (would a deeper FIFO absorb it, or is the ep0 dirty-miss
    // streak service-rate-bound?). Sampled on the write clock; wbin-rbin_sampled is a safe over-estimate.
    // (Codex correction: occupancy must be computed ENTIRELY in the write clock domain — wbin vs the
    // write-domain-SYNCHRONIZED read pointer rgray_s2 converted gray->binary — with the modulus derived from the
    // selected depth, not a hard-coded 2*256.)
    int req_hiwater; logic hw_clear;
    function automatic int g2b(input int g);
        int b; b=g; b^=b>>1; b^=b>>2; b^=b>>4; b^=b>>8; b^=b>>16; return b;
    endfunction
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) req_hiwater <= 0;
        else if (hw_clear) req_hiwater <= 0;
        else begin
            int occ;
            occ = (int'(u_zc.u_req.wbin) - g2b(int'(u_zc.u_req.rgray_s2))) % (2*TB_REQ_DEPTH);
            if (occ < 0) occ += 2*TB_REQ_DEPTH;
            if (occ > req_hiwater) req_hiwater <= occ;
        end
    end
    // fragment-drop guard (design clk): the request FIFO silently drops when g_ready is low
    int g_drops; int frags_fed;
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin g_drops<=0; frags_fed<=0; end
        else begin
            if (frag_v && !g_ready) g_drops<=g_drops+1;
            if (frag_v && g_ready)  frags_fed<=frags_fed+1;
        end
    end
    // clear ordering: clear_done must rise before the first GO. no psm0 fragment before clear_done.
    logic saw_clear_done=0, first_go_done=0; int pre_clear_frags;
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin saw_clear_done<=0; pre_clear_frags<=0; end
        else begin
            if (clear_done) saw_clear_done<=1;
            if (frag_v && !saw_clear_done) pre_clear_frags<=pre_clear_frags+1;
        end
    end

    // ===== REAL de25 write arbiter: s0=color, s2=Z-write ; s1/s3 inert =====
    logic [29:0] m_awaddr; logic [1:0] m_awburst; logic [6:0] m_awid; logic [7:0] m_awlen; logic [2:0] m_awsize;
    logic m_awvalid, m_awready; logic [255:0] m_wdata; logic [31:0] m_wstrb; logic m_wlast, m_wvalid, m_wready;
    logic m_bvalid, m_bready; logic [1:0] m_bresp;
    gs_lpddr_wr_arb u_wr_arb (
        .clk(emif_clk), .rst_n(rst_n),
        .s0_awaddr(c_awaddr[29:0]), .s0_awburst(c_awburst), .s0_awid(7'd0), .s0_awlen(c_awlen), .s0_awsize(c_awsize),
        .s0_awvalid(c_awvalid), .s0_awready(c_awready), .s0_wdata(c_wdata), .s0_wstrb(c_wstrb), .s0_wlast(c_wlast),
        .s0_wvalid(c_wvalid), .s0_wready(c_wready), .s0_bresp(c_bresp), .s0_bvalid(c_bvalid), .s0_bready(c_bready),
        .s1_awaddr(30'd0), .s1_awburst(2'b01), .s1_awid(7'd1), .s1_awlen(8'd0), .s1_awsize(3'b101),
        .s1_awvalid(1'b0), .s1_awready(), .s1_wdata(256'd0), .s1_wstrb(32'd0), .s1_wlast(1'b0),
        .s1_wvalid(1'b0), .s1_wready(), .s1_bresp(), .s1_bvalid(), .s1_bready(1'b0),
        .s2_awaddr(z_awaddr[29:0]), .s2_awburst(z_awburst), .s2_awid(7'd2), .s2_awlen(z_awlen), .s2_awsize(z_awsize),
        .s2_awvalid(z_awvalid), .s2_awready(z_awready), .s2_wdata(z_wdata), .s2_wstrb(z_wstrb), .s2_wlast(z_wlast),
        .s2_wvalid(z_wvalid), .s2_wready(z_wready), .s2_bresp(z_bresp), .s2_bvalid(z_bvalid), .s2_bready(z_bready),
        .s3_awaddr(30'd0), .s3_awburst(2'b01), .s3_awid(7'd3), .s3_awlen(8'd0), .s3_awsize(3'b101),
        .s3_awvalid(1'b0), .s3_awready(), .s3_wdata(256'd0), .s3_wstrb(32'd0), .s3_wlast(1'b0),
        .s3_wvalid(1'b0), .s3_wready(), .s3_bresp(), .s3_bvalid(), .s3_bready(1'b0),
        .m_awaddr(m_awaddr), .m_awburst(m_awburst), .m_awid(m_awid), .m_awlen(m_awlen), .m_awsize(m_awsize),
        .m_awvalid(m_awvalid), .m_awready(m_awready), .m_wdata(m_wdata), .m_wstrb(m_wstrb), .m_wlast(m_wlast),
        .m_wvalid(m_wvalid), .m_wready(m_wready), .m_bvalid(m_bvalid), .m_bready(m_bready), .m_bresp(m_bresp)
    );

    // ===== REAL de25 read arbiter: s3=Z-read (mirrors de25 reload port) ; s0/s1/s2 idle during render =====
    logic [29:0] m_araddr; logic [1:0] m_arburst; logic [6:0] m_arid; logic [7:0] m_arlen; logic [2:0] m_arsize;
    logic m_arvalid, m_arready; logic [255:0] m_rdata; logic [1:0] m_rresp; logic m_rlast, m_rvalid, m_rready;
    gs_lpddr_rd_arb u_rd_arb (
        .clk(emif_clk), .rst_n(rst_n),
        .s0_araddr(30'd0), .s0_arburst(2'b01), .s0_arid(7'd0), .s0_arlen(8'd0), .s0_arsize(3'b101),
        .s0_arvalid(1'b0), .s0_arready(), .s0_rdata(), .s0_rresp(), .s0_rlast(), .s0_rvalid(), .s0_rready(1'b0),
        .s1_araddr(30'd0), .s1_arburst(2'b01), .s1_arid(7'd1), .s1_arlen(8'd0), .s1_arsize(3'b101),
        .s1_arvalid(1'b0), .s1_arready(), .s1_rdata(), .s1_rresp(), .s1_rlast(), .s1_rvalid(), .s1_rready(1'b0),
        .s2_araddr(30'd0), .s2_arburst(2'b01), .s2_arid(7'd2), .s2_arlen(8'd0), .s2_arsize(3'b101),
        .s2_arvalid(1'b0), .s2_arready(), .s2_rdata(), .s2_rresp(), .s2_rlast(), .s2_rvalid(), .s2_rready(1'b0),
        .s3_araddr(z_araddr[29:0]), .s3_arburst(z_arburst), .s3_arid(7'd3), .s3_arlen(z_arlen), .s3_arsize(z_arsize),
        .s3_arvalid(z_arvalid), .s3_arready(z_arready), .s3_rdata(z_rdata), .s3_rresp(z_rresp), .s3_rlast(z_rlast),
        .s3_rvalid(z_rvalid), .s3_rready(z_rready),
        .m_araddr(m_araddr), .m_arburst(m_arburst), .m_arid(m_arid), .m_arlen(m_arlen), .m_arsize(m_arsize),
        .m_arvalid(m_arvalid), .m_arready(m_arready), .m_rdata(m_rdata), .m_rresp(m_rresp), .m_rlast(m_rlast),
        .m_rvalid(m_rvalid), .m_rready(m_rready)
    );

    logic [15:0] lf=16'hF00D; always_ff @(posedge emif_clk) lf<={lf[14:0], lf[15]^lf[13]^lf[12]^lf[10]};

    // ===== ONE shared behavioral LPDDR (write + read), address-decoded, random backpressure =====
    logic [255:0] shmem [0:MEMBEATS-1];
    logic [29:0] wa; logic [255:0] wd; logic [31:0] ws; logic aw_s, w_s; logic [3:0] bd; logic bp;
    logic [29:0] ra; logic rp; logic [3:0] rd_dly;
    always_ff @(posedge emif_clk or negedge rst_n) begin
        if(!rst_n) begin
            m_awready<=0; m_wready<=0; m_bvalid<=0; m_bresp<=0; aw_s<=0; w_s<=0; bp<=0; bd<=0;
            m_arready<=0; m_rvalid<=0; m_rlast<=0; m_rresp<=0; m_rdata<=0; rp<=0; rd_dly<=0;
        end else begin
            m_awready<=(!aw_s)?lf[0]:1'b0; m_wready<=(!w_s)?lf[1]:1'b0;
            if(m_awvalid&&m_awready) begin wa<=m_awaddr; aw_s<=1; m_awready<=0; end
            if(m_wvalid&&m_wready)  begin wd<=m_wdata; ws<=m_wstrb; w_s<=1; m_wready<=0; end
            if(aw_s&&w_s&&!bp&&!m_bvalid) begin
                for(int bb=0;bb<32;bb++) if(ws[bb]) shmem[wa>>5][bb*8+:8]<=wd[bb*8+:8];
                bp<=1; bd<={1'b0,lf[4:2]};
            end
            if(bp&&!m_bvalid) begin if(bd==0) begin m_bvalid<=1;m_bresp<=0;bp<=0;aw_s<=0;w_s<=0; end else bd<=bd-1; end
            if(m_bvalid&&m_bready) m_bvalid<=0;
            m_arready<=(!rp&&!m_rvalid)?lf[8]:1'b0;
            if(m_arvalid&&m_arready) begin ra<=m_araddr; rp<=1; rd_dly<={1'b0,lf[7:5]}; m_arready<=0; end
            if(rp&&!m_rvalid) begin if(rd_dly==0) begin m_rdata<=shmem[ra>>5]; m_rresp<=0; m_rvalid<=1; m_rlast<=1; rp<=0; end else rd_dly<=rd_dly-1; end
            if(m_rvalid&&m_rready) begin m_rvalid<=0; m_rlast<=0; end
        end
    end
    // disjoint-map monitors on the MERGED master streams
    always_ff @(posedge emif_clk) if(rst_n) begin
        if(m_awvalid) begin
            if(!((m_awaddr<30'(COLBASE+COL_TOP)) || (m_awaddr>=30'(ZBASE) && m_awaddr<30'(ZBASE+Z_TOP))))
                begin $error("[zint] merged AW %h not in color/Z range",m_awaddr); errors++; end
        end
        if(m_arvalid && (m_araddr<30'(ZBASE) || m_araddr>=30'(ZBASE+Z_TOP))) begin $error("[zint] merged AR %h not in Z range",m_araddr); errors++; end
    end

    // ===== clamp16 persistent-Z SCOREBOARD over the ACTUAL emitted fragments (design clk) =====
    // Mirrors gs_lpddr_z_rmw: GEQUAL vs stored, Z_CLEAR=0. Persists across epochs. Gated active after clear_done.
    logic [15:0] sb_z  [0:NPX-1];
    logic [31:0] sb_col[0:NPX-1];
    logic        sb_wr [0:NPX-1];
    integer sb_frag, sb_pass;
    // no-fragment-after-marker: a fragment must not appear in the same scene after fb_flush (before next GO)
    logic scene_ended=0; int frag_after_marker;
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sb_frag<=0; sb_pass<=0; scene_ended<=0; frag_after_marker<=0;
        end else begin
            if (feeder_go_tb && feeder_ready_tb) scene_ended<=0;         // new scene opens
            if (fb_flush) scene_ended<=1;                                // ordered marker closes the scene
            if (frag_v && saw_clear_done) begin
                int o; logic [15:0] zq;
                if (scene_ended) frag_after_marker<=frag_after_marker+1; // fragment after this scene's marker = ordering bug
                o = g_y*W + g_x; zq = cl16(flush_z_w);
                sb_frag<=sb_frag+1;
                if (o>=0 && o<NPX) begin
                    if (zq >= sb_z[o]) begin sb_z[o]<=zq; sb_col[o]<=flush_color32_w; sb_wr[o]<=1'b1; sb_pass<=sb_pass+1; end
                end
            end
        end
    end

    // FRESH-DRAIN observer (emif domain)
    logic fd_q; int fd_rises, fd_falls;
    always_ff @(posedge emif_clk or negedge rst_n) begin
        if (!rst_n) begin fd_q<=0; fd_rises<=0; fd_falls<=0; end
        else begin fd_q<=frame_drained;
            if (frame_drained && !fd_q) fd_rises<=fd_rises+1;
            if (!frame_drained && fd_q) fd_falls<=fd_falls+1;
        end
    end

    // clear_start: mirror de25 — pulse once at the writer ARM (after preclear, before first GO)
    logic wr_arm=0, arm_e1, arm_e2, arm_e3;
    always_ff @(posedge emif_clk or negedge rst_n) begin
        if (!rst_n) begin arm_e1<=0; arm_e2<=0; arm_e3<=0; clear_start<=0; end
        else begin arm_e1<=wr_arm; arm_e2<=arm_e1; arm_e3<=arm_e2; clear_start<=(arm_e2 && !arm_e3); end
    end

    // ---- feeder staging stream (write port), per epoch ----
    logic [63:0] stg_buf [0:STG_WORDS-1];
    task automatic stream_list(input int k);
        string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/feeder_sh3_zs640c12%0d.mem", k);
        for (int i=0;i<STG_WORDS;i++) stg_buf[i]=64'd0;
        $readmemh(fn, stg_buf);
        @(negedge clk);
        for (int i=0;i<STG_WORDS;i++) begin stg_we<=1'b1; stg_waddr<=i[11:0]; stg_wdata<=stg_buf[i]; @(negedge clk); end
        stg_we<=1'b0; stg_waddr<=12'd0; stg_wdata<=64'd0; @(negedge clk);
        if (dut.g_feeder.feeder_stg[0][31:0] !== stg_buf[0][31:0]) begin
            $error("[zint] epoch %0d: staged word0=%08x exp %08x", k, dut.g_feeder.feeder_stg[0][31:0], stg_buf[0][31:0]); errors++; end
    endtask

    task automatic fill_cache(input int k, input logic [31:0] exp_crc);
        int d=0;
        fills=fills+1;
        @(posedge clk) fill_start<=1'b1; @(posedge clk) fill_start<=1'b0;
        d=0; while (fill_done && d<1000) begin @(posedge clk); d++; end
        d=0; while (!fill_done && d<400000) begin @(posedge clk); d++; end
        if (!fill_done)           begin $error("[zint] fill %0d: fill_done never rose", k); errors++; end
        if (fill_crc_w!==exp_crc) begin $error("[zint] fill %0d: crc=%08x exp %08x", k, fill_crc_w, exp_crc); errors++; end
        if (tex_rd_errs!==0)      begin $error("[zint] fill %0d: rd_errs=%0d", k, tex_rd_errs); errors++; end
    endtask

    // one-scene runner: GO, wait render+drain, REQUIRE a FRESH ordered frame_drained
    task automatic run_scene(input int k, input int exp_records);
        int r0, f0, d=0; logic fd_before; int gd0, fm0;
        r0=fd_rises; f0=fd_falls; fd_before=frame_drained; gd0=g_drops; fm0=frag_after_marker;
        @(negedge clk); feeder_go_tb=1'b1; @(negedge clk); feeder_go_tb=1'b0;
        d=0; while (feeder_ready_tb && d<4000) begin @(posedge clk); d++; end
        if (feeder_ready_tb!==1'b0) begin $error("[zint] epoch %0d: feeder_ready did not drop after GO", k); errors++; end
        if (fd_before) begin
            d=0; while (fd_falls==f0 && d<6000000) begin @(posedge emif_clk); d++; end
            if (fd_falls==f0) begin $error("[zint] epoch %0d: frame_drained never fell from stale high", k); errors++; end
        end
        d=0; while (fd_rises<=r0 && d<6000000) begin @(posedge emif_clk); d++; end
        if (fd_rises<=r0) begin $error("[zint] epoch %0d: frame_drained never rose", k); errors++; end
        repeat(100) @(posedge clk);
        if (feeder_records_w!==exp_records) begin $error("[zint] epoch %0d: records=%0d exp %0d", k, feeder_records_w, exp_records); errors++; end
        if (col_ovf!==0 || bresp_err!==0)   begin $error("[zint] epoch %0d: col_ovf=%0d bresp_err=%0d", k, col_ovf, bresp_err); errors++; end
        if (g_drops!==gd0)                  begin $error("[zint] epoch %0d: %0d FRAGMENT DROPS (request FIFO overflow)", k, g_drops-gd0); errors++; end
        if (frag_after_marker!==fm0)        begin $error("[zint] epoch %0d: %0d fragment(s) after scene marker (ordering)", k, frag_after_marker-fm0); errors++; end
        $display("[zint] epoch %0d: fresh drain (f %0d->%0d, r %0d->%0d) records=%0d drops=%0d fed=%0d pass=%0d",
                 k, f0, fd_falls, r0, fd_rises, feeder_records_w, g_drops, frags_fed, sb_pass);
    endtask

    task automatic run_epoch(input int k);
        string fn; fn=$sformatf("../../data/top_psmct32_raster_demo/sh3_zs640c12%0d_tex_lpddr.mem", k);
        if (!EP_REUSE[k]) begin
            $readmemh(fn, lpddr_mem);
            fill_cache(k, EP_CRC[k]);
        end else begin
            // Ch359 — EXPLICIT REUSE: no reload, no fill pulse. Fail-closed residency: the fill CRC register is
            // only rewritten by a fill, so it must still hold the prior epoch's verified CRC (same shared texture).
            if (fill_crc_w!==EP_CRC[k]) begin $error("%s epoch %0d: REUSE but resident crc=%08x exp %08x — residency broken", "[zint]", k, fill_crc_w, EP_CRC[k]); errors++; end
            else $display("%s epoch %0d: REUSE resident texture (crc=0x%08x, no fill)", "[zint]", k, fill_crc_w);
        end
        stream_list(k);
        run_scene(k, EP_REC[k]);
    endtask

    initial begin
        errors=0; feeder_go_tb=1'b0; fill_start=1'b0; wr_arm=0; zc_enable=1'b1; stg_we=0; stg_waddr=0; stg_wdata=0; hw_clear=1'b0;
        for (int i=0;i<NPX;i++) begin sb_z[i]=16'h0000; sb_col[i]=32'd0; sb_wr[i]=1'b0; end
        for (int b=0;b<MEMBEATS;b++) shmem[b]=256'd0;            // PRECLEAR color+Z EXACTLY ONCE
        // skip-guard: probe a fixture (via $fopen — avoids the $readmemh range warning)
        begin int pfh; pfh=$fopen("../../data/top_psmct32_raster_demo/sh3_zs640c120_tex_lpddr.mem","r");
            if (pfh==0) begin $display("[tb_top_psmct32_sh3_zint640c12] SKIP — sh3_zs640c12*.mem absent (run gs_make_sh3_scheduler_fixture.py --authz --fb640 --tag zs640c12 --emit)"); $finish; end
            $fclose(pfh);
        end

        rst_n=1'b0; core_go=1'b0; repeat(6) @(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); repeat(50) @(posedge clk);

        // ARM (-> clear_start) AFTER preclear, BEFORE first GO. z_rmw's clear_start writes Z_CLEAR to EVERY Z beat
        // (NBEATS=NPX/16, through wr_arb s2 under random backpressure) then asserts clear_done -> it is the authoritative
        // Z preclear. Wait for it (the board host likewise gates the first GO on the clear_done status bit).
        wr_arm<=1'b1;
        begin int d=0; while (!clear_done && d<600000) begin @(posedge emif_clk); d++; end end
        if (!clear_done) begin $error("[zint] clear_done never rose after ARM (Z preclear did not finish)"); errors++; end
        else $display("[zint] Z preclear complete (clear_done), z_beats_written(clear)=%0d", z_beats_written);
        repeat(40) @(posedge clk);

        for (int k=0;k<NEP;k++) begin
            int fed0, dr0;
            fed0=frags_fed; dr0=g_drops;
            run_epoch(k);
            $display("[zint][sweep] depth=%0d epoch %0d: accepted=%0d drops=%0d req_hiwater=%0d",
                     TB_REQ_DEPTH, k, frags_fed-fed0, g_drops-dr0, req_hiwater);
            @(negedge clk); hw_clear<=1'b1; @(negedge clk); hw_clear<=1'b0;   // per-epoch high-water
        end

        if (pre_clear_frags!==0) begin $error("[zint] %0d psm0 fragment(s) before clear_done", pre_clear_frags); errors++; end
        if (fd_rises<NEP)   begin $error("[zint] expected %0d ordered drains: rises=%0d", NEP, fd_rises); errors++; end
        if (fd_falls<NEP-1) begin $error("[zint] epochs after the first never cleared stale (falls=%0d)", fd_falls); errors++; end
        if (eof_count!==NEP)begin $error("[zint] scene markers=%0d != accepted GOs=%0d", eof_count, NEP); errors++; end
        if (g_drops!==0)    begin $error("[zint] TOTAL %0d fragment drops", g_drops); errors++; end
        // Ch359 — the WHOLE run must perform exactly ONE verified texture fill (epoch 1 reuses the resident cache)
        if (fills!==1)      begin $error("[zint] expected exactly ONE texture fill, got %0d (residency contract)", fills); errors++; end

        // ===== final memory compare: shared LPDDR color + Z == scoreboard =====
        begin
            int zmis, cmis, zchk, cchk; zmis=0; cmis=0; zchk=0; cchk=0;
            for (int o=0;o<NPX;o++) begin
                logic [15:0] zs; logic [31:0] cs; int zb, zl;
                zb=(ZBASE + o*2)>>5; zl=o&15; zs=shmem[zb][zl*16+:16];
                zchk++;
                if (zs !== sb_z[o]) begin if (zmis<10) $error("[zint] Z px%0d got %04x exp %04x", o, zs, sb_z[o]); zmis++; end
                if (sb_wr[o]) begin
                    cs=shmem[o>>3][(o[2:0])*32+:32]; cchk++;
                    if (cs[23:0] !== sb_col[o][23:0]) begin if (cmis<10) $error("[zint] COL px%0d got %06x exp %06x", o, cs[23:0], sb_col[o][23:0]); cmis++; end
                end
            end
            $display("[zint] final compare: Z %0d/%0d mismatch, COLOR %0d/%0d mismatch (written px=%0d)", zmis, zchk, cmis, cchk, cchk);
        end

        begin
            string fdump;
            if ($value$plusargs("FBDUMP=%s", fdump)) begin
                int fh;
                fh = $fopen(fdump, "w");
                if (fh == 0) begin
                    $error("[zint] could not open FBDUMP '%s'", fdump);
                    errors++;
                end else begin
                    for (int o=0;o<NPX;o++) begin
                        logic [31:0] cs;
                        cs = shmem[o>>3][(o[2:0])*32+:32];
                        $fdisplay(fh, "%08x", cs);
                    end
                    $fclose(fh);
                    $display("[zint] dumped final COLOR FB (%0dx%0d) -> %s", W, H, fdump);
                end
            end
        end

        $display("[tb_top_psmct32_sh3_zint640c12] fed=%0d pass=%0d drops=%0d markers=%0d drains(r/f)=%0d/%0d col_ovf=%0d bresp_err=%0d errors=%0d",
                 frags_fed, sb_pass, g_drops, eof_count, fd_rises, fd_falls, col_ovf, bresp_err, errors);
        $display("[zint] req-FIFO depth=%0d (diagnostic sweep variant when != 1024)", TB_REQ_DEPTH);
        $display("[zint] map: COLOR 0x%0h..0x%0h  Z 0x%0h..0x%0h", COLBASE, COLBASE+COL_TOP, ZBASE, ZBASE+Z_TOP);
        if (errors==0) $display("[tb_top_psmct32_sh3_zint640c12] PASS"); else $display("[tb_top_psmct32_sh3_zint640c12] FAIL");
        $finish;
    end
    initial begin #400000000; $error("[tb_top_psmct32_sh3_zint640c12] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_zint640c12
