// retroDE_ps2 — gif_packed_stub (Ch72 + Ch73) // // Real-format GIF parser. Sits in the same upstream/downstream slot // as gif_path_stub but accepts a real PS2 GIFtag in front of the // data, instead of the project-local single-qword register-write // format that gif_path_stub uses for Wave 2. // // Scope: // - PACKED (FLG=0): NLOOP×NREG PACKED entries, one entry per qword. // A+D (REGS nibble 0xE) emits a GS register write; other nibbles // are traced EV_MODE no-ops. (Ch72.) // - REGLIST (FLG=1): NLOOP×NREG REGLIST entries, two entries per // qword (low 64 = entry #0, high 64 = entry #1). REGLIST data // bytes are register values keyed by REGS nibbles in order; only // A+D (0xE) gets a GS write here, since real REGLIST treats each // nibble as the register *number* (not A+D), and we don't yet // have a reg# → gs-offset map. Other nibbles consume the entry // and trace EV_MODE. (Ch73.) // - FLG=2 (IMAGE) and FLG=3 (DISABLE): payload is NLOOP qwords, // consumed silently with EV_MODE traces, no GS effect. (Ch73 — // prevents the "next data qword is mistaken for a new GIFtag" // desync flagged in the Ch73 audit.) // - NLOOP up to 15 bits, NREG up to 16 registers. PS2 docs: a // NREG field of 0 means 16; we use a 5-bit effective count to // represent 16 correctly (Ch73 audit-medium fix — was clamped // to 4'd15, which mis-counted PACKED 16-reg packets by one). // - EOP carries no behavioral difference here (always-ready // sink); preserved as trace metadata. // // PACKED A+D data qword layout — selectable via REAL_AD_REG_MAP: // // REAL_AD_REG_MAP=0 (default, project-local Ch72/Ch73 back-compat): // bits[ 63: 0] = 64-bit register data // bits[ 79: 64] = 16-bit project-local GS privileged offset // (drives gs_stub.reg_wr_*) // bits[127: 80] = reserved // // REAL_AD_REG_MAP=1 (real PS2 layout, Ch75): // bits[ 63: 0] = 64-bit register data // bits[ 71: 64] = 8-bit GIF A+D register number per PCSX2 GSRegs.h // (drives gs_stub.gif_reg_*; gs_stub owns the // decode into PRIM/RGBAQ/XYZF2/XYZ2/FRAME_1/ZBUF_1) // bits[127: 72] = reserved // // The two namespaces are architecturally distinct. Do NOT add a // reg# → privileged-offset LUT here — that conflation is the Ch74 // mistake Ch75 corrected. New GIF-context registers belong inside // gs_stub, keyed by reg#. // // in_ready is held high — same one-shot contract as gif_path_stub. // // Trace schema: // On tag accept: EV_GIFTAG arg0={flg,path_id} arg1={eop,nreg,nloop_lo} // arg2=regs_64 arg3=0 flags={in_last,1} // On PACKED data (A+D): EV_WRITE arg0=path_id arg1=regnib // arg2={offset16} arg3=data64 // flags={in_last,0} // On PACKED data (other): EV_MODE same layout, no GS write. // On REGLIST entry: EV_MODE arg0=path_id arg1=regnib // arg2=0 arg3=entry64 // flags={in_last,0} // On IMAGE/DISABLE qword: EV_MODE arg0={flg,path_id} // arg2=0 arg3=in_data[63:0] // flags={in_last,0} `timescale 1ns/1ps module gif_packed_stub import trace_pkg::*; #( parameter logic [3:0] PATH_ID = 4'd2, // Ch75 (was Ch74, corrected): switch the PACKED A+D address-source // from the project-local 16-bit-offset layout (default, // bits[79:64]=gs_offset) to the real PS2 A+D layout where // bits[71:64] is the 8-bit GS A+D register *number*. Per PCSX2's // GSRegs.h, the GIF A+D register namespace is distinct from the // GS privileged-MMIO offset namespace — Ch74's LUT mistakenly // mapped one to the other. The corrected design hands the 8-bit // reg# to gs_stub via its new gif_reg_* port and lets gs_stub // own the GIF-context register file decode. When this parameter // is 0, the legacy gs_wr_* port (16-bit privileged-style offset) // is driven for back-compat with Ch72/Ch73 PACKED-A+D TBs and // tb_bgcolor_via_dma. parameter bit REAL_AD_REG_MAP = 1'b0 ) ( input logic clk, input logic rst_n, // Upstream from DMAC input logic in_valid, input logic [127:0] in_data, input logic in_last, output logic in_ready, // Downstream — legacy 16-bit-offset port (REAL_AD_REG_MAP=0). // Drives gs_stub's privileged-style reg_wr_* port. output logic gs_wr_en, output logic [15:0] gs_wr_addr, output logic [63:0] gs_wr_data, // Ch110 — IMAGE-mode (FLG=2) data passthrough. `image_data_valid` // pulses for one cycle on every accepted IMAGE qword (i.e., when // gif_packed_stub.state == S_IMAGE and a qword is consumed). // `image_data` is the raw 128-bit qword payload; `image_data_last` // mirrors the upstream `in_last`. Downstream `gif_image_xfer_stub` // captures the qword and writes the unpacked pixels into vram_stub // at the BITBLTBUF/TRXPOS/TRXREG-described destination. NOT wired // by TBs that don't model image transfers — leaving these outputs // unconnected is fine (named-port instantiation). output logic image_data_valid, output logic [127:0] image_data, output logic image_data_last, // Ch110 — backpressure from the IMAGE consumer. When state is // S_IMAGE, in_ready is gated by image_data_ready so the upstream // DMA stalls while gif_image_xfer_stub is busy emitting the // previous qword's pixel writes. Outside S_IMAGE the gate has no // effect — in_ready stays high. TBs that don't model image // transfers tie this to 1'b1 (the always-ready default). input logic image_data_ready, // Ch172 — backpressure from the raster command FIFO inside // gs_stub. When the GIF is processing PACKED/REGLIST qwords // and the raster FIFO is full, deassert in_ready so the DMAC // pauses BEFORE the next qword is consumed (which might // trigger prim_complete → push_drop). The stall point is the // qword-acceptance handshake — once a qword is `accept`-ed, // the parser fully processes it; we never have a "consumed // but not committed" state. Outside PACKED/REGLIST this input // is ignored. TBs that don't model the raster FIFO tie this // to 1'b0 (always-have-space). input logic raster_fifo_full, // Ch75 — real-PS2 GIF A+D register-number port (REAL_AD_REG_MAP=1). // Drives gs_stub's GIF-context gif_reg_* port. Only one of // {gs_wr_*, gif_reg_*} is active per accept depending on the // parameter. output logic gif_reg_wr_en, output logic [7:0] gif_reg_num, output logic [63:0] gif_reg_data, // Trace output logic ev_valid, output subsys_e ev_subsys, output event_e ev_event, output logic [63:0] ev_arg0, output logic [63:0] ev_arg1, output logic [63:0] ev_arg2, output logic [63:0] ev_arg3, output logic [31:0] ev_flags ); // Ch73: state widened to cover REGLIST and IMAGE/DISABLE payloads // so unsupported FLG packets don't desync onto the next qword. typedef enum logic [1:0] { S_TAG = 2'd0, S_PACKED = 2'd1, S_REGLIST = 2'd2, S_IMAGE = 2'd3 // also used for FLG=3 DISABLE } state_e; state_e state; // Ch110 — in_ready is high outside S_IMAGE (no backpressure // path) and gated by image_data_ready ONLY when an actual // FLG=2 IMAGE payload is in flight. S_IMAGE is also reused // for FLG=3 DISABLE qwords (per Ch73 desync fix); those are // opaque consume-only and must NOT route to // gif_image_xfer_stub or apply backpressure. flg_q is the // currently-running tag's FLG; gating on flg_q==2'd2 keeps // DISABLE payloads silent. logic image_active; assign image_active = (state == S_IMAGE) && (flg_q == 2'd2); // Ch172 — three-tier in_ready policy: // 1) S_IMAGE with FLG=2 → wait for gif_image_xfer_stub to be // ready for the next qword. // 2) Otherwise (PACKED / REGLIST / TAG-fetch) → stall when // the downstream raster command FIFO is full so the // next register write can't trigger an unrecoverable // prim_complete push_drop. Over-stalling on PACKED qwords // that aren't going to cause a push is intentionally // conservative — it has no functional impact and keeps // the gate simple. // 3) Default (sim TBs without the raster path) → always // ready (raster_fifo_full tied to 1'b0 in those cases). assign in_ready = image_active ? image_data_ready : !raster_fifo_full; logic accept; assign accept = in_valid && in_ready; // Ch110 — IMAGE-mode data passthrough (combinational). // Gated on image_active so FLG=3 DISABLE qwords are NOT // forwarded to gif_image_xfer_stub. assign image_data = in_data; assign image_data_last = in_last; assign image_data_valid = accept && image_active; // Latched tag context (valid in S_PACKED / S_REGLIST / S_IMAGE) logic [14:0] nloop_q; logic eop_q; logic [1:0] flg_q; // Ch73: nreg_eff widened to 5 bits. NREG field is 4 bits (0..15); // PS2 docs say a value of 0 means 16. Encoding 16 as 5'b10000 // lets reg_idx == nreg_eff_q correctly terminate a 16-register // packet. (Old 4-bit clamp made NREG=0 consume only 15 entries.) logic [4:0] nreg_eff_q; logic [63:0] regs_q; logic [4:0] reg_idx; // Ch73: REGLIST and IMAGE/DISABLE consume opaque qwords. We track // how many qwords are still left in the payload, computed at // S_TAG entry. PACKED keeps its per-entry reg_idx scheme. // REGLIST count = ceil(NLOOP * NREG / 2) (2 entries / qword) // IMAGE count = NLOOP (1 qword / loop) logic [19:0] payload_qwords_left; // Combinational tag-field decode for the qword on the wire in S_TAG. logic [14:0] tag_nloop; logic tag_eop; logic [1:0] tag_flg; logic [3:0] tag_nreg_field; logic [4:0] tag_nreg_eff; logic [63:0] tag_regs; assign tag_nloop = in_data[14:0]; assign tag_eop = in_data[15]; assign tag_flg = in_data[59:58]; assign tag_nreg_field = in_data[63:60]; assign tag_nreg_eff = (tag_nreg_field == 4'd0) ? 5'd16 : {1'b0, tag_nreg_field}; assign tag_regs = in_data[127:64]; // Ch73 audit-low: replace indexed bit-select with shift/mask. The // big case statement triggered iverilog's "constant selects in // always_*" "sorry" warnings repeatedly. Use concat-pad to form // the shift amount (reg_idx * 4) without going through `*`, which // iverilog truncates to operand width and would alias high // reg_idx values back to small shifts (e.g., reg_idx=8 wrapping // to shift=0 — the bug found in Ch73 bring-up). logic [6:0] cur_regnib_shift; logic [3:0] cur_regnib; assign cur_regnib_shift = {reg_idx, 2'b00}; // reg_idx*4 in 7 bits assign cur_regnib = (regs_q >> cur_regnib_shift) & 64'hF; // ------------------------------------------------------------------ // FSM // ------------------------------------------------------------------ logic packed_last_in_loop; logic packet_loop_last; assign packed_last_in_loop = (reg_idx + 5'd1 == nreg_eff_q); assign packet_loop_last = (nloop_q == 15'd1); // Ch73: pre-compute REGLIST payload-qword count = ceil(NLOOP * // NREG / 2). Done at S_TAG accept so the FSM only needs an // opaque countdown afterwards. logic [19:0] reglist_total_entries; logic [19:0] reglist_total_qwords; assign reglist_total_entries = tag_nloop * tag_nreg_eff; assign reglist_total_qwords = (reglist_total_entries + 20'd1) >> 1; always_ff @(posedge clk) begin if (!rst_n) begin state <= S_TAG; nloop_q <= 15'd0; eop_q <= 1'b0; flg_q <= 2'd0; nreg_eff_q <= 5'd0; regs_q <= 64'd0; reg_idx <= 5'd0; payload_qwords_left <= 20'd0; end else if (accept) begin unique case (state) S_TAG: begin nloop_q <= tag_nloop; eop_q <= tag_eop; flg_q <= tag_flg; nreg_eff_q <= tag_nreg_eff; regs_q <= tag_regs; reg_idx <= 5'd0; payload_qwords_left <= 20'd0; if (tag_nloop == 15'd0) begin state <= S_TAG; // empty tag end else begin unique case (tag_flg) 2'd0: state <= S_PACKED; 2'd1: begin state <= S_REGLIST; payload_qwords_left <= reglist_total_qwords; end default: begin state <= S_IMAGE; // FLG=2/3 payload_qwords_left <= {5'd0, tag_nloop}; end endcase end end S_PACKED: begin if (packed_last_in_loop) begin reg_idx <= 5'd0; if (packet_loop_last) state <= S_TAG; else nloop_q <= nloop_q - 15'd1; end else begin reg_idx <= reg_idx + 5'd1; end end S_REGLIST, S_IMAGE: begin // Both branches consume opaque qwords. Trace fires // per accept (see trace block below); decode of // individual REGLIST entries is left to a future // chapter once gs_stub gains the matching reg // surface. The point of Ch73 here is just: don't // desync onto the next GIFtag. if (payload_qwords_left == 20'd1) state <= S_TAG; else payload_qwords_left <= payload_qwords_left - 20'd1; end default: state <= S_TAG; endcase end end // ------------------------------------------------------------------ // GS write — fires only on PACKED A+D data accepts. REGLIST // entries don't generate GS writes here (real REGLIST treats each // nibble as a register *number*, not A+D, and we don't have GS // routing for that path yet). IMAGE/DISABLE never generates GS // writes. // // Ch75: split into two output ports based on REAL_AD_REG_MAP. // Only one fires per accept; the other stays low. gs_stub's // privileged-side `reg_wr_*` and GIF-A+D-side `gif_reg_*` ports // are architecturally distinct. // ------------------------------------------------------------------ always_ff @(posedge clk) begin if (!rst_n) begin gs_wr_en <= 1'b0; gs_wr_addr <= 16'd0; gs_wr_data <= 64'd0; gif_reg_wr_en <= 1'b0; gif_reg_num <= 8'd0; gif_reg_data <= 64'd0; end else if (accept && state == S_PACKED && cur_regnib == 4'hE) begin if (REAL_AD_REG_MAP) begin gs_wr_en <= 1'b0; gif_reg_wr_en <= 1'b1; gif_reg_num <= in_data[71:64]; gif_reg_data <= in_data[63:0]; end else begin gs_wr_en <= 1'b1; gs_wr_addr <= in_data[79:64]; gs_wr_data <= in_data[63:0]; gif_reg_wr_en <= 1'b0; end end else begin gs_wr_en <= 1'b0; gif_reg_wr_en <= 1'b0; end end // ------------------------------------------------------------------ // Trace // ------------------------------------------------------------------ always_ff @(posedge clk) begin if (!rst_n) begin ev_valid <= 1'b0; ev_subsys <= SUBSYS_GIF; ev_event <= EV_GIFTAG; ev_arg0 <= 64'd0; ev_arg1 <= 64'd0; ev_arg2 <= 64'd0; ev_arg3 <= 64'd0; ev_flags <= 32'd0; end else if (accept && state == S_TAG) begin ev_valid <= 1'b1; ev_subsys <= SUBSYS_GIF; ev_event <= EV_GIFTAG; // arg0[3:0]=path_id, arg0[5:4]=flg → callers can grep by FLG ev_arg0 <= {58'd0, tag_flg, PATH_ID}; // Compact tag summary: {eop[15], reserved[14:13]=flg, nreg[12:9], nloop[8:0]} ev_arg1 <= {49'd0, tag_eop, tag_flg, tag_nreg_field, tag_nloop[8:0]}; ev_arg2 <= tag_regs; ev_arg3 <= 64'd0; ev_flags <= {30'd0, in_last, 1'b1}; // bit0=is_tag end else if (accept && state == S_PACKED) begin ev_valid <= 1'b1; ev_subsys <= SUBSYS_GIF; ev_event <= (cur_regnib == 4'hE) ? EV_WRITE : EV_MODE; ev_arg0 <= {60'd0, PATH_ID}; ev_arg1 <= {60'd0, cur_regnib}; ev_arg2 <= {48'd0, in_data[79:64]}; ev_arg3 <= in_data[63:0]; ev_flags <= {30'd0, in_last, 1'b0}; // bit0=is_data end else if (accept && state == S_REGLIST) begin // Two entries per qword: low half (reglist_half=0) → bits // [63:0]; high half (reglist_half=1) → bits[127:64]. Trace // each as EV_MODE (no GS write). reglist_half is the // already-flopped bit, so the same trace block fires for // both halves of the same qword on consecutive accepts — // wait, no: REGLIST's S_REGLIST branch consumes one accept // per half of the same qword? In our FSM, the high half // re-enters S_REGLIST on the SAME qword? It does not — the // FSM advances reglist_half within a single accept. Trace // the low-half entry on the accept; the high-half entry's // trace is omitted in this minimal Ch73 path. ev_valid <= 1'b1; ev_subsys <= SUBSYS_GIF; ev_event <= EV_MODE; ev_arg0 <= {60'd0, PATH_ID}; ev_arg1 <= {60'd0, cur_regnib}; ev_arg2 <= 64'd0; ev_arg3 <= in_data[63:0]; // low-half entry data ev_flags <= {30'd0, in_last, 1'b0}; end else if (accept && state == S_IMAGE) begin ev_valid <= 1'b1; ev_subsys <= SUBSYS_GIF; ev_event <= EV_MODE; ev_arg0 <= {58'd0, flg_q, PATH_ID}; ev_arg1 <= 64'd0; ev_arg2 <= 64'd0; ev_arg3 <= in_data[63:0]; ev_flags <= {30'd0, in_last, 1'b0}; end else begin ev_valid <= 1'b0; end end endmodule : gif_packed_stub