// retroDE_ps2 — ee_core_stub
//
// First EE-side execution primitive. Structural mirror of
// `iop_core_stub` — same multi-cycle FSM, same R3000 subset, same
// delay-slot discipline, same strict-unsupported gate, same COP0
// exception-entry shape. The files are kept separate because the EE
// is fundamentally an R5900 (MIPS III + SCE extensions + VU) and will
// eventually need 64-bit register widths, COP1/COP2, and VU-side
// plumbing the IOP will never grow. For the first EE chapter the core
// is deliberately as narrow as iop_core_stub so the earliest EE
// programs can exercise fetch + decode + branches + halt before any
// of that complexity is needed.
//
// Where the two cores differ right now:
//   - Trace subsystem: SUBSYS_EE (iop_core_stub uses SUBSYS_IOP).
//   - Default PC_RESET and EXC_VECTOR are the same architectural
//     values (0xBFC0_0000 / 0x0000_0080) — real R5900 and real R3000
//     both reset to 0xBFC0_0000.
//
// Supported opcodes (MIPS encoding):
//   SPECIAL (opcode = 0x00):
//     func 0x00 (SLL), 0x02 (SRL), 0x03 (SRA) — shift family
//       (destination = rd; operand = rt; count = shamt [10:6].
//        SRA uses arithmetic right shift so the MSB sign-extends;
//        SRL zero-fills. SLL $0,$0,0 is the canonical NOP encoding
//        and flows through this path harmlessly — the rd_idx=0
//        guard suppresses the writeback.)
//     func 0x08 (JR), func 0x09 (JALR), func 0x0C (SYSCALL),
//       (JALR: register-indirect call. Target is rs_val (same
//        path as JR), link address pc+8 is written to rd_idx.
//        rd_idx==0 suppresses the link write — valid encoding
//        for "jump indirect without keeping a return address".
//        First real-BIOS trip at 0xBFC5_29E8 after SH.)
//     func 0x20 (ADD), 0x21 (ADDU), 0x22 (SUB), 0x23 (SUBU)
//       (ADD/SUB implemented as ADDU/SUBU — same pragmatic policy
//        as ADDI vs ADDIU; Arithmetic Overflow exception deferred.
//        First real-BIOS ADD trip at 0x0000_060C (RAM-resident
//        code) after JALR unlocked retired=84112.)
//     func 0x24 (AND), 0x25 (OR), 0x26 (XOR), 0x27 (NOR)
//     func 0x2A (SLT), 0x2B (SLTU) — R-type compare, register form
//       pair of SLTI/SLTIU (first real-BIOS SLTU trip at 0xBFC0_2644)
//       (R-type ALU family; destination is rd, not rt.
//        OR was the first trip in the logic subset at 0xBFC0_2074;
//        the rest landed because they share the exact same R-type
//        plumbing. ADDU was the first arith trip at 0xBFC0_2640 after
//        JAL landed, paired with SUBU in the same addition.)
//     other funcs → NOP.
//   0x02 J   0x03 JAL   0x04 BEQ   0x05 BNE
//     (JAL: jump-and-link; target is the same form as J, but writes
//      pc+8 into $31. First real-BIOS trip at 0xBFC0_23D0 after LB
//      unlocked retired=1714.)
//   0x08 ADDI   (behaves as ADDIU — we do not model the Arithmetic
//                Overflow exception yet; real BIOS emits ADDI where
//                overflow cannot happen in practice)
//   0x09 ADDIU
//   0x0A SLTI   0x0B SLTIU   (first evidence-driven growth past IOP's
//                             subset — real BIOS hit SLTI at
//                             0xBFC0_0008 under strict mode, then ADDI
//                             at 0xBFC0_206C, then ANDI at 0xBFC0_2070
//                             as each new op unlocked the next)
//   0x0C ANDI   0x0D ORI   0x0F LUI
//   0x10 COP0 (rs=0 MFC0, rs=4 MTC0, rs=0x10/func=0x10 RFE)
//   0x20 LB   0x21 LH   0x23 LW   0x25 LHU   0x28 SB   0x29 SH   0x2B SW
//     (SB: byte store; broadcasts rt[7:0] into the addressed lane
//      and sets a one-hot byte-enable on the map_wr_be bus. First
//      real-BIOS trip at 0xBFC0_20A0 after the R-type logic family
//      unlocked retired=180.
//      LB: byte load with sign-extension; extracts byte at ea[1:0]
//      from the 32-bit word returned by the map and 24-bit
//      sign-extends. First real-BIOS trip at 0xBFC0_23A8 after SB
//      unlocked retired=1704.
//      LH/LHU: halfword load, sign- and zero-extended respectively.
//      Halfword addressing uses ea[1] only (ea[0] must be zero for
//      aligned access). First real-BIOS LH trip at 0xBFC0_2684 after
//      SLT/SLTU unlocked retired=7385; LHU landed in the same
//      chapter because it shares the exact same extraction plumbing
//      with a zero-fill instead of sign-fill.)
//   Anything else → NOP (or strict trap when STRICT_UNSUPPORTED=1).
//
// Intentionally NOT yet (evidence-driven growth will resolve these
// as the real BIOS demands them):
//   - 64-bit regs / LD / SD / LQ / SQ (R5900 territory)
//   - COP1 (FPU), COP2 (VU0 macro mode), VU1, VIF
//   - TLB / caches
//   - Signed DIV, MULT, MULTU, MTHI/MTLO — HI/LO architectural
//     state + unsigned DIVU + MFHI/MFLO landed in ch43; the
//     remaining mul/div/signed-div and HI/LO *writes* defer until
//     BIOS demands them. Divide latency and overflow detail are
//     not modelled (DIVU completes in one cycle, matching the
//     ALU style; revisit only if a timing-sensitive test needs
//     it).
//   - Most of COP0 beyond {Count (read-only), Status, Cause, EPC}
//     — Compare, Random, EntryHi/Lo, etc. Count write via MTC0 is
//     silently dropped; revisit if a BIOS loop depends on reset-
//     to-value semantics.
//   - Arithmetic Overflow exception on ADD / SUB / ADDI (they
//     execute as ADDU / SUBU / ADDIU for now; the exception path
//     lights up only when real BIOS code actually overflows)
//   - REGIMM link variants (BLTZAL/BGEZAL) — the non-linking
//     BLTZ/BGEZ landed in ch41 alongside BLEZ/BGTZ; link variants
//     defer until the real BIOS needs them.
//   - unaligned load/store (LWL/LWR/SWL/SWR) — rare in early boot
//   - BD bit in Cause; nested interrupts; syscall/break dispatch
//     beyond SYSCALL-as-halt.
//
// Trace (SUBSYS_EE, EV_IFETCH one-per-retire; flag bits match
// iop_core_stub):
//   bit 0 = SW     bit 1 = LW     bit 2 = branch/jump taken
//   bit 3 = SYSCALL halt          bit 4 = instruction in delay slot
//   bit 5 = exception taken       bit 6 = RFE retired
//   bit 7 = strict trap
//
// Strict mode (STRICT_UNSUPPORTED parameter) matches iop_core_stub.

