// retroDE_ps2 — top_psmct32_raster_demo_bram (Ch157 BRAM
// wrapper with writer-side PSM normalization + PSMT4 RMW pipe)
//
// **Ch155 origin / Ch156 generalization / Ch157 PSMT4 enable.**
// Ch156 plumbs `vram_normalize_pkg::normalize_write` between the
// writer engines (gs_stub raster + gif_image_xfer_stub) and
// `vram_bram_stub`. Ch157 adds a 1-cycle wrapper-site read-modify-
// write pipeline that lets `normalize_write` splice the PSMT4
// nibble against the live `old_byte` from VRAM (instead of
// `old_byte=0`, which Ch156 worked around with a hard-gate). The
// VRAM instance is `vram_bram_stub` (Ch154 BRAM-friendly: 2048 ×
// 32-bit, sync read 1-cycle latency, byte-WE only, no per-bit
// mask) instead of `vram_stub` (legacy 8192 × 8-bit byte-
// addressable + per-bit mask RMW + combinational reads).
//
// **PSM coverage** (Ch157):
//   - CT32: passthrough (byte_addr is word-aligned, payload is
//     the ABGR word, be=4'b1111). Same-cycle write.
//   - CT16: byte_addr[1] selects low/high halfword lane; payload
//     shifted to the right 16-bit lane; be=4'b0011 or 4'b1100.
//     Same-cycle write.
//   - PSMT8: byte_addr[1:0] selects 1 of 4 byte lanes; payload
//     shifted; be picks one byte. Same-cycle write.
//   - PSMT4: 1-cycle delayed write through the RMW pipe. The
//     wrapper drives `read2_addr = byte_addr` on the emit cycle
//     and uses the registered `read2_data` one cycle later as
//     `old_byte` for `normalize_write`. The Ch156 hard-gate is
//     gone. Back-to-back same-byte T4 emits (e.g. PSMT4 SPRITE
//     pixels at x=2k and x=2k+1 share a byte) hazard-forward
//     through `t4_prev_*` registers — see the comment block at
//     the pipe instantiation below.
//
// **Ch158 PCRTC sync-read alignment**: `gs_pcrtc_stub` is now
// instantiated with `VRAM_SYNC_READ=1` so its data-decode +
// sync-output stages are delayed by 1 cycle to align with
// `vram_bram_stub`'s registered `read_data`. The address-side
// (`vram_read_addr`) keeps using the current scanout coords so
// the read is issued one pixel "ahead"; the registered
// `vram_read_data` returns one cycle later, paired with the
// matching delayed counter view inside the PCRTC. Captured
// scanout pixels are no longer 1-column shifted, so the
// integration TB can now verify the captured frame matches the
// rasterized VRAM contents end-to-end (Ch155 had to skip frame
// capture, Ch156/Ch157 still skipped it; Ch158 unblocks it).
//
// Topology — same as the Ch146 wrapper (see
// rtl/top/top_psmct32_raster_demo.sv) except for the VRAM
// instance:
//
//   bios_rom_stub#(.IMAGE_FILE(BIOS_IMAGE_FILE))                — EE bootlet at 0xBFC0_0000
//   ee_ram_stub#(.IMAGE_FILE(PAYLOAD_IMAGE_FILE))               — GIF payload at phys 0x100
//   ee_memory_map_stub#(.USEG_SHADOW_WORDS_PARAM(1024))         — Ch145 BRAM shrink
//   ee_core_stub#(.PC_RESET(0xBFC00000))                        — MIPS R5900 core
//   ee_gs_priv_bridge_stub                                      — 32-bit MMIO → 64-bit GS-priv
//   dmac_reg_stub                                               — DMAC ch2
//   gif_packed_stub#(.REAL_AD_REG_MAP(1'b1))                    — GIFtag + PACKED A+D parser
//   gs_stub#(.PSMCT32_SWIZZLE(1'b1))                            — GS register file + raster
//   gif_image_xfer_stub#(.PSMCT32_SWIZZLE(1'b1))                — TRXDIR/IMAGE engine (idle in Ch123)
//   **vram_bram_stub#(.BYTES(8192))                             — Ch154 BRAM-friendly VRAM** (DIFF)
//   gs_pcrtc_stub#(.PSMCT32_SWIZZLE(1'b1), .VRAM_SYNC_READ(1'b1)) — PCRTC sync-read scanout (Ch158)
//
// All other behavior is inherited from the Ch146 wrapper:
// `$readmemh` from IMAGE_FILE parameters, useg_shadow trimmed
// to 1024 words via Ch145, status bundle (core_halt /
// dma_done_seen / frame_seen) exposed for LEDs, no procedural
// drives.
//
// Top-level ports:
//   clk, rst_n          — single clock domain, active-low synchronous reset
//   core_go             — pulsed high for one cycle to start the EE bootlet
//                         (a board reset-release sequencer can tie it high
//                         after rst_n deasserts)
//   r/g/b, hsync, vsync, de  — 8-bit RGB scanout (PCRTC active region)
//   core_halt           — high once SYSCALL halts the EE
//   dma_done_seen       — sticky: high once DMAC channel-2 fires its DONE event
//   frame_seen          — sticky: high once one full PCRTC frame end-of-frame fires
//
// Parameters:
//   H_ACTIVE / V_ACTIVE — PCRTC active region (defaults to the Ch123 16×8)
//   BIOS_SIZE_BYTES     — bios_rom_stub size (default 4 KiB)
//   RAM_SIZE_BYTES      — ee_ram_stub size (default 4 KiB)
//   VRAM_BYTES          — vram_stub size (default 8 KiB)
//   USEG_SHADOW_WORDS_PARAM — Ch145 useg-shadow size (default 1024 = 4 KiB)
//
// Macros (NOT parameters — iverilog-12 string-parameter forwarding
// limitation forced them to be macros; see the `\`define` block
// below the `timescale directive):
//   TOP_PSMCT32_RASTER_DEMO_BIOS_IMAGE_FILE     — path to bios.mem
//                                                 (one 32-bit hex word/line)
//   TOP_PSMCT32_RASTER_DEMO_PAYLOAD_IMAGE_FILE  — path to payload.mem
//                                                 (one 128-bit hex qword/line)
// Both default to "" so the wrapper is still elaborable without
// fixtures (synthetic NOP-sled in bios_rom_stub + zero-init
// ee_ram_stub, which produces no DMAC payload but a stable PCRTC
// frame). On synthesis these become FPGA-tool defines.
//
// PASS for the integration TB (`tb_top_psmct32_raster_demo_bram`):
//   - all 128 PSMCT32 pixel words at canonical swizzled byte
//     addresses match expected ABGR via hierarchical probe of
//     `dut.u_vram.mem[byte_addr >> 2]` (Phase 1, Ch155)
//   - core_halt + dma_done_seen + frame_seen latched after the
//     EE bootlet SYSCALLs and the DMAC drains
//   - one full PCRTC frame captured and per-pixel verified
//     against the rasterized image (Phase 2, Ch158)

`timescale 1ns/1ps

// BIOS / payload image paths are passed via macros (iverilog-12
// limitation: string parameter forwarding through hierarchy
// elaborates inconsistently). On synthesis the same macros become
// FPGA-tool defines pointing at .mem fixtures or board-specific
// files. The macros default to empty strings (synthetic NOP-sled +
// zero-RAM fallback in bios_rom_stub / ee_ram_stub) so the wrapper
// is still elaborable without bake artifacts present.
`ifndef TOP_PSMCT32_RASTER_DEMO_BIOS_IMAGE_FILE
  `define TOP_PSMCT32_RASTER_DEMO_BIOS_IMAGE_FILE ""
`endif
`ifndef TOP_PSMCT32_RASTER_DEMO_PAYLOAD_IMAGE_FILE
  `define TOP_PSMCT32_RASTER_DEMO_PAYLOAD_IMAGE_FILE ""
