Skip to content

fix(runtime): anchor SetupThread's automatic main stack at the top of RDRAM and record kernel-managed stack metadata - #398

Open
LeandroSSB wants to merge 2 commits into
ran-j:mainfrom
LeandroSSB:pr-01-setupthread-anchor
Open

LeandroSSB wants to merge 2 commits into
ran-j:mainfrom
LeandroSSB:pr-01-setupthread-anchor

Conversation

@LeandroSSB

Copy link
Copy Markdown

Root cause

SetupThread (syscall 0x3C) with stack = -1 — the automatic-stack case, the default crt0 path — computed the initial $sp as PS2_RAM_SIZE - requestedSize. Games link static arenas right under the region they expect the kernel to pick, so the honored placement can land the live init frames inside the game's own arena.

On MOH:Vanguard (SLUS_215.97) the static arena ends at 0x1FD81C0 and the game asks for a 0x28000 stack: the delivered sp was 0x1FD8000 — 0x1C0 below the arena end. The game's first static malloc returned a block adjacent to the stack base and its init zero-fill wiped the live frames of the whole init call chain: the epilogue did jr $ra with $ra read from zeroed stack → pc=0 → silent makeDormant of the main thread; only interrupts ran from then on.

This is also the metadata half of the shape documented in #259 ("the stack value is the base after CreateThread but a top after SetupThread"): SetupThread anchors $sp at the region's TOP, but stack is a base everywhere else in the ABI (CreateThread passes the low address, ReferThreadStatus reports the allocation's lower bound).

Fix

  • With stack = -1, anchor the initial sp at the top of RDRAM (PS2_RAM_SIZE - 0x10, the same value run() seeds); the requested size stays recorded as the thread's stack_size metadata.
  • ThreadInfo.stack records the kernel-managed BASE (anchor − stackSize), not the anchor top, so consumers reading the metadata see the real extent (base + size reconstructs the anchor exactly; stackSize == 0 keeps the anchor).
  • Companion commit: the three silent dispatchGuestBranch failure paths ([dispatch-pc0], [dispatch-yield], no-function branch) now log behind RUNTIME_LOG — the [dispatch-pc0] line is what pinpointed the death above.

Test

ps2xTest (ps2_runtime_kernel_tests.cpp, ps2_gs_tests.cpp):

  • "SetupThread anchors the automatic main stack at the top of RDRAM" — fails without the fix; asserts the anchor sits above the arena placement by the requested size.
  • "SetupThread exposes stable main stack metadata through ReferThreadStatus" — asserts the corrected base, its stability at two different live $sp values, and base + size == anchor.
  • The GS sync-V callback test now performs the game's own SetupThread(stack=-1, 0x28000) shape before registering the callback and asserts the anchor (PS2_RAM_SIZE - 0x10) with the metadata present.

Measured (MOH:Vanguard boot)

Distinct guest functions per 60 s run: 54 → 1041 (~19x); SifInitRpc completes with IOP modules loading; the [dispatch-pc0] silent-dormancy signature: present in every run before → 0 after.

🤖 Generated with Claude Code

Suite evidence

Fresh configure+build of ps2x_tests on this branch (VS 2022, Release): 447/447 passed, 0 failed.

LeandroSSB and others added 2 commits October 26, 2026 00:26
… RDRAM

SetupThread (syscall 0x3C) with stack=-1 (the automatic-stack case, the
default crt0 path) used to return PS2_RAM_SIZE - requestedSize as the
initial $sp. Games link static arenas right under the region they expect
the kernel to pick: on MOH:Vanguard (SLUS_215.97) the arena ends at
0x1FD81C0 and the honored placement (0x1FD8000) landed the live init
frames *inside* it. The game's first static malloc returned a block
adjacent to the stack base and its zero-fill wrote over the saved $ra of
the whole init call chain; the epilogue then jumped to pc=0,
dispatchGuestBranch saw pc==0, the scheduler made the main thread
dormant, and boot stalled with only interrupts running.

Anchor the automatic stack at the top of RDRAM (PS2_RAM_SIZE - 0x10, the
same value run() seeds) so it stays clear of any arena a game links near
the top of RAM.

Also record the kernel-managed region correctly in the thread metadata:
SetupThread anchors $sp at the region's TOP, but `stack` is a base
everywhere else in the ABI (CreateThread passes the low address,
ReferThreadStatus reports the allocation's lower bound), so ThreadInfo
now records initialStack = anchoredSp - stackSize (base + size
reconstructs the anchor; stackSize==0 keeps the anchor). This is the
metadata defect documented in issue ran-j#259 ("base after CreateThread but a
top after SetupThread").

Tests: "SetupThread anchors the automatic main stack at the top of
RDRAM" (the anchor must leave the requested size clear of the arena),
"SetupThread exposes stable main stack metadata through
ReferThreadStatus" (corrected base, stable at two live $sp values,
base+size == anchor), and the GS sync-V callback test now performs the
game's own SetupThread shape before registering the callback.

Evidence on MOH:Vanguard: distinct guest functions per 60s run went from
54 to 1041, SIF RPC init and IOP module loads (SIO2MAN/MTAPMAN/PADMAN)
now execute.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Three failure paths in the guest dispatch loop used to be invisible:

- EeScheduler::run(): the !hasFunction(context.pc) branch poisons
  context.pc to 0 when invocations are pending, with no report; the
  thread then goes dormant at the next pc==0 check. Log the pc before
  the poisoning.
- PS2Runtime::dispatchGuestBranch(): a checkpoint yield unwinds the
  whole guest C++ stack leaving pc at the pending call target; log the
  source/target so the resume point is visible.
- PS2Runtime::dispatchGuestBranch(): a callee returning with pc==0 (an
  epilogue jumping through a smashed $ra) silently unwinds the caller
  chain; log the registers at that point.

All three are RUNTIME_LOG-gated and fire at most once per failure, not
per dispatch. Found while booting MOH:Vanguard, where the only symptom
of the stack collision was a silent dormancy.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant