Files
retroDE_ps2/docs/ch303_closeout.md
T
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

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 (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.