# Ch294 closeout — wait-loop autopsy; verdict = `qbert_waiting_on_memory_flag`

**Status:** Closed. Observation-only chapter per Codex's framing.
**Named verdict:** `qbert_waiting_on_memory_flag` — specifically,
qbert is waiting on a **syscall-returned status word** with bit 17
(0x00020000) set. Our HLE returns 0 unconditionally → bit 17 never
appears → loop runs forever.

No RTL changes. No new TBs. Two artifacts produced: the
disassembly + runtime-trace analysis below, and the Ch295 framing
proposal at the bottom.

## The wait loop, fully decoded

### Disassembly: `0x00112400..0x00112480`

```
0x00112400: 0x24020001   addiu  $v0, $zero, 1
0x00112404: 0x3c048000   lui    $a0, 0x8000
0x00112408: 0x0c044264   jal    0x00110990         ← syscall 0x7A wrapper
0x0011240c: 0xae22c020   sw     $v0, -16352($s1)   (delay slot)
0x00112410: 0x14400021   bne    $v0, $zero, 0x00112498
0x00112414: 0xae020008   sw     $v0, 8($s0)        (delay slot)
0x00112418: 0x3c100002   lui    $s0, 0x2           ; $s0 = 0x00020000 (the mask!)
0x0011241c: 0x00000000   nop
─── LOOP TOP ───────────────────────────────────────────────────
0x00112420: 0x0c044264   jal    0x00110990         ← call wrapper
0x00112424: 0x24040004   addiu  $a0, $zero, 4      (delay slot — $a0 = 4)
0x00112428: 0x00501024   and    $v0, $v0, $s0      ; $v0 &= 0x00020000
0x0011242c: 0x1040fffc   beq    $v0, $zero, 0x00112420  ← HOT BRANCH
─── exit-of-loop continues from 0x00112430 ────────────────────
0x00112430: 0x24040002   addiu  $a0, $zero, 2
0x00112434: 0x0c044264   jal    0x00110990         ; one more 0x7A call (different $a0)
0x00112438: 0x3c110013   lui    $s1, 0x13
0x0011243c: 0x2630c000   addiu  $s0, $s1, -16384   ; $s0 = 0x0012C000
...
```

### The called function at `0x00110990`

```
0x00110990: 0x2403007a   addiu  $v1, $zero, 122   ; $v1 = 0x7A
0x00110994: 0x0000000c   syscall                  ; ← syscall 0x7A
0x00110998: 0x03e00008   jr     $ra
0x0011099c: 0x00000000   nop                      ; (delay slot)
```

A 4-instruction syscall-0x7A wrapper. Zero memory access. Just sets
`$v1 = 0x7A` and traps. Whatever arg is in `$a0` at call-time gets
threaded through.

A neighboring wrapper at `0x00110980` does the same for syscall
0x71 (= 113) — not exercised by this wait loop but visible in the
disassembly.

## Runtime confirmation (from trace files)

After re-running qbert.elf with the current model:

| PC        | IFETCH count | Notes |
|-----------|--------------|-------|
| 0x00112420 (loop-top JAL) | 181,494 | matches `syscall_0x7A count=181494` exactly |
| 0x00112424 (addiu delay)  | 181,494 | (same) |
| 0x00112428 (AND)          | 181,494 | (same) |
| 0x0011242C (BEQ)          | 181,493 | one fewer — the iteration that left the loop never reached it... wait, that's the OPPOSITE direction. Actually 181,494 reaches BEQ but loops back, the 181,495th call doesn't fire because we hit the watchdog mid-iteration. Either way: ~181k iterations confirmed. |
| 0x00110990 (wrapper)      | 181,494 | matches |
| 0x00110994 (syscall)      | 181,494 | matches |

**Map-event region breakdown across the full 1.66M-retire run:**

| Region | Count | Meaning |
|--------|-------|---------|
| REGION_USEG_SHADOW (0x0B) | 1,677,113 | qbert's own code+data (almost all IFETCH-side) |
| REGION_BIOS (0x00) | 4 | initial trampoline (before ELF entry) |
| REGION_EE_DMAC_PASSIVE (0x0E) | 1 | one access during Ch288's per-channel init |
| REGION_EE_DMAC_CTRL (0x0D) | 1 | one access during Ch287's D_STAT init |

**The wait loop performs ZERO MMIO accesses.** Not INTC, not D_STAT,
not GS CSR, not BIU, not GS_PRIV. The only data traffic in the
loop is the syscall return value through $v0.

## Verdict, per Codex's 5-verdict enum

**`qbert_waiting_on_memory_flag`** is the closest match — though
strictly the polled state is a *syscall-returned bitmask*, not a
direct memory read. The "memory" being polled is the kernel's
internal state, surfaced via the syscall 0x7A return value.

Specifically: **bit 17 (0x00020000) of the value returned by
`syscall 0x7A($a0=4)`.**

