// retroDE_ps2 — tb_vram_bram_stub_equivalence (Ch154)
//
// Focused equivalence TB for the new BRAM-friendly `vram_bram_stub`
// (sync read + byte-WE only, 2048 × 32-bit, no per-bit mask) vs the
// legacy `vram_stub` (combinational dual read + byte-WE + per-bit
// mask RMW, 8192 × 8-bit). Drives both DUTs in lockstep with byte-
// WE-only writes — i.e., write_mask = 0xFFFFFFFF on the legacy
// module, so the per-bit RMW path is a no-op and both modules
// should produce the same data. The new module's reads are
// 1 cycle delayed; the TB samples vram_stub's combinational
// reads ONE CYCLE EARLIER than vram_bram_stub's registered
// reads to align them.
//
// Coverage:
//   Phase 1 — origin word writes (be=4'b1111, fully-overlapped 32-bit).
//   Phase 2 — byte-lane writes (be=4'b0001 / 0010 / 0100 / 1000) at
//             word-aligned addresses, exercising each byte lane.
//   Phase 3 — per-byte non-wrapping admission near MAX_BASE: the
//             write that would wrap past BYTES is dropped on both
//             modules.
//   Phase 4 — dual-port read agreement: read_data and read2_data
//             both produce the same word at the same byte address.
//
// Out of scope (NOT exercised here):
//   - Per-bit mask RMW (Ch106 PSMT4 nibble path). The new module
//     deliberately doesn't support it; the writer-side rework that
//     splices nibbles BEFORE issuing the write lands in Ch155+.
//   - Unaligned reads. vram_stub accepts any byte address; the new
//     module requires word-aligned addresses. The TB only issues
//     word-aligned reads.

