// retroDE_ps2 — vram_stub (Ch89) // // Linear byte-addressable VRAM backing store for gs_stub's // `raster_pixel_emit` channel. This is the FIRST persistence // layer the rasterizer has had — pre-Ch89, pixels only pulsed as // trace-visible events and updated `raster_pixel_color_q` / // `raster_pixel_fb_addr_q` snapshot regs, then evaporated. Now // they actually land somewhere a TB (or a future scanout path) // can read back. // // Scope (intentionally minimal for Ch89): // - Linear byte-addressable: NO page/block VRAM swizzle. Real // PS2 VRAM is 4 MiB, organized into pages × blocks × columns // per PSM. The fb_addr math in gs_stub matches the linear- // framebuffer layout that PCSX2's gs_state pages out for // "linear" PSM channels; that's what this stub speaks. // - PSMCT32 only: writes 4 bytes per emitted pixel. PSMCT16 // (2 bytes) and PSMT8 (1 byte) are deferred until a future // chapter exposes per-pixel PSM at the raster channel. // - Combinational debug read port: byte-addressable, returns // the 4 bytes starting at read_addr packed little-endian. // For TBs to verify pixel storage; not on any hardware path. // // Wiring contract: // - write_en ← gs_stub.raster_pixel_emit // - write_addr ← gs_stub.raster_pixel_fb_addr_q // - write_data ← gs_stub.raster_pixel_color_q[31:0] (lower 32 bits) // - write_be ← gs_stub.raster_pixel_be_q (Ch95) // // The full 64-bit raster_pixel_color_q carries Q (texture-coord // IEEE float) in the upper 32 bits — those bits are NOT part of // the framebuffer pixel and are deliberately discarded here. // // `write_be[3:0]` (Ch95): per-byte write enable. byte i (the // byte at `write_addr + i`) is committed only when // `write_en && write_be[i]`. PSMCT32 writes use 4'b1111; // PSMCT16 writes use 4'b0011 (the 2 bytes at write_addr — gs_stub // passes the actual byte address of the pixel, which is // 2-byte-aligned but not necessarily 4-byte-aligned). TBs that // bypass gs_stub (e.g. `tb_vram_stub`, `tb_gs_scanout_psm16`) // tie write_be to 4'b1111. // // `write_mask[31:0]` (Ch106): per-BIT merge mask used to support // sub-byte writes (PSMT4 — 4-bit nibble per pixel). The committed // byte i (still gated by write_be[i]) is: // mem[addr+i] <= (mem[addr+i] & ~mask_i) | (data_i & mask_i) // where mask_i = write_mask[i*8 +: 8] and data_i = // write_data[i*8 +: 8]. PSMCT32/16 + PSMT8 writes tie write_mask // to 32'hFFFFFFFF (full byte writes — equivalent to the pre-Ch106 // behavior). PSMT4 emits use 0x0F (low nibble) or 0xF0 (high // nibble) on the enabled byte. The merge happens inside the same // always_ff that commits the byte, so back-to-back nibble writes // to the SAME byte chain cleanly through NBA semantics: the // second write samples mem[addr] AFTER the prior NBA committed. // // Bounds check (Ch95 audit-medium fix): the write is admitted // only if EVERY enabled byte's address is in [0, BYTES). This // uses non-wrapping 33-bit arithmetic so a write near the 32-bit // address space limit (e.g. write_addr near 0xFFFF_FFFC with // be=4'b1111) is rejected cleanly. Halfword writes at the last // valid 2-byte slot (write_addr=BYTES-2 with be=4'b0011) are // accepted; write_addr=BYTES-1 with be=4'b0011 is rejected // because byte 1 of that slot is OOB. `timescale 1ns/1ps module vram_stub #( parameter int unsigned BYTES = 65536 ) ( input logic clk, input logic rst_n, // Write side: one 32-bit pixel slot per cycle when write_en // pulses. write_addr is a byte offset (already PSM-aware via // gs_stub's bpp_shift math). write_be[i] gates byte i — used // by Ch95 to commit just the 2 bytes of a PSMCT16 pixel // without stomping the adjacent halfword. input logic write_en, input logic [31:0] write_addr, input logic [31:0] write_data, input logic [3:0] write_be, input logic [31:0] write_mask, // Debug read port: combinational, byte-addressable, little- // endian 4-byte read. Used by gs_pcrtc_stub for scanout, and // by TBs for verification. input logic [31:0] read_addr, output logic [31:0] read_data, // Ch99 — second combinational read port for clients that // need to read VRAM concurrently with pcrtc scanout (the // canonical example is `clut_loader_stub`, which copies // CLUT bytes from VRAM into clut_stub when TEX0.CLD fires). // Same byte-addressed 4-byte semantics as port 0. Tie // `read2_addr` to 0 in TBs that don't use it; the unused // `read2_data` output can be left unconnected. input logic [31:0] read2_addr, output logic [31:0] read2_data ); logic [7:0] mem [0:BYTES-1]; // Largest base address that admits a 4-byte access without // overrunning the array. Used by the READ port (always 4 // bytes). The write port now does per-byte admission below // (Ch95 audit-medium fix) so it can accept halfword writes // near the end of VRAM that the old `addr <= MAX_BASE` gate // would have spuriously dropped. localparam logic [31:0] MAX_BASE = (BYTES >= 4) ? (32'(BYTES) - 32'd4) : 32'd0; // Sim-only memory init. Real Altera/Intel BRAM is power-on-zero // on FPGA configuration, so the procedural loop is unnecessary // in synthesis — and at BYTES=8192 it exceeds Quartus's 5000- // iteration synthesizable-loop limit (Quartus error 13356). // The pragma pair tells Quartus to skip this initial block; // iverilog and other simulators ignore the pragma and run the // init normally so time-0 values are deterministic in sim. // synthesis translate_off initial begin if (BYTES < 4) $error("vram_stub: BYTES (%0d) must be >= 4", BYTES); for (int i = 0; i < BYTES; i++) mem[i] = 8'd0; end // synthesis translate_on always_comb begin if (read_addr <= MAX_BASE) begin read_data = {mem[read_addr + 32'd3], mem[read_addr + 32'd2], mem[read_addr + 32'd1], mem[read_addr]}; end else begin read_data = 32'd0; end end always_comb begin if (read2_addr <= MAX_BASE) begin read2_data = {mem[read2_addr + 32'd3], mem[read2_addr + 32'd2], mem[read2_addr + 32'd1], mem[read2_addr]}; end else begin read2_data = 32'd0; end end // Per-byte admission. We use non-wrapping 33-bit arithmetic // for `write_addr + i` so a near-0xFFFFFFFF address can't // wrap and falsely pass the comparison. An enabled byte is // admitted only if its byte address is strictly less than // BYTES; the entire write is dropped if ANY enabled byte // would land out of range, matching the Ch89-audit "no // partial writes near the boundary" stance. logic [32:0] addr33; logic admit_b0, admit_b1, admit_b2, admit_b3; logic write_admit; assign addr33 = {1'b0, write_addr}; assign admit_b0 = (addr33 + 33'd0) < 33'(BYTES); assign admit_b1 = (addr33 + 33'd1) < 33'(BYTES); assign admit_b2 = (addr33 + 33'd2) < 33'(BYTES); assign admit_b3 = (addr33 + 33'd3) < 33'(BYTES); assign write_admit = write_en && (!write_be[0] || admit_b0) && (!write_be[1] || admit_b1) && (!write_be[2] || admit_b2) && (!write_be[3] || admit_b3); always_ff @(posedge clk) begin if (rst_n && write_admit) begin if (write_be[0]) mem[write_addr] <= (mem[write_addr] & ~write_mask[7:0]) | (write_data[7:0] & write_mask[7:0]); if (write_be[1]) mem[write_addr + 32'd1] <= (mem[write_addr + 32'd1] & ~write_mask[15:8]) | (write_data[15:8] & write_mask[15:8]); if (write_be[2]) mem[write_addr + 32'd2] <= (mem[write_addr + 32'd2] & ~write_mask[23:16]) | (write_data[23:16] & write_mask[23:16]); if (write_be[3]) mem[write_addr + 32'd3] <= (mem[write_addr + 32'd3] & ~write_mask[31:24]) | (write_data[31:24] & write_mask[31:24]); end end endmodule : vram_stub