// retroDE_ps2 — tb_top_psmct32_sh3_lpddr_fb_seq  (Ch353 Brick 3 — LPDDR-only FB host-start SEQUENCE proof)
//
// Proves Codex's round-5 board sequence at the module level, ahead of the owner fit:
//   * FEEDER_AUTOSTART=0 -> boot setup reaches C_READY WITHOUT a boot render.
//   * RENDER EPOCH: the end-of-scene flush (EOF) fires ONLY on a feeder_ready RISE that follows an accepted
//     host GO (render_inflight), so the setup->ready edge can NOT enqueue a false EOF.
//   * frame_drained is a HARD gate (no timeout): scanout enable is driven by it, EMIF-domain coherent.
//
// Acceptance (Codex): boot reaches ready; BEFORE GO zero raster writes / zero EOF / frame_drained=0; preclear (FB=0)
// no BRESP; texture fill verifies (crc); host sets base 0 / canary off / arms writer; GO drops then re-raises ready;
// exactly ONE EOF -> exactly ONE frame_drained; scanout reads exactly 334*32=10688 beats and matches ALL 256x334
// pixels (incl. black); underflow=0, rd/wr errors=0, FIFO overflow=0; a synchronized frame_drained reaches "bridge".
// LOCAL/gitignored fixtures (dump-derived); skip-guard if absent.
`timescale 1ns/1ps
`define SH3_FULL_FRAME 1