`timescale 1ns/1ps

module ee_core_stub
    import trace_pkg::*;
#(
    parameter logic [31:0] PC_RESET           = 32'hBFC0_0000,
    parameter logic [31:0] EXC_VECTOR         = 32'h0000_0080,
    parameter bit          STRICT_UNSUPPORTED = 1'b0,
    // Ch47: address-error enforcement on word/halfword loads and
    // stores. When enabled, any SW/LW with ea[1:0] != 0 or
    // SH/LH/LHU with ea[0] != 0 halts the core and sets
    // trap_o / trap_pc_o / trap_instr_o (same mechanism as
    // STRICT_UNSUPPORTED). Default on — real MIPS raises an
    // AdEL/AdES exception here; silently aliasing to the aligned
    // word was a simulation artifact that previously made
    // unaligned writes look like valid aligned stores.
    parameter bit          TRAP_ALIGN_ERROR   = 1'b1,
    // Ch50: Status.BEV (boot exception vector) modeling. Real MIPS
    // resets with BEV=1 so exceptions vector to a ROM-resident
    // "boot" handler (BEV_EXC_VECTOR). The BIOS writes BEV=0 via
    // MTC0 once its RAM-resident handler is installed, at which
    // point exceptions vector to EXC_VECTOR. INIT_BEV controls the
    // reset state of Status.BEV; default 0 keeps the pre-ch50
    // backward-compatible behavior (always vector to EXC_VECTOR).
    // tb_ee_core_bios_smoke sets INIT_BEV=1 to match real-BIOS
    // semantics.
    parameter bit          INIT_BEV           = 1'b0,
    parameter logic [31:0] BEV_EXC_VECTOR     = 32'hBFC0_0380,
    // Ch162 — strip the synthesized 32-bit hardware divider that
    // Quartus infers from the Ch43 DIVU instruction's `/` and `%`
    // operators. The auto-generated divider is the Ch159+ STA
    // critical path (top-10 worst paths all live in
    // `u_demo|u_core|div_0_rtl_0|auto_generated|divider|...`,
    // ~32 ns of combinational ripple). Default 0 keeps the
    // load-bearing DIVU semantics for every existing Ch43
    // integration TB (`tb_ee_core_divu_mflo`, etc.). When set to
    // 1, the DIVU writeback path becomes a no-op (HI/LO stay at
    // their prior values, just as in the divisor==0 case the
    // spec calls undefined). Synthesis builds that target the
    // PSMCT32 SPRITE-only raster demo can set this to 1 — the
    // bootlet doesn't execute DIVU, so removing the divider is
    // behavior-neutral for that demo while freeing the
    // critical-path budget for everything else. Sim TBs that
    // assert DIVU output (only `tb_ee_core_divu_mflo` today)
    // keep the parameter at its default 0.
    parameter bit          STRIP_HW_DIVIDER   = 1'b0,
    // Ch215 — labeled sim-only jmp_buf restore for SYSCALL #8 ($v1=8,
    // $a0=2). When enabled, the SYSCALL handler enters a 12-step LW
    // FSM that loads $ra/$sp/$fp/$s0..$s7/$gp from a hardcoded frame
    // at CH215_JMPBUF_BASE (proven by Ch214 to be the BIOS-side
    // 0xA000B1E0 jmp_buf assembled via LUI $r,0xA001 + ADDIU $r,$r,
    // -0x4E20 at the setjmp call site 0xBFC52340/4C). Also sets $v0=1
    // so the post-setjmp `beq $v0,$0` at 0xBFC52350 falls through to
    // the longjmp-return path. NOT general syscall #8 semantics — a
    // BIOS-bringup shim until the real kernel handler is modeled.
    parameter bit          CH215_JMPBUF_RESTORE_ENABLE = 1'b0,
    parameter logic [31:0] CH215_JMPBUF_BASE           = 32'hA000_B1E0,
    // Ch273 — minimal EE syscall HLE dispatcher for the ELF runner /
    // user-mode flow. When enabled, SYSCALL with a known $v1 ($v1 is
    // the PS2-EE syscall-number convention) sets $v0 to a stub return
    // and advances PC to PC+4 (normal user-code SYSCALL resume — NOT
    // RFE; that's the Ch199 path). Unknown $v1 still halts (with $v1
    // and $a0..$a3 readable via hierarchical sim peek from the TB) so
    // the next blocker surfaces.
    //
    // Default 0 to keep every existing TB's "syscall = halt-PASS"
    // pattern working (those tests don't set $v1, so $v1 could be
    // anything; gating prevents accidental dispatch).
    //
    // Recognized $v1 values (qbert.elf crt0 prolog):
    //   0x3C EndOfHeap       -> $v0 = SYSCALL_HEAP_END  (default 0x001E0000)
    //   0x3D InitMainThread  -> $v0 = 0                 (stub success)
    //   0x64 FlushCache      -> $v0 = 0                 (no-op success)
    parameter bit          EE_SYSCALL_HLE_ENABLE = 1'b0,
    parameter logic [31:0] SYSCALL_HEAP_END      = 32'h001E_0000
) (
    input  logic          clk,
    input  logic          rst_n,

    input  logic          go_i,

    output logic          map_rd_en,
    output logic [31:0]   map_rd_addr,
    input  logic [31:0]   map_rd_data,
    input  logic          map_rd_valid,

    output logic          map_wr_en,
    output logic [31:0]   map_wr_addr,
    output logic [31:0]   map_wr_data,
    output logic [3:0]    map_wr_be,

    input  logic          cpu_irq,

    output logic          halt_o,
    output logic [31:0]   pc_o,

    output logic          trap_o,
    output logic [31:0]   trap_pc_o,
    output logic [31:0]   trap_instr_o,

    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
);

    // ------------------------------------------------------------------
    // Opcode / func / COP0 rs constants
    // ------------------------------------------------------------------

    localparam logic [5:0] OP_SPECIAL = 6'h00;
    // REGIMM = opcode 0x01. rt-field selects the branch sub-op:
    //   rt=0x00 -> BLTZ, rt=0x01 -> BGEZ (link variants 0x10/0x11
    //   intentionally not yet modeled; add when real-BIOS needs them).
    localparam logic [5:0] OP_REGIMM  = 6'h01;
    localparam logic [4:0] REGIMM_BLTZ = 5'h00;
    localparam logic [4:0] REGIMM_BGEZ = 5'h01;
    localparam logic [5:0] OP_J       = 6'h02;
    localparam logic [5:0] OP_JAL     = 6'h03;
    localparam logic [5:0] OP_BEQ     = 6'h04;
    // Ch274 — MIPS-II BEQL (Branch on Equal Likely), opcode 0x14.
    // Same compare as BEQ; differs from BEQ only in the not-taken
    // path: the delay slot is SQUASHED (PC jumps directly to PC+8)
    // instead of executing. qbert.elf hits this at PC 0x001000C0 in
    // its C++ static-constructor walker, where the delay slot at
    // PC+4 clobbers $a0 and MUST be squashed when the table is empty.
    localparam logic [5:0] OP_BEQL    = 6'h14;
    // Ch277 — MIPS-II BNEL (Branch on Not Equal Likely), opcode 0x15.
    // Mirror of BEQL with the BNE condition. Taken when rs!=rt
    // (delay slot executes, branch target reached); not-taken when
    // rs==rt (delay slot SQUASHED, PC jumps to PC+8). qbert.elf hits
    // this at PC 0x00112C7C inside a function body — predicted as
    // the Ch276→Ch277 follow-on at the Ch274 closeout.
    localparam logic [5:0] OP_BNEL    = 6'h15;
    localparam logic [5:0] OP_BNE     = 6'h05;
    localparam logic [5:0] OP_BLEZ    = 6'h06;
    localparam logic [5:0] OP_BGTZ    = 6'h07;
    localparam logic [5:0] OP_ADDI    = 6'h08;
    localparam logic [5:0] OP_ADDIU   = 6'h09;
    localparam logic [5:0] OP_SLTI    = 6'h0A;
    localparam logic [5:0] OP_SLTIU   = 6'h0B;
    localparam logic [5:0] OP_ANDI    = 6'h0C;
    localparam logic [5:0] OP_ORI     = 6'h0D;
    localparam logic [5:0] OP_LUI     = 6'h0F;
    localparam logic [5:0] OP_COP0    = 6'h10;
    localparam logic [5:0] OP_LB      = 6'h20;
    localparam logic [5:0] OP_LH      = 6'h21;
    localparam logic [5:0] OP_LW      = 6'h23;
    // Ch279 — R5900 EE Load Quadword. opcode=0x1E, I-type:
    //   lq rt, imm(base) → rt[127:0] = mem[base+imm][127:0]
    // 128-bit load symmetric to SQ (Ch271). Our regfile is 32-bit
    // so we read only the low 32 bits at EA+0 — upper 96 bits are
    // unrepresentable and discarded. Requires 16-byte alignment;
    // misaligned trips the existing AdEL path. qbert.elf hits
    // `lq $t1, 0($a1)` at PC 0x00112C88.
    localparam logic [5:0] OP_LQ      = 6'h1E;
    localparam logic [5:0] OP_LBU     = 6'h24;
    localparam logic [5:0] OP_LHU     = 6'h25;
    localparam logic [5:0] OP_SB      = 6'h28;
    localparam logic [5:0] OP_SH      = 6'h29;
    localparam logic [5:0] OP_SW      = 6'h2B;
    // Ch278 — R5900 EE Multimedia Instruction (MMI) prefix.
    // Primary opcode 0x1C; the actual sub-instruction is selected
    // by a combination of the funct field (bits 5:0) and the sa
    // field (bits 10:6), depending on which sub-group the funct
    // names (MMI0/MMI1/MMI2/MMI3). For Ch278 we ONLY decode the
    // single (funct=0x09 / sa=0x0E) pattern = MMI2/PCPYLD; every
    // other MMI sub-instruction continues to fall through to
    // strict-trap so the runner surfaces the next concrete blocker.
    localparam logic [5:0] OP_MMI       = 6'h1C;
    localparam logic [5:0] FUNC_MMI2    = 6'h09;
    localparam logic [4:0] MMI2_PCPYLD  = 5'h0E;
    // Ch282 — R5900 EE MMI2/PAND (Parallel AND). Same MMI2 funct
    // group as PCPYLD; sa 0x12 selects PAND. Architectural 128-bit
    // bitwise AND; in our 32-bit model identical to standard AND
    // (SPECIAL funct 0x24). qbert.elf hits `pand $v0, $v0, $v1`
    // (instr 0x70431489) at PC 0x00112C98.
    localparam logic [4:0] MMI2_PAND    = 5'h12;
    // Ch280 — R5900 EE MMI0/PSUBB (Parallel Subtract Byte).
    // primary opcode 0x1C, funct 0x08 (MMI0), sa 0x09 (PSUBB).
    // 16-way parallel byte subtract across 128 bits; in our
    // 32-bit-GPR model, 4 parallel byte subs on the low 32 bits
    // (each lane modulo 256, no carry between bytes). qbert.elf
    // hits `psubb $v0, $t1, $t2` (instr 0x712A1248) at PC 0x00112C90.
    localparam logic [5:0] FUNC_MMI0    = 6'h08;
    localparam logic [4:0] MMI0_PSUBB   = 5'h09;
    // Ch281 — R5900 EE MMI3/PNOR (Parallel Not-OR), opcode 0x1C,
    // funct 0x29 (MMI3), sa 0x13 (PNOR). Architectural 128-bit
    // bitwise NOR; in our 32-bit model, identical to standard NOR
    // (SPECIAL funct 0x27). With rs=$zero this is the canonical
    // MIPS "NOT" pseudo. qbert.elf hits `pnor $v1, $zero, $t1`
    // (instr 0x70091CE9) at PC 0x00112C94.
    localparam logic [5:0] FUNC_MMI3    = 6'h29;
    localparam logic [4:0] MMI3_PNOR    = 5'h13;
    // Ch283 — R5900 EE MMI3/PCPYUD (Parallel Copy Upper Doubleword),
    // opcode 0x1C, funct 0x29 (MMI3), sa 0x0E (PCPYUD). Architectural:
    //   $rd[127:64] = $rs[127:64]; $rd[63:0] = $rt[127:64]
    // i.e. extract the *upper* doubleword of each source, with $rt's
    // upper-D becoming $rd's lower-D. First MMI op that *reads* from
    // the upper 64 bits of source registers — drove the introduction
    // of the gpr128 shadow in Ch283. qbert.elf hits `pcpyud $a0, $v0,
    // $t1` (instr 0x704923A9) at PC 0x00112CA0.
    localparam logic [4:0] MMI3_PCPYUD  = 5'h0E;
    // Ch300 — R5900 EE MMI3/PCPYH (Parallel Copy Halfword), opcode
    // 0x1C, funct 0x29 (MMI3), sa 0x1B. Broadcasts the low halfword
    // of each 64-bit doubleword of $rt across the four halfword lanes
    // of the corresponding doubleword in $rd. rs is ignored.
    //   h0 = $rt[15:0],   h4 = $rt[79:64]
    //   $rd low 64  = {h0, h0, h0, h0}
    //   $rd high 64 = {h4, h4, h4, h4}
    // qbert.elf hits `pcpyh $v1, $t0` (instr 0x70081EE9) at PC
    // 0x00110BB4 — first call that uses MMI3 with sa=0x1B after
    // Ch299's library-gate unblock.
    localparam logic [4:0] MMI3_PCPYH   = 5'h1B;
    // Ch271 — R5900 EE Store Quadword. opcode=0x1F, I-type:
    //   sq rt, imm(base)  → store 128 bits ([rt[127:0]]) to mem[base+imm].
    // First real-ELF (qbert.elf) hit `sq $zero, 0($v0)` 8 instructions
    // into its prolog. Minimal scope: 4-beat 32-bit-stripe write
    // through the existing 32-bit map_wr_data port. Upper 96 bits of
    // GPRs aren't modelled, so for non-$zero rt the lower 32 land at
    // beat 0 and beats 1-3 write zero (degrades gracefully for the
    // common "clear a 128-bit slot" use case). Requires 16-byte
    // alignment; misaligned trips the existing AdES path.
    localparam logic [5:0] OP_SQ      = 6'h1F;
    // Ch275 — R5900 EE Store Doubleword. MIPS-III opcode 0x3F:
    //   sd rt, imm(base) → store 64 bits ([rt[63:0]]) to mem[base+imm].
    // qbert.elf hits `sd $ra, 0x20($sp)` (0xFFBF0020) in a function
    // prologue at PC 0x00112DAC. Modelled as a 2-beat 32-bit-stripe
    // write FSM through the existing map_wr port. Upper 32 bits of
    // GPRs aren't modelled, so beat 0 lands rt_val[31:0] and beat 1
    // writes 0 — matching the SQ approximation. Requires 8-byte
    // alignment; misaligned falls into the existing AdES path.
    localparam logic [5:0] OP_SD      = 6'h3F;
    // Ch284 — MIPS-III LD (Load Doubleword), opcode 0x37. The read-
    // side of SD. 2-beat 32-bit load FSM (sq_beat counter, terminal
    // beat = 1) reusing the LQ map-driver beat addressing. Beat 0
    // captures mem[ea+0] into gpr128[rt][31:0] and mirrors low 32 to
    // regfile[rt]; beat 1 captures mem[ea+4] into gpr128[rt][63:32].
    // gpr128[rt][127:64] is unchanged (architectural LD only loads
    // doubleword; upper 64 of $rt are preserved in real R5900). 8-byte
    // alignment required; misaligned trips the existing AdEL path.
    // qbert.elf hits `ld $ra, 0($ra)` (0xDFBF0000) at PC 0x00113378
    // — the function-epilogue $ra restore.
    localparam logic [5:0] OP_LD      = 6'h37;
    // Ch178 — CACHE accepted as legal no-op. The EE BIOS issues
    // CACHE (often Hit-Invalidate or Index-Invalidate of the
    // I-cache, op=0x05) after copying exception handlers / trampolines
    // into RAM to flush stale icache lines. The stub has no modeled
    // caches, so executing CACHE as a semantic NOP (no register/memory
    // side effects, PC advances normally) is the standard pragmatic
    // choice — see Codex Ch178 caution: accept the whole CACHE class
    // regardless of sub-op (`rt` field). If a later chapter actually
    // models cache coherency, this is the natural place to revisit.
    localparam logic [5:0] OP_CACHE   = 6'h2F;

    localparam logic [5:0] FUNC_SLL      = 6'h00;
    // Ch276 — R5900 DSLL (Doubleword Shift Left Logical), MIPS-III
    // SPECIAL funct 0x38. Architecturally a 64-bit left shift by
    // `sa` (sa ∈ [0,31]). Our regfile is 32-bit, so for any valid
    // sa < 32 the low 32-bit result is identical to SLL. PS2 ELFs
    // use DSLL to build 64-bit constants and to do unsigned shifts
    // where the implicit 64-bit width matters; qbert.elf hits
    // `dsll $t1, $t1, 16` at PC 0x00112C54.
    localparam logic [5:0] FUNC_DSLL     = 6'h38;
    localparam logic [5:0] FUNC_SRL      = 6'h02;
    localparam logic [5:0] FUNC_SRA      = 6'h03;
    // Ch67 — variable-shift family. Operand = rt_val, shift amount =
    // rs_val[4:0], destination = rd. Surfaced by the real-BIOS
    // copied-code region (first hit SRLV at pc=0x001A459C after
    // Ch64/Ch65 mirrors); SLLV/SRAV are the direct siblings and tend
    // to co-occur in shift-heavy code paths.
    localparam logic [5:0] FUNC_SLLV     = 6'h04;
    localparam logic [5:0] FUNC_SRLV     = 6'h06;
    localparam logic [5:0] FUNC_SRAV     = 6'h07;
    localparam logic [5:0] FUNC_JR       = 6'h08;
    localparam logic [5:0] FUNC_JALR     = 6'h09;
    localparam logic [5:0] FUNC_SYSCALL  = 6'h0C;
    // Ch292 — MIPS SYNC (SPECIAL funct 0x0F). Memory-ordering barrier;
    // architecturally orders prior loads/stores against subsequent ones.
    // In this in-order stub model with no cache coherency, store buffer,
    // or out-of-order memory, SYNC has no visible side effect. qbert.elf
    // hits the canonical encoding 0x0000000F (rs/rt/rd/sa all zero) at
    // PC 0x00112994 — the post-registration memory barrier after the
    // Ch290/291 paired Add+Enable handler calls.
    localparam logic [5:0] FUNC_SYNC     = 6'h0F;
    // SPECIAL-encoded HI/LO ops (ch43 addition):
    //   MFHI=0x10, MFLO=0x12, DIVU=0x1B.
    // MFHI shares its numeric value with RFE below (both 6'h10),
    // but RFE lives under OP_COP0 while MFHI lives under OP_SPECIAL
    // — different opcodes so the collision is harmless.
    localparam logic [5:0] FUNC_MFHI     = 6'h10;
    localparam logic [5:0] FUNC_MFLO     = 6'h12;
    localparam logic [5:0] FUNC_MULTU    = 6'h19;
    localparam logic [5:0] FUNC_DIVU     = 6'h1B;
    localparam logic [5:0] FUNC_RFE      = 6'h10;
    // Ch286 — R5900 EI (Enable Interrupts), EE-specific extension to
    // the MIPS COP0 CO sub-table. qbert.elf hits the exact 32-bit
    // encoding 0x42000038 (opcode=COP0, rs=CO, rt/rd/sa=0, funct=0x38)
    // at PC 0x001000FC during init. Codex framing: decode the EXACT
    // 32-bit instruction, accept it as side-effect-free (no GPR
    // writeback, PC += 4, no halt/trap), and do NOT NOP-class all
    // COP0/CO instructions. The companion DI (funct 0x39) is left
    // trapping until a future ELF surfaces it.
    localparam logic [31:0] EI_INSTR_R5900 = 32'h4200_0038;
    localparam logic [5:0] FUNC_ADD      = 6'h20;
    localparam logic [5:0] FUNC_ADDU     = 6'h21;
    // Ch272 — R5900 DADDU (doubleword add unsigned), MIPS-III SPECIAL
    // funct 0x2D. Architecturally a 64-bit unsigned add, with no
    // overflow trap. Our regfile is 32-bit, so we model it as ADDU
    // (low 32 bits only); upper 32 bits silently dropped as
    // elsewhere. PS2 ELFs use DADDU as the canonical 64-bit
    // `move rd, rs` (= daddu rd, rs, $zero) pseudo-instruction;
    // qbert.elf hits this at PC 0x00100068.
    localparam logic [5:0] FUNC_DADDU    = 6'h2D;
    localparam logic [5:0] FUNC_SUB      = 6'h22;
    localparam logic [5:0] FUNC_SUBU     = 6'h23;
    // Ch305 — R5900 DSUBU (doubleword subtract unsigned), MIPS-III
    // SPECIAL funct 0x2F. The 64-bit-subtract sibling of DADDU
    // (Ch272). Our regfile is 32-bit, so modelled as SUBU on the low
    // 32 bits, no overflow trap (matching the DADDU low-32
    // approximation). qbert.elf hits `dsubu $v0, $v1, $v0`
    // (instr 0x0062102F) at PC 0x00110A60.
    localparam logic [5:0] FUNC_DSUBU    = 6'h2F;
    localparam logic [5:0] FUNC_AND      = 6'h24;
    localparam logic [5:0] FUNC_OR       = 6'h25;
    localparam logic [5:0] FUNC_XOR      = 6'h26;
    localparam logic [5:0] FUNC_NOR      = 6'h27;
    localparam logic [5:0] FUNC_SLT      = 6'h2A;
    localparam logic [5:0] FUNC_SLTU     = 6'h2B;

    localparam logic [4:0] COP0_RS_MF = 5'h00;
    localparam logic [4:0] COP0_RS_MT = 5'h04;
    localparam logic [4:0] COP0_RS_CO = 5'h10;

    localparam logic [4:0] COP0_REG_BADVADDR = 5'd8;   // ch49 addition
    localparam logic [4:0] COP0_REG_COUNT    = 5'd9;
    localparam logic [4:0] COP0_REG_STATUS   = 5'd12;
    localparam logic [4:0] COP0_REG_CAUSE    = 5'd13;
    localparam logic [4:0] COP0_REG_EPC      = 5'd14;

    // Ch49: MIPS exception code values for synchronous address
    // errors. EXC_CODE_ADEL fires on a misaligned load (LW/LH/LHU);
    // EXC_CODE_ADES fires on a misaligned store (SW/SH). Other
    // ExcCode values (Int=0, Sys=8, Bp=9, RI=10, CpU=11, Ov=12,
    // Tr=13) are not yet modelled — they'll land when real BIOS
    // code demands them, same way the opcode batches grew earlier.
    localparam logic [4:0] EXC_CODE_INT  = 5'd0;
    localparam logic [4:0] EXC_CODE_ADEL = 5'd4;
    localparam logic [4:0] EXC_CODE_ADES = 5'd5;

    // ------------------------------------------------------------------
    // FSM state
    // ------------------------------------------------------------------

    typedef enum logic [3:0] {
        S_IDLE         = 4'd0,
        S_IFETCH_REQ   = 4'd1,
        S_IFETCH_WAIT  = 4'd2,
        S_EXECUTE      = 4'd3,
        S_MEM_REQ      = 4'd4,
        S_MEM_WAIT     = 4'd5,
        S_MEM_WRITE    = 4'd6,
        S_HALT         = 4'd7,
        // Ch215 — labeled sim-only jmp_buf restore for SYSCALL #8
        // ($v1=8, $a0=2). 12-step LW sequence walks frame at
        // CH215_JMPBUF_BASE (proven hardcoded constant 0xA000B1E0 by
        // Ch214). Each step issues one LW (S_CH215_REQ) and captures
        // its return value into the right regfile slot (S_CH215_WAIT).
        S_CH215_REQ    = 4'd8,
        S_CH215_WAIT   = 4'd9
    } state_e;

    state_e state;
    // Ch215 — jmp_buf restore FSM counter (0..11 walking 12 slots).
    logic [3:0]  ch215_count;
    // Ch271 — SQ 4-beat write counter. While SQ is in flight, this
    // counts 0..3; each beat writes one 32-bit lane at ea + beat*4.
    logic [1:0]  sq_beat;

    logic [31:0] pc;
    logic [31:0] instr;
    logic [31:0] regfile [0:31];

    // Ch283 — 128-bit GPR shadow. The R5900 architectural register is
    // 128 bits, but every prior chapter has lived inside a "low 32
    // only" approximation. gpr128 is parallel storage: scalar writes
    // mirror their result into gpr128[X][31:0] with the upper 96 bits
    // zeroed (matches the R5900 rule that scalar ops clear the upper
    // bits of the destination). MMI ops and LQ are the only producers
    // of non-zero upper bits; PCPYUD (Ch283) and similar upper-half
    // readers consume from this shadow. regfile remains the canonical
    // scalar surface so existing decode/ALU/load logic is unchanged.
    logic [127:0] gpr128 [0:31];

    // Ch43: architectural HI/LO register pair, populated by DIVU
    // (and, when added later, DIV/MULT/MULTU) and read back through
    // MFHI/MFLO. Reset to 0. Divide-by-zero policy: leave HI/LO
    // unchanged (deterministic, simplest; real HW treats the result
    // as undefined on DIVU with rt=0, so any consistent stub is
    // spec-compatible).
    logic [31:0] hi_reg;
    logic [31:0] lo_reg;

    logic        branch_pending;
    logic [31:0] branch_target;
    logic        instr_in_delay_slot;

    logic        status_iec, status_iep, status_ieo;
    logic        status_kuc, status_kup, status_kuo;
    logic [7:0]  status_im;
    // Ch50: BEV bit (bit 22 of Status). Controls the exception
    // vector base. 1 after reset (if INIT_BEV=1) so exceptions go
    // to BEV_EXC_VECTOR; cleared by BIOS via MTC0 once its RAM
    // handler is installed, which switches exceptions to
    // EXC_VECTOR.
    logic        status_bev;

    logic [4:0]  cause_exc_code;
    logic [7:0]  cause_ip_sw;
    logic [31:0] epc;
    // Ch49: COP0 BadVAddr. Captures the offending effective address
    // on a synchronous address-error exception (AdEL/AdES). Read-
    // only via MFC0; real HW also updates this on TLB exceptions,
    // which we don't model yet. Default 0 until the first fault.
    logic [31:0] badvaddr;

    // COP0 Count (reg 9): free-running 32-bit cycle counter. Increments
    // every clock, resets to 0. Exposed read-only through MFC0 $9 — MTC0
    // writes are silently dropped by the existing cop0_write default
    // case (real hardware allows software to reset Count, but we haven't
    // seen BIOS depend on that yet; revisit if a real-BIOS wait loop
    // breaks because of the unreset counter).
    //
    // Rate: advances once per CPU clock. Real R5900 Count advances at
    // half CPU clock, but any BIOS polling loop of the form
    //   do { x = mfc0 $9; } while (x < target);
    // will exit in a bounded number of cycles regardless — the only
    // observable effect of the faster rate is that waits finish sooner
    // in sim than on hardware, which is fine for bring-up.
    logic [31:0] cop0_count;

    logic [7:0]  cause_ip;
    always_comb begin
        cause_ip        = 8'd0;
        cause_ip[1:0]   = cause_ip_sw[1:0];
        cause_ip[2]     = cpu_irq;
    end

    logic [31:0] status_word;
    logic [31:0] cause_word;
    always_comb begin
        status_word       = 32'd0;
        status_word[0]    = status_iec;
        status_word[1]    = status_kuc;
        status_word[2]    = status_iep;
        status_word[3]    = status_kup;
        status_word[4]    = status_ieo;
        status_word[5]    = status_kuo;
        status_word[15:8] = status_im;
        status_word[22]   = status_bev;   // Ch50

        cause_word        = 32'd0;
        cause_word[6:2]   = cause_exc_code;
        cause_word[15:8]  = cause_ip;
    end

    // ------------------------------------------------------------------
    // Decode — combinational extraction from `instr`
    // ------------------------------------------------------------------

    logic [5:0]  opcode;
    logic [4:0]  rs_idx;
    logic [4:0]  rt_idx;
    logic [4:0]  rd_idx;
    logic [4:0]  shamt;
    logic [5:0]  func;
    logic [15:0] imm16;
    logic [25:0] imm26;
    logic [31:0] imm_sx;
    logic [31:0] imm_zx;
    logic [31:0] branch_offset;
    logic [31:0] branch_tgt;
    logic [31:0] j_tgt;
    logic [31:0] rs_val;
    logic [31:0] rt_val;
    logic [31:0] ea;
    // Ch283 — 128-bit reads from the gpr128 shadow. Used by MMI ops
    // and LQ-consuming downstream code that needs the full 128-bit
    // architectural view. $0 reads as 128'd0 (architectural).
    logic [127:0] rs128_val;
    logic [127:0] rt128_val;

    assign opcode        = instr[31:26];
    assign rs_idx        = instr[25:21];
    assign rt_idx        = instr[20:16];
    assign rd_idx        = instr[15:11];
    assign shamt         = instr[10:6];
    assign imm16         = instr[15:0];
    assign imm26         = instr[25:0];
    assign func          = instr[5:0];
    assign imm_sx        = {{16{imm16[15]}}, imm16};
    assign imm_zx        = {16'd0, imm16};
    assign branch_offset = {{14{imm16[15]}}, imm16, 2'b00};
    assign branch_tgt    = pc + 32'd4 + branch_offset;
    assign j_tgt         = {pc[31:28], imm26, 2'b00};
    assign rs_val        = (rs_idx == 5'd0) ? 32'd0 : regfile[rs_idx];
    assign rt_val        = (rt_idx == 5'd0) ? 32'd0 : regfile[rt_idx];
    assign rs128_val     = (rs_idx == 5'd0) ? 128'd0 : gpr128[rs_idx];
    assign rt128_val     = (rt_idx == 5'd0) ? 128'd0 : gpr128[rt_idx];
    assign ea            = rs_val + imm_sx;

    logic is_special, is_syscall, is_jr, is_jalr, is_sync;
    logic is_and, is_or, is_xor, is_nor;
    logic is_add, is_addu, is_daddu, is_sub, is_subu, is_dsubu;
    logic is_slt, is_sltu;
    logic is_sll, is_srl, is_sra, is_dsll;
    logic is_sllv, is_srlv, is_srav;
    logic is_rtype_alu;
    logic is_multu, is_divu, is_mfhi, is_mflo, is_hilo_op;
    logic is_cop0, is_mfc0, is_mtc0, is_rfe, is_ei;
    logic is_nop_class;
    logic is_lui, is_ori, is_andi;
    logic is_addi, is_addiu, is_slti, is_sltiu;
    logic is_lw, is_lb, is_lh, is_lhu, is_lbu, is_lq, is_ld;
    logic is_sw, is_sb, is_sh, is_sq, is_sd;
    logic is_beq, is_beql, is_bne, is_bnel, is_j, is_jal;
    logic is_blez, is_bgtz, is_regimm, is_bltz, is_bgez;
    logic is_branch, is_jump;
    logic branch_taken;
    logic is_taken_branch_or_jump;
    logic is_cache;     // Ch178 — opcode 0x2F, accepted as no-op

    assign is_special = (opcode == OP_SPECIAL);
    assign is_syscall = is_special && (func == FUNC_SYSCALL);
    assign is_sync    = is_special && (func == FUNC_SYNC);   // Ch292
    assign is_jr      = is_special && (func == FUNC_JR);
    assign is_jalr    = is_special && (func == FUNC_JALR);
    assign is_and     = is_special && (func == FUNC_AND);
    assign is_or      = is_special && (func == FUNC_OR);
    assign is_xor     = is_special && (func == FUNC_XOR);
    assign is_nor     = is_special && (func == FUNC_NOR);
    assign is_add     = is_special && (func == FUNC_ADD);
    assign is_addu    = is_special && (func == FUNC_ADDU);
    assign is_daddu   = is_special && (func == FUNC_DADDU);
    assign is_sub     = is_special && (func == FUNC_SUB);
    assign is_subu    = is_special && (func == FUNC_SUBU);
    assign is_dsubu   = is_special && (func == FUNC_DSUBU);   // Ch305
    assign is_slt     = is_special && (func == FUNC_SLT);
    assign is_sltu    = is_special && (func == FUNC_SLTU);
    assign is_sll     = is_special && (func == FUNC_SLL);
    assign is_dsll    = is_special && (func == FUNC_DSLL);
    // Ch278/Ch280 — MMI sub-instruction narrow-decodes. Each
    // recognized op fires only when the exact (opcode + funct + sa)
    // triple matches; everything else under opcode 0x1C falls
    // through to strict-trap.
    logic is_mmi;
    logic is_pcpyld;   // Ch278 — MMI2 / sa 0x0E
    logic is_psubb;    // Ch280 — MMI0 / sa 0x09
    logic is_pnor;     // Ch281 — MMI3 / sa 0x13
    logic is_pand;     // Ch282 — MMI2 / sa 0x12
    logic is_pcpyud;   // Ch283 — MMI3 / sa 0x0E
    logic is_pcpyh;    // Ch300 — MMI3 / sa 0x1B
    assign is_mmi     = (opcode == OP_MMI);
    assign is_pcpyld  = is_mmi
                     && (func  == FUNC_MMI2)
                     && (shamt == MMI2_PCPYLD);
    assign is_psubb   = is_mmi
                     && (func  == FUNC_MMI0)
                     && (shamt == MMI0_PSUBB);
    assign is_pnor    = is_mmi
                     && (func  == FUNC_MMI3)
                     && (shamt == MMI3_PNOR);
    assign is_pand    = is_mmi
                     && (func  == FUNC_MMI2)
                     && (shamt == MMI2_PAND);
    assign is_pcpyud  = is_mmi
                     && (func  == FUNC_MMI3)
                     && (shamt == MMI3_PCPYUD);
    assign is_pcpyh   = is_mmi
                     && (func  == FUNC_MMI3)
                     && (shamt == MMI3_PCPYH);
    assign is_srl     = is_special && (func == FUNC_SRL);
    assign is_sra     = is_special && (func == FUNC_SRA);
    assign is_sllv    = is_special && (func == FUNC_SLLV);
    assign is_srlv    = is_special && (func == FUNC_SRLV);
    assign is_srav    = is_special && (func == FUNC_SRAV);
    assign is_rtype_alu = is_and || is_or || is_xor || is_nor
                          || is_add || is_addu || is_daddu  // Ch272 — DADDU
                          || is_sub || is_subu || is_dsubu  // Ch305 — DSUBU
                          || is_slt || is_sltu
                          || is_sll  || is_srl  || is_sra
                          || is_dsll                          // Ch276 — DSLL
                          || is_pcpyld                        // Ch278 — MMI2/PCPYLD
                          || is_psubb                         // Ch280 — MMI0/PSUBB
                          || is_pnor                          // Ch281 — MMI3/PNOR
                          || is_pand                          // Ch282 — MMI2/PAND
                          || is_pcpyud                        // Ch283 — MMI3/PCPYUD
                          || is_pcpyh                         // Ch300 — MMI3/PCPYH
                          || is_sllv || is_srlv || is_srav;
    // Ch43: HI/LO ops — DIVU writes HI/LO, MFHI/MFLO read them back
    // into rd. Separate from is_rtype_alu because DIVU has no rd
    // writeback and the MF* ops route through a different mux path.
    // Ch203 — MULTU (SPECIAL funct=0x19, $HI:$LO = unsigned rs*rt, no
    // rd writeback). Grouped with the HI/LO ops so it auto-excludes
    // from is_nop_class.
    assign is_multu   = is_special && (func == FUNC_MULTU);
    assign is_divu    = is_special && (func == FUNC_DIVU);
    assign is_mfhi    = is_special && (func == FUNC_MFHI);
    assign is_mflo    = is_special && (func == FUNC_MFLO);
    assign is_hilo_op = is_multu || is_divu || is_mfhi || is_mflo;
    assign is_cop0    = (opcode == OP_COP0);
    assign is_mfc0    = is_cop0 && (rs_idx == COP0_RS_MF);
    assign is_mtc0    = is_cop0 && (rs_idx == COP0_RS_MT);
    assign is_rfe     = is_cop0 && (rs_idx == COP0_RS_CO) && (func == FUNC_RFE);
    // Ch286 — narrow exact-32-bit match for R5900 EI (see EI_INSTR_R5900
    // comment block). Keeps every other COP0/CO encoding trapping.
    assign is_ei      = (instr == EI_INSTR_R5900);

    assign is_lui     = (opcode == OP_LUI);
    assign is_ori     = (opcode == OP_ORI);
    assign is_andi    = (opcode == OP_ANDI);
    assign is_addi    = (opcode == OP_ADDI);
    assign is_addiu   = (opcode == OP_ADDIU);
    assign is_slti    = (opcode == OP_SLTI);
    assign is_sltiu   = (opcode == OP_SLTIU);
    assign is_lw      = (opcode == OP_LW);
    assign is_lq      = (opcode == OP_LQ);
    assign is_ld      = (opcode == OP_LD);   // Ch284
    assign is_lb      = (opcode == OP_LB);
    assign is_lbu     = (opcode == OP_LBU);
    assign is_lh      = (opcode == OP_LH);
    assign is_lhu     = (opcode == OP_LHU);
    assign is_sw      = (opcode == OP_SW);
    assign is_sq      = (opcode == OP_SQ);
    assign is_sd      = (opcode == OP_SD);
    assign is_sb      = (opcode == OP_SB);
    assign is_sh      = (opcode == OP_SH);
    assign is_beq     = (opcode == OP_BEQ);
    assign is_beql    = (opcode == OP_BEQL);
    assign is_bne     = (opcode == OP_BNE);
    assign is_bnel    = (opcode == OP_BNEL);
    assign is_blez    = (opcode == OP_BLEZ);
    assign is_bgtz    = (opcode == OP_BGTZ);
    assign is_regimm  = (opcode == OP_REGIMM);
    assign is_bltz    = is_regimm && (rt_idx == REGIMM_BLTZ);
    assign is_bgez    = is_regimm && (rt_idx == REGIMM_BGEZ);
    assign is_j       = (opcode == OP_J);
    assign is_jal     = (opcode == OP_JAL);
    // Ch178 — CACHE class. We accept the whole opcode regardless
    // of sub-op (`rt` field) per Codex's caution; BIOS may issue
    // multiple cache ops in close succession (Hit-Invalidate /
    // Index-Invalidate / etc.) and the stub has no cache state to
    // distinguish them. Side-effect-free: no register or memory
    // writes, PC advances via the normal "not a branch / not a
    // load / not a store" path.
    assign is_cache   = (opcode == OP_CACHE);

    assign is_branch  = is_beq || is_beql || is_bne || is_bnel
                        || is_blez || is_bgtz
                        || is_bltz || is_bgez;
    assign is_jump    = is_j || is_jal || is_jr || is_jalr;
    assign branch_taken = (is_beq  && (rs_val == rt_val))
                        || (is_beql && (rs_val == rt_val))  // Ch274 — BEQL taken
                        || (is_bne  && (rs_val != rt_val))
                        || (is_bnel && (rs_val != rt_val))  // Ch277 — BNEL taken
                        || (is_blez && ($signed(rs_val) <= 0))
                        || (is_bgtz && ($signed(rs_val) >  0))
                        || (is_bltz && ($signed(rs_val) <  0))
                        || (is_bgez && ($signed(rs_val) >= 0));
    assign is_taken_branch_or_jump = branch_taken || is_jump;

    // Ch274/Ch277 — branch-likely not-taken: squash the delay slot.
    // Skip PC by 8 so the delay-slot instruction at PC+4 is never
    // fetched or retired. Generalized to cover BEQL + BNEL; adding
    // BLEZL/BGTZL/REGIMM-likely later is a one-line OR-extension.
    logic is_branch_likely_squash;
    assign is_branch_likely_squash =
            (is_beql && (rs_val != rt_val))   // Ch274 — BEQL not-taken
         || (is_bnel && (rs_val == rt_val));  // Ch277 — BNEL not-taken

    assign is_nop_class = (is_special
                             && !is_syscall && !is_jr && !is_jalr
                             && !is_rtype_alu
                             && !is_hilo_op
                             && !is_sync)             // Ch292 — narrow SYNC
                          || (is_cop0 && !is_mfc0 && !is_mtc0 && !is_rfe
                                       && !is_ei)            // Ch286 — narrow EI
                          // REGIMM: BLTZ/BGEZ are handled; other rt
                          // encodings (e.g. BLTZAL=0x10, BGEZAL=0x11
                          // link variants) trap as unsupported until
                          // a real-BIOS path needs them.
                          || (is_regimm && !is_bltz && !is_bgez)
                          || (!is_special && !is_cop0 && !is_regimm
                              && !is_lui && !is_ori && !is_andi
                              && !is_addi && !is_addiu
                              && !is_slti && !is_sltiu
                              && !is_lw && !is_lb && !is_lbu && !is_lh && !is_lhu
                              && !is_lq                          // Ch279 — LQ
                              && !is_ld                          // Ch284 — LD
                              && !is_sw && !is_sb && !is_sh
                              && !is_sq                          // Ch271 — SQ
                              && !is_sd                          // Ch275 — SD
                              && !is_beq && !is_beql && !is_bne && !is_bnel  // Ch274/Ch277
                              && !is_blez && !is_bgtz
                              && !is_j && !is_jal
                              && !is_pcpyld                      // Ch278 — MMI2/PCPYLD only
                              && !is_psubb                       // Ch280 — MMI0/PSUBB only
                              && !is_pnor                        // Ch281 — MMI3/PNOR only
                              && !is_pand                        // Ch282 — MMI2/PAND only
                              && !is_pcpyud                      // Ch283 — MMI3/PCPYUD only
                              && !is_pcpyh                       // Ch300 — MMI3/PCPYH only
                              && !is_cache);  // Ch178

    logic is_nop_instr;
    logic is_unsupported;
    logic strict_trap;
    assign is_nop_instr   = (instr == 32'd0);
    assign is_unsupported = is_nop_class && !is_nop_instr;
    assign strict_trap    = STRICT_UNSUPPORTED && is_unsupported;

    // Ch47: AdEL/AdES detection. `ea` is the effective address of
    // the pending load/store (rs_val + imm_sx). MIPS requires SW /
    // LW to be word-aligned (ea[1:0]==0) and SH / LH / LHU to be
    // halfword-aligned (ea[0]==0). SB / LB / LBU are unconstrained.
    // Anything else is an address-error exception on real HW; in
    // this stub we route it through the same trap machinery as
    // STRICT_UNSUPPORTED so the first misaligned access halts
    // loudly instead of silently aliasing to the nearest aligned
    // slot (ch46 evidence showed the old behavior was clobbering
    // neighbouring words and confusing post-run analysis).
    logic is_word_access;
    logic is_half_access;
    logic is_align_fault;
    logic align_trap;
    logic align_except;
    logic is_align_store;
    assign is_word_access  = is_sw || is_lw;
    assign is_half_access  = is_sh || is_lh || is_lhu;
    // Ch271/Ch279 — SQ + LQ require 16-byte alignment (ea[3:0] == 0).
    // Ch275 — SD requires 8-byte alignment (ea[2:0] == 0).
    logic is_quad_access;
    logic is_dword_access;
    assign is_quad_access  = is_sq || is_lq;          // Ch279 — add LQ
    assign is_dword_access = is_sd || is_ld;          // Ch284 — add LD
    assign is_align_fault  = (is_word_access  && (ea[1:0] != 2'd0))
                          || (is_half_access  && (ea[0]   != 1'b0))
                          || (is_quad_access  && (ea[3:0] != 4'd0))
                          || (is_dword_access && (ea[2:0] != 3'd0));
    assign is_align_store  = is_sw || is_sh || is_sq || is_sd;
    // Ch47: when TRAP_ALIGN_ERROR=1 the fault halts the core (debug
    // mode used by tb_ee_core_align and default for iteration).
    assign align_trap      = TRAP_ALIGN_ERROR && is_align_fault;
    // Ch49: when TRAP_ALIGN_ERROR=0 the fault vectors to EXC_VECTOR
    // through the standard MIPS exception path (BadVAddr + Cause
    // ExcCode + EPC + Status IE/KU stack shift).
    assign align_except    = !TRAP_ALIGN_ERROR && is_align_fault;

    // Ch50: select exception vector based on Status.BEV. Interrupts
    // and synchronous faults (AdEL/AdES) both use this. When BEV=1
    // (boot / reset), exceptions go to the ROM bootstrap vector
    // (BEV_EXC_VECTOR, default 0xBFC0_0380 — the standard MIPS
    // R4000 "common" vector for BEV=1). When BEV=0 (BIOS has
    // installed its RAM handler), they go to EXC_VECTOR.
    logic [31:0] exc_target_pc;
    assign exc_target_pc = status_bev ? BEV_EXC_VECTOR : EXC_VECTOR;

    logic [31:0] alu_wb;
    always_comb begin
        if      (is_lui)                  alu_wb = {imm16, 16'd0};
        else if (is_ori)                  alu_wb = rs_val | imm_zx;
        else if (is_andi)                 alu_wb = rs_val & imm_zx;
        else if (is_addi || is_addiu)     alu_wb = rs_val + imm_sx;
        else if (is_slti)                 alu_wb = ($signed(rs_val) < $signed(imm_sx))
                                                    ? 32'd1 : 32'd0;
        else if (is_sltiu)                alu_wb = (rs_val < imm_sx) ? 32'd1 : 32'd0;
        else                              alu_wb = 32'd0;
    end

    // R-type ALU (destination = rd). Families:
    //   arith (ADD/ADDU/SUB/SUBU):        func 0x20/0x21/0x22/0x23
    //   logic (AND/OR/XOR/NOR):           func 0x24-0x27
    //   compare (SLT/SLTU):               func 0x2A/0x2B
    //   shifts (SLL/SRL/SRA):             func 0x00/0x02/0x03
    // ADD/SUB architecturally trap on signed overflow (Arithmetic
    // Overflow exception); this core does not model that exception
    // yet, so ADD behaves as ADDU and SUB behaves as SUBU. Same
    // pragmatic policy as ADDI vs ADDIU. Real BIOS code emits
    // ADD/SUB in contexts where overflow cannot happen in practice;
    // the trapping variants would only matter if BIOS ever
    // deliberately overflows.
    // Shifts use `rt_val` as the operand and `shamt` (bits [10:6]
    // of instr) as the shift count; SRA uses $signed() with SV's
    // arithmetic right-shift (>>>) so the MSB propagates.
    // SLL/$0,$0,0 is architectural NOP; this block still produces
    // 0 and the rd_idx=0 guard in the writeback path blocks the
    // phantom regfile write.
    logic [31:0] rtype_alu_wb;
    always_comb begin
        if      (is_and || is_pand)   rtype_alu_wb = rs_val & rt_val;
        else if (is_or)               rtype_alu_wb = rs_val | rt_val;
        else if (is_xor)              rtype_alu_wb = rs_val ^ rt_val;
        else if (is_nor || is_pnor)   rtype_alu_wb = ~(rs_val | rt_val);
        else if (is_add || is_addu || is_daddu)
                                      rtype_alu_wb = rs_val + rt_val;
        else if (is_sub || is_subu || is_dsubu)  // Ch305 — DSUBU low-32
                                      rtype_alu_wb = rs_val - rt_val;
        else if (is_slt)              rtype_alu_wb = ($signed(rs_val) < $signed(rt_val))
                                                      ? 32'd1 : 32'd0;
        else if (is_sltu)             rtype_alu_wb = (rs_val < rt_val) ? 32'd1 : 32'd0;
        else if (is_sll || is_dsll)   rtype_alu_wb = rt_val <<  shamt;
        // Ch278 — MMI2/PCPYLD: 128-bit "pack lower doublewords"
        // collapses in our 32-bit model to `rd_low32 = rt_low32`.
        // Architectural rd[63:0]=rt[63:0] is observable here; the
        // rd[127:64]=rs[63:0] half is unrepresentable.
        else if (is_pcpyld)           rtype_alu_wb = rt_val;
        // Ch280 — MMI0/PSUBB: 4 parallel byte subtracts in the low
        // 32 bits (architectural is 16-way across 128 bits; upper
        // 96 unrepresentable). Each byte lane is independently
        // modulo-256; no carry/borrow propagates between bytes.
        else if (is_psubb) begin
            rtype_alu_wb[ 7: 0] = rs_val[ 7: 0] - rt_val[ 7: 0];
            rtype_alu_wb[15: 8] = rs_val[15: 8] - rt_val[15: 8];
            rtype_alu_wb[23:16] = rs_val[23:16] - rt_val[23:16];
            rtype_alu_wb[31:24] = rs_val[31:24] - rt_val[31:24];
        end
        // Ch283 — MMI3/PCPYUD: $rd[63:0] = $rt[127:64]. The low 32
        // bits of $rd come from $rt[95:64] (low 32 of $rt's upper
        // doubleword). regfile mirror lands this value; gpr128 path
        // below sets the full 128-bit result.
        else if (is_pcpyud)           rtype_alu_wb = rt128_val[95:64];
        // Ch300 — MMI3/PCPYH: broadcast $rt[15:0] across the low 64
        // halfword lanes. The low 32 of $rd = {h0, h0}.
        else if (is_pcpyh)            rtype_alu_wb = {rt128_val[15:0], rt128_val[15:0]};
        else if (is_srl)              rtype_alu_wb = rt_val >>  shamt;
        else if (is_sra)              rtype_alu_wb = $signed(rt_val) >>> shamt;
        // Ch67: variable-shift — shift amount comes from rs_val[4:0].
        else if (is_sllv)             rtype_alu_wb = rt_val <<  rs_val[4:0];
        else if (is_srlv)             rtype_alu_wb = rt_val >>  rs_val[4:0];
        else if (is_srav)             rtype_alu_wb = $signed(rt_val) >>> rs_val[4:0];
        else                          rtype_alu_wb = 32'd0;
    end

    // Ch283 — full 128-bit MMI writeback. Computed in parallel with
    // rtype_alu_wb (the legacy low-32 value); the writeback block
    // selects between {96'd0, rtype_alu_wb} (scalar) and
    // rtype_alu128_wb (MMI) when updating gpr128[rd]. Only the five
    // MMI ops modelled to date land here; everything else stays in
    // the "zero-extend the scalar result" path. The PSUBB body works
    // across all 16 lanes; PNOR/PAND across the full 128 bits;
    // PCPYLD/PCPYUD do their architectural doubleword swaps.
    logic [127:0] rtype_alu128_wb;
    logic         is_mmi_wb;
    assign is_mmi_wb = is_pcpyld || is_psubb || is_pnor
                    || is_pand   || is_pcpyud
                    || is_pcpyh;                              // Ch300
    always_comb begin
        rtype_alu128_wb = 128'd0;
        if (is_pcpyld) begin
            // $rd[127:64] = $rs[63:0]; $rd[63:0] = $rt[63:0].
            rtype_alu128_wb = {rs128_val[63:0], rt128_val[63:0]};
        end else if (is_psubb) begin
            // 16-way parallel byte subtract.
            for (int b = 0; b < 16; b++) begin
                rtype_alu128_wb[b*8 +: 8] =
                    rs128_val[b*8 +: 8] - rt128_val[b*8 +: 8];
            end
        end else if (is_pnor) begin
            rtype_alu128_wb = ~(rs128_val | rt128_val);
        end else if (is_pand) begin
            rtype_alu128_wb = rs128_val & rt128_val;
        end else if (is_pcpyud) begin
            // $rd[127:64] = $rs[127:64]; $rd[63:0] = $rt[127:64].
            rtype_alu128_wb = {rs128_val[127:64], rt128_val[127:64]};
        end else if (is_pcpyh) begin
            // Ch300 — Parallel Copy Halfword.
            //   h0 = $rt[15:0]   (low halfword of low D)
            //   h4 = $rt[79:64]  (low halfword of high D)
            //   $rd low 64  = {h0, h0, h0, h0}
            //   $rd high 64 = {h4, h4, h4, h4}
            // $rs is architecturally ignored.
            rtype_alu128_wb = { {4{rt128_val[79:64]}},
                                {4{rt128_val[15:0]}} };
        end
    end

    logic [31:0] cop0_read_val;
    always_comb begin
        unique case (rd_idx)
            COP0_REG_BADVADDR: cop0_read_val = badvaddr;
            COP0_REG_COUNT:    cop0_read_val = cop0_count;
            COP0_REG_STATUS:   cop0_read_val = status_word;
            COP0_REG_CAUSE:    cop0_read_val = cause_word;
            COP0_REG_EPC:      cop0_read_val = epc;
            default:           cop0_read_val = 32'd0;
        endcase
    end

    // Count advance. Separate from the main FSM reset block so the
    // counter's behaviour is locally self-contained and easy to audit.
    always_ff @(posedge clk) begin
        if (!rst_n) cop0_count <= 32'd0;
        else        cop0_count <= cop0_count + 32'd1;
    end

    logic [31:0] taken_target;
    always_comb begin
        if (is_jr || is_jalr)    taken_target = rs_val;
        else if (is_j || is_jal) taken_target = j_tgt;
        else                     taken_target = branch_tgt;
    end

    // ------------------------------------------------------------------
    // Trace book-keeping (captured at retire)
    // ------------------------------------------------------------------

    logic [31:0] retired_pc;
    logic [31:0] retired_instr;
    logic [31:0] retired_arg2;
    logic [31:0] retired_arg3;
    logic        retired_flag_write;
    logic        retired_flag_read;
    logic        retired_flag_branch;
    logic        retired_flag_halt;
    logic        retired_flag_in_delay;
    logic        retired_flag_except;
    logic        retired_flag_rfe;
    logic        retired_flag_trap;
    logic        retire_pulse;

    // ------------------------------------------------------------------
    // Map-port drive (combinational on state)
    // ------------------------------------------------------------------

    always_comb begin
        map_rd_en   = 1'b0;
        map_rd_addr = 32'd0;
        map_wr_en   = 1'b0;
        map_wr_addr = 32'd0;
        map_wr_data = 32'd0;
        map_wr_be   = 4'd0;

        case (state)
            S_IFETCH_REQ: begin
                map_rd_en   = 1'b1;
                map_rd_addr = pc;
            end
            S_MEM_REQ: begin
                map_rd_en   = 1'b1;
                // Ch283 — LQ drives a real 4-beat load FSM using
                // sq_beat as the counter (same counter SQ uses).
                // Ch284 — LD reuses the same beat addressing for 2
                // beats. LW/LB/LBU/LH/LHU stay single-beat with
                // sq_beat=0.
                map_rd_addr = (is_lq || is_ld)
                                ? (ea + {28'd0, sq_beat, 2'b00})
                                : ea;
            end
            // Ch215 — drive the read port for the jmp_buf restore
            // FSM. Address = base + count*4 (offsets 0..0x2C).
            S_CH215_REQ: begin
                map_rd_en   = 1'b1;
                map_rd_addr = CH215_JMPBUF_BASE
                              + {26'd0, ch215_count, 2'b00};
            end
            S_MEM_WRITE: begin
                map_wr_en   = 1'b1;
                // Ch271/Ch275 — multi-beat stores (SQ/SD) use
                // `ea + sq_beat*4` for the per-beat address. Single-
                // beat stores (SW/SB/SH) ignore sq_beat (which stays
                // at 0) and resolve to `ea`.
                map_wr_addr = (is_sq || is_sd)
                                ? (ea + {28'd0, sq_beat, 2'b00})
                                : ea;
                if (is_sq || is_sd) begin
                    // Ch283 — SQ/SD source per-beat from gpr128[rt],
                    // not the legacy "low 32 only, zeros above" path.
                    // SQ emits all four 32-bit lanes; SD emits the
                    // low two lanes (gpr128[rt][63:0]). Reads of gpr128
                    // for rt==0 resolve to 0 via the rt_idx==0 check
                    // already in the read helper.
                    case (sq_beat)
                        2'd0: map_wr_data = (rt_idx == 5'd0) ? 32'd0 : gpr128[rt_idx][31:0];
                        2'd1: map_wr_data = (rt_idx == 5'd0) ? 32'd0 : gpr128[rt_idx][63:32];
                        2'd2: map_wr_data = (rt_idx == 5'd0) ? 32'd0 : gpr128[rt_idx][95:64];
                        2'd3: map_wr_data = (rt_idx == 5'd0) ? 32'd0 : gpr128[rt_idx][127:96];
                    endcase
                    map_wr_be   = 4'b1111;
                end else if (is_sb) begin
                    // Byte store: broadcast rt_val[7:0] into the
                    // addressed lane; one-hot byte-enable selects it.
                    case (ea[1:0])
                        2'd0: begin
                            map_wr_data = {24'd0,       rt_val[7:0]};
                            map_wr_be   = 4'b0001;
                        end
                        2'd1: begin
                            map_wr_data = {16'd0, rt_val[7:0], 8'd0};
                            map_wr_be   = 4'b0010;
                        end
                        2'd2: begin
                            map_wr_data = {8'd0, rt_val[7:0], 16'd0};
                            map_wr_be   = 4'b0100;
                        end
                        2'd3: begin
                            map_wr_data = {rt_val[7:0], 24'd0};
                            map_wr_be   = 4'b1000;
                        end
                    endcase
                end else if (is_sh) begin
                    // Halfword store: broadcast rt_val[15:0] into the
                    // addressed halfword lane (ea[1] selects). 2-of-4
                    // byte-enable leaves the other halfword untouched.
                    // Aligned access assumed (ea[0]==0); unaligned
                    // halfword store is not modelled as an exception.
                    case (ea[1])
                        1'b0: begin
                            map_wr_data = {16'd0, rt_val[15:0]};
                            map_wr_be   = 4'b0011;
                        end
                        1'b1: begin
                            map_wr_data = {rt_val[15:0], 16'd0};
                            map_wr_be   = 4'b1100;
                        end
                    endcase
                end else begin
                    // SW — full word
                    map_wr_data = rt_val;
                    map_wr_be   = 4'b1111;
                end
            end
            default: ;
        endcase
    end

    // ------------------------------------------------------------------
    // Retire helper — pc advance, branch queuing, exception entry
    // ------------------------------------------------------------------

    task automatic retire_advance;
        logic [31:0] next_pc;
        logic        new_branch_pending;
        logic [31:0] new_branch_target;
        logic        irq_pending_masked;
        logic        exception_now;

        // Ch274/Ch277 — branch-likely squash: when BEQL/BNEL fires
        // and the condition is FALSE (not-taken), the delay slot is
        // squashed (not executed), so PC jumps directly to PC+8.
        // branch_pending is also forced low below so no stale
        // branch_target leaks through.
        if (is_branch_likely_squash)
            next_pc         = pc + 32'd8;
        else
            next_pc         = branch_pending ? branch_target : pc + 32'd4;
        new_branch_pending  = is_taken_branch_or_jump;
        new_branch_target   = taken_target;

        irq_pending_masked  = |(cause_ip & status_im);
        exception_now       = !new_branch_pending
                              && status_iec
                              && irq_pending_masked;

        if (exception_now) begin
            epc             <= next_pc;
            cause_exc_code  <= 5'h00;
            status_ieo      <= status_iep;
            status_iep      <= status_iec;
            status_iec      <= 1'b0;
            status_kuo      <= status_kup;
            status_kup      <= status_kuc;
            status_kuc      <= 1'b0;
            pc              <= exc_target_pc;  // Ch50: BEV select
            branch_pending  <= 1'b0;
            retired_flag_except <= 1'b1;
        end else begin
            pc              <= next_pc;
            branch_pending  <= new_branch_pending;
            if (new_branch_pending) branch_target <= new_branch_target;
            retired_flag_except <= 1'b0;
        end
    endtask

    // ------------------------------------------------------------------
    // Main FSM
    // ------------------------------------------------------------------

    always_ff @(posedge clk) begin
        if (!rst_n) begin
            state               <= S_IDLE;
            pc                  <= PC_RESET;
            instr               <= 32'd0;
            branch_pending      <= 1'b0;
            branch_target       <= 32'd0;
            instr_in_delay_slot <= 1'b0;
            ch215_count         <= 4'd0;
            sq_beat             <= 2'd0;

            status_iec          <= 1'b0;
            status_iep          <= 1'b0;
            status_ieo          <= 1'b0;
            status_kuc          <= 1'b0;
            status_kup          <= 1'b0;
            status_kuo          <= 1'b0;
            status_im           <= 8'd0;
            status_bev          <= INIT_BEV; // Ch50
            cause_exc_code      <= 5'd0;
            cause_ip_sw         <= 8'd0;
            epc                 <= 32'd0;
            badvaddr            <= 32'd0;   // Ch49

            retire_pulse          <= 1'b0;
            retired_pc            <= 32'd0;
            retired_instr         <= 32'd0;
            retired_arg2          <= 32'd0;
            retired_arg3          <= 32'd0;
            retired_flag_write    <= 1'b0;
            retired_flag_read     <= 1'b0;
            retired_flag_branch   <= 1'b0;
            retired_flag_halt     <= 1'b0;
            retired_flag_in_delay <= 1'b0;
            retired_flag_except   <= 1'b0;
            retired_flag_rfe      <= 1'b0;
            retired_flag_trap     <= 1'b0;

            trap_o                <= 1'b0;
            trap_pc_o             <= 32'd0;
            trap_instr_o          <= 32'd0;

            for (int i = 0; i < 32; i++) regfile[i] <= 32'd0;
            // Ch283 — gpr128 shadow starts at zero everywhere.
            for (int i = 0; i < 32; i++) gpr128[i] <= 128'd0;
            hi_reg <= 32'd0;
            lo_reg <= 32'd0;
        end else begin
            retire_pulse <= 1'b0;

            case (state)
                S_IDLE: begin
                    if (go_i) state <= S_IFETCH_REQ;
                end

                S_IFETCH_REQ: state <= S_IFETCH_WAIT;

                S_IFETCH_WAIT: begin
                    if (map_rd_valid) begin
                        instr               <= map_rd_data;
                        instr_in_delay_slot <= branch_pending;
                        state               <= S_EXECUTE;
                    end
                end

                S_EXECUTE: begin
                    retired_pc            <= pc;
                    retired_instr         <= instr;
                    retired_arg2          <= 32'd0;
                    retired_arg3          <= 32'd0;
                    retired_flag_write    <= 1'b0;
                    retired_flag_read     <= 1'b0;
                    retired_flag_branch   <= is_taken_branch_or_jump;
                    retired_flag_halt     <= 1'b0;
                    retired_flag_in_delay <= instr_in_delay_slot;
                    retired_flag_except   <= 1'b0;
                    retired_flag_rfe      <= 1'b0;
                    retired_flag_trap     <= 1'b0;

                    if (is_syscall) begin
                        // Ch199 — narrow _ReturnFromException(2)
                        // semantics: when SYSCALL fires with the
                        // exact contract $v1==8 && $a0==2, do NOT
                        // halt. Ch215 extends this with an optional
                        // jmp_buf restore (when CH215_JMPBUF_RESTORE_
                        // ENABLE=1) that walks the BIOS-side jmp_buf
                        // at CH215_JMPBUF_BASE and loads $ra/$sp/$fp/
                        // $s0..$s7/$gp from there before resuming.
                        // Other syscall variants keep the pre-Ch199
                        // halt path so the ch197 verdict still fires.
                        if (regfile[3] == 32'd8 && regfile[4] == 32'd2) begin
                            branch_pending      <= 1'b0;
                            status_iec          <= status_iep;
                            status_iep          <= status_ieo;
                            status_kuc          <= status_kup;
                            status_kup          <= status_kuo;
                            retired_flag_rfe    <= 1'b1;
                            retired_flag_halt   <= 1'b0;
                            retire_pulse        <= 1'b1;
                            if (CH215_JMPBUF_RESTORE_ENABLE) begin
                                // Ch215 — enter the 12-step jmp_buf
                                // restore FSM. PC update deferred
                                // until the FSM has loaded $ra at
                                // count=0; final transition to
                                // S_IFETCH_REQ sets pc<-regfile[31].
                                ch215_count <= 4'd0;
                                state       <= S_CH215_REQ;
                            end else begin
                                // Ch199 minimal: PC<-$k0, no GPR restore.
                                pc          <= regfile[26];
                                state       <= S_IFETCH_REQ;
                            end
                        end else if (EE_SYSCALL_HLE_ENABLE) begin
                            // Ch273 — minimal EE syscall HLE for the
                            // ELF runner. Known $v1 values get a stub
                            // return; unknown $v1 falls through to
                            // halt so the next blocker surfaces.
                            // PC advances to pc+4 (normal user-code
                            // syscall resume), NOT RFE — that's
                            // Ch199's path.
                            case (regfile[3])  // $v1
                                32'h0000_003C: begin
                                    // EndOfHeap — top-of-usable-RAM.
                                    regfile[2]   <= SYSCALL_HEAP_END;
                                    gpr128[2]    <= {96'd0, SYSCALL_HEAP_END}; // Ch283
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                32'h0000_003D: begin
                                    // InitMainThread — stub success.
                                    // No scheduler/thread state mutated.
                                    regfile[2]   <= 32'd0;
                                    gpr128[2]    <= 128'd0; // Ch283
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                32'h0000_0040: begin
                                    // Ch285 — syscall #64. qbert hits
                                    // this at PC 0x00111D24 with
                                    // $a0=heap-ish, $a1=code-ptr-ish.
                                    // Almost certainly a kernel
                                    // registration / handler-install
                                    // call (the standard PS2 syscall
                                    // table lists names like
                                    // SetVCommonHandler / SetV
                                    // TLBRefillHandler in this slot).
                                    // Per Codex framing, we accept it
                                    // with the least-invasive shape:
                                    // $v0 = 0 ("registered OK") and
                                    // PC += 4. If qbert misbranches
                                    // downstream, revisit and return
                                    // the previous handler pointer
                                    // instead of zero.
                                    regfile[2]   <= 32'd0;
                                    gpr128[2]    <= 128'd0;
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                32'h0000_0064: begin
                                    // FlushCache — cacheless model, no-op.
                                    regfile[2]   <= 32'd0;
                                    gpr128[2]    <= 128'd0; // Ch283
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                32'h0000_0078: begin
                                    // Ch289 — syscall #120. qbert hits
                                    // this at PC 0x00112AA4 with
                                    // $a1=0x00130000, $a2=0x20000000
                                    // (uncached-pointer base), $a3=
                                    // 0x001328C0. Args look like setup/
                                    // registration parameters (likely
                                    // threading/heap/uncached-memory
                                    // related per Codex framing). First
                                    // pass per the Ch285 precedent:
                                    // accept with $v0 = 0 ("kernel
                                    // setup OK") and PC += 4. If qbert
                                    // misbranches downstream, revisit
                                    // and try $a2/$a1 as the return.
                                    // The ELF runner adds a named
                                    // SUMMARY line for this syscall so
                                    // post-run analysis can confirm the
                                    // arg shape without re-reading the
                                    // trace file.
                                    regfile[2]   <= 32'd0;
                                    gpr128[2]    <= 128'd0;
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                32'h0000_006B: begin
                                    // Ch304 — syscall #107. qbert hits
                                    // this at PC 0x00111D64 (the 0x6B
                                    // wrapper in Table1 @0x00111D40).
                                    // Ch303 autopsy proved the caller
                                    // at 0x00111B00 IGNORES the return
                                    // value: after `jal 0x00111d60` the
                                    // next instruction (0x111B24) sets
                                    // $a1=0 without reading $v0. So
                                    // $v0=0 is safe. Args at the call:
                                    //   $a0=5 (channel), $a1=0,
                                    //   $a2=0xFFFFFFFF, $a3=0x00137568.
                                    // Per Codex: add 0x6B ALONE first
                                    // so the next run confirms the
                                    // Ch303-predicted table flow (next
                                    // blocker should be 0x76, 0x44, or
                                    // 0xFFFFFFBD). Ch305 batches the
                                    // rest once the flow is confirmed.
                                    regfile[2]   <= 32'd0;
                                    gpr128[2]    <= 128'd0;
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                32'h0000_0013: begin
                                    // Ch302 — syscall #19. qbert hits
                                    // this at PC 0x00112A64 right after
                                    // Ch301's 0x17 with LITERALLY
                                    // IDENTICAL args:
                                    //   $a0 = 5         (channel id)
                                    //   $a1 = 0
                                    //   $a2 = 0xFFFFFFFF (sentinel/-1)
                                    //   $a3 = 0x00137568 (per-channel
                                    //                    ctx, same as
                                    //                    0x17)
                                    // Second paired-call pattern on the
                                    // syscall track (first was Ch290/291
                                    // 0x12/0x16). 0x17+0x13 are the
                                    // "set + register" pair for channel
                                    // 5's new context. Per Codex: accept
                                    // ($v0 = 0, PC += 4); paired-call
                                    // symmetry makes this well-supported.
                                    regfile[2]   <= 32'd0;
                                    gpr128[2]    <= 128'd0;
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                32'h0000_0017: begin
                                    // Ch301 — syscall #23. qbert hits
                                    // this at PC 0x00112A84 right after
                                    // Ch300's PCPYH. Args:
                                    //   $a0 = 5         (channel id —
                                    //                    matches the
                                    //                    Ch290/291
                                    //                    handler slot)
                                    //   $a1 = 0
                                    //   $a2 = 0xFFFFFFFF (sentinel/-1)
                                    //   $a3 = 0x00137568 (NEW context
                                    //                    ptr, NOT the
                                    //                    prior global
                                    //                    ctx 0x001328C0
                                    //                    — second
                                    //                    context-shift
                                    //                    on syscall
                                    //                    track)
                                    // PS2 standard table cites syscall
                                    // 23 plausibly as SetVTLBRefill-
                                    // Handler or iWakeupThread. The
                                    // $a0=5 channel-id pattern + $a2=
                                    // -1 sentinel fit a per-channel
                                    // kernel call. Per Codex: accept
                                    // ($v0 = 0, PC += 4); the runner
                                    // observer with distinct-tuple
                                    // tracking will surface whether
                                    // qbert calls 0x17 multiple times
                                    // with varying args.
                                    regfile[2]   <= 32'd0;
                                    gpr128[2]    <= 128'd0;
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                32'h0000_0077: begin
                                    // Ch297 — syscall #119. qbert hits
                                    // this at PC 0x00111D84 right after
                                    // Ch296's 0x79 acceptance, with a
                                    // NOTICEABLY DIFFERENT arg shape:
                                    //   $a0 = 0x001DFD50 (heap address,
                                    //                    not kseg0 base)
                                    //   $a1 = 1
                                    //   $a2 = 0
                                    //   $a3 = 20 (small int, NOT the
                                    //                    global ctx
                                    //                    pointer threaded
                                    //                    through 0x78/
                                    //                    0x12/0x16/0x7A/
                                    //                    0x79)
                                    // The $a3 change is the strongest
                                    // "syscall-family boundary" signal
                                    // we've seen — qbert has crossed
                                    // into a different kernel call
                                    // convention. PS2 standard table
                                    // cites syscall 119 as plausibly
                                    // SetVTLBRefillHandler or
                                    // RegisterLibraryEntries. Per Codex:
                                    // accept first-pass ($v0 = 0,
                                    // PC += 4) but treat the runner-side
                                    // observer as the falsifiability
                                    // surface (richer than prior
                                    // observers — first/last args, up
                                    // to 4 distinct tuples).
                                    regfile[2]   <= 32'd0;
                                    gpr128[2]    <= 128'd0;
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                32'h0000_0079: begin
                                    // Ch296 — syscall #121. qbert hits
                                    // this at PC 0x00111D94 right after
                                    // Ch295's $a0-aware 0x7A patch
                                    // unblocked the wait loop. Args:
                                    //   $a0 = 0x80000000 (kseg0 base —
                                    //                    same shape as
                                    //                    Ch293's 0x7A
                                    //                    init call)
                                    //   $a1 = 0
                                    //   $a3 = 0x001328C0 (same global
                                    //                    ctx threaded
                                    //                    throughout)
                                    // PC sits in the same kernel-
                                    // wrapper neighborhood as the
                                    // Ch289 syscall 0x78 site (PC
                                    // 0x00111D24). Likely an
                                    // adjacent finalize/reset call.
                                    // Per Codex: accept ($v0 = 0,
                                    // PC += 4), keep marked as
                                    // adjacent/experimental like 0x7A.
                                    regfile[2]   <= 32'd0;
                                    gpr128[2]    <= 128'd0;
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                32'h0000_007A: begin
                                    // Ch293/Ch295 — syscall #122,
                                    // $a0-aware HLE.
                                    //
                                    // Ch294's autopsy showed qbert calls
                                    // syscall 0x7A with two distinct
                                    // arg shapes and expects different
                                    // return values:
                                    //   $a0 = 0x80000000  → $v0 = 0
                                    //         (init-style call;
                                    //          falls into wait loop)
                                    //   $a0 = 0x00000004  → $v0 with
                                    //         bit 17 (0x00020000) set
                                    //         (poll-style call; bit 17
                                    //          is qbert's readiness
                                    //          flag, mask in $s0 at
                                    //          PC 0x00112418).
                                    // Ch295 is Codex's "Strategy A"
                                    // EXPERIMENTAL unblock — return the
                                    // expected bit shape based on $a0
                                    // alone. Not architectural truth;
                                    // the real PS2 syscall 122 semantics
                                    // are still TBD. If qbert misbranches
                                    // downstream we back this out and
                                    // pursue real-SDK lookup or
                                    // interrupt-delivery wiring.
                                    if (regfile[4] == 32'h0000_0004) begin
                                        regfile[2] <= 32'h0002_0000;
                                        gpr128[2]  <= {96'd0, 32'h0002_0000};
                                    end else begin
                                        regfile[2] <= 32'd0;
                                        gpr128[2]  <= 128'd0;
                                    end
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                32'h0000_0016: begin
                                    // Ch291 — syscall #22. qbert hits
                                    // this at PC 0x00112A74 with args
                                    // LITERALLY IDENTICAL to the Ch290
                                    // syscall 0x12 call eight
                                    // instructions earlier:
                                    //   $a0 = 0x05 (channel/slot)
                                    //   $a1 = 0x00112AB0 (fn ptr)
                                    //   $a2 = 0x00000000
                                    //   $a3 = 0x001328C0 (ctx ptr)
                                    // PS2 standard syscall table cites
                                    // `EnableDmacHandler` (or
                                    // EnableIntcHandler) in this slot —
                                    // the activation companion to Ch290's
                                    // AddDmacHandler. Per Codex framing:
                                    // accept the enable ($v0 = 0, PC +=
                                    // 4); do NOT actually call the
                                    // handler or synthesize a DMAC
                                    // completion. If qbert subsequently
                                    // polls for the handler to fire,
                                    // Ch292 will need to model
                                    // handler-invocation; for now just
                                    // accept the registration+enable
                                    // pair and see what qbert demands.
                                    regfile[2]   <= 32'd0;
                                    gpr128[2]    <= 128'd0;
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                32'h0000_0012: begin
                                    // Ch290 — syscall #18. qbert hits
                                    // this at PC 0x00112A54 right after
                                    // Ch289's 0x78 with the classic
                                    // handler-install arg shape:
                                    //   $a0 = 0x05         (channel /
                                    //                       event id /
                                    //                       handler slot)
                                    //   $a1 = 0x00112AB0   (fn pointer
                                    //                       in code seg)
                                    //   $a2 = 0x00000000
                                    //   $a3 = 0x001328C0   (ctx ptr,
                                    //                       same global
                                    //                       block as
                                    //                       0x78's $a3)
                                    // PS2 standard syscall table cites
                                    // names like AddDmacHandler in slot
                                    // 18 ($a0 = DMAC channel; $a0=5 =
                                    // SIF0). Per Codex: accept the
                                    // registration ($v0 = 0, PC += 4),
                                    // do NOT invoke the handler or
                                    // mutate DMAC/INTC state. The ELF
                                    // runner observes args for the
                                    // first occurrence so the SUMMARY
                                    // shows the registration shape.
                                    regfile[2]   <= 32'd0;
                                    gpr128[2]    <= 128'd0;
                                    pc           <= pc + 32'd4;
                                    retire_pulse <= 1'b1;
                                    state        <= S_IFETCH_REQ;
                                end
                                default: begin
                                    // Unhandled — halt; TB reads
                                    // $v1/$a0..$a3 hierarchically for
                                    // the verdict.
                                    retired_flag_halt <= 1'b1;
                                    retire_pulse      <= 1'b1;
                                    state             <= S_HALT;
                                end
                            endcase
                        end else begin
                            retired_flag_halt <= 1'b1;
                            retire_pulse      <= 1'b1;
                            state             <= S_HALT;
                        end
                    end else if (strict_trap) begin
                        retired_flag_trap <= 1'b1;
                        retire_pulse      <= 1'b1;
                        trap_o            <= 1'b1;
                        trap_pc_o         <= pc;
                        trap_instr_o      <= instr;
                        state             <= S_HALT;
                    end else if (align_trap) begin
                        // Ch47: AdEL/AdES address error, debug-halt
                        // mode (TRAP_ALIGN_ERROR=1). retired_arg2
                        // carries the offending EA so traces name
                        // the byte address that caused the fault.
                        retired_flag_trap <= 1'b1;
                        retire_pulse      <= 1'b1;
                        trap_o            <= 1'b1;
                        trap_pc_o         <= pc;
                        trap_instr_o      <= instr;
                        retired_arg2      <= ea;
                        retired_arg3      <= 32'd0;
                        state             <= S_HALT;
                    end else if (align_except) begin
                        // Ch49: AdEL/AdES synchronous exception
                        // (TRAP_ALIGN_ERROR=0). Take the MIPS
                        // exception path instead of halting — the
                        // BIOS presumably has a handler at
                        // EXC_VECTOR that fixes up the misaligned
                        // access and RFEs back.
                        //
                        // EPC := pc of the FAULTING instruction
                        // (not next_pc as retire_advance does for
                        // inter-instruction interrupts). Real MIPS
                        // additionally sets Cause.BD=1 and writes
                        // EPC = (branch pc) when the fault fires in
                        // a delay slot; we don't model Cause.BD yet
                        // — a delay-slot AdES simply records the
                        // SW's own pc and the handler is expected
                        // to cope. Revisit if BIOS emulation
                        // depends on the BD bit.
                        epc             <= pc;
                        badvaddr        <= ea;
                        cause_exc_code  <= is_align_store ? EXC_CODE_ADES
                                                          : EXC_CODE_ADEL;
                        status_ieo      <= status_iep;
                        status_iep      <= status_iec;
                        status_iec      <= 1'b0;
                        status_kuo      <= status_kup;
                        status_kup      <= status_kuc;
                        status_kuc      <= 1'b0;
                        pc              <= exc_target_pc;  // Ch50: BEV select
                        branch_pending  <= 1'b0;

                        // Retire event: treat the fault as an
                        // exception-retire (flag bit 5), carry ea
                        // in arg2 so traces name the bad address
                        // directly, and clear the load/store flags
                        // since the SW/LW did not actually execute.
                        retired_pc          <= pc;
                        retired_instr       <= instr;
                        retired_arg2        <= ea;
                        retired_arg3        <= 32'd0;
                        retired_flag_write  <= 1'b0;
                        retired_flag_read   <= 1'b0;
                        retired_flag_branch <= 1'b0;
                        retired_flag_halt   <= 1'b0;
                        retired_flag_except <= 1'b1;
                        retired_flag_rfe    <= 1'b0;
                        retired_flag_trap   <= 1'b0;
                        retired_flag_in_delay <= instr_in_delay_slot;

                        retire_pulse <= 1'b1;
                        state        <= S_IFETCH_REQ;
                    end else if (is_lw || is_lb || is_lbu || is_lh || is_lhu) begin
                        state <= S_MEM_REQ;
                    end else if (is_lq) begin
                        // Ch283 — LQ: 4-beat 32-bit load FSM. Beat N
                        // captures mem[ea + N*4] into the matching
                        // 32-bit lane of gpr128[rt]. After beat 3, the
                        // low 32 are mirrored to regfile[rt]. Replaces
                        // the Ch279 single-beat LW-style approximation.
                        sq_beat <= 2'd0;
                        state   <= S_MEM_REQ;
                    end else if (is_ld) begin
                        // Ch284 — LD: 2-beat 32-bit load FSM mirroring
                        // SD's beat layout. Beat 0 → gpr128[rt][31:0]
                        // and regfile[rt] mirror; beat 1 → gpr128[rt]
                        // [63:32]. gpr128[rt][127:64] is preserved
                        // (architectural LD only loads doubleword).
                        sq_beat <= 2'd0;
                        state   <= S_MEM_REQ;
                    end else if (is_sw || is_sb || is_sh) begin
                        state <= S_MEM_WRITE;
                    end else if (is_sq) begin
                        // Ch271 — SQ: 4-beat 32-bit write FSM.
                        // Beat 0 emits the lower 32 bits of rt; beats
                        // 1-3 emit zero (upper 96 bits of $rt aren't
                        // modelled). For sq $zero,... all four beats
                        // are zero — the qbert prolog case.
                        sq_beat <= 2'd0;
                        state   <= S_MEM_WRITE;
                    end else if (is_sd) begin
                        // Ch275 — SD: 2-beat 32-bit write FSM.
                        // Beat 0 emits rt_val[31:0]; beat 1 emits 0
                        // (upper 32 bits of $rt aren't modelled). qbert
                        // does sd $ra, 0x20($sp) in a function prologue.
                        sq_beat <= 2'd0;
                        state   <= S_MEM_WRITE;
                    end else begin
                        if ((is_lui || is_ori || is_andi ||
                             is_addi || is_addiu ||
                             is_slti || is_sltiu) && (rt_idx != 5'd0)) begin
                            regfile[rt_idx] <= alu_wb;
                            // Ch283 — scalar mirror: zero-extend into the
                            // 128-bit shadow (R5900 clears upper bits on
                            // every scalar destination write).
                            gpr128[rt_idx]  <= {96'd0, alu_wb};
                        end

                        if (is_rtype_alu && (rd_idx != 5'd0)) begin
                            regfile[rd_idx] <= rtype_alu_wb;
                            // Ch283 — MMI ops get the full 128-bit
                            // result into gpr128; scalar ops just zero-
                            // extend their 32-bit result. The regfile
                            // mirror above always lands the low 32
                            // (rtype_alu_wb is computed accordingly).
                            if (is_mmi_wb)
                                gpr128[rd_idx] <= rtype_alu128_wb;
                            else
                                gpr128[rd_idx] <= {96'd0, rtype_alu_wb};
                        end

                        // Ch203: MULTU writes {HI, LO} = unsigned 64-bit
                        // product of (rs, rt). No rd writeback (rd bits
                        // are architecturally ignored). MFLO/MFHI in a
                        // following instruction observe the result.
                        if (is_multu) begin
                            logic [63:0] mu_product;
                            mu_product = {32'd0, rs_val} * {32'd0, rt_val};
                            lo_reg <= mu_product[31:0];
                            hi_reg <= mu_product[63:32];
                        end

                        // Ch43: DIVU writes LO=quotient, HI=remainder.
                        // Divisor==0 is UNDEFINED per MIPS spec; we
                        // take the deterministic "leave HI/LO
                        // unchanged" option. Ch162: STRIP_HW_DIVIDER
                        // gates the `/` and `%` operators away so
                        // Quartus doesn't infer the 32-bit hardware
                        // divider on PSMCT32-only hardware builds.
                        if (!STRIP_HW_DIVIDER && is_divu && (rt_val != 32'd0)) begin
                            lo_reg <= rs_val / rt_val;
                            hi_reg <= rs_val % rt_val;
                        end

                        // Ch43: MFHI/MFLO move HI/LO into rd. rd==0
                        // suppresses the write (architectural $0
                        // protection).
                        if (is_mfhi && (rd_idx != 5'd0)) begin
                            regfile[rd_idx] <= hi_reg;
                            gpr128[rd_idx]  <= {96'd0, hi_reg}; // Ch283 mirror
                        end
                        if (is_mflo && (rd_idx != 5'd0)) begin
                            regfile[rd_idx] <= lo_reg;
                            gpr128[rd_idx]  <= {96'd0, lo_reg}; // Ch283 mirror
                        end

                        // JAL: link address is pc + 8 (instruction after
                        // the delay slot). $31 is the architectural $ra.
                        if (is_jal) begin
                            regfile[5'd31] <= pc + 32'd8;
                            gpr128[5'd31]  <= {96'd0, pc + 32'd8}; // Ch283 mirror
                        end

                        // JALR: same pc+8 link semantics, but the link
                        // destination is explicit in rd. rd==0 suppresses
                        // the write (valid JALR encoding for "jump
                        // indirect without keeping a return address").
                        if (is_jalr && (rd_idx != 5'd0)) begin
                            regfile[rd_idx] <= pc + 32'd8;
                            gpr128[rd_idx]  <= {96'd0, pc + 32'd8}; // Ch283 mirror
                        end

                        if (is_mfc0 && (rt_idx != 5'd0)) begin
                            regfile[rt_idx] <= cop0_read_val;
                            gpr128[rt_idx]  <= {96'd0, cop0_read_val}; // Ch283 mirror
                        end

                        if (is_mtc0) begin
                            unique case (rd_idx)
                                COP0_REG_STATUS: begin
                                    status_iec   <= rt_val[0];
                                    status_kuc   <= rt_val[1];
                                    status_iep   <= rt_val[2];
                                    status_kup   <= rt_val[3];
                                    status_ieo   <= rt_val[4];
                                    status_kuo   <= rt_val[5];
                                    status_im    <= rt_val[15:8];
                                    status_bev   <= rt_val[22];   // Ch50
                                end
                                COP0_REG_CAUSE:  begin
                                    cause_exc_code <= rt_val[6:2];
                                    cause_ip_sw[1:0] <= rt_val[9:8];
                                end
                                COP0_REG_EPC:    epc <= rt_val;
                                default: ;
                            endcase
                        end

                        if (is_rfe) begin
                            status_iec       <= status_iep;
                            status_iep       <= status_ieo;
                            status_kuc       <= status_kup;
                            status_kup       <= status_kuo;
                            retired_flag_rfe <= 1'b1;
                        end

                        if (is_mfc0) begin
                            retired_arg2 <= {27'd0, rd_idx};
                            retired_arg3 <= cop0_read_val;
                        end else if (is_mtc0) begin
                            retired_arg2 <= {27'd0, rd_idx};
                            retired_arg3 <= rt_val;
                        end else if (is_taken_branch_or_jump) begin
                            retired_arg2 <= taken_target;
                            retired_arg3 <= 32'd0;
                        end else if (is_lui || is_ori || is_andi ||
                                     is_addi || is_addiu ||
                                     is_slti || is_sltiu) begin
                            retired_arg3 <= alu_wb;
                        end else if (is_rtype_alu) begin
                            retired_arg2 <= {27'd0, rd_idx};
                            retired_arg3 <= rtype_alu_wb;
                        end else if (is_multu) begin
                            // Ch203: arg2 = rt_val (so traces show
                            // both operands together with rs in
                            // arg0/regfile context), arg3 = LO half
                            // of the product = the value MFLO would
                            // read next. The full 64-bit product
                            // isn't carried in trace events; HI is
                            // exposed via the MFHI retire below.
                            retired_arg2 <= rt_val;
                            retired_arg3 <= ({32'd0, rs_val} * {32'd0, rt_val});
                        end else if (is_divu) begin
                            // arg2 = divisor (rt_val) so a div-by-zero
                            // event is obvious in traces; arg3 =
                            // quotient (or 0 on div-by-zero, since
                            // LO is left unchanged — the trace just
                            // records what *would* have been written).
                            // Ch162: STRIP_HW_DIVIDER gates the `/`
                            // here too so Quartus doesn't keep an
                            // inferred divider just for the trace.
                            retired_arg2 <= rt_val;
                            retired_arg3 <= (STRIP_HW_DIVIDER || rt_val == 32'd0)
                                            ? 32'd0
                                            : (rs_val / rt_val);
                        end else if (is_mfhi) begin
                            retired_arg2 <= {27'd0, rd_idx};
                            retired_arg3 <= hi_reg;
                        end else if (is_mflo) begin
                            retired_arg2 <= {27'd0, rd_idx};
                            retired_arg3 <= lo_reg;
                        end

                        retire_pulse <= 1'b1;
                        retire_advance();
                        state <= S_IFETCH_REQ;
                    end
                end

                S_MEM_REQ: state <= S_MEM_WAIT;

                S_MEM_WAIT: begin
                    if (map_rd_valid) begin
                        // Ch283/Ch284 — multi-beat loads. LQ takes 4
                        // beats (terminal = 2'd3), LD takes 2 (terminal
                        // = 2'd1). Each beat captures a 32-bit lane
                        // into gpr128[rt]; the last beat mirrors low
                        // 32 to regfile[rt] and retires. LD mirrors
                        // regfile on beat 0 (the moment the low 32 are
                        // actually written into gpr128) so the value
                        // is observable immediately on retire.
                        if (is_lq || is_ld) begin
                            logic [1:0] terminal_beat;
                            terminal_beat = is_lq ? 2'd3 : 2'd1;
                            if (rt_idx != 5'd0) begin
                                case (sq_beat)
                                    2'd0: gpr128[rt_idx][31:0]   <= map_rd_data;
                                    2'd1: gpr128[rt_idx][63:32]  <= map_rd_data;
                                    2'd2: gpr128[rt_idx][95:64]  <= map_rd_data;
                                    2'd3: gpr128[rt_idx][127:96] <= map_rd_data;
                                endcase
                                // Last beat mirrors low 32 to regfile.
                                // For LQ the low 32 was committed on
                                // beat 0; for LD the same. By terminal
                                // beat the NBA has settled, so reading
                                // gpr128[rt_idx][31:0] is safe.
                                if (sq_beat == terminal_beat)
                                    regfile[rt_idx] <= gpr128[rt_idx][31:0];
                            end

                            if (sq_beat != terminal_beat) begin
                                sq_beat <= sq_beat + 2'd1;
                                state   <= S_MEM_REQ;
                            end else begin
                                retired_pc            <= pc;
                                retired_instr         <= instr;
                                retired_arg2          <= ea;
                                retired_arg3          <= map_rd_data;
                                retired_flag_write    <= 1'b0;
                                retired_flag_read     <= 1'b1;
                                retired_flag_branch   <= 1'b0;
                                retired_flag_halt     <= 1'b0;
                                retired_flag_in_delay <= instr_in_delay_slot;
                                retired_flag_rfe      <= 1'b0;
                                retire_pulse          <= 1'b1;
                                sq_beat               <= 2'd0;
                                retire_advance();
                                state <= S_IFETCH_REQ;
                            end
                        end else begin
                        // Sub-word loads extract the addressed byte or
                        // halfword out of the returned 32-bit word.
                        //   LB  : byte at ea[1:0], 24-bit sign-extend
                        //   LH  : halfword at ea[1], 16-bit sign-extend
                        //   LHU : halfword at ea[1], 16-bit zero-extend
                        //   LW  : whole word, as-is
                        // Halfword access uses only ea[1] — ea[0] must
                        // be 0 for aligned access (unaligned halfword
                        // reads are not modelled as an exception yet).
                        if (rt_idx != 5'd0) begin
                            // Ch283 — compute the 32-bit load value into
                            // a temporary, then write both regfile and
                            // gpr128 (zero-extended into the upper 96).
                            // LW/LB/LBU/LH/LHU all produce a single
                            // 32-bit lane; the architectural rule is
                            // upper bits of $rt clear to 0.
                            logic [31:0] load_wb;
                            load_wb = map_rd_data;  // LW default
                            if (is_lb) begin
                                case (ea[1:0])
                                    2'd0: load_wb = {{24{map_rd_data[7]}},  map_rd_data[7:0]};
                                    2'd1: load_wb = {{24{map_rd_data[15]}}, map_rd_data[15:8]};
                                    2'd2: load_wb = {{24{map_rd_data[23]}}, map_rd_data[23:16]};
                                    2'd3: load_wb = {{24{map_rd_data[31]}}, map_rd_data[31:24]};
                                endcase
                            end else if (is_lbu) begin
                                case (ea[1:0])
                                    2'd0: load_wb = {24'd0, map_rd_data[7:0]};
                                    2'd1: load_wb = {24'd0, map_rd_data[15:8]};
                                    2'd2: load_wb = {24'd0, map_rd_data[23:16]};
                                    2'd3: load_wb = {24'd0, map_rd_data[31:24]};
                                endcase
                            end else if (is_lh) begin
                                case (ea[1])
                                    1'b0: load_wb = {{16{map_rd_data[15]}}, map_rd_data[15:0]};
                                    1'b1: load_wb = {{16{map_rd_data[31]}}, map_rd_data[31:16]};
                                endcase
                            end else if (is_lhu) begin
                                case (ea[1])
                                    1'b0: load_wb = {16'd0, map_rd_data[15:0]};
                                    1'b1: load_wb = {16'd0, map_rd_data[31:16]};
                                endcase
                            end
                            regfile[rt_idx] <= load_wb;
                            gpr128[rt_idx]  <= {96'd0, load_wb};
                        end

                        retired_pc            <= pc;
                        retired_instr         <= instr;
                        retired_arg2          <= ea;
                        retired_arg3          <= map_rd_data;
                        retired_flag_write    <= 1'b0;
                        retired_flag_read     <= 1'b1;
                        retired_flag_branch   <= 1'b0;
                        retired_flag_halt     <= 1'b0;
                        retired_flag_in_delay <= instr_in_delay_slot;
                        retired_flag_rfe      <= 1'b0;
                        retire_pulse          <= 1'b1;

                        retire_advance();
                        state <= S_IFETCH_REQ;
                        end
                    end
                end

                S_MEM_WRITE: begin
                    if ((is_sq && sq_beat != 2'd3)
                            || (is_sd && sq_beat != 2'd1)) begin
                        // Ch271/Ch275 — multi-beat store mid-beats:
                        // drive the next 32-bit lane on the next cycle.
                        // Don't retire yet — the single architectural
                        // instruction (SQ=4 beats / SD=2 beats) maps to
                        // one retire event.
                        sq_beat <= sq_beat + 2'd1;
                        // stay in S_MEM_WRITE
                    end else begin
                        retired_pc            <= pc;
                        retired_instr         <= instr;
                        retired_arg2          <= ea;
                        retired_arg3          <= rt_val;
                        retired_flag_write    <= 1'b1;
                        retired_flag_read     <= 1'b0;
                        retired_flag_branch   <= 1'b0;
                        retired_flag_halt     <= 1'b0;
                        retired_flag_in_delay <= instr_in_delay_slot;
                        retired_flag_rfe      <= 1'b0;
                        retire_pulse          <= 1'b1;

                        retire_advance();
                        sq_beat <= 2'd0;
                        state <= S_IFETCH_REQ;
                    end
                end

                S_HALT: state <= S_HALT;

                // Ch215 — jmp_buf restore FSM.
                S_CH215_REQ: state <= S_CH215_WAIT;

                S_CH215_WAIT: begin
                    if (map_rd_valid) begin
                        // Store the loaded word into the canonical
                        // regfile slot per the Ch212 field-map.
                        case (ch215_count)
                            4'd0:  begin regfile[31] <= map_rd_data; gpr128[31] <= {96'd0, map_rd_data}; end  // $ra at +0x00
                            4'd1:  begin regfile[29] <= map_rd_data; gpr128[29] <= {96'd0, map_rd_data}; end  // $sp at +0x04
                            4'd2:  begin regfile[30] <= map_rd_data; gpr128[30] <= {96'd0, map_rd_data}; end  // $fp at +0x08
                            4'd3:  begin regfile[16] <= map_rd_data; gpr128[16] <= {96'd0, map_rd_data}; end  // $s0 at +0x0C
                            4'd4:  begin regfile[17] <= map_rd_data; gpr128[17] <= {96'd0, map_rd_data}; end  // $s1 at +0x10
                            4'd5:  begin regfile[18] <= map_rd_data; gpr128[18] <= {96'd0, map_rd_data}; end  // $s2 at +0x14
                            4'd6:  begin regfile[19] <= map_rd_data; gpr128[19] <= {96'd0, map_rd_data}; end  // $s3 at +0x18
                            4'd7:  begin regfile[20] <= map_rd_data; gpr128[20] <= {96'd0, map_rd_data}; end  // $s4 at +0x1C
                            4'd8:  begin regfile[21] <= map_rd_data; gpr128[21] <= {96'd0, map_rd_data}; end  // $s5 at +0x20
                            4'd9:  begin regfile[22] <= map_rd_data; gpr128[22] <= {96'd0, map_rd_data}; end  // $s6 at +0x24
                            4'd10: begin regfile[23] <= map_rd_data; gpr128[23] <= {96'd0, map_rd_data}; end  // $s7 at +0x28
                            4'd11: begin regfile[28] <= map_rd_data; gpr128[28] <= {96'd0, map_rd_data}; end  // $gp at +0x2C
                            default: ;
                        endcase
                        if (ch215_count == 4'd11) begin
                            // Done — set $v0=1 (longjmp-style return
                            // value so post-setjmp `beq $v0,$0` at
                            // 0xBFC52350 falls through to the
                            // longjmp-return path), set PC to the
                            // loaded $ra (committed at count==0).
                            regfile[2] <= 32'd1;
                            gpr128[2]  <= 128'd1; // Ch283 mirror
                            pc         <= regfile[31];
                            state      <= S_IFETCH_REQ;
                        end else begin
                            ch215_count <= ch215_count + 4'd1;
                            state       <= S_CH215_REQ;
                        end
                    end
                end

                default: state <= S_IDLE;
            endcase
        end
    end

    assign halt_o = (state == S_HALT);
    assign pc_o   = pc;

    // ------------------------------------------------------------------
    // Trace emission — one event per retire, SUBSYS_EE
    // ------------------------------------------------------------------

    always_ff @(posedge clk) begin
        if (!rst_n) begin
            ev_valid  <= 1'b0;
            ev_subsys <= SUBSYS_EE;
            ev_event  <= EV_IFETCH;
            ev_arg0   <= 64'd0;
            ev_arg1   <= 64'd0;
            ev_arg2   <= 64'd0;
            ev_arg3   <= 64'd0;
            ev_flags  <= 32'd0;
        end else if (retire_pulse) begin
            ev_valid  <= 1'b1;
            ev_subsys <= SUBSYS_EE;
            ev_event  <= EV_IFETCH;
            ev_arg0   <= {32'd0, retired_pc};
            ev_arg1   <= {32'd0, retired_instr};
            ev_arg2   <= {32'd0, retired_arg2};
            ev_arg3   <= {32'd0, retired_arg3};
            ev_flags  <= {24'd0,
                          retired_flag_trap,
                          retired_flag_rfe,
                          retired_flag_except,
                          retired_flag_in_delay,
                          retired_flag_halt,
                          retired_flag_branch,
                          retired_flag_read,
                          retired_flag_write};
        end else begin
            ev_valid <= 1'b0;
        end
    end

endmodule : ee_core_stub
