// ============================================================================
// gs_lpddr_scanout.sv  (Ch320 Brick 1)
//
// LPDDR4B-backed scanout for the SMALL framebuffer demo (explicitly scoped to
// a tiny frame — default 64x64 PSMCT16 = 8 KiB). Instead of ao486-style line
// buffering/streaming, it copies the WHOLE framebuffer from LPDDR4B into a
// small on-chip cache once per frame, then serves scanout pixels from the
// cache. When the framebuffer grows past "tiny demo", revisit ao486's
// vga_fb_ddr line-buffer approach.
//
// Pixel mapping is automatic: the GS writer mirrors BRAM VRAM byte-for-byte
// into LPDDR4B, so cache[addr] == BRAM_VRAM[addr]. We index the cache by the
// PCRTC's own `vram_read_addr`, so the decoded pixel is identical to the BRAM
// scanout pixel for the same raster position — the video-source mux is seamless
// regardless of swizzle/MAG.
//
// Two clock domains:
//   axi_clk   (emif_clk, ~310 MHz) — fill the cache from LPDDR4B over AXI4
//   video_clk (design_clk)         — index the cache by vram_read_addr -> r/g/b
//
// Fill happens on frame_start (vsync) and completes during vblank (256 single
// beats ~ 1 us); scanout reads the stable cache during the active region. The
// read channel is shared with the Ch319 read-probe via an external arbiter.
// ============================================================================
`timescale 1ns/1ps

module gs_lpddr_scanout #(
    parameter [29:0] FB_BASE     = 30'd0,   // LPDDR byte base to FETCH the framebuffer from
    parameter int    CACHE_BEATS = 256,     // 256 * 32 B = 8 KiB = 64x64 PSMCT16
    // Ch324 — the LPDDR fetch base (FB_BASE) and the PCRTC vram_read_addr base can DIFFER:
    // the spill framebuffer lives at COLOR_SPILL_BASE in LPDDR but the PCRTC addresses it
    // BRAM-relative (0-based). VRAM_BASE is the vram_read_addr origin (defaults to FB_BASE
    // for the Ch320/321 mirror case where they coincide).
    parameter [29:0] VRAM_BASE   = FB_BASE,
    // Ch324 — pixel format: 0 = PSMCT16 (RGBA5551, 16 px/beat), 1 = PSMCT32 (ABGR, 8 px/beat).
    parameter bit    PSMCT32     = 1'b0
)(
    // ---- AXI read clock domain (emif_clk) ----
    input  logic         axi_clk,
    input  logic         axi_rst_n,
    input  logic         enable,        // 1 = refill the cache on frame_start
    input  logic         frame_start,   // video-domain pulse/level (vsync); synced internally

    // ---- video clock domain (design_clk) ----
    input  logic         video_clk,
    input  logic [31:0]  vram_read_addr,
    output logic [7:0]   r,
    output logic [7:0]   g,
    output logic [7:0]   b,

    // ---- status (axi_clk domain; the bridge syncs these) ----
    output logic         cache_valid,    // a full frame has been loaded
    output logic [31:0]  rd_beats,       // beats read (cumulative)
    output logic [31:0]  rd_errs,        // non-OKAY read responses (cumulative)

    // ---- AXI4 read channel to the EMIF user port (axi_clk, 256-bit) ----
    output logic [29:0]  araddr,
    output logic [1:0]   arburst,
    output logic [6:0]   arid,
    output logic [7:0]   arlen,
    output logic [2:0]   arsize,
    output logic         arvalid,
    input  logic         arready,
    input  logic [255:0] rdata,
    input  logic [1:0]   rresp,
    input  logic         rlast,
    input  logic         rvalid,
    output logic         rready
);
    localparam int BEAT_BITS = $clog2(CACHE_BEATS);   // 8 for 256
    localparam int FB_SPAN    = CACHE_BEATS * 32;      // 8192 bytes

    assign arburst = 2'b01;   // INCR
    assign arid    = 7'd2;    // distinct from writer (0) and read-probe (1)
    assign arlen   = 8'd0;    // single beat
    assign arsize  = 3'b101;  // 32 bytes

    // Frame cache: one 256-bit word per 32-byte beat.
    logic [255:0] cache [0:CACHE_BEATS-1];

    // ---------------- fill side (axi_clk) ----------------
    logic [2:0] fs_sync;
    wire fs_edge = (fs_sync[2] != fs_sync[1]);

    typedef enum logic [1:0] { F_IDLE, F_AR, F_R } fstate_t;
    fstate_t fst;
    logic [BEAT_BITS:0] beat;     // 0..CACHE_BEATS (extra bit for the terminal compare)

    always_ff @(posedge axi_clk) begin
        if (!axi_rst_n) begin
            fs_sync <= 3'd0; fst <= F_IDLE; beat <= '0;
            araddr <= '0; arvalid <= 1'b0; rready <= 1'b0;
            cache_valid <= 1'b0; rd_beats <= 32'd0; rd_errs <= 32'd0;
        end else begin
            fs_sync <= {fs_sync[1:0], frame_start};
            case (fst)
                F_IDLE: begin
                    if (fs_edge && enable) begin
                        beat        <= '0;
                        cache_valid <= 1'b0;
                        araddr      <= FB_BASE;
                        arvalid     <= 1'b1;
                        fst         <= F_AR;
                    end
                end
                F_AR: begin
                    if (arready) begin
                        arvalid <= 1'b0;
                        rready  <= 1'b1;
                        fst     <= F_R;
                    end
                end
                F_R: begin
                    if (rvalid) begin
                        cache[beat[BEAT_BITS-1:0]] <= rdata;
                        rready   <= 1'b0;
                        rd_beats <= rd_beats + 32'd1;
                        if (rresp != 2'b00) rd_errs <= rd_errs + 32'd1;
                        if (beat == CACHE_BEATS-1) begin
                            cache_valid <= 1'b1;
                            fst         <= F_IDLE;
                        end else begin
                            beat    <= beat + 1'b1;
                            araddr  <= FB_BASE + (({{(30-BEAT_BITS-1){1'b0}}, (beat + 1'b1)}) << 5);
                            arvalid <= 1'b1;
                            fst     <= F_AR;
                        end
                    end
                end
                default: fst <= F_IDLE;
            endcase
        end
    end

    // ---------------- scanout side (video_clk) ----------------
    // Byte offset of the requested pixel within the framebuffer (vram_read_addr is BRAM-relative
    // = VRAM_BASE-origin; the cache holds the SAME bytes fetched from FB_BASE in LPDDR).
    wire [31:0] off       = vram_read_addr - {2'b00, VRAM_BASE};
    wire [BEAT_BITS-1:0] beat_ix = off[BEAT_BITS+4 -: BEAT_BITS];  // off>>5, low BEAT_BITS
    wire [3:0]  hw_sel    = off[4:1];                              // PSMCT16: 16 halfwords / beat
    wire [2:0]  w_sel     = off[4:2];                              // PSMCT32: 8 words / beat
    wire in_range = cache_valid
                 && (vram_read_addr >= {2'b00, VRAM_BASE})
                 && (off < FB_SPAN);

    // Registered (sync-read) cache lookup — 1-cycle latency to match the PCRTC's
    // VRAM_SYNC_READ pixel timing so the muxed output aligns with PCRTC de/sync.
    // Split the array-index and the part-select across the register boundary
    // (chained index+part-select in one expr trips iverilog-12).
    logic [255:0] word_q;
    logic [3:0]   hw_q;
    logic [2:0]   w_q;
    logic         in_range_q;
    always_ff @(posedge video_clk) begin
        word_q     <= cache[beat_ix];
        hw_q       <= hw_sel;
        w_q        <= w_sel;
        in_range_q <= in_range;
    end
    wire [15:0] px16_q = word_q[hw_q*16 +: 16];
    wire [31:0] px32_q = word_q[w_q*32  +: 32];

    // PSMCT16 (RGBA5551): R[4:0] G[9:5] B[14:10], 5->8 by bit-replication ({c5,c5[4:2]}).
    wire [4:0] r5 = px16_q[4:0];
    wire [4:0] g5 = px16_q[9:5];
    wire [4:0] b5 = px16_q[14:10];
    wire [7:0] r16 = {r5, r5[4:2]}, g16 = {g5, g5[4:2]}, b16 = {b5, b5[4:2]};
    // PSMCT32 (ABGR8888): R[7:0] G[15:8] B[23:16] (A discarded) — identical decode to gs_pcrtc.
    wire [7:0] r32 = px32_q[7:0], g32 = px32_q[15:8], b32 = px32_q[23:16];
    assign r = !in_range_q ? 8'd0 : (PSMCT32 ? r32 : r16);
    assign g = !in_range_q ? 8'd0 : (PSMCT32 ? g32 : g16);
    assign b = !in_range_q ? 8'd0 : (PSMCT32 ? b32 : b16);
endmodule