`endif

module top_psmct32_raster_demo_bram
    import trace_pkg::*;
#(
    parameter int     H_ACTIVE               = 16,
    parameter int     V_ACTIVE               = 8,
    // Ch169 — expose PCRTC blanking parameters so the board wrapper can
    // override them to a standard HDMI mode (e.g. VGA 640x480@60). The
    // defaults (1-pixel borders) preserve the Ch123 tiny-frame behavior
    // used by every existing sim TB.
    parameter int     H_FRONT                = 1,
    parameter int     H_SYNC                 = 1,
    parameter int     H_BACK                 = 1,
    parameter int     V_FRONT                = 1,
    parameter int     V_SYNC                 = 1,
    parameter int     V_BACK                 = 1,
    parameter int     BIOS_SIZE_BYTES        = 4 * 1024,
    parameter int     RAM_SIZE_BYTES         = 4 * 1024,
    parameter int     VRAM_BYTES             = 8 * 1024,
    // Ch251.4 — VRAM second-read-port enable. Default = 1 keeps every
    // simulation TB byte-identical (PSMT4 RMW path is live). Hardware
    // build overrides to 0 to halve VRAM's M20K footprint by avoiding
    // the 1W+2R → 2×(1W+1R) replication. See vram_bram_stub for the
    // full contract.
    parameter bit     VRAM_ENABLE_READ2      = 1'b1,
    parameter int unsigned USEG_SHADOW_WORDS_PARAM = 1024,
    // Ch296 — useg-shadow backing enable, threaded down to
    // ee_memory_map_stub. Default 1 keeps every sim TB byte-identical
    // (useg shadow live). The board build overrides to 0 to remove the
    // ~33k-FF useg_shadow_mem array; the PSMCT32 SPRITE-only bootlet
    // runs from BIOS + EE-RAM and issues no useg traffic, so the shadow
    // is dead on the board path. See ee_memory_map_stub.USEG_SHADOW_ENABLE.
    parameter bit     USEG_SHADOW_ENABLE     = 1'b1,
    // Ch162 — passes through to `ee_core_stub.STRIP_HW_DIVIDER`. Set
    // to 1 on hardware builds (the PSMCT32 SPRITE-only bootlet
    // doesn't execute DIVU) so Quartus doesn't infer the 32-bit
    // hardware divider and can close timing on a faster clock.
    // Default 0 keeps every existing sim TB unchanged.
    parameter bit STRIP_HW_DIVIDER = 1'b0,
    // Ch163 — passes through to `gs_pcrtc_stub.STRIP_PCRTC_MAG_DIV`.
    // Hardware builds set this to 1 (the demo locks MAGH=MAGV=0 so
    // the divisor is constant 1 and the math collapses to a
    // passthrough); Quartus then can't infer the PCRTC magnification
    // divider, retiring the Ch162-onwards STA worst path. Default 0
    // keeps every existing scanout MAG TB unchanged.
    parameter bit STRIP_PCRTC_MAG_DIV = 1'b0,

    // Ch295 — PSMCT32 page/block swizzle gate, mirroring the
    // vram_stub variant (top_psmct32_raster_demo). Default 1'b1
    // preserves the Ch123/Ch251 swizzled raster+scanout behavior and
    // every existing TB that drives this BRAM top (the flat
    // production demo). A TEXTURED-sprite fixture sets this to 0 so
    // the linear gs_texel_addr fetch and the BITBLT texture upload
    // land in the SAME (linear) VRAM layout — the v1 textured-path
    // scope. Forwarded to gs_stub / gif_image_xfer_stub / gs_pcrtc_stub
    // together so all three VRAM views stay consistent.
    parameter bit     PSMCT32_SWIZZLE        = 1'b1,
    // Ch298 — SWIZZLED PSMT4 texture path. When 1, the PSMT4 texture UPLOAD
    // (gif_image_xfer_stub) writes the real PS2 block layout AND the texture
    // SAMPLER (gs_stub -> gs_texture_unit) reads it back swizzled, so the two
    // VRAM views are consistent. Default 0 keeps every linear PSMT4/PSMT8/
    // PSMCT32 demo + TB byte-identical; the swizzle demo sets it to 1.
    parameter bit     PSMT4_SWIZZLE          = 1'b0,
    // Ch299 — SWIZZLED PSMT8 texture path. The sibling of PSMT4_SWIZZLE,
    // MINUS the nibble (PSMT8 is 1 byte/texel). When 1, the PSMT8 texture
    // UPLOAD (gif_image_xfer_stub) writes the real PS2 block layout AND the
    // texture SAMPLER (gs_stub -> gs_texture_unit) reads it back swizzled, so
    // the two VRAM views are consistent. The framebuffer SCANOUT stays linear
    // PSMCT32 (gs_pcrtc PSMT8_SWIZZLE untouched). Default 0 keeps every linear
    // PSMT8/PSMT4/PSMCT32 demo + TB byte-identical; the swizzle demo sets it 1.
    parameter bit     PSMT8_SWIZZLE          = 1'b0,
    // Ch301 — PERSPECTIVE-CORRECT textured triangles (forwarded to gs_stub).
    // When 1, a TME TRIANGLE supplied via ST (S=u/w,T=v/w) + RGBAQ.Q (=1/w)
    // is interpolated perspective-correctly via the pipelined reciprocal LUT.
    // Default 0 generate-guards all perspective logic out (zero cost); only the
    // GS_PERSP_DEMO board profile sets it 1.
    parameter bit     PERSPECTIVE_CORRECT    = 1'b0,
    // GS per-vertex FOG compile gate, forwarded to gs_stub. Default 1 (fog
    // active). Board profiles whose scene fog is a functional no-op set 0 to
    // prune the fog cone off the texture->color critical path.
    parameter bit     FOG_ENABLE             = 1'b1,
    parameter bit     SUBPIXEL_XY            = 1'b0,
    parameter int     PERSP_RECIP_IDX_BITS   = 8,     // Ch351 — perspective reciprocal LUT width (far-W -> 11)
    parameter int     GRAD_DIV_CYCLES        = 1,     // Ch352 — triangle-setup divide settle cycles (board fits -> 4)
    parameter bit     GRAD_SEQ_DIVIDER       = 1'b0,  // Ch352 — sequential gradient divider (board fits -> 1)
    // Ch344 — TEXTURED + source-over ALPHA SPRITE path (forwarded to gs_stub). Default 0 -> byte-identical.
    parameter bit     SPRITE_TEX_ALPHA       = 1'b0,
    parameter bit     SPRITE_TEX_ALPHA_CLUT  = 1'b0,   // Ch408 — admit PSMT8/PSMT4 CLUT textures into alpha sprites
    parameter bit     CLUT_CSM1_ENABLE       = 1'b0,   // Ch350 — CSM1 16x16 CT32 grid CLUT load (SH3 indexed env path)
    // Ch302 — COMBINED textured+alpha+depth probe (forwarded to gs_stub). When 1,
    // a TME+ABE+ZTE triangle runs the multi-beat per-pixel FSM (Zread->Ztest->
    // texel->dest->colorwrite->Zwrite). Default 0 generate-guards it out (every
    // existing demo byte-identical); only the GS_COMBINED_DEMO board profile sets 1.
    parameter bit     COMBINED_TAZ           = 1'b0,
    // Ch303 — TILE-LOCAL render mode (forwarded to gs_stub). When 1, a combined
    // TME+ABE+ZTE triangle renders into an on-chip 16x16 color+Z tile
    // (CLEAR->RENDER->FLUSH); texture still from VRAM, only color/Z move on-chip.
    // Default 0 generate-guards it out (every existing demo byte-identical); only
    // the GS_TILE_DEMO board profile sets 1 (implies COMBINED_TAZ=1).
    parameter bit     TILE_LOCAL             = 1'b0,
    // Ch304 — tile GRID dimensions (forwarded to gs_stub). Default 1x1 = the
    // Ch303 single tile (byte-identical); GS_TILE2X2_DEMO sets 2x2 to render one
    // primitive across a 2x2 grid of 16x16 tiles (per-tile clear/render/flush).
    parameter int     TILE_COLS              = 1,
    parameter int     TILE_ROWS              = 1,
    // Ch305 — MULTI-PRIMITIVE tiled scene. When TILE_MULTIPRIM=1 the tile grid
    // re-renders a LIST of TILE_PRIM_COUNT primitives (all buffered in the FIFO)
    // per tile, in order, so later primitives depth-test/alpha-blend over earlier
    // ones within each tile. Default 0 = the Ch304 single-primitive grid
    // (byte-identical); GS_TILE_MULTIPRIM_DEMO sets 1 + the batch size.
    parameter bit     TILE_MULTIPRIM         = 1'b0,
    parameter int     TILE_PRIM_COUNT        = 1,
    // Ch329 — the Ch255 heartbeat read-splicer forcibly patches EE-RAM qword 115's low 32
    // bits (0x730) with hb_rgbaq on every DMAC read, for the input-driven heartbeat demo.
    // That CORRUPTS any GIF payload that happens to occupy qword 115 (e.g. a depth-64 multi-
    // prim batch whose 17th prim's first XYZ2 lands there → vertex reads 0xFFF/0xFF0). Gate it
    // OFF for non-heartbeat profiles (GS tile/capacity tests). Default ON = byte-identical.
    parameter bit     HEARTBEAT_SPLICE_ENABLE = 1'b1,
    // Ch330 — runtime primitive-list feeder. When 1, gs_prim_list_feeder becomes the EXCLUSIVE
    // owner of the gif_reg_* stream into gs_stub (the GIF unpacker is muxed out), expanding a
    // normalized combined-TAZ list from staging instead of the baked DMA payload. Default 0 =
    // dead logic, byte-identical for every existing profile.
    parameter bit     FEEDER_ENABLE          = 1'b0,
    parameter int     FEEDER_STG_WORDS       = 256,
    // Ch353 — auto-start the FIRST feeder render at C_SETUP (default, byte-identical). Set 0 ONLY for
    // the GS_SH3_LPDDR_FB profile: the disarmed boot render would otherwise leave a pending EOF that
    // falsely asserts frame_drained the instant the host later arms the writer. With AUTOSTART=0,
    // C_SETUP transitions straight to C_READY (no render) and every render waits for an explicit GO.
    parameter bit     FEEDER_AUTOSTART       = 1'b1,
    // Ch315 — primitive FIFO / per-tile bin depth (capacity). Power-of-2, default
    // 4 = byte-identical. GS_TILE_CAP_DEMO sets 8 to scale capacity past the old 4.
    parameter int     TILE_FIFO_DEPTH        = 4,
    // Ch317 — LPDDR-backed framebuffer (tile-flush only). When 1, the PSMCT16 tile
    // FLUSH stream is ALSO committed to an LPDDR-style framebuffer model
    // (gs_lpddr_fb_writer) — a transitional additive MIRROR alongside the on-chip
    // BRAM FB, so the LPDDR write path (linear address, per-row bursts, 4 KiB cap)
    // can be proven by readback while scanout still comes from BRAM. Tile color/Z
    // and texture stay on-chip. Default 0 → the writer is inert (byte-identical).
    parameter bit     LPDDR_FB_ENABLE        = 1'b0,
    parameter int     LPDDR_FB_BYTES         = 8192,    // 64x64 PSMCT16 = 8 KiB
    // Ch306 — GS SCISSOR_1 rectangular clipping baked into the tile walker bounds
    // (effective bounds = primitive bbox ∩ tile bbox ∩ scissor rect). Default 0 =
    // no scissor (byte-identical); GS_TILE_SCISSOR_DEMO sets 1.
    parameter bit     SCISSOR_ENABLE         = 1'b0,
    // Ch307 — GS texture WRAP MODES (CLAMP_1 WMS/WMT: REPEAT/CLAMP) applied in the
    // sampler before texel-address gen. Default 0 = pass-through (byte-identical);
    // GS_TILE_WRAP_DEMO sets 1.
    parameter bit     TEX_WRAP_ENABLE        = 1'b0,
    // Ch308 — on-chip tile COLOR buffer stored as PSMCT16 (RGB5A1, 16-bit) instead
    // of PSMCT32 (32-bit): halves the tile color RAM + flushes a PSMCT16 framebuffer.
    // Default 0 = PSMCT32 (byte-identical); GS_TILE_PSMCT16_DEMO sets 1.
    parameter bit     TILE_COLOR_PSMCT16     = 1'b0,
    // Ch309 — generic GS ALPHA blend modes (Cv=((A-B)*C>>7)+D selectors + FIX) per
    // primitive, instead of only source-over. Default 0 = source-over (byte-identical);
    // GS_TILE_ALPHA_DEMO sets 1.
    parameter bit     ALPHA_MODES_ENABLE     = 1'b0,
    // Ch310 — 4-tap BILINEAR texture filtering (PSMCT32) in the combined tile path,
    // per-primitive via TEX1.MMAG. Default 0 = nearest (byte-identical);
    // GS_TILE_BILINEAR_DEMO sets 1.
    parameter bit     BILINEAR_ENABLE        = 1'b0,
    // Ch314 — bilinear for PALETTIZED (PSMT8/PSMT4) textures in the combined
    // path (CLUT-before-interp). Default 0 = byte-identical; GS_TILE_PALBILINEAR_DEMO sets 1.
    parameter bit     PALETTE_BILINEAR       = 1'b0,
    // Ch311 — real per-tile BIN BUFFER: precompute per-tile primitive lists in a
    // binning pass, render walks each tile's bin (vs re-testing all prims per tile).
    // Default 0 = Ch305 re-test path (byte-identical); GS_TILE_BIN_DEMO sets 1.
    parameter bit     BIN_BUFFER_ENABLE      = 1'b0,
    // Ch323 — tile COLOR+Z spill/reload to LPDDR (forwarded to gs_stub). Default 0 →
    // byte-identical (no RELOAD/ZFLUSH phases, no Z-flush stream). The GS_TILE_SPILL_DEMO
    // board/e2e profile sets 1.
    parameter bit     TILE_SPILL_ENABLE      = 1'b0,
    parameter bit     SPILL_FORCE_VALID      = 1'b0,   // Ch323 test hook (negative bootstrap test)
    // Ch326 — LPDDR-ONLY framebuffer: when 1, the tile color flush does NOT mirror into the
    // on-chip vram_stub FB (it only spills to LPDDR via the dedicated color channel), so the
    // 64 KiB BRAM FB mirror is reclaimed and the displayed FB is the EXTERNAL LPDDR one. Only
    // valid with TILE_SPILL_ENABLE + LPDDR scanout (no BRAM-scanout fallback). vram_stub then
    // holds only the texture (place it low, e.g. TBP0=0, and shrink VRAM_BYTES accordingly).
    parameter bit     FB_LPDDR_ONLY          = 1'b0,
    // Ch322 — LPDDR-backed texture (PREFILLED cache). When 1, the texel-fetch
    // read port is MUXED to an external prefilled texture cache (gs_texture_cache,
    // instantiated in the de25 top on emif_clk, filled from LPDDR4B before raster)
    // once that cache reports ready — at the SAME 1-cycle latency as the BRAM read2,
    // so the nearest-path sampler's fixed-latency contract is preserved. Default 0
    // generate-guards the mux to a constant pass-through (byte-identical); only the
    // GS_LPDDR_TEX board/e2e profile sets 1. TEX_VRAM_BASE/TEX_CACHE_BYTES bound the
    // cached VRAM byte range (reads outside it still come from BRAM).
    parameter bit     GS_LPDDR_TEX           = 1'b0,
    parameter int     TEX_VRAM_BASE          = 2048,   // TBP0*256 (tritex: TBP0=8)
    parameter int     TEX_CACHE_BYTES        = 256     // 8x8 PSMCT32
) (
    input  logic        clk,
    input  logic        rst_n,
    input  logic        core_go,

    output logic [7:0]  r,
    output logic [7:0]  g,
    output logic [7:0]  b,
    output logic        hsync,
    output logic        vsync,
    output logic        de,
    // Ch320 — PCRTC scanout VRAM byte address, for the LPDDR4B scanout reader.
    // LPDDR mirrors BRAM VRAM byte-for-byte, so indexing the LPDDR frame cache
    // by this address yields the identical pixel (seamless video-source mux).
    output logic [31:0] vram_read_addr_o,
    // Ch320 — high when the scanout pixel is inside the displayed frame window
    // (for gating an external LPDDR4B scanout so it shows one frame, not a tiled fill).
    output logic        pix_window_o,

    output logic        core_halt,
    output logic        dma_done_seen,
    output logic        frame_seen,
    // Ch173 — pass the gs_stub raster_overflow flag out so the
    // board wrapper can route it into the HPS bridge's
    // RASTER_OVERFLOW_COUNT register. Under Ch172 backpressure
    // this should stay LOW forever; non-zero on hardware reads
    // means the backpressure path broke somewhere.
    output logic        raster_overflow,
    // Ch174 — event toggles (not raw pulses). These flip on every
    // PCRTC end-of-frame / DMAC done pulse in the design clock
    // domain. The bridge 2-FF syncs the toggle, XORs against its
    // last sample, and increments the matching counter on each
    // detected edge. This is the textbook pulse-CDC primitive —
    // the toggle stays at its new value until the next event
    // (~16.7 ms for frames), so the synchronizer has megacycles
    // of slack and cannot miss an event. Do NOT "simplify" this
    // back into raw 1-cycle pulses crossing CLOCK2_50: a 25 MHz
    // pulse is borderline against a 50 MHz 2-FF sync.
    output logic        frame_toggle,
    output logic        dma_done_toggle,

    // Ch255 — heartbeat color override from the controller. Tied
    // straight to INPUT_P1_RAW[9] (Sony ○ / JOY_A) and INPUT_P1_RAW[7]
    // (Sony × / JOY_B) at the board top. While the demo's normal EE-
    // driven cyan↔red toggle keeps running in the background, holding
    // a face button overrides the heartbeat color that the splicer
    // injects into the DMAC read response on the NEXT drain:
    //
    //   joy_a_pressed_i alone : force RED   (0xFF0000FF)
    //   joy_b_pressed_i alone : force CYAN  (0xFFFFFF00)
    //   both pressed          : invert the current EE value
    //                           (XOR with 0x00FFFFFF, swaps cyan↔red)
    //   neither pressed       : EE's hb_rgbaq_reg passes through
    //
    // Response latency is one DMAC drain cycle (~2 s at Ch254 cadence)
    // since the GS only repaints the heartbeat sprite once per drain.
    // Sim TBs default these to 0 to keep regression byte-identical
    // (tb_ch171 exercises the four combinations explicitly).
    input  logic        joy_a_pressed_i,
    input  logic        joy_b_pressed_i,

    // Ch318 — PSMCT16 tile-FLUSH stream exposed for an external LPDDR AXI writer
    // (gs_lpddr_axi_master, instantiated in the de25 top). These mirror the internal
    // flush emit; unused (left open) by every existing consumer. flush_psm lets the
    // writer gate on PSMCT16 (0x02).
    output logic        flush_emit_o,
    output logic [31:0] flush_addr_o,     // linear FB byte address
    output logic [15:0] flush_pix16_o,
    output logic [31:0] flush_color32_o,  // Ch323 — full 32-bit flushed color (for spill round-trip capture)
    output logic [3:0]  flush_be_o,       // FRAME.FBMSK-compressed byte enables
    output logic [5:0]  flush_psm_o,
    output logic [11:0] flush_x_o,        // Ch357 — fragment x/y/z for the external LPDDR-Z ROP (perspective path)
    output logic [11:0] flush_y_o,
    output logic [31:0] flush_z_o,
    output logic        flush_abe_o,
    output logic [16:0] flush_alpha_o,
    output logic        flush_ztest_o,
    output logic [1:0]  flush_ztst_o,
    output logic        flush_zmsk_o,
    // Ch323 — tile Z-FLUSH stream (TILE_SPILL_ENABLE; the de25 routes it to an LPDDR Z-backing
    // writer). z_flush_addr_o is Z-backing-relative (pixel_index*4, 32-bit Z). Inert/0 unless
    // spilling; unused (left open) by every existing consumer.
    output logic        z_flush_emit_o,
    output logic [31:0] z_flush_addr_o,
    output logic [31:0] z_flush_data_o,
    // Ch323 — DEDICATED color-flush spill stream (TP_FLUSH only; the de25 feeds the color writer
    // from THIS, not the generic flush_emit_o which also carries RENDER-phase raster emits).
    output logic        tile_color_flush_emit_o,
    output logic [31:0] tile_color_flush_addr_o,
    output logic [31:0] tile_color_flush_data_o,
    // Ch323 — tile RELOAD staging interface (TILE_SPILL_ENABLE; de25 attaches gs_tile_reload).
    // reload_start_o arms the staging fill; tile_reload_raddr_o sweeps tile indices; the engine
    // returns color/Z (1-cyc) on tile_reload_color_i/z_i with tile_reload_ready_i = warm.
    output logic        reload_start_o,
    output logic [7:0]  tile_reload_raddr_o,
    output logic [29:0] reload_base_o,       // Ch324 — current tile's raster-FB byte offset
    input  logic        tile_reload_ready_i,
    input  logic [31:0] tile_reload_color_i,
    input  logic [31:0] tile_reload_z_i,
    output logic [2:0]  tile_phase_o,        // Ch323 diag — current tile phase (de25 event counters)

    // Ch322 — texel-fetch request exposed for an external prefilled texture cache
    // (gs_texture_cache in the de25 top). The cache returns the texel on
    // tex_cache_data_i (1-cycle registered, matching read2) and asserts
    // tex_cache_ready_i once warm. Unused (left open / tied 0) by every existing
    // consumer; only meaningful under GS_LPDDR_TEX=1.
    output logic        gs_tex_rd_en_o,
    output logic [31:0] gs_tex_rd_addr_o,
    input  logic [31:0] tex_cache_data_i,
    input  logic        tex_cache_ready_i,
    // Ch322 — texel-source proof counters (design_clk; reset with the core/rst_n, so each
    // render's counts are fresh). cache_hits = texel reads served from the LPDDR cache;
    // bram_hits = texel reads served from BRAM (fallback). After a render, cache_hits>0 is
    // the bridge-visible proof the triangle's texels came from LPDDR. Tied 0 unless GS_LPDDR_TEX.
    output logic [31:0] tex_cache_hits_o,
    output logic [31:0] tex_bram_hits_o,

    // Ch330 Brick 4 — HPS/bridge runtime command-list feeder interface. The bridge
    // writes the staging RAM one 64-bit record word at a time ({we,waddr,wdata}) and
    // pulses feeder_go to retrigger; the feeder exposes ready + the records/wait
    // counters back. Tied off / left open by non-feeder profiles (FEEDER_ENABLE=0 ->
    // g_no_feeder doesn't use them), so default builds are byte-identical.
    input  logic        feeder_stg_we_i,
    input  logic [11:0] feeder_stg_waddr_i,
    input  logic [63:0] feeder_stg_wdata_i,
    input  logic        feeder_go_i,           // retrigger pulse (honoured only in C_READY)
    output logic        feeder_ready_o,         // control FSM is in C_READY (a new list may start)
    output logic [15:0] feeder_records_o,       // primitives emitted by the current list
    output logic [31:0] feeder_waits_o,         // cycles the feeder paused under fifo_full

    // Ch367 -- runtime palette residency. The design-clock staging copier
    // owns this writer while busy; the legacy TEX0/VRAM loader remains the
    // sole writer at every other time.
    input  logic        runtime_clut_wr_en_i,
    input  logic [7:0]  runtime_clut_wr_idx_i,
    input  logic [31:0] runtime_clut_wr_data_i,
    input  logic        runtime_clut_busy_i
);

    localparam int RAM_ADDR_W  = $clog2(RAM_SIZE_BYTES);
    localparam int BIOS_ADDR_W = $clog2(BIOS_SIZE_BYTES);

    // ---------------------------------------------------------------------
    // ee_ram_stub — DMAC-side GIF payload
    // ---------------------------------------------------------------------
    logic                  ram_rd_en;
    logic [RAM_ADDR_W-1:0] ram_rd_addr;
    logic [127:0]          ram_rd_data;
    logic                  ram_rd_valid;
    // Top has no TB-direct write path; ee_ram_stub's wr_* are tied
    // off and ee_memory_map_stub's ram_wr_* outputs are unconnected
    // (Ch251 fitter-rescue — see comment on u_ram). Heartbeat
    // snoop lives below and uses the EE core's already-routed
    // map_wr_* wires (`ee_cpu_wr_*`) instead.
    logic [7:0]            ram_master_id;
    assign ram_master_id = ram_rd_en ? 8'd1 : 8'd0;

    // Ch251 fitter-rescue: keep ee_ram_stub.wr_* tied off so the
    // synthesizer keeps inferring it as a ROM (the pre-Ch251 fit
    // that the Agilex 5 M20K budget had headroom for). Enabling the
    // live write port — the natural way to let the bootlet update
    // the heartbeat RGBAQ qword — turned the 128-bit-wide × 16-byte-
    // enable backing into a heavy distributed memory + per-byte M20K
    // split, blowing the budget by ~160 blocks even with explicit
    // `ramstyle = "M20K"`. The Ch251.3 "patch register + DMAC-read
    // splicer" below sidesteps that: the EE's SW to 0x8000_0730
    // captures into a 32-bit register; when the DMAC fetches qword
    // 115 for the heartbeat SPRITE, the register's value is muxed
    // into the low 32 bits of the read response. ee_ram_stub stays
    // a cheap ROM, the bootlet still gets the live RGBAQ update.
    ee_ram_stub #(
        .SIZE_BYTES(RAM_SIZE_BYTES),
        .IMAGE_FILE(`TOP_PSMCT32_RASTER_DEMO_PAYLOAD_IMAGE_FILE)
    ) u_ram (
        .clk(clk), .rst_n(rst_n),
        .rd_en(ram_rd_en), .rd_addr(ram_rd_addr),
        .rd_data(ram_rd_data), .rd_valid(ram_rd_valid),
        .wr_en(1'b0), .wr_addr('0), .wr_data(128'd0), .wr_be(16'd0),
        .master_id(ram_master_id),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    // ---------------------------------------------------------------------
    // Ch251.3 — Heartbeat RGBAQ patch register (M20K-budget-safe).
    //
    // The Ch251 animated bootlet does `SW r5, 0(r3)` with r3 =
    // 0x8000_0730 to alternate the heartbeat SPRITE's RGBAQ between
    // CYAN (0xFFFFFF00) and RED (0xFF0000FF). 0x8000_0730 maps to
    // ee_ram_stub byte offset 0x730 = qword 115 byte 0, the low
    // 32 bits of the 17th SPRITE's RGBAQ A+D packet.
    //
    // **M20K-budget-safe capture path.** We snoop directly from
    // `ee_cpu_wr_*` (the EE core's output, already routed into
    // ee_memory_map_stub via the wires above) rather than from
    // ee_memory_map_stub's `ram_wr_*` output bundle. Consuming the
    // latter ran the design past the Agilex 5's 358-M20K budget
    // even though the bundle is purely combinational — Quartus's
    // inference around the 128-bit lane-expansion + bridge-mux
    // path was substantially heavier than expected. ee_cpu_wr_*
    // was already a live consumer of those signals (going into
    // ee_memory_map_stub.ee_wr_*) so adding a parallel snoop is
    // free.
    //
    // The address-decode below recognizes the heartbeat as any SW
    // (full-word BE) whose physical address (low 29 bits, kseg0
    // bit stripped) is exactly 0x730. The bootlet uses kseg0
    // (0x8000_0730) but useg (0x0000_0730) would map to the same
    // RAM location too, so we mask off the segment bit.
    //
    // Splice path: ee_ram_stub.rd_data has a 1-cycle latency. We
    // register the "is-heartbeat-qword" decoded read address one
    // cycle so the splice mux fires on the same cycle that
    // ram_rd_data presents qword 115.
    // ---------------------------------------------------------------------
    localparam logic [RAM_ADDR_W-5:0] HEARTBEAT_QW_INDEX =
        (25'h0000_0730) >> 4;   // = qword 115

    logic [31:0] hb_rgbaq_reg;
    wire hb_write_hit = ee_cpu_wr_en
                        && (ee_cpu_wr_addr[28:0] == 29'h0000_0730)
                        && (ee_cpu_wr_be == 4'b1111);
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            // Match bake.py's initial heartbeat RGBAQ (CYAN). The
            // first DMAC kick paints this without needing the patch
            // (ee_ram_stub's $readmemh already has CYAN at qword
            // 115); matching just avoids any reset-window artifact.
            hb_rgbaq_reg <= 32'hFFFF_FF00;
        end else if (hb_write_hit) begin
            hb_rgbaq_reg <= ee_cpu_wr_data;
        end
    end

    // Ch255 — controller-driven override layer ahead of the splicer.
    // The EE keeps animating hb_rgbaq_reg in the background; this mux
    // only changes what the splicer INJECTS into the DMAC read response
    // when the GS is repainting the heartbeat sprite. Priority order:
    //
    //   both buttons : invert EE's current value (XOR 0x00FFFFFF)
    //   A only       : force RED   (0xFF0000FF)
    //   B only       : force CYAN  (0xFFFFFF00)
    //   neither      : hb_rgbaq_reg (EE pass-through)
    //
    // Pure combinational — joy_*_pressed_i comes from the bridge
    // input_p1_raw_o (already design_clk-synced) and is stable for
    // millions of cycles relative to a human button press. The next
    // DMAC drain captures whatever value this mux outputs.
    localparam logic [31:0] HB_OVERRIDE_RED  = 32'hFF00_00FF;
    localparam logic [31:0] HB_OVERRIDE_CYAN = 32'hFFFF_FF00;
    localparam logic [31:0] HB_OVERRIDE_XOR  = 32'h00FF_FFFF;
    wire [31:0] hb_rgbaq_effective =
        (joy_a_pressed_i && joy_b_pressed_i) ? (hb_rgbaq_reg ^ HB_OVERRIDE_XOR) :
        joy_a_pressed_i                      ? HB_OVERRIDE_RED                  :
        joy_b_pressed_i                      ? HB_OVERRIDE_CYAN                 :
                                               hb_rgbaq_reg;

    // Read splicer: delay the "qword 115" detection by 1 cycle to
    // align with ram_rd_data (registered output of ee_ram_stub).
    logic hb_read_hit_d;
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) hb_read_hit_d <= 1'b0;
        else        hb_read_hit_d <= HEARTBEAT_SPLICE_ENABLE   // Ch329 — off for non-heartbeat profiles
                                     && ram_rd_en
                                     && (ram_rd_addr[RAM_ADDR_W-1:4] == HEARTBEAT_QW_INDEX);
    end
    wire [127:0] ram_rd_data_patched = hb_read_hit_d
                                       ? {ram_rd_data[127:32], hb_rgbaq_effective}
                                       : ram_rd_data;

    // ---------------------------------------------------------------------
    // bios_rom_stub — EE bootlet at 0xBFC0_0000
    // ---------------------------------------------------------------------
    logic                   bios_rd_en;
    logic [21:0]            bios_rd_addr_full;
    logic [BIOS_ADDR_W-1:0] bios_rd_addr;
    logic                   bios_rd_valid;
    logic [31:0]            bios_rd_data;
    assign bios_rd_addr = bios_rd_addr_full[BIOS_ADDR_W-1:0];

    bios_rom_stub #(
        .SIZE_BYTES(BIOS_SIZE_BYTES),
        .IMAGE_FILE(`TOP_PSMCT32_RASTER_DEMO_BIOS_IMAGE_FILE)
    ) u_bios (
        .clk(clk), .rst_n(rst_n),
        .rd_en(bios_rd_en),
        .rd_addr(bios_rd_addr),
        .rd_data(bios_rd_data),
        .rd_valid(bios_rd_valid),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    // ---------------------------------------------------------------------
    // dmac_reg_stub — channel-2 NORMAL transfer
    // ---------------------------------------------------------------------
    logic        dmac_reg_wr_en;
    logic [7:0]  dmac_reg_offset;
    logic [31:0] dmac_reg_wr_data;
    logic        dmac_mem_rd_en;
    logic [31:0] dmac_mem_rd_addr;
    logic         dmac_gif_valid;
    logic [127:0] dmac_gif_data;
    logic         dmac_gif_last;
    logic         dmac_gif_ready;

    logic                dmac_ev_valid;
    subsys_e             dmac_ev_subsys;
    event_e              dmac_ev_event;

    logic [127:0] map_to_dmac_rd_data;
    logic         map_to_dmac_rd_valid;

    dmac_reg_stub u_dmac (
        .clk(clk), .rst_n(rst_n),
        .reg_wr_en(dmac_reg_wr_en), .reg_offset(dmac_reg_offset),
        .reg_wr_data(dmac_reg_wr_data),
        .reg_rd_en(1'b0), .reg_rd_data(), .reg_rd_valid(),
        .mem_rd_en(dmac_mem_rd_en), .mem_rd_addr(dmac_mem_rd_addr),
        .mem_rd_data(map_to_dmac_rd_data), .mem_rd_valid(map_to_dmac_rd_valid),
        .ep_valid(dmac_gif_valid), .ep_data(dmac_gif_data),
        .ep_last(dmac_gif_last), .ep_ready(dmac_gif_ready),
        .irq_completion_o(),
        .ev_valid(dmac_ev_valid), .ev_subsys(dmac_ev_subsys),
        .ev_event(dmac_ev_event),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    // ---------------------------------------------------------------------
    // ee_memory_map_stub — bus arbiter (USEG_SHADOW shrunk per Ch145)
    // ---------------------------------------------------------------------
    logic        ee_cpu_rd_en;
    logic [31:0] ee_cpu_rd_addr;
    logic [31:0] ee_cpu_rd_data;
    logic        ee_cpu_rd_valid;
    logic        ee_cpu_wr_en;
    logic [31:0] ee_cpu_wr_addr;
    logic [31:0] ee_cpu_wr_data;
    logic [3:0]  ee_cpu_wr_be;

    logic        map_gs_priv_wr_en;
    logic [15:0] map_gs_priv_wr_addr;
    logic [31:0] map_gs_priv_wr_data;
    logic [3:0]  map_gs_priv_wr_be;

    logic         map_ram_rd_en;
    logic [24:0]  map_ram_rd_addr;

    ee_memory_map_stub #(
        .USEG_SHADOW_WORDS_PARAM(USEG_SHADOW_WORDS_PARAM),
        .USEG_SHADOW_ENABLE(USEG_SHADOW_ENABLE)
    ) u_map (
        .clk(clk), .rst_n(rst_n),
        .ee_rd_en  (ee_cpu_rd_en),
        .ee_rd_addr(ee_cpu_rd_addr),
        .ee_rd_data(ee_cpu_rd_data),
        .ee_rd_valid(ee_cpu_rd_valid),
        .ee_wr_en  (ee_cpu_wr_en),
        .ee_wr_addr(ee_cpu_wr_addr),
        .ee_wr_data(ee_cpu_wr_data),
        .ee_wr_be  (ee_cpu_wr_be),
        .dmac_rd_en(dmac_mem_rd_en), .dmac_rd_addr(dmac_mem_rd_addr),
        .dmac_rd_data(map_to_dmac_rd_data),
        .dmac_rd_valid(map_to_dmac_rd_valid),
        .bios_rd_en  (bios_rd_en),
        .bios_rd_addr(bios_rd_addr_full),
        .bios_rd_data(bios_rd_data),
        .bios_rd_valid(bios_rd_valid),
        .ram_rd_en(map_ram_rd_en), .ram_rd_addr(map_ram_rd_addr),
        .ram_rd_data(ram_rd_data_patched), .ram_rd_valid(ram_rd_valid),
        .bridge_wr_en(1'b0), .bridge_wr_addr(32'd0),
        .bridge_wr_data(128'd0), .bridge_wr_be(16'd0),
        .bridge_master_id(8'd0),
        // Ch251 fitter-rescue: leave ee_memory_map_stub's ram_wr_*
        // outputs unconnected at the wrapper. Consuming them forced
        // Quartus to materialize the 128-bit lane-expansion + bridge-
        // mux logic that lives behind those outputs — combinational
        // alone, but the M20K usage cascaded past the 358-block
        // Agilex 5 budget when the wide data paths got wired up. The
        // heartbeat-patch register below snoops the EE's SW directly
        // from `ee_cpu_wr_*` (already-existing consumers) instead.
        .ram_wr_en(), .ram_wr_addr(), .ram_wr_data(),
        .ram_wr_be(), .ram_master_id(),
        .ee_dmac_ch2_wr_en  (dmac_reg_wr_en),
        .ee_dmac_ch2_wr_addr(dmac_reg_offset),
        .ee_dmac_ch2_wr_data(dmac_reg_wr_data),
        .ee_dmac_ch2_rd_en(), .ee_dmac_ch2_rd_addr(),
        .ee_dmac_ch2_rd_data(32'd0), .ee_dmac_ch2_rd_valid(1'b0),
        .ee_intc_wr_en(), .ee_intc_wr_addr(), .ee_intc_wr_data(),
        .ee_intc_rd_en(), .ee_intc_rd_addr(),
        .ee_intc_rd_data(32'd0), .ee_intc_rd_valid(1'b0),
        .ee_misc_mmio_wr_en(), .ee_misc_mmio_wr_addr(), .ee_misc_mmio_wr_data(), .ee_misc_mmio_wr_be(),
        .ee_misc_mmio_rd_en(), .ee_misc_mmio_rd_addr(),
        .ee_misc_mmio_rd_data(32'd0), .ee_misc_mmio_rd_valid(1'b0),
        .ee_biu_wr_en(), .ee_biu_wr_addr(), .ee_biu_wr_data(), .ee_biu_wr_be(),
        .ee_biu_rd_en(), .ee_biu_rd_addr(),
        .ee_biu_rd_data(32'd0), .ee_biu_rd_valid(1'b0),
        .ee_gs_priv_wr_en  (map_gs_priv_wr_en),
        .ee_gs_priv_wr_addr(map_gs_priv_wr_addr),
        .ee_gs_priv_wr_data(map_gs_priv_wr_data),
        .ee_gs_priv_wr_be  (map_gs_priv_wr_be),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    assign ram_rd_en   = map_ram_rd_en;
    assign ram_rd_addr = map_ram_rd_addr[RAM_ADDR_W-1:0];

    // ---------------------------------------------------------------------
    // ee_core_stub
    // ---------------------------------------------------------------------
    logic [31:0] core_pc;
    logic        core_trap;

    ee_core_stub #(
        .PC_RESET(32'hBFC0_0000),
        .STRICT_UNSUPPORTED(1'b0),
        .STRIP_HW_DIVIDER(STRIP_HW_DIVIDER)
    ) u_core (
        .clk(clk), .rst_n(rst_n),
        .go_i(core_go),
        .map_rd_en (ee_cpu_rd_en),
        .map_rd_addr(ee_cpu_rd_addr),
        .map_rd_data(ee_cpu_rd_data),
        .map_rd_valid(ee_cpu_rd_valid),
        .map_wr_en (ee_cpu_wr_en),
        .map_wr_addr(ee_cpu_wr_addr),
        .map_wr_data(ee_cpu_wr_data),
        .map_wr_be (ee_cpu_wr_be),
        .cpu_irq(1'b0),
        .halt_o(core_halt),
        .pc_o  (core_pc),
        .trap_o(core_trap),
        .trap_pc_o(),
        .trap_instr_o(),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    // ---------------------------------------------------------------------
    // gif_packed_stub
    // ---------------------------------------------------------------------
    logic         gif_in_ready;
    logic [7:0]   gif_gif_reg_num;
    logic         gif_gif_reg_wr_en;
    logic [63:0]  gif_gif_reg_data;
    logic         gif_image_data_valid;
    logic [127:0] gif_image_data;
    logic         gif_image_data_last;
    logic         xfer_data_ready;
    // Ch172 — raster FIFO full from gs_stub feeds gif_packed_stub's
    // backpressure input. Declared here, driven by u_gs below, and
    // consumed by u_gif above.
    logic         gs_raster_fifo_full;

    gif_packed_stub #(.REAL_AD_REG_MAP(1'b1)) u_gif (
        .clk(clk), .rst_n(rst_n),
        .in_valid(dmac_gif_valid), .in_data(dmac_gif_data),
        .in_last(dmac_gif_last), .in_ready(gif_in_ready),
        .image_data_valid(gif_image_data_valid),
        .image_data(gif_image_data),
        .image_data_last(gif_image_data_last),
        .image_data_ready(xfer_data_ready),
        .raster_fifo_full(gs_raster_fifo_full),
        .gs_wr_en(), .gs_wr_addr(), .gs_wr_data(),
        .gif_reg_wr_en(gif_gif_reg_wr_en),
        .gif_reg_num(gif_gif_reg_num),
        .gif_reg_data(gif_gif_reg_data),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    // ---------------------------------------------------------------------
    // Ch330 — runtime primitive-list feeder with a PHASE OWNER on gif_reg_*.
    // The bootlet's setup/upload/state writes (BITBLTBUF/TRXPOS/TRXREG/TRXDIR/
    // TEX0/FRAME/ALPHA/TEST/ZBUF) are latched by gs_stub from the SAME gif_reg_*
    // stream — so a blanket feeder mux from reset would mux out the texture-upload
    // arming. Instead:
    //   Phase 0 (feeder_owns_bus=0): the UNPACKER owns gif_reg_*; the bootlet does
    //     texture upload + state setup, reaching gs_stub exactly as proven.
    //   Handoff: dma_done_seen && !xfer_busy (setup + TRX/IMAGE upload complete).
    //   Phase 1 (feeder_owns_bus=1): the FEEDER owns gif_reg_* and emits the prims.
    // FEEDER_ENABLE=0 → feeder_owns_bus tied 0 → dead logic, byte-identical.
    // ---------------------------------------------------------------------
    logic        feeder_gif_reg_wr_en;
    logic [7:0]  feeder_gif_reg_num;
    logic [63:0] feeder_gif_reg_data;
    logic        feeder_owns_bus;          // drives the mux (feeder owns from the first handoff onward)
    logic        gs_raster_active;          // from gs_stub — raster pipeline active (dips between batches)
    logic        gs_scene_busy;             // Ch337 — from gs_stub — whole multi-batch scene busy (no inter-batch dip)
    logic [15:0] feeder_records;            // Ch330 observability counters
    logic [31:0] feeder_waits;
    logic        feeder_done_p;             // feeder finished emitting the current list (1-cyc)
    logic        feeder_list_flush;         // Ch331 — feeder end-of-list grid flush (delayed done)
    generate
    if (FEEDER_ENABLE) begin : g_feeder
        logic [63:0]  feeder_stg [0:FEEDER_STG_WORDS-1];   // staging list (1R1W: feeder reads, bridge writes)
`ifdef FEEDER_STG_INIT_FILE
        // Ch330 Brick 4 — board power-up list (list A): bitstream-init the staging RAM so the
        // first feeder auto-run after setup draws a real scene instead of uninitialized garbage.
        // Synth/board-only — sim TBs $readmemh the staging directly and never define this macro.
        initial $readmemh(`FEEDER_STG_INIT_FILE, feeder_stg);
`endif
        logic [11:0]  fdr_rd_addr;
        logic [63:0]  fdr_rd_data;
        // 1R1W: the feeder's sequential read + the bridge's runtime staging write.
        always_ff @(posedge clk) begin
            fdr_rd_data <= feeder_stg[fdr_rd_addr[$clog2(FEEDER_STG_WORDS)-1:0]];
            if (feeder_stg_we_i)
                feeder_stg[feeder_stg_waddr_i[$clog2(FEEDER_STG_WORDS)-1:0]] <= feeder_stg_wdata_i;
        end

        logic fdr_start, fdr_busy, fdr_done;
        logic grid_ran;      // gs_stub raster_active was high for the in-flight list
        // Control FSM enforcing Codex's "known-empty" rule: a new list never starts until the
        // previous list's grid has actually RUN and DRAINED (raster_active high->low) — so list B
        // can never append onto list A's still-resident FIFO/grid state.
        localparam logic [1:0] C_SETUP = 2'd0, C_RUN = 2'd1, C_DRAIN = 2'd2, C_READY = 2'd3;
        logic [1:0] cst;
        always_ff @(posedge clk or negedge rst_n) begin
            if (!rst_n) begin cst <= C_SETUP; fdr_start <= 1'b0; grid_ran <= 1'b0; end
            else begin
                fdr_start <= 1'b0;
                unique case (cst)
                    C_SETUP: if (dma_done_seen && !xfer_busy) begin   // bootlet setup + TRX/IMAGE done
                                 if (FEEDER_AUTOSTART) begin
                                     fdr_start <= 1'b1; grid_ran <= 1'b0; cst <= C_RUN;   // auto boot render
                                 end else begin
                                     grid_ran <= 1'b0; cst <= C_READY;   // Ch353: no boot render — await explicit GO
                                 end
                             end
                    C_RUN:   begin if (gs_scene_busy) grid_ran <= 1'b1;
                                   if (fdr_done) cst <= C_DRAIN; end
                    // Ch337 — gate on gs_scene_busy (NOT gs_raster_active): a >FIFO_DEPTH scene
                    // renders in multiple batches and raster_active DIPS between them, but the
                    // next batch's prims are still queued (gs_scene_busy stays high). Waiting on
                    // raster_active alone would reach C_READY mid-scene and let a retrigger race
                    // the last batch's render/flush. gs_scene_busy only clears once EVERY batch has
                    // rendered + flushed and the FIFO is empty.
                    C_DRAIN: begin if (gs_scene_busy) grid_ran <= 1'b1;
                                   if (grid_ran && !gs_scene_busy) cst <= C_READY; end  // whole scene drained
                    C_READY: if (feeder_go_i) begin fdr_start <= 1'b1; grid_ran <= 1'b0; cst <= C_RUN; end
                endcase
            end
        end
        assign feeder_owns_bus = (cst != C_SETUP);   // feeder owns gif_reg_* from the first handoff onward
        assign feeder_done_p   = fdr_done;
        assign feeder_ready_o  = (cst == C_READY);   // a new list may be retriggered

        // Ch331 — end-of-list grid flush: fdr_done delayed a few cycles so the LAST primitive's
        // raster-FIFO push + gradient-pending are guaranteed set before gs_stub latches the flush
        // (gs_stub's all_grad_done gate then holds the grid until every prim is graded). The
        // delay (vs a same-cycle done) closes the "done arrives before the last prim commits" race.
        logic [3:0] fdr_done_dly;
        always_ff @(posedge clk or negedge rst_n) begin
            if (!rst_n) fdr_done_dly <= 4'd0;
            else        fdr_done_dly <= {fdr_done_dly[2:0], fdr_done};
        end
        assign feeder_list_flush = fdr_done_dly[3];

        gs_prim_list_feeder #(.STG_ADDR_W(12)) u_feeder (
            .clk(clk), .rst_n(rst_n), .start(fdr_start), .busy(fdr_busy), .done(fdr_done),
            .records_emitted(feeder_records), .fifo_wait_cycles(feeder_waits),
            .stg_rd_addr(fdr_rd_addr), .stg_rd_data(fdr_rd_data),
            .fifo_full(gs_raster_fifo_full),
            .gif_reg_wr_en(feeder_gif_reg_wr_en),
            .gif_reg_num(feeder_gif_reg_num),
            .gif_reg_data(feeder_gif_reg_data));
    end else begin : g_no_feeder
        assign feeder_gif_reg_wr_en = 1'b0;
        assign feeder_gif_reg_num   = 8'd0;
        assign feeder_gif_reg_data  = 64'd0;
        assign feeder_owns_bus      = 1'b0;
        assign feeder_records       = 16'd0;
        assign feeder_waits         = 32'd0;
        assign feeder_done_p        = 1'b0;
        assign feeder_ready_o       = 1'b0;
        assign feeder_list_flush    = 1'b0;
    end
    endgenerate

    // Ch330 Brick 4 — surface the feeder counters to the bridge (both profiles).
    assign feeder_records_o = feeder_records;
    assign feeder_waits_o   = feeder_waits;

    // Phased gif_reg_* mux: feeder ONLY once it owns the bus (post-setup); else the unpacker.
    wire        gs_gif_reg_wr_en = feeder_owns_bus ? feeder_gif_reg_wr_en : gif_gif_reg_wr_en;
    wire [7:0]  gs_gif_reg_num   = feeder_owns_bus ? feeder_gif_reg_num   : gif_gif_reg_num;
    wire [63:0] gs_gif_reg_data  = feeder_owns_bus ? feeder_gif_reg_data  : gif_gif_reg_data;

    // DMAC ready follows gif_packed_stub's in_ready directly (Ch110
    // image-xfer backpressure propagates through gif_packed_stub).
    assign dmac_gif_ready = gif_in_ready;

    // ---------------------------------------------------------------------
    // gs_stub — PSMCT32 raster, swizzled
    // ---------------------------------------------------------------------
    logic        priv_reg_wr_en;
    logic [15:0] priv_reg_wr_addr;
    logic [63:0] priv_reg_wr_data;

    logic [63:0] pmode_q, dispfb1_q, display1_q;
    logic [63:0] bitbltbuf_q, trxpos_q, trxreg_q, trxdir_q;
    logic        trxdir_wr_q;

    logic        raster_pixel_emit;
    logic [63:0] raster_pixel_color_q;
    logic [31:0] raster_pixel_fb_addr_q;
    logic [11:0] raster_pixel_x_q, raster_pixel_y_q;
    logic [31:0] raster_pixel_z_q;
    logic [3:0]  raster_pixel_be_q;
    logic [31:0] raster_pixel_mask_q;
    logic [63:0] gs_frame_1_q;
    logic [5:0]  raster_pixel_psm_q;
    logic        raster_pixel_abe_q;
    logic [16:0] raster_pixel_alpha_q;
    logic        raster_pixel_ztest_q;
    logic [1:0]  raster_pixel_ztst_q;
    logic        raster_pixel_zmsk_q;

    // Ch318 — expose the flush stream for an external LPDDR AXI writer (de25 top).
    assign flush_emit_o    = raster_pixel_emit;
    assign flush_addr_o    = raster_pixel_fb_addr_q;
    assign flush_pix16_o   = raster_pixel_color_q[15:0];
    assign flush_color32_o = raster_pixel_color_q[31:0];   // Ch323 — full color for spill capture
    // The production LPDDR ROP supports the byte-granular FBMSK subset used
    // by the captured SH3 frame. A set mask byte preserves that destination
    // byte; partial-bit mask bytes are rejected by fixture generation.
    wire [3:0] frame_byte_be = {gs_frame_1_q[63:56] == 8'h00,
                                gs_frame_1_q[55:48] == 8'h00,
                                gs_frame_1_q[47:40] == 8'h00,
                                gs_frame_1_q[39:32] == 8'h00};
    assign flush_be_o      = raster_pixel_be_q & frame_byte_be;
    assign flush_x_o       = raster_pixel_x_q;             // Ch357 — fragment coords + Z for the LPDDR-Z ROP
    assign flush_y_o       = raster_pixel_y_q;
    assign flush_z_o       = raster_pixel_z_q;
    assign flush_abe_o     = raster_pixel_abe_q;
    assign flush_alpha_o   = raster_pixel_alpha_q;
    assign flush_ztest_o   = raster_pixel_ztest_q;
    assign flush_ztst_o    = raster_pixel_ztst_q;
    assign flush_zmsk_o    = raster_pixel_zmsk_q;
    assign flush_psm_o     = raster_pixel_psm_q;

    // Ch295 — texture-sampler read port out of gs_stub. Wired to
    // vram_bram_stub's SECOND read port (read2) below, MUXED with the
    // PSMT4 RMW old-byte read. See the read2 arbitration block.
    //
    // TEX_RD_REGISTERED(1): vram_bram_stub.read2 is a 1-cycle
    // REGISTERED (sync) read — unlike vram_stub's combinational read2
    // that the Brick-1 demo top uses. gs_stub therefore generates the
    // texel address one pipeline stage earlier (S0 coords) so the
    // registered data lands at the S1 stage, where the existing single
    // S1->S2 texel register samples it. This keeps the emit-stage
    // timing identical to the combinational-read variant.
    logic        gs_tex_rd_en;
    logic [31:0] gs_tex_rd_addr;
    logic [31:0] gs_tex_rd_data;

    // Ch296 — PSMT8 indexed-texture CLUT lookup. gs_stub fetches an 8-bit
    // index through the texel read2 port, then `gs_clut_rd_idx` looks it
    // up in clut_stub (the palette filled by clut_loader_stub at TEX0
    // commit) to produce the PSMCT32 texel color. clut_stub's second
    // (combinational) read port returns it on `gs_clut_rd_data`.
    logic [7:0]  gs_clut_rd_idx;
    logic [31:0] gs_clut_rd_data;

    // Ch296 — CLUT-load-busy (declared here, ahead of the gs_stub instance
    // that consumes it as clut_load_busy to hold the FIFO pop while the
    // VRAM->CLUT load runs). Driven by clut_loader_stub.load_busy below.
    logic        clut_ld_busy;
    // Existing unit tops leave the new runtime ports open. Treat only a
    // driven-one as active so those legacy fixtures stay byte-identical.
    wire         runtime_clut_busy = (runtime_clut_busy_i === 1'b1);
    wire         runtime_clut_wr_en = (runtime_clut_wr_en_i === 1'b1);
    wire         clut_busy = clut_ld_busy | runtime_clut_busy;

    // Ch296 — gs_stub TEX0_1 decode taps that feed clut_loader_stub: the
    // 1-cycle commit pulse + CBP/CPSM/CSM/CSA/CLD that decide when (and
    // from where) the VRAM→CLUT copy fires.
    logic [13:0] gs_tex0_cbp;
    logic [3:0]  gs_tex0_cpsm;
    logic        gs_tex0_csm;
    logic [4:0]  gs_tex0_csa;
    logic [2:0]  gs_tex0_cld;
    logic        gs_tex0_wr;

    // Brick 2a — dest-framebuffer read port for alpha blending. Wired
    // to vram_bram_stub.read2 below, arbitrated with the texel-fetch
    // port (mutually exclusive: a flat blend never textures). Uses the
    // 1-cycle registered read model (FB_RD_REGISTERED=1) to match the
    // BRAM read2 latency.
    logic        gs_fb_rd_en;
    logic [31:0] gs_fb_rd_addr;
    logic [31:0] gs_fb_rd_data;

    // Brick 2b — Z-buffer stored-Z read port. Wired to vram_bram_stub.read2
    // below, arbitrated with the texel-fetch + alpha dest-fb ports (mutually
    // exclusive: a flat Z-tested sprite never textures and never blends).
    // Uses the 1-cycle registered read model (Z_RD_REGISTERED=1) to match
    // the BRAM read2 latency.
    logic        gs_z_rd_en;
    logic [31:0] gs_z_rd_addr;
    logic [31:0] gs_z_rd_data;

    gs_stub #(
        .PSMCT32_SWIZZLE  (PSMCT32_SWIZZLE),
        .PSMT4_SWIZZLE    (PSMT4_SWIZZLE),
        .PSMT8_SWIZZLE    (PSMT8_SWIZZLE),
        .TEX_RD_LATENCY   (1),
        .CLUT_STALL       (1'b1),       // hold pop while VRAM->CLUT load runs
        .TEX_RD_REGISTERED(1'b1),
        .FB_RD_REGISTERED (1'b1),
        .Z_RD_REGISTERED  (1'b1),
        .PERSPECTIVE_CORRECT(PERSPECTIVE_CORRECT),  // Ch301 — forwarded board param (default 0)
        .FOG_ENABLE        (FOG_ENABLE),             // per-vertex fog gate; 0 prunes the fog cone
        .SUBPIXEL_XY       (SUBPIXEL_XY),            // Ch416 — native GS 12.4 coverage (default 0)
        .PERSP_RECIP_IDX_BITS(PERSP_RECIP_IDX_BITS), // Ch351 — perspective reciprocal LUT width (far-W -> 11)
        .GRAD_DIV_CYCLES(GRAD_DIV_CYCLES),           // Ch352 — triangle-setup divide settle cycles (board -> 4)
        .GRAD_SEQ_DIVIDER(GRAD_SEQ_DIVIDER),         // Ch352 — sequential gradient divider (board -> 1)
        .SPRITE_TEX_ALPHA   (SPRITE_TEX_ALPHA),     // Ch344 — textured + source-over alpha SPRITE (default 0)
        .SPRITE_TEX_ALPHA_CLUT (SPRITE_TEX_ALPHA_CLUT),  // Ch347 — PSMT8 CLUT into the alpha-sprite path (default 0)
        .COMBINED_TAZ       (COMBINED_TAZ),         // Ch302 — combined tex+alpha+depth probe (default 0)
        .TILE_LOCAL         (TILE_LOCAL),           // Ch303 — on-chip tile color+Z render (default 0)
        .TILE_COLS          (TILE_COLS),            // Ch304 — tile grid cols (default 1)
        .TILE_ROWS          (TILE_ROWS),            // Ch304 — tile grid rows (default 1)
        .TILE_MULTIPRIM     (TILE_MULTIPRIM),       // Ch305 — render a primitive LIST per tile (default 0)
        .TILE_PRIM_COUNT    (TILE_PRIM_COUNT),      // Ch305 — primitives in the batch (default 1)
        .TILE_FIFO_DEPTH    (TILE_FIFO_DEPTH),      // Ch315 — prim FIFO / bin depth (capacity, default 4)
        .SCISSOR_ENABLE     (SCISSOR_ENABLE),       // Ch306 — SCISSOR_1 rect clip in tile walker (default 0)
        .TEX_WRAP_ENABLE    (TEX_WRAP_ENABLE),      // Ch307 — texture wrap/clamp in sampler (default 0)
        .TILE_COLOR_PSMCT16 (TILE_COLOR_PSMCT16),   // Ch308 — PSMCT16 tile color buffer (default 0)
        .ALPHA_MODES_ENABLE (ALPHA_MODES_ENABLE),   // Ch309 — generic ALPHA blend modes (default 0)
        .BILINEAR_ENABLE    (BILINEAR_ENABLE),      // Ch310 — bilinear filtering in combined path (default 0)
        .PALETTE_BILINEAR   (PALETTE_BILINEAR),     // Ch314 — bilinear for PSMT8/PSMT4 indexed textures (default 0)
        .BIN_BUFFER_ENABLE  (BIN_BUFFER_ENABLE),    // Ch311 — per-tile bin buffer (default 0)
        .TILE_SPILL_ENABLE  (TILE_SPILL_ENABLE),    // Ch323 — tile color+Z spill/reload (default 0)
        .SPILL_FORCE_VALID  (SPILL_FORCE_VALID),    // Ch323 — test hook (negative bootstrap test)
        .MP_FLUSH_ONLY      (FEEDER_ENABLE),        // Ch331 — feeder mode: grid fires on end-of-list flush
        .TILE_ACCUM_ENABLE  (FEEDER_ENABLE),        // Ch336 — >FIFO_DEPTH framebuffer accumulation (feeder mode)
        .TILE_Z_PERSIST     (FEEDER_ENABLE)         // Ch338 — persistent cross-batch Z (feeder mode)
    ) u_gs (
        .clk(clk), .rst_n(rst_n),
        .reg_wr_en  (priv_reg_wr_en),
        .reg_wr_addr(priv_reg_wr_addr),
        .reg_wr_data(priv_reg_wr_data),  // Ch330: gif_reg_* below is muxed (feeder vs unpacker)
        .gif_reg_wr_en(gs_gif_reg_wr_en),
        .gif_reg_num  (gs_gif_reg_num),
        .gif_reg_data (gs_gif_reg_data),
        .prim_list_flush_i(feeder_list_flush),  // Ch331 — feeder end-of-list grid flush (0 in non-feeder)
        .bg_r(), .bg_g(), .bg_b(),
        .pmode_q(pmode_q), .dispfb1_q(dispfb1_q), .display1_q(display1_q),
        .prim_q(), .rgbaq_q(),
        .xyz2_q(), .xyzf2_q(),
        .frame_1_q(gs_frame_1_q), .zbuf_1_q(),
        .tex0_1_q(), .tex0_1_cbp_q(gs_tex0_cbp), .tex0_1_cpsm_q(gs_tex0_cpsm),
        .tex0_1_csm_q(gs_tex0_csm), .tex0_1_csa_q(gs_tex0_csa),
        .tex0_1_cld_q(gs_tex0_cld), .tex0_1_wr_q(gs_tex0_wr),
        .bitbltbuf_q(bitbltbuf_q),
        .trxpos_q(trxpos_q),
        .trxreg_q(trxreg_q),
        .trxdir_q(trxdir_q),
        .trxdir_wr_q(trxdir_wr_q),
        .prim_complete(), .prim_complete_count(),
        .prim_v0_q(), .prim_v1_q(), .prim_v2_q(),
        .prim_color_q(),
        .prim_color_v0_q(), .prim_color_v1_q(), .prim_color_v2_q(),
        .prim_v0_decoded_q(), .prim_v1_decoded_q(), .prim_v2_decoded_q(),
        .prim_v0_color_decoded_q(), .prim_v1_color_decoded_q(), .prim_v2_color_decoded_q(),
        .pixel_emit(), .pixel_emit_count(),
        .pixel_x_q(), .pixel_y_q(),
        .pixel_color_q(),
        .pixel_fbp_q(), .pixel_fbw_q(), .pixel_psm_q(), .pixel_fb_addr_q(),
        .raster_pixel_emit(raster_pixel_emit),
        .raster_pixel_emit_count(),
        .raster_pixel_x_q(raster_pixel_x_q), .raster_pixel_y_q(raster_pixel_y_q),
        .raster_pixel_color_q(raster_pixel_color_q),
        .raster_pixel_fb_addr_q(raster_pixel_fb_addr_q),
        .raster_pixel_z_q(raster_pixel_z_q),
        .raster_pixel_be_q(raster_pixel_be_q),
        .raster_pixel_mask_q(raster_pixel_mask_q),
        .raster_pixel_psm_q(raster_pixel_psm_q),
        .raster_pixel_abe_q(raster_pixel_abe_q),
        .raster_pixel_alpha_q(raster_pixel_alpha_q),
        .raster_pixel_ztest_q(raster_pixel_ztest_q),
        .raster_pixel_ztst_q(raster_pixel_ztst_q),
        .raster_pixel_zmsk_q(raster_pixel_zmsk_q),
        .z_flush_emit_o(z_flush_emit_o),     // Ch323 — tile Z-flush stream (de25 Z-writer)
        .z_flush_addr_o(z_flush_addr_o),
        .z_flush_data_o(z_flush_data_o),
        .tile_color_flush_emit_o(tile_color_flush_emit_o),  // Ch323 — dedicated color-flush spill stream
        .tile_color_flush_addr_o(tile_color_flush_addr_o),
        .tile_color_flush_data_o(tile_color_flush_data_o),
        .reload_start_o(reload_start_o),     // Ch323 — tile reload staging interface
        .tile_reload_raddr_o(tile_reload_raddr_o),
        .reload_base_o(reload_base_o),       // Ch324 — per-tile raster-FB byte offset
        .tile_reload_ready_i(tile_reload_ready_i),
        .tile_reload_color_i(tile_reload_color_i[(TILE_COLOR_PSMCT16?16:32)-1:0]),
        .tile_reload_z_i(tile_reload_z_i),
        .tile_phase_o(tile_phase_o),         // Ch323 diag — current tile phase
        .raster_active(gs_raster_active),    // Ch330 — drives the feeder retrigger 'known-empty' gate
        .raster_scene_busy(gs_scene_busy),   // Ch337 — whole-scene drain gate (no inter-batch dip)
        .raster_overflow(raster_overflow),
        .raster_fifo_full(gs_raster_fifo_full),
        .raster_degenerate(),
        .tex_rd_en  (gs_tex_rd_en),
        .tex_rd_addr(gs_tex_rd_addr),
        .tex_rd_data(gs_tex_rd_data),
        .clut_rd_idx (gs_clut_rd_idx),
        .clut_rd_data(gs_clut_rd_data),
        .clut_load_busy(clut_busy),      // hold pop while either CLUT writer runs
        .fb_rd_en  (gs_fb_rd_en),
        .fb_rd_addr(gs_fb_rd_addr),
        .fb_rd_data(gs_fb_rd_data),
        .z_rd_en  (gs_z_rd_en),
        .z_rd_addr(gs_z_rd_addr),
        .z_rd_data(gs_z_rd_data),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    // ---------------------------------------------------------------------
    // Ch317 — LPDDR-backed framebuffer write sink (tile-flush MIRROR). Fed by the
    // PSMCT16 tile-FLUSH stream (emit + linear fb byte addr + pix16). Commits to an
    // LPDDR-style model with per-row bursts + 4 KiB cap + bandwidth/over-underflow
    // counters for write/readback proof. enable=0 → inert. Scanout still comes from
    // the on-chip BRAM FB; a later rung swaps this model for the real EMIF AXI master
    // + LPDDR scanout. Gated to PSMCT16 flushes (psm==0x02) so it only fires in a
    // full-PSMCT16-FB demo (TILE_COLOR_PSMCT16=1); harmless otherwise.
    // ---------------------------------------------------------------------
    logic [31:0] lpfb_bytes_written, lpfb_burst_count, lpfb_busy_cycles;
    logic [31:0] lpfb_overflow_count, lpfb_underflow_count;
    logic [15:0] lpfb_occ;
    generate
    if (LPDDR_FB_ENABLE) begin : g_lpddr_fb
        gs_lpddr_fb_writer #(.FB_BYTES(LPDDR_FB_BYTES), .FIFO_DEPTH(32), .MAX_BURST_BYTES(4096)) u_lpddr_fb (
            .clk(clk), .rst_n(rst_n),
            .enable(1'b1),
            .px_emit(raster_pixel_emit && (raster_pixel_psm_q == 6'h02)),
            .px_addr(raster_pixel_fb_addr_q),
            .px_pix16(raster_pixel_color_q[15:0]),
            .bytes_written(lpfb_bytes_written),
            .burst_count(lpfb_burst_count),
            .busy_cycles(lpfb_busy_cycles),
            .fifo_overflow_count(lpfb_overflow_count),
            .fifo_underflow_count(lpfb_underflow_count),
            .fifo_occ(lpfb_occ)
        );
    end else begin : g_no_lpddr_fb
        // default OFF — no fbmem / FIFO instantiated (pruned), status tied to 0.
        assign lpfb_bytes_written = '0; assign lpfb_burst_count = '0;
        assign lpfb_busy_cycles   = '0; assign lpfb_overflow_count = '0;
        assign lpfb_underflow_count = '0; assign lpfb_occ = '0;
    end
    endgenerate

    // ---------------------------------------------------------------------
    // ee_gs_priv_bridge_stub
    // ---------------------------------------------------------------------
    ee_gs_priv_bridge_stub u_priv_bridge (
        .clk(clk), .rst_n(rst_n),
        .ee_wr_en  (map_gs_priv_wr_en),
        .ee_wr_addr(map_gs_priv_wr_addr),
        .ee_wr_data(map_gs_priv_wr_data),
        .ee_wr_be  (map_gs_priv_wr_be),
        .gs_reg_wr_en  (priv_reg_wr_en),
        .gs_reg_wr_addr(priv_reg_wr_addr),
        .gs_reg_wr_data(priv_reg_wr_data)
    );

    // ---------------------------------------------------------------------
    // gif_image_xfer_stub — idle in Ch123 (no TRXDIR/IMAGE), but
    // instantiated for symmetry. The TRXDIR-driven Ch124 demo would
    // turn it load-bearing.
    // ---------------------------------------------------------------------
    logic        xfer_we;
    logic [31:0] xfer_waddr;
    logic [31:0] xfer_wdata;
    logic [3:0]  xfer_wbe;
    logic [31:0] xfer_wmask;
    logic        xfer_busy;

    gif_image_xfer_stub #(
        .PSMCT32_SWIZZLE(PSMCT32_SWIZZLE),
        .PSMT4_SWIZZLE  (PSMT4_SWIZZLE),
        .PSMT8_SWIZZLE  (PSMT8_SWIZZLE)
    ) u_xfer (
        .clk(clk), .rst_n(rst_n),
        .trxdir_wr_pulse(trxdir_wr_q),
        .trxdir(trxdir_q),
        .bitbltbuf(bitbltbuf_q),
        .trxpos(trxpos_q),
        .trxreg(trxreg_q),
        .data_valid(gif_image_data_valid),
        .data_qword(gif_image_data),
        .data_last (gif_image_data_last),
        .data_ready(xfer_data_ready),
        .vram_we   (xfer_we),
        .vram_waddr(xfer_waddr),
        .vram_wdata(xfer_wdata),
        .vram_wbe  (xfer_wbe),
        .vram_wmask(xfer_wmask),
        .busy      (xfer_busy)
    );

    // ---------------------------------------------------------------------
    // VRAM mux: xfer-OWNED when xfer.busy, raster-OWNED otherwise.
    // (Sequenced: in Ch123 raster fills exclusively; xfer never fires.
    // In a future TRXDIR variant the mux still works — payload upload
    // finishes before raster starts.)
    //
    // Ch156: the legacy writer engines emit at byte-addressable
    // granularity with per-bit `vram_wmask`. `vram_bram_stub` is
    // word-aligned + byte-WE only. We mux the writer engines'
    // pre-normalize signals first, then run the result through
    // `vram_normalize_pkg::normalize_write` to translate into the
    // BRAM contract. The PSM source is the raster-side
    // `raster_pixel_psm_q` during raster emits and `bitbltbuf_q`'s
    // DPSM field (bits [61:56]) during xfer emits — both match
    // what the corresponding writer engine used to compute its
    // emit shape.
    // ---------------------------------------------------------------------
    logic        vram_we_pre;
    logic [31:0] vram_waddr_pre;
    logic [31:0] vram_wdata_pre;
    logic [5:0]  vram_psm_pre;
    logic [31:0] vram_mask_pre;

    // Ch326 — FB_LPDDR_ONLY gates the flush's BRAM-mirror write; texture upload (xfer) still
    // writes vram_stub. The dedicated LPDDR color flush (tile_color_flush_emit_o) is unaffected.
    assign vram_we_pre     = xfer_busy ? xfer_we    : (raster_pixel_emit && !FB_LPDDR_ONLY);
    assign vram_waddr_pre  = xfer_busy ? xfer_waddr : raster_pixel_fb_addr_q;
    assign vram_wdata_pre  = xfer_busy ? xfer_wdata : raster_pixel_color_q[31:0];
    assign vram_psm_pre    = xfer_busy ? bitbltbuf_q[61:56] : raster_pixel_psm_q;
    assign vram_mask_pre   = xfer_busy ? xfer_wmask : raster_pixel_mask_q;

    // Ch157 PSMT4 RMW pipeline (replaces Ch156's hard-gate).
    //
    // PSMT4 packs 2 pixels per byte: the writer emits a 4-bit
    // nibble pre-shifted into either the LOW or HIGH nibble of
    // `vram_wdata_pre[7:0]`, with `vram_mask_pre[7:0]` set to 0x0F
    // (low) or 0xF0 (high). To commit one nibble while preserving
    // the other, vram_bram_stub (byte-WE only, no per-bit RMW)
    // needs the FULL byte value spliced upstream — that is what
    // `vram_normalize_pkg::normalize_write`'s PSMT4 branch does
    // when handed the LIVE `old_byte` from `mem[byte_addr]`.
    //
    // The pipe drives `read2_addr = byte_addr` on the T4 emit
    // cycle. One cycle later (`vram_bram_stub` registers reads
    // with 1-cycle latency), `vram_read2_data` is mem[byte_addr]
    // BEFORE any pending writes; we extract `old_byte` from the
    // `byte_addr[1:0]` lane, splice in the new nibble, and drive
    // a full-byte write at the same address — one cycle after the
    // emit fired. Non-T4 emits skip the pipe entirely and write
    // same-cycle through `vram_norm` (CT32/CT16/T8 normalize_write
    // is pure-comb and doesn't need a read).
    //
    // **Forwarding hazard**: a PSMT4 SPRITE rasters adjacent
    // pixels x=2k and x=2k+1 to the SAME byte_addr (low + high
    // nibble). At cycle N+1 we read mem[byte_addr] for emit-2
    // while emit-1's write is firing in the SAME posedge. With
    // separate always_ff blocks for write and read inside
    // vram_bram_stub, the read sees the PRE-write value due to
    // NBA semantics. We forward emit-1's just-computed
    // `t4_prev_new_byte_q` when `byte_addr` of the in-flight
    // emit-2 matches the previous emit's `byte_addr`. This keeps
    // the chain correct across any number of back-to-back same-
    // byte writes — emit-N reads emit-(N-1)'s new_byte from the
    // forward register, splices on top, and emit-(N+1) reads
    // emit-N's new_byte from that same register.
    logic        is_t4_emit;
    logic        t4_nibble_hi;
    logic [3:0]  t4_nibble_value;

    assign is_t4_emit       = vram_we_pre &&
                              (vram_psm_pre == vram_normalize_pkg::PSM_PSMT4);
    assign t4_nibble_hi     = (vram_mask_pre[7:0] == 8'hF0);
    assign t4_nibble_value  = t4_nibble_hi ? vram_wdata_pre[7:4]
                                            : vram_wdata_pre[3:0];

    // Ch157 writer-side normalization. CT32/CT16/T8 use the pure-
    // comb path (same-cycle write). T4 uses `vram_norm_t4` below
    // with the read-back `old_byte` plumbed in.
    vram_normalize_pkg::norm_out_t vram_norm;
    assign vram_norm = vram_normalize_pkg::normalize_write(
        vram_waddr_pre,
        vram_psm_pre,
        vram_wdata_pre,
        1'b0,              // nibble_hi — non-T4 paths ignore it
        8'd0               // old_byte  — non-T4 paths ignore it
    );

    // Pipe stage 1 — captured T4 emit signals one cycle after the
    // emit fired. Read2 has produced mem[byte_addr] at this point.
    logic        t4_pipe_valid_q;
    logic [31:0] t4_pipe_addr_q;
    logic        t4_pipe_nibble_hi_q;
    logic [3:0]  t4_pipe_nibble_q;

    always_ff @(posedge clk) begin
        if (!rst_n) begin
            t4_pipe_valid_q     <= 1'b0;
            t4_pipe_addr_q      <= 32'd0;
            t4_pipe_nibble_hi_q <= 1'b0;
            t4_pipe_nibble_q    <= 4'd0;
        end else begin
            t4_pipe_valid_q     <= is_t4_emit;
            t4_pipe_addr_q      <= vram_waddr_pre;
            t4_pipe_nibble_hi_q <= t4_nibble_hi;
            t4_pipe_nibble_q    <= t4_nibble_value;
        end
    end

    // Pipe stage 2 forward — the just-completed RMW's address +
    // produced byte. Used to forward when the next T4 emit hits
    // the same byte_addr.
    logic        t4_prev_valid_q;
    logic [31:0] t4_prev_addr_q;
    logic [7:0]  t4_prev_new_byte_q;

    // Read-back lane extraction.
    logic [7:0]  t4_read_byte_lane;
    always_comb begin
        case (t4_pipe_addr_q[1:0])
            2'b00: t4_read_byte_lane = vram_read2_data[ 7: 0];
            2'b01: t4_read_byte_lane = vram_read2_data[15: 8];
            2'b10: t4_read_byte_lane = vram_read2_data[23:16];
            2'b11: t4_read_byte_lane = vram_read2_data[31:24];
        endcase
    end

    // Forwarded `old_byte`: prev-RMW's new_byte if same address,
    // else the freshly-read lane from vram_read2_data.
    logic [7:0] t4_effective_old_byte;
    assign t4_effective_old_byte = (t4_prev_valid_q && (t4_prev_addr_q == t4_pipe_addr_q))
                                 ? t4_prev_new_byte_q
                                 : t4_read_byte_lane;

    // Splice — same math as `normalize_write`'s PSMT4 branch.
    logic [7:0] t4_new_byte;
    always_comb begin
        if (t4_pipe_nibble_hi_q)
            t4_new_byte = {t4_pipe_nibble_q,        t4_effective_old_byte[3:0]};
        else
            t4_new_byte = {t4_effective_old_byte[7:4], t4_pipe_nibble_q};
    end

    always_ff @(posedge clk) begin
        if (!rst_n) begin
            t4_prev_valid_q    <= 1'b0;
            t4_prev_addr_q     <= 32'd0;
            t4_prev_new_byte_q <= 8'd0;
        end else begin
            t4_prev_valid_q    <= t4_pipe_valid_q;
            t4_prev_addr_q     <= t4_pipe_addr_q;
            t4_prev_new_byte_q <= t4_new_byte;
        end
    end

    // T4 RMW write-side: word-aligned addr, full-byte data shifted
    // to the byte_addr[1:0] lane, single-byte write_be.
    logic [31:0] t4_write_addr;
    logic [31:0] t4_write_data;
    logic [3:0]  t4_write_be;
    assign t4_write_addr = t4_pipe_addr_q & ~32'd3;
    always_comb begin
        case (t4_pipe_addr_q[1:0])
            2'b00: begin t4_write_data = {24'd0, t4_new_byte};        t4_write_be = 4'b0001; end
            2'b01: begin t4_write_data = {16'd0, t4_new_byte,  8'd0}; t4_write_be = 4'b0010; end
            2'b10: begin t4_write_data = { 8'd0, t4_new_byte, 16'd0}; t4_write_be = 4'b0100; end
            2'b11: begin t4_write_data = {       t4_new_byte, 24'd0}; t4_write_be = 4'b1000; end
        endcase
    end

    // VRAM final mux: T4 RMW pipe write OR same-cycle non-T4 write.
    // gs_stub keeps PSM constant within a raster, so a T4 pipe
    // entry never overlaps with a non-T4 emit cycle from the
    // raster path (the raster pipe drains before any FRAME_1
    // swap can re-arm a different PSM). Non-T4 emits route
    // through `vram_norm` directly.
    //
    // **Ch157 audit Medium fix — overlap assertion**: the wrapper
    // also muxes `gif_image_xfer_stub` writes through `vram_we_pre`
    // when `xfer_busy=1`. The "no T4-pipe / non-T4-emit overlap"
    // property is therefore an *unenforced* invariant in the design
    // today (raster + xfer aren't allowed to overlap PSM-mismatched
    // writes by the surrounding flow, but nothing in this wrapper
    // checks it). The mux below gives the T4 pipe unconditional
    // priority — if a caller ever violates the invariant, the non-
    // T4 write would be silently dropped. The sim-only block at
    // the end of the file asserts the invariant loudly so the
    // violation surfaces as a $error instead of as silent data
    // loss. Synthesis sees only the mux logic; the assertion is
    // bracketed by translate_off/_on.
    logic        vram_we_final;
    logic [31:0] vram_waddr_final;
    logic [31:0] vram_wdata_final;
    logic [3:0]  vram_wbe_final;

    assign vram_we_final    = t4_pipe_valid_q ? 1'b1
                                              : (vram_we_pre && !is_t4_emit);
    assign vram_waddr_final = t4_pipe_valid_q ? t4_write_addr  : vram_norm.write_addr;
    assign vram_wdata_final = t4_pipe_valid_q ? t4_write_data  : vram_norm.write_data;
    assign vram_wbe_final   = t4_pipe_valid_q ? t4_write_be    : vram_norm.write_be;

    // synthesis translate_off
    always_ff @(posedge clk) begin
        if (rst_n && t4_pipe_valid_q && vram_we_pre && !is_t4_emit)
            $error("Ch157: T4 RMW pipe write @%0t collides with non-T4 vram_we_pre (psm=0x%02h, addr=0x%08h); non-T4 write would be dropped — caller violated the no-overlap invariant",
                   $time, vram_psm_pre, vram_waddr_pre);
    end
    // synthesis translate_on

    // ---------------------------------------------------------------------
    // Ch296 — CLUT load engine + palette table for PSMT8 indexed textures.
    //
    // The read2 bus signals are declared here (ahead of the arbitration
    // mux) so the CLUT loader instance below can tap vram_read2_data.
    // (clut_ld_busy is declared up by the gs_clut_* signals so the gs_stub
    // instance above can reference it.)
    logic [31:0] vram_read2_addr;
    logic [31:0] vram_read2_data;
    logic [31:0] vram_raddr;
    logic [31:0] vram_rdata;

    // A CSM1-only profile whose textures come from the external LPDDR cache
    // does not need the replicated VRAM read2 array during rasterization. Reuse
    // the existing scanout read port for the short boot-time CLUT load instead.
    // The load precedes raster, so temporarily pausing scanout reads is harmless;
    // both VRAM ports have the same registered one-cycle latency.
    localparam bit CLUT_LOAD_USE_READ0 = CLUT_CSM1_ENABLE && !VRAM_ENABLE_READ2;
    //
    // clut_loader_stub copies 256 PSMCT32 entries from VRAM[CBP*256] into
    // clut_stub when a TEX0_1 commit (gs_tex0_wr) carries a load-enabling
    // CLD/CSM/CPSM. This runs at TEX0 commit — BEFORE the raster scan of
    // the textured primitive — so the loader's read2 use is time-disjoint
    // from the texel fetch (the mutual exclusion the architecture relies
    // on; it is the FIFTH read2 consumer, given top priority below).
    //
    // The loader was written against a COMBINATIONAL VRAM read (data in
    // the SAME cycle as the address). vram_bram_stub.read2 is a 1-cycle
    // REGISTERED read, so the word for entry N arrives one cycle AFTER the
    // loader presents addr(N)+write_idx=N. The loader's clut_write_data
    // (= vram_read_data = the registered read) is therefore ALREADY
    // aligned to the late entry, while clut_write_en/idx are one cycle
    // early. To realign we delay ONLY en+idx by one cycle (so en/idx for
    // entry N land in the same cycle the registered data for entry N
    // returns) and use the loader's LIVE write_data — which at that cycle
    // already carries entry N's word.
    // ---------------------------------------------------------------------
    logic [31:0] clut_ld_rd_addr;
    logic        clut_ld_wr_en_c;
    logic [7:0]  clut_ld_wr_idx_c;
    logic [31:0] clut_ld_wr_data_c;

    clut_loader_stub #(.CLUT_CSM1_ENABLE(CLUT_CSM1_ENABLE)) u_clut_loader (
        .clk(clk), .rst_n(rst_n),
        .tex0_wr_pulse (gs_tex0_wr),
        .tex0_cbp      (gs_tex0_cbp),
        .tex0_cpsm     (gs_tex0_cpsm),
        .tex0_csm      (gs_tex0_csm),
        .tex0_csa      (gs_tex0_csa),
        .tex0_cld      (gs_tex0_cld),
        .vram_read_addr(clut_ld_rd_addr),
        .vram_read_data(CLUT_LOAD_USE_READ0 ? vram_rdata : vram_read2_data),
        .clut_write_en (clut_ld_wr_en_c),
        .clut_write_idx(clut_ld_wr_idx_c),
        .clut_write_data(clut_ld_wr_data_c),
        .load_busy     (clut_ld_busy)
    );

    // 1-cycle realignment: delay en+idx only; write_data is taken LIVE
    // (it is the registered read, already aligned to the late entry).
    logic        clut_ld_wr_en_q;
    logic [7:0]  clut_ld_wr_idx_q;
    always_ff @(posedge clk) begin
        if (!rst_n) begin
            clut_ld_wr_en_q   <= 1'b0;
            clut_ld_wr_idx_q  <= 8'd0;
        end else begin
            clut_ld_wr_en_q   <= clut_ld_wr_en_c;
            clut_ld_wr_idx_q  <= clut_ld_wr_idx_c;
        end
    end

    wire        clut_wr_en  = runtime_clut_wr_en ? 1'b1 : clut_ld_wr_en_q;
    wire [7:0]  clut_wr_idx = runtime_clut_wr_en ? runtime_clut_wr_idx_i : clut_ld_wr_idx_q;
    wire [31:0] clut_wr_data = runtime_clut_wr_en ? runtime_clut_wr_data_i : clut_ld_wr_data_c;

    // synthesis translate_off
    always_ff @(posedge clk) begin
        if (rst_n && runtime_clut_wr_en && clut_ld_wr_en_q)
            $error("runtime CLUT copy collided with TEX0 CLUT loader");
    end
    // synthesis translate_on

    clut_stub u_clut (
        .clk(clk), .rst_n(rst_n),
        .write_en (clut_wr_en),
        .write_idx(clut_wr_idx),
        .write_data(clut_wr_data),
        // pcrtc scanout read port — unused in this textured-board top
        // (PCRTC clut_enable=0). Tie its index to 0.
        .read_idx (8'd0),
        .read_data(),
        // texture-sampler read port — drives the PSMT8 index lookup.
        .tex_read_idx (gs_clut_rd_idx),
        .tex_read_data(gs_clut_rd_data)
    );

    // ---------------------------------------------------------------------
    // Ch295 — read2 (second VRAM read port) ARBITRATION.
    //
    // vram_bram_stub exposes ONE second read port (read2). Two
    // consumers want it, but they are MUTUALLY EXCLUSIVE BY PIXEL
    // FORMAT, so a static-priority mux is collision-free:
    //
    //   (A) PSMT4 RMW old-byte read  — fires only on `is_t4_emit`, i.e.
    //       when the FRAME buffer PSM == PSMT4 (the raster/xfer writer
    //       is committing a 4-bit nibble and needs the live byte to
    //       splice). gs_stub holds PSM constant within a raster.
    //   (B) Textured-SPRITE texel fetch — fires only on `gs_tex_rd_en`,
    //       i.e. SPRITE + TME + texture-PSM==PSMCT32 (see gs_stub
    //       s1_tex_active). A textured PSMCT32 SPRITE writes the FRAME
    //       buffer as PSMCT32, so `is_t4_emit` is 0 for every pixel of
    //       that primitive — the T4 path is dormant.
    //
    // Because a given primitive is EITHER a PSMT4 write (consumer A) OR
    // a PSMCT32 textured SPRITE (consumer B), the two enables never
    // assert in the same cycle. T4 gets static priority (it is the
    // load-bearing RMW path for the PSMT4 production cases); the texel
    // fetch takes the port whenever T4 is idle. The runtime assertion
    // below flags any overlap loudly in sim (it must never fire).
    //
    // Latency: vram_bram_stub.read2 is a 1-cycle REGISTERED read. The
    // T4 pipe already assumes this (it presents addr on the emit cycle
    // and consumes read2_data one cycle later). gs_stub is built with
    // TEX_RD_REGISTERED=1 so it presents the texel address one stage
    // early (S0) and consumes the registered data at S1 — the same
    // 1-cycle round trip. Both consumers therefore see the identical
    // BRAM read latency they were each designed against.
    //
    // Brick 2a — THIRD read2 consumer: the alpha-blend dest-fb read
    // (gs_fb_rd_en/gs_fb_rd_addr). It is mutually exclusive with BOTH
    // existing consumers by pixel format:
    //   - vs PSMT4 RMW (A): a flat alpha-blend SPRITE writes PSMCT32, so
    //     is_t4_emit is 0 for every pixel of that primitive.
    //   - vs texel fetch (B): gs_stub only sets fb_rd_en for a FLAT
    //     (non-textured) blended SPRITE, and only sets tex_rd_en for a
    //     TEXTURED SPRITE. A given SPRITE is one or the other, never
    //     both — gs_stub.new_abe_active requires !close_tme_effective.
    // Brick 2b — FOURTH read2 consumer: the Z-buffer stored-Z read
    // (gs_z_rd_en/gs_z_rd_addr). Mutually exclusive with ALL three
    // existing consumers by pixel format / feature:
    //   - vs PSMT4 RMW (A): a flat Z-tested SPRITE writes PSMCT32, so
    //     is_t4_emit is 0 for every pixel of that primitive.
    //   - vs texel fetch (B): gs_stub only sets z_rd_en for a FLAT
    //     (non-textured) Z-tested SPRITE (new_zte_active requires
    //     !close_tme_effective); tex_rd_en is set only for TEXTURED.
    //   - vs alpha dest-fb (C): new_zte_active requires !new_abe_active,
    //     so a primitive is EITHER Z-tested OR alpha-blended, never both.
    // Static priority T4 > texel > dest-fb > Z-read; the assertion below
    // flags any overlap loudly in sim (it must never fire).
    // Ch296 — FIFTH read2 consumer: the CLUT load (clut_ld_busy). It runs
    // at TEX0 commit, strictly BEFORE the raster scan, so it is
    // time-disjoint from the four raster-time consumers below. Given top
    // priority; the assertion confirms it never coincides with a raster
    // read in practice. (vram_read2_addr/_data are declared above.)
    assign vram_read2_addr = clut_ld_busy ? clut_ld_rd_addr
                           : is_t4_emit   ? (vram_waddr_pre & ~32'd3)
                           : gs_tex_rd_en ? gs_tex_rd_addr
                           : gs_fb_rd_en  ? gs_fb_rd_addr
                           : gs_z_rd_en   ? gs_z_rd_addr
                                          : 32'd0;

    // Texel fetch, dest-fb read AND Z-read consume the SAME registered
    // read2_data. (The T4 lane extraction above also reads
    // vram_read2_data; none of the four overlap.)
    // Ch322 — TEXEL data is muxed to the external prefilled texture cache when
    // GS_LPDDR_TEX and the cache is warm and the byte address is inside the cached
    // texture range. The select is registered ONE cycle so it aligns with the cache's
    // 1-cycle registered data AND vram_read2_data (both land the cycle after the
    // address is presented) — identical timing to the BRAM texel path. fb/Z reads
    // always come from BRAM (the cache only services texture).
    assign gs_tex_rd_en_o   = gs_tex_rd_en;
    assign gs_tex_rd_addr_o = gs_tex_rd_addr;
    generate if (GS_LPDDR_TEX) begin : g_lpddr_tex
        wire tex_in_range = (gs_tex_rd_addr >= TEX_VRAM_BASE)
                         && (gs_tex_rd_addr <  (TEX_VRAM_BASE + TEX_CACHE_BYTES));
        logic tex_cache_sel_q, gs_tex_rd_en_d;
        logic [31:0] cache_hits_q, bram_hits_q;
        always_ff @(posedge clk) begin
            if (!rst_n) begin
                tex_cache_sel_q <= 1'b0; gs_tex_rd_en_d <= 1'b0;
                cache_hits_q <= 32'd0; bram_hits_q <= 32'd0;
            end else begin
                tex_cache_sel_q <= tex_cache_ready_i && gs_tex_rd_en && tex_in_range;
                gs_tex_rd_en_d  <= gs_tex_rd_en;
                // count each served texel read by source (sel_q and en_d are both 1-cycle
                // delayed off the same read, so they align): cache vs BRAM-fallback.
                if      (tex_cache_sel_q) cache_hits_q <= cache_hits_q + 32'd1;
                else if (gs_tex_rd_en_d)  bram_hits_q  <= bram_hits_q  + 32'd1;
            end
        end
        assign gs_tex_rd_data   = tex_cache_sel_q ? tex_cache_data_i : vram_read2_data;
        assign tex_cache_hits_o = cache_hits_q;
        assign tex_bram_hits_o  = bram_hits_q;
    end else begin : g_no_lpddr_tex
        assign gs_tex_rd_data   = vram_read2_data;
        assign tex_cache_hits_o = 32'd0;
        assign tex_bram_hits_o  = 32'd0;
    end endgenerate
    assign gs_fb_rd_data  = vram_read2_data;
    assign gs_z_rd_data   = vram_read2_data;

    // synthesis translate_off
    always_ff @(posedge clk) begin
        if (rst_n && is_t4_emit && gs_tex_rd_en)
            $error("Ch295: read2 arbitration overlap @%0t — PSMT4 RMW (is_t4_emit) and texel fetch (gs_tex_rd_en) both active; one read is being dropped. These are supposed to be mutually exclusive by pixel format.",
                   $time);
        if (rst_n && gs_fb_rd_en && (is_t4_emit || gs_tex_rd_en))
            $error("Brick2a: read2 arbitration overlap @%0t — alpha dest-fb read (gs_fb_rd_en) collides with %s; one read is being dropped. Flat-blend must be mutually exclusive with both.",
                   $time, is_t4_emit ? "PSMT4 RMW" : "texel fetch");
        if (rst_n && gs_z_rd_en && (is_t4_emit || gs_tex_rd_en || gs_fb_rd_en))
            $error("Brick2b: read2 arbitration overlap @%0t — Z-buffer read (gs_z_rd_en) collides with another consumer; one read is being dropped. Z-tested flat sprite must be mutually exclusive with T4/texel/alpha.",
                   $time);
        if (rst_n && clut_ld_busy && (is_t4_emit || gs_tex_rd_en || gs_fb_rd_en || gs_z_rd_en))
            $error("Ch296: read2 arbitration overlap @%0t — CLUT load (clut_ld_busy) collides with a raster-time consumer; one read is being dropped. CLUT load must complete BEFORE the raster scan begins.",
                   $time);
    end
    // synthesis translate_on

    assign vram_read_addr_o = vram_raddr;   // Ch320 — forward PCRTC scanout addr to the wrapper
    logic [31:0] vram_read0_addr;
    assign vram_read0_addr = (CLUT_LOAD_USE_READ0 && clut_ld_busy)
                           ? clut_ld_rd_addr : vram_raddr;

    // Ch157 — vram_bram_stub fed by the normalize_write output for
    // CT32/CT16/T8 (same-cycle) and by the T4 RMW pipe (1-cycle
    // delayed). PCRTC's `vram_read_data` arrives one cycle late vs
    // the legacy combinational read, and gs_pcrtc_stub now consumes
    // it via VRAM_SYNC_READ=1 (Ch158): the data-decode + sync-output
    // pipeline is shifted right by 1 cycle so it lines up with the
    // BRAM's registered output, and the sub-word lane extract
    // resolves the right byte/halfword/nibble inside the 32-bit
    // word for CT16/T8/T4 reads.
    vram_bram_stub #(
        .BYTES        (VRAM_BYTES),
        .ENABLE_READ2 (VRAM_ENABLE_READ2)
    ) u_vram (
        .clk(clk), .rst_n(rst_n),
        .write_en  (vram_we_final),
        .write_addr(vram_waddr_final),
        .write_data(vram_wdata_final),
        .write_be  (vram_wbe_final),
        .read_addr (vram_read0_addr),
        .read_data (vram_rdata),
        .read_valid(),
        .read2_addr(vram_read2_addr),
        .read2_data(vram_read2_data),
        .read2_valid()
    );

    // ---------------------------------------------------------------------
    // gs_pcrtc_stub — PSMCT32 swizzled scanout
    // ---------------------------------------------------------------------
    logic end_of_frame;

    gs_pcrtc_stub #(
        .H_ACTIVE(H_ACTIVE), .H_FRONT(H_FRONT), .H_SYNC(H_SYNC), .H_BACK(H_BACK),
        .V_ACTIVE(V_ACTIVE), .V_FRONT(V_FRONT), .V_SYNC(V_SYNC), .V_BACK(V_BACK),
        .PSMCT32_SWIZZLE   (PSMCT32_SWIZZLE),
        .VRAM_SYNC_READ    (1'b1),
        .STRIP_PCRTC_MAG_DIV(STRIP_PCRTC_MAG_DIV)
    ) u_pcrtc (
        .clk(clk), .rst_n(rst_n),
        .pmode_q       (pmode_q),
        .dispfb1_q     (dispfb1_q),
        .display1_q    (display1_q),
        .vram_read_addr(vram_raddr),
        .vram_read_data(vram_rdata),
        .clut_enable   (1'b0),
        // (vram_read_addr_o forwarded below)
        .clut_csa      (5'd0),
        .clut_read_idx (),
        .clut_read_data(32'd0),
        .hsync(hsync), .vsync(vsync), .de(de),
        .r(r), .g(g), .b(b),
        .pix_window_o(pix_window_o),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    // gs_pcrtc_stub doesn't expose end_of_frame as a port; the Ch123 TB
    // taps it via hierarchical ref. For the top wrapper we synthesize an
    // equivalent edge by watching vsync rise.
    logic vsync_d;
    always_ff @(posedge clk) begin
        if (!rst_n) vsync_d <= 1'b0;
        else        vsync_d <= vsync;
    end
    assign end_of_frame = vsync && !vsync_d;

    // ---------------------------------------------------------------------
    // Sticky status outputs.
    // ---------------------------------------------------------------------
    logic dma_done_seen_q;
    logic frame_seen_q;
    always_ff @(posedge clk) begin
        if (!rst_n) begin
            dma_done_seen_q <= 1'b0;
            frame_seen_q    <= 1'b0;
        end else begin
            if (dmac_ev_valid && (dmac_ev_event == EV_DMA_DONE))
                dma_done_seen_q <= 1'b1;
            if (end_of_frame)
                frame_seen_q <= 1'b1;
        end
    end
    assign dma_done_seen = dma_done_seen_q;
    assign frame_seen    = frame_seen_q;

    // ---------------------------------------------------------------------
    // Ch174 — event toggles for HPS-visible counters. Separate from the
    // sticky status latches above so LED bits / CORE_STATUS keep their
    // existing one-shot semantics.
    // ---------------------------------------------------------------------
    logic frame_toggle_q;
    logic dma_done_toggle_q;
    always_ff @(posedge clk) begin
        if (!rst_n) begin
            frame_toggle_q    <= 1'b0;
            dma_done_toggle_q <= 1'b0;
        end else begin
            if (end_of_frame)
                frame_toggle_q <= ~frame_toggle_q;
            if (dmac_ev_valid && (dmac_ev_event == EV_DMA_DONE))
                dma_done_toggle_q <= ~dma_done_toggle_q;
        end
    end
    assign frame_toggle    = frame_toggle_q;
    assign dma_done_toggle = dma_done_toggle_q;

endmodule : top_psmct32_raster_demo_bram