`timescale 1ns/1ps

module tb_vram_bram_stub_equivalence;

    localparam int unsigned BYTES = 8192;

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

    // ----------------------------------------------------------------
    // Shared write driver.
    // ----------------------------------------------------------------
    logic        write_en;
    logic [31:0] write_addr;
    logic [31:0] write_data;
    logic [3:0]  write_be;

    // ----------------------------------------------------------------
    // DUT 0 — legacy vram_stub. Drive write_mask = 0xFFFFFFFF so the
    // per-bit RMW path is a no-op and the legacy module behaves as
    // pure byte-WE.
    // ----------------------------------------------------------------
    logic [31:0] legacy_read_addr;
    logic [31:0] legacy_read_data;
    logic [31:0] legacy_read2_addr;
    logic [31:0] legacy_read2_data;

    vram_stub #(.BYTES(BYTES)) u_legacy (
        .clk(clk), .rst_n(rst_n),
        .write_en  (write_en),
        .write_addr(write_addr),
        .write_data(write_data),
        .write_be  (write_be),
        .write_mask(32'hFFFF_FFFF),
        .read_addr (legacy_read_addr),
        .read_data (legacy_read_data),
        .read2_addr(legacy_read2_addr),
        .read2_data(legacy_read2_data)
    );

    // ----------------------------------------------------------------
    // DUT 1 — new vram_bram_stub. Same write inputs.
    // ----------------------------------------------------------------
    logic [31:0] bram_read_addr;
    logic [31:0] bram_read_data;
    logic        bram_read_valid;
    logic [31:0] bram_read2_addr;
    logic [31:0] bram_read2_data;
    logic        bram_read2_valid;

    vram_bram_stub #(.BYTES(BYTES)) u_bram (
        .clk(clk), .rst_n(rst_n),
        .write_en  (write_en),
        .write_addr(write_addr),
        .write_data(write_data),
        .write_be  (write_be),
        .read_addr (bram_read_addr),
        .read_data (bram_read_data),
        .read_valid(bram_read_valid),
        .read2_addr(bram_read2_addr),
        .read2_data(bram_read2_data),
        .read2_valid(bram_read2_valid)
    );

    // ----------------------------------------------------------------
    // Helpers.
    // ----------------------------------------------------------------
    int errors;
    initial begin
        errors            = 0;
        write_en          = 1'b0;
        write_addr        = 32'd0;
        write_data        = 32'd0;
        write_be          = 4'b0000;
        legacy_read_addr  = 32'd0;
        legacy_read2_addr = 32'd0;
        bram_read_addr    = 32'd0;
        bram_read2_addr   = 32'd0;
    end

    task automatic do_write(input logic [31:0] addr,
                            input logic [31:0] data,
                            input logic [3:0]  be);
        @(negedge clk);
        write_en   = 1'b1;
        write_addr = addr;
        write_data = data;
        write_be   = be;
        @(posedge clk);
        @(negedge clk);
        write_en   = 1'b0;
        write_addr = 32'd0;
        write_data = 32'd0;
        write_be   = 4'b0000;
    endtask

    // Cross-port equivalence read at word-aligned `addr`. Issues the
    // address to both modules' read_addr port; the legacy module's
    // read_data is combinational (settles immediately) and the new
    // module's read_data is registered (1-cycle delay). Sample the
    // legacy module's value AT the read-issue cycle, sample the new
    // module's value ONE CYCLE LATER, then compare.
    task automatic check_word(input logic [31:0] addr,
                              input logic [31:0] expected,
                              input string       phase);
        logic [31:0] legacy_v;
        logic [31:0] bram_v;
        @(negedge clk);
        legacy_read_addr = addr;
        bram_read_addr   = addr;
        #1;
        legacy_v = legacy_read_data;
        @(posedge clk);
        @(negedge clk);
        bram_v = bram_read_data;
        if (legacy_v !== expected) begin
            $error("[%s] legacy read @0x%08x got 0x%08x expected 0x%08x",
                   phase, addr, legacy_v, expected);
            errors = errors + 1;
        end
        if (bram_v !== expected) begin
            $error("[%s] bram read @0x%08x got 0x%08x expected 0x%08x",
                   phase, addr, bram_v, expected);
            errors = errors + 1;
        end
        if (!bram_read_valid) begin
            $error("[%s] bram read_valid low @0x%08x", phase, addr);
            errors = errors + 1;
        end
    endtask

    // Same shape but on read port 2.
    task automatic check_word2(input logic [31:0] addr,
                               input logic [31:0] expected,
                               input string       phase);
        logic [31:0] legacy_v;
        logic [31:0] bram_v;
        @(negedge clk);
        legacy_read2_addr = addr;
        bram_read2_addr   = addr;
        #1;
        legacy_v = legacy_read2_data;
        @(posedge clk);
        @(negedge clk);
        bram_v = bram_read2_data;
        if (legacy_v !== expected) begin
            $error("[%s/r2] legacy read2 @0x%08x got 0x%08x expected 0x%08x",
                   phase, addr, legacy_v, expected);
            errors = errors + 1;
        end
        if (bram_v !== expected) begin
            $error("[%s/r2] bram read2 @0x%08x got 0x%08x expected 0x%08x",
                   phase, addr, bram_v, expected);
            errors = errors + 1;
        end
    endtask

    // ----------------------------------------------------------------
    // Drive sequence.
    // ----------------------------------------------------------------
    initial begin
        rst_n = 1'b0;
        repeat (4) @(posedge clk);
        rst_n = 1'b1;
        repeat (4) @(posedge clk);

        // ------------------------------------------------------------
        // Phase 1 — origin word writes (be=4'b1111).
        // ------------------------------------------------------------
        do_write(32'h0000_0000, 32'hDEAD_BEEF, 4'b1111);
        do_write(32'h0000_0004, 32'hCAFE_BABE, 4'b1111);
        do_write(32'h0000_0010, 32'h1234_5678, 4'b1111);
        check_word (32'h0000_0000, 32'hDEAD_BEEF, "P1-w0");
        check_word (32'h0000_0004, 32'hCAFE_BABE, "P1-w1");
        check_word (32'h0000_0010, 32'h1234_5678, "P1-w2");
        check_word (32'h0000_0008, 32'h0000_0000, "P1-zero");

        // ------------------------------------------------------------
        // Phase 2 — byte-lane writes. Build 0xAABBCCDD one byte at a
        // time, asserting the partial state matches between modules
        // after each lane.
        // ------------------------------------------------------------
        do_write(32'h0000_0100, 32'h0000_00DD, 4'b0001);
        check_word (32'h0000_0100, 32'h0000_00DD, "P2-b0");
        do_write(32'h0000_0100, 32'h0000_CC00, 4'b0010);
        check_word (32'h0000_0100, 32'h0000_CCDD, "P2-b1");
        do_write(32'h0000_0100, 32'h00BB_0000, 4'b0100);
        check_word (32'h0000_0100, 32'h00BB_CCDD, "P2-b2");
        do_write(32'h0000_0100, 32'hAA00_0000, 4'b1000);
        check_word (32'h0000_0100, 32'hAABB_CCDD, "P2-b3");

        // ------------------------------------------------------------
        // Phase 3 — per-byte non-wrapping admission near MAX_BASE.
        // BYTES=8192 so bytes [8188..8191] is the last word.
        // A write at addr=8188, be=4'b1111 is admitted (b3 lands at
        // byte 8191, still < BYTES=8192).
        // A write at addr=8192 is BEYOND the array; both modules
        // must drop it (legacy via per-byte addr33 admission, bram
        // via the same gate).
        // ------------------------------------------------------------
        do_write(32'h0000_1FFC, 32'h11223344, 4'b1111);
        check_word(32'h0000_1FFC, 32'h11223344, "P3-last-word");

        do_write(32'h0000_2000, 32'hFFFFFFFF, 4'b1111);  // beyond
        // Reading addr=8192 (out of range): legacy vram_stub gates
        // with `read_addr <= MAX_BASE` and returns 0; vram_bram_stub
        // does NOT gate the BRAM data path (the canonical Quartus
        // inference template demands an unconditional registered
        // read), so out-of-range data is GARBAGE — only `read_valid`
        // is the contract. Check it stays low.
        @(negedge clk);
        legacy_read_addr = 32'h0000_2000;
        bram_read_addr   = 32'h0000_2000;
        #1;
        if (legacy_read_data !== 32'd0) begin
            $error("[P3-oor] legacy out-of-range read got 0x%08x expected 0",
                   legacy_read_data);
            errors = errors + 1;
        end
        @(posedge clk);
        @(negedge clk);
        // bram_read_data deliberately not asserted — out-of-range
        // reads return X / garbage on the new module.
        if (bram_read_valid) begin
            $error("[P3-oor] bram read_valid high on out-of-range read");
            errors = errors + 1;
        end

        // ------------------------------------------------------------
        // Phase 4 — dual-port read agreement.
        // ------------------------------------------------------------
        check_word2(32'h0000_0000, 32'hDEAD_BEEF, "P4-r2-w0");
        check_word2(32'h0000_0100, 32'hAABB_CCDD, "P4-r2-bytewise");

        // ------------------------------------------------------------
        // Phase 5 — out-of-contract documentation.
        //
        // The legacy `vram_stub` accepts byte-addressed writes (any
        // `write_addr[1:0]`); the new `vram_bram_stub` REQUIRES
        // word-aligned writes and DROPS the write entirely when
        // `write_addr[1:0] != 0`. Today's PSMCT16 / PSMT8 / PSMT4
        // raster + image-xfer paths emit at sub-word addresses, so
        // a swap of the board top to `vram_bram_stub` without
        // writer-side normalization would silently drop those
        // writes. This phase pins that contract divergence: drive
        // an unaligned byte write to addr=0x205 with be=4'b0001 +
        // data=0x99, and assert that the legacy module commits
        // (mem[0x205]=0x99, so word at 0x204 reads as
        // 0x00_99_00_00) while the new module drops it (word at
        // 0x204 stays at whatever Phase 1-3 left there, which is
        // zero — no writes have touched 0x204..0x207 yet).
        //
        // This is NOT a failure mode — it's a documented contract
        // boundary that Ch155+ writer-side normalization must
        // bridge for the CT16/T8/T4 paths. See the Ch154 doc
        // section "Ch155+ surface — writer-side normalization for
        // ALL sub-32-bit PSMs" for the rework plan.
        // ------------------------------------------------------------
        do_write(32'h0000_0205, 32'h0000_0099, 4'b0001);

        // Read word at 0x204 from both modules.
        @(negedge clk);
        legacy_read_addr = 32'h0000_0204;
        bram_read_addr   = 32'h0000_0204;
        #1;
        // Legacy commits the byte at the unaligned addr; bytes
        // 0x204 / 0x206 / 0x207 stay zero from time-0; byte 0x205
        // is now 0x99. Word read = {0x00, 0x00, 0x99, 0x00} =
        // 0x0000_9900.
        if (legacy_read_data !== 32'h0000_9900) begin
            $error("[P5-doc] legacy unaligned-write commit got 0x%08x expected 0x00009900",
                   legacy_read_data);
            errors = errors + 1;
        end
        @(posedge clk);
        @(negedge clk);
        // Bram dropped the write — word at 0x204 is still 0.
        if (bram_read_data !== 32'h0000_0000) begin
            $error("[P5-doc] bram unaligned-write was NOT dropped: 0x%08x (expected 0)",
                   bram_read_data);
            errors = errors + 1;
        end

        $display("[tb_vram_bram_stub_equivalence] Phase 5 documented contract divergence:");
        $display("  legacy vram_stub      committed 0x99 at byte 0x205 (read word 0x204 = 0x00009900)");
        $display("  vram_bram_stub        dropped the unaligned write   (read word 0x204 = 0x00000000)");
        $display("  Ch155+ writer-side normalization will bridge this for CT16/T8/T4 paths.");

        // ------------------------------------------------------------
        // Final report.
        // ------------------------------------------------------------
        repeat (4) @(posedge clk);
        $display("[tb_vram_bram_stub_equivalence] errors=%0d", errors);
        if (errors == 0) $display("[tb_vram_bram_stub_equivalence] PASS");
        else             $display("[tb_vram_bram_stub_equivalence] FAIL");
        $finish;
    end

    initial begin
        #1000000;
        $error("[tb_vram_bram_stub_equivalence] TIMEOUT");
        $finish;
    end

endmodule : tb_vram_bram_stub_equivalence
