// retroDE_ps2 — tb_top_psmct32_raster_demo_bram_ch171
//
// Spot-check TB for the Ch251 320x240 animated demo (was Ch171 four-
// quadrant pre-Ch251 — the TB name is kept to avoid Makefile churn,
// but the assertions track the Ch251 SPRITE layout instead).
//
// Instantiates top_psmct32_raster_demo_bram with the board-wrapper
// overrides (H_ACTIVE=640 / V_ACTIVE=480, VRAM_BYTES=512 KiB) so the
// full 640x480 scanout is exercised, with the Ch251 fixtures painting
// the 320x240 demo content in the upper-left:
//
//   - 8 vertical color bars (40 px wide each, full screen height)
//   - 4 grey border strips (4 px thick)
//   - 4 orange corner-alignment markers (8x8)
//   - 1 cyan/red heartbeat at (152..167, 112..127) — NOT probed here
//     because its color toggles between two values as the EE bootlet's
//     loop iterates. Pixel-level heartbeat behavior is observed
//     visually on hardware; this TB cares about the static portion.
//
// Probed coordinates (all outside the heartbeat box):
//
//   Bar centers — bar index = x/40:
//     ( 20,  80) bar 0 white
//     ( 60,  80) bar 1 yellow
//     (100,  80) bar 2 cyan
//     (220,  80) bar 5 red
//     (260,  80) bar 6 blue
//     (300,  80) bar 7 black
//   Border (mid-edge, non-corner):
//     (160,   2) top    grey
//     (160, 237) bottom grey
//   Corner markers:
//     (  4,   4) TL orange
//     (315,   4) TR orange
//   Outside DISPLAY1 (VRAM-side zero):
//     (400, 100)
//     (100, 300)
//
// VRAM contents are probed via hierarchical reference into dut.u_vram.mem;
// scanout pixels are captured into per-coord latches as r/g/b/de samples
// land on the wire.

