RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression (272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps, and all dump-derived textures/traces) is excluded via .gitignore and stays local. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.9 KiB
Ch303 closeout — caller-loop autopsy; verdict channel_loop_returns_ignored
Status: Closed. Observation-only chapter per Codex's framing.
No RTL, no new HLE cases. Named verdict:
channel_loop_returns_ignored for the syscall 0x6B path. The
disassembly also revealed the complete bounded set of remaining
syscall-wrapper functions, which lets Ch304 batch with confidence.
Key structural finding: these are wrapper TABLES, not loops
The regions Codex pointed at are tables of 4-instruction syscall-wrapper leaf functions, each of the form:
addiu $v1, $zero, <syscall_num>
syscall
jr $ra
nop
Table 1 — 0x00111D40..0x00111D9C
| Wrapper PC | $v1 set | syscall | status |
|---|---|---|---|
| 0x00111D40 | -67 (0xFFFFFFBD) | i-variant of 67 (0x43) | unhandled |
| 0x00111D50 | 68 (0x44) | 0x44 | unhandled |
| 0x00111D60 | 107 (0x6B) | 0x6B | current blocker |
| 0x00111D70 | 118 (0x76) | 0x76 | unhandled |
| 0x00111D80 | 119 (0x77) | 0x77 | done Ch297 |
| 0x00111D90 | 121 (0x79) | 0x79 | done Ch296 |
Table 2 — 0x00112A50..0x00112A8C
| Wrapper PC | $v1 set | syscall | status |
|---|---|---|---|
| 0x00112A50 | 18 (0x12) | 0x12 | done Ch290 |
| 0x00112A60 | 19 (0x13) | 0x13 | done Ch302 |
| 0x00112A70 | 22 (0x16) | 0x16 | done Ch291 |
| 0x00112A80 | 23 (0x17) | 0x17 | done Ch301 |
Table 2 is fully handled. Table 1 has 4 remaining: the i-variant -67, 0x44, 0x6B, 0x76.
The 0x6B caller IGNORES the return value
The immediate caller of the 0x6B wrapper is a function at
0x00111B00:
0x111b00: daddu $s1, $a0, $zero ; save $a0
0x111b08: daddu $s0, $a1, $zero ; save $a1
0x111b0c: lw $v0, -16392($v1) ; load a counter
0x111b10: addiu $v0, $v0, 1 ; ++counter
0x111b14: jal 0x00111330 ; helper
0x111b18: sw $v0, -16392($v1) ; store counter (delay slot)
0x111b1c: jal 0x00111d60 ; ← call syscall_0x6B wrapper
0x111b20: nop ; (delay slot)
0x111b24: daddu $a1, $zero, $zero ; $a1 = 0 ← does NOT read $v0
0x111b28: addiu $a2, $zero, 112 ; $a2 = 112
0x111b2c: jal 0x00110b88 ; next call (args set, $v0 ignored)
0x111b30: daddu $a0, $sp, $zero ; (delay slot)
After jal 0x00111d60 returns, the very next real instruction
(0x111b24) overwrites $a1 with 0 and sets up args for a different
call — $v0 from the 0x6B syscall is never tested or consumed.
→ For the 0x6B path: channel_loop_returns_ignored.
The 0x112A00 driver IS a loop, and it DOES capture $v0
For completeness (Codex asked about loop shape generally), the
function at 0x00112A00 is a genuine loop:
0x112a00: jal 0x00112a80 ; call syscall_0x17 wrapper
0x112a04: daddu $a0, $s1, $zero ; (delay) $a0 = $s1
0x112a08: daddu $s1, $v0, $zero ; $s1 = $v0 ← CAPTURES return
0x112a0c: sync
0x112a10: bne $s0, $zero, 0x112a30 ; loop-control on $s0 (NOT $v0)
0x112a18: daddu $v0, $s1, $zero ; return $s1
0x112a28: jr $ra
...
0x112a30: jal 0x00111c60
0x112a38: beq $zero, $zero, 0x112a1c
0x112a40: jal 0x00111c10
0x112a48: beq $zero, $zero, 0x112a00 ; ← loop back to top
So this loop captures $v0 into $s1 and threads it forward (as $a0 for the next iteration, or as the function's return value). However:
- It drives syscall 0x17 (already HLE'd to return 0).
- qbert progressed +87 retires with that 0 return — so a 0 return is tolerated here.
- The loop EXIT is gated by
$s0(0x112a10bne $s0,$0), not by the syscall return value.
So even the loop that captures $v0 doesn't gate on it — it just propagates it. Returning 0 is consistent with observed forward progress.
$a0=5 is constant — NOT a per-channel iteration
Across every observed call (0x17, 0x13, 0x6B), $a0 = 5 is
constant. If this were a for (ch=0..N) loop, $a0 would vary.
It doesn't. This is channel-5-specific initialization, not a
loop over all channels. The count=2 for 0x17/0x13 comes from
the 0x112A00 driver looping twice (gated by $s0), not from
iterating channel ids.
Verdict, per Codex's enum
| Verdict | Fit? |
|---|---|
channel_loop_returns_ignored |
YES (best) — the 0x6B caller at 0x111B24 discards $v0; the 0x112A00 loop captures but tolerates 0. |
channel_loop_checks_v0 |
Partial — the 0x112A00 loop captures $v0, but doesn't gate on it (loop exit is on $s0), and 0 has been tolerated. |
channel_loop_waits_on_event |
No — no spin; qbert progresses each chapter. |
channel_loop_unbounded |
No — the wrapper tables are finite; remaining set is exactly {-67, 0x44, 0x6B, 0x76}. |
channel_loop_shape_unknown |
No — fully decoded. |
Pick: channel_loop_returns_ignored. The current blocker
(0x6B) discards its return; the one loop that captures a syscall
return ($v0→$s1) tolerates 0 and gates its exit on a different
register.
Ch304 framing — batch the bounded remaining set
The autopsy proves the remaining unhandled syscalls in these init tables form a finite, enumerable set of 4:
- 0x6B (107) — current blocker, return ignored.
- 0x76 (118) — same wrapper table, almost certainly same "ignored return" treatment.
- 0x44 (68) — same table.
- -67 / 0xFFFFFFBD — i-variant (interrupt-context) of
syscall 67 (0x43). Negative-$v1 convention. Needs a dispatcher
case matching the 32-bit value
0xFFFFFFBD(or a "negative $v1 → treat as i-variant" decode if more i-variants surface).
Recommendation: Ch304 adds $v1 == 0x6B → $v0=0 (the proven
blocker). Then — given the autopsy shows the bounded set —
Ch305 could batch 0x76, 0x44, and the -67 i-variant in one
chapter, since they're all in the same wrapper table and the
0x6B caller pattern (ignored return) is representative.
Per Codex's "prefer the closeout propose Ch304 rather than combine," I'm NOT adding any HLE case in Ch303. Ch304 = add 0x6B alone, confirm qbert reaches 0x76 (or 0x44 or -67) next, then Ch305 batches the rest if the pattern holds.
One caution for the -67 i-variant: our dispatcher currently
matches exact unsigned $v1 values. -67 arrives as $v1 =
0xFFFFFFBD. A naive 32'h0000_0043 case would NOT match it. The
i-variant needs either its own 32'hFFFF_FFBD case or a
sign-aware decode. Flag for whoever frames the -67 chapter.
Files
/tmp/ch294_disasm.py— disassembler retargeted across 0x00111D40, 0x00112A00, 0x00111B00, 0x00111300 windows. Same one-shot diagnostic.- This closeout.
Pattern note — autopsy prevented trap-by-trap guessing
This is the value Codex predicted: instead of discovering 0x6B, 0x76, 0x44, -67 one trap at a time across four chapters, the single caller-loop autopsy enumerated the complete remaining set AND established that returns are ignored. Ch304+Ch305 can now clear the whole init-table sequence in two chapters with confidence rather than four blind ones.
Regression
Unchanged at 177/177 — no RTL or TB changes in Ch303.