// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2025-2026 retroDE contributors
// ============================================================================
// ps2_hps_bridge — Ch173/Ch174/Ch176 AXI4 register surface for the PS2 core
// ============================================================================
//
// Replaces ps2_hps_bridge_null (Ch170 placeholder). Adds live status
// registers the HPS-side Linux userspace can read to confirm what the
// fabric is doing without watching the HDMI output. All identity +
// status registers are read-only; writes ACK with OKAY and discard
// (matches the Codex Ch173 caution to keep Ch173 read-only unless
// the write path is trivial).
//
// Port signature is a strict superset of ps2_hps_bridge_null so the
// top wrapper can swap the instantiation without re-plumbing AXI.
//
// Register map (read-only, single-beat AXI4, 128-bit data with 32-bit
// lane selection via {awaddr[3:2] / araddr[3:2]}). 0x000-0x01F is the
// retroDE common ABI v1.0 window (matches splash/nes/coco2 layout per
// retroDE_splash/software/input_common.h); the PS2-specific counters
// land at 0x020+ to avoid colliding with shared-ABI semantics.
//
//   0x000  CORE_ID                   32'h50533200  ("PS2\0")
//   0x004  ABI_VERSION               32'h00000100  (v1.0)
//   0x008  CORE_STATUS
//            bit  0: loaded          (always 1 after configuration)
//            bit  1: core_halt       (EE core syscall'd / halted)
//            bit  2: dma_done_seen   (DMAC channel 2 reached done)
//            bit  3: frame_seen      (PCRTC produced end-of-frame)
//            bit  4: hdmi_init_done  (ADV7513 I²C config FSM complete)
//            bit  5: hdmi_i2c_error  (Ch166 NACK watchdog latched)
//            bit  6: raster_overflow (Ch87 FIFO push_drop ever fired
//                                     — should stay 0 under Ch172
//                                     backpressure)
//            bits[31:7]: reserved-zero
//   0x00C  CORE_CAPS                 32'h00000000  (no caps advertised
//                                     yet; ROM/savestate/etc capability
//                                     bits land here when wired)
//   0x010  CORE_CTRL                 Ch176 writable + readable. Bit layout
//                                     matches shared retroDE ABI:
//                                       [0] RESET       — functional. Held
//                                                         high = PS2 design
//                                                         domain in reset.
//                                                         Exposed on the
//                                                         `core_reset_req`
//                                                         port; the board
//                                                         top 2-FF syncs it
//                                                         into design_clk
//                                                         and gates the
//                                                         existing reset.
//                                       [1] ROM_LOADED  — latch only. PS2
//                                                         has no ROM
//                                                         lifecycle today
//                                                         (we ship as
//                                                         backend=splash).
//                                                         Written/read for
//                                                         ABI shape only.
//                                       [2] PAUSE       — latch only. No
//                                                         clean stall point
//                                                         in this design
//                                                         yet; later chapter
//                                                         wires the gate.
//                                     Unused bits readback as 0.
//   0x014  CORE_PULSE                Ch176 write-only side-effect register.
//                                     Self-clearing (always reads 0).
//                                     Bit layout matches shared ABI:
//                                       [3] HDMI_CLR    — clears FRAME_COUNT,
//                                                         DMA_DONE_COUNT,
//                                                         RASTER_OVERFLOW_COUNT.
//                                                         PS2 widens the
//                                                         shared "HDMI
//                                                         diagnostic clear"
//                                                         to "video/bridge
//                                                         diagnostic clear"
//                                                         per Codex Ch176
//                                                         naming note.
//                                       [0,1,2] DIRTY_CLR / SS_DONE_CLR /
//                                                         VIDEO_REC — ACK'd
//                                                         and ignored (PS2
//                                                         has no savestate
//                                                         or HDMI re-init
//                                                         path yet).
//   0x018  reserved                  32'h00000000
//   0x01C  reserved                  32'h00000000
//
//   PS2-specific diagnostics (Ch173, retroDE-ps2 extension to the shared
//   ABI; safe because the shared map only reserves 0x000-0x01F):
//
//   0x020  FRAME_COUNT               Ch174: increments on each edge of
//                                     `frame_toggle` (the event-toggle
//                                     primitive — flips on every PCRTC
//                                     end-of-frame pulse in design_clk).
//                                     Replaces Ch173's edge-of-sticky
//                                     source so the counter actually
//                                     tracks frames in hardware.
//   0x024  DMA_DONE_COUNT            Ch174: increments on each edge of
//                                     `dma_done_toggle` (event-toggle —
//                                     flips on every DMAC EV_DMA_DONE).
//   0x028  RASTER_OVERFLOW_COUNT     counts raster_overflow cycles
//                                     (cycle-by-cycle, not edge — a
//                                     non-zero value = the backpressure
//                                     path missed somewhere)
//   0x02C  reserved                  32'h00000000
//
//   Ch222 — input latch block. HPS-visible write/read registers at the
//   shared-ABI input offsets. No SIO2 / DualShock wiring yet; retrodesd
//   writes here so future PS2-side controller emulation has a stable
//   read port. Reset clears all three to 0.
//
//   0x040  INPUT_P1                  Ch222: 32-bit RW latch. retrodesd's
//                                     `input_thread` writes the remapped
//                                     gamepad bitmap (JOY_* bits per
//                                     input_common.h).
//   0x044  INPUT_P2                  Ch222: 32-bit RW latch (player 2).
//   0x048  INPUT_P1_RAW              Ch222: 32-bit RW latch. Un-remapped
//                                     mirror of the player-1 buttons,
//                                     used by the OSD navigation FSM in
//                                     other cores. PS2 stores it without
//                                     consumers today.
//   0x04C  reserved                  32'h00000000
//
//   Ch225 — VIDEO_STATUS / HDMI_DIAG diagnostic read surface. Both are
//   pure-read views into the synchronized status signals the bridge
//   already tracks; writes are ack'd-and-ignored. retrodesd / operator
//   tooling can poll these to answer "is the display path alive?"
//   without depending on LEDs or `ps2_status.sh`.
//
//   0x060  VIDEO_STATUS              Ch225: read-only diagnostic bits.
//                                     [0] frame_seen      (live sticky from
//                                                          PCRTC end-of-frame)
//                                     [1] scanout_alive   (= frame_count!=0,
//                                                          i.e. ≥ 1 frame
//                                                          completed since
//                                                          last HDMI_CLR)
//                                     [2] raster_overflow (live — drops if
//                                                          the input dies)
//                                     [3] dma_done_seen   (live sticky)
//                                     [31:4] reserved-zero.
//   0x064  HDMI_DIAG                 Ch225: read-only diagnostic bits.
//                                     [0] hdmi_init_done  (ADV7513 LUT walk
//                                                          complete)
//                                     [1] hdmi_i2c_error  (Ch166 NACK
//                                                          watchdog latched)
//                                     [31:2] reserved-zero.
//
//   Ch226 — DS2 wired-controller stub at the shared-ABI offsets. PS2
//   has no DS2 platform path today, so the stub honors the sibling-ABI
//   contract consumed by retrodesd's `ds2_poll_thread.c`:
//   `connected = status & 0x1` and `error = (status >> 1) & 0x1`. With
//   connected = 0, retrodesd's poll thread treats DS2 as absent and
//   never calls `osd_input_update_ds2(buttons)` — the right behavior.
//   Bit [2] is a PS2-local diagnostic (`input_latches_valid`) confirming
//   that the Ch222 latch surface exists; it deliberately sits above the
//   shared bits to avoid colliding with the sibling-consumed
//   `[0]=connected / [1]=error` semantics. DS2_BUTTONS mirrors
//   INPUT_P1 so HPS can confirm the Ch222 store landed via the same
//   bridge offset the sibling cores expose.
//
//   0x0F0  DS2_STATUS                Ch226: read-only sibling-ABI stub.
//                                     [0] connected           = 1'b0
//                                                 (no DS2 wired path)
//                                     [1] error               = 1'b0
//                                                 (not an error state)
//                                     [2] input_latches_valid = 1'b1
//                                                 (PS2-local: Ch222 OK)
//                                     [31:3] reserved-zero.
//                                     Reads = 32'h00000004.
//   0x0F4  DS2_BUTTONS               Ch226: read-only mirror of INPUT_P1
//                                     (Ch222). Reads return whatever
//                                     retrodesd last wrote to 0x040.
//                                     With `connected=0` retrodesd
//                                     never consumes this, but operator
//                                     tooling can use it to confirm the
//                                     Ch222 store via the DS2 path.
//
//   Ch227 — tile RAM storage surface at 0x1000..0x1FFF (4 KB window).
//   The shared-ABI software view is "2048 × 16-bit cells packed 2/word"
//   (see input_common.h's `osd_setup` + `osd_putchar`), but at the bus
//   level the bridge stores plain 32-bit words and returns exactly
//   what HPS wrote. Sibling cores forward tile writes to an external
//   overlay engine via `tile_wr_addr/data/en` output ports; PS2 has
//   no overlay engine yet (Ch228 will wire one), so for Ch227 the
//   bridge owns the storage internally as a 1024 × 32-bit memory
//   array. No PCRTC overlay composition; reads simply mirror the
//   last write. Reset semantics: contents are *retained* across warm
//   reset (no sibling precedent for clearing — siblings don't even
//   own the storage). Sim initializes to 0 for determinism; power-up
//   value on hardware is undefined until the overlay-engine chapter
//   pins a reset contract.
//
//   0x1000..0x1FFF  Tile RAM         Ch227: 1024 × 32-bit RW. Decode
//                                     guard `addr[37:12] == 26'h1`;
//                                     word index = addr[11:2]. WSTRB
//                                     ignored (full-word writes only,
//                                     same contract as the rest of
//                                     the bridge).
//
//   Ch223 — OSD compatibility sink. retrodesd's splash backend writes
//   OSD_CTRL / OSD_CFG0 / OSD_CFG1 at these offsets during start(),
//   followed by tile RAM writes at 0x1000+. PS2 has no overlay path
//   yet, so these three latches just store-and-readback — they exist
//   so retrodesd's writes land in real registers instead of being
//   silently dropped on the side-effect map (Ch221 boundary).
//
//   0x100  OSD_CTRL                  Ch223: 32-bit RW latch. Splash sets
//                                     ENABLE | INPUT_LOCK | FORCE_OPEN
//                                     here; the bridge doesn't act on
//                                     those bits yet but the value is
//                                     stored and readable for software
//                                     introspection.
//   0x104  OSD_STATUS                Ch224: always-zero source. Reads
//                                     return 0; writes accepted with
//                                     BRESP=OKAY and ignored. The FSM
//                                     that drives [0]=active and
//                                     [12:8]=cursor_row exists in other
//                                     cores' bridges but PS2 has no
//                                     overlay engine yet, so the status
//                                     bits stay 0.
//   0x108  OSD_TRIGGER               Ch224: W1C-shape sink. Reads
//                                     return 0; writes accepted with
//                                     BRESP=OKAY and ignored. In other
//                                     cores the FSM SETs the action /
//                                     back / scroll bits, retrodesd
//                                     reads, processes, and writes-1
//                                     to clear. PS2 has no FSM source
//                                     today so the bits stay 0 and the
//                                     W1C semantics collapse to a sink.
//                                     Behavior matches the ABI v1.0
//                                     contract — retrodesd's poll loop
//                                     sees `trig=0` and never invokes
//                                     `osd_action`.
//   0x10C  OSD_INPUT                 reserved — reads 0. Sibling cores
//                                     expose this as a "debug override"
//                                     for injecting simulated button
//                                     events; PS2 has no compelling use
//                                     for it until an overlay engine
//                                     exists.
//   0x110  OSD_CFG0                  Ch223: 32-bit RW latch. cols/rows/
//                                     x_chars/y_chars layout fields per
//                                     input_common.h's `osd_setup`.
//   0x114  OSD_CFG1                  Ch223: 32-bit RW latch. first_row/
//                                     last_row + highlight/normal attrs.
//   0x118..0x11F  reserved           32'h00000000
//
// CDC: status signals arrive on the design_clk (Ch169 IOPLL @ 25 MHz)
// domain or the CLOCK2_50 domain (HDMI I²C FSM). The bridge runs on
// clk (= CLOCK2_50, the qsys bridge clock per Ch170). Every signal
// is passed through a 2-stage synchronizer before being consumed by
// register logic.
//
// Ch174 — event-counter CDC contract: a 1-cycle pulse in design_clk
// (40 ns @ 25 MHz) is borderline against a 50 MHz 2-FF synchronizer
// and would occasionally miss frames on hardware. Counters here are
// fed by *event toggles* (`frame_toggle` / `dma_done_toggle`), which
// flip on every event in the design domain and stay at the new value
// until the next event (~16.7 ms for frames). The synchronizer has
// megacycles of slack and cannot miss an edge; the counter increments
// on each XOR between sync_d[1] and sync_d[2]. The sticky-latch
// signals `frame_seen` / `dma_done_seen` remain the *status-bit*
// sources (CORE_STATUS[3] / [2]) — do not "simplify" this back into
// raw pulse synchronizers.
//
// Write contract: the bridge accepts every AXI write addressed inside
// its 38-bit window (`s_axi_awready` rises in W_IDLE regardless of
// address) and always returns `BRESP=OKAY`. The 32-bit register slot
// is picked out of `s_axi_wdata[127:0]` by `aw_addr_q[3:2]` (the lane
// select). **`s_axi_wstrb` is intentionally ignored** — every accepted
// write is treated as a full 32-bit-word write into the targeted
// register. Partial-byte writes are not supported; the corresponding
// register field will be overwritten in full. This matches the AXI4
// usage that retroDE userspace produces (`devmem2 ... w` / native
// 32-bit stores from retrodesd's bridge mmap) and the sibling-core
// bridges, all of which also ignore WSTRB. Writes to undecoded
// offsets inside or outside the side-effect window are still ACK'd
// but have no register effect.
// ============================================================================

