// retroDE_ps2 — tb_ee_core_elf_runner
//
// Ch270 — BIOS-bypass EE ELF runner. The goal is "can our core
// execute real PS2 software?" — answered without committing the
// IOP/SBUS/kernel modeling work that the BIOS arc still needs.
//
// Inputs (via plusargs):
//   +ELF_IMAGE=<path>     EE RAM image in `$readmemh` format,
//                          @<qw_idx> directives + 128-bit qword lines.
//                          Produced by tools/generate_synthetic_image.py
//                          or tools/elf_to_eeram.py.
//   +ELF_MANIFEST=<path>  Manifest: line 0 = entry (32-bit hex),
//                          line 1 = stack-top hint (32-bit hex).
//   +ELF_WATCHDOG_NS=<n>  Override the 50 ms watchdog (rarely needed).
//
// Bootstrap: TB pokes the BIOS reset vector at 0xBFC00000 with a
// 3-instruction trampoline that loads the ELF entry into $at via
// LUI+ORI and JRs to it. EE core boots at 0xBFC00000, immediately
// transfers control to the ELF entry, then runs under strict-mode
// supervision.
//
// Verdict (one of):
//   elf_reached_entry             — PC reached entry, but the run
//                                    ended before any trap/halt/timeout
//                                    (shouldn't normally trigger)
//   elf_first_unsupported_opcode  — STRICT_UNSUPPORTED trap fired
//   elf_first_unmapped_mmio       — ee_memory_map emitted EV_UNMAPPED
//                                    for an access we didn't model
//   elf_halted                    — core halt_o asserted
//   elf_timeout_with_hot_pc       — watchdog fired; we report the
//                                    most-frequently-retired PC over
//                                    the last 256 retires
//
// The synthetic image (tools/generate_synthetic_image.py) is a
// 5-instruction loop-to-self at phys 0x00100000 — the expected
// verdict is elf_timeout_with_hot_pc with hot_pc near 0x00100010.