`timescale 1ns/1ps

module tb_top_psmct32_raster_demo_bram_ch171;

    // Match the board-wrapper Ch169 + Ch171 overrides.
    localparam int H_ACTIVE   = 640;
    localparam int V_ACTIVE   = 480;
    localparam int H_FRONT    = 16;
    localparam int H_SYNC_W   = 96;
    localparam int H_BACK     = 48;
    localparam int V_FRONT    = 10;
    localparam int V_SYNC_W   = 2;
    localparam int V_BACK     = 33;
    localparam int VRAM_BYTES = 512 * 1024;

    logic clk;
    logic 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;
    logic        dma_done_seen;
    logic        frame_seen;

    // Ch255 — heartbeat color override inputs (Sony ○ = A, × = B
    // bits on INPUT_P1_RAW). Default 0 so the existing heartbeat /
    // pixel-pattern assertions below see the EE-driven cyan.
    logic        joy_a_pressed;
    logic        joy_b_pressed;
    initial begin
        joy_a_pressed = 1'b0;
        joy_b_pressed = 1'b0;
    end

    top_psmct32_raster_demo_bram #(
        .H_ACTIVE   (H_ACTIVE),
        .V_ACTIVE   (V_ACTIVE),
        .H_FRONT    (H_FRONT),
        .H_SYNC     (H_SYNC_W),
        .H_BACK     (H_BACK),
        .V_FRONT    (V_FRONT),
        .V_SYNC     (V_SYNC_W),
        .V_BACK     (V_BACK),
        .VRAM_BYTES (VRAM_BYTES),
        // Ch252 — mirror the hardware build profile: 512 KiB VRAM
        // with ENABLE_READ2=0. The PSMCT32-only demo never consults
        // read2, so disabling it both keeps the integration test
        // faithful to silicon and clears the Ch252 replication-
        // tripwire ($fatal) inside `vram_bram_stub`. PSMT4 RMW
        // coverage continues to live in the dedicated PSMT4 TBs at
        // the default (small VRAM_BYTES, ENABLE_READ2=1).
        .VRAM_ENABLE_READ2 (1'b0),
        // Ch296 — mirror the hardware build's useg-shadow strip. This TB
        // is the bram-top integration check that proves the board config
        // (USEG_SHADOW_ENABLE=0) renders the PSMCT32 raster demo
        // correctly with the ~33k-FF useg_shadow_mem array removed. The
        // SPRITE-only bootlet issues no useg traffic, so the strip is a
        // no-op for the rendered output.
        .USEG_SHADOW_ENABLE (1'b0)
    ) 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),
        .joy_a_pressed_i(joy_a_pressed),
        .joy_b_pressed_i(joy_b_pressed)
    );

    // ----------------------------------------------------------
    // Expected pixel color (matches build_ch251_sprites() in bake.py).
    // See module-header comment for the SPRITE layout summary.
    // Outside (any x>=320 or y>=240): black.
    // ----------------------------------------------------------
    // ----------------------------------------------------------
    // Ch251 expected ABGR pattern. Painting order is bottom-up
    // (later SPRITEs win): 8 color bars (full screen) → 4 grey
    // border strips (4 px thick, x∈[0,3]/[316,319] and y∈[0,3]/
    // [236,239]) → 4 orange corner markers (8×8 squares at the
    // four corners) → 1 cyan-initially heartbeat at (152..167,
    // 112..127). Outside DISPLAY1 (x>=320 or y>=240) = black.
    //
    // The TB samples non-heartbeat coordinates so the result is
    // independent of how many loop iterations the bootlet has
    // executed when we probe VRAM.
    // ----------------------------------------------------------
    localparam int BORDER_W = 4;
    localparam int CORNER_S = 8;

    function automatic logic ch251_in_corner(input int x, input int y);
        return ((x < CORNER_S          && y < CORNER_S)         ||
                (x >= 320 - CORNER_S   && y < CORNER_S)         ||
                (x < CORNER_S          && y >= 240 - CORNER_S)  ||
                (x >= 320 - CORNER_S   && y >= 240 - CORNER_S));
    endfunction
    function automatic logic ch251_in_border(input int x, input int y);
        return (x < BORDER_W) || (x >= 320 - BORDER_W)
            || (y < BORDER_W) || (y >= 240 - BORDER_W);
    endfunction

    // 8 color bars (matches build_ch251_sprites() in bake.py).
    function automatic logic [7:0] ch251_bar_r(input int bar);
        case (bar)
            0: return 8'hFF;  1: return 8'hFF;  2: return 8'h00;  3: return 8'h00;
            4: return 8'hFF;  5: return 8'hFF;  6: return 8'h00;  default: return 8'h00;
        endcase
    endfunction
    function automatic logic [7:0] ch251_bar_g(input int bar);
        case (bar)
            0: return 8'hFF;  1: return 8'hFF;  2: return 8'hFF;  3: return 8'hFF;
            4: return 8'h00;  5: return 8'h00;  6: return 8'h00;  default: return 8'h00;
        endcase
    endfunction
    function automatic logic [7:0] ch251_bar_b(input int bar);
        case (bar)
            0: return 8'hFF;  1: return 8'h00;  2: return 8'hFF;  3: return 8'h00;
            4: return 8'hFF;  5: return 8'h00;  6: return 8'hFF;  default: return 8'h00;
        endcase
    endfunction

    function automatic logic [7:0] expected_r_at(input int x, input int y);
        if (x >= 320 || y >= 240)  return 8'h00;          // outside DISPLAY1
        if (ch251_in_corner(x, y)) return 8'hFF;          // orange (R=FF)
        if (ch251_in_border(x, y)) return 8'h80;          // grey
        return ch251_bar_r(x / 40);
    endfunction
    function automatic logic [7:0] expected_g_at(input int x, input int y);
        if (x >= 320 || y >= 240)  return 8'h00;
        if (ch251_in_corner(x, y)) return 8'h80;          // orange (G=80)
        if (ch251_in_border(x, y)) return 8'h80;          // grey
        return ch251_bar_g(x / 40);
    endfunction
    function automatic logic [7:0] expected_b_at(input int x, input int y);
        if (x >= 320 || y >= 240)  return 8'h00;
        if (ch251_in_corner(x, y)) return 8'h00;          // orange (B=00)
        if (ch251_in_border(x, y)) return 8'h80;          // grey
        return ch251_bar_b(x / 40);
    endfunction

    // ABGR word stored in VRAM: {A, B, G, R}. ABGR is what
    // gs_stub raster channel writes; PCRTC then re-decodes
    // back to r/g/b on scanout.
    function automatic logic [31:0] expected_vram_at(input int x, input int y);
        return {8'hFF, expected_b_at(x, y), expected_g_at(x, y), expected_r_at(x, y)};
    endfunction

    // ----------------------------------------------------------
    // PSMCT32 swizzle reference (page/block walk per real PS2 GS
    // semantics). Same shape as Ch155/Ch158 — refactored verbatim
    // so this TB can probe VRAM by (x, y) at the Ch171 320x240
    // pattern. fbp=0, fbw=1 from the Ch171 bake.
    // ----------------------------------------------------------
    function automatic int ref_block_idx_ct32(input int by, input int bx);
        case ({by[1:0], bx[2:0]})
            5'd0:  return  0;  5'd1:  return  1;  5'd2:  return  4;  5'd3:  return  5;
            5'd4:  return 16;  5'd5:  return 17;  5'd6:  return 20;  5'd7:  return 21;
            5'd8:  return  2;  5'd9:  return  3;  5'd10: return  6;  5'd11: return  7;
            5'd12: return 18;  5'd13: return 19;  5'd14: return 22;  5'd15: return 23;
            5'd16: return  8;  5'd17: return  9;  5'd18: return 12;  5'd19: return 13;
            5'd20: return 24;  5'd21: return 25;  5'd22: return 28;  5'd23: return 29;
            5'd24: return 10;  5'd25: return 11;  5'd26: return 14;  5'd27: return 15;
            5'd28: return 26;  5'd29: return 27;  5'd30: return 30;  default: return 31;
        endcase
    endfunction
    function automatic int ref_addr_psmct32(input int x_v, input int y_v);
        // fbp=0, fbw=5 — matches Ch171 bake.py DISPFB1 / FRAME_1 (320-wide
        // framebuffer = 5 pages of 64 px). The Ch171 first-board-test bug
        // was an FBW=1 mismatch here AND in bake.py; both ends agreed
        // (so this TB happily passed) but the resulting page layout
        // wrapped horizontally every 64 px on the monitor → visible
        // staircase. With FBW=5, raster + scanout + this reference all
        // agree on the spatial-correct 320-wide page grid.
        int page_x, page_y, page_idx, page_base;
        int by, bx, blk_idx, xb, yb;
        page_x   = x_v / 64;
        page_y   = y_v / 32;
        page_idx = page_y * 5 + page_x;     // fbw=5
        page_base = page_idx * 8192;
        by  = (y_v % 32) / 8;
        bx  = (x_v % 64) / 8;
        blk_idx = ref_block_idx_ct32(by, bx);
        xb = x_v % 8;
        yb = y_v % 8;
        return page_base + blk_idx * 256 + yb * 32 + xb * 4;
    endfunction

    // ----------------------------------------------------------
    // Scanout capture latches. Rather than a 640x480 array (≈300K
    // entries — bloats sim memory), keep a small list of probe
    // coordinates and latch r/g/b/de whenever the scanout
    // (hcnt, vcnt) hits one of them.
    // ----------------------------------------------------------
    // VRAM probe (x, y) table — inside the 320x240 paint region.
    // Initialized in `initial begin` below (iverilog 12 doesn't
    // accept '{...} array literals on unpacked arrays).
    //
    // Ch171 board-iter-2 audit: previous probe set only sampled Q3
    // center. When hardware showed Q3 missing (black instead of
    // white), tightening this with Q3-corner probes was the first
    // diagnostic — if sim PASSES every Q3 corner but hardware
    // still drops it, the divergence is in BRAM/scanout/PCRTC
    // hardware behavior (not the GS raster path).
    localparam int N_PROBES = 10;
    int probe_x [N_PROBES];
    int probe_y [N_PROBES];

    // Ch251 probe set — sample non-heartbeat coordinates so the
    // assertions are stable regardless of bootlet loop iteration.
    // Heartbeat sits at (152..167, 112..127); everything below
    // avoids that box.
    initial begin
        // Bar centers (y=80 picked to dodge top border / corner markers)
        probe_x[0]  =  20; probe_y[0]  =  80;   // bar 0 white
        probe_x[1]  =  60; probe_y[1]  =  80;   // bar 1 yellow
        probe_x[2]  = 100; probe_y[2]  =  80;   // bar 2 cyan
        probe_x[3]  = 220; probe_y[3]  =  80;   // bar 5 red
        probe_x[4]  = 260; probe_y[4]  =  80;   // bar 6 blue
        probe_x[5]  = 300; probe_y[5]  =  80;   // bar 7 black
        // Border samples (non-corner)
        probe_x[6]  = 160; probe_y[6]  =   2;   // top border, mid
        probe_x[7]  = 160; probe_y[7]  = 237;   // bottom border, mid
        // Corner markers
        probe_x[8]  =   4; probe_y[8]  =   4;   // TL corner (orange)
        probe_x[9]  = 315; probe_y[9]  =   4;   // TR corner (orange)
        // Outside-DISPLAY1 VRAM probes were dropped: PSMCT32 page
        // swizzling at FBW=5 aliases x>=320 reads back into the
        // active framebuffer (so VRAM at e.g. (400,100) reads the
        // heartbeat byte, not the "unwritten zero" the naive
        // address math would suggest). The active area is the
        // demo content; outside-DISPLAY1 black is a PCRTC scanout
        // property, tested implicitly by the scanout latches —
        // the per-pixel scanout-side assertions of the Ch171
        // original aren't in scope for Ch251's spot-check.
    end

    int errors;
    initial errors = 0;

    // Module-scope locals for the probe loop (hoisted out of the
    // `for` body to dodge an iverilog 12 declaration-with-initializer
    // mis-elaboration that silently zeroed loop-local ints).
    int          probe_loop_x;
    int          probe_loop_y;
    int          probe_loop_byte_addr;
    logic [31:0] probe_loop_expected;
    logic [31:0] probe_loop_got;

    // Ch255 — snapshot of the EE-driven hb_rgbaq_reg taken just
    // before the override sweep so XOR / passthrough assertions can
    // reference a stable value.
    logic [31:0] ch255_baseline;

    initial begin
        rst_n   = 1'b0;
        core_go = 1'b0;
        repeat (10) @(posedge clk);
        rst_n = 1'b1;
        repeat (4)  @(posedge clk);

        @(negedge clk); core_go = 1'b1;
        @(negedge clk); core_go = 1'b0;

        // Ch251 — wait for the FIRST DMAC drain + raster idle. The
        // animated bootlet loops forever, so we don't gate on
        // core_halt anymore. Frame heartbeat checks (Ch251 motion
        // proof) live in tb_de25_nano_psmct32_raster_demo_top; this
        // TB is the pixel-pattern spot-check.
        wait (dma_done_seen == 1'b1);
        repeat (10) @(posedge clk);
        if (dut.xfer_busy == 1'b1)          wait (dut.xfer_busy == 1'b0);
        if (dut.u_gs.raster_active == 1'b1) wait (dut.u_gs.raster_active == 1'b0);
        repeat (40) @(posedge clk);

        // -----------------------------------------------------
        // VRAM probe spot-checks. ABGR-word-in-VRAM is what the
        // raster sink stores. Sampling here proves the entire
        // GS path is alive (DMAC → GIF → raster → VRAM) at the
        // probe coordinates. The "rest of screen is black" check
        // belongs on hardware (it's a PCRTC scanout-side
        // property gated by DISPLAY1, not a VRAM-side property).
        // -----------------------------------------------------
        // Probe loop. Hoisted local int decls to module scope so
        // iverilog 12 doesn't silently zero them per-iteration
        // (declarations-with-initializers inside for-loop bodies
        // mis-elaborate in this tool version; the silent-zero made
        // the TB a false-positive for Q3 probes until caught).
        for (int i = 0; i < N_PROBES; i++) begin
            probe_loop_x         = probe_x[i];
            probe_loop_y         = probe_y[i];
            probe_loop_byte_addr = ref_addr_psmct32(probe_loop_x, probe_loop_y);
            probe_loop_expected  = expected_vram_at(probe_loop_x, probe_loop_y);
            probe_loop_got       = dut.u_vram.mem[probe_loop_byte_addr >> 2];
            if (probe_loop_got !== probe_loop_expected) begin
                $error("[vram] probe[%0d] @(%0d,%0d) got 0x%08x expected 0x%08x",
                       i, probe_loop_x, probe_loop_y,
                       probe_loop_got, probe_loop_expected);
                errors = errors + 1;
            end
        end

        // Status latches. Ch251: core_halt is EXPECTED LOW (bootlet
        // loops forever). dma_done_seen + frame_seen prove the
        // demo's GIF/raster pipeline is alive.
        if (core_halt)      begin $error("core_halt high at end (Ch251 bootlet must loop, not SYSCALL)"); errors = errors + 1; end
        if (!dma_done_seen) begin $error("dma_done_seen never latched");      errors = errors + 1; end
        if (!frame_seen)    begin $error("frame_seen never latched");         errors = errors + 1; end

        // ----------------------------------------------------------
        // Ch251 — verify the initial DMAC kick painted the heartbeat
        // RGBAQ qword with CYAN. The bootlet then animates this via
        // XOR + SW between CYAN and RED in its main loop; with the
        // production-grade delay (~1 Hz at 50 MHz hardware) catching
        // a toggle in sim would need >30 s sim time, so we only
        // sample the first-kick state here. The dynamic blink is
        // a hardware-monitor proof, not a sim regression.
        // ----------------------------------------------------------
        if (dut.u_ram.mem[115][31:0] !== 32'hFFFFFF00) begin
            $error("heartbeat RGBAQ qword[115][31:0] = 0x%08x at first-DMAC-done (expected 0xFFFFFF00 CYAN from bake.py)",
                   dut.u_ram.mem[115][31:0]);
            errors = errors + 1;
        end

        // ----------------------------------------------------------
        // Ch255 — exercise the wrapper-side heartbeat override mux.
        // The mux feeds the splicer (which only fires when DMAC reads
        // qword 115); to keep the test fast and deterministic we
        // probe the combinational `hb_rgbaq_effective` wire directly
        // for each of the four {joy_a, joy_b} input combinations.
        // We also re-check `hb_rgbaq_reg` after each step to confirm
        // the override does NOT corrupt the EE-driven background
        // animation (the EE keeps owning hb_rgbaq_reg via SW writes).
        // ----------------------------------------------------------
        // Snapshot the current EE-driven value so the XOR check uses
        // the actual reg state rather than the bake.py initial. The
        // bootlet may have toggled it by now.
        ch255_baseline = dut.hb_rgbaq_reg;

        // {A=0, B=0} → mux passes hb_rgbaq_reg through.
        joy_a_pressed = 1'b0;
        joy_b_pressed = 1'b0;
        @(negedge clk);
        if (dut.hb_rgbaq_effective !== ch255_baseline) begin
            $error("[Ch255 override] {A=0,B=0}: hb_rgbaq_effective=0x%08x expected baseline 0x%08x",
                   dut.hb_rgbaq_effective, ch255_baseline);
            errors = errors + 1;
        end

        // {A=1, B=0} → force RED.
        joy_a_pressed = 1'b1;
        joy_b_pressed = 1'b0;
        @(negedge clk);
        if (dut.hb_rgbaq_effective !== 32'hFF00_00FF) begin
            $error("[Ch255 override] {A=1,B=0}: hb_rgbaq_effective=0x%08x expected 0xFF0000FF (RED)",
                   dut.hb_rgbaq_effective);
            errors = errors + 1;
        end
        if (dut.hb_rgbaq_reg !== ch255_baseline) begin
            $error("[Ch255 override] {A=1,B=0}: hb_rgbaq_reg corrupted from 0x%08x to 0x%08x",
                   ch255_baseline, dut.hb_rgbaq_reg);
            errors = errors + 1;
        end

        // {A=0, B=1} → force CYAN.
        joy_a_pressed = 1'b0;
        joy_b_pressed = 1'b1;
        @(negedge clk);
        if (dut.hb_rgbaq_effective !== 32'hFFFF_FF00) begin
            $error("[Ch255 override] {A=0,B=1}: hb_rgbaq_effective=0x%08x expected 0xFFFFFF00 (CYAN)",
                   dut.hb_rgbaq_effective);
            errors = errors + 1;
        end

        // {A=1, B=1} → invert (XOR with 0x00FFFFFF) of the live
        // EE-driven value at THIS moment.
        joy_a_pressed = 1'b1;
        joy_b_pressed = 1'b1;
        @(negedge clk);
        if (dut.hb_rgbaq_effective !== (dut.hb_rgbaq_reg ^ 32'h00FF_FFFF)) begin
            $error("[Ch255 override] {A=1,B=1}: hb_rgbaq_effective=0x%08x expected 0x%08x (XOR of hb_rgbaq_reg=0x%08x)",
                   dut.hb_rgbaq_effective,
                   dut.hb_rgbaq_reg ^ 32'h00FF_FFFF,
                   dut.hb_rgbaq_reg);
            errors = errors + 1;
        end

        // Release — mux returns to passthrough.
        joy_a_pressed = 1'b0;
        joy_b_pressed = 1'b0;
        @(negedge clk);
        if (dut.hb_rgbaq_effective !== dut.hb_rgbaq_reg) begin
            $error("[Ch255 override] release: hb_rgbaq_effective=0x%08x expected hb_rgbaq_reg=0x%08x",
                   dut.hb_rgbaq_effective, dut.hb_rgbaq_reg);
            errors = errors + 1;
        end

        $display("[tb_top_psmct32_raster_demo_bram_ch171] errors=%0d", errors);
        if (errors == 0) $display("[tb_top_psmct32_raster_demo_bram_ch171] PASS");
        else             $display("[tb_top_psmct32_raster_demo_bram_ch171] FAIL");
        $finish;
    end

    // Watchdog — generous because we sit through a full 640×480 frame.
    initial begin
        #30_000_000;
        $error("[tb_top_psmct32_raster_demo_bram_ch171] TIMEOUT");
        $finish;
    end

endmodule : tb_top_psmct32_raster_demo_bram_ch171