`timescale 1ns/1ps

module ps2_hps_bridge (
    input  logic         clk,        // qsys clk_100_clk (=CLOCK2_50)
    input  logic         reset_n,
    input  logic         h2f_reset,  // HPS-driven fabric reset (unused; reserved)

    // ---- Ch173 status inputs (asynchronous to clk; sync'd internally) ----
    input  logic         core_halt,
    input  logic         dma_done_seen,
    input  logic         frame_seen,
    input  logic         hdmi_init_done,
    input  logic         hdmi_i2c_error,
    input  logic         raster_overflow,

    // ---- Ch174 event-toggle counter inputs (async; sync'd internally) ----
    input  logic         frame_toggle,
    input  logic         dma_done_toggle,

    // ---- Ch318 LPDDR-framebuffer test control/status (CLOCK2_50 — SAME domain as
    // the gs_lpddr_axi_master AXI side, so no synchronizers). HPS-armed runtime
    // controls + status counters; defaults safe (arm=0, canary=1, base=0x80000000).
    output logic         lpddr_arm_o,        // 0x018[0] — permit AXI writes (default 0)
    output logic         lpddr_canary_o,     // 0x018[1] — write only the 32-byte top line (default 1)
    output logic [31:0]  lpddr_fb_base_o,    // 0x01C    — LPDDR byte base (default 0x8000_0000)
    output logic         lpddr_ctrl_commit_o,// Ch352 — toggles on any arm/canary/fb_base write. The EMIF
                                             //   domain syncs this and latches {arm,canary,fb_base} on its edge,
                                             //   so the multi-bit control crosses COHERENTLY (no raw fb_base bus).
    input  logic [31:0]  lpddr_bytes_i,      // 0x030    — bytes written
    input  logic [31:0]  lpddr_bursts_i,     // 0x034    — bursts issued
    input  logic [31:0]  lpddr_bresp_err_i,  // (folded into 0x02C status)
    input  logic [31:0]  lpddr_fifo_ovf_i,   // (folded into 0x02C status)
    input  logic         lpddr_idle_i,       // 0x02C[0] — writer idle/done
    input  logic         clear_done_i,       // 0x02C[7] — Ch357 persistent-Z preclear complete (EMIF domain; synced).
                                             //   Host MUST wait for this before the first GO (z_rmw clear_start done).
    input  logic [31:0]  frag_drops_i,       // Ch357 — persistent-Z request-FIFO dropped-fragment count (design_clk).
                                             //   0x0EC snapshot (latched while drained/quiescent) + 0x02C[8] sticky-nonzero.
    input  logic         frame_drained_i,    // 0x02C[6] — Ch353 ordered drain ack (EMIF domain; synced). STABLE,
                                             //   unlike transient idle: asserts only after the EOF marker's last BRESP,
                                             //   so the host confirms the FULL render drained (not a mid-batch idle).

    // ---- Ch319 Brick 3: LPDDR4B read-probe (HPS reads FB content back through the
    // bridge — no /dev/mem). 0x03C: WRITE sets the byte address + triggers a read;
    // READ returns the 32-bit word. rd_done/rd_data arrive on emif_clk → synced here.
    output logic [31:0]  lpddr_rd_addr_o,    // 0x03C write — read byte address (write triggers a read)
    output logic         lpddr_rd_pulse_o,   // toggles on each 0x03C write (the read trigger)
    input  logic [31:0]  lpddr_rd_data_i,    // read-probe result word (emif_clk; latched here)
    input  logic         lpddr_rd_done_i,    // read-probe done toggle (emif_clk; synced here)

    // ---- Ch320: LPDDR4B scanout source-select + status ----
    output logic         lpddr_video_src_o,  // 0x018[2] — 1 = HDMI sourced from LPDDR4B scanout (default 0 = BRAM)
    output logic         lpddr_scanout_lb_o, // 0x018[3] — 1 = line-buffer scanout, 0 = frame-cache (Ch321; default 0)
    input  logic         lpddr_scan_valid_i, // scanout frame cache loaded (emif_clk; synced -> 0x02C[4])
    input  logic         lpddr_scan_err_i,   // scanout read errors seen (emif_clk; synced -> 0x02C[5])

    // ---- Ch322: LPDDR write-probe (HPS stages texture words) + texture-cache fill ----
    // 0x040 LPDDR_WRADDR (W): set the LPDDR byte address (auto-increments +4 per data write).
    // 0x044 LPDDR_WRDATA (W): a 32-bit word -> latches data + toggles wr_pulse (one write).
    // 0x048 TEX_FILL_CTRL (W bit0): arm the texture-cache fill. 0x048 (R): tex status.
    output logic [31:0]  lpddr_wr_addr_o,    // 0x040 — write byte address (auto-inc per 0x044 write)
    output logic [31:0]  lpddr_wr_data_o,    // 0x044 — write data word (stable when wr_pulse flips)
    output logic         lpddr_wr_pulse_o,   // toggles on each 0x044 write (write trigger)
    input  logic         lpddr_wr_busy_i,    // write-probe busy (emif_clk; synced)
    input  logic         lpddr_wr_done_i,    // write-probe done toggle (emif_clk; synced)
    input  logic [31:0]  lpddr_wr_bresp_err_i,// 0x058 — write-probe non-OKAY responses
    output logic [7:0]   diag_ctrl_o,        // 0x0C0 — Ch323 diag: [0]wr-probe full-beat [1]spill force-lane0
                                             //   [3:2]probe-port(0=s3,1=s1,2=s2) [5:4]spill-awid(0=6,1=5,2=0)
    output logic         tex_fill_start_o,   // 0x048[0] — arm the prefilled texture-cache fill
    input  logic         tex_fill_done_i,    // texture fully resident (emif_clk; synced -> 0x048[0])
    input  logic [31:0]  tex_fill_beats_i,   // 0x058 — fill beats completed
    input  logic [31:0]  tex_fill_bytes_i,   // 0x05C — fill bytes completed
    input  logic [31:0]  tex_rd_errs_i,      // 0x068 — texture-fill non-OKAY read responses
    input  logic [31:0]  tex_fill_crc_i,     // 0x070 — Ch352 sum32 of words written to tex_mem (integrity proof)
    // Ch322 — texel-source proof counters (design_clk; reset per render). After a render,
    // tex_cache_hits_i > 0 proves the triangle's texels came from the LPDDR cache.
    input  logic [31:0]  tex_cache_hits_i,   // 0x078 — texel reads served from the LPDDR cache
    input  logic [31:0]  tex_bram_hits_i,    // 0x07C — texel reads served from BRAM (fallback)

    // Ch323 — tile COLOR+Z spill/reload status counters (R-only; 0 when GS_TILE_SPILL off).
    // The HPS board script reads these to prove the spill/reload round-trip on silicon:
    // both spill writers advance, the reload reads them back, no write/read errors.
    input  logic [31:0]  spill_color_beats_i, // 0x080 — color flush words written to LPDDR
    input  logic [31:0]  spill_z_beats_i,     // 0x084 — Z     flush words written to LPDDR
    input  logic [31:0]  reload_color_beats_i,// 0x088 — color beats read back per reload
    input  logic [31:0]  reload_z_beats_i,    // 0x08C — Z     beats read back per reload
    input  logic [31:0]  reload_rd_errs_i,    // 0x090 — reload non-OKAY read responses
    input  logic [31:0]  spill_color_errs_i,  // 0x094 — color flush non-OKAY write responses
    input  logic [31:0]  spill_z_errs_i,      // 0x098 — Z     flush non-OKAY write responses
    input  logic         spill_color_ovf_i,   // 0x09C[0] — color spill FIFO overflow (sticky)
    input  logic         spill_z_ovf_i,       // 0x09C[1] — Z     spill FIFO overflow (sticky)
    // Ch323 diag — upstream tile-spill/reload event counters (reset per-render; 0 off-spill).
    input  logic [31:0]  ev_tp_flush_i,       // 0x0A0 — entries into TP_FLUSH
    input  logic [31:0]  ev_tp_zflush_i,      // 0x0A4 — entries into TP_ZFLUSH
    input  logic [31:0]  ev_tp_reload_i,      // 0x0A8 — entries into TP_RELOAD
    input  logic [31:0]  ev_tp_render_i,      // 0x0AC — entries into TP_RENDER
    input  logic [31:0]  ev_flush_emit_i,     // 0x0B0 — color flush emit pulses
    input  logic [31:0]  ev_zflush_emit_i,    // 0x0B4 — Z flush emit pulses
    input  logic [31:0]  ev_reload_start_i,   // 0x0B8 — reload-arm pulses
    input  logic [31:0]  ev_reload_ready_i,   // 0x0BC — reload-ready rising edges
    // Color-writer spill PIPELINE counters (localize any spill divergence): beats=B handshakes,
    // emit/push=GS side, pop=EMIF side. Healthy two-batch render = 512/64/64/64.
    input  logic [31:0]  dbg_c_beat_count_i,   // 0x0E0 — beats committed since trace_clear
    input  logic [31:0]  dbg_c_emit_count_i,   // 0x0CC — GS:   emits accepted (expect 512)
    input  logic [31:0]  dbg_c_push_count_i,   // 0x0F8 — GS:   beats pushed to FIFO (expect 64)
    input  logic [31:0]  dbg_c_pop_count_i,    // 0x0FC — EMIF: beats popped from FIFO (expect == beats)
    // Ch324 Z-writer pipeline counters (same split): compare against color in one run to localize
    // the grid spill_z over-production. emit too high=TP_ZFLUSH/iteration; push high=packer; pop/beats high=FIFO/CDC.
    input  logic [31:0]  dbg_z_beat_count_i,   // 0x0C4 — Z beats committed since trace_clear
    input  logic [31:0]  dbg_z_emit_count_i,   // 0x0C8 — Z GS emits accepted
    input  logic [31:0]  dbg_z_push_count_i,   // 0x0D0 — Z GS beats pushed to FIFO
    input  logic [31:0]  dbg_z_pop_count_i,    // 0x0D4 — Z EMIF beats popped from FIFO

    // ---- Ch330 Brick 4 — runtime command-list feeder control ----
    // The HPS writes a staging RAM entry as two 32-bit halves (0x0DC LO, 0x0E4 HI);
    // writing HI commits {hi,lo} -> staging[addr] and auto-increments addr. 0x0E8 GO
    // retriggers. The we/go outputs are TOGGLES (the feeder lives in design_clk; the
    // de25 top syncs+edge-detects them into pulses) with stable level addr/data.
    output logic         feeder_stg_we_tgl_o,   // toggles on each staging commit (HI write)
    output logic [11:0]  feeder_stg_waddr_o,    // staging word index for the committed word (level)
    output logic [63:0]  feeder_stg_wdata_o,    // {hi,lo} committed word (level)
    output logic         feeder_go_tgl_o,       // toggles on each 0x0E8 GO write (bit0=1)
    input  logic         feeder_ready_i,        // 0x0D8[0] feeder control FSM in C_READY (async; synced)
    input  logic [15:0]  feeder_records_i,      // 0x0E4 records_emitted (async counter)
    input  logic [31:0]  feeder_waits_i,        // 0x0E8 fifo_wait_cycles (async counter)

    // Ch367 -- runtime CLUT staging. HPS writes a 1 KiB bank at
    // 0x200..0x5FC, stores its expected sum32 at 0x1E4, then writes bit0
    // at 0x1E0 to commit. One toggle crosses to the design-clock copier.
    output logic [7:0]   clut_stage_waddr_o,
    output logic [31:0]  clut_stage_wdata_o,
    output logic         clut_stage_we_o,
    output logic         clut_commit_tgl_o,
    output logic [31:0]  clut_expected_crc_o,
    input  logic         clut_busy_i,
    input  logic         clut_done_tgl_i,
    input  logic [31:0]  clut_crc_i,

    // ---- AXI4 slave (signature matches ps2_hps_bridge_null) ----
    input  logic [3:0]   s_axi_awid,
    input  logic [37:0]  s_axi_awaddr,
    input  logic [7:0]   s_axi_awlen,
    input  logic [2:0]   s_axi_awsize,
    input  logic [1:0]   s_axi_awburst,
    input  logic         s_axi_awlock,
    input  logic [3:0]   s_axi_awcache,
    input  logic [2:0]   s_axi_awprot,
    input  logic         s_axi_awvalid,
    output logic         s_axi_awready,

    input  logic [127:0] s_axi_wdata,
    input  logic [15:0]  s_axi_wstrb,
    input  logic         s_axi_wlast,
    input  logic         s_axi_wvalid,
    output logic         s_axi_wready,

    output logic [3:0]   s_axi_bid,
    output logic [1:0]   s_axi_bresp,
    output logic         s_axi_bvalid,
    input  logic         s_axi_bready,

    input  logic [3:0]   s_axi_arid,
    input  logic [37:0]  s_axi_araddr,
    input  logic [7:0]   s_axi_arlen,
    input  logic [2:0]   s_axi_arsize,
    input  logic [1:0]   s_axi_arburst,
    input  logic         s_axi_arlock,
    input  logic [3:0]   s_axi_arcache,
    input  logic [2:0]   s_axi_arprot,
    input  logic         s_axi_arvalid,
    output logic         s_axi_arready,

    output logic [3:0]   s_axi_rid,
    output logic [127:0] s_axi_rdata,
    output logic [1:0]   s_axi_rresp,
    output logic         s_axi_rlast,
    output logic         s_axi_rvalid,
    input  logic         s_axi_rready,

    // ---- Ch176 outputs ---------------------------------------------------
    // core_reset_req: latched CORE_CTRL[0]. Active-high. Asynchronous to
    // the design domain — the board top is responsible for 2-FF syncing
    // this into design_clk and gating the design's reset.
    output logic         core_reset_req,

    // ---- Ch229 tile write-event broadcast --------------------------------
    // On every accepted AXI write into the Ch227 tile RAM window, latch
    // the index + data and toggle a 1-bit "event" signal. A design-clock
    // CDC (`tile_ram_cdc`) detects edges on the synchronized toggle to
    // gate writes into its shadow RAM. The bridge still owns the
    // HPS-side `tile_mem` for AXI readback — Ch229 only adds the
    // broadcast outputs; it does not change the Ch227 storage path.
    // Toggle starts at 0 after reset (matched on the receiver side, so
    // no spurious post-reset edge if both domains reset together).
    output logic         tile_wr_toggle,    // toggles per tile write
    output logic [9:0]   tile_wr_index,     // word index (= addr[11:2])
    output logic [31:0]  tile_wr_data,      // 32-bit word data

    // ---- Ch235 input latch broadcast (bridge clock domain) ---------------
    // Ch222 `INPUT_P1`/`INPUT_P2` latches (@ 0x040/0x044) surfaced for
    // downstream PS2-fabric consumers. The Ch234 `sio2_input_stub`
    // (instantiated inside `iop_memory_map_stub`) is the first
    // consumer; it owns the bridge-clk → design-clk CDC via its
    // internal 2-FF synchronizer, so this port is plain combinational
    // out of the bridge — no extra sync stage here.
    output logic [31:0]  input_p1_o,        // = input_p1_q
    output logic [31:0]  input_p2_o,        // = input_p2_q
    // Ch245 — INPUT_P1_RAW (un-remapped retroDE bitmap) surfaced for
    // the platform osd_menu_fsm. retrodesd may per-game-remap
    // INPUT_P1 (A↔B swap etc.) — the OSD navigation uses the raw
    // bitmap directly so D-pad / Start / Select / A / B map to the
    // FSM's expected bit positions regardless of per-game remaps.
    // Matches NES at retroDE_nes.sv:1235 (".joy0_buttons(bridge_joy0_osd)").
    output logic [31:0]  input_p1_raw_o,    // = input_p1_raw_q

    // ---- Ch245 platform OSD register surface -----------------------------
    // Mirror sibling-ABI bridge ports so the shared
    // retroDE_splash osd_overlay + osd_menu_fsm slot in with no
    // PS2-local cell-format assumptions. The whole 32-bit register
    // value is exposed; the top extracts fields per the sibling
    // layout (cols=bits[5:0], rows=bits[12:8], origin_x_chars=
    // bits[23:16], origin_y_chars=bits[31:24]).
    output logic [31:0]  osd_ctrl_o,        // = osd_ctrl_q
    output logic [31:0]  osd_cfg0_o,        // = osd_cfg0_q
    output logic [31:0]  osd_cfg1_o,        // = osd_cfg1_q

    // Menu-FSM outputs (sys_clk = CLOCK2_50 domain). Compose
    // OSD_STATUS read + OSD_TRIGGER set-bits.
    input  logic         osd_active_i,
    input  logic [4:0]   osd_cursor_row_i,
    input  logic         osd_set_trigger_i,        // A button (one-cycle pulse)
    input  logic         osd_back_trigger_i,       // B button (one-cycle pulse)
    input  logic         osd_scroll_down_trigger_i,// cursor down past end
    input  logic         osd_scroll_up_trigger_i,  // cursor up past start
    input  logic         osd_open_trigger_i,       // OSD just became visible
    input  logic [4:0]   osd_trigger_row_i,        // active row at trigger

    // ---- Ch248 DS2 wired-controller inputs (from the platform
    //      `ds2_controller` instantiated in the top, polling the
    //      DE25-Nano GPIO header at the PSX SPI-like protocol rate).
    //      These replace the Ch226 hardcoded stub that always reported
    //      "no controller plugged in." Same clock domain as the
    //      bridge (both ride sys_clk = CLOCK2_50), so no synchronizer
    //      needed at this end.
    input  logic [31:0]  ds2_buttons_i,
    input  logic         ds2_connected_i,
    input  logic         ds2_error_i
);

    // ----------------------------------------------------------------
    // 2-stage synchronizers for the async status signals.
    // ----------------------------------------------------------------
    logic [1:0] core_halt_sync;
    logic [1:0] dma_done_sync;
    logic [1:0] frame_seen_sync;
    logic [1:0] hdmi_init_sync;
    logic [1:0] hdmi_err_sync;
    logic [1:0] raster_of_sync;
    logic [1:0] frame_tgl_sync;        // Ch174
    logic [1:0] dma_tgl_sync;          // Ch174
    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            core_halt_sync  <= 2'b00;
            dma_done_sync   <= 2'b00;
            frame_seen_sync <= 2'b00;
            hdmi_init_sync  <= 2'b00;
            hdmi_err_sync   <= 2'b00;
            raster_of_sync  <= 2'b00;
            frame_tgl_sync  <= 2'b00;
            dma_tgl_sync    <= 2'b00;
        end else begin
            core_halt_sync  <= {core_halt_sync[0],  core_halt};
            dma_done_sync   <= {dma_done_sync[0],   dma_done_seen};
            frame_seen_sync <= {frame_seen_sync[0], frame_seen};
            hdmi_init_sync  <= {hdmi_init_sync[0],  hdmi_init_done};
            hdmi_err_sync   <= {hdmi_err_sync[0],   hdmi_i2c_error};
            raster_of_sync  <= {raster_of_sync[0],  raster_overflow};
            frame_tgl_sync  <= {frame_tgl_sync[0],  frame_toggle};
            dma_tgl_sync    <= {dma_tgl_sync[0],    dma_done_toggle};
        end
    end
    wire core_halt_q  = core_halt_sync[1];
    wire dma_done_q   = dma_done_sync[1];
    wire frame_seen_q = frame_seen_sync[1];
    wire hdmi_init_q  = hdmi_init_sync[1];
    wire hdmi_err_q   = hdmi_err_sync[1];
    wire raster_of_q  = raster_of_sync[1];
    wire frame_tgl_q  = frame_tgl_sync[1];
    wire dma_tgl_q    = dma_tgl_sync[1];

    // ----------------------------------------------------------------
    // Ch176 — writable control register at 0x010.
    //   bit 0: RESET       (functional, drives core_reset_req)
    //   bit 1: ROM_LOADED  (latch only)
    //   bit 2: PAUSE       (latch only)
    //   bits[31:3]: writable but ignored (readback as 0 — only the
    //               three ABI-defined bits survive a write/read
    //               round-trip).
    // Write path: the write FSM signals `core_ctrl_we` for one clk
    // cycle when AXI accepts a write addressed to 0x010 and pulls
    // the new value out of the AXI wstrb-aligned lane.
    // ----------------------------------------------------------------
    logic [2:0]  core_ctrl_q;
    logic        core_ctrl_we;
    logic [2:0]  core_ctrl_d;
    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n)
            core_ctrl_q <= 3'b000;
        else if (core_ctrl_we)
            core_ctrl_q <= core_ctrl_d;
    end
    assign core_reset_req = core_ctrl_q[0];

    // ----------------------------------------------------------------
    // Ch176 — CORE_PULSE write decode. Self-clearing pulses live in
    // CLOCK2_50 (bridge clock) only; HDMI_CLR (bit 3) zeros the
    // diagnostic counters. Other bits ACK and discard.
    // ----------------------------------------------------------------
    logic        core_pulse_we;
    logic        hdmi_clr_pulse;

    // ----------------------------------------------------------------
    // Ch222 — INPUT_P1 / INPUT_P2 / INPUT_P1_RAW write/read latches
    // at 0x040 / 0x044 / 0x048. HPS-visible state only — retrodesd's
    // `input_thread.c` writes the active controller bitmap here and
    // the supervisor menu reads INPUT_P1_RAW. PS2 has no SIO2 /
    // DualShock pipeline yet, so the values are stored and read back,
    // nothing else. Reset clears unconditionally. Matches the shared
    // ABI v1.0 input block used by retroDE_{nes,gb,Atari2600,coco2}.
    // ----------------------------------------------------------------
    logic [31:0] input_p1_q;
    logic [31:0] input_p2_q;
    logic [31:0] input_p1_raw_q;

    // ----------------------------------------------------------------
    // Ch223 — OSD compatibility sink at 0x100 / 0x110 / 0x114. Same
    // "store-and-readback" pattern as Ch222: retrodesd's splash
    // backend writes these latches during `osd_setup`; the bridge
    // doesn't render an overlay yet, but the writes now land in real
    // registers instead of being silently dropped. **Nothing in the
    // PS2 fabric consumes these bits** — readback returns whatever
    // userspace wrote, not a confirmation that an overlay engine
    // observed it. Reset clears unconditionally.
    // ----------------------------------------------------------------
    logic [31:0] osd_ctrl_q;
    // Ch318 — LPDDR test control registers (declared here; reset/write below).
    logic        lpddr_arm_q;
    logic        lpddr_canary_q;
    logic [31:0] lpddr_fb_base_q;
    // Ch319 Brick 3 — LPDDR4B read-probe registers + return-path CDC.
    logic [31:0] lpddr_rd_addr_q;
    logic        lpddr_rd_pulse_q;
    logic [1:0]  lpddr_rd_done_sync;
    logic [31:0] lpddr_rd_data_lat;
    wire         lpddr_rd_pending = (lpddr_rd_pulse_q != lpddr_rd_done_sync[1]);
    assign       lpddr_rd_addr_o  = lpddr_rd_addr_q;
    assign       lpddr_rd_pulse_o = lpddr_rd_pulse_q;
    // Ch323 diag — write-path commit experiment control (0x0C0).
    logic [7:0]  diag_ctrl_q;
    assign       diag_ctrl_o = diag_ctrl_q;
    // Ch320 — scanout source-select + synced status.
    logic        lpddr_video_src_q;
    logic        lpddr_scanout_lb_q;
    logic [1:0]  lpddr_scan_valid_sync, lpddr_scan_err_sync;
    assign       lpddr_video_src_o  = lpddr_video_src_q;
    assign       lpddr_scanout_lb_o = lpddr_scanout_lb_q;
    // Ch322 — LPDDR write-probe + texture-cache fill registers + return-path CDC.
    logic [31:0] lpddr_wr_addr_q;       // "next write" pointer (auto-increments)
    logic [31:0] lpddr_wr_addr_present_q;// the address that goes WITH the current data word
    logic [31:0] lpddr_wr_data_q;
    logic        lpddr_wr_pulse_q;
    logic        tex_fill_start_q;
    logic [1:0]  tex_fill_done_sync;
    logic [1:0]  lpddr_wr_busy_sync;
    // Ch352 CDC hardening (Codex) — the EMIF-domain status no longer crosses raw into reg_read:
    //   * lpddr_wr_done_sync : 3-FF sync of the write-probe done TOGGLE -> a STABLE write-pending bit
    //     (write_pending = issued_pulse ^ done_synced). Stop polling the transient wr_busy level.
    //   * tex_fill_*_snap_q  : the multi-bit fill counters latched into THIS (clk) domain on the synced
    //     fill_done rising edge — they are stable then (fill complete), so the sample is coherent. reg_read
    //     returns these snapshots, never the raw emif counters.
    logic [2:0]  lpddr_wr_done_sync;
    logic [31:0] tex_fill_beats_snap_q;
    logic [31:0] tex_fill_bytes_snap_q;
    logic [31:0] tex_rd_errs_snap_q;
    logic [31:0] tex_fill_crc_snap_q;
    logic        lpddr_ctrl_commit_q;     // toggles on any arm/canary/fb_base write (-> EMIF control snapshot)
    wire         write_pending = lpddr_wr_pulse_q ^ lpddr_wr_done_sync[2];
    assign       lpddr_ctrl_commit_o = lpddr_ctrl_commit_q;
    // Ch352 (Codex) — the remaining raw emif->bridge status signals. lpddr_idle is single-bit (2-FF sync).
    // The FB-writer counters (bytes/bursts/bresp_err/fifo_ovf) are multi-bit and only stable while the writer
    // is IDLE, so latch them into clk-domain snapshots WHILE the synced idle is high (data stable -> coherent).
    // The write-probe error counter latches on the synced write-done edge (count stable when a write completes).
    logic [1:0]  lpddr_idle_sync;
    logic [1:0]  frame_drained_sync;     // Ch353 — 2-FF sync of frame_drained_i into the bridge clock
    logic [1:0]  clear_done_sync;        // Ch357 — 2-FF sync of clear_done_i (persistent-Z preclear done)
    logic [31:0] frag_drops_snap_q;      // Ch357 — clk-domain snapshot of frag_drops_i, latched while drained (quiescent)
    logic        frag_drops_nz_sticky_q; // Ch357 — sticky: set once any per-scene snapshot is nonzero (fail-closed drop flag)
    logic [31:0] lpddr_bytes_snap_q;
    logic [31:0] lpddr_bursts_snap_q;
    logic [31:0] lpddr_bresp_err_snap_q;
    logic [31:0] lpddr_fifo_ovf_snap_q;
    logic [31:0] lpddr_wr_bresp_err_snap_q;
    // Present the SNAPSHOT taken when the data word was written — NOT lpddr_wr_addr_q,
    // which auto-increments the same cycle the pulse toggles. The write-probe samples
    // wr_addr asynchronously across the CDC, so it must stay stable between data writes
    // (like wr_data). Presenting the live (incrementing) addr made every texel land +4B
    // past its slot (Ch322 board bug).
    assign       lpddr_wr_addr_o  = lpddr_wr_addr_present_q;
    assign       lpddr_wr_data_o  = lpddr_wr_data_q;
    assign       lpddr_wr_pulse_o = lpddr_wr_pulse_q;
    assign       tex_fill_start_o = tex_fill_start_q;
    logic [31:0] osd_cfg0_q;
    logic [31:0] osd_cfg1_q;
    // Ch245 — OSD_TRIGGER is a R/W register: HPS clears bits with W1C,
    // the menu FSM (sys_clk domain) sets bits on action edges.
    logic [31:0] osd_trigger_q;

    // ----------------------------------------------------------------
    // Ch227 — tile RAM storage at 0x1000..0x1FFF (4 KB window).
    // 1024 × 32-bit words; HPS writes land here, reads return the
    // last value written. No overlay engine yet (Ch228). The sim
    // initial block zero-fills for deterministic TBs; on hardware
    // the power-up value is undefined until the overlay chapter
    // pins a reset contract (BRAM blocks on Agilex 5 don't typically
    // sync-reset every word, and there is no sibling precedent for
    // bridge-side tile-RAM clearing).
    //
    // Ch232 hotfix: explicit `ramstyle = "M20K"` attribute forces
    // Quartus to put the 32 Kbit storage into a single M20K block
    // instead of distributed LABs. Without the hint the AXI read
    // path (combinational lookup via a function call) confused the
    // inferencer.
    // ----------------------------------------------------------------
    (* ramstyle = "M20K" *) logic [31:0] tile_mem [0:1023];
    initial begin
        for (int i = 0; i < 1024; i++)
            tile_mem[i] = 32'd0;
    end

    // ----------------------------------------------------------------
    // Counters. Ch174: FRAME_COUNT / DMA_DONE_COUNT increment on each
    // edge (rising OR falling) of the synchronized event-toggle —
    // because the toggle flips per event in the design domain, every
    // edge in the bridge domain is exactly one event. See the
    // event-counter CDC contract in the header comment.
    //
    // RASTER_OVERFLOW_COUNT counts every clk cycle the synced
    // raster_overflow stays high (cheap stickyness + approximate
    // severity in one register).
    //
    // Ch176: a CORE_PULSE write with HDMI_CLR set takes priority over
    // the increment in the same cycle and zeros all three counters
    // synchronously in CLOCK2_50.
    // ----------------------------------------------------------------
    logic        frame_tgl_q1;
    logic        dma_tgl_q1;
    logic [31:0] frame_count;
    logic [31:0] dma_done_count;
    logic [31:0] raster_overflow_count;
    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            frame_tgl_q1          <= 1'b0;
            dma_tgl_q1            <= 1'b0;
            frame_count           <= 32'd0;
            dma_done_count        <= 32'd0;
            raster_overflow_count <= 32'd0;
        end else begin
            frame_tgl_q1 <= frame_tgl_q;
            dma_tgl_q1   <= dma_tgl_q;
            if (hdmi_clr_pulse) begin
                frame_count           <= 32'd0;
                dma_done_count        <= 32'd0;
                raster_overflow_count <= 32'd0;
            end else begin
                if (frame_tgl_q ^ frame_tgl_q1)
                    frame_count <= frame_count + 32'd1;
                if (dma_tgl_q ^ dma_tgl_q1)
                    dma_done_count <= dma_done_count + 32'd1;
                if (raster_of_q)
                    raster_overflow_count <= raster_overflow_count + 32'd1;
            end
        end
    end

    // ----------------------------------------------------------------
    // Register-window decode (read side). Returns the 32-bit register
    // value for the given byte address; out-of-range reads return 0.
    // ----------------------------------------------------------------
    localparam logic [31:0] CORE_ID     = 32'h50533200;   // "PS2\0"
    localparam logic [31:0] ABI_VERSION = 32'h00000100;
    // Ch246 — advertise OSD geometry via CORE_CAPS following the
    // sibling-ABI bit layout (matches nes_hps_bridge.sv:1024-1031).
    //   bit 0     has_save_ram     = 0 (PS2 memory card not yet wired)
    //   bit 1     has_savestates   = 0 (not yet implemented)
    //   bit 2     two_player       = 0 (P2 input wiring is bringup-only)
    //   bit 3     analog_input     = 0 (DS2 analog sticks not yet plumbed)
    //   [7:4]     max_savestate_slots = 0
    //   [15:8]    osd_columns      = 40
    //   [20:16]   osd_rows         = 16
    // = (16<<16) | (40<<8) = 0x00102800
    //
    // The runtime fix for PS2's current 640×480 picture lives in
    // retrodesd's backend (it still computes origin from a hardcoded
    // 1280×720 assumption, which clips on us). This advertisement
    // is forward-looking metadata so a CORE_CAPS-aware retrodesd can
    // use it directly; Ch246's hardware-side change is intentionally
    // minimal per Codex's framing.
    localparam logic [31:0] CORE_CAPS   = 32'h00102800;

    function automatic logic [31:0] reg_read(input logic [37:0] addr);
        logic [31:0] core_status;
        logic [31:0] video_status;
        logic [31:0] hdmi_diag;
        core_status = '0;
        core_status[0] = 1'b1;             // loaded
        core_status[1] = core_halt_q;
        core_status[2] = dma_done_q;
        core_status[3] = frame_seen_q;
        core_status[4] = hdmi_init_q;
        core_status[5] = hdmi_err_q;
        core_status[6] = raster_of_q;
        // Ch225 — diagnostic views into the same synchronized signals.
        video_status = '0;
        video_status[0] = frame_seen_q;
        video_status[1] = (frame_count != 32'd0);  // scanout_alive
        video_status[2] = raster_of_q;
        video_status[3] = dma_done_q;
        hdmi_diag = '0;
        hdmi_diag[0] = hdmi_init_q;
        hdmi_diag[1] = hdmi_err_q;
        // Decode is split into two windows:
        //   1. 0x000..0x0FF — shared-ABI prefix (0x000-0x01F), Ch174
        //      diagnostics (0x020-0x02F), Ch222 input latches
        //      (0x040-0x048), Ch225 diagnostic reads (0x060/0x064),
        //      Ch226 DS2 stub (0x0F0/0x0F4). Selected by
        //      addr[37:8]=='0 (Ch226 widened the window from 128 B
        //      → 256 B; existing slot indices keep their values
        //      since the high bit is implicitly 0 for offsets ≤ 0x07F).
        //   2. 0x100..0x11F — Ch223 OSD compatibility sink. Selected
        //      by addr[37:5]==33'h08 (= 0x100/32).
        // Anything else reads 0.
        reg_read = 32'd0;
        if (addr == 38'h00000001E0) begin
            reg_read = {30'd0, clut_done_sync[2], clut_busy_sync[1]}; // CLUT_STATUS
        end else if (addr == 38'h00000001E4) begin
            reg_read = clut_expected_crc_q;                            // CLUT_EXPECTED_CRC
        end else if (addr == 38'h00000001E8) begin
            reg_read = clut_crc_q;                                     // CLUT_RESULT_CRC
        end else if (addr[37:8] == '0) begin
            case (addr[7:2])
                6'h00: reg_read = CORE_ID;                  // 0x000
                6'h01: reg_read = ABI_VERSION;              // 0x004
                6'h02: reg_read = core_status;              // 0x008
                6'h03: reg_read = CORE_CAPS;                // 0x00C
                6'h04: reg_read = {29'd0, core_ctrl_q};     // 0x010 CORE_CTRL (Ch176)
                6'h05: reg_read = 32'd0;                    // 0x014 CORE_PULSE — self-clearing, reads 0
                6'h06: reg_read = {28'd0, lpddr_scanout_lb_q, lpddr_video_src_q, lpddr_canary_q, lpddr_arm_q}; // 0x018 LPDDR_CTRL: [0]arm [1]canary [2]video_src [3]scanout_lb (Ch318/320/321 RW)
                6'h07: reg_read = lpddr_fb_base_q;          // 0x01C LPDDR_FB_BASE (Ch318 RW)
                6'h08: reg_read = frame_count;              // 0x020
                6'h09: reg_read = dma_done_count;           // 0x024
                6'h0A: reg_read = raster_overflow_count;    // 0x028
                // 0x02C LPDDR_STATUS (R): [0]idle [1]bresp_err [2]fifo_ovf [3]rd_pending [4]scan_cache_valid [5]scan_rd_err.
                // Ch352 — idle via 2-FF sync; bresp_err/fifo_ovf reduced from the clk-domain snapshots (coherent).
                // Ch353 — [6]frame_drained (STABLE ordered drain ack; poll THIS, not transient [0]idle).
                6'h0B: reg_read = {23'd0, frag_drops_nz_sticky_q, clear_done_sync[1], frame_drained_sync[1], lpddr_scan_err_sync[1], lpddr_scan_valid_sync[1], lpddr_rd_pending, (|lpddr_fifo_ovf_snap_q), (|lpddr_bresp_err_snap_q), lpddr_idle_sync[1]};
                6'h0C: reg_read = lpddr_bytes_snap_q;       // 0x030 LPDDR_BYTES (Ch352 — clk-domain snapshot)
                6'h0D: reg_read = lpddr_bursts_snap_q;      // 0x034 LPDDR_BURSTS (Ch352 — clk-domain snapshot)
                6'h0E: reg_read = lpddr_bresp_err_snap_q;   // 0x038 LPDDR_BRESP_ERRS (Ch352 — clk-domain snapshot)
                6'h0F: reg_read = lpddr_rd_data_lat;        // 0x03C LPDDR_RDATA (Ch319 R): last read-probe word. WRITE this offset to set addr + trigger a read.
                6'h10: reg_read = input_p1_q;               // 0x040 INPUT_P1     (Ch222)
                6'h11: reg_read = input_p2_q;               // 0x044 INPUT_P2     (Ch222)
                6'h12: reg_read = input_p1_raw_q;           // 0x048 INPUT_P1_RAW (Ch222)
                6'h13: reg_read = lpddr_wr_addr_q;          // 0x04C LPDDR_WRADDR (Ch322 RW; auto-inc on 0x050 write)
                6'h14: reg_read = 32'd0;                    // 0x050 LPDDR_WRDATA (Ch322 W-only: data+pulse; read 0)
                // 0x054 TEX_FILL_CTRL(W[0]) / TEX_STATUS(R): [0]fill_done [1]wr_busy [2]write_pending (Ch352 — the
                // STABLE write-done flag; poll [2]==0 between writes instead of catching the transient [1]wr_busy).
                6'h15: reg_read = {29'd0, write_pending, lpddr_wr_busy_sync[1], tex_fill_done_sync[1]};
                6'h16: reg_read = tex_fill_beats_snap_q;    // 0x058 TEX_FILL_BEATS (Ch352 — clk-domain snapshot)
                6'h17: reg_read = tex_fill_bytes_snap_q;    // 0x05C TEX_FILL_BYTES (Ch352 — clk-domain snapshot)
                6'h1A: reg_read = tex_rd_errs_snap_q;       // 0x068 TEX_RD_ERRS (Ch352 — clk-domain snapshot)
                6'h1B: reg_read = lpddr_wr_bresp_err_snap_q;// 0x06C LPDDR_WR_BRESP_ERRS (Ch352 — clk-domain snapshot)
                6'h1C: reg_read = tex_fill_crc_snap_q;      // 0x070 TEX_FILL_CRC (Ch352 — sum32 of tex_mem words; integrity proof)
                6'h1E: reg_read = tex_cache_hits_i;         // 0x078 TEX_CACHE_HITS (Ch322 R): texels served from LPDDR cache
                6'h1F: reg_read = tex_bram_hits_i;          // 0x07C TEX_BRAM_HITS  (Ch322 R): texels served from BRAM
                // Ch323 — tile COLOR+Z spill/reload counters (all R; 0 when GS_TILE_SPILL off).
                6'h20: reg_read = spill_color_beats_i;      // 0x080 SPILL_COLOR_WR_BEATS: color flush words → LPDDR
                6'h21: reg_read = spill_z_beats_i;          // 0x084 SPILL_Z_WR_BEATS:    Z flush words → LPDDR
                6'h22: reg_read = reload_color_beats_i;     // 0x088 RELOAD_COLOR_BEATS:  color beats read back per reload
                6'h23: reg_read = reload_z_beats_i;         // 0x08C RELOAD_Z_BEATS:      Z beats read back per reload
                6'h24: reg_read = reload_rd_errs_i;         // 0x090 RELOAD_RD_ERRS:      reload non-OKAY read responses
                6'h25: reg_read = spill_color_errs_i;       // 0x094 SPILL_COLOR_WR_ERRS: color flush non-OKAY write responses
                6'h26: reg_read = spill_z_errs_i;           // 0x098 SPILL_Z_WR_ERRS:     Z flush non-OKAY write responses
                6'h27: reg_read = {30'd0, spill_z_ovf_i, spill_color_ovf_i}; // 0x09C SPILL_OVF: [0]color FIFO ovf [1]Z FIFO ovf
                // Ch323 diag — UPSTREAM tile-spill/reload EVENT counters (reset per-render). Prove
                // the rasterizer entered the phases + emitted BEFORE diagnosing LPDDR commit.
                6'h28: reg_read = ev_tp_flush_i;     // 0x0A0 EV_TP_FLUSH   : entries into TP_FLUSH
                6'h29: reg_read = ev_tp_zflush_i;    // 0x0A4 EV_TP_ZFLUSH  : entries into TP_ZFLUSH
                6'h2A: reg_read = ev_tp_reload_i;    // 0x0A8 EV_TP_RELOAD  : entries into TP_RELOAD
                6'h2B: reg_read = ev_tp_render_i;    // 0x0AC EV_TP_RENDER  : entries into TP_RENDER
                6'h2C: reg_read = ev_flush_emit_i;   // 0x0B0 EV_FLUSH_EMIT : color flush emit pulses
                6'h2D: reg_read = ev_zflush_emit_i;  // 0x0B4 EV_ZFLUSH_EMIT: Z flush emit pulses
                6'h2E: reg_read = ev_reload_start_i; // 0x0B8 EV_RELOAD_START: reload-arm pulses
                6'h2F: reg_read = ev_reload_ready_i; // 0x0BC EV_RELOAD_READY: reload-ready rising edges
                6'h30: reg_read = {24'd0, diag_ctrl_q}; // 0x0C0 DIAG_CTRL (RW): [6]=trace_clear (per-render counter reset)
                6'h31: reg_read = dbg_z_beat_count_i;   // 0x0C4 — Z writer beats since trace_clear
                6'h32: reg_read = dbg_z_emit_count_i;   // 0x0C8 — Z writer GS emits accepted
                6'h33: reg_read = dbg_c_emit_count_i;   // 0x0CC — color writer GS emits accepted (expect 512)
                6'h34: reg_read = dbg_z_push_count_i;   // 0x0D0 — Z writer GS beats pushed to FIFO
                6'h35: reg_read = dbg_z_pop_count_i;    // 0x0D4 — Z writer EMIF beats popped from FIFO
                6'h38: reg_read = dbg_c_beat_count_i;   // 0x0E0 — color writer beats since trace_clear
                // Ch330 Brick 4 — feeder status reads.
                6'h36: reg_read = {31'd0, feeder_ready_sync[1]}; // 0x0D8 FEEDER_STATUS [0]=ready (synced)
                6'h37: reg_read = {20'd0, feeder_addr_q};        // 0x0DC FEEDER_STG_ADDR (current write index)
                6'h39: reg_read = {16'd0, feeder_records_i};     // 0x0E4 FEEDER_RECORDS (records_emitted)
                6'h3A: reg_read = feeder_waits_i;                // 0x0E8 FEEDER_WAITS   (fifo_wait_cycles)
                6'h3B: reg_read = frag_drops_snap_q;             // 0x0EC LPDDR_FRAG_DROPS (Ch357 R): persistent-Z dropped-fragment snapshot (per-scene delta must be 0)
                6'h18: reg_read = video_status;             // 0x060 VIDEO_STATUS (Ch225)
                6'h19: reg_read = hdmi_diag;                // 0x064 HDMI_DIAG    (Ch225)
                // Ch248 — replaced the Ch226 fake DS2 with the real
                // platform `ds2_controller` outputs. Bit layout matches
                // the sibling-ABI shape that retrodesd's
                // `ds2_poll_thread` consumes:
                //   [0] connected   (1 = wired controller present + IDed)
                //   [1] error       (1 = comms timeout / bad framing)
                //   [2] reserved    (PS2-local "input_latches_valid" bit
                //                    from the Ch226 stub kept at 1 so
                //                    operators-of-prior-tooling still
                //                    see a familiar pattern when the
                //                    controller is unplugged)
                6'h3C: reg_read = {29'd0, 1'b1, ds2_error_i, ds2_connected_i}; // 0x0F0 DS2_STATUS
                6'h3D: reg_read = ds2_buttons_i;                                // 0x0F4 DS2_BUTTONS
                6'h3E: reg_read = dbg_c_push_count_i;   // 0x0F8 — color writer GS beats pushed to FIFO (expect 64)
                6'h3F: reg_read = dbg_c_pop_count_i;    // 0x0FC — color writer EMIF beats popped (expect == beats)
                default: reg_read = 32'd0;
            endcase
        end else if (addr[37:5] == 33'h08) begin
            case (addr[4:2])
                3'h0: reg_read = osd_ctrl_q;                // 0x100 OSD_CTRL
                // Ch245 — OSD_STATUS in sibling-ABI layout:
                //   bit 0     = osd_active (from menu FSM)
                //   bits 12:8 = cursor_row (from menu FSM)
                // sys_clk → bridge_clk are the SAME clock here
                // (both run from CLOCK2_50), so no synchronizer needed.
                3'h1: reg_read = {19'd0, osd_cursor_row_i, 7'd0, osd_active_i};
                3'h2: reg_read = osd_trigger_q;             // 0x108 OSD_TRIGGER (Ch245 R/W)
                3'h3: reg_read = 32'd0;                     // 0x10C OSD_INPUT   (reserved)
                3'h4: reg_read = osd_cfg0_q;                // 0x110 OSD_CFG0
                3'h5: reg_read = osd_cfg1_q;                // 0x114 OSD_CFG1
                default: reg_read = 32'd0;                  // 0x118 / 0x11C reserved-zero
            endcase
        end else if (addr[37:12] == 26'h1) begin
            // Ch227 — tile RAM 0x1000..0x1FFF, 1024 × 32-bit.
            reg_read = tile_mem[addr[11:2]];
        end
    endfunction

    // ----------------------------------------------------------------
    // Write FSM. Single-beat: accept aw + w together, emit b, return.
    // Ch176: writes to 0x010 / 0x014 now take effect (CORE_CTRL latch
    // + CORE_PULSE self-clearing). Writes to any other address are
    // ACK'd-and-discarded (the read-only identity / counter window).
    // ----------------------------------------------------------------
    typedef enum logic [1:0] { W_IDLE, W_DATA, W_RESP } w_state_t;
    w_state_t      w_state;
    logic [3:0]    aw_id_q;
    logic [37:0]   aw_addr_q;

    // Pull the 32-bit register value out of the 128-bit AXI wdata via
    // the awaddr[3:2] lane select (matches the read-side encoding).
    logic [31:0]   wdata_lane;
    always_comb begin
        case (aw_addr_q[3:2])
            2'b00:   wdata_lane = s_axi_wdata[31:0];
            2'b01:   wdata_lane = s_axi_wdata[63:32];
            2'b10:   wdata_lane = s_axi_wdata[95:64];
            default: wdata_lane = s_axi_wdata[127:96];
        endcase
    end

    // Address decode for write side. Same 6-bit word index inside the
    // first 256 bytes (Ch226 widened from 128 B) that the read side
    // uses. DS2 stub at 0x0F0/0x0F4 is read-only — no write_is_*
    // signals; writes there fall through to the no-side-effect path.
    wire write_in_window  = (aw_addr_q[37:8] == '0);
    wire write_is_ctrl    = write_in_window && (aw_addr_q[7:2] == 6'h04);
    wire write_is_pulse   = write_in_window && (aw_addr_q[7:2] == 6'h05);
    // Ch222 — input-latch decode (offsets 0x040 / 0x044 / 0x048).
    wire write_is_p1      = write_in_window && (aw_addr_q[7:2] == 6'h10);
    wire write_is_p2      = write_in_window && (aw_addr_q[7:2] == 6'h11);
    wire write_is_p1_raw  = write_in_window && (aw_addr_q[7:2] == 6'h12);
    // Ch318 — LPDDR test control writes (0x018 LPDDR_CTRL, 0x01C LPDDR_FB_BASE).
    wire write_is_lpddr_ctrl = write_in_window && (aw_addr_q[7:2] == 6'h06);
    wire write_is_lpddr_base = write_in_window && (aw_addr_q[7:2] == 6'h07);
    // Ch319 Brick 3 — LPDDR4B read-probe address write (0x03C); the write also triggers a read.
    wire write_is_lpddr_rdaddr = write_in_window && (aw_addr_q[7:2] == 6'h0F);
    // Ch323 diag — write-commit experiment control (0x0C0 = 6'h30).
    wire write_is_diag_ctrl    = write_in_window && (aw_addr_q[7:2] == 6'h30);
    // Ch322 — LPDDR write-probe + texture-cache fill writes.
    wire write_is_lpddr_wraddr = write_in_window && (aw_addr_q[7:2] == 6'h13); // 0x04C set write addr
    wire write_is_lpddr_wrdata = write_in_window && (aw_addr_q[7:2] == 6'h14); // 0x050 data -> write + pulse
    wire write_is_tex_fill     = write_in_window && (aw_addr_q[7:2] == 6'h15); // 0x054 bit0 arm fill
    // Ch330 Brick 4 — feeder staging/control writes (0x0D8/0x0DC/0x0E4/0x0E8).
    wire write_is_feeder_addr  = write_in_window && (aw_addr_q[7:2] == 6'h36); // 0x0D8 set staging word index
    wire write_is_feeder_lo    = write_in_window && (aw_addr_q[7:2] == 6'h37); // 0x0DC low 32 bits (latched)
    wire write_is_feeder_hi    = write_in_window && (aw_addr_q[7:2] == 6'h39); // 0x0E4 high 32 -> commit {hi,lo}, addr++
    wire write_is_feeder_go    = write_in_window && (aw_addr_q[7:2] == 6'h3A); // 0x0E8 bit0 -> retrigger pulse

    wire write_is_clut_commit  = (aw_addr_q == 38'h00000001E0);
    wire write_is_clut_expected= (aw_addr_q == 38'h00000001E4);
    wire write_in_clut_stage   = (aw_addr_q >= 38'h0000000200)
                              && (aw_addr_q <= 38'h00000005FC);

    // Ch223 — OSD compatibility-sink decode at 0x100..0x11F. The OSD
    // block is a 32-byte window outside the first-128-byte side-effect
    // region, so it gets its own `addr[37:5] == 33'h08` guard
    // (addr[37:5]==0x08 ⇔ addr ∈ [0x100, 0x120)).
    wire write_in_osd        = (aw_addr_q[37:5] == 33'h08);
    wire write_is_osd_ctrl   = write_in_osd && (aw_addr_q[4:2] == 3'h0); // 0x100
    wire write_is_osd_trigger= write_in_osd && (aw_addr_q[4:2] == 3'h2); // 0x108 (Ch245 W1C)
    wire write_is_osd_cfg0   = write_in_osd && (aw_addr_q[4:2] == 3'h4); // 0x110
    wire write_is_osd_cfg1   = write_in_osd && (aw_addr_q[4:2] == 3'h5); // 0x114

    // Ch227 — tile RAM write decode at 0x1000..0x1FFF (4 KB window).
    // 1024 × 32-bit words; addr[11:2] picks the slot.
    wire write_in_tile     = (aw_addr_q[37:12] == 26'h1);

    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            w_state      <= W_IDLE;
            aw_id_q      <= '0;
            aw_addr_q    <= '0;
            s_axi_bvalid <= 1'b0;
        end else begin
            case (w_state)
                W_IDLE: begin
                    s_axi_bvalid <= 1'b0;
                    if (s_axi_awvalid && s_axi_awready) begin
                        aw_id_q   <= s_axi_awid;
                        aw_addr_q <= s_axi_awaddr;
                        w_state   <= W_DATA;
                    end
                end
                W_DATA: begin
                    if (s_axi_wvalid && s_axi_wready) begin
                        s_axi_bvalid <= 1'b1;
                        w_state      <= W_RESP;
                    end
                end
                W_RESP: begin
                    if (s_axi_bready) begin
                        s_axi_bvalid <= 1'b0;
                        w_state      <= W_IDLE;
                    end
                end
                default: w_state <= W_IDLE;
            endcase
        end
    end
    assign s_axi_awready = (w_state == W_IDLE);
    assign s_axi_wready  = (w_state == W_DATA);
    assign s_axi_bid     = aw_id_q;
    assign s_axi_bresp   = 2'b00;          // OKAY

    // Ch176 — one-cycle write enable when the W beat lands on a write
    // addressed at CORE_CTRL or CORE_PULSE. Use the W_DATA accept edge
    // so the side effect happens exactly once per AXI write.
    wire write_accept = (w_state == W_DATA) && s_axi_wvalid && s_axi_wready;
    assign core_ctrl_we   = write_accept && write_is_ctrl;
    assign core_ctrl_d    = wdata_lane[2:0];
    assign core_pulse_we  = write_accept && write_is_pulse;
    assign hdmi_clr_pulse = core_pulse_we && wdata_lane[3];
    assign clut_stage_we_o    = write_accept && write_in_clut_stage;
    assign clut_stage_waddr_o = (aw_addr_q - 38'h0000000200) >> 2;
    assign clut_stage_wdata_o = wdata_lane;

    // Ch222 — input-latch register file. One always_ff covers all three
    // 32-bit latches; the lane-aligned `wdata_lane` (selected by
    // aw_addr_q[3:2] earlier) supplies the data for each. Reset clears
    // to 0; non-decoded writes leave existing values untouched.
    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            input_p1_q     <= 32'd0;
            input_p2_q     <= 32'd0;
            input_p1_raw_q <= 32'd0;
        end else if (write_accept) begin
            if (write_is_p1)     input_p1_q     <= wdata_lane;
            if (write_is_p2)     input_p2_q     <= wdata_lane;
            if (write_is_p1_raw) input_p1_raw_q <= wdata_lane;
        end
    end

    // Ch318 — LPDDR test control registers. SAFE reset defaults: arm OFF, canary ON,
    // base = 0x8000_0000 (the HPS-physical reserved region). The HPS arms via 0x018.
    assign lpddr_arm_o     = lpddr_arm_q;
    assign lpddr_canary_o  = lpddr_canary_q;
    assign lpddr_fb_base_o = lpddr_fb_base_q;
    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            lpddr_arm_q     <= 1'b0;
            lpddr_canary_q  <= 1'b1;
            lpddr_fb_base_q <= 32'h8000_0000;
            lpddr_rd_addr_q    <= 32'd0;
            lpddr_rd_pulse_q   <= 1'b0;
            lpddr_rd_done_sync <= 2'b00;
            lpddr_rd_data_lat  <= 32'd0;
            lpddr_video_src_q     <= 1'b0;   // Ch320 — default BRAM scanout
            lpddr_scanout_lb_q    <= 1'b0;   // Ch321 — default frame-cache (line-buffer = opt-in)
            lpddr_scan_valid_sync <= 2'b00;
            lpddr_scan_err_sync   <= 2'b00;
            lpddr_wr_addr_q    <= 32'd0;
            lpddr_wr_addr_present_q <= 32'd0;
            lpddr_wr_data_q    <= 32'd0;
            lpddr_wr_pulse_q   <= 1'b0;
            tex_fill_start_q   <= 1'b0;
            diag_ctrl_q        <= 8'd0;
            tex_fill_done_sync <= 2'b00;
            lpddr_wr_busy_sync <= 2'b00;
            lpddr_wr_done_sync <= 3'b000;
            tex_fill_crc_snap_q <= 32'd0;
            tex_fill_beats_snap_q <= 32'd0;
            tex_fill_bytes_snap_q <= 32'd0;
            tex_rd_errs_snap_q    <= 32'd0;
            lpddr_ctrl_commit_q   <= 1'b0;
            lpddr_idle_sync       <= 2'b00;
            frame_drained_sync    <= 2'b00;
            clear_done_sync       <= 2'b00;
            frag_drops_snap_q     <= 32'd0;
            frag_drops_nz_sticky_q<= 1'b0;
            lpddr_bytes_snap_q    <= 32'd0;
            lpddr_bursts_snap_q   <= 32'd0;
            lpddr_bresp_err_snap_q<= 32'd0;
            lpddr_fifo_ovf_snap_q <= 32'd0;
            lpddr_wr_bresp_err_snap_q <= 32'd0;
        end else begin
            // Ch319 — read-probe return path CDC (emif_clk -> clk). rd_data is stable in
            // the probe before rd_done toggles, so latching on the synced edge is safe.
            lpddr_rd_done_sync <= {lpddr_rd_done_sync[0], lpddr_rd_done_i};
            if (lpddr_rd_done_sync[0] != lpddr_rd_done_sync[1])
                lpddr_rd_data_lat <= lpddr_rd_data_i;
            // Ch320 — scanout status sync (emif_clk -> clk).
            lpddr_scan_valid_sync <= {lpddr_scan_valid_sync[0], lpddr_scan_valid_i};
            lpddr_scan_err_sync   <= {lpddr_scan_err_sync[0],   lpddr_scan_err_i};
            // Ch322 — texture-fill / write-probe status sync (emif_clk -> clk).
            tex_fill_done_sync <= {tex_fill_done_sync[0], tex_fill_done_i};
            lpddr_wr_busy_sync <= {lpddr_wr_busy_sync[0], lpddr_wr_busy_i};
            // Ch352 — 3-FF sync of the write-probe done toggle (feeds the stable write_pending bit).
            lpddr_wr_done_sync <= {lpddr_wr_done_sync[1:0], lpddr_wr_done_i};
            // Ch352 — latch the fill counters into clk domain on the synced fill_done RISING edge. The fill
            // FSM raises fill_done with the final beat, so the counters are stable when the synced edge lands
            // (2 FF later) — coherent multi-bit capture, same idea as the rd_data latch above.
            if (tex_fill_done_sync[0] && !tex_fill_done_sync[1]) begin
                tex_fill_beats_snap_q <= tex_fill_beats_i;
                tex_fill_bytes_snap_q <= tex_fill_bytes_i;
                tex_rd_errs_snap_q    <= tex_rd_errs_i;
                tex_fill_crc_snap_q   <= tex_fill_crc_i;
            end
            // Ch352 — remaining status CDC. idle: 2-FF sync. FB-writer counters: capture WHILE idle (stable).
            lpddr_idle_sync <= {lpddr_idle_sync[0], lpddr_idle_i};
            frame_drained_sync <= {frame_drained_sync[0], frame_drained_i};
            if (lpddr_idle_sync[1]) begin
                lpddr_bytes_snap_q     <= lpddr_bytes_i;
                lpddr_bursts_snap_q    <= lpddr_bursts_i;
                lpddr_bresp_err_snap_q <= lpddr_bresp_err_i;
                lpddr_fifo_ovf_snap_q  <= lpddr_fifo_ovf_i;
            end
            // Ch357 — persistent-Z preclear ack: 2-FF sync (single-bit, EMIF domain), same as frame_drained.
            clear_done_sync <= {clear_done_sync[0], clear_done_i};
            // Ch357 — drop count: latch WHILE drained (fragment flow stopped -> frag_drops_i stable, coherent multi-bit
            // capture, same idle-gated idea as the FB-writer counters). The sticky flag OR-accumulates any nonzero
            // snapshot so a single dropped fragment latches a fail-closed bit the host reads after every scene.
            if (frame_drained_sync[1]) begin
                frag_drops_snap_q      <= frag_drops_i;
                frag_drops_nz_sticky_q <= frag_drops_nz_sticky_q | (|frag_drops_i);
            end
            // write-probe error count: stable when a write completes -> latch on the synced write-done edge.
            if (lpddr_wr_done_sync[2] != lpddr_wr_done_sync[1])
                lpddr_wr_bresp_err_snap_q <= lpddr_wr_bresp_err_i;
            if (write_accept) begin
                if (write_is_lpddr_ctrl) begin
                    lpddr_arm_q       <= wdata_lane[0];
                    lpddr_canary_q    <= wdata_lane[1];
                    lpddr_video_src_q <= wdata_lane[2];   // Ch320 — select LPDDR4B scanout
                    lpddr_scanout_lb_q <= wdata_lane[3];  // Ch321 — line-buffer vs frame-cache
                end
                if (write_is_lpddr_base) lpddr_fb_base_q <= wdata_lane;
                // Ch352 — flip the commit toggle on any control/base write so the EMIF domain re-latches
                // {arm,canary,fb_base} coherently. (arm/canary/video_src/scanout_lb all live in the ctrl word.)
                if (write_is_lpddr_ctrl || write_is_lpddr_base)
                    lpddr_ctrl_commit_q <= ~lpddr_ctrl_commit_q;
                // 0x03C write: latch the read address AND pulse the read trigger.
                if (write_is_lpddr_rdaddr) begin
                    lpddr_rd_addr_q  <= wdata_lane;
                    lpddr_rd_pulse_q <= ~lpddr_rd_pulse_q;
                end
                // Ch322 — write-probe: 0x04C sets the byte address; 0x050 writes a data
                // word (toggles the write trigger) and auto-increments the address by 4 so
                // the HPS can stream a texture without re-writing the address each word.
                if (write_is_lpddr_wraddr) lpddr_wr_addr_q <= wdata_lane;
                if (write_is_lpddr_wrdata) begin
                    lpddr_wr_data_q         <= wdata_lane;
                    lpddr_wr_pulse_q        <= ~lpddr_wr_pulse_q;
                    lpddr_wr_addr_present_q <= lpddr_wr_addr_q;       // addr for THIS word (snapshot)
                    lpddr_wr_addr_q         <= lpddr_wr_addr_q + 32'd4; // advance for NEXT word
                end
                // arm (bit0=1) TOGGLES the cache fill_start — the cache edge-detects it and
                // (re)fills, so re-staging a new texture + re-arming reloads the cache.
                if (write_is_tex_fill && wdata_lane[0]) tex_fill_start_q <= ~tex_fill_start_q;
                if (write_is_diag_ctrl) diag_ctrl_q <= wdata_lane[7:0];
            end
        end
    end

    // ----------------------------------------------------------------
    // Ch330 Brick 4 — feeder staging-write + retrigger control.
    // A staging entry is written as two halves (0x0DC LO then 0x0E4 HI);
    // the HI write commits {hi,lo} -> staging[addr], TOGGLES the we strobe,
    // and auto-increments addr so the HPS streams a whole list without
    // re-writing the address. 0x0E8 GO bit0 toggles the retrigger. we/go are
    // toggles (not pulses) because the feeder runs in design_clk; the de25 top
    // syncs+edge-detects them. feeder_ready_i is synced for the 0x0D8 read.
    // ----------------------------------------------------------------
    logic [11:0] feeder_addr_q;
    logic [31:0] feeder_lo_q;
    logic [63:0] feeder_wdata_q;
    logic [11:0] feeder_waddr_q;
    logic        feeder_we_tgl_q;
    logic        feeder_go_tgl_q;
    logic [1:0]  feeder_ready_sync;
    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            feeder_addr_q     <= 12'd0;
            feeder_lo_q       <= 32'd0;
            feeder_wdata_q    <= 64'd0;
            feeder_waddr_q    <= 12'd0;
            feeder_we_tgl_q   <= 1'b0;
            feeder_go_tgl_q   <= 1'b0;
            feeder_ready_sync <= 2'b00;
        end else begin
            feeder_ready_sync <= {feeder_ready_sync[0], feeder_ready_i};
            if (write_accept) begin
                if (write_is_feeder_addr) feeder_addr_q <= wdata_lane[11:0];
                if (write_is_feeder_lo)   feeder_lo_q   <= wdata_lane;
                if (write_is_feeder_hi) begin
                    feeder_wdata_q  <= {wdata_lane, feeder_lo_q};   // {hi,lo}
                    feeder_waddr_q  <= feeder_addr_q;               // addr for THIS word (snapshot)
                    feeder_we_tgl_q <= ~feeder_we_tgl_q;            // commit strobe (toggle)
                    feeder_addr_q   <= feeder_addr_q + 12'd1;       // advance for NEXT word
                end
                if (write_is_feeder_go && wdata_lane[0]) feeder_go_tgl_q <= ~feeder_go_tgl_q;
            end
        end
    end
    assign feeder_stg_we_tgl_o = feeder_we_tgl_q;
    assign feeder_stg_waddr_o  = feeder_waddr_q;
    assign feeder_stg_wdata_o  = feeder_wdata_q;
    assign feeder_go_tgl_o     = feeder_go_tgl_q;

    // The staging write port is in this bridge clock domain. Only the
    // commit toggle crosses domains, after the host has written all entries.
    logic        clut_commit_tgl_q;
    logic [31:0] clut_expected_crc_q;
    logic [1:0]  clut_busy_sync;
    logic [2:0]  clut_done_sync;
    logic [31:0] clut_crc_q;
    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            clut_commit_tgl_q  <= 1'b0;
            clut_expected_crc_q <= 32'd0;
            clut_busy_sync     <= 2'b00;
            clut_done_sync     <= 3'b000;
            clut_crc_q         <= 32'd0;
        end else begin
            clut_busy_sync <= {clut_busy_sync[0], clut_busy_i};
            clut_done_sync <= {clut_done_sync[1:0], clut_done_tgl_i};
            if (clut_done_sync[2] != clut_done_sync[1])
                clut_crc_q <= clut_crc_i;
            if (write_accept) begin
                if (write_is_clut_expected) clut_expected_crc_q <= wdata_lane;
                if (write_is_clut_commit && wdata_lane[0] && !clut_busy_sync[1])
                    clut_commit_tgl_q <= ~clut_commit_tgl_q;
            end
        end
    end
    assign clut_commit_tgl_o    = clut_commit_tgl_q;
    assign clut_expected_crc_o  = clut_expected_crc_q;

    // Ch223 — OSD-sink register file. Same pattern as Ch222.
    // Ch245 — OSD_TRIGGER added with sibling-ABI semantics: bits are
    // SET by single-cycle pulses from the menu FSM and CLEARED by HPS
    // via write-1-to-clear. The bridge clock IS sys_clk (both ride
    // CLOCK2_50), so the FSM-side set pulses are same-cycle inputs —
    // no synchronizer required. OSD_CTRL[3] is a self-clearing
    // request bit (matches nes_hps_bridge.sv:741).
    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            osd_ctrl_q    <= 32'd0;
            osd_cfg0_q    <= 32'd0;
            osd_cfg1_q    <= 32'd0;
            osd_trigger_q <= 32'd0;
        end else begin
            // OSD_CTRL[3] self-clear (HPS-write below overrides if it
            // wrote a fresh 1 on the same cycle, NBA last-write-wins).
            if (osd_ctrl_q[3]) osd_ctrl_q[3] <= 1'b0;

            // OSD_TRIGGER: HPS W1C clear AND/OR FSM set pulses. Sets
            // take priority over clears on the same bit (the set-bit
            // assignments are lexically later in this block, so NBA
            // semantics route them to win for any common bit).
            if (write_accept && write_is_osd_trigger) begin
                osd_trigger_q <= osd_trigger_q & ~wdata_lane;
            end
            if (osd_set_trigger_i) begin
                osd_trigger_q[4]    <= 1'b1;        // action_pending (A)
                osd_trigger_q[12:8] <= osd_trigger_row_i;
            end
            if (osd_back_trigger_i) begin
                osd_trigger_q[5]    <= 1'b1;        // back_pending (B)
                osd_trigger_q[12:8] <= osd_trigger_row_i;
            end
            if (osd_scroll_down_trigger_i) osd_trigger_q[6]  <= 1'b1;
            if (osd_scroll_up_trigger_i)   osd_trigger_q[7]  <= 1'b1;
            if (osd_open_trigger_i)        osd_trigger_q[16] <= 1'b1;

            // HPS writes to OSD_CTRL/CFG0/CFG1 — full-register overwrite
            // (intentionally last so it wins over the self-clear above).
            if (write_accept) begin
                if (write_is_osd_ctrl) osd_ctrl_q <= wdata_lane;
                if (write_is_osd_cfg0) osd_cfg0_q <= wdata_lane;
                if (write_is_osd_cfg1) osd_cfg1_q <= wdata_lane;
            end
        end
    end

    // Ch249 — Ch230 osd_ctrl_enable retired. The platform OSD path
    // reads OSD_CTRL[0] out of `osd_ctrl_o[0]` (one of the three new
    // 32-bit register exports), so the single-bit Ch230 broadcast
    // had become redundant when the stub was unwired in Ch245 and
    // fully obsolete after Ch249 removed the stub instantiation.

    // Ch245 — surface full OSD register values for the platform OSD
    // stack. Sibling cores (NES, etc.) extract field bits at the top:
    //   osd_cfg0[5:0]   = cols
    //   osd_cfg0[12:8]  = rows
    //   osd_cfg0[23:16] = origin_x_chars (× 16 for 2x scale)
    //   osd_cfg0[31:24] = origin_y_chars (× 16 for 2x scale)
    //   osd_cfg1[23:16] = cursor_attr
    assign osd_ctrl_o = osd_ctrl_q;
    assign osd_cfg0_o = osd_cfg0_q;
    assign osd_cfg1_o = osd_cfg1_q;

    // Ch235 — surface Ch222 INPUT_P1/P2 latches as bridge-clock-domain
    // outputs for the downstream sio2_input_stub (which owns the CDC).
    assign input_p1_o     = input_p1_q;
    assign input_p2_o     = input_p2_q;
    assign input_p1_raw_o = input_p1_raw_q;

    // Ch227 — tile RAM write port. No reset clear (see header note —
    // retained across warm reset, sim-initialized to 0 via the
    // `initial` block above).
    always_ff @(posedge clk) begin
        if (write_accept && write_in_tile)
            tile_mem[aw_addr_q[11:2]] <= wdata_lane;
    end

    // Ch229 — broadcast tile writes to the design-domain CDC. Toggle
    // bit flips on every write; index + data latch at the same edge
    // and remain stable until the next write. CDC primitive on the
    // receiver detects toggle edges and samples index/data once they
    // are guaranteed stable (2 dclk synchronizer stages = ≥ 2 dclk
    // periods of holdover, more than enough at any reasonable HPS
    // tile-write rate). Reset clears toggle + latches so a co-reset
    // of bridge and design domains starts both sides at the same
    // value (no spurious post-reset edge).
    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            tile_wr_toggle <= 1'b0;
            tile_wr_index  <= 10'd0;
            tile_wr_data   <= 32'd0;
        end else if (write_accept && write_in_tile) begin
            tile_wr_toggle <= ~tile_wr_toggle;
            tile_wr_index  <= aw_addr_q[11:2];
            tile_wr_data   <= wdata_lane;
        end
    end

    // ----------------------------------------------------------------
    // Read FSM. Same shape — accept arvalid, drive rdata + rvalid,
    // hold until rready. rdata is the 32-bit reg value replicated
    // into the matching 32-bit lane of the 128-bit response.
    // ----------------------------------------------------------------
    typedef enum logic [0:0] { R_IDLE, R_RESP } r_state_t;
    r_state_t r_state;
    logic [3:0]   ar_id_q;
    logic [37:0]  ar_addr_q;
    logic [127:0] rdata_q;
    always_ff @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            r_state      <= R_IDLE;
            ar_id_q      <= '0;
            ar_addr_q    <= '0;
            rdata_q      <= '0;
            s_axi_rvalid <= 1'b0;
        end else begin
            case (r_state)
                R_IDLE: begin
                    s_axi_rvalid <= 1'b0;
                    if (s_axi_arvalid && s_axi_arready) begin
                        ar_id_q   <= s_axi_arid;
                        ar_addr_q <= s_axi_araddr;
                        case (s_axi_araddr[3:2])
                            2'b00:   rdata_q <= {96'd0,           reg_read(s_axi_araddr)};
                            2'b01:   rdata_q <= {64'd0,           reg_read(s_axi_araddr), 32'd0};
                            2'b10:   rdata_q <= {32'd0,           reg_read(s_axi_araddr), 64'd0};
                            default: rdata_q <= {reg_read(s_axi_araddr), 96'd0};
                        endcase
                        s_axi_rvalid <= 1'b1;
                        r_state      <= R_RESP;
                    end
                end
                R_RESP: begin
                    if (s_axi_rready) begin
                        s_axi_rvalid <= 1'b0;
                        r_state      <= R_IDLE;
                    end
                end
                default: r_state <= R_IDLE;
            endcase
        end
    end
    assign s_axi_arready = (r_state == R_IDLE);
    assign s_axi_rid     = ar_id_q;
    assign s_axi_rdata   = rdata_q;
    assign s_axi_rresp   = 2'b00;          // OKAY
    assign s_axi_rlast   = 1'b1;           // single-beat

    // ----------------------------------------------------------------
    // Tie off unused AXI4 fields to silence lint warnings.
    // ----------------------------------------------------------------
    // verilator lint_off UNUSED
    wire _unused_ok = &{ 1'b0,
        s_axi_awlen, s_axi_awsize, s_axi_awburst,
        s_axi_awlock, s_axi_awcache, s_axi_awprot,
        s_axi_wdata, s_axi_wstrb, s_axi_wlast,
        s_axi_arlen, s_axi_arsize, s_axi_arburst,
        s_axi_arlock, s_axi_arcache, s_axi_arprot,
        h2f_reset,
        1'b0 };
    // verilator lint_on UNUSED

endmodule : ps2_hps_bridge
