// retroDE_ps2 — vram_bram_stub (Ch154)
//
// Hardware-friendly sibling of `vram_stub`. Maps cleanly onto Agilex 5
// M20K block-RAM:
//   - 2048 × 32-bit word storage (instead of 8192 × 8-bit byte
//     storage). Internal width matches Agilex M20K native widths;
//     external addressing stays byte-addressable to keep the same
//     mental model as `vram_stub`.
//   - SYNCHRONOUS reads (registered 32-bit output). One-cycle read
//     latency — the rd_valid pulse fires the cycle the data is on
//     read_data.
//   - BYTE write enable only (4-bit `write_be`). The Ch106 PSMT4
//     per-bit `write_mask` RMW is NOT supported; PSMT4 callers must
//     do the nibble splice on the writer side BEFORE issuing the
//     write here. Ch155+ task to rework gs_stub.raster_pixel_emit
//     and gif_image_xfer_stub for that.
//   - Two synchronous read ports. Quartus implements two
//     independent read addresses by REPLICATING the M20K storage
//     across two RAM blocks rather than using a single native
//     dual-read port — exp_c shows 8 RAM Blocks for 8 KB vs
//     exp_a's 4 RAM Blocks for the same 8 KB single-port shape.
//     Two replicated RAM blocks is still vastly cheaper than the
//     65,536 flip-flops the legacy `vram_stub` shape produced;
//     the cost just isn't free.
//
// Empirical motivation (Ch153 forensics):
//   The legacy `vram_stub` shape (byte-addressable + combinational
//   dual reads + per-bit-mask RMW) failed to fit on Agilex 5 — the
//   8 KB array consumed 65,536 dedicated registers and 261,578
//   combinational nodes, dominating Ch152's 331 % ALM overrun.
//   `exp_a_bram_friendly` proved that a 2048 × 32-bit sync-read
//   byte-WE shape maps to 4 RAM Blocks + 0 registers + 46 ALMs.
//
// External port shape vs `vram_stub`:
//   IDENTICAL: clk, rst_n, write_en, write_addr[31:0],
//              write_data[31:0], write_be[3:0], read_addr[31:0],
//              read_data[31:0], read2_addr[31:0], read2_data[31:0].
//   NEW       : read_valid + read2_valid (1-cycle pulse with the data).
//   DROPPED   : write_mask[31:0] (Ch106 per-bit RMW; callers must
//              splice nibbles on the writer side).
//
// Address contract:
//   - Writes: write_addr is byte-aligned; the low 2 bits MUST be 0
//             (4-byte writes only). Each `write_be[i]` independently
//             commits byte `i` of the addressed word. Per-byte non-
//             wrapping admission: an enabled byte beyond `BYTES`
//             drops the WHOLE write (matches vram_stub Ch95 audit).
//   - Reads:  read_addr is byte-aligned; the low 2 bits MUST be 0.
//             `read_data` is the 32-bit word at `read_addr / 4`.
//             Byte / halfword extraction is the caller's job
//             (matches Ch141 / Ch142 nibble-readback pattern).
//
// Sim behaviour: time-0 mem is power-on-zero matching real M20K (the
// `// synthesis translate_off` initial block matches vram_stub's
// post-Ch152 pattern).

