ec82764bef
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>
177 lines
6.9 KiB
Markdown
177 lines
6.9 KiB
Markdown
# 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` (0x112a10 `bne $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**:
|
|
|
|
1. **0x6B** (107) — current blocker, return ignored.
|
|
2. **0x76** (118) — same wrapper table, almost certainly same
|
|
"ignored return" treatment.
|
|
3. **0x44** (68) — same table.
|
|
4. **-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.
|