Files
retroDE_ps2/docs/ch289_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

5.5 KiB

Ch289 closeout — syscall 0x78 HLE + runner-side observer; next is syscall 0x12 (handler install)

Status: Closed. Verdict from re-running qbert.elf: elf_first_unhandled_syscall (pc=0x00112A54 $v1=0x12 (=18)). qbert advanced 27,920 → 27,930 retires (+10) through the Ch289 syscall and into the next one. The new runner-side observer worked first try:

syscall_0x78 = seen=1 count=1 first_pc=0x00112aa4
  $a0=0x00000000 $a1=0x00130000 $a2=0x20000000 $a3=0x001328c0 → $v0=0

count=1 means qbert called syscall 0x78 exactly once, took our $v0=0 return, and continued. No tight loop or error branch — the return shape is good for at least the first occurrence.

What landed

Dispatcher case — rtl/ee/ee_core_stub.sv

One new case in the existing Ch273 HLE switch, identical shape to Ch285's 0x40 case:

32'h0000_0078: begin
    regfile[2]   <= 32'd0;
    gpr128[2]    <= 128'd0;
    pc           <= pc + 32'd4;
    retire_pulse <= 1'b1;
    state        <= S_IFETCH_REQ;
end

Focused TB extension — tb_ee_core_syscall_hle.sv

The same mechanical pattern used for the Ch285 0x40 extension: 4 new BIOS slots (S_ORI_V1_78 / S_SYS_78 / S_BNE_78 / S_DS_78), a new latch group (v0_after_78 / seen_78_return), a new init in the initial block, a new arm in the trace always_ff, a new post-halt assertion, and a new field in the final display. The UN/FAIL slot indices bumped by 4. The TB now covers five known syscall numbers (3C / 3D / 40 / 64 / 78) plus the unknown-halt path.

Result: retired=25 halt=1 trap=0 errors=0 PASS. Final display:

$v0_after_3C=0x001e0000 $v0_after_3D=0x00000000 $v0_after_64=0x00000000 $v0_after_40=0x00000000 $v0_after_78=0x00000000 $v1_at_halt=0x00007777

Runner-side observer — tb_ee_core_elf_runner.sv

Per Codex's "named trace/log line for syscall 0x78" ask, a small observer block captures the first occurrence of the syscall during the qbert run:

if (core_ev_valid && u_core.retired_instr == 32'h0000_000C
                  && u_core.regfile[3] == 32'h0000_0078) begin
    syscall_0x78_count <= syscall_0x78_count + 1;
    if (!seen_syscall_0x78) begin
        seen_syscall_0x78     <= 1'b1;
        syscall_0x78_first_pc <= u_core.retired_pc;
        syscall_0x78_first_a0 <= u_core.regfile[4];
        ...
    end
end

And a SUMMARY line:

[tb_ee_core_elf_runner]   syscall_0x78 = seen=1 count=1 first_pc=0x00112aa4
  $a0=0x00000000 $a1=0x00130000 $a2=0x20000000 $a3=0x001328c0 → $v0=0

Pattern is extensible: any future HLE'd syscall whose arg shape matters can drop a parallel observer block in. Each new tracked syscall costs ~10 LOC: declarations, init, observer, summary line.

qbert progression

Chapter Blocker retire_count
Post-Ch286 (EI) unmapped 0x1000E010 D_STAT 27,907
Post-Ch287 (DMAC ctrl) unmapped 0x1000C000 ch4 27,912
Post-Ch288 (DMAC passive) syscall $v1=0x78 at 0x00112AA4 27,920
Post-Ch289 (syscall 0x78) syscall $v1=0x12 at 0x00112A54 27,930

Three chapters in a row each in the +5 to +10 range — qbert is sweeping through its kernel-init sequence one HLE call at a time.

Ch290 framing — syscall 0x12

Args at halt (the new blocker):

  • $v1 = 0x12 (= 18 decimal)
  • $a0 = 0x00000005 — small int. Likely an IRQ number, priority, or handler slot index.
  • $a1 = 0x00112AB0 — falls in code segment (qbert main range was around 0x00112xxx). Almost certainly a function pointer.
  • $a2 = 0x00000000 — null/context.
  • $a3 = 0x001328C0 — data pointer (consistent with the $a3 seen in 0x78 and earlier syscalls — looks like a global context block).

Shape: (int small_id, fn_ptr handler, void* ctx0, void* ctx1) — this is the classic handler-install pattern. PS2 standard syscall table cites names like AddIntcHandler (syscall 16/0x10), RemoveIntcHandler (syscall 17/0x11), and AddDmacHandler (syscall 18/0x12) in this range — so $a0=5 is plausibly a DMAC channel number (we landed in the DMAC region last chapter; channel 5 = SIF0).

Per the Ch285 precedent: first pass returns $v0 = 0 ("handler installed OK") and PC += 4. If qbert misbranches downstream, the fallback shapes to try are: $v0 = $a0 (returns the slot index for later RemoveIntcHandler), or $v0 = some non-zero handle. The runner-side observer pattern from Ch289 makes the diagnostic cheap.

Files changed

  • rtl/ee/ee_core_stub.sv — one new HLE case (~10 LOC).
  • sim/tb/integration/tb_ee_core_syscall_hle.sv — extended with syscall 0x78 case (slots / latch / assertion / display).
  • sim/tb/integration/tb_ee_core_elf_runner.sv — syscall_0x78 observer + SUMMARY line.

No new TB, no new Makefile target; regression count unchanged at 175/175.

Pattern review (19 chapters)

Ch Blocker Pattern
271..286 opcodes opcode-era
287 DMAC ctrl MMIO NEW MMIO stub
288 DMAC passive REUSE Ch287 pattern
289 syscall 0x78 REUSE Ch273/285 dispatcher

Two narrow HLE extensions in five chapters (Ch285 + Ch289). The Ch273 dispatcher's switch-by-$v1 architecture continues to absorb new cases with minimal cost. The new runner-side observer pattern is a small upgrade that pays for itself the first time a syscall return value is wrong — instead of re-reading the trace file, the SUMMARY block tells you immediately what qbert handed the kernel.

Regression

175/175 PASS (unchanged from Ch288; no new TB added in this chapter, existing tb_ee_core_syscall_hle extended in place and runner observer added).