Files
retroDE_ps2/docs/ch303_closeout.md
thejayman77 ec82764bef Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)
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>
2026-06-29 20:10:50 -04:00

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.