`timescale 1ns/1ps

module vram_bram_stub #(
    parameter int unsigned BYTES = 8192,

    // Ch251.4 — hardware-demo M20K rescue. When ENABLE_READ2 = 0, the
    // second sync-read port is FEATURE-STRIPPED: `read2_data` ties to
    // 0, `read2_valid` ties to 0, and Quartus no longer infers a
    // separate read port on `mem`. This collapses the storage from
    // two replicated 1W+1R simple-dual-port M20K banks (~410 M20Ks at
    // 512 KiB) to ONE 1W+1R bank (~205 M20Ks) — the savings that get
    // the 512 KiB framebuffer to fit on Agilex 5 (358 M20K budget).
    //
    // Contract caveat: read2 is the PSMT4 RMW old-byte read path. Any
    // build that exercises PSMT4 rasterization MUST keep this `1`. The
    // PSMCT32-only hardware demo (top_psmct32_raster_demo_bram) sets
    // it to `0`; all simulation TBs leave it at the default `1`.
    //
    // This is a SCOPED build profile, not a general fix — see
    // docs/decisions/0006-vram-roadmap.md for the longer-term
    // arbitrated / line-buffered VRAM plan.
    parameter bit         ENABLE_READ2 = 1'b1
) (
    input  logic        clk,
    input  logic        rst_n,

    // Write port (byte-WE; 4-byte-aligned write_addr).
    input  logic        write_en,
    input  logic [31:0] write_addr,
    input  logic [31:0] write_data,
    input  logic [3:0]  write_be,

    // Read port 0 (sync read; 4-byte-aligned read_addr).
    input  logic [31:0] read_addr,
    output logic [31:0] read_data,
    output logic        read_valid,

    // Read port 1 (sync read; 4-byte-aligned).
    input  logic [31:0] read2_addr,
    output logic [31:0] read2_data,
    output logic        read2_valid
);

    // 2048 × 32-bit storage. Index is the WORD index (write_addr / 4).
    //
    // Parameter contract: `BYTES` MUST be a power-of-two multiple of 4.
    // The WORD_AW-bit slice `*_addr[WORD_AW+1:2]` truncates the byte
    // address to a word index; for non-power-of-two `WORDS`, an out-
    // of-range byte address can map to a slice value that exceeds
    // `WORDS-1` and indexes beyond `mem[]`. `read_valid` already
    // marks such reads invalid downstream, but the BRAM read template
    // still indexes the array unconditionally to satisfy Quartus's
    // M20K inference (Ch154 audit), so the index itself must remain
    // in bounds. The Ch155 audit-low fix: clamp the read indices
    // with `& (WORDS-1)` so a power-of-two depth is required AND any
    // bit beyond the legal slice is masked away. Power-of-two also
    // matches every Agilex M20K depth target (256/512/1024/2048/...).
    localparam int unsigned WORDS     = BYTES / 4;
    localparam int unsigned WORD_AW   = $clog2(WORDS);

    logic [31:0] mem [0:WORDS-1];

    // synthesis translate_off
    initial begin
        if (BYTES < 4 || (BYTES & 32'd3) != 0)
            $error("vram_bram_stub: BYTES (%0d) must be >= 4 and a multiple of 4", BYTES);
        // Power-of-two check on WORDS: (WORDS != 0) && ((WORDS & (WORDS-1)) == 0).
        if (WORDS == 0 || (WORDS & (WORDS - 1)) != 0)
            $error("vram_bram_stub: BYTES (%0d) must yield a power-of-two WORDS depth (got %0d)",
                   BYTES, WORDS);

        // Ch252 — VRAM replication tripwire (simulation/elaboration only).
        //
        // At BYTES >= 256 KiB, each 1W+1R simple-dual-port replica costs
        // ~100 M20Ks. With ENABLE_READ2 = 1, Quartus replicates the
        // storage to give the second read its own port, doubling that
        // cost (>= 200 M20Ks per pair). Above this threshold a Quartus
        // fitter overrun on Agilex 5 (358 M20K budget) becomes likely.
        //
        // This `$fatal` runs in simulation and elaboration-aware lint
        // tools — it is the loud canary. The REAL protection is the
        // board-top profile: hardware builds explicitly set
        // ENABLE_READ2 = 0 when VRAM_BYTES is large (see
        // de25_nano_psmct32_raster_demo_top). Re-enabling read2 on a
        // large hardware VRAM requires landing one of the architectural
        // follow-ups in docs/decisions/0006-vram-roadmap.md first.
        if (ENABLE_READ2 && (BYTES >= 32'd262144)) begin
            $display("vram_bram_stub: ENABLE_READ2=1 with BYTES=%0d (>= 256 KiB) trips the replication tripwire.", BYTES);
            $display("  The 2nd read port forces Quartus to replicate the storage, ~doubling M20K cost.");
            $display("  Either set ENABLE_READ2=0 (PSMCT32-only hardware profile) or land the");
            $display("  arbitrated/line-buffered VRAM follow-up before re-enabling read2 at this size.");
            $display("  See docs/decisions/0006-vram-roadmap.md.");
            $fatal(1, "vram_bram_stub: replication-tripwire fatal exit");
        end

        for (int i = 0; i < int'(WORDS); i++) mem[i] = 32'd0;
    end
    // synthesis translate_on

    // ----------------------------------------------------------------
    // Write port — per-byte WE, per-byte non-wrapping admission.
    // ----------------------------------------------------------------
    logic [32:0] addr33;
    logic        admit_b0, admit_b1, admit_b2, admit_b3;
    logic        write_admit;
    assign addr33   = {1'b0, write_addr};
    assign admit_b0 = (addr33 + 33'd0) < 33'(BYTES);
    assign admit_b1 = (addr33 + 33'd1) < 33'(BYTES);
    assign admit_b2 = (addr33 + 33'd2) < 33'(BYTES);
    assign admit_b3 = (addr33 + 33'd3) < 33'(BYTES);
    assign write_admit = write_en
                       && (write_addr[1:0] == 2'b00)   // word-aligned
                       && (!write_be[0] || admit_b0)
                       && (!write_be[1] || admit_b1)
                       && (!write_be[2] || admit_b2)
                       && (!write_be[3] || admit_b3);

    logic [WORD_AW-1:0] write_word_idx;
    assign write_word_idx = write_addr[WORD_AW+1:2];

    // BRAM-native byte-WE template — each `if (write_be[i])` slice
    // updates a separate 8-bit lane of the 32-bit word. This is the
    // canonical Quartus inference shape (proven in Ch153 exp_a).
    always_ff @(posedge clk) begin
        if (rst_n && write_admit) begin
            if (write_be[0]) mem[write_word_idx][ 7: 0] <= write_data[ 7: 0];
            if (write_be[1]) mem[write_word_idx][15: 8] <= write_data[15: 8];
            if (write_be[2]) mem[write_word_idx][23:16] <= write_data[23:16];
            if (write_be[3]) mem[write_word_idx][31:24] <= write_data[31:24];
        end
    end

    // ----------------------------------------------------------------
    // Read ports — sync, registered output, 1-cycle latency.
    //
    // The read path is the CANONICAL Quartus M20K inference template:
    // a single unconditional `read_data <= mem[idx]` registered
    // assignment, with NO reset on the data register and NO read-side
    // gating. Quartus rejected an earlier draft that gated reads on
    // `read_addr[1:0]==2'b00 && in-bounds` with
    //   "Info (276007): RAM logic ... uninferred due to asynchronous
    //   read logic"
    // and synthesized the storage as flip-flops. Bounds + alignment
    // checks land on the separate `read_valid` pipeline below where
    // they don't poison the data path.
    // ----------------------------------------------------------------
    // Word-index extraction. For a power-of-two `WORDS` depth (the
    // parameter contract enforced above), the slice
    // `read_addr[WORD_AW+1:2]` is naturally bounded to `[0, WORDS-1]`
    // — the high bits beyond WORD_AW+1 represent address ranges
    // already rejected by the `read_valid` gate below. The mask
    // `& WORD_AW'(WORDS - 1)` is redundant for power-of-two WORDS
    // (it just keeps the same bits) but documents the contract: a
    // future relaxation that allows non-power-of-two depths would
    // need to either remove that change OR force the mem-read index
    // through a real range-clamp rather than relying on the natural
    // truncation.
    logic [WORD_AW-1:0] read_word_idx;
    assign read_word_idx  = read_addr [WORD_AW+1:2] & WORD_AW'(WORDS - 1);

    always_ff @(posedge clk) begin
        read_data <= mem[read_word_idx];
    end

    // Out-of-range / misaligned detection on a parallel pipeline so
    // it doesn't gate the BRAM read path. read_valid pulses 1 cycle
    // late, aligned with read_data.
    logic read_in_range_pre;
    assign read_in_range_pre  = (read_addr [1:0] == 2'b00) &&
                                ({1'b0, read_addr } + 33'd3 < 33'(BYTES));
    always_ff @(posedge clk) begin
        if (!rst_n) read_valid <= 1'b0;
        else        read_valid <= read_in_range_pre;
    end

    // ----------------------------------------------------------------
    // Read port 1 — feature-strippable via ENABLE_READ2 (Ch251.4).
    // When ENABLE_READ2=1: full sync read + range gate, matching the
    // pre-Ch251.4 behaviour. When ENABLE_READ2=0: NO reference to
    // `mem` from this branch, so Quartus does not infer a second M20K
    // read port and the VRAM storage stops replicating.
    // ----------------------------------------------------------------
    generate
    if (ENABLE_READ2) begin : g_read2_en
        logic [WORD_AW-1:0] read2_word_idx;
        assign read2_word_idx = read2_addr[WORD_AW+1:2] & WORD_AW'(WORDS - 1);

        always_ff @(posedge clk) begin
            read2_data <= mem[read2_word_idx];
        end

        logic read2_in_range_pre;
        assign read2_in_range_pre = (read2_addr[1:0] == 2'b00) &&
                                    ({1'b0, read2_addr} + 33'd3 < 33'(BYTES));
        always_ff @(posedge clk) begin
            if (!rst_n) read2_valid <= 1'b0;
            else        read2_valid <= read2_in_range_pre;
        end
    end else begin : g_read2_dis
        always_ff @(posedge clk) begin
            read2_data  <= 32'd0;
            read2_valid <= 1'b0;
        end
    end
    endgenerate

endmodule : vram_bram_stub
