ec82764bef
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression (272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps, and all dump-derived textures/traces) is excluded via .gitignore and stays local. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
197 lines
10 KiB
Systemverilog
197 lines
10 KiB
Systemverilog
// ============================================================================
|
|
// gs_texture_cache.sv (Ch322 — PREFILLED texture cache, correctness-first)
|
|
//
|
|
// Proves texture bytes can live in FPGA-private LPDDR4B and be consumed by the
|
|
// GS sampler through an on-chip RAM at the EXISTING 1-cycle texel latency.
|
|
//
|
|
// This is a PREFILLED cache, NOT a demand cache. The whole known texture is
|
|
// filled from LPDDR into an on-chip RAM ONCE, before rendering (mirroring the
|
|
// Ch321 line-buffer trick: warm a bounded buffer, then serve at native latency).
|
|
// Every sampler read is therefore a HIT at 1-cycle latency — the nearest-path
|
|
// sampler's fixed-latency contract (gs_texture_unit valid_pipe, RD_LATENCY=1) is
|
|
// preserved with ZERO pipeline/stall surgery. Demand miss/stall is explicitly
|
|
// DEFERRED to a later chapter (it would be a raster-walker pipeline redesign).
|
|
//
|
|
// Two clock domains (same split as gs_lpddr_scanout_lb):
|
|
// axi_clk (emif_clk) — fill FSM: single-beat 256-bit reads (arlen=0, the
|
|
// only read pattern proven on this EMIF) from LPDDR.
|
|
// sample_clk (design) — the sampler's texel read port; 1-cycle registered,
|
|
// byte-identical timing to the vram_bram_stub read2.
|
|
//
|
|
// The fill is one-shot before raster, so the on-chip RAM is static when the
|
|
// sampler reads it (write side idle) — a plain dual-clock simple-dual-port RAM,
|
|
// no read/write CDC hazard. `fill_done` is 2-FF synced into sample_clk so the
|
|
// read mux only goes live after the texture is fully resident.
|
|
// ============================================================================
|
|
`timescale 1ns/1ps
|
|
|
|
module gs_texture_cache #(
|
|
parameter [29:0] LPDDR_TEX_BASE = 30'd0, // byte base of the texture in LPDDR4B
|
|
parameter [31:0] TEX_VRAM_BASE = 32'd2048,// VRAM byte base the sampler addresses (TBP0*256)
|
|
parameter int TEX_BYTES = 256, // texture size in bytes (8x8 PSMCT32 = 256)
|
|
parameter int N_BEATS = 8 // TEX_BYTES / 32
|
|
)(
|
|
// ---- AXI read clock domain (emif_clk) — fill side ----
|
|
input logic axi_clk,
|
|
input logic axi_rst_n,
|
|
input logic fill_start, // TOGGLE (bridge domain, CDC-synced): each edge (re)fills
|
|
output logic fill_done, // texture fully resident (until the next fill arm)
|
|
output logic [31:0] fill_beats, // beats completed (cumulative)
|
|
output logic [31:0] fill_bytes, // bytes filled (cumulative)
|
|
output logic [31:0] rd_errs, // non-OKAY read responses (cumulative)
|
|
output logic [31:0] fill_crc, // Ch352 — sum32 of EVERY 32-bit word actually written into tex_mem. The
|
|
// host compares this to the file's sum32 to PROVE tex_mem integrity on
|
|
// silicon (the LPDDR readback only proves LPDDR, not the cache contents).
|
|
|
|
// ---- AXI4 read channel to the EMIF user port (axi_clk, 256-bit) ----
|
|
output logic [29:0] araddr,
|
|
output logic [1:0] arburst,
|
|
output logic [6:0] arid,
|
|
output logic [7:0] arlen,
|
|
output logic [2:0] arsize,
|
|
output logic arvalid,
|
|
input logic arready,
|
|
input logic [255:0] rdata,
|
|
input logic [1:0] rresp,
|
|
input logic rlast,
|
|
input logic rvalid,
|
|
output logic rready,
|
|
|
|
// ---- sampler clock domain (design_clk) — texel read port ----
|
|
input logic sample_clk,
|
|
input logic tex_rd_en, // sampler issues a texel read this cycle
|
|
input logic [31:0] tex_rd_addr, // VRAM byte address (TEX_VRAM_BASE + offset)
|
|
output logic [31:0] tex_rd_data, // 1-cycle REGISTERED texel word (matches read2)
|
|
output logic tex_ready // fill_done synced into sample_clk (mux gate)
|
|
);
|
|
localparam int TEX_WORDS = TEX_BYTES/4; // 32-bit words in the cache
|
|
localparam int WIDX_BITS = (TEX_WORDS <= 1) ? 1 : $clog2(TEX_WORDS);
|
|
localparam int BIDX_BITS = (N_BEATS <= 1) ? 1 : $clog2(N_BEATS);
|
|
|
|
assign arburst = 2'b01; // INCR
|
|
assign arid = 7'd4; // distinct id: writer=0, probe=1, frame-cache=2, line-buf=3, tex-fill=4
|
|
assign arlen = 8'd0; // SINGLE-BEAT (the only AXI read pattern proven on this EMIF)
|
|
assign arsize = 3'b101; // 32 bytes
|
|
|
|
// On-chip texture RAM: written by the fill FSM (axi_clk), read by the sampler
|
|
// (sample_clk). One-shot warm fill => static during reads => no CDC hazard.
|
|
//
|
|
// One ordinary 32-bit simple-dual-port RAM. History of cache geometry vs Quartus Place:
|
|
// flat 8-write array -> exploded to flops (707k ALUT)
|
|
// 8x 8192x32 banks -> width fragmentation, 344/358, 9h Place thrash
|
|
// 1x 8192x256 RAM -> count OK (320/358) but one ~104-M20K macro too WIDE -> Place 40min+, no QDB
|
|
// 2x 8192x128 halves -> still rigid, Place 50min+ no progress
|
|
// 4x 8192x64 banks -> 328/358; Place still stalled with ample RAM after read2 was removed.
|
|
// 1x 65536x32 (HERE) -> latch each AXI beat, drain 8 lanes over 8 axi_clk cycles.
|
|
// Serializing the fill removes the multi-bank/multi-write geometry while preserving the sampler's
|
|
// one-cycle registered 32-bit read. The one-shot fill is still tiny compared with board startup.
|
|
(* ramstyle = "M20K" *) logic [31:0] tex_mem [0:TEX_WORDS-1];
|
|
|
|
// ================= fill side (axi_clk) =================
|
|
typedef enum logic [2:0] { F_IDLE, F_AR, F_R, F_DRAIN, F_DONE } fstate_t;
|
|
fstate_t fst;
|
|
logic [$clog2(N_BEATS):0] beat; // 0..N_BEATS
|
|
logic [255:0] fill_data_q;
|
|
logic [2:0] fill_lane;
|
|
logic [WIDX_BITS-1:0] fill_word_base;
|
|
wire [WIDX_BITS-1:0] fill_word_idx = fill_word_base + WIDX_BITS'(fill_lane);
|
|
// fill_start is an EDGE/TOGGLE (bridge toggles it on each arm), CDC-synced here so the
|
|
// cache is RE-FILLABLE: each arm reloads the texture (lets the HPS re-stage a different
|
|
// texture without a board reset). 3-FF sync + edge-detect, like the read/write probes.
|
|
logic [2:0] fs_sync;
|
|
wire fs_edge = (fs_sync[2] != fs_sync[1]);
|
|
|
|
always_ff @(posedge axi_clk) begin
|
|
if (!axi_rst_n) begin
|
|
fst <= F_IDLE; araddr <= '0; arvalid <= 1'b0; rready <= 1'b0;
|
|
beat <= '0; fill_done <= 1'b0; fill_beats <= 32'd0; fill_bytes <= 32'd0;
|
|
rd_errs <= 32'd0; fs_sync <= 3'd0; fill_data_q <= '0;
|
|
fill_lane <= 3'd0; fill_word_base <= '0; fill_crc <= 32'd0;
|
|
end else begin
|
|
fs_sync <= {fs_sync[1:0], fill_start};
|
|
case (fst)
|
|
F_IDLE: begin
|
|
if (fs_edge) begin
|
|
araddr <= LPDDR_TEX_BASE;
|
|
beat <= '0;
|
|
fill_done <= 1'b0; // re-arm: drop ready until reloaded
|
|
fill_beats <= 32'd0;
|
|
fill_bytes <= 32'd0;
|
|
rd_errs <= 32'd0;
|
|
fill_crc <= 32'd0; // Ch352 — restart the tex_mem integrity sum for this fill
|
|
arvalid <= 1'b1;
|
|
fst <= F_AR;
|
|
end
|
|
end
|
|
F_AR: begin
|
|
if (arready) begin
|
|
arvalid <= 1'b0;
|
|
rready <= 1'b1;
|
|
fst <= F_R;
|
|
end
|
|
end
|
|
F_R: begin
|
|
if (rvalid) begin
|
|
// Capture the AXI beat, then issue one M20K-native 32-bit write per cycle.
|
|
fill_data_q <= rdata;
|
|
fill_lane <= 3'd0;
|
|
fill_word_base <= {beat[BIDX_BITS-1:0], 3'b000};
|
|
if (rresp != 2'b00) rd_errs <= rd_errs + 32'd1;
|
|
rready <= 1'b0;
|
|
fst <= F_DRAIN;
|
|
end
|
|
end
|
|
F_DRAIN: begin
|
|
tex_mem[fill_word_idx] <= fill_data_q[fill_lane*32 +: 32];
|
|
fill_crc <= fill_crc + fill_data_q[fill_lane*32 +: 32]; // sum32 over the words written
|
|
if (fill_lane == 3'd7) begin
|
|
fill_beats <= fill_beats + 32'd1;
|
|
fill_bytes <= fill_bytes + 32'd32;
|
|
if (beat == N_BEATS-1) begin
|
|
fill_done <= 1'b1;
|
|
fst <= F_DONE;
|
|
end else begin
|
|
beat <= beat + 1'b1;
|
|
araddr <= araddr + 30'd32; // next single-beat read
|
|
arvalid <= 1'b1;
|
|
fst <= F_AR;
|
|
end
|
|
end else begin
|
|
fill_lane <= fill_lane + 3'd1;
|
|
end
|
|
end
|
|
F_DONE: begin
|
|
// resident until the next arm. A fresh fill_start edge re-loads the
|
|
// texture (e.g. HPS re-stages a different one) — start a new fill.
|
|
if (fs_edge) begin
|
|
araddr <= LPDDR_TEX_BASE;
|
|
beat <= '0;
|
|
fill_done <= 1'b0;
|
|
fill_beats <= 32'd0;
|
|
fill_bytes <= 32'd0;
|
|
rd_errs <= 32'd0;
|
|
fill_crc <= 32'd0; // Ch352 — restart the tex_mem integrity sum for this fill
|
|
arvalid <= 1'b1;
|
|
fst <= F_AR;
|
|
end
|
|
end
|
|
default: fst <= F_IDLE;
|
|
endcase
|
|
end
|
|
end
|
|
|
|
// ================= sampler side (sample_clk) =================
|
|
// 1-cycle REGISTERED read, identical timing to vram_bram_stub.read2:
|
|
// present (tex_rd_addr) when tex_rd_en, data lands next cycle.
|
|
wire [31:0] word_off = (tex_rd_addr - TEX_VRAM_BASE) >> 2;
|
|
wire [WIDX_BITS-1:0] rd_word = word_off[WIDX_BITS-1:0];
|
|
always_ff @(posedge sample_clk) begin
|
|
if (tex_rd_en) tex_rd_data <= tex_mem[rd_word];
|
|
end
|
|
|
|
// fill_done -> sample_clk (2-FF). The read mux only goes live once warm.
|
|
logic [1:0] done_sync;
|
|
always_ff @(posedge sample_clk) done_sync <= {done_sync[0], fill_done};
|
|
assign tex_ready = done_sync[1];
|
|
endmodule
|