module tb_top_psmct32_sh3_lpddr_fb_seq;
`include "sh3_full_params.vh"                  // 256x334 full-frame bounding box (FBPXW,FBH,STG_WORDS,TEX_*,TW,TH,...)
    localparam int W = FBPXW, H = FBH;         // 256 x 334
    localparam int STRIDE = W*4;               // 1024 B (PSMCT32, no padding)
    localparam int ROW_BEATS = STRIDE/32;      // 32
    localparam int FB_BYTES = STRIDE*H;        // 342016
    localparam int FB_WORDS = W*H;
    localparam int BEATS_PER_FRAME = ROW_BEATS*H; // 10688
    localparam int H_ACT=W, V_ACT=H, H_BP=32, H_FP=8, V_BP=16, V_FP=8;
    localparam int H_TOT=H_BP+H_ACT+H_FP, V_TOT=V_BP+V_ACT+V_FP;

    logic clk=0;       always #5 clk=~clk;        // GS/design clock (feeder + writer gs-side)
    logic emif_clk=0;  always #2 emif_clk=~emif_clk; // EMIF (fast, independent) — writer axi-side + scanout axi-side
    logic video_clk=0; always #7 video_clk=~video_clk;// video (independent of both)
    logic bridge_clk=0;always #11 bridge_clk=~bridge_clk; // "HPS bridge" domain (frame_drained diagnostic sync)
    logic rst_n;
    logic fb_commit=0; always #13 fb_commit=~fb_commit;   // free-run ctrl_commit so arm/base/canary latch

    int errors; initial errors=0;

    // ================= bram-top: SH3 draw via the feeder, FB_LPDDR_ONLY, AUTOSTART OFF =================
    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        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;

    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), // Ch353 — no boot render
        .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(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(),
        .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),
        .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 (from Brick 1c)
    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 (Codex round 5) — flush only on a ready-rise that FOLLOWS an accepted GO =================
    // render_inflight set when a GO is accepted (feeder_go pulse while ready); the end-of-scene EOF flush fires on the
    // NEXT feeder_ready rise (render+drain done) and clears the epoch. The FEEDER_AUTOSTART=0 setup->ready edge has
    // render_inflight=0, so it can NOT produce a false EOF.
    logic fb_flush=0; logic 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;   // accepted host GO opens the epoch
            if (render_inflight && feeder_ready_rise) begin
                fb_flush        <= 1'b1;                 // exactly one EOF per render epoch
                render_inflight <= 1'b0;
                eof_count       <= eof_count + 1;
            end
        end
    end

    // ================= Ch353 PSMCT32 writer + behavioral (precleared) LPDDR FB =================
    logic        wr_arm=0, wr_canary=0; logic [31:0] wr_base=32'h0;   // host-controlled (Codex step 5)
    logic [255:0] fbw_wdata; logic [31:0] fbw_wstrb, fbw_awaddr; logic fbw_awvalid, fbw_wvalid;
    logic [31:0] fbw_beats, fbw_ovf, fbw_bresp_err; logic fbw_idle, fbw_drained;
    gs_lpddr_axi_master #(.FIFO_DEPTH(64), .PIX_BYTES(4)) u_wr (
        .gs_clk(clk), .gs_rst_n(rst_n), .enable(1'b1),
        .arm(wr_arm), .canary(wr_canary), .fb_base(wr_base), .ctrl_commit(fb_commit),
        .px_emit(flush_emit_w && (flush_psm_w == 6'h00)), .px_addr(flush_addr_w), .px_pix32(flush_color32_w), .flush(fb_flush),
        .axi_clk(emif_clk), .axi_rst_n(rst_n),
        .awaddr(fbw_awaddr), .awlen(), .awsize(), .awburst(), .awid(),
        .awvalid(fbw_awvalid), .awready(1'b1),
        .wdata(fbw_wdata), .wstrb(fbw_wstrb), .wlast(), .wvalid(fbw_wvalid), .wready(1'b1),
        .bvalid(1'b1), .bready(), .bresp(2'b00),
        .beats_written(fbw_beats), .bursts_issued(), .bresp_err_count(fbw_bresp_err),
        .fifo_overflow_count(fbw_ovf), .idle(fbw_idle), .frame_drained(fbw_drained)
    );
    logic [7:0] fb [0:FB_BYTES-1]; logic [31:0] fb_awlat;
    always_ff @(posedge emif_clk) begin
        if (fbw_awvalid) fb_awlat <= fbw_awaddr;
        if (fbw_wvalid) for (int i=0;i<32;i++) if (fbw_wstrb[i]) begin
            int a; a = fb_awlat + i; if (a>=0 && a<FB_BYTES) fb[a] <= fbw_wdata[i*8 +: 8];
        end
    end
    // "ideal" render (every emitted PSMCT32 pixel) — for the bounded oracle
    logic [31:0] ideal [0:FB_WORDS-1]; logic ideal_set [0:FB_WORDS-1];
    always_ff @(posedge clk) if (rst_n && flush_emit_w && (flush_psm_w==6'h00)) begin
        ideal[flush_addr_w>>2] <= flush_color32_w; ideal_set[flush_addr_w>>2] <= 1'b1;
    end

    // ================= SCANOUT reads the SAME FB back (Brick 2), gated on frame_drained =================
    logic [11:0] px, py; logic vsync, in_win;
    logic [7:0]  so_r, so_g, so_b; logic so_underflow; logic [31:0] so_rd_errs; logic so_line_valid;
    logic [29:0] so_araddr; logic [1:0] so_arburst; logic [6:0] so_arid; logic [7:0] so_arlen;
    logic [2:0]  so_arsize; logic so_arvalid, so_arready;
    logic [255:0] so_rdata; logic [1:0] so_rresp; logic so_rlast, so_rvalid, so_rready;
    gs_lpddr_scanout_lb #(.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
                          .N_ROWS(H), .PSMCT32(1'b1)) u_scan (
        .axi_clk(emif_clk), .axi_rst_n(rst_n), .enable(fbw_drained),   // HARD gate — frame_drained, no timeout
        .video_clk(video_clk), .frame_start(vsync),
        .pixel_x(px), .pixel_y(py), .in_window(in_win),
        .r(so_r), .g(so_g), .b(so_b),
        .line_valid(so_line_valid), .underflow(so_underflow), .rd_errs(so_rd_errs),
        .araddr(so_araddr), .arburst(so_arburst), .arid(so_arid), .arlen(so_arlen), .arsize(so_arsize),
        .arvalid(so_arvalid), .arready(so_arready), .rdata(so_rdata), .rresp(so_rresp),
        .rlast(so_rlast), .rvalid(so_rvalid), .rready(so_rready)
    );
    // FB read model — single-beat, VARIABLE AR/R latency; counts read beats per EMIF-domain frame
    logic [7:0] rlfsr=8'h3C; always_ff @(posedge emif_clk) rlfsr<={rlfsr[6:0], rlfsr[7]^rlfsr[5]^rlfsr[4]^rlfsr[3]};
    typedef enum logic [1:0] { R_IDLE, R_WAIT, R_DATA } rst_t; rst_t rst_state;
    logic [3:0] rdly; logic [29:0] rd_addr_l; int read_beats;
    logic [2:0] vs_e; wire vs_edge_e = vs_e[1] && !vs_e[2]; int fb_reads, fb_reads_last;
    always_ff @(posedge emif_clk or negedge rst_n) begin
        if (!rst_n) begin rst_state<=R_IDLE; so_arready<=0; so_rvalid<=0; so_rlast<=0; so_rresp<=0; so_rdata<=0; rdly<=0;
                          read_beats<=0; vs_e<=0; fb_reads<=0; fb_reads_last<=0; end
        else begin
            so_arready<=0; so_rvalid<=0; so_rlast<=0;
            vs_e <= {vs_e[1:0], vsync};
            if (vs_edge_e) begin fb_reads_last <= fb_reads; fb_reads <= 0; end
            case (rst_state)
                R_IDLE: if (so_arvalid) begin so_arready<=1; rd_addr_l<=so_araddr; rdly<=rlfsr[2:0]; rst_state<=R_WAIT; end
                R_WAIT: if (rdly==0) rst_state<=R_DATA; else rdly<=rdly-1'b1;
                R_DATA: if (so_rready) begin
                    for (int w=0;w<8;w++) begin int a; a=rd_addr_l+w*4;
                        so_rdata[w*32 +: 32] <= (a+3<FB_BYTES) ? {fb[a+3],fb[a+2],fb[a+1],fb[a]} : 32'd0; end
                    so_rresp<=2'b00; so_rvalid<=1; so_rlast<=1; read_beats<=read_beats+1;
                    if (!vs_edge_e) fb_reads<=fb_reads+1;
                    rst_state<=R_IDLE;
                end
            endcase
        end
    end

    // REAL video raster (independent video_clk, active + blanking incl. vertical back porch)
    logic vid_run=0; logic [11:0] rawx, rawy;
    always_ff @(posedge video_clk) begin
        if (!vid_run) begin rawx<=0; rawy<=0; end
        else if (rawx==H_TOT-1) begin rawx<=0; rawy<=(rawy==V_TOT-1)?12'd0:rawy+1'b1; end
        else rawx<=rawx+1'b1;
    end
    wire active = (rawx>=H_BP)&&(rawx<H_BP+H_ACT)&&(rawy>=V_BP)&&(rawy<V_BP+V_ACT);
    assign px     = active ? (rawx - H_BP) : 12'd0;
    assign py     = (rawy>=V_BP && rawy<V_BP+V_ACT) ? (rawy - V_BP) : 12'd0;
    assign in_win = active;
    assign vsync  = vid_run && (rawx==0) && (rawy==0);

    // pixel compare — 1-video-cycle registered latency; compare vs the FB (incl. black background)
    logic [11:0] px_q, py_q; logic inwin_q, run_q;
    always_ff @(posedge video_clk) begin px_q<=px; py_q<=py; inwin_q<=in_win; run_q<=vid_run; end
    int checked; initial checked=0; logic scoring=0;
    always_ff @(posedge video_clk) if (scoring && run_q) begin
        logic [7:0] er,eg,eb; logic [31:0] w;
        if (inwin_q) begin int a; a=py_q*STRIDE+px_q*4; w={fb[a+3],fb[a+2],fb[a+1],fb[a]}; er=w[7:0]; eg=w[15:8]; eb=w[23:16]; end
        else begin er=0; eg=0; eb=0; end
        checked++;
        if (so_r!==er || so_g!==eg || so_b!==eb) begin
            if (errors<12) $error("[scan] px(%0d,%0d) in=%b got(%02x,%02x,%02x) exp(%02x,%02x,%02x)",
                                  px_q,py_q,inwin_q, so_r,so_g,so_b, er,eg,eb);
            errors++;
        end
    end

    // ================= "HPS bridge" frame_drained diagnostic sync (Codex step: expose synchronized frame_drained) =================
    logic [2:0] bd_sync; wire bridge_drained = bd_sync[2];
    always_ff @(posedge bridge_clk or negedge rst_n) begin
        if (!rst_n) bd_sync <= 3'd0; else bd_sync <= {bd_sync[1:0], fbw_drained};
    end

    // pre-GO raster-write monitor (raster emits before GO would be a boot-render leak)
    int raster_emits_preGO; logic count_preGO=0;
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) raster_emits_preGO<=0;
        else if (count_preGO && flush_emit_w && (flush_psm_w==6'h00)) raster_emits_preGO<=raster_emits_preGO+1;
    end

    // ============================================ SEQUENCE ============================================
    // full-frame bounded oracle helpers
    logic [31:0] idx_words [0:(512*512/4)-1]; logic [31:0] pal [0:255]; logic [31:0] refmap [0:FB_WORDS-1];
    function automatic logic [7:0] sh3_idx(input integer u, input integer v);
        integer lin; logic [31:0] w; lin=v*TW+u; w=idx_words[lin/4]; sh3_idx=w[(8*(lin%4)) +: 8]; endfunction
    function automatic logic [23:0] exp_cell(input integer u, input integer v); exp_cell=pal[sh3_idx(u,v)][23:0]; endfunction

    initial begin
        errors=0; feeder_go_tb=1'b0; fill_start=1'b0; wr_arm=0; wr_canary=0; wr_base=32'h0;
        for (int i=0;i<FB_WORDS;i++) begin ideal_set[i]=1'b0; ideal[i]=32'd0; end
        for (int i=0;i<FB_BYTES;i++) fb[i]=8'h00;      // Codex step 2 — model the HPS FB preclear (starts black)
        lpddr_mem[0]='x;
        $readmemh(`FEEDER_SH3_REAL_FILE, dut.g_feeder.feeder_stg);
        $readmemh("../../data/top_psmct32_raster_demo/sh3_real_tex_lpddr.mem", lpddr_mem);
        $readmemh("../../data/top_psmct32_raster_demo/sh3_real_idx.mem", idx_words);
        $readmemh("../../data/top_psmct32_raster_demo/sh3_real_pal.mem", pal);
        $readmemh("../../data/top_psmct32_raster_demo/sh3_full_refmap.mem", refmap);
        if (lpddr_mem[0] === 32'bx) begin
            $display("[tb_top_psmct32_sh3_lpddr_fb_seq] SKIP — sh3_*.mem absent (run gs_make_sh3_real_draw_fixture.py --full-frame)");
            $finish;
        end

        rst_n=1'b0; core_go=1'b0;
        repeat(6) @(posedge clk); rst_n=1'b1; repeat(8) @(posedge clk);

        // ---- Codex step 4: fill + verify the texture cache ----
        @(posedge clk) fill_start<=1'b1;
        begin int gd=0; while(!fill_done && gd<200000) begin @(posedge clk); gd++; end end
        if (!fill_done)               begin $error("[seq] cache fill_done never asserted"); errors++; end
        if (fill_crc_w!==32'hfbdeaa32)begin $error("[seq] cache fill_crc=%08x exp fbdeaa32", fill_crc_w); errors++; end
        if (tex_rd_errs!==0)          begin $error("[seq] tex fill read errors=%0d", tex_rd_errs); errors++; end

        // ---- Codex step 1: boot setup + CLUT upload; feeder must reach READY with NO boot render (AUTOSTART=0) ----
        count_preGO<=1'b1;
        @(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);            // setup -> C_READY (no render)
        repeat(200) @(posedge clk);              // dwell: a boot render (if any) would have emitted by now

        // ---- Codex acceptance: BEFORE GO — zero raster writes, zero EOF, frame_drained=0 ----
        if (raster_emits_preGO!==0) begin $error("[seq] %0d raster emits BEFORE GO (boot-render leak — AUTOSTART!=0?)", raster_emits_preGO); errors++; end
        if (fbw_beats!==0)          begin $error("[seq] writer wrote %0d beats BEFORE GO (should be 0)", fbw_beats); errors++; end
        if (eof_count!==0)          begin $error("[seq] %0d EOF markers BEFORE GO (render-epoch leaked on setup ready-rise)", eof_count); errors++; end
        if (fbw_drained!==1'b0)     begin $error("[seq] frame_drained asserted BEFORE GO"); errors++; end
        if (fbw_bresp_err!==0)      begin $error("[seq] preclear/idle BRESP errors=%0d", fbw_bresp_err); errors++; end
        $display("[seq] pre-GO clean: raster_emits=0 writer_beats=0 eof=0 frame_drained=0 (ready reached without boot render)");

        // ---- Codex step 5: host configures base 0, canary off, arms the writer (latched via ctrl_commit) ----
        wr_base<=32'h0; wr_canary<=1'b0; wr_arm<=1'b1;
        repeat(40) @(posedge clk);               // let a ctrl_commit edge snapshot arm/base/canary
        count_preGO<=1'b0;

        // ---- Codex step 6: GO — ready must DROP then RISE after rendering ----
        @(negedge clk); feeder_go_tb=1'b1; @(negedge clk); feeder_go_tb=1'b0;
        begin int d=0; while(feeder_ready_tb && d<2000) begin @(posedge clk); d++; end end
        if (feeder_ready_tb!==1'b0) begin $error("[seq] feeder_ready did not drop after GO"); errors++; end
        wait (feeder_ready_tb==1'b1);            // whole scene rendered + drained -> ready rises (fires the EOF flush)

        // ---- Codex step 7: exactly ONE EOF -> exactly ONE frame_drained ----
        begin int d=0; while(!fbw_drained && d<400000) begin @(posedge clk); d++; end end
        if (!fbw_drained)   begin $error("[seq] frame_drained never asserted after GO"); errors++; end
        repeat(200) @(posedge clk);
        if (eof_count!==1)  begin $error("[seq] EOF count=%0d exp 1 (render epoch must emit exactly one)", eof_count); errors++; end
        if (fbw_ovf!==0)    begin $error("[seq] writer FIFO overflow=%0d", fbw_ovf); errors++; end
        if (fbw_bresp_err!==0) begin $error("[seq] writer BRESP errors=%0d", fbw_bresp_err); errors++; end
        $display("[seq] post-GO: eof_count=%0d frame_drained=1 writer_beats=%0d ovf=0", eof_count, fbw_beats);

        // ---- Codex step: bridge receives a synchronized frame_drained ----
        begin int d=0; while(!bridge_drained && d<2000) begin @(posedge bridge_clk); d++; end end
        if (!bridge_drained) begin $error("[seq] synchronized frame_drained never reached the bridge domain"); errors++; end

        // ---- Codex step 8: scanout auto-enables from the ack; score exactly one frame ----
        vid_run=1;
        begin int d=0; while(!vsync && d<200000) begin @(posedge video_clk); d++; end end
        @(posedge video_clk); while(!vsync) @(posedge video_clk);      // prime one frame -> 2nd vsync
        begin scoring=1;
            @(posedge video_clk); while(!vsync) @(posedge video_clk);  // score exactly one frame
            scoring=0;
            repeat(60) @(posedge emif_clk);
            if (fb_reads_last!==BEATS_PER_FRAME) begin
                $error("[seq] scanout read beats/frame=%0d exp %0d (%0dx%0d)", fb_reads_last, BEATS_PER_FRAME, H, ROW_BEATS); errors++;
            end
        end
        if (so_underflow!==0) begin $error("[seq] scanout underflow asserted"); errors++; end
        if (so_rd_errs !==0)  begin $error("[seq] scanout rd_errs=%0d", so_rd_errs); errors++; end
        if (checked < H_ACT*V_ACT) begin $error("[seq] only %0d pixels checked (exp >= %0d)", checked, H_ACT*V_ACT); errors++; end

        // ---- bounded oracle: the rendered frame is a correct SH3 draw (<=1 texel) ----
        begin
            int m_total,m_ok,int_total,int_ok,cb; m_total=0;m_ok=0;int_total=0;int_ok=0;cb=0;
            for (int y=0;y<H;y++) for (int x=0;x<W;x++) begin
                logic [31:0] rm; logic [23:0] fbc; int tu,tv,D; rm=refmap[y*W+x];
                if (rm[31]) begin
                    tu=(rm>>9)&9'h1FF; tv=rm&9'h1FF; fbc=ideal[y*W+x][23:0]; m_total++; D=9;
                    for (int rad=0;rad<=1;rad++) for (int du=-rad;du<=rad;du++) for (int dv=-rad;dv<=rad;dv++) begin
                        int ch; ch=(du<0?-du:du); if((dv<0?-dv:dv)>ch) ch=(dv<0?-dv:dv);
                        if (ch==rad && D==9 && (tu+du)>=0 && (tu+du)<TW && (tv+dv)>=0 && (tv+dv)<TH && fbc===exp_cell(tu+du,tv+dv)) D=rad;
                    end
                    if (D<=1) m_ok++;
                    if (rm[30]) begin int_total++; if (D<=1) int_ok++; end
                    begin bit f; f=1'b0; for (int i=0;i<256;i++) if (pal[i][23:0]===fbc) f=1'b1; if (!f) cb++; end
                end
            end
            $display("[seq][oracle] <=1texel ALL=%0d/%0d (%.1f%%) INT=%.1f%% clut_bad=%0d",
                     m_ok,m_total,(m_total>0)?100.0*m_ok/m_total:0.0,(int_total>0)?100.0*int_ok/int_total:0.0,cb);
            if (cb!==0) begin $error("[seq][oracle] %0d covered px not a CLUT entry", cb); errors++; end
            if (int_total>0 && (100.0*int_ok/int_total)<95.0) begin $error("[seq][oracle] interior <=1texel %.1f%% < 95%%", 100.0*int_ok/int_total); errors++; end
        end

        $display("[tb_top_psmct32_sh3_lpddr_fb_seq] checked=%0d px, read_beats/frame=%0d (exp %0d), eof=%0d underflow=%0b rd_errs=%0d ovf=%0d errors=%0d",
                 checked, fb_reads_last, BEATS_PER_FRAME, eof_count, so_underflow, so_rd_errs, fbw_ovf, errors);
        if (errors==0) $display("[tb_top_psmct32_sh3_lpddr_fb_seq] PASS");
        else           $display("[tb_top_psmct32_sh3_lpddr_fb_seq] FAIL");
        $finish;
    end
    initial begin #40000000; $error("[tb_top_psmct32_sh3_lpddr_fb_seq] TIMEOUT"); $finish; end
endmodule : tb_top_psmct32_sh3_lpddr_fb_seq