Other verdicts ruled out:
- `qbert_waiting_on_dmac_handler` — qbert is NOT polling D_STAT or
  D_PCR. (Although the wait *might* exit when the registered DMAC
  handler at 0x00112AB0 fires and sets some kernel state that
  syscall 0x7A surfaces. That's an indirect dependency.)
- `qbert_waiting_on_vblank` — qbert is NOT polling GS CSR or any
  VBLANK-related MMIO.
- `qbert_waiting_on_thread_scheduler` — possible secondary
  interpretation if syscall 0x7A is a sema/event-flag poll, but
  there's no thread-switch primitive being called.
- `qbert_wait_loop_unknown` — definitely not unknown; we have full
  decoding.

## What is syscall 0x7A really?

Two earlier chapters introduced syscall 0x7A as a stub. At Ch292
we labeled it "likely SyncDCache" because of the proximity to MIPS
SYNC. **The Ch294 autopsy makes that label questionable.** A real
SyncDCache wouldn't be invoked 181k+ times in a tight poll, and
SyncDCache returns void or a status code with bit 17 having no
defined meaning.

The observed shape — `(small int $a0)` → `(bitmask $v0)` polled in
a loop — fits better with one of:

1. **`GsGetIMR` / `iGsGetIMR` / `GsPutIMR`** — GS Interrupt Mask
   Register access. Bit 17 in some kernel-layered GS-IMR-related
   word could correspond to "VSYNC complete" or "GS finish."
2. **`PollSema` / `iPollSema`** — semaphore-state poll. $a0 would
   be a sema handle; the return is a status word with one of the
   bits indicating "released."
3. **A multiplexed `GetEvent` / `iGetEvent`** — kernel
   event-channel query. $a0 is a channel selector; return is a
   bitmask of pending events.
4. **A kernel-internal status word** that the SyncDCache call
   *also* returns alongside the cache-sync side effect. Bit 17
   would be some "subsystem ready" flag.

In all four cases, the structural fact is the same: **qbert is
waiting for a kernel-managed bit that the HLE doesn't currently
update**. The exact SDK name is less important than: "what should
make bit 17 set?"

Notable: the call at `0x00112408` (BEFORE the wait loop) uses
`$a0 = 0x80000000`, and qbert *expects $v0 = 0* (BNE not-taken
falls into the wait). With our HLE returning 0, qbert correctly
takes the "init OK" path and enters the wait. So this is not a
case where syscall 0x7A's HLE is wrong universally — it's only
wrong for the `$a0 = 4` polling call, where qbert wants a
non-zero specific bit.

## Ch295 framing — the gate is named, now decide how to open it

Three concrete strategies for Codex to weigh:

### Strategy A: Bit-17-flipper HLE patch (cheapest)

After N calls to syscall 0x7A with `$a0 = 4`, the dispatcher
returns `$v0` with bit 17 set (0x00020000). Lets qbert progress.
Risk: bit 17 may not be the *only* thing qbert checks; downstream
code might check additional bits (different `$a0` selectors,
different bit masks). Empirically cheap; one experiment.

Sub-question for Codex: should bit 17 set on every call, or only
after N calls? Setting it always might cause downstream "saw the
ready bit, now go process the event" code to misbehave (e.g., it
might try to read a "completed" event that doesn't exist).
Setting after N might let qbert see one "no" then a "yes" —
matching realistic interrupt-arrival semantics.

### Strategy B: Identify the real SDK semantics (correct path)

Look up PS2 SDK syscall 122 / 0x7A in the canonical kernel
sources (ps2sdk's iop/kernel/include/kernel.h or similar). The
syscall name + arg-shape + return-shape will tell us what kernel
state to model. If it's `GsGetIMR`, we need a GS IMR register;
if it's `PollSema`, a sema table; if it's `GetEvent`, an event-
channel table.

This is more correct but requires more upfront work. The
disassembly is rich enough that the SDK name is probably
identifiable. Codex likely knows or can look up.

### Strategy C: Wire DMAC-completion to bit 17 (interpretive)

The handler registered in Ch290/291 (at 0x00112AB0, for DMAC ch5
SIF0) was never invoked. **Hypothesis:** the wait loop is qbert
asking "has my DMAC-ch5-SIF0 handler run yet?" If we can fire
that handler — even just once — bit 17 might set as a side
effect. This requires modeling interrupt delivery:
COP0 Status → Cause IP → vector to handler.

Strategy C is correct architecturally but is multiple chapters
worth of work (interrupt delivery isn't modeled at all yet).
Don't pivot to this without confirming the hypothesis first.

## Recommendation for Codex

Try **Strategy A** as a one-experiment chapter: HLE patches
syscall 0x7A($a0=4) to return `$v0 = 0x00020000` after, say, the
10th call. If qbert progresses past the wait and the next blocker
is informative, great. If qbert misbranches into garbage, fall
back to **Strategy B** (look up the SDK semantics) and we'll
know which bit-17 source to model.

The disassembly evidence makes Strategy A safe to try: bit 17 is
the only thing the wait loop checks; there's no other "consumer"
state that depends on the value being a specific channel-bitmask
encoding. Setting bit 17 alone should make the wait exit cleanly.

## Files

- `/tmp/ch294_disasm.py` — focused R5900 disassembler used to
  produce the listings above. Not committed; one-shot diagnostic.
- This closeout document.

## Pattern review (24 chapters; first investigation chapter since
Ch263..Ch269)

| Era | Chapters | Description |
|-----|----------|-------------|
| Opcode-blocker | Ch271..Ch286 | R5900 opcodes, one per chapter |
| MMIO stubs | Ch287..Ch288 | DMAC ctrl + per-channel |
| Syscall HLE | Ch273, 285, 289..291, 293 | $v0=0 narrow extensions |
| Narrow NOP-class | Ch286 (EI), Ch292 (SYNC) | side-effect-free accepts |
| **Investigation** | **Ch294** | **wait-loop autopsy, no RTL change** |

The Ch263..Ch269 BIOS-treadmill autopsies established the
"investigation chapter" pattern: spend a chapter understanding a
steady-state loop before deciding what to change. Ch294 is the
qbert-side analog and produces the same artifact: a *named gate*
+ a *concrete next-step proposal*.

## Regression

Unchanged at **176/176** — no RTL or TB changes in Ch294.
