// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2025-2026 retroDE contributors
// ============================================================================
// tile_ram_cdc — Ch229 bridge-clock → design-clock tile-RAM shadow
// ============================================================================
// Implements the design-domain side of the Ch229 tile-RAM CDC. Owns a
// 1024 × 32-bit shadow memory in the design clock domain. Bridge-side
// writes arrive as a toggle-based "event" signal plus latched index +
// data; a 2-FF synchronizer + XOR edge detector turns each toggle edge
// into a 1-cycle write pulse against the shadow RAM. Read port is
// purely combinational (the consumer is the Ch245 platform-OSD
// char-BRAM read adapter in the top, which selects high/low 16-bit
// cells from each 32-bit shadow word and feeds them to the platform
// `osd_overlay`. Pre-Ch245 the consumer was the now-retired
// PS2-local `osd_overlay_stub`). No back-pressure — the bridge is assumed
// to space tile writes far enough apart for the sync chain to keep up.
//
// **CDC contract (read carefully before refactoring):**
//   - The bridge updates `bclk_wr_toggle`, `bclk_wr_index`, `bclk_wr_data`
//     at the same `bclk` edge (one bridge clock cycle).
//   - The receiver sees the toggle through a 2-FF synchronizer; the
//     edge-detection wire `wr_pulse` fires on the dclk cycle where the
//     synchronized toggle has FULLY settled. That guarantees ≥ 2 dclk
//     periods of stability on `bclk_wr_index/data` before they're
//     sampled into the shadow memory.
//   - Multiple bridge writes faster than ~3 dclk periods apart will
//     race and may drop or merge events. For the Ch229 use case
//     (retrodesd OSD updates at ≤ 1 kHz, design_clk at 25–50 MHz),
//     this is many orders of magnitude of slack. **Do not** wire a
//     fast-cycling source (e.g. a counter) into the bridge's tile
//     write path without first replacing this CDC with an async FIFO.
//
// **Reset behavior:**
//   - On `breset_n` deasserted: bridge clears `bclk_wr_toggle` to 0
//     (matching the receiver's post-reset state). When both domains
//     reset together (the normal case on FPGA configure), no spurious
//     edge fires after release.
//   - On `dreset_n` deasserted: synchronizer chain clears to 0;
//     shadow memory contents are NOT cleared (matches Ch227 retention
//     semantics — sim `initial` block zeroes for determinism, hardware
//     power-up is undefined). The Ch229 contract is "tile RAM survives
//     warm reset"; rebooting both sides is a power-cycle scenario and
//     the bridge will re-broadcast any written tiles via the next set
//     of AXI writes from HPS.
// ============================================================================

