# Ch299 closeout — Strategy B-lite: narrow library-ready gate poke; wait loop collapses **Status:** Closed. Codex's "Strategy B-lite" (TB-side poke triggered by narrow syscall 0x77 match) worked first try. **Verdict from re-running qbert.elf:** `elf_first_unsupported_opcode (pc=0x00110BB4 instr=0x70081EE9)` — qbert exited the Ch298 wait loop on iteration 1 and advanced into new code, hitting an unimplemented MMI3 sub-op. ## What landed The TB-side gate-poke pattern: tb_ee_core_elf_runner now observes syscall 0x77 retires and, when the args match the qbert-specific narrow guard, writes 1 to the polled memory location. ### Implementation — `sim/tb/integration/tb_ee_core_elf_runner.sv` Per Codex's framing ("if direct memory write from syscall FSM is awkward, then a TB-side poke is acceptable, but trigger it on observing syscall 0x77, not on an arbitrary cycle"): ```sv 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; ``` Narrow guard: ```sv if ((a0 >= 32'h001D_FD50) && (a0 <= 32'h001D_FDB0) && ((a3 == 32'h0000_0010) || (a3 == 32'h0000_0014))) begin u_ee_map.useg_shadow_mem[LIBRARY_READY_SHADOW_IDX] <= LIBRARY_READY_GATE_VALUE; library_ready_poke_count <= library_ready_poke_count + 1; ... end ``` The guard matches **exactly** the two arg tuples Ch297 observed ($a0 ∈ {0x001DFD50, 0x001DFDB0}, $a3 ∈ {0x14, 0x10}). RTL-side direct write from the syscall FSM was rejected as too invasive (would require a new state and combinational map-driver changes). TB-side poke is Codex's authorized fallback. ### SUMMARY line — `library_gate` ``` library_gate = addr=0x001329c0 initial=0x00000000 final=0x00000001 poked=1 poke_count=2 first_poke_cycle=100093 source=syscall_0x77_narrow_match ``` - **initial** (sampled at cycle 100): 0 (matches Ch298's "starts zero" observation). - **final** (sampled continuously, latches last value): 1 (gate is now non-zero, wait condition satisfied). - **poke_count = 2**: both qbert-observed 0x77 calls (with $a3=0x14 and $a3=0x10) fired the poke. - **first_poke_cycle = 100,093**: just after qbert's second init zero-write at cycle 98,576 — the order is correct (zero-write first, then poke, so the poked-1 doesn't get clobbered). - **source = "syscall_0x77_narrow_match"**: the poke fired from the narrow-matched syscall observer, NOT a blind cycle-fixed poke. ## The narrow guard's third-tuple falsifier The qbert run after Ch299 shows a **THIRD** distinct 0x77 tuple: ``` syscall_0x77 = count=3 distinct_tuples=3 tuple[0] = ($a0=0x001dfd50, $a3=0x14) count=1 ← matches guard, fires poke tuple[1] = ($a0=0x001dfdb0, $a3=0x10) count=1 ← matches guard, fires poke tuple[2] = ($a0=0x001dfd70, $a3=0x40) count=1 ← $a3 outside guard, NO poke ``` The new third call wasn't visible in Ch297's qbert run because the wait loop blocked qbert from making it. With the Ch299 gate opening, qbert advanced past the wait loop and made this third 0x77 call before hitting the opcode trap. **The narrow guard correctly excluded the third tuple** ($a3=0x40 is not in {0x10, 0x14}). poke_count=2 (not 3) confirms it. This is exactly the falsifiability surface Codex asked for — if the guard were too broad, poke_count would equal count_0x77 even when new arg shapes surface. ## qbert progression | Chapter | Blocker | retire_count | Notes | |---|---|---|---| | Post-Ch297 (0x77) | wait loop spinning | 1,469,235 (watchdog) | gate never set | | **Post-Ch299 (gate poke)** | **MMI3 opcode trap at 0x70081EE9** | **28,655** | gate→1 at cycle 100,093; loop exits iter 1 | The retire count *appears* smaller (28,655 < 1,469,235) but that's misleading — Ch297's number included the 1.44M spin. The MEANINGFUL signal is the **verdict-shape change** from `elf_timeout_with_hot_pc` (stuck) → `elf_first_unsupported_opcode` (concrete next demand). Same shape transition as Ch295. ## Ch300 framing — new MMI3 sub-op at sa=0x1B The new trap is opcode `0x70081EE9` at PC 0x00110BB4. Decode: - opcode = `011100` = 0x1C (MMI) - rs = `00000` = $0 - rt = `01000` = 8 = $t0 - rd = `00011` = 3 = $v1 - sa = `11011` = 0x1B (= 27) - funct = `101001` = 0x29 = MMI3 So this is **MMI3 / sa = 0x1B**, an unimplemented MMI3 sub-op. Our current MMI3 coverage: - sa 0x0E = PCPYUD (Ch283) - sa 0x13 = PNOR (Ch281) sa 0x1B is **new**. Per R5900 references, possibilities: - **PEXEH** (Parallel Exchange Even Halfword) — sa 0x1A in some sources - **PREVH** (Parallel Reverse Halfword) — sa 0x1B - **PEXCH** (Parallel Exchange Center Halfword) — sa 0x1A If sa 0x1B is PREVH: reverses the order of 16-bit halfwords within each 64-bit doubleword. Mechanical Ch300 chapter: extend MMI3 narrow-decode (Ch278 pattern) with `MMI3_PREVH = 5'h1B`, add `is_prevh`, add the writeback arm that implements halfword reversal across the 128-bit shadow (similar to PCPYUD's full-128 writeback). ~4-5 RTL edits + focused TB. This is **back to opcode-era for one chapter** — fitting since Ch299 cleared the wait loop and qbert progressed to executable code with new MMI demands. ## Pattern milestone The third clean "inflection → autopsy → unblock" cycle is **not** needed yet. Ch299 successfully unblocked the second wait loop, and qbert is back in opcode-trap mode. The pattern can be sequenced more flexibly than I expected: | Cycle | Inflection | Autopsy | Unblock | |-------|------------|---------|---------| | 1 | Ch293 (1.66M, 0x0011242C) | Ch294 (syscall 0x7A bit-17) | Ch295 ($a0-aware HLE) | | 2 | Ch297 (1.47M, 0x00112554) | Ch298 (memory poll 0x001329C0) | **Ch299 (narrow 0x77 gate poke)** | ## Documentation status: qbert-specific HLE Per Codex's instruction: "document this as a qbert-specific library-ready HLE, not architectural truth." This is explicitly **NOT** a faithful model of PS2 kernel behavior. The real PS2 kernel's RegisterLibraryEntries writes a "library ready" word based on the registration record layout + the registered library's status. Our TB-side poke writes 1 to a hardcoded address that happens to match qbert's specific poll target. Risks if another ELF uses syscall 0x77: - A different ELF with $a0 in the same range AND $a3 in {0x10, 0x14} would also get its 0x001329C0 word poked to 1 — potentially wrong if the ELF expects 0 or a different value. - An ELF with different registration buffer addresses won't get the poke at all (correct, since the guard is narrow). The risk is **low for qbert** but should be revisited if Ch300+ surfaces another ELF or another syscall pattern in the same area. ## Files changed - `sim/tb/integration/tb_ee_core_elf_runner.sv` — 6 new state signals + observer arm with narrow guard + SUMMARY display. No RTL changes. No new TB target. Regression count unchanged at **176/176**. ## Regression **176/176 PASS** (unchanged from Ch298; runner-only changes).