`timescale 1ns/1ps

module tb_ee_core_elf_runner;

    // ------------------------------------------------------------------
    // Sizing
    // ------------------------------------------------------------------
    localparam int BIOS_BYTES    = 4 * 1024 * 1024;
    localparam int EE_RAM_BYTES  = 2 * 1024 * 1024;  // 2 MiB — fits PS2 ELFs at 0x100000+
    localparam int EE_RAM_ADDR_W = $clog2(EE_RAM_BYTES);
    localparam int EE_RAM_QWORDS = EE_RAM_BYTES / 16;

    localparam logic [31:0] RESET_VECTOR = 32'hBFC0_0000;

    // ------------------------------------------------------------------
    // Clock / reset
    // ------------------------------------------------------------------
    logic clk;
    logic rst_n;
    initial clk = 1'b0;
    always #5 clk = ~clk;

    int  watchdog_ns;
    initial watchdog_ns = 50_000_000;  // 50 ms default

    // ------------------------------------------------------------------
    // ELF state — populated from manifest by the bootstrap initial.
    // ------------------------------------------------------------------
    logic [31:0] elf_entry;
    logic [31:0] elf_stack_top;
    logic [31:0] manifest_mem [0:1];

    // ------------------------------------------------------------------
    // EE core (strict mode ON) + memory map + EE RAM + (mostly-empty)
    // BIOS ROM that gets the bootstrap trampoline poked into it.
    // ------------------------------------------------------------------
    logic        core_go;
    logic        core_halt;
    logic [31:0] core_pc;
    logic        core_trap;
    logic [31:0] core_trap_pc;
    logic [31:0] core_trap_instr;

    logic        ee_rd_en;
    logic [31:0] ee_rd_addr;
    logic [31:0] ee_rd_data;
    logic        ee_rd_valid;

    logic        ee_wr_en;
    logic [31:0] ee_wr_addr;
    logic [31:0] ee_wr_data;
    logic [3:0]  ee_wr_be;

    logic        ee_cpu_irq;
    assign ee_cpu_irq = 1'b0;

    logic                core_ev_valid;
    trace_pkg::subsys_e  core_ev_subsys;
    trace_pkg::event_e   core_ev_event;
    logic [63:0]         core_ev_arg0, core_ev_arg1, core_ev_arg2, core_ev_arg3;
    logic [31:0]         core_ev_flags;

    ee_core_stub #(
        .STRICT_UNSUPPORTED(1'b1),
        // Ch273 — minimal EE syscall HLE for user-ELF runs.
        // EndOfHeap / InitMainThread / FlushCache get stub returns;
        // unknown $v1 still halts so the next blocker surfaces.
        .EE_SYSCALL_HLE_ENABLE(1'b1)
    ) u_core (
        .clk(clk), .rst_n(rst_n),
        .go_i(core_go),
        .map_rd_en(ee_rd_en), .map_rd_addr(ee_rd_addr),
        .map_rd_data(ee_rd_data), .map_rd_valid(ee_rd_valid),
        .map_wr_en(ee_wr_en), .map_wr_addr(ee_wr_addr),
        .map_wr_data(ee_wr_data), .map_wr_be(ee_wr_be),
        .cpu_irq(ee_cpu_irq),
        .halt_o(core_halt), .pc_o(core_pc),
        .trap_o(core_trap), .trap_pc_o(core_trap_pc),
        .trap_instr_o(core_trap_instr),
        .ev_valid(core_ev_valid), .ev_subsys(core_ev_subsys),
        .ev_event(core_ev_event),
        .ev_arg0(core_ev_arg0), .ev_arg1(core_ev_arg1),
        .ev_arg2(core_ev_arg2), .ev_arg3(core_ev_arg3),
        .ev_flags(core_ev_flags)
    );

    // EE memory map
    logic         bios_rd_en;
    logic [21:0]  bios_rd_addr;
    logic [31:0]  bios_rd_data;
    logic         bios_rd_valid;

    logic         ee_ram_rd_en;
    logic [24:0]  ee_ram_rd_addr;
    logic [127:0] ee_ram_rd_data;
    logic         ee_ram_rd_valid;
    logic         ee_ram_wr_en;
    logic [24:0]  ee_ram_wr_addr;
    logic [127:0] ee_ram_wr_data;
    logic [15:0]  ee_ram_wr_be;
    logic [7:0]   ee_ram_master_id;

    logic                map_ev_valid;
    trace_pkg::subsys_e  map_ev_subsys;
    trace_pkg::event_e   map_ev_event;
    logic [63:0]         map_ev_arg0, map_ev_arg1, map_ev_arg2, map_ev_arg3;
    logic [31:0]         map_ev_flags;

    ee_memory_map_stub u_ee_map (
        .clk(clk), .rst_n(rst_n),
        .ee_rd_en(ee_rd_en), .ee_rd_addr(ee_rd_addr),
        .ee_rd_data(ee_rd_data), .ee_rd_valid(ee_rd_valid),
        .ee_wr_en(ee_wr_en), .ee_wr_addr(ee_wr_addr),
        .ee_wr_data(ee_wr_data), .ee_wr_be(ee_wr_be),
        .dmac_rd_en(1'b0), .dmac_rd_addr(32'd0),
        .dmac_rd_data(), .dmac_rd_valid(),
        .bridge_wr_en(1'b0), .bridge_wr_addr(32'd0),
        .bridge_wr_data(128'd0), .bridge_wr_be(16'd0),
        .bridge_master_id(8'd0),
        .bios_rd_en(bios_rd_en), .bios_rd_addr(bios_rd_addr),
        .bios_rd_data(bios_rd_data), .bios_rd_valid(bios_rd_valid),
        .ram_rd_en(ee_ram_rd_en), .ram_rd_addr(ee_ram_rd_addr),
        .ram_rd_data(ee_ram_rd_data), .ram_rd_valid(ee_ram_rd_valid),
        .ram_wr_en(ee_ram_wr_en), .ram_wr_addr(ee_ram_wr_addr),
        .ram_wr_data(ee_ram_wr_data), .ram_wr_be(ee_ram_wr_be),
        .ram_master_id(ee_ram_master_id),
        .ee_dmac_ch2_wr_en(), .ee_dmac_ch2_wr_addr(), .ee_dmac_ch2_wr_data(),
        .ee_dmac_ch2_rd_en(), .ee_dmac_ch2_rd_addr(),
        .ee_dmac_ch2_rd_data(32'd0), .ee_dmac_ch2_rd_valid(1'b0),
        .ee_intc_wr_en(), .ee_intc_wr_addr(), .ee_intc_wr_data(),
        .ee_intc_rd_en(), .ee_intc_rd_addr(),
        .ee_intc_rd_data(32'd0), .ee_intc_rd_valid(1'b0),
        .ee_misc_mmio_wr_en(), .ee_misc_mmio_wr_addr(), .ee_misc_mmio_wr_data(), .ee_misc_mmio_wr_be(),
        .ee_misc_mmio_rd_en(), .ee_misc_mmio_rd_addr(),
        .ee_misc_mmio_rd_data(32'd0), .ee_misc_mmio_rd_valid(1'b0),
        .ee_biu_wr_en(), .ee_biu_wr_addr(), .ee_biu_wr_data(), .ee_biu_wr_be(),
        .ee_biu_rd_en(), .ee_biu_rd_addr(),
        .ee_biu_rd_data(32'd0), .ee_biu_rd_valid(1'b0),
        .ev_valid(map_ev_valid), .ev_subsys(map_ev_subsys), .ev_event(map_ev_event),
        .ev_arg0(map_ev_arg0), .ev_arg1(map_ev_arg1),
        .ev_arg2(map_ev_arg2), .ev_arg3(map_ev_arg3), .ev_flags(map_ev_flags)
    );

    ee_ram_stub #(.SIZE_BYTES(EE_RAM_BYTES)) u_ee_ram (
        .clk(clk), .rst_n(rst_n),
        .rd_en(ee_ram_rd_en), .rd_addr(ee_ram_rd_addr[EE_RAM_ADDR_W-1:0]),
        .rd_data(ee_ram_rd_data), .rd_valid(ee_ram_rd_valid),
        .wr_en(ee_ram_wr_en), .wr_addr(ee_ram_wr_addr[EE_RAM_ADDR_W-1:0]),
        .wr_data(ee_ram_wr_data), .wr_be(ee_ram_wr_be),
        .master_id(ee_ram_master_id),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    bios_rom_stub #(.SIZE_BYTES(BIOS_BYTES)) u_bios (
        .clk(clk), .rst_n(rst_n),
        .rd_en(bios_rd_en), .rd_addr(bios_rd_addr),
        .rd_data(bios_rd_data), .rd_valid(bios_rd_valid),
        .ev_valid(), .ev_subsys(), .ev_event(),
        .ev_arg0(), .ev_arg1(), .ev_arg2(), .ev_arg3(), .ev_flags()
    );

    trace_sink_stub #(.FILENAME("ee_core_elf_runner_core.trace"),
                      .SINK_LABEL("ee_core"))
        u_trace_core (.clk(clk), .rst_n(rst_n),
            .ev_valid(core_ev_valid), .ev_subsys(core_ev_subsys),
            .ev_event(core_ev_event), .ev_arg0(core_ev_arg0),
            .ev_arg1(core_ev_arg1), .ev_arg2(core_ev_arg2),
            .ev_arg3(core_ev_arg3), .ev_flags(core_ev_flags));

    trace_sink_stub #(.FILENAME("ee_core_elf_runner_map.trace"),
                      .SINK_LABEL("ee_map"))
        u_trace_map (.clk(clk), .rst_n(rst_n),
            .ev_valid(map_ev_valid), .ev_subsys(map_ev_subsys),
            .ev_event(map_ev_event), .ev_arg0(map_ev_arg0),
            .ev_arg1(map_ev_arg1), .ev_arg2(map_ev_arg2),
            .ev_arg3(map_ev_arg3), .ev_flags(map_ev_flags));

    // ------------------------------------------------------------------
    // Verdict trackers (live latches — see
    // feedback_observer_design_for_lineage.md for why we never use
    // fill-from-boot arrays for trigger-shaped questions).
    // ------------------------------------------------------------------
    logic         entry_reached;
    logic [31:0]  first_trap_pc;
    logic [31:0]  first_trap_instr;
    logic         saw_trap;
    logic [31:0]  first_unmap_ea;
    logic [31:0]  first_unmap_pc;
    logic         saw_unmap;
    logic         saw_halt;
    int           retire_count;
    int           sim_cycle_ctr;

    // Ch289 — observer for known-HLE syscall 0x78. Captures the
    // first occurrence (PC, $a0-$a3) so post-run analysis sees what
    // qbert handed the kernel. The return value is structurally 0
    // (per the Ch285/289 dispatcher contract) and not separately
    // sampled. Extensible: add a parallel observer for any future
    // HLE'd syscall whose argument shape matters.
    logic         seen_syscall_0x78;
    int           syscall_0x78_count;
    logic [31:0]  syscall_0x78_first_pc;
    logic [31:0]  syscall_0x78_first_a0;
    logic [31:0]  syscall_0x78_first_a1;
    logic [31:0]  syscall_0x78_first_a2;
    logic [31:0]  syscall_0x78_first_a3;
    // Ch290 — same observer pattern for syscall 0x12 (the handler-
    // install call). The arg shape is `(int slot, fn_ptr, void*
    // ctx0, void* ctx1)`; capturing $a0..$a3 on first occurrence
    // tells us which slot/IRQ qbert is hooking with which handler.
    logic         seen_syscall_0x12;
    int           syscall_0x12_count;
    logic [31:0]  syscall_0x12_first_pc;
    logic [31:0]  syscall_0x12_first_a0;
    logic [31:0]  syscall_0x12_first_a1;
    logic [31:0]  syscall_0x12_first_a2;
    logic [31:0]  syscall_0x12_first_a3;
    // Ch291 — same observer pattern for syscall 0x16 (the paired
    // enable companion to Ch290's 0x12). Codex's prediction was that
    // args would arrive identical to 0x12; the observer here is the
    // confirmation surface.
    logic         seen_syscall_0x16;
    int           syscall_0x16_count;
    logic [31:0]  syscall_0x16_first_pc;
    logic [31:0]  syscall_0x16_first_a0;
    logic [31:0]  syscall_0x16_first_a1;
    logic [31:0]  syscall_0x16_first_a2;
    logic [31:0]  syscall_0x16_first_a3;
    // Ch293 — same observer pattern for syscall 0x7A (plausibly
    // SyncDCache, the cache-sync neighbor to Ch292's MIPS SYNC).
    // $a0 is the first kseg0-base arg we've seen on the syscall
    // track; the observer captures it for downstream analysis.
    logic         seen_syscall_0x7A;
    int           syscall_0x7A_count;
    logic [31:0]  syscall_0x7A_first_pc;
    logic [31:0]  syscall_0x7A_first_a0;
    logic [31:0]  syscall_0x7A_first_a1;
    logic [31:0]  syscall_0x7A_first_a2;
    logic [31:0]  syscall_0x7A_first_a3;
    // Ch295 — per-$a0-class counters for syscall 0x7A. The Ch294
    // autopsy showed qbert calls 0x7A with two distinct $a0 shapes
    // (0x80000000 init, 0x00000004 poll); the $a0-aware HLE returns
    // different values per shape. These counters confirm the split
    // is exercised in qbert's run.
    int           syscall_0x7A_count_a0_4;
    int           syscall_0x7A_count_a0_80000000;
    int           syscall_0x7A_count_a0_other;
    logic [31:0]  syscall_0x7A_last_a0;
    logic [31:0]  syscall_0x7A_first_v0_after;   // sampled cycle AFTER retire
    logic [31:0]  syscall_0x7A_last_v0_after;
    logic         syscall_0x7A_v0_pending;       // 1 = sample $v0 next cycle
    // Ch296 — same observer pattern for syscall 0x79 (adjacent
    // finalize-style call surfaced after Ch295's $a0-aware unblock).
    logic         seen_syscall_0x79;
    int           syscall_0x79_count;
    logic [31:0]  syscall_0x79_first_pc;
    logic [31:0]  syscall_0x79_first_a0;
    logic [31:0]  syscall_0x79_first_a1;
    logic [31:0]  syscall_0x79_first_a2;
    logic [31:0]  syscall_0x79_first_a3;
    // Ch301 — observer for syscall 0x17 (per-channel call,
    // possibly SetVTLBRefillHandler / iWakeupThread). Codex flagged
    // the $a3 context-shift (0x001328C0 → 0x00137568) as worth
    // careful logging, so this observer reuses the richer Ch297
    // pattern (up to 4 distinct ($a0,$a1,$a2,$a3) tuples).
    logic         seen_syscall_0x17;
    int           syscall_0x17_count;
    logic [31:0]  syscall_0x17_first_pc;
    logic [31:0]  syscall_0x17_first_a0;
    logic [31:0]  syscall_0x17_first_a1;
    logic [31:0]  syscall_0x17_first_a2;
    logic [31:0]  syscall_0x17_first_a3;
    logic [31:0]  syscall_0x17_last_a0;
    logic [31:0]  syscall_0x17_last_a1;
    logic [31:0]  syscall_0x17_last_a2;
    logic [31:0]  syscall_0x17_last_a3;
    logic         syscall_0x17_tuple_valid [0:3];
    logic [31:0]  syscall_0x17_tuple_a0    [0:3];
    logic [31:0]  syscall_0x17_tuple_a1    [0:3];
    logic [31:0]  syscall_0x17_tuple_a2    [0:3];
    logic [31:0]  syscall_0x17_tuple_a3    [0:3];
    int           syscall_0x17_tuple_count [0:3];
    int           syscall_0x17_distinct_tuples;

    // Ch302 — observer for syscall 0x13 (paired with Ch301's 0x17).
    // Same richer distinct-tuple pattern. Expected to confirm the
    // paired-call symmetry (count=1, same args as 0x17).
    logic         seen_syscall_0x13;
    int           syscall_0x13_count;
    logic [31:0]  syscall_0x13_first_pc;
    logic [31:0]  syscall_0x13_first_a0;
    logic [31:0]  syscall_0x13_first_a1;
    logic [31:0]  syscall_0x13_first_a2;
    logic [31:0]  syscall_0x13_first_a3;
    logic [31:0]  syscall_0x13_last_a0;
    logic [31:0]  syscall_0x13_last_a1;
    logic [31:0]  syscall_0x13_last_a2;
    logic [31:0]  syscall_0x13_last_a3;
    logic         syscall_0x13_tuple_valid [0:3];
    logic [31:0]  syscall_0x13_tuple_a0    [0:3];
    logic [31:0]  syscall_0x13_tuple_a1    [0:3];
    logic [31:0]  syscall_0x13_tuple_a2    [0:3];
    logic [31:0]  syscall_0x13_tuple_a3    [0:3];
    int           syscall_0x13_tuple_count [0:3];
    int           syscall_0x13_distinct_tuples;

    // Ch304 — observer for syscall 0x6B (first Table1 wrapper to be
    // HLE'd; Ch303 proved its caller ignores the return). Simple
    // count + first/last args; the distinct-tuple richness isn't
    // needed here since Ch303 already mapped the table shape.
    logic         seen_syscall_0x6B;
    int           syscall_0x6B_count;
    logic [31:0]  syscall_0x6B_first_pc;
    logic [31:0]  syscall_0x6B_first_a0;
    logic [31:0]  syscall_0x6B_first_a1;
    logic [31:0]  syscall_0x6B_first_a2;
    logic [31:0]  syscall_0x6B_first_a3;
    logic [31:0]  syscall_0x6B_last_a0;
    logic [31:0]  syscall_0x6B_last_a3;

    // Ch299 — qbert-specific library-ready gate poke.
    //
    // Ch298 autopsy identified that qbert's second wait loop polls
    // memory at 0x001329C0 (global_ctx + 0x100) for non-zero. The
    // setter was identified as the kernel-side callback from syscall
    // 0x77 (RegisterLibraryEntries) — our HLE returns $v0=0 but
    // never writes to memory.
    //
    // Strategy B-lite (per Codex): make the syscall 0x77 retire
    // *trigger* a write of 1 to the gate, but narrowly — only when
    // the observed-during-qbert arg shape matches:
    //   $a0 in [0x001DFD50, 0x001DFDB0]   (heap registration buffer)
    //   $a3 in {0x10, 0x14}               (entry count = 16 or 20)
    // This is qbert-specific HLE, NOT architectural truth. If a
    // future ELF calls 0x77 with these args expecting NO gate
    // write, this needs revisiting.
    //
    // RTL-side write from the syscall FSM was rejected as too
    // invasive (would need a new state and combinational
    // map-driver changes). TB-side poke is Codex's authorized
    // fallback.
    localparam logic [31:0] LIBRARY_READY_GATE_ADDR  = 32'h0013_29C0;
    localparam logic [19:0] LIBRARY_READY_SHADOW_IDX = 20'h4_CA70;
    localparam logic [31:0] LIBRARY_READY_GATE_VALUE = 32'h0000_0001;
    logic         library_ready_poked;
    int           library_ready_poke_count;
    int           library_ready_first_poke_cycle;
    logic [31:0]  library_ready_initial_value;
    logic         library_ready_initial_sampled;
    logic [31:0]  library_ready_final_value;

    // Ch297 — RICHER observer for syscall 0x77. Codex's framing:
    // this is a new syscall-family boundary (first syscall whose
    // $a3 is not the global ctx pointer 0x001328C0). If qbert
    // calls 0x77 repeatedly with varying $a0 (e.g., iterating a
    // table), the standard "first only" snapshot misses the
    // shape. Keep first + last + up to 4 distinct arg tuples (the
    // count of distinct tuples already differentiates "called
    // once" from "iterating over N rows").
    logic         seen_syscall_0x77;
    int           syscall_0x77_count;
    logic [31:0]  syscall_0x77_first_pc;
    logic [31:0]  syscall_0x77_first_a0;
    logic [31:0]  syscall_0x77_first_a1;
    logic [31:0]  syscall_0x77_first_a2;
    logic [31:0]  syscall_0x77_first_a3;
    logic [31:0]  syscall_0x77_last_a0;
    logic [31:0]  syscall_0x77_last_a1;
    logic [31:0]  syscall_0x77_last_a2;
    logic [31:0]  syscall_0x77_last_a3;
    // Distinct-tuple table (up to 4 entries). A "tuple" is the
    // 4-tuple ($a0,$a1,$a2,$a3). The table records each unique
    // shape seen + the count of times that shape was used.
    logic         syscall_0x77_tuple_valid [0:3];
    logic [31:0]  syscall_0x77_tuple_a0    [0:3];
    logic [31:0]  syscall_0x77_tuple_a1    [0:3];
    logic [31:0]  syscall_0x77_tuple_a2    [0:3];
    logic [31:0]  syscall_0x77_tuple_a3    [0:3];
    int           syscall_0x77_tuple_count [0:3];
    int           syscall_0x77_distinct_tuples;

    // Hot-PC histogram — bounded ring of the last 256 retired PCs.
    // At verdict-time we walk the ring and report the mode.
    localparam int CH270_RING = 256;
    logic [31:0]  hotpc_ring [0:CH270_RING-1];
    int           hotpc_head;
    int           hotpc_fill;

    initial begin
        entry_reached    = 1'b0;
        first_trap_pc    = 32'd0;
        first_trap_instr = 32'd0;
        saw_trap         = 1'b0;
        first_unmap_ea   = 32'd0;
        first_unmap_pc   = 32'd0;
        saw_unmap        = 1'b0;
        saw_halt         = 1'b0;
        retire_count     = 0;
        sim_cycle_ctr    = 0;
        hotpc_head       = 0;
        hotpc_fill       = 0;
        for (int i = 0; i < CH270_RING; i++) hotpc_ring[i] = 32'd0;
        seen_syscall_0x78     = 1'b0;
        syscall_0x78_count    = 0;
        syscall_0x78_first_pc = 32'd0;
        syscall_0x78_first_a0 = 32'd0;
        syscall_0x78_first_a1 = 32'd0;
        syscall_0x78_first_a2 = 32'd0;
        syscall_0x78_first_a3 = 32'd0;
        seen_syscall_0x12     = 1'b0;
        syscall_0x12_count    = 0;
        syscall_0x12_first_pc = 32'd0;
        syscall_0x12_first_a0 = 32'd0;
        syscall_0x12_first_a1 = 32'd0;
        syscall_0x12_first_a2 = 32'd0;
        syscall_0x12_first_a3 = 32'd0;
        seen_syscall_0x16     = 1'b0;
        syscall_0x16_count    = 0;
        syscall_0x16_first_pc = 32'd0;
        syscall_0x16_first_a0 = 32'd0;
        syscall_0x16_first_a1 = 32'd0;
        syscall_0x16_first_a2 = 32'd0;
        syscall_0x16_first_a3 = 32'd0;
        seen_syscall_0x7A     = 1'b0;
        syscall_0x7A_count    = 0;
        syscall_0x7A_first_pc = 32'd0;
        syscall_0x7A_first_a0 = 32'd0;
        syscall_0x7A_first_a1 = 32'd0;
        syscall_0x7A_first_a2 = 32'd0;
        syscall_0x7A_first_a3 = 32'd0;
        syscall_0x7A_count_a0_4         = 0;
        syscall_0x7A_count_a0_80000000  = 0;
        syscall_0x7A_count_a0_other     = 0;
        syscall_0x7A_last_a0            = 32'd0;
        syscall_0x7A_first_v0_after     = 32'hDEAD_BEEF;
        syscall_0x7A_last_v0_after      = 32'hDEAD_BEEF;
        syscall_0x7A_v0_pending         = 1'b0;
        seen_syscall_0x79     = 1'b0;
        syscall_0x79_count    = 0;
        syscall_0x79_first_pc = 32'd0;
        syscall_0x79_first_a0 = 32'd0;
        syscall_0x79_first_a1 = 32'd0;
        syscall_0x79_first_a2 = 32'd0;
        syscall_0x79_first_a3 = 32'd0;
        seen_syscall_0x77     = 1'b0;
        syscall_0x77_count    = 0;
        syscall_0x77_first_pc = 32'd0;
        syscall_0x77_first_a0 = 32'd0;
        syscall_0x77_first_a1 = 32'd0;
        syscall_0x77_first_a2 = 32'd0;
        syscall_0x77_first_a3 = 32'd0;
        syscall_0x77_last_a0  = 32'd0;
        syscall_0x77_last_a1  = 32'd0;
        syscall_0x77_last_a2  = 32'd0;
        syscall_0x77_last_a3  = 32'd0;
        for (int i = 0; i < 4; i++) begin
            syscall_0x77_tuple_valid[i] = 1'b0;
            syscall_0x77_tuple_a0[i]    = 32'd0;
            syscall_0x77_tuple_a1[i]    = 32'd0;
            syscall_0x77_tuple_a2[i]    = 32'd0;
            syscall_0x77_tuple_a3[i]    = 32'd0;
            syscall_0x77_tuple_count[i] = 0;
        end
        syscall_0x77_distinct_tuples = 0;
        seen_syscall_0x17     = 1'b0;
        syscall_0x17_count    = 0;
        syscall_0x17_first_pc = 32'd0;
        syscall_0x17_first_a0 = 32'd0;
        syscall_0x17_first_a1 = 32'd0;
        syscall_0x17_first_a2 = 32'd0;
        syscall_0x17_first_a3 = 32'd0;
        syscall_0x17_last_a0  = 32'd0;
        syscall_0x17_last_a1  = 32'd0;
        syscall_0x17_last_a2  = 32'd0;
        syscall_0x17_last_a3  = 32'd0;
        for (int i = 0; i < 4; i++) begin
            syscall_0x17_tuple_valid[i] = 1'b0;
            syscall_0x17_tuple_a0[i]    = 32'd0;
            syscall_0x17_tuple_a1[i]    = 32'd0;
            syscall_0x17_tuple_a2[i]    = 32'd0;
            syscall_0x17_tuple_a3[i]    = 32'd0;
            syscall_0x17_tuple_count[i] = 0;
        end
        syscall_0x17_distinct_tuples = 0;
        seen_syscall_0x13     = 1'b0;
        syscall_0x13_count    = 0;
        syscall_0x13_first_pc = 32'd0;
        syscall_0x13_first_a0 = 32'd0;
        syscall_0x13_first_a1 = 32'd0;
        syscall_0x13_first_a2 = 32'd0;
        syscall_0x13_first_a3 = 32'd0;
        syscall_0x13_last_a0  = 32'd0;
        syscall_0x13_last_a1  = 32'd0;
        syscall_0x13_last_a2  = 32'd0;
        syscall_0x13_last_a3  = 32'd0;
        for (int i = 0; i < 4; i++) begin
            syscall_0x13_tuple_valid[i] = 1'b0;
            syscall_0x13_tuple_a0[i]    = 32'd0;
            syscall_0x13_tuple_a1[i]    = 32'd0;
            syscall_0x13_tuple_a2[i]    = 32'd0;
            syscall_0x13_tuple_a3[i]    = 32'd0;
            syscall_0x13_tuple_count[i] = 0;
        end
        syscall_0x13_distinct_tuples = 0;
        seen_syscall_0x6B     = 1'b0;
        syscall_0x6B_count    = 0;
        syscall_0x6B_first_pc = 32'd0;
        syscall_0x6B_first_a0 = 32'd0;
        syscall_0x6B_first_a1 = 32'd0;
        syscall_0x6B_first_a2 = 32'd0;
        syscall_0x6B_first_a3 = 32'd0;
        syscall_0x6B_last_a0  = 32'd0;
        syscall_0x6B_last_a3  = 32'd0;
        library_ready_poked              = 1'b0;
        library_ready_poke_count         = 0;
        library_ready_first_poke_cycle   = 0;
        library_ready_initial_value      = 32'd0;
        library_ready_initial_sampled    = 1'b0;
        library_ready_final_value        = 32'd0;
    end

    always_ff @(posedge clk) begin
        if (!rst_n) begin
            sim_cycle_ctr <= 0;
        end else begin
            sim_cycle_ctr <= sim_cycle_ctr + 1;
            // Retire-driven captures.
            if (core_ev_valid) begin
                retire_count <= retire_count + 1;
                hotpc_ring[hotpc_head] <= u_core.retired_pc;
                hotpc_head <= (hotpc_head + 1) % CH270_RING;
                if (hotpc_fill < CH270_RING) hotpc_fill <= hotpc_fill + 1;
                if (!entry_reached && u_core.retired_pc == elf_entry)
                    entry_reached <= 1'b1;
            end
            // Strict-mode trap.
            if (core_trap && !saw_trap) begin
                first_trap_pc    <= core_trap_pc;
                first_trap_instr <= core_trap_instr;
                saw_trap         <= 1'b1;
            end
            // Unmapped MMIO from the ee_memory_map's trace event.
            if (map_ev_valid && map_ev_arg3[7:0] == 8'hFF && !saw_unmap) begin
                first_unmap_ea <= map_ev_arg0[31:0];
                first_unmap_pc <= core_pc;
                saw_unmap      <= 1'b1;
            end
            // Halt.
            if (core_halt && !saw_halt)
                saw_halt <= 1'b1;
            // Ch289 — capture syscall 0x78 retires. SYSCALL instr =
            // 0x0000000C; $v1 (= regfile[3]) holds the syscall
            // number. At retire, $a0..$a3 hold the input args (the
            // HLE dispatcher only updates $v0 + PC, not the args).
            if (core_ev_valid && u_core.retired_instr == 32'h0000_000C
                              && u_core.regfile[3] == 32'h0000_0078) begin
                syscall_0x78_count <= syscall_0x78_count + 1;
                if (!seen_syscall_0x78) begin
                    seen_syscall_0x78     <= 1'b1;
                    syscall_0x78_first_pc <= u_core.retired_pc;
                    syscall_0x78_first_a0 <= u_core.regfile[4];
                    syscall_0x78_first_a1 <= u_core.regfile[5];
                    syscall_0x78_first_a2 <= u_core.regfile[6];
                    syscall_0x78_first_a3 <= u_core.regfile[7];
                end
            end
            // Ch290 — same pattern for syscall 0x12 (handler install).
            if (core_ev_valid && u_core.retired_instr == 32'h0000_000C
                              && u_core.regfile[3] == 32'h0000_0012) begin
                syscall_0x12_count <= syscall_0x12_count + 1;
                if (!seen_syscall_0x12) begin
                    seen_syscall_0x12     <= 1'b1;
                    syscall_0x12_first_pc <= u_core.retired_pc;
                    syscall_0x12_first_a0 <= u_core.regfile[4];
                    syscall_0x12_first_a1 <= u_core.regfile[5];
                    syscall_0x12_first_a2 <= u_core.regfile[6];
                    syscall_0x12_first_a3 <= u_core.regfile[7];
                end
            end
            // Ch291 — same pattern for syscall 0x16 (handler enable).
            if (core_ev_valid && u_core.retired_instr == 32'h0000_000C
                              && u_core.regfile[3] == 32'h0000_0016) begin
                syscall_0x16_count <= syscall_0x16_count + 1;
                if (!seen_syscall_0x16) begin
                    seen_syscall_0x16     <= 1'b1;
                    syscall_0x16_first_pc <= u_core.retired_pc;
                    syscall_0x16_first_a0 <= u_core.regfile[4];
                    syscall_0x16_first_a1 <= u_core.regfile[5];
                    syscall_0x16_first_a2 <= u_core.regfile[6];
                    syscall_0x16_first_a3 <= u_core.regfile[7];
                end
            end
            // Ch293 — same pattern for syscall 0x7A (cache-sync).
            // Ch295 — split per-$a0-class counters. At retire, $a0
            // (regfile[4]) still holds the input arg; $v0 (regfile[2])
            // holds the OLD return value (not yet the NBA-committed new
            // one). The $v0_pending flag arms a one-cycle-later sample
            // of the actual returned $v0.
            if (core_ev_valid && u_core.retired_instr == 32'h0000_000C
                              && u_core.regfile[3] == 32'h0000_007A) begin
                syscall_0x7A_count   <= syscall_0x7A_count + 1;
                syscall_0x7A_last_a0 <= u_core.regfile[4];
                if (u_core.regfile[4] == 32'h0000_0004)
                    syscall_0x7A_count_a0_4 <= syscall_0x7A_count_a0_4 + 1;
                else if (u_core.regfile[4] == 32'h8000_0000)
                    syscall_0x7A_count_a0_80000000 <= syscall_0x7A_count_a0_80000000 + 1;
                else
                    syscall_0x7A_count_a0_other <= syscall_0x7A_count_a0_other + 1;
                syscall_0x7A_v0_pending <= 1'b1;
                if (!seen_syscall_0x7A) begin
                    seen_syscall_0x7A     <= 1'b1;
                    syscall_0x7A_first_pc <= u_core.retired_pc;
                    syscall_0x7A_first_a0 <= u_core.regfile[4];
                    syscall_0x7A_first_a1 <= u_core.regfile[5];
                    syscall_0x7A_first_a2 <= u_core.regfile[6];
                    syscall_0x7A_first_a3 <= u_core.regfile[7];
                end
            end
            // One-cycle-after sample: by now the dispatcher's NBA
            // (regfile[2] <= return value) has committed.
            if (syscall_0x7A_v0_pending) begin
                syscall_0x7A_v0_pending <= 1'b0;
                syscall_0x7A_last_v0_after <= u_core.regfile[2];
                if (syscall_0x7A_first_v0_after === 32'hDEAD_BEEF)
                    syscall_0x7A_first_v0_after <= u_core.regfile[2];
            end
            // Ch296 — same pattern for syscall 0x79 (adjacent
            // finalize-style call).
            if (core_ev_valid && u_core.retired_instr == 32'h0000_000C
                              && u_core.regfile[3] == 32'h0000_0079) begin
                syscall_0x79_count <= syscall_0x79_count + 1;
                if (!seen_syscall_0x79) begin
                    seen_syscall_0x79     <= 1'b1;
                    syscall_0x79_first_pc <= u_core.retired_pc;
                    syscall_0x79_first_a0 <= u_core.regfile[4];
                    syscall_0x79_first_a1 <= u_core.regfile[5];
                    syscall_0x79_first_a2 <= u_core.regfile[6];
                    syscall_0x79_first_a3 <= u_core.regfile[7];
                end
            end
            // Ch297 — richer observer for syscall 0x77. Tracks
            // first/last args + up to 4 distinct ($a0,$a1,$a2,$a3)
            // tuples. The distinct-tuple counter answers "is qbert
            // iterating over a table?" — count > 1 with matching
            // tuples = same call repeated; count > 1 with distinct
            // tuples = iteration.
            if (core_ev_valid && u_core.retired_instr == 32'h0000_000C
                              && u_core.regfile[3] == 32'h0000_0077) begin
                logic [31:0] a0, a1, a2, a3;
                logic matched;
                int  i;
                a0 = u_core.regfile[4];
                a1 = u_core.regfile[5];
                a2 = u_core.regfile[6];
                a3 = u_core.regfile[7];
                syscall_0x77_count   <= syscall_0x77_count + 1;
                syscall_0x77_last_a0 <= a0;
                syscall_0x77_last_a1 <= a1;
                syscall_0x77_last_a2 <= a2;
                syscall_0x77_last_a3 <= a3;
                if (!seen_syscall_0x77) begin
                    seen_syscall_0x77     <= 1'b1;
                    syscall_0x77_first_pc <= u_core.retired_pc;
                    syscall_0x77_first_a0 <= a0;
                    syscall_0x77_first_a1 <= a1;
                    syscall_0x77_first_a2 <= a2;
                    syscall_0x77_first_a3 <= a3;
                end
                // Lookup existing tuple; if found, bump count. If
                // not found and we have a free slot, record it.
                matched = 1'b0;
                for (i = 0; i < 4; i++) begin
                    if (syscall_0x77_tuple_valid[i]
                            && syscall_0x77_tuple_a0[i] == a0
                            && syscall_0x77_tuple_a1[i] == a1
                            && syscall_0x77_tuple_a2[i] == a2
                            && syscall_0x77_tuple_a3[i] == a3) begin
                        syscall_0x77_tuple_count[i] <= syscall_0x77_tuple_count[i] + 1;
                        matched = 1'b1;
                    end
                end
                if (!matched) begin
                    for (i = 0; i < 4; i++) begin
                        if (!matched && !syscall_0x77_tuple_valid[i]) begin
                            syscall_0x77_tuple_valid[i] <= 1'b1;
                            syscall_0x77_tuple_a0[i]    <= a0;
                            syscall_0x77_tuple_a1[i]    <= a1;
                            syscall_0x77_tuple_a2[i]    <= a2;
                            syscall_0x77_tuple_a3[i]    <= a3;
                            syscall_0x77_tuple_count[i] <= 1;
                            syscall_0x77_distinct_tuples <= syscall_0x77_distinct_tuples + 1;
                            matched = 1'b1;
                        end
                    end
                end
                // Ch299 — narrow gate poke. Matching guard:
                //   $a0 in [0x001DFD50, 0x001DFDB0]
                //   $a3 in {0x10, 0x14}
                // Both qbert-observed Ch297 0x77 calls satisfy this.
                if ((a0 >= 32'h001D_FD50) && (a0 <= 32'h001D_FDB0)
                        && ((a3 == 32'h0000_0010) || (a3 == 32'h0000_0014))) begin
                    u_ee_map.g_useg_shadow.useg_shadow_mem[LIBRARY_READY_SHADOW_IDX] <= LIBRARY_READY_GATE_VALUE;
                    library_ready_poke_count <= library_ready_poke_count + 1;
                    if (!library_ready_poked) begin
                        library_ready_poked            <= 1'b1;
                        library_ready_first_poke_cycle <= sim_cycle_ctr;
                    end
                end
            end
            // Ch301 — same richer pattern for syscall 0x17.
            // Parallel structure to the 0x77 observer; tracks up to
            // 4 distinct ($a0,$a1,$a2,$a3) tuples + first/last args
            // + per-tuple counts. Codex flagged the $a3 context
            // shift as worth careful logging, so distinct-tuple
            // tracking is deliberate (not just first-call snapshot).
            if (core_ev_valid && u_core.retired_instr == 32'h0000_000C
                              && u_core.regfile[3] == 32'h0000_0017) begin
                logic [31:0] a0_17, a1_17, a2_17, a3_17;
                logic        matched_17;
                int          j;
                a0_17 = u_core.regfile[4];
                a1_17 = u_core.regfile[5];
                a2_17 = u_core.regfile[6];
                a3_17 = u_core.regfile[7];
                syscall_0x17_count   <= syscall_0x17_count + 1;
                syscall_0x17_last_a0 <= a0_17;
                syscall_0x17_last_a1 <= a1_17;
                syscall_0x17_last_a2 <= a2_17;
                syscall_0x17_last_a3 <= a3_17;
                if (!seen_syscall_0x17) begin
                    seen_syscall_0x17     <= 1'b1;
                    syscall_0x17_first_pc <= u_core.retired_pc;
                    syscall_0x17_first_a0 <= a0_17;
                    syscall_0x17_first_a1 <= a1_17;
                    syscall_0x17_first_a2 <= a2_17;
                    syscall_0x17_first_a3 <= a3_17;
                end
                matched_17 = 1'b0;
                for (j = 0; j < 4; j++) begin
                    if (syscall_0x17_tuple_valid[j]
                            && syscall_0x17_tuple_a0[j] == a0_17
                            && syscall_0x17_tuple_a1[j] == a1_17
                            && syscall_0x17_tuple_a2[j] == a2_17
                            && syscall_0x17_tuple_a3[j] == a3_17) begin
                        syscall_0x17_tuple_count[j] <= syscall_0x17_tuple_count[j] + 1;
                        matched_17 = 1'b1;
                    end
                end
                if (!matched_17) begin
                    for (j = 0; j < 4; j++) begin
                        if (!matched_17 && !syscall_0x17_tuple_valid[j]) begin
                            syscall_0x17_tuple_valid[j] <= 1'b1;
                            syscall_0x17_tuple_a0[j]    <= a0_17;
                            syscall_0x17_tuple_a1[j]    <= a1_17;
                            syscall_0x17_tuple_a2[j]    <= a2_17;
                            syscall_0x17_tuple_a3[j]    <= a3_17;
                            syscall_0x17_tuple_count[j] <= 1;
                            syscall_0x17_distinct_tuples <= syscall_0x17_distinct_tuples + 1;
                            matched_17 = 1'b1;
                        end
                    end
                end
            end
            // Ch302 — same richer pattern for syscall 0x13 (paired
            // with 0x17). Expected to confirm count=1 + identical
            // args to 0x17.
            if (core_ev_valid && u_core.retired_instr == 32'h0000_000C
                              && u_core.regfile[3] == 32'h0000_0013) begin
                logic [31:0] a0_13, a1_13, a2_13, a3_13;
                logic        matched_13;
                int          k;
                a0_13 = u_core.regfile[4];
                a1_13 = u_core.regfile[5];
                a2_13 = u_core.regfile[6];
                a3_13 = u_core.regfile[7];
                syscall_0x13_count   <= syscall_0x13_count + 1;
                syscall_0x13_last_a0 <= a0_13;
                syscall_0x13_last_a1 <= a1_13;
                syscall_0x13_last_a2 <= a2_13;
                syscall_0x13_last_a3 <= a3_13;
                if (!seen_syscall_0x13) begin
                    seen_syscall_0x13     <= 1'b1;
                    syscall_0x13_first_pc <= u_core.retired_pc;
                    syscall_0x13_first_a0 <= a0_13;
                    syscall_0x13_first_a1 <= a1_13;
                    syscall_0x13_first_a2 <= a2_13;
                    syscall_0x13_first_a3 <= a3_13;
                end
                matched_13 = 1'b0;
                for (k = 0; k < 4; k++) begin
                    if (syscall_0x13_tuple_valid[k]
                            && syscall_0x13_tuple_a0[k] == a0_13
                            && syscall_0x13_tuple_a1[k] == a1_13
                            && syscall_0x13_tuple_a2[k] == a2_13
                            && syscall_0x13_tuple_a3[k] == a3_13) begin
                        syscall_0x13_tuple_count[k] <= syscall_0x13_tuple_count[k] + 1;
                        matched_13 = 1'b1;
                    end
                end
                if (!matched_13) begin
                    for (k = 0; k < 4; k++) begin
                        if (!matched_13 && !syscall_0x13_tuple_valid[k]) begin
                            syscall_0x13_tuple_valid[k] <= 1'b1;
                            syscall_0x13_tuple_a0[k]    <= a0_13;
                            syscall_0x13_tuple_a1[k]    <= a1_13;
                            syscall_0x13_tuple_a2[k]    <= a2_13;
                            syscall_0x13_tuple_a3[k]    <= a3_13;
                            syscall_0x13_tuple_count[k] <= 1;
                            syscall_0x13_distinct_tuples <= syscall_0x13_distinct_tuples + 1;
                            matched_13 = 1'b1;
                        end
                    end
                end
            end
            // Ch304 — observer for syscall 0x6B.
            if (core_ev_valid && u_core.retired_instr == 32'h0000_000C
                              && u_core.regfile[3] == 32'h0000_006B) begin
                syscall_0x6B_count   <= syscall_0x6B_count + 1;
                syscall_0x6B_last_a0 <= u_core.regfile[4];
                syscall_0x6B_last_a3 <= u_core.regfile[7];
                if (!seen_syscall_0x6B) begin
                    seen_syscall_0x6B     <= 1'b1;
                    syscall_0x6B_first_pc <= u_core.retired_pc;
                    syscall_0x6B_first_a0 <= u_core.regfile[4];
                    syscall_0x6B_first_a1 <= u_core.regfile[5];
                    syscall_0x6B_first_a2 <= u_core.regfile[6];
                    syscall_0x6B_first_a3 <= u_core.regfile[7];
                end
            end
            // Sample the gate's initial value once, just before
            // qbert's init zero-writes hit it. Cycle 100 is safely
            // before the earliest observed init zero-write (cycle
            // 39739 in the Ch298 trace).
            if (!library_ready_initial_sampled && sim_cycle_ctr == 100) begin
                library_ready_initial_value   <= u_ee_map.g_useg_shadow.useg_shadow_mem[LIBRARY_READY_SHADOW_IDX];
                library_ready_initial_sampled <= 1'b1;
            end
            // Continuously latch the gate's current value; the last
            // value at end-of-sim is the "final value" for the
            // SUMMARY line.
            library_ready_final_value <= u_ee_map.g_useg_shadow.useg_shadow_mem[LIBRARY_READY_SHADOW_IDX];
        end
    end

    // ------------------------------------------------------------------
    // Hot-PC mode (run at end-of-test).
    // ------------------------------------------------------------------
    task automatic compute_hot_pc(output logic [31:0] hot_pc,
                                  output int          hot_count);
        int  uniq_count;
        logic [31:0] uniq_pc  [0:CH270_RING-1];
        int          uniq_n   [0:CH270_RING-1];
        int          j;
        logic        found;
        hot_pc = 32'd0;
        hot_count = 0;
        uniq_count = 0;
        for (int i = 0; i < hotpc_fill; i++) begin
            found = 1'b0;
            for (j = 0; j < uniq_count; j++)
                if (uniq_pc[j] === hotpc_ring[i]) begin
                    found = 1'b1;
                    uniq_n[j] = uniq_n[j] + 1;
                end
            if (!found) begin
                uniq_pc[uniq_count] = hotpc_ring[i];
                uniq_n [uniq_count] = 1;
                uniq_count = uniq_count + 1;
            end
        end
        for (int i = 0; i < uniq_count; i++) begin
            if (uniq_n[i] > hot_count) begin
                hot_count = uniq_n[i];
                hot_pc    = uniq_pc[i];
            end
        end
    endtask

    task automatic emit_verdict_and_finish();
        logic [31:0] hot_pc;
        int          hot_count;
        string       verdict;
        // Ch273 — snapshot caller-saved regs at halt so an
        // unhandled SYSCALL verdict carries the kernel-call ABI.
        // $v1 = syscall number; $a0..$a3 = call args.
        logic [31:0] halt_v1, halt_a0, halt_a1, halt_a2, halt_a3;
        halt_v1 = u_core.regfile[3];
        halt_a0 = u_core.regfile[4];
        halt_a1 = u_core.regfile[5];
        halt_a2 = u_core.regfile[6];
        halt_a3 = u_core.regfile[7];

        compute_hot_pc(hot_pc, hot_count);
        $display("[tb_ee_core_elf_runner] SUMMARY:");
        $display("[tb_ee_core_elf_runner]   elf_entry           = 0x%08h", elf_entry);
        $display("[tb_ee_core_elf_runner]   elf_stack_top       = 0x%08h", elf_stack_top);
        $display("[tb_ee_core_elf_runner]   entry_reached       = %0b", entry_reached);
        $display("[tb_ee_core_elf_runner]   retire_count        = %0d", retire_count);
        $display("[tb_ee_core_elf_runner]   sim_cycles          = %0d", sim_cycle_ctr);
        $display("[tb_ee_core_elf_runner]   saw_trap            = %0b  first_trap_pc=0x%08h instr=0x%08h",
                 saw_trap, first_trap_pc, first_trap_instr);
        $display("[tb_ee_core_elf_runner]   saw_unmapped_mmio   = %0b  first_unmap_ea=0x%08h pc=0x%08h",
                 saw_unmap, first_unmap_ea, first_unmap_pc);
        $display("[tb_ee_core_elf_runner]   saw_halt            = %0b  at_pc=0x%08h $v1=0x%08h $a0=0x%08h $a1=0x%08h $a2=0x%08h $a3=0x%08h",
                 saw_halt, core_pc, halt_v1, halt_a0, halt_a1, halt_a2, halt_a3);
        $display("[tb_ee_core_elf_runner]   hot_pc              = 0x%08h (count=%0d / ring=%0d)",
                 hot_pc, hot_count, hotpc_fill);
        // Ch289 — surface the first syscall 0x78 we HLE'd through,
        // since Codex asked for visibility into the arg shape. $v0
        // is the structural return value (0) from the dispatcher.
        $display("[tb_ee_core_elf_runner]   syscall_0x78        = seen=%0b count=%0d first_pc=0x%08h $a0=0x%08h $a1=0x%08h $a2=0x%08h $a3=0x%08h → $v0=0",
                 seen_syscall_0x78, syscall_0x78_count,
                 syscall_0x78_first_pc,
                 syscall_0x78_first_a0, syscall_0x78_first_a1,
                 syscall_0x78_first_a2, syscall_0x78_first_a3);
        // Ch290 — parallel surface for syscall 0x12 (handler install).
        $display("[tb_ee_core_elf_runner]   syscall_0x12        = seen=%0b count=%0d first_pc=0x%08h $a0=0x%08h $a1=0x%08h $a2=0x%08h $a3=0x%08h → $v0=0",
                 seen_syscall_0x12, syscall_0x12_count,
                 syscall_0x12_first_pc,
                 syscall_0x12_first_a0, syscall_0x12_first_a1,
                 syscall_0x12_first_a2, syscall_0x12_first_a3);
        // Ch291 — parallel surface for syscall 0x16 (handler enable).
        $display("[tb_ee_core_elf_runner]   syscall_0x16        = seen=%0b count=%0d first_pc=0x%08h $a0=0x%08h $a1=0x%08h $a2=0x%08h $a3=0x%08h → $v0=0",
                 seen_syscall_0x16, syscall_0x16_count,
                 syscall_0x16_first_pc,
                 syscall_0x16_first_a0, syscall_0x16_first_a1,
                 syscall_0x16_first_a2, syscall_0x16_first_a3);
        // Ch293 — parallel surface for syscall 0x7A (cache-sync).
        $display("[tb_ee_core_elf_runner]   syscall_0x7A        = seen=%0b count=%0d first_pc=0x%08h $a0=0x%08h $a1=0x%08h $a2=0x%08h $a3=0x%08h",
                 seen_syscall_0x7A, syscall_0x7A_count,
                 syscall_0x7A_first_pc,
                 syscall_0x7A_first_a0, syscall_0x7A_first_a1,
                 syscall_0x7A_first_a2, syscall_0x7A_first_a3);
        // Ch295 — per-$a0-class breakdown for syscall 0x7A. The
        // $a0-aware HLE returns 0x00020000 for $a0=4 and 0 otherwise.
        // first_v0_after / last_v0_after sample regfile[2] one cycle
        // AFTER the syscall retire (after the NBA commit).
        $display("[tb_ee_core_elf_runner]   syscall_0x7A_split  = count_a0_4=%0d count_a0_0x80000000=%0d count_a0_other=%0d last_a0=0x%08h first_v0=0x%08h last_v0=0x%08h",
                 syscall_0x7A_count_a0_4,
                 syscall_0x7A_count_a0_80000000,
                 syscall_0x7A_count_a0_other,
                 syscall_0x7A_last_a0,
                 syscall_0x7A_first_v0_after,
                 syscall_0x7A_last_v0_after);
        // Ch296 — parallel surface for syscall 0x79.
        $display("[tb_ee_core_elf_runner]   syscall_0x79        = seen=%0b count=%0d first_pc=0x%08h $a0=0x%08h $a1=0x%08h $a2=0x%08h $a3=0x%08h → $v0=0",
                 seen_syscall_0x79, syscall_0x79_count,
                 syscall_0x79_first_pc,
                 syscall_0x79_first_a0, syscall_0x79_first_a1,
                 syscall_0x79_first_a2, syscall_0x79_first_a3);
        // Ch297 — richer observer for syscall 0x77.
        $display("[tb_ee_core_elf_runner]   syscall_0x77        = seen=%0b count=%0d first_pc=0x%08h first_args=(0x%08h,0x%08h,0x%08h,0x%08h) last_args=(0x%08h,0x%08h,0x%08h,0x%08h) distinct_tuples=%0d → $v0=0",
                 seen_syscall_0x77, syscall_0x77_count,
                 syscall_0x77_first_pc,
                 syscall_0x77_first_a0, syscall_0x77_first_a1,
                 syscall_0x77_first_a2, syscall_0x77_first_a3,
                 syscall_0x77_last_a0, syscall_0x77_last_a1,
                 syscall_0x77_last_a2, syscall_0x77_last_a3,
                 syscall_0x77_distinct_tuples);
        for (int i = 0; i < 4; i++) begin
            if (syscall_0x77_tuple_valid[i]) begin
                $display("[tb_ee_core_elf_runner]     0x77 tuple[%0d] = ($a0=0x%08h,$a1=0x%08h,$a2=0x%08h,$a3=0x%08h) count=%0d",
                         i,
                         syscall_0x77_tuple_a0[i], syscall_0x77_tuple_a1[i],
                         syscall_0x77_tuple_a2[i], syscall_0x77_tuple_a3[i],
                         syscall_0x77_tuple_count[i]);
            end
        end
        // Ch301 — parallel surface for syscall 0x17.
        $display("[tb_ee_core_elf_runner]   syscall_0x17        = seen=%0b count=%0d first_pc=0x%08h first_args=(0x%08h,0x%08h,0x%08h,0x%08h) last_args=(0x%08h,0x%08h,0x%08h,0x%08h) distinct_tuples=%0d → $v0=0",
                 seen_syscall_0x17, syscall_0x17_count,
                 syscall_0x17_first_pc,
                 syscall_0x17_first_a0, syscall_0x17_first_a1,
                 syscall_0x17_first_a2, syscall_0x17_first_a3,
                 syscall_0x17_last_a0, syscall_0x17_last_a1,
                 syscall_0x17_last_a2, syscall_0x17_last_a3,
                 syscall_0x17_distinct_tuples);
        for (int i = 0; i < 4; i++) begin
            if (syscall_0x17_tuple_valid[i]) begin
                $display("[tb_ee_core_elf_runner]     0x17 tuple[%0d] = ($a0=0x%08h,$a1=0x%08h,$a2=0x%08h,$a3=0x%08h) count=%0d",
                         i,
                         syscall_0x17_tuple_a0[i], syscall_0x17_tuple_a1[i],
                         syscall_0x17_tuple_a2[i], syscall_0x17_tuple_a3[i],
                         syscall_0x17_tuple_count[i]);
            end
        end
        // Ch302 — parallel surface for syscall 0x13.
        $display("[tb_ee_core_elf_runner]   syscall_0x13        = seen=%0b count=%0d first_pc=0x%08h first_args=(0x%08h,0x%08h,0x%08h,0x%08h) last_args=(0x%08h,0x%08h,0x%08h,0x%08h) distinct_tuples=%0d → $v0=0",
                 seen_syscall_0x13, syscall_0x13_count,
                 syscall_0x13_first_pc,
                 syscall_0x13_first_a0, syscall_0x13_first_a1,
                 syscall_0x13_first_a2, syscall_0x13_first_a3,
                 syscall_0x13_last_a0, syscall_0x13_last_a1,
                 syscall_0x13_last_a2, syscall_0x13_last_a3,
                 syscall_0x13_distinct_tuples);
        for (int i = 0; i < 4; i++) begin
            if (syscall_0x13_tuple_valid[i]) begin
                $display("[tb_ee_core_elf_runner]     0x13 tuple[%0d] = ($a0=0x%08h,$a1=0x%08h,$a2=0x%08h,$a3=0x%08h) count=%0d",
                         i,
                         syscall_0x13_tuple_a0[i], syscall_0x13_tuple_a1[i],
                         syscall_0x13_tuple_a2[i], syscall_0x13_tuple_a3[i],
                         syscall_0x13_tuple_count[i]);
            end
        end
        // Ch304 — surface for syscall 0x6B.
        $display("[tb_ee_core_elf_runner]   syscall_0x6B        = seen=%0b count=%0d first_pc=0x%08h first_args=(0x%08h,0x%08h,0x%08h,0x%08h) last_a0=0x%08h last_a3=0x%08h → $v0=0",
                 seen_syscall_0x6B, syscall_0x6B_count,
                 syscall_0x6B_first_pc,
                 syscall_0x6B_first_a0, syscall_0x6B_first_a1,
                 syscall_0x6B_first_a2, syscall_0x6B_first_a3,
                 syscall_0x6B_last_a0, syscall_0x6B_last_a3);
        // Ch299 — library-ready gate state. initial = value at cycle
        // 100 (before init zero-writes); final = value at end-of-sim;
        // poke source = "syscall_0x77_narrow_match" if any matching
        // 0x77 call fired the gate write.
        $display("[tb_ee_core_elf_runner]   library_gate        = addr=0x%08h initial=0x%08h final=0x%08h poked=%0b poke_count=%0d first_poke_cycle=%0d source=%s",
                 LIBRARY_READY_GATE_ADDR,
                 library_ready_initial_value,
                 library_ready_final_value,
                 library_ready_poked,
                 library_ready_poke_count,
                 library_ready_first_poke_cycle,
                 library_ready_poked ? "syscall_0x77_narrow_match" : "(unwritten)");

        // Verdict precedence: trap > unmapped > halt > timeout > reached_only.
        if (saw_trap) begin
            verdict = $sformatf("elf_first_unsupported_opcode (pc=0x%08h instr=0x%08h — strict-mode trap; the EE core doesn't decode this opcode and there's no NOP-class fallback. Implement the missing opcode OR widen is_nop_class if it's a no-op-equivalent.)",
                                first_trap_pc, first_trap_instr);
        end else if (saw_unmap) begin
            verdict = $sformatf("elf_first_unmapped_mmio (ea=0x%08h pc=0x%08h — the ELF accessed an EE address with no region mapping. Add a region stub OR identify the protocol/device behind it.)",
                                first_unmap_ea, first_unmap_pc);
        end else if (saw_halt) begin
            // Ch273 — detect SYSCALL halt: the retired_instr was 0xc
            // (syscall opcode). If so, frame it as
            // elf_first_unhandled_syscall with $v1/$a0..$a3 + PC.
            // peek_instr_at-halt is the syscall PC; the instr there
            // is the syscall word.
            if (u_core.retired_instr == 32'h0000_000C) begin
                verdict = $sformatf("elf_first_unhandled_syscall (pc=0x%08h $v1=0x%08h (syscall_num=%0d) $a0=0x%08h $a1=0x%08h $a2=0x%08h $a3=0x%08h — the EE syscall HLE dispatcher doesn't recognize this $v1. Next chapter: add the case OR widen the dispatcher.)",
                                    core_pc, halt_v1, halt_v1, halt_a0, halt_a1, halt_a2, halt_a3);
            end else begin
                verdict = $sformatf("elf_halted (pc=0x%08h retired_instr=0x%08h — core asserted halt_o on a non-syscall instruction. Likely a strict-mode classification edge case; inspect the instr.)",
                                    core_pc, u_core.retired_instr);
            end
        end else if (retire_count == 0) begin
            verdict = "elf_no_retires (the EE core never retired any instruction — bootstrap or rst/go wiring is broken)";
        end else if (!entry_reached) begin
            verdict = $sformatf("elf_entry_unreached (retired_count=%0d but PC never matched elf_entry=0x%08h — bootstrap trampoline failed to jump correctly)",
                                retire_count, elf_entry);
        end else begin
            verdict = $sformatf("elf_timeout_with_hot_pc (watchdog after %0d ns, %0d retires, hot_pc=0x%08h count=%0d in ring of %0d — ELF reached entry and ran real code. For the synthetic test this is the expected verdict; for a real ELF it means the program is in a steady-state loop, often waiting on a service we haven't modelled.)",
                                watchdog_ns, retire_count, hot_pc, hot_count, hotpc_fill);
        end
        $display("[tb_ee_core_elf_runner] verdict=%s", verdict);
        $display("[tb_ee_core_elf_runner] PASS");
        $finish;
    endtask

    // End on first decisive event (other than entry_reached, which is
    // just an enabler) — strict-mode trap, unmapped MMIO, halt, or
    // watchdog. We don't short-circuit the run on entry_reached itself
    // because that's the success-but-keep-going case.
    //
    // Plain `always` (not _ff) so we can issue an extra event control to
    // let trace sinks flush before $finish.
    logic verdict_emitted;
    initial verdict_emitted = 1'b0;
    always @(posedge clk) begin
        if (rst_n && !verdict_emitted
                && (saw_trap || saw_unmap || saw_halt)) begin
            verdict_emitted <= 1'b1;
            @(posedge clk);  // let trace sinks flush
            emit_verdict_and_finish();
        end
    end

    // Watchdog.
    initial begin
        // wait until manifest is loaded (rst_n goes high), then start
        // counting time for the watchdog from rst-release.
        @(posedge rst_n);
        #(watchdog_ns);
        emit_verdict_and_finish();
    end

    // ------------------------------------------------------------------
    // Bootstrap: pre-zero EE RAM, $readmemh the image, read manifest,
    // poke BIOS trampoline at 0xBFC00000, release go and rst.
    //
    // Order:
    //  1. Wait for ee_ram_stub's own initial to run (zero-init when
    //     IMAGE_FILE param is empty — which it is here).
    //  2. Read +ELF_IMAGE plusarg, $readmemh into u_ee_ram.mem.
    //  3. Read +ELF_MANIFEST plusarg, $readmemh into manifest_mem;
    //     extract entry, stack_top.
    //  4. Optional: read +ELF_WATCHDOG_NS plusarg.
    //  5. Compute LUI+ORI+JR trampoline bytes for entry; poke BIOS.
    //  6. core_go = 0, rst_n low for ~10 cycles, rst_n high, core_go 1.
    // ------------------------------------------------------------------
    string elf_image_path;
    string elf_manifest_path;
    int    watchdog_override;

    initial begin
        core_go = 1'b0;
        rst_n   = 1'b0;

        // Allow ee_ram_stub's $display + zero-init initial to run.
        #1;

        if (!$value$plusargs("ELF_IMAGE=%s", elf_image_path)) begin
            $display("[tb_ee_core_elf_runner] FATAL: no +ELF_IMAGE=<path> plusarg");
            $finish;
        end
        if (!$value$plusargs("ELF_MANIFEST=%s", elf_manifest_path)) begin
            $display("[tb_ee_core_elf_runner] FATAL: no +ELF_MANIFEST=<path> plusarg");
            $finish;
        end

        $display("[tb_ee_core_elf_runner] loading EE RAM image: %s", elf_image_path);
        $readmemh(elf_image_path, u_ee_ram.mem);

        // Mirror the image into useg_shadow_mem too. ee_memory_map_stub
        // routes kuseg (top bit 0) to a separate 4-MiB shadow backing,
        // so a kuseg entry would otherwise fetch zeros. Real PS2
        // programs assume kuseg ≡ kseg0 via TLB; this mirror restores
        // that invariant at load time so either entry style works.
        // The image covers the first 4 MiB of EE RAM (qword 0..262143
        // = byte 0..0x3FFFFF) which exactly fits the useg_shadow's
        // capacity. Each qword splits into 4 word-indexed entries.
        $display("[tb_ee_core_elf_runner] mirroring image into useg_shadow_mem");
        for (int qw = 0; qw < EE_RAM_QWORDS; qw++) begin
            u_ee_map.g_useg_shadow.useg_shadow_mem[(qw*4) + 0] = u_ee_ram.mem[qw][ 31:  0];
            u_ee_map.g_useg_shadow.useg_shadow_mem[(qw*4) + 1] = u_ee_ram.mem[qw][ 63: 32];
            u_ee_map.g_useg_shadow.useg_shadow_mem[(qw*4) + 2] = u_ee_ram.mem[qw][ 95: 64];
            u_ee_map.g_useg_shadow.useg_shadow_mem[(qw*4) + 3] = u_ee_ram.mem[qw][127: 96];
        end

        $display("[tb_ee_core_elf_runner] loading manifest: %s", elf_manifest_path);
        $readmemh(elf_manifest_path, manifest_mem);
        elf_entry     = manifest_mem[0];
        elf_stack_top = manifest_mem[1];
        $display("[tb_ee_core_elf_runner] elf_entry=0x%08h elf_stack_top=0x%08h",
                 elf_entry, elf_stack_top);

        if ($value$plusargs("ELF_WATCHDOG_NS=%d", watchdog_override))
            watchdog_ns = watchdog_override;

        // Bootstrap trampoline at 0xBFC00000:
        //   LUI  $1, hi(entry)
        //   ORI  $1, $1, lo(entry)
        //   JR   $1
        //   NOP  (delay slot)
        u_bios.mem[0] = {16'h3C01, elf_entry[31:16]};            // lui $1, hi
        u_bios.mem[1] = {16'h3421, elf_entry[15:0]};             // ori $1, $1, lo
        u_bios.mem[2] = 32'h00200008;                            // jr $1
        u_bios.mem[3] = 32'h00000000;                            // nop
        $display("[tb_ee_core_elf_runner] BIOS trampoline @0xBFC00000:");
        $display("[tb_ee_core_elf_runner]   lui $1, 0x%04h", elf_entry[31:16]);
        $display("[tb_ee_core_elf_runner]   ori $1, $1, 0x%04h", elf_entry[15:0]);
        $display("[tb_ee_core_elf_runner]   jr  $1");
        $display("[tb_ee_core_elf_runner]   nop");

        // Reset pulse.
        repeat (10) @(posedge clk);
        rst_n   = 1'b1;
        @(posedge clk);
        core_go = 1'b1;
        $display("[tb_ee_core_elf_runner] go released at cyc=%0d", sim_cycle_ctr);
    end

endmodule
