// retroDE_ps2 — gif_image_xfer_stub (Ch110) // // Host→local image-transfer engine. On a TRXDIR write that arms a // host→local upload (XDIR == 0), the engine snapshots the // already-latched BITBLTBUF / TRXPOS / TRXREG fields and consumes // IMAGE-mode quadwords from gif_packed_stub, unpacking them into // per-pixel VRAM writes at the destination region defined by // (DBP, DBW, DPSM, DSAX, DSAY, RRW, RRH). // // Scope (after Ch139): // - PSMCT32 (DPSM == 6'h00): 4 bytes/pixel, 4 pixels/qword, // row_stride = DBW * 256, write_be = 4'b1111, mask=0xFFFFFFFF. // - PSMCT16 (DPSM == 6'h02): 2 bytes/pixel, 8 pixels/qword, // row_stride = DBW * 128, write_be = 4'b0011, mask=0xFFFFFFFF. // - PSMT8 (DPSM == 6'h13): 1 byte/pixel (an 8-bit CLUT index), // 16 pixels/qword, row_stride = DBW * 64, write_be = 4'b0001, // mask = 0xFFFFFFFF. // - PSMT4 (DPSM == 6'h14): 0.5 bytes/pixel (a 4-bit CLUT index), // 32 pixels/qword (2 px/byte × 16 bytes), row_stride = DBW * 32, // write_be = 4'b0001 with a per-emit nibble mask: 0x0000_000F // for the LOW nibble of the byte (when (DSAX+x) is even) or // 0x0000_00F0 for the HIGH nibble (when (DSAX+x) is odd). The // 4-bit index sits at the matching nibble position in // write_data[7:0]; vram_stub's per-bit merge commits exactly // that nibble — the OTHER nibble of the same byte is preserved. // Back-to-back emits to the same byte (e.g. x=0 + x=1 of the // same row) chain through NBA semantics without bypass logic // (same trick the raster channel uses since Ch106). // - Other PSMs (PSMCT24/PSMZ-*): the engine still consumes // IMAGE qwords (so gif_packed_stub doesn't desync) but emits // zero VRAM writes. Lane cadence falls back to PSMCT32 // (4 lanes/qword). // - Addressing: linear by DEFAULT — the destination address // math is // dest_base = DBP * 256 // row_stride = DBW * 64 * bpp // addr(x, y) = dest_base + (DSAY + y) * row_stride // + (DSAX + x) * bpp // Four OPTIONAL per-PSM swizzle paths gated by parameters: // `PSMCT32_SWIZZLE=1` (Ch121) routes PSMCT32 uploads through // gs_swizzle_psmct32_stub; `PSMCT16_SWIZZLE=1` (Ch127) routes // PSMCT16 uploads through gs_swizzle_psmct16_stub; // `PSMT8_SWIZZLE=1` (Ch133) routes PSMT8 uploads through // gs_swizzle_psmt8_stub (page=128×64 px, bw_pg=DBW>>1 — DBW // must be even for PSMT8); `PSMT4_SWIZZLE=1` (Ch139) routes // PSMT4 uploads through gs_swizzle_psmt4_stub (page=128×128 // px, bw_pg=DBW>>1 — DBW must be even for PSMT4 too; module // also outputs nibble_hi selector since PSMT4 packs 2 pixels // per byte). In all four cases the per-pixel byte address is // `dest_base + swizzle(FBP=0, FBW=DBW, x=DSAX+cur_x, // y=DSAY+cur_y)`. The PSMT4 path additionally uses the // swizzle's `nibble_hi` output (instead of the linear // formula's x_eff[0]) to pick which nibble of the byte gets // the upload's 4-bit pixel — the existing Ch118 nibble RMW // write-mask machinery (write_be=4'b0001, write_mask= // 0x0F or 0xF0) layers on top of the swizzled byte address. // The four parameters are independent. All four parameter // defaults are 0 → legacy linear behavior. // - One pending qword buffer + a 5-bit lane counter (0..3 for // PSMCT32, 0..7 for PSMCT16, 0..15 for PSMT8, 0..31 for // PSMT4; the last-lane index is snapshotted at TRXDIR-arm // time per `lane_last_q`). // Backpressure to the upstream is exposed via `data_ready`. // Wired into `gif_packed_stub.image_data_ready` (Ch110), so // the GIF gates `in_ready` only in S_IMAGE state with FLG=2; // the DMAC's ep_ready follows gif_in_ready directly. Outside // S_IMAGE the gate is a no-op. // // Wiring contract (TB-level): // trxdir_wr_pulse ← gs_stub.trxdir_wr_q // trxdir ← gs_stub.trxdir_q // bitbltbuf ← gs_stub.bitbltbuf_q // trxpos ← gs_stub.trxpos_q // trxreg ← gs_stub.trxreg_q // data_valid ← gif_packed_stub.image_data_valid // data_qword ← gif_packed_stub.image_data // data_last ← gif_packed_stub.image_data_last // data_ready → gif_packed_stub.image_data_ready (Ch110). // The GIF FSM uses it to gate in_ready only in // S_IMAGE+FLG=2; dmac.ep_ready follows // gif.in_ready directly (no TB-level AND). // vram_we / waddr / wdata / wbe / wmask → muxed into vram_stub's // write port (the TB selects between the engine, the raster // channel, and any TB-direct path). // busy → high while a transfer is active (between trxdir_wr arm // and the last lane emit). TB uses this for the vram_stub // write-port mux. // // What this stub does NOT do: // - Source-direction (local→host or local→local) transfers. // - PSMCT24 / PSMZ-* image transfers (not currently exercised // in the demo flow). // - Mid-transfer TRXDIR re-arm or interleaving with REGLIST. // - HWREG-side legacy/non-PSM-aware swizzle (out of scope — // PSMCT32 since Ch121, PSMCT16 since Ch127, PSMT8 since // Ch133, PSMT4 since Ch139 all support the canonical PCSX2 // swizzle behind their respective parameter gates). // - HWREG via privileged-MMIO (the real PS2 path that reads // pixel data through the privileged HWREG register at // 0x12001000); IMAGE-mode GIF qwords are the only data // source modelled here. `timescale 1ns/1ps module gif_image_xfer_stub import trace_pkg::*; #( // Ch121 — when set, PSMCT32 uploads compute the per-pixel VRAM // byte address via the real PS2 GS page/block swizzle // (gs_swizzle_psmct32_stub) instead of the legacy linear formula // `dest_base + (DSAY+y)*row_stride + (DSAX+x)*4`. Other PSMs // are not affected by this parameter — PSMCT16 has its own // gate (PSMCT16_SWIZZLE, Ch127), PSMT8 has PSMT8_SWIZZLE // (Ch133), PSMT4 has PSMT4_SWIZZLE (Ch139, see below). // Default 0 keeps every existing PSMCT32 image-xfer TB on // the original linear addressing — its expectations don't // change. parameter bit PSMCT32_SWIZZLE = 1'b0, // Ch127 — when set, PSMCT16 uploads compute the per-pixel VRAM // byte address via the canonical PS2 GS page/block/column // swizzle (gs_swizzle_psmct16_stub) instead of the legacy // linear formula `dest_base + (DSAY+y)*row_stride + // (DSAX+x)*2`. PSMCT32 / PSMT8 / PSMT4 are governed by their // own gates (PSMCT32_SWIZZLE / PSMT8_SWIZZLE / PSMT4_SWIZZLE). // Default 0 keeps every existing PSMCT16 image-xfer // TB on the legacy linear path. Mirrors the Ch126 PCRTC // read-side wiring at the upload write side, completing // the second integration point for the Ch125 PSMCT16 // primitive. parameter bit PSMCT16_SWIZZLE = 1'b0, // Ch133 — when set, PSMT8 uploads compute the per-pixel VRAM // byte address via the canonical PS2 GS page/block/column // swizzle (gs_swizzle_psmt8_stub) instead of the legacy // linear formula `dest_base + (DSAY+y)*row_stride + // (DSAX+x)*1`. PSMT8 pages are 128 px wide so the swizzle // internally uses `bw_pg = DBW >> 1` — PCSX2 asserts DBW must // be even for PSMT8 at GSLocalMemory.h:553. PSMCT32 / PSMCT16 // / PSMT4 are governed by their own gates. // Default 0 keeps every existing PSMT8 image-xfer TB // (Ch117 PSMT8, Ch107 PSMT4-via-CT16-CLUT palette path) on // the legacy linear addressing. Mirrors the Ch132 PCRTC // read-side wiring at the upload write side, completing // the second integration point for the Ch131 PSMT8 primitive. parameter bit PSMT8_SWIZZLE = 1'b0, // Ch139 — when set, PSMT4 uploads compute the per-pixel VRAM // byte address via the canonical PS2 GS page/block/column // swizzle (gs_swizzle_psmt4_stub) instead of the legacy // linear formula `dest_base + (DSAY+y)*row_stride + // (DSAX+x)*0.5`. PSMT4 pages are 128 px wide AND 128 px tall; // the swizzle internally uses `bw_pg = DBW >> 1` — PCSX2 // asserts DBW must be even for PSMT4 at GSLocalMemory.h:560. // The PSMT4 swizzle module also outputs a `nibble_hi` // selector that picks which nibble of the byte at the // swizzled address holds this pixel — the linear formula's // x_eff[0] selector is wrong under the swizzled layout // because the canonical PCSX2 column table reorders nibbles // within a block. The existing Ch118 nibble RMW machinery // (write_be=4'b0001 + write_mask 0x0F or 0xF0) layers on top // of the swizzled byte address: the mask is selected by the // swizzle's nibble_hi when this gate is on, instead of by // x_eff[0]. PSMCT32 / PSMCT16 / PSMT8 are governed by their // own gates. Default 0 keeps every existing PSMT4 image-xfer // TB (Ch118 PSMT4, Ch107 PSMT4-e2e palette path) on the // legacy linear addressing. Mirrors the Ch138 PCRTC // read-side wiring at the upload write side, completing the // second integration point for the Ch137 PSMT4 primitive. parameter bit PSMT4_SWIZZLE = 1'b0 )( input logic clk, input logic rst_n, // Arm input — pulses for one cycle on TRXDIR commit. input logic trxdir_wr_pulse, input logic [63:0] trxdir, input logic [63:0] bitbltbuf, input logic [63:0] trxpos, input logic [63:0] trxreg, // IMAGE qword stream from gif_packed_stub. input logic data_valid, input logic [127:0] data_qword, input logic data_last, output logic data_ready, // VRAM write port. PSM-aware be + per-bit merge mask: // PSMCT32 (Ch110): be = 4'b1111, mask = 0xFFFFFFFF. // PSMCT16 (Ch116): be = 4'b0011, mask = 0xFFFFFFFF. // PSMT8 (Ch117): be = 4'b0001, mask = 0xFFFFFFFF. // PSMT4 (Ch118): be = 4'b0001, mask = 0x0000_000F (low // nibble) or 0x0000_00F0 (high nibble), // keyed by (DSAX+x)[0]. The 4-bit index // sits at the matching nibble position in // write_data[7:0]; vram_stub merges only // the targeted nibble. output logic vram_we, output logic [31:0] vram_waddr, output logic [31:0] vram_wdata, output logic [3:0] vram_wbe, output logic [31:0] vram_wmask, // Engine status. output logic busy ); // BITBLTBUF field decode (real PS2 layout, per PCSX2 GSRegs.h): // [13:0] SBP // [21:16] SBW // [29:24] SPSM // [45:32] DBP // [53:48] DBW // [61:56] DPSM logic [13:0] dbp; logic [5:0] dbw; logic [5:0] dpsm; assign dbp = bitbltbuf[45:32]; assign dbw = bitbltbuf[53:48]; assign dpsm = bitbltbuf[61:56]; // TRXPOS field decode: // [10:0] SSAX // [26:16] SSAY // [42:32] DSAX // [58:48] DSAY // [60:59] DIR logic [10:0] dsax; logic [10:0] dsay; assign dsax = trxpos[42:32]; assign dsay = trxpos[58:48]; // TRXREG field decode: // [11:0] RRW // [43:32] RRH logic [11:0] rrw; logic [11:0] rrh; assign rrw = trxreg[11:0]; assign rrh = trxreg[43:32]; // TRXDIR field decode (XDIR is bits [1:0]). logic [1:0] xdir; assign xdir = trxdir[1:0]; // Snapshotted transfer parameters (latched at trxdir_wr arm). logic [13:0] dbp_q; logic [5:0] dbw_q; logic [5:0] dpsm_q; logic [10:0] dsax_q; logic [10:0] dsay_q; logic [11:0] rrw_q; logic [11:0] rrh_q; logic [31:0] dest_base_q; // DBP * 256 (bytes) logic [31:0] row_stride_q; // DBW * 64 * bpp logic psmct32_q; // DPSM == 0x00 → 4 bytes/pixel logic psmct16_q; // DPSM == 0x02 → 2 bytes/pixel (Ch116) logic psmt8_q; // DPSM == 0x13 → 1 byte/pixel (Ch117) logic psmt4_q; // DPSM == 0x14 → 0.5 byte/pixel (Ch118) // Last-lane index for the current PSM (3 for PSMCT32 → 4 // lanes, 7 for PSMCT16 → 8 lanes, 15 for PSMT8 → 16 lanes, // 31 for PSMT4 → 32 lanes). // Other PSMs use the PSMCT32 cadence (3) for silent consume. logic [4:0] lane_last_q; // Per-emit progression: which qword (0..NLOOP-1) and which // lane within the qword (0..3 for PSMCT32, 0..7 for PSMCT16, // 0..15 for PSMT8, 0..31 for PSMT4). logic [127:0] qword_q; logic [4:0] lane_q; // widened to 5 bits for PSMT4 logic lane_valid_q; // a buffered qword is being drained // Pixel cursor (cur_x, cur_y) within the destination rect, // measured from (DSAX, DSAY). Wrap at RRW. logic [11:0] cur_x_q; logic [11:0] cur_y_q; logic [23:0] pix_total_q; // RRW * RRH (cap 16M) logic [23:0] pix_done_q; // FSM. typedef enum logic [1:0] { S_IDLE = 2'd0, S_RUN = 2'd1 } state_e; state_e state; assign busy = (state == S_RUN); // The engine is "ready" for a new qword when no qword is // currently being drained. In S_IDLE we admit qwords too — // upstream image_data_valid won't pulse outside an active // S_IMAGE state, so this is benign. assign data_ready = !lane_valid_q; // Combinational pixel address for the in-flight lane. // PSMCT32: addr = dest_base + (DSAY+cur_y) * row_stride // + (DSAX+cur_x) * 4 // PSMCT16: addr = dest_base + (DSAY+cur_y) * row_stride // + (DSAX+cur_x) * 2 // PSMT8 : addr = dest_base + (DSAY+cur_y) * row_stride // + (DSAX+cur_x) * 1 // PSMT4 : addr = dest_base + (DSAY+cur_y) * row_stride // + ((DSAX+cur_x) >> 1) // nibble = (DSAX+cur_x)[0] high vs low // (row_stride already encodes the bpp factor.) logic [31:0] cur_addr_c; logic [31:0] cur_data_c; logic [3:0] cur_be_c; logic [31:0] cur_mask_c; always_comb begin logic [31:0] x_off; logic [11:0] x_eff; logic [3:0] t4_nibble; x_eff = dsax_q + cur_x_q; if (psmt4_q) x_off = {21'd0, x_eff[11:1]}; // (x_eff >> 1) else if (psmt8_q) x_off = ({20'd0, dsax_q} + {20'd0, cur_x_q}); else if (psmct16_q) x_off = ({20'd0, dsax_q} + {20'd0, cur_x_q}) * 32'd2; else x_off = ({20'd0, dsax_q} + {20'd0, cur_x_q}) * 32'd4; cur_addr_c = dest_base_q + (32'(dsay_q) + 32'(cur_y_q)) * row_stride_q + x_off; // PSMT4: extract the 4-bit nibble at lane_q from qword_q. // qword[lane*4 +: 4] for lane in 0..31. iverilog 12 supports // indexed part-select with variable base + constant width. t4_nibble = qword_q[(5'(lane_q) * 4) +: 4]; if (psmt4_q) begin // 32 PSMT4 pixels per qword (2 px/byte × 16 bytes). // Place the 4-bit index at the matching nibble position // in write_data[7:0] keyed by the nibble selector. // Linear (PSMT4_SWIZZLE=0): x_eff[0] is the selector // (low nibble = even pixel, high = odd pixel). // Swizzled (Ch139, PSMT4_SWIZZLE=1): the swizzle module // outputs `nibble_hi` directly — required because the // canonical PCSX2 columnTable4 reorders nibbles within // a block, so x_eff[0] is no longer correct. write_be // is 4'b0001 (single-byte commit) and write_mask // gates the targeted nibble; vram_stub merges only // that nibble, preserving the OTHER nibble of the // same byte. logic psmt4_nibble_select; psmt4_nibble_select = PSMT4_SWIZZLE ? swizzle4_nibble_hi : x_eff[0]; if (psmt4_nibble_select) begin cur_data_c = {24'd0, t4_nibble, 4'd0}; // high nibble cur_mask_c = 32'h0000_00F0; end else begin cur_data_c = {24'd0, 4'd0, t4_nibble}; // low nibble cur_mask_c = 32'h0000_000F; end cur_be_c = 4'b0001; end else if (psmt8_q) begin // 16 PSMT8 pixels per qword. Place the 8-bit index in // the LOW byte of write_data; vram_stub's per-byte BE // commits exactly 1 byte at the exact pixel address // (write_addr = cur_addr_c) at any byte alignment. cur_mask_c = 32'hFFFF_FFFF; unique case (lane_q[3:0]) 4'd0: cur_data_c = {24'd0, qword_q[ 7: 0]}; 4'd1: cur_data_c = {24'd0, qword_q[ 15: 8]}; 4'd2: cur_data_c = {24'd0, qword_q[ 23: 16]}; 4'd3: cur_data_c = {24'd0, qword_q[ 31: 24]}; 4'd4: cur_data_c = {24'd0, qword_q[ 39: 32]}; 4'd5: cur_data_c = {24'd0, qword_q[ 47: 40]}; 4'd6: cur_data_c = {24'd0, qword_q[ 55: 48]}; 4'd7: cur_data_c = {24'd0, qword_q[ 63: 56]}; 4'd8: cur_data_c = {24'd0, qword_q[ 71: 64]}; 4'd9: cur_data_c = {24'd0, qword_q[ 79: 72]}; 4'd10: cur_data_c = {24'd0, qword_q[ 87: 80]}; 4'd11: cur_data_c = {24'd0, qword_q[ 95: 88]}; 4'd12: cur_data_c = {24'd0, qword_q[103: 96]}; 4'd13: cur_data_c = {24'd0, qword_q[111:104]}; 4'd14: cur_data_c = {24'd0, qword_q[119:112]}; default: cur_data_c = {24'd0, qword_q[127:120]}; endcase cur_be_c = 4'b0001; end else if (psmct16_q) begin // 8 PSMCT16 pixels per qword. Place the 16-bit value // in the LOW halfword of write_data; vram_stub's per- // byte BE commits exactly 2 bytes at the 2-byte- // aligned pixel address (write_addr = cur_addr_c). cur_mask_c = 32'hFFFF_FFFF; unique case (lane_q[2:0]) 3'd0: cur_data_c = {16'd0, qword_q[ 15: 0]}; 3'd1: cur_data_c = {16'd0, qword_q[ 31: 16]}; 3'd2: cur_data_c = {16'd0, qword_q[ 47: 32]}; 3'd3: cur_data_c = {16'd0, qword_q[ 63: 48]}; 3'd4: cur_data_c = {16'd0, qword_q[ 79: 64]}; 3'd5: cur_data_c = {16'd0, qword_q[ 95: 80]}; 3'd6: cur_data_c = {16'd0, qword_q[111: 96]}; default: cur_data_c = {16'd0, qword_q[127:112]}; endcase cur_be_c = 4'b0011; end else begin // PSMCT32: 4 pixels per qword, full 32-bit. cur_mask_c = 32'hFFFF_FFFF; unique case (lane_q[1:0]) 2'd0: cur_data_c = qword_q[ 31: 0]; 2'd1: cur_data_c = qword_q[ 63: 32]; 2'd2: cur_data_c = qword_q[ 95: 64]; default: cur_data_c = qword_q[127: 96]; endcase cur_be_c = 4'b1111; end end // Ch121 — optional PSMCT32 swizzled write address. // // When PSMCT32_SWIZZLE=1 AND the active PSM is PSMCT32, route // the per-pixel byte address through gs_swizzle_psmct32_stub // instead of the linear formula. The swizzle module gives // a within-FB byte offset relative to FBP=0; we add dest_base_q // (= DBP*256) to anchor the upload at the same DBP-relative // base the linear path uses. dbw_q feeds the swizzle's FBW // input directly (both are in 64-pixel units, matching the // PSMCT32 page = 64 px wide convention). The per-pixel x and // y inputs are the FULL effective coordinates (DSAX+cur_x, // DSAY+cur_y), so the swizzle correctly handles non-zero // DSAX/DSAY uploads as well. // // Other PSMs are governed by their own dispatch branches in // the per-PSM mux below (PSMCT16 via PSMCT16_SWIZZLE Ch127, // PSMT8 via PSMT8_SWIZZLE Ch133, PSMT4 via PSMT4_SWIZZLE // Ch139). With PSMCT32_SWIZZLE=0 the PSMCT32 path falls // through to cur_addr_c. The swizzle module is purely // combinational; when its gate is off its output is unused // and the synthesizer trims it. logic [31:0] cur_addr_swizzled_c; logic [11:0] swizzle_x_in; logic [11:0] swizzle_y_in; assign swizzle_x_in = dsax_q + cur_x_q; assign swizzle_y_in = dsay_q + cur_y_q; logic [31:0] swizzle_addr_off; gs_swizzle_psmct32_stub u_swizzle ( .fbp (9'd0), .fbw (dbw_q), .x (swizzle_x_in), .y (swizzle_y_in), .addr(swizzle_addr_off) ); assign cur_addr_swizzled_c = dest_base_q + swizzle_addr_off; // Ch127 — optional PSMCT16 swizzled write address. Same shape // as Ch121 above but uses gs_swizzle_psmct16_stub. The PSMCT16 // page (64×64) and block grid (4 cols × 8 rows of 16×8 blocks) // and within-block columnTable16 are all baked into that // module — we just feed it `dbw_q` as FBW and the full // effective coords. dest_base_q (= DBP*256) is added on top // so any DBP works; the swizzle module is given FBP=0 so its // output is the within-FB byte offset only. logic [31:0] cur_addr_swizzled16_c; logic [31:0] swizzle16_addr_off; gs_swizzle_psmct16_stub u_swizzle16 ( .fbp (9'd0), .fbw (dbw_q), .x (swizzle_x_in), .y (swizzle_y_in), .addr(swizzle16_addr_off) ); assign cur_addr_swizzled16_c = dest_base_q + swizzle16_addr_off; // Ch133 — optional PSMT8 swizzled write address. Same shape as // Ch121 / Ch127 above but uses gs_swizzle_psmt8_stub. PSMT8 // pages are 128 px wide so the swizzle internally uses // bw_pg = DBW>>1 (PCSX2 asserts DBW must be even for PSMT8). // dest_base_q (= DBP*256) is added on top so any DBP works; // the swizzle module is given FBP=0 so its output is the // within-FB byte offset only. logic [31:0] cur_addr_swizzled8_c; logic [31:0] swizzle8_addr_off; gs_swizzle_psmt8_stub u_swizzle8 ( .fbp (9'd0), .fbw (dbw_q), .x (swizzle_x_in), .y (swizzle_y_in), .addr(swizzle8_addr_off) ); assign cur_addr_swizzled8_c = dest_base_q + swizzle8_addr_off; // Ch139 — optional PSMT4 swizzled write address. Same wiring // shape as Ch121/Ch127/Ch133 but uses gs_swizzle_psmt4_stub, // which outputs both an absolute byte address AND a // `nibble_hi` selector. PSMT4 pages are 128 px wide AND tall; // the swizzle internally uses bw_pg=DBW>>1 (PCSX2 asserts // DBW must be even for PSMT4). dest_base_q (= DBP*256) is // added on top so any DBP works; the swizzle module is given // FBP=0 so its addr output is the within-FB byte offset only. // The nibble_hi output threads into the PSMT4 data lane mux // below: when this gate is on AND psmt4_q, the existing Ch118 // nibble RMW machinery (write_be=4'b0001, write_mask 0x0F or // 0xF0) keys on the swizzle's nibble_hi instead of x_eff[0]. logic [31:0] cur_addr_swizzled4_c; logic [31:0] swizzle4_addr_off; logic swizzle4_nibble_hi; gs_swizzle_psmt4_stub u_swizzle4 ( .fbp (9'd0), .fbw (dbw_q), .x (swizzle_x_in), .y (swizzle_y_in), .addr (swizzle4_addr_off), .nibble_hi(swizzle4_nibble_hi) ); assign cur_addr_swizzled4_c = dest_base_q + swizzle4_addr_off; // VRAM write outputs — pulse for one cycle per pixel emit. // Only fire when DPSM is supported (PSMCT32, PSMCT16, PSMT8, // or PSMT4). Other PSMs still consume qwords lane-by-lane to // keep gif_packed_stub from desync, but no VRAM write happens. logic emit_now; assign emit_now = lane_valid_q && (psmct32_q || psmct16_q || psmt8_q || psmt4_q); // Per-PSM swizzle dispatch. The four parameters are // independent; defaults of 0 keep every PSM on the legacy // linear path. assign vram_we = emit_now; assign vram_waddr = (PSMCT32_SWIZZLE && psmct32_q) ? cur_addr_swizzled_c : (PSMCT16_SWIZZLE && psmct16_q) ? cur_addr_swizzled16_c : (PSMT8_SWIZZLE && psmt8_q) ? cur_addr_swizzled8_c : (PSMT4_SWIZZLE && psmt4_q) ? cur_addr_swizzled4_c : cur_addr_c; assign vram_wdata = cur_data_c; assign vram_wbe = cur_be_c; assign vram_wmask = cur_mask_c; // Compute target pixel count = RRW * RRH (24-bit cap is fine // for any palette/texture upload we model here). logic [23:0] pix_total_calc; assign pix_total_calc = {12'd0, rrw} * {12'd0, rrh}; // Step / wrap logic (on the cycle a pixel emits). logic [11:0] next_x; logic [11:0] next_y; logic wrap_row; assign wrap_row = (cur_x_q + 12'd1 == rrw_q); assign next_x = wrap_row ? 12'd0 : (cur_x_q + 12'd1); assign next_y = wrap_row ? (cur_y_q + 12'd1) : cur_y_q; always_ff @(posedge clk) begin if (!rst_n) begin state <= S_IDLE; dbp_q <= 14'd0; dbw_q <= 6'd0; dpsm_q <= 6'd0; dsax_q <= 11'd0; dsay_q <= 11'd0; rrw_q <= 12'd0; rrh_q <= 12'd0; dest_base_q <= 32'd0; row_stride_q <= 32'd0; psmct32_q <= 1'b0; psmct16_q <= 1'b0; psmt8_q <= 1'b0; psmt4_q <= 1'b0; lane_last_q <= 5'd3; qword_q <= 128'd0; lane_q <= 5'd0; lane_valid_q <= 1'b0; cur_x_q <= 12'd0; cur_y_q <= 12'd0; pix_total_q <= 24'd0; pix_done_q <= 24'd0; end else begin unique case (state) S_IDLE: begin if (trxdir_wr_pulse && (xdir == 2'd0)) begin logic is_ct32, is_ct16, is_t8, is_t4; is_ct32 = (dpsm == 6'h00); is_ct16 = (dpsm == 6'h02); is_t8 = (dpsm == 6'h13); is_t4 = (dpsm == 6'h14); // Snapshot all transfer params. dbp_q <= dbp; dbw_q <= dbw; dpsm_q <= dpsm; dsax_q <= dsax; dsay_q <= dsay; rrw_q <= rrw; rrh_q <= rrh; dest_base_q <= {18'd0, dbp} << 8; // row_stride = DBW * 64 * bpp: // PSMCT32 → DBW * 256 (DBW << 8) // PSMCT16 → DBW * 128 (DBW << 7) // PSMT8 → DBW * 64 (DBW << 6) // PSMT4 → DBW * 32 (DBW << 5) // other → fall back to PSMCT32-stride // (no VRAM emit anyway) row_stride_q <= is_t4 ? ({18'd0, dbw} << 5) : is_t8 ? ({18'd0, dbw} << 6) : is_ct16 ? ({18'd0, dbw} << 7) : ({18'd0, dbw} << 8); psmct32_q <= is_ct32; psmct16_q <= is_ct16; psmt8_q <= is_t8; psmt4_q <= is_t4; // Lanes/qword: 4 (PSMCT32) → last=3, // 8 (PSMCT16) → last=7, 16 (PSMT8) → last=15, // 32 (PSMT4) → last=31. Other PSMs use the // PSMCT32 cadence (silent consume). lane_last_q <= is_t4 ? 5'd31 : is_t8 ? 5'd15 : is_ct16 ? 5'd7 : 5'd3; cur_x_q <= 12'd0; cur_y_q <= 12'd0; pix_total_q <= pix_total_calc; pix_done_q <= 24'd0; lane_valid_q <= 1'b0; state <= S_RUN; end end S_RUN: begin if (!lane_valid_q && data_valid && data_ready) begin // Latch a fresh qword to drain. qword_q <= data_qword; lane_q <= 5'd0; lane_valid_q <= 1'b1; end else if (lane_valid_q) begin // Drain one lane per cycle. Step the cursor // when the emit fires (supported PSM or not — // for unsupported PSM, no VRAM write fires // but we still consume the lane to keep // gif_packed_stub from desync). if (lane_q == lane_last_q) begin lane_valid_q <= 1'b0; lane_q <= 5'd0; end else begin lane_q <= lane_q + 5'd1; end // Pixel-cursor + done-count step. cur_x_q <= next_x; cur_y_q <= next_y; pix_done_q <= pix_done_q + 24'd1; // Did this lane emit complete the rect? if (pix_done_q + 24'd1 >= pix_total_q) begin // End of transfer. Drop any remaining // unused lanes — for PSMCT32 the rect // size should be a multiple of 4 px, // for PSMCT16 a multiple of 8 px, for // PSMT8 a multiple of 16 px, for PSMT4 // a multiple of 32 px (else the extra // trailing lanes within the last qword // are silently swallowed). // Return to IDLE on the same cycle the // last lane emits. state <= S_IDLE; lane_valid_q <= 1'b0; lane_q <= 5'd0; end end end default: state <= S_IDLE; endcase end end endmodule : gif_image_xfer_stub