`timescale 1ns/1ps

module tile_ram_cdc (
    // ---- Bridge clock domain (write port) ----
    input  logic        bclk,
    input  logic        breset_n,
    input  logic        bclk_wr_toggle,
    input  logic [9:0]  bclk_wr_index,
    input  logic [31:0] bclk_wr_data,

    // ---- Design clock domain (read port) ----
    input  logic        dclk,
    input  logic        dreset_n,
    input  logic [9:0]  dclk_rd_index,
    output logic [31:0] dclk_rd_data,

    // ---- Ch230 design-domain diagnostic counter ----
    // Saturating count of "tile writes too close" events — successive
    // wr_pulse events fewer than MIN_DCLK_GAP dclk cycles apart.
    // Exposed as an output so the top can route it to a reverse-CDC +
    // bridge-readable diagnostic register in a future chapter (Ch231+).
    // For Ch230 the top leaves it unconnected; the counter still exists
    // in the design domain as a synthesis artifact ready for hookup.
    output logic [15:0] tile_wr_too_close_count
);

    // Shadow RAM lives in the design clock domain. Matched-size with
    // the bridge-side `ps2_hps_bridge.tile_mem` (1024 × 32-bit). The
    // `ramstyle = "M20K"` attribute (added in the Ch232 hardware
    // bring-up hotfix) forces Quartus to use a single M20K block
    // instead of distributing the storage across LABs.
    (* ramstyle = "M20K" *) logic [31:0] shadow_mem [0:1023];
    initial begin
        for (int i = 0; i < 1024; i++)
            shadow_mem[i] = 32'd0;
    end

    // 2-FF synchronizer on the bridge toggle into the design clock.
    // Three stages let us compute an edge detector against the
    // already-resampled bits ([2] ^ [1]), giving the wr_pulse a full
    // dclk cycle of bclk_wr_index/data stability before we sample.
    (* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED" *)
    logic [2:0] toggle_sync;
    always_ff @(posedge dclk or negedge dreset_n) begin
        if (!dreset_n)
            toggle_sync <= 3'b000;
        else
            toggle_sync <= {toggle_sync[1:0], bclk_wr_toggle};
    end
    wire wr_pulse = toggle_sync[2] ^ toggle_sync[1];

    // Shadow write port. At the dclk edge where wr_pulse fires,
    // sample bclk_wr_index + bclk_wr_data. Both have been stable for
    // ≥ 2 dclk cycles by construction of the CDC contract above.
    always_ff @(posedge dclk) begin
        if (wr_pulse)
            shadow_mem[bclk_wr_index] <= bclk_wr_data;
    end

    // Read port: combinational lookup. The consumer pulls index from
    // its pixel position and uses the data to decide overlay vs
    // transparent for each pixel.
    assign dclk_rd_data = shadow_mem[dclk_rd_index];

    // ---- Ch229 / Ch230 tile-write rate watchdog ----
    // The CDC contract requires writes to be spaced far enough apart
    // that each toggle edge passes through the sync chain cleanly.
    // Two consecutive bridge writes that both flip toggle within one
    // dclk of each other can be merged into a single transition at
    // sync[0] — the first write's bclk_wr_index/bclk_wr_data are
    // overwritten before the receiver samples them, and the write is
    // silently lost.
    //
    // The actual minimum gap is ≥ 3 dclk between successive
    // wr_pulse events at the receiver:
    //   - 1 dclk for the synchronizer to fully settle (so the
    //     second edge is visible as a distinct transition)
    //   - 1 dclk for the receiver to fire wr_pulse for write 1
    //   - 1 dclk of margin for jitter / setup time
    //
    // Production rate enforcer is software-side (retrodesd OSD
    // updates at ≤ 1 kHz ≫ 3 dclk @ 25 MHz = 120 ns); the bridge
    // does not back-pressure AXI on this constraint. Ch229 added a
    // sim-only `$display` warning; Ch230 promotes the gap-tracker to
    // a real **saturating counter** (16-bit) exposed as
    // `tile_wr_too_close_count` so a future chapter can route it
    // through a reverse CDC into a bridge-readable register
    // (HDMI_DIAG upper bits or a new diagnostic offset). The
    // `$display` aid remains in `\`ifndef SYNTHESIS` for pre-silicon
    // log visibility.
    localparam int unsigned MIN_DCLK_GAP = 3;
    logic [31:0]  dclk_since_last_pulse;
    wire          too_close = wr_pulse && (dclk_since_last_pulse < MIN_DCLK_GAP);

    always_ff @(posedge dclk or negedge dreset_n) begin
        if (!dreset_n) begin
            dclk_since_last_pulse   <= 32'hFFFF_FFFF;
            tile_wr_too_close_count <= 16'd0;
        end else begin
            if (wr_pulse)
                dclk_since_last_pulse <= 32'd0;
            else if (dclk_since_last_pulse != 32'hFFFF_FFFF)
                dclk_since_last_pulse <= dclk_since_last_pulse + 32'd1;

            if (too_close && (tile_wr_too_close_count != 16'hFFFF))
                tile_wr_too_close_count <= tile_wr_too_close_count + 16'd1;
        end
    end

`ifndef SYNTHESIS
    always_ff @(posedge dclk) begin
        if (dreset_n && too_close) begin
            $display(
                "[tile_ram_cdc] WARN time=%0t: tile writes too close - %0d dclk cycles between toggle edges (CDC needs >= %0d for safe sample).",
                $time, dclk_since_last_pulse, MIN_DCLK_GAP);
        end
    end
`endif

    // ---- Lint: bclk + breset_n are intentionally referenced ONLY
    //      via the bclk_wr_toggle path. Tie a placeholder reference
    //      to silence "unused" warnings on tools that don't trace
    //      through the upstream toggle source.
    // verilator lint_off UNUSED
    wire _unused_ok = &{1'b0, bclk, breset_n, 1'b0};
    // verilator lint_on UNUSED

endmodule : tile_ram_cdc
