Skip to content

integrate/fold-2026-07-24: HLE correctness + flow SPU dispatch + lifter jump-table fix - #81

Open
sp00nznet wants to merge 53 commits into
ps3recompfrom
integrate/fold-2026-07-24
Open

integrate/fold-2026-07-24: HLE correctness + flow SPU dispatch + lifter jump-table fix#81
sp00nznet wants to merge 53 commits into
ps3recompfrom
integrate/fold-2026-07-24

Conversation

@sp00nznet

Copy link
Copy Markdown
Owner

Folds the branches that accumulated since the last integration into one branch off ps3recomp. Three folded cleanly; the fourth (the 150-commit SPU branch) is held for a dedicated pass -- see the end.

What's in

sagemono/fix/hle-correctness -- @sagemono (16 commits)

A guest-ABI HLE correctness batch, mostly surfaced by LBP and DeferredShading: guest-EA / big-endian out-param marshalling across the HLE surface (cellUserInfo, trophy u64s, cellGame DISC, PSID, cellRtc, save-data paths, cellJpgDec, cellPngDec, cellAudio read index), cellFsSdataOpen with real SDATA/EDAT decryption, cellFsMkdir, a /dev_hdd0 update overlay (PS3_HDD0_ROOT), honest offline NP state + real SDK error codes, real-microsecond sys_time_get_system_time, raw-sys_fs mount-prefix stripping, and HLSL-safe NaN/Inf FP constants.

ydkj -- flow/YDKJ SPURS + SPU task dispatch (31 commits)

The flow/YDKJ SPU line, up to "complete the pending->running task transition (matches the RPCS3 dump)": the CRI taskset task run through the pure SPU interpreter with real context/args (validated against an RPCS3 SPU-LS dump), event-flag init routed to libsre, and the supporting SPURS/vtable-order tracers. Includes cherry-picked sagemono taskset-context + BE cellSpursEventFlag ports.

ppu_lifter jump-table-base fix + diagnostics (2 commits)

discover_jump_tables picked the table-base register for lwzx rD, rIdx, rBase as "first operand whose TOC slot reads back" -- but both operands are often TOC-loaded, so an unrelated lwz rIdx, d(r2) won and the table decoded from the wrong base (flow's app-loop state machine fell through to an unresolved bctr). Now the base is the operand the add ..., rBase feeding mtctr combines with the sign-extended offset. Plus FLOW_FSDBG on the lv2 read path and a Mystery/guest-callstack hook in tty_write.

Conflict resolutions worth a look

  • runtime/syscalls/sys_fs.c -- flow's USRDIR junction-bypass shortcut kept as top priority, falling through to sagemono's mount-prefix stripping (flow's failing paths carry USRDIR/; LBP's raw-sys_fs disc paths don't, so both work).
  • runtime/ppu/ppu_fs.cpp -- kept both the /dev_hdd0 update overlay (sagemono) and the /dev_flash firmware serving (ydkj); different mounts.
  • libs/system/cellSysutil.c -- took ydkj's full CellSysutilBgmPlaybackStatus struct write over sagemono's single-u32 (real API shape; both agree the arg is a guest EA).

Contributor credit

CONTRIBUTORS.md updated with a new sagemono subsection for the HLE correctness batch. -- @sagemono

Held back: sagemono/spu/faithful-adopt-caner (150 commits)

Merges with 72 conflict hunks across 23 files -- 19 in tools/ppu_lifter.py alone, plus cellSpurs.c, ppu_sysprx.cpp, ppu_loader.cpp and the SPU runtime. It re-adopts caner's decode/lift work and brings LBP SPU/SPURS bring-up + RSX texture support (G8B8/DXT, deswizzle). Correctness-critical lifter/SPU-core territory that wants careful resolution + a build/re-lift/run check on both flow and LBP, not a blind fold. Proposed as its own follow-up integration. -- @sagemono, adopting @canersaka's decode work.

🤖 Generated with Claude Code

claude and others added 30 commits July 17, 2026 02:10
Findings from a YDKJ FMOD bring-up session (golden-trace driven):
- cellAvconfExt: cellAudioOutGetSoundAvailability reported stereo only; a real
  PS3 reports LPCM 2/5.1/7.1 + DD/DTS 5.1 (RPCS3 golden log). FMOD's init
  branches on ==8/==6 and fell through to DTS/AC3 probes that returned 0.
- cellAudio: CellAudioPortConfig marshalled with readIndexAddr as u64, shifting
  every field; FMOD reads nChannel at +0x08 and requires 8.
- ppu_fs: /dev_flash is FIRMWARE, not game data -- it was mapped into the game
  root so every firmware lookup missed (FMOD needs
  /dev_flash/sys/external/flashMP3.pic). Map to $PS3_DEV_FLASH.
- cellSysutil: cellSysutilGetBgmPlaybackStatus dereferenced a GUEST EA as a HOST
  pointer (crash) and used the wrong shape (it fills a 0x28 struct).
- tools/ppu_lifter.py: discover_jump_tables threw on the first dispatcher
  ("can only concatenate list (not int) to list", then NameError base_is_ld) and
  the caller's except silently disabled jump-table discovery for the WHOLE
  binary. Repaired + emit tail-entry wrappers for code ptrs found in DATA.
- cellPad: cache getenv; gate injection on wall-clock (still broken, see notes).

NOTE: the ppu_lifter jump-table breakage is a MERGE REGRESSION in
integrate/all (PR #76) -- sagemono/dev/cellmark still has the correct
disp/r_base/base_is_ld code. Prefer his version over this repair.
LBP spun forever printing its own diagnostic:

    ERROR - LIBAUDIO DROPOUT - LIBAUDIO POSITION NO LONGER INCREMENTING!!

3387 of 3792 log lines. Its watchdog (sub_48411C) says exactly what it wants:

    lwz  r11, 0x21C(r3)    ; readIndexAddr, held as a 32-bit pointer
    ld   r0, 0(r11)        ; 64-bit load from it
    cmpw cr7, r9, r0       ; 32-bit compare -> LOW word only
    beq  cr7, ...          ; unchanged -> strike; >8 strikes -> dropout

Two bugs, both ours:

1. CellAudioPortConfig was marshalled as though every address field were 64
   bits. sys_addr_t is uintptr_t, which on the 32-bit PPU ABI is FOUR bytes,
   so writing readIndexAddr as a u64 clobbered status and shifted every field
   after it. The game read readIndexAddr as a u32 at +0 and got the high --
   zero -- half of that write, then `ld r0, 0(0)`. The VM demand-commits and
   returns zeros for a wild pointer rather than faulting, so this read quietly
   forever instead of crashing. Correct layout:
       readIndexAddr u32@0, status u32@4, nChannel u64@8,
       nBlock u64@16, portSize u32@24, portAddr u32@28   -> 32 bytes

2. The counter itself is a 64-bit big-endian value; `ld` + `cmpw` means the
   live bits are bytes [4..7]. Publishing it with vm_write32 only ever wrote
   bytes [0..3] -- the half the game never looks at. Use vm_write64, and widen
   the PortOpen initialiser to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dropped

Our cellSpursCreateTask was declared with six parameters (taskset, taskId, elf,
context, sizeContext, attr). The real SDK ABI has seven (verified against RPCS3
cellSpurs.cpp:370):

    cellSpursCreateTask(taskset, taskId, elf, context, sizeContext,
                        CellSpursTaskLsPattern *lsPattern,
                        CellSpursTaskArgument  *argument)

r9 is the 16-byte task ARGUMENT -- a guest EA the generic HLE adapter already
forwards (it passes r3..r10). Stopping at six params treated r8 as an opaque
`attr` and never read r9, so the argument was silently dropped and the task's
TaskInfo.args stayed zero.

That argument is the task's work-descriptor pointer. LBP's audio SPURS task
(spu_0003 main at LS 0x17e70) computes its first DMA source EA straight out of
r3 -- `wrch $ch18, f(r3.word3)` -- so a zero argument makes it GET-DMA from
EA 0 and get nothing. Read the 16-byte argument (and LS pattern) from guest
memory and pass them to spurs_taskset_add_task, which already accepted them but
was being handed NULL.

With this, the task receives its real argument (e.g. r3.word3 = 0x0094F700, a
valid game-data EA) and DMAs its actual work descriptor instead of address 0 --
confirmed via a taskset run: image 6 now issues real GETs from 0x0094F700 and
the surrounding descriptor region. (Completing the task -- its remaining loop,
and the SPU->PPU event-flag signal -- is separate follow-up work.)

Default boot unaffected: the argument is stored into the taskset but only read
when the taskset context is built, which is off by default. Verified 145k log
lines, 0 crashes. cellSpursCreateTaskWithAttribute passes 0 for now (our
attribute struct does not yet model the argument; LBP uses the 7-arg form).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gated)

The SPURS PM cycle works, but SPURS *tasks* (cellSpursCreateTask) were dispatched
as bare ELFs with r3=r4=0 and no taskset context, so they spun immediately. The
real kernel runs a task UNDER the taskset policy, which plants a
SpursTasksetContext at LS 0x2700 (taskset header, TaskInfo, syscallAddr=0xA70)
and enters with r3 = the task argument and r4 = {spurs, taskset args} -- verified
against RPCS3 spursTasksetStartTask (cellSpursSpu.cpp:1395).

That machinery already existed for the cri image-22 path. Generalize it:

  - spu_lifted_job.h keys the r4 (and now r3) taskset ABI off a sentinel --
    LS[0x27C4] == 0xA70, which only spurs_pm_build_context ever writes -- instead
    of `image_id == 22`. flOw jobs never plant it, so their register setup is
    byte-identical. For a taskset task r3 is now the 16-byte TaskInfo argument
    (LS 0x2780), not the cri 0x40 marker.
  - spu_workload.c builds the context for any non-cri task when the taskset EA
    is known (recorded by cellSpursCreateTask).

Gated behind LBP_TASKSET while the last gap is worked; default boot is untouched
and verified (207k log lines, 0 crashes, no taskset activity with the flag off).

With this plus the 7-arg CreateTask fix (aa4754d), LBP's audio task goes from
"spin at r3=0" to running its real code and DMAing its actual work descriptor
from the game EA in its argument. It does not yet COMPLETE: its work-array base
pointer lives in a heap buffer (arg[1], e.g. 0x45C7A3A4) that LBP leaves all
zeros in our run, so it iterates near-null EAs in a loop and never reaches the
task-API syscall that would set event flag 0x0100. That empty audio buffer is
the next gap -- an LBP audio-engine population step, downstream of SPURS.

Also adds reusable diagnostics: YDKJ_DMA_IMG=N filters the DMA trace to one
image uncapped; LBP_TASKSET_TRACE dumps task r3/r4 + the argument's pointed
regions; and cellSpurs event-flag logs now carry the flag EA for correlation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tch race

Two real fixes plus diagnostics, all on the LBP audio-taskset path (gated behind
LBP_TASKSET; default boot untouched):

1. The LS 0xA70 task-API syscall intercept (EXIT/YIELD/WAIT/POLL) was hard-gated
   to image_id == 22 (the cri task). Any other SPURS task that reached its
   EXIT/YIELD syscall would branch into empty LS 0xA70 and halt as a bogus
   "branch-to-0". Fire it for any task whose SpursTasksetContext we planted,
   detected by the syscallAddr sentinel at LS 0x27C4 (== 0xA70), which only
   spurs_pm_build_context writes -- so non-taskset images are unaffected. The
   existing num==0 -> halt semantics are already correct for non-cri tasks.

2. cellSpursCreateTask records the taskset+taskId in single-slot globals
   (g_ydkj_real_taskset_ea/taskid). The async task thread read them LATER, so the
   second CreateTask clobbered them before the first task ran -- both LBP audio
   tasks ended up running task 1's descriptor. Capture them per-job into
   spu_async_job at dispatch time (PPU thread, race-free), like j->r3.

Diagnostics (LBP_TASKSET_TRACE): [heapobj] dumps the FMOD audio object so its
stream count (+0xC8) and buffer-pointer region (+0x124) are visible; LBP_TASK_DELAY
defers the task run to A/B timing-vs-fill-gap.

Findings these produced (see memory lbp-bringup): the blocking SPU task is FMOD's
SPU DSP mixer. Its output buffer wires correctly to cellAudioPortOpen (0x01000000),
but its internal DSP buffers (a1[140]/a1[141], 7664 & 15136 B) are null in our run
because FMOD's PPU init never populates them -- so the task DMAs from EA 0 and
loops, never reaching its 0xA70 syscall (confirmed 0 syscalls with the intercept
now ungated). That FMOD-init buffer allocation is the remaining gap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The conflict resolution took sagemono's EventFlagWait body (honest stall
report + opt-in SPURS_EF_FORCE) but kept our old 'timeouts' declaration,
leaving 'waits'/'s_force' undeclared. Take his decls to match the body.
…oundLocalError)

The TOC-candidate harvesting lives inside 'if not args.raw:', so raw/PRX
lifts crashed at 'if len(toc_candidates)==1'. Initialize it empty alongside
jt_dispatchers/data_code_targets; the ELF path still fills it. Unblocks the
libsre re-lift.
The _CS_SAVE/REST/ANY_STORE regexes matched only a plain 'ctx->gpr[1] + off'
stack base, but loads/stores now lower the ra=0 form as '((1) ? ctx->gpr[1] :
0) + off'. So the regexes matched nothing and the entire _cs entry-capture
pass silently no-op'd (600 -> 0 captures on a libsre re-lift), regressing the
s5m fix: reused callee-save slots were reloaded as stale/corrupt values.
Accept both base forms via a shared _R1 sub-pattern.
d-form ra=0 means a literal 0 base. It was lowered as a runtime
'((N) ? ctx->gpr[N] : 0)' ternary, but N is a lift-time constant -- emit plain
'ctx->gpr[N]' (N!=0) or '0' (ra=0) via _mem_base(). Equivalent, cleaner, and
restores the bare 'ctx->gpr[1]' stack-base form that the callee-save and TOC
pattern-matching expect (the ternary silently defeated them).
def73da)

Ports the PPU side of sagemono/dev-cellmark def73da into the ydkj tree:
the cellSpursEventFlag family rewritten on the REAL big-endian guest layout
(events@+0x00 be u16, SPU wait-slot tables, clearMode@+0x0F) so the SPU side
that DMAs this exact struct can see PPU Set/Wait/Clear -- the old native-endian
host-condvar struct was invisible to tasks. Adds spu_taskset_signal_task /
spu_taskset_wait_signal (guest CSTS_SIGNALLED bitset park/wake) and wires the
WAIT_SIGNAL(2) taskset syscall to actually park instead of returning success.

Effect (with the EventFlag NIDs forced to HLE): the FMOD taskset's
EventFlagInitialize/Attach now SUCCEED (no STAT/INVAL, no 'SPURS is aborted');
the frontier moves forward to cellSpursCreateTask. Rendering unaffected
(flips ~950, draws 32). SPU-side jump-table discovery + FMOD re-lift still TODO.

Ported from sagemono <https://github.com/sagemono/ps3recomp> def73da.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports sagemono/dev-cellmark 35c2767 + 1e6b343 (spurs_pm part) + 5bd3421:
 - 35c2767: plant the SpursTasksetContext at LS 0x2700 for every generic
   taskset task by DEFAULT (was opt-in LBP_TASKSET). Without it a dispatched
   task DMAs from EA 0 and the PPU pump blocks forever.
 - 1e6b343: stamp the SPURS KERNEL context (LS 0x1C0 spurs ptr, 0x1C8 spuNum,
   0x1CC dmaTagId, 0x1E4 moduleId 'TK'). The SPU task library's fast-path
   wait validation checks moduleId; without it every task-API wait fails
   ERROR_STAT 0x80410909.
 - 5bd3421: full-coverage (all-ones) LS pattern in the TaskInfo -- the task
   library refuses a blocking wait whose pattern doesn't cover the task stack
   (0x8041090F). We run each task in its own 256KB LS so all-ones is accurate.

Effect, with the SPURS taskset/task NIDs forced to HLE
(YDKJ_FORCE_HLE=5EF96465,87630976,B8474EFF,22AAB31D,BEB600AC,1D46FEDF):
FMOD FULLY INITIALIZES -- all 5 golden FMOD threads spawn, no FMOD error,
and all 7 menu .fsb sound banks load (asset-parity with the RPCS3 golden).
The taskset context planting also fixes the render-break that forcing
CreateTask to HLE previously caused (flips 0 -> 8000+). Menu state-transition
(Scaleform) is a separate, still-open gate.

Ported from sagemono <https://github.com/sagemono/ps3recomp>.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ssert)

Session findings wired as env-gated probes/fixes:
- TUNERFIX (ppu_hle.cpp): sysPrxForUser 0xE0998DBF -> 0x8001112E so libsre
  _cellSpursIsLaunchedFromTuner reports "profiler not loaded" instead of
  asserting (usertrace.c:123) + wrongly reporting launched-from-tuner. Real
  fix; the assert is gone but the task-attach STAT (0x8041090F) is separate.
- SPURSTRACE-derived: real cri taskset=0x4000C900, spurs=0x40009F00; the
  attach fails the taskset+0xC==0xFF workload-state check in libsre 0x300158C4.
- Diagnostics: YDKJ_TSFIND (attr->taskset memmove watch), YDKJ_ASSERTBT
  (assert back-chain), YDKJ_CRI_DUMP (taskset/instance struct dump), the
  libsre_func_30014DC4 CreateTaskset capture, YDKJ_USMRD.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	runtime/ppu/ppu_hle.cpp
#	tools/ppu_lifter.py
YDKJ_CRI_INTERP: dispatch image 22 via spu_interp_run on the prepared LS
(SpursTasksetContext planted at 0x2700), same SPURS task r3 ABI as the lifted
path. Merged SPU-interpreter branch (e92d159) provides the engine. The
interpreter runs the task, but it halts at entry with r3=0 -- the YDKJ_SPUTASK
dispatch doesn't carry a real SPURS r3 and the taskset (0x4000C900) attach
fails the workload-state check (STAT), so there's no populated decode-job
context. Infrastructure is in place; the context/attach blocker is upstream.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SPURSTRACE now dumps the task-descriptor state at the failing attach calls
(nid 0x87630976 / 0x22AAB31D). Findings:
- task-create 0x300158C4 STATs (0x8041090F) because struct(r3=0x005C1200)+0xC
  != 0xFF (it's 0x00); the taskset is r4=0x4000C900 (distinct).
- 0x30015AA4 (0x22AAB31D) atomically stamps +0xC=0xFF (preconds +4==0 && +7==0
  are met, value-verified CAS is in), THEN makes an lv2 event syscall that
  errors -> returns 0x80410902.
- The attach uses real event syscalls (128/129/130/134 = queue create/destroy/
  receive, port create). One returns an error the SPURS repacks as STAT, so the
  task-attach aborts even though the stamp succeeded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lift

Full struct + taskset dump at the failing task-attach proves the taskset
0x4000C900 is ENTIRELY ZEROED after cellSpursCreateTasksetWithAttribute
(libsre 0x30014CF8) returns success -- header +0x00=0, task-slot bitmap
+0xda8=0. The initializer 0x30014A58 (r3=taskset, 0x2f0 frame, saves r21-r31,
real work at 0x30014B08) is FRAGMENTED in func0.minimal into separate seeds
(0x30014AAC end 0x30014B08, 0x30014B08 end 0x30014CF8), so the work at
0x30014B08 runs as its own function WITHOUT the prologue frame -> its stores
land wrong -> taskset never initialized -> the task-attach slot-allocation
finds a zeroed bitmap -> STAT/0x80410902 -> no workload -> no cri decode.

Fix (next): make 0x30014A58 one contiguous function [0x30014A58,0x30014CF8)
in func0 and re-lift libsre.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
libsre's func0 fragments many functions that use the guard->`bne work`->
work-branches-back-to-shared-epilogue pattern (func0.json splits each at its
interior branch targets). The fragments then lift as separate functions that
run without the parent's prologue frame -> stores land wrong -> e.g. the
SPURS taskset init (0x30014A58) and its sub-call 0x3000A000 produce a zeroed
taskset -> task-attach STATs -> no cri decode.

defrag_func0.py: entries = bl-targets + frame prologues; merge a seed into its
predecessor only if the predecessor's CFG actually reaches it AND it is not a
bl-target (hard boundary). 552 -> 344 functions, 0 overlaps.

Result after re-lift: 0x30014A58/0x3000A000/0x30014CF8 now contiguous;
0x22AAB31D task-slot alloc returns 0 (0x80410902 error GONE, was x4). Taskset
bitmap still 0 + 0x87630976 STAT remain -> deeper layers (bitmap init /
0x87630976 ordering). Kept: correct systemic fix. ps3_hle_call still 33.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sksets

SPURSTRACE now logs caller lr. After the defrag re-lift:
- create-task 0x87630976 STATs from TWO callers: lr=0x002E0958 (taskset
  0x4000C900 = audio/FMOD, reused for .fsb paths) and lr=0x00331F08 (taskset
  0x40131000 = cri movie, func_0033xxxx cri chain).
- attr-init 0x22AAB31D runs from func_002DF5xx (4-iter loop), AFTER the create.
- The task-attr struct (0x005C1200 / 0x006B4600) is only partially inited:
  +0xE=01 (type) set, but +0xC not stamped to 0xFF -> create-task STATs.

So the remaining blocker is game-side: create-task runs before its attr is
fully initialized (missing/mis-ordered cellSpursTaskAttribute2Initialize),
distinct from the libsre fragmentation the defrag fixed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…locker

Forcing task-attr struct+0xC=0xFF before create-task (nid 0x87630976) makes
BOTH create calls succeed (STAT 0x8041090F gone) -- but the game then hangs
(0 flips): the task is created with an under-initialized attr and breaks the
SPURS/render flow. So the +0xC check isn't the issue to paper over; the attr
genuinely needs full init, which means the attr-init (0x22AAB31D from
func_002DF320) must run BEFORE create (func_002E0510). Both are indirect-
dispatched (vtable/state-machine) with 0 direct callers -> the blocker is a
game-side C++ object-lifecycle / vtable ordering mis-lift, distinct from the
libsre fragmentation the defrag pass fixed. Env-gated diagnostic only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l-call dispatch

YDKJ_VTORDER hooks ps3_indirect_call for the SPURS create/init methods. The
init (func_002DF320) is reached via bctr (tail-call), so ctx->lr carries a
stale value (0x2E609C, from an earlier `bl 0x2de860` whose target is just a
wrapper) rather than the real dispatcher. The guest back-chain walk also comes
up empty (SPURS thread, non-standard frame). So lr-based tracing can't pin the
C++ virtual dispatcher that runs create-before-init; a ctr-target + reliable
caller mechanism (or C++ object-state analysis) is needed. Env-gated diag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…SPURS dispatch

ppu_guest_callstack(tag): recover the guest call chain from the HOST stack
(RtlCaptureStackBackTrace -> function_table). Each lifted func_X(ctx) is a real
host frame and an indirect dispatch runs nested (dispatcher -> ps3_indirect_call
-> target), so the host backtrace reliably names guest callers -- unlike the
guest-sp back-chain that breaks on SPURS threads. Paired with the raw-stack scan
in ppu_dump_guest_stack for tail-call coverage.

Wired into YDKJ_VTORDER (indirect dispatch of the create/init methods) and the
SPURSTRACE create/init NID sites. Result: the create/init full guest chains
converge on the common dispatcher func_002B6B5C, whose control flow is:
  lbz r0,0x11(r3); bne (skip) -> bl 0x2b4200 (INIT, guarded by r3[0x11]==0)
  ... lwz r11,0xb4(r27); lwz r0,0(r11); mtctr; bctrl (vtable CREATE)
So the task-attr init is gated by an object state byte r3[0x11]; when it's wrong
the descriptor is never inited (+0xC stays 0x00) and create-task STATs. Root =
a corrupted/mis-lifted C++ object state field (ydkj-lift-corruption class), the
game-side layer beneath the (fixed) libsre fragmentation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-scoped)

YDKJ_GUESTINIT: the create-task (nid 0x87630976) runs before the game inits its
task descriptor, so descriptor+0xC != 0xFF -> STAT. Eagerly invoke the REAL init
(0x22AAB31D) on the descriptor via ppu_guest_call (scratch ctx preserves the
create's args) -- full init incl. the task-slot syscall, unlike the raw +0xC
force that hung.

Result: create-task RETURNS 0 (STAT gone). But forcing BOTH creates to succeed
hangs the game at timer_usleep (0 flips) -- the create-STAT was a *tolerated*
degradation (game fell back to rendering the legal screen), and success makes it
wait for a task the SPU side never runs. YDKJ_GUESTINIT_CRI scopes it to the cri
create (taskset r4=0x40131000) so audio keeps its tolerated STAT and the legal
screen still renders (633 flips) while the cri create succeeds. Movie STILL
doesn't decode: image 22 interp halts at entry (r3=0), taskset context elf=0x0
-> the created task's SPU context is never populated. So the create gate is now
passable; the SPU-side task execution/context is the remaining wall (needs the
working SpursTasksetContext, e.g. from the RPCS3 memory dumper's SPU LS files).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…unc_002DF518)

Point the guest-call-stack tracer at the actual dispatched methods so it names
the common dispatcher func_002B6B5C. Env-gated (YDKJ_VTORDER) diagnostic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…stub)

The cri movie task never got created: create-precheck 0x87630976 STATs, and when
forced past it, the real task-add libsre_func_30012310 (0x1D46FEDF, r6=movie ELF
0x004F5F80) STAT'd 0x80410902 because *(u32*)(attr+0)!=1. Dumping the attribute
(YDKJ_SPURSTRACE) showed it built LITTLE-ENDIAN with the wrong layout: our HLE
_cellSpursTaskAttributeInitialize (cellSpurs.c) native-writes attr->eaElf etc.
(LE) at our struct offsets, but real libsre reads it big-endian at its own
offsets. NID 0xB8474EFF IS exported by libsre (0x300315DC) -- it was only hitting
the HLE stub because it was pinned in the baseline YDKJ_FORCE_HLE list.

Drop B8474EFF from FORCE_HLE (baseline is now YDKJ_FORCE_HLE=5EF96465 only) and
the attr-init routes to real libsre: task-add returns 0 and the taskset is
populated with the movie ELF (task0/1 elf=0x4F5F80 + valid args/ctx) -- the elf=0
that blocked the SPU dispatch is gone. Verified safe: without forcing create,
this is a no-op (820 flips, no regression).

Diagnostics added (all env-gated, YDKJ_SPURSTRACE/YDKJ_CRI_CREATE_OK): TSDUMP
scans the taskset TaskInfo after create; CRI_ATTR dumps the attribute at the
task-add; CRI_CREATE_OK forces only create's return (leaving the descriptor
pristine) so the game proceeds to the real ELF-carrying init; attr-init logs its
NID. Remaining is downstream SPU-side scheduling (mark-ready + dispatch timing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…actually interpret

Chain of fixes so the image-22 cri SPU dispatch runs the real decode task now that
the LLE task-add populates the taskset (prior commit):

- CRI_WAIT: the SPU workload is dispatched ~150 log lines before the PPU task-add
  writes TaskInfo[].elf, so build_context read elf=0. Poll (detached thread, ~6s)
  for TaskInfo[taskid].elf != 0 first -> context now built with elf=0x4F5F80.
- r3 from TaskInfo: feed the task's real CellSpursTaskArgument (TI_ARGS) as the
  entry r3 instead of the stale dispatch-time j->r3 (was all-zero).
- CRI_READY: the task-add leaves tasks PENDING_READY+ENABLED but not READY;
  promote pending->ready (the kernel's scheduling pass) so the policy sees a
  runnable task.
- image_id = -1 for the interp: THE key bug. spu_interp_run rejoins the compiled
  fast path the instant it finds a lifted function at the PC (image_id>=0), so
  with image_id=22 it returned at the entry (0x3050) without interpreting one
  instruction. Pure interpretation (image_id<0) is the whole point here.

Result: image 22 now genuinely executes (halts at LSA=0x0, not stuck at entry) --
it runs real code and hits the "branch-to-0" through the SPURS task-API jump table
at LS 0x2700 that build_context doesn't fully populate (the taskset policy module
isn't resident in LS). That's the remaining wall: the in-LS policy/task-API table,
the exact SPU LS state the RPCS3 memory dumper captures. All env-gated
(YDKJ_CRI_INTERP + YDKJ_CRI_TS); diagnostics print entry code + taskset bitsets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(not branch-to-0)

Policy-module load, take 2: rework YDKJ_CRI_TASKSET to (a) wait for the LLE
task-add to populate the taskset + promote pending->ready, then (b) INTERPRET the
policy entry (LS 0xA00) instead of running it lifted -- the policy has the same
computed-branch/jump-table code the static lift can't resolve. Kernel->policy
handoff registers (r80=0x100, r3=0x2700) are best-guess pending the RPCS3 LS dump.

Added YDKJ_SPU_TRACE=N: ring-buffer of the last 64 PCs, dumped on interp halt.
This corrected a misread: spu_interp_run returns the STOP_CODE, not the PC, so the
CRI_INTERP report's "LSA=0x0" was stop_code=0 (a `stop 0`), NOT a branch to null.
The trace shows the cri task actually EXECUTES 323 real instructions (linearly
through 0x26EF4..0x26FE4) and hits `stop 0` at pc=0x270EC -- the SPU interpreter
genuinely runs the decode code. It does NO DMA/channel/task-API op first, so it
takes an early exit/error path: its context/args still aren't sufficient to reach
real decode work (video_dma=0). Fixed the CRI_INTERP printf to report the real
halt PC + step count.

Remaining wall is the task's decode context/work protocol -- the exact in-LS state
the RPCS3 memory dumper captures. All env-gated (YDKJ_CRI_INTERP/CRI_TASKSET/
SPU_TRACE).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cellPngDec.c was written but never exercised -- no prior sample decoded a
PNG. DeferredShading (skin.png + font_tex.png) is the first, and it faulted
in cellPngDecCreate writing the output handle to host address 0x8EF88: the
HLE bridge forwards raw gpr values, so a `CellPngDecMainHandle*` arrives as a
big-endian GUEST address, not a host pointer. The whole file assumed
host-native little-endian struct access.

Three ABI bugs, all against the real SDK <cell/codec/pngdec.h>:
 - No translation/byte-swap. Every pointer param is now a u32 guest address;
   struct fields are marshalled with vm_read32/vm_write32/vm_read64 at
   explicit big-endian offsets rather than overlaying a host struct.
 - CellPngDecSrc layout was wrong: the repo header modelled fileName as an
   inline char[1024], but the SDK has it as a char* at +4 -- which mis-placed
   fileOffset/streamPtr/streamSize by ~1KB. fileName is now read as a guest
   string; the working copy is host-side only.
 - cellPngDecDecodeData had the wrong arity: the real SDK takes 5 args with
   dataCtrlParam (carrying outputBytesPerLine, the destination pitch) BEFORE
   dataOutInfo. The 4-arg version read dataOutInfo from what was actually the
   pitch struct. DecodeData now honours the guest pitch and writes decoded
   pixels row-by-row into the guest output buffer via vm_base.

Result: both DeferredShading textures decode + upload correctly
(256x256 ARGB, verified in-log). No other port calls cellPngDec, so no
regression surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The FP decompiler formatted embedded constants with %g, which prints the
bare tokens "nan"/"inf" for non-finite values. HLSL rejects those (X3004
undeclared identifier 'nan'), so any fragment program with a NaN/Inf
constant failed to compile and its pass rendered black -- DeferredShading's
light passes embed one. Reproduce non-finite lanes bit-exactly via
asfloat(0xBITS); finite values keep %g.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ooks

Two coupled VFS fixes that unblock LittleBigPlanet's boot (it exited(0) at a
bringup stage that polls cellFsStat('/dev_hdd0/game/<title>/USRDIR/') ~15ms and
requires the dir to exist):

- runtime/ppu/ppu_fs.cpp: cellFsMkdir was a no-op stub (returned CELL_OK,
  created nothing) -> games that create then poll a directory silently stalled.
  Implement it as a recursive host mkdir. Add env-gated PS3_FSLOG tracing to
  stat/opendir/mkdir.

- libs/system/cellGame.c: cellGameContentPermit created the game-data dir only
  at ./gamedata/dev_hdd0/game/... , but ppu_fs.cpp host_path maps
  /dev_hdd0/game/<title>/ to $ppu_vfs_root/game/<title>/ -- a different host
  location cellFs never reads, so cellFsStat always returned ENOENT. Also create
  the dir at the VFS-mapped location so the two agree.

LBP now boots from the early exit(0) all the way to D3D12 window-open + network
init (148 -> 399 log lines) before a separate cellHttp crash. The 0x95BF60 job
spin was a red herring (the stage gates on the dir, not the job).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cellHttpCreateClient wrote *clientId directly, but the generic HLE adapter
passes r3 as a raw guest effective address -> writing through it faulted on a
host pointer (host AV at 0x47100FA0 during LBP boot). Translate via vm_base
(GUEST_PTR), matching the other libs. LBP now boots past network init into its
main engine loading threads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sagemono and others added 23 commits July 24, 2026 08:11
…ee as cellFs

sys_fs_translate_path mapped /dev_bdvd/X -> <root>/dev_bdvd/X, a directory that
doesn't exist, while the cellFs layer (ppu_fs.cpp) strips the mount prefix to
<root>/X. A title that opens disc content through the raw sys_fs path then failed
even when the file is present -- LBP's Bink videos (gamedata/videos/
localisation_test.bik etc.) went through sys_fs and returned ENOENT. Strip the
known mount prefixes (/dev_bdvd, /app_home, /dev_hdd0, /dev_hdd1, /dev_flash,
/host_root, /dev_usb*) up front, matching ppu_fs; keep the non-leading app_home
spelling + extracted-dump fallback. Video opens now succeed (0 sys_fs ENOENT).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cellNetCtlGetState claimed IPObtained unconditionally, so titles took their
online boot path and waited on PSN jobs that can never complete. Offline (the
default) now FAILS the query with NOT_INITIALIZED and reports the link down,
matching a console with no connection -- and LBP's working RPCS3 boot, where
exactly one failing GetState sends the game down its offline path. Returning
OK+Disconnected is not enough: LBP's connect job (sub_F433C) polls GetState
until IPObtained and only exits on a negative return. PS3_NET_ONLINE=1
restores the old fake-connected behaviour for online experiments.

sceNp.h carried invented 0x8055xxxx error codes; replace them with the
official SDK values (0x8002aa01..). Games compare exact codes: LBP
special-cases NOT_INITIALIZED (0x8002aa01) from sceNpManagerGetStatus as
"NP not up -> treat as offline" and cleanly skips NP init.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LBP spun forever printing its own diagnostic:

    ERROR - LIBAUDIO DROPOUT - LIBAUDIO POSITION NO LONGER INCREMENTING!!

3387 of 3792 log lines. Its watchdog (sub_48411C) says exactly what it wants:

    lwz  r11, 0x21C(r3)    ; readIndexAddr, held as a 32-bit pointer
    ld   r0, 0(r11)        ; 64-bit load from it
    cmpw cr7, r9, r0       ; 32-bit compare -> LOW word only
    beq  cr7, ...          ; unchanged -> strike; >8 strikes -> dropout

Two bugs, both ours:

1. CellAudioPortConfig was marshalled as though every address field were 64
   bits. sys_addr_t is uintptr_t, which on the 32-bit PPU ABI is FOUR bytes,
   so writing readIndexAddr as a u64 clobbered status and shifted every field
   after it. The game read readIndexAddr as a u32 at +0 and got the high --
   zero -- half of that write, then `ld r0, 0(0)`. The VM demand-commits and
   returns zeros for a wild pointer rather than faulting, so this read quietly
   forever instead of crashing. Correct layout:
       readIndexAddr u32@0, status u32@4, nChannel u64@8,
       nBlock u64@16, portSize u32@24, portAddr u32@28   -> 32 bytes

2. The counter itself is a 64-bit big-endian value; `ld` + `cmpw` means the
   live bits are bytes [4..7]. Publishing it with vm_write32 only ever wrote
   bytes [0..3] -- the half the game never looks at. Use vm_write64, and widen
   the PortOpen initialiser to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ointer

The decode output went straight into `data` and the status into `dataInfo`,
both of which arrive as raw guest effective addresses (the generic HLE
dispatcher passes GPRs through untranslated). Dereferencing them as host
pointers writes decoded pixels to an arbitrary host address -- a crash or silent
corruption -- while the guest's real output buffer never receives the image. The
input side already translated (`sub->src_data = vm_base + guest_addr`); only the
output was left raw.

Translate both through vm_base / vm_write32, mirroring cellPngDecDecodeData
(the one file in libs/codec that follows the convention correctly). cellJpgDec
is imported by LBP (7 NIDs).

Verified: no boot regression across 4 runs (104k-119k log lines, 0 crashes).
Correct-by-construction against the cellPngDec reference; not positively
exercised because LBP does not reach a JPEG decode during the observable boot
window. The sibling raw-pointer functions in this file (Create/Open/ReadHeader/
SetParameter) share the pattern and are left for a codec-wide pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cellKbGetInfo/cellKbGetData and cellMouseGetData/cellMouseGetDataList wrote
their result structs through the raw argument pointer -- but HLE struct args
are guest EAs, so the memset/field stores dereferenced a guest address as
host memory. LBP crashed the moment its frontend polled the keyboard
(cellKbGetInfo memset at guest-stack EA 0x0FEFF814 -> host AV), which was
the very first thing it did after surviving the loading phase.

Write through the vm accessors big-endian instead (the cellPad.c idiom,
same as the earlier cellMouseGetInfo fix), with the SDK's guest layouts:
CellKbInfo status[] is BYTES at +0xC (not u32s), CellKbData is
led/mkey/len/keycode[62] at 0/4/8/0xC, CellMouseData is 6 used bytes + 2
pad, CellMouseDataList is u32 count + 8 entries. The NO_DEVICE paths now
zero the guest struct instead of memsetting host memory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…counter

The HLE was a static counter advancing 1 ms PER CALL ("so callers see time
progress") -- a boot-era stopgap that made every guest clock built on it run
at call-rate instead of wall-time. LBP's Bink movie clock (sysGetSystemTime,
liblv2 NID 0x8461E528, resolves here via the computed-NID registration) paced
the whole intro off it: video decoded at <1 fps with ~13 IO submissions per
run, the movie audio preload threshold effectively never filled (the port was
opened and configured but cellAudioPortStart never fired -> silence), and
frontend animations timed off the same clock crawled.

Return real monotonic microseconds (QPC on Windows, CLOCK_MONOTONIC
elsewhere).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sys_spu_segment.src is a u32 effective address at offset +0x10 -- the guest
reads read32(seg+0x10) and uses it directly as the DMA source EA. We were
writing it as if it were a big-endian u64 (hi=0 at +0x10, lo=addr at +0x14),
so the guest read the zero high word: every runtime-imported SPU image DMA'd
from address 0.

Found via LBP's FMOD SPU mixer, which materializes its DSP-plugin overlays
with sys_spu_image_import and then DMAs each into LS from seg[+0x10]; with the
address in the wrong word the overlays loaded as zeros, and the mixer branched
into empty LS (unresolved indirect branch, loading hang after intro-skip).
Writing the address at +0x10 (and keeping +0x14 = addr, harmless for any
u64-low reader) makes the overlay bytes land correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cellFsSdataOpen (0xB1840B53) was unresolved and silently faked (default
CELL_OK, no handle). LBP stores its SPU job code modules -- the physics/mesh
jobs (VERLET, SKIN, SPRING, SQUISH) -- zlib-compressed inside patch.sdat and
data.sdat (NPD/SDATA containers), opened via this call. Faking it meant the
containers were never read, zero job modules ever reached memory, every SPU
job dispatched into an empty code buffer, and the loader deadlocked waiting on
completions that could never come. Confirmed: the module ila-header signatures
inside these files match the resident modules in an RPCS3 save state exactly.

Implement it for real:
  - runtime/ppu/sdata_decrypt.h: self-contained AES-128 (ECB + CBC) and the
    SDATA block cipher. Per 0x4000-byte block:
        crypt_key  = dev_hash ^ SDAT_KEY
        key_result = AES_ECB(crypt_key, dev_hash[0:12] || be32(block))
        key_final  = (flags & ENCRYPTED_KEY 0x08) ? AES_CBC(EDAT_KEY_0, key_result)
                                                   : key_result
        plaintext  = AES_CBC(key_final, iv = npd.digest, ciphertext)
    Algorithm and keys ported from RPCS3 Crypto/unedat.cpp; validated by
    decrypting LBP's data.sdat to its plaintext FSHb container.
  - cellFsSdataOpen: open the file; if it's an already-decrypted container
    (hdd0 patch.sdat is plain FSHb) serve it directly; if NPD-encrypted,
    decrypt the whole file in memory and serve the plaintext from a tmpfile.
    Registered by the literal import NID (the friendly-name hash didn't match).

With this the SPU job modules finally load (they never did before). The loader
now proceeds into real job execution and stalls on a separate, downstream
job-completion issue -- but the code is present now, which it never was.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A disc title patched to e.g. v1.30 runs the update's EBOOT and reads its
patchN.farc from /dev_hdd0/game/<title>/ while base data stays on /dev_bdvd
-- the PS3/RPCS3 layout. PS3_HDD0_ROOT names the host dir /dev_hdd0 maps
into (the one containing game/<title>/); both path translators (cellFs
host_path and raw sys_fs_translate_path) honor it ahead of the generic
mount-prefix strip, so both layers hit the same host tree.

Also accept cellFsAllocateFileAreaWithoutZeroFill (LBP's cache warm-up spams
it): host filesystems grow files on write, so no preallocation is needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…user paths

LittleBigPlanet boot progression. Several HLE functions dereferenced their
pointer parameters as host pointers, but the generic adapter passes PPC
registers raw so those are GUEST effective addresses -- each was a host access
violation the moment the title called it.

- cellGameDataCheck: printed dirName (raw EA) and returned ERROR_NOTFOUND for a
  missing game-data dir; translate before use and return CELL_GAME_RET_NONE
  (firmware/RPCS3 semantics) so the title creates its data instead of bailing.
- cellSysCacheMount: marshalled CellSysCacheParam (cacheId in @+0, path out
  @+0x20) instead of strncpy through the raw EA.
- cellSysutilGetBgmPlaybackStatus, cellDiscGameGetBootDiscInfo: vm_write the
  out-params.
- cellSaveData: translate dirName in AutoSave/AutoLoad (+_2) and add the
  User* wrappers (userId after version; the 9th arg is on the guest stack).
- cellRtc: dual host/guest accessors (guest fields are big-endian) for
  GetCurrentTick/Clock*/Time_t/SetTick/GetTick, plus the missing
  cellRtcGetCurrentClock.
- cellSysmodule: the module-ID table was scrambled (a stray SYNC2=0x0E shifted
  everything after FS); rewrite it from the firmware enum, and accept the
  0xF0xx extended range (LBP's NP_TROPHY = 0xF035 was rejected every boot).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eExec DISC, open PSID)

Four more members of the raw-host-write bug class (the guest is big-endian
and HLE pointer args are guest effective addresses):

- cellUserInfo: CellUserInfoUserStat carried a homeDir[128] member the real
  SDK struct doesn't have (homeDir comes from cellUserInfoGetHomeDir), making
  sizeof()=196 while guests reserve ~80 bytes on their STACK: GetStat's
  memset+strncpy smashed the caller's frame, wiping the PPC CR save slot
  (nonvolatile cr2-4) -- in LBP this silently derailed the bringup init
  walker so the Video/RenderTargets/GfxMemPools nodes never ran. Use the real
  68-byte layout, and write all u32 out-params byte-swapped (a count of 1 was
  being read back as 0x01000000).

- sceNpTrophyGetRequiredDiskSpace wrote its u64 host-LE: 1 MB read back as
  2^44, underflowing LBP's save-data free-space accounting (sub_379EC0) into
  a GameData ERROR-B boot bail.

- cellGameExec: all three entry points dereferenced raw guest EAs (host AV
  at 0xD000FDF0-class addresses); cellGameGetBootGameInfo also claimed an
  HDD boot -- report DISC, matching cellGame's disc-content check sessions,
  which unlocks the disc-boot init sublist.

- syscall 872 sys_ss_get_open_psid was unimplemented, leaving the out-param
  as heap garbage that an LBP boot job then read. Fill zeros (RPCS3's
  unconfigured console_psid) and return CELL_OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…CELL_OK

gen_hle_nids.py resolves each module name to libs/**/<name>.c. A name that
matched nothing printed "warning: <lib>.c not found" and carried on -- the worst
available outcome, because every NID that module exports then stays
unregistered, and the dispatcher's unresolved path answers CELL_OK with
untouched out-params. The title runs on fabricated success, and the only clue
scrolls past in the build noise.

The trap: these are PRX names, but the files are named after the LIBRARY.
LBP's list carried six that match no file at all --

    sys_io, sys_fs, sys_net, sceNp2, cellSysutilAvconfExt, cellDiscGame

-- so every NID behind them has been faked from the start. sys_io is the one
that bit: cellMouseInit, cellMouseGetInfo and cellKbInit are all fully
implemented in libs/input/cellMouse.c and cellKb.c, and none were ever
registered. LBP polls the mouse during boot, got CELL_OK and an untouched
CellMouseInfo every time, and wandered off down a path that never reaches SPU
init -- which is why runs kept "nondeterministically" failing to reach SPURS.

Fail loudly instead, listing each unmatched name with the nearest real file.

Registering the mouse then exposed why the module had never worked: it had never
executed. cellMouseGetInfo took a CellMouseInfo* and memset/assigned through it,
dereferencing the raw guest EA as a host pointer -- an instant AV, and exactly
what happened on the first run after registering it. The local struct also
declares u32 vendor_id/product_id/status where hardware has u16/u16/u8, so even
a translated pointer would lay the fields out wrong. Rewritten against the SDK
layout (cell/mouse/mouse_codes.h, CELL_MAX_MICE=127), writing BE fields through
vm_write*.

cellMouseInit also asserted port 0 was always connected. That reports hardware
that is not there: a title is entitled to open a mouse-driven path and wait on
input that can never arrive. Report what actually exists -- ports connect when
the host injects events, and an unplugged port is a normal thing to see.

Verified: NID table 527 -> 546 handlers, nothing lost (the six dead names
contributed nothing, so dropping them changes no registration). The mouse crash
is gone and boot reaches 8,063 log lines, past the 7,445 it managed while
faking. The remaining five dead names are now loud, for a deliberate decision
rather than a silent fake.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… now passes

The cri decode task ran ~323 instructions and exited via `stop 0` because its
in-LS context validator (func_00026E80/F18/FC4, the same checks as libsre's PPU
task-add) rejected our reconstructed SpursTasksetContext with 0x80410911.

Diffed our LS 0x2700/0x2FB0 against a WORKING reference captured with caner's
RPCS3 guest-memory-dumper fork (github.com/canersaka/rpcs3-guest-memory-dumper):
YDKJ BLUS30569, SPU0 "CellSpursKernel0" mid-cri-decode, file offset == LS address.
Mismatched fields the validator reads:
  - moduleId @0x2840 = "SPURSTASK MODULE" (we never wrote it)
  - TI_LS_PATTERN @0x27A0 = 0 (build_context forced all-ones "full coverage";
    the validator does andc(r20,r21) on it, and all-ones forced the error branch)
  - task descriptor @0x2FB0 word0/word1 = 0/0 (we planted 0xFFFFFFFF/0x400)
  - dmaTagId @0x27D0 = 0x1F (we wrote 0)

Applied CRI-SCOPED (image 22 only) in the dispatch after build_context -- putting
them in the shared build_context breaks the audio SPURS task (BGM crash @0x2DF320).

Result: the task no longer hits the validator/error path. It now runs the real
body (0x257xx -> func_00026DE0) for 373 steps and branches to the task-API syscall
trampoline (0xA70) -- i.e. it passed validation and reached the work/yield call.
Next gate: HLE that task-API syscall in the pure interpreter (the lifted path
intercepts 0xA70 via spu_channels.c; the interp does not yet).

Reference dump made possible by caner's dumper fork -- a genuinely good idea.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erpreter

With the RPCS3-dump context fix, the cri task passes validation and reaches its
task-API call: func_00026DE0 reads syscallAddr @0x27C4 (0xA70) and branches there.
The lifted path intercepts 0xA70 via spu_indirect_branch, but the pure interpreter
(image_id<0) executed empty LS at 0xA70 and stopped 0.

Intercept pc==0xA70 in spu_interp_run (gated by the syscallAddr sentinel that only
spurs_pm_build_context writes), call spu_spurs_taskset_syscall (now non-static),
and resume at the task link (r0). Present image_id==22 across the call so the
handler returns on EXIT(num=0) instead of longjmp-ing out of a context the interp
loop isn't wrapped for.

Result: the task runs its body, makes the task-syscall (num=0, resumes at 0x26E18)
and continues past it instead of stopping. Still no decode (video_dma=0) -- it now
loops post-syscall; next is to find that loop and wire the producer-side stream.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A task stuck in an infinite work/wait loop never halts, so the trace ring (which
dumps on halt) never fires. YDKJ_SPU_STEPCAP=N forces a one-shot ring dump after N
steps -- used to locate the cri task's post-syscall wait-for-work spin
(0x26E18 -> func_00026E80 -> 0x26E18).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sses naturally

The cri create-task pre-check (libsre 0x300158C4) STATs 0x8041090F unless the task
event-flag @0x006B4600 has +0xC==0xFF. That magic is stamped by
_cellSpursEventFlagInitialize (NID 0x5EF96465) -- but that NID was pinned in
YDKJ_FORCE_HLE, and the HLE stub doesn't write +0xC. (It was force-HLE'd because
libsre's version once STAT'd and tripped CRI's CellSpursTaskset.cc:423 assertion --
but that predates the taskset/attr fixes.)

Confirmed against a working RPCS3 PPU-RAM dump (caner's guest-memory-dumper fork):
the cri descriptor @0x006B4600 is fully populated there (+0xC set), so the flag IS
libsre-inited before create on real hw.

Route ONLY the cri event flag to real libsre (the eventFlag EA arrives in r5 for
this internal variant; matched against YDKJ_CRI_EVFLAG, default 0x006B4600). Audio
flags stay on the HLE stub -- routing them makes the game wait for an audio SPU
task we don't dispatch (timer_usleep hang).

Result WITHOUT the CRI_CREATE_OK hack: cri create RETURNS 0, the taskset is
populated with the movie ELF, the game keeps rendering (879 flips), and the SPU
cri task runs (passes validation, makes its task syscall). The game returns to the
q2/q3 decode-completion wait -- next the task needs movie data fed to decode+post.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…S3 dump)

Comparing our spinning cri task's LS 0x2700 SpursTasksetContext against the working
RPCS3 SPU0 dump: ours had RUNNING(0x00)=0 and PENDING(0x20)=0x80000000, RPCS3 had
RUNNING=0x80000000 and PENDING=0. Our CRI_READY only did ready|=pending; complete
the kernel's full transition (RUNNING|=pending, PENDING=0) so the state matches.

Also: YDKJ_SPU_STEPCAP now dumps the LS 0x2700 bitsets so the task's context can be
diffed against the dump without halting.

This aligns the bitsets, but the task still busy-spins in func_00026DE0/E80: it
re-reads its STATIC LS context (build_context runs once) for a work-ready change
that never arrives, because the interpreter runs the task in isolation from the
game's ongoing SPU-directed work delivery (the SPURS kernel re-DMA/signal path).
That kernel<->task work-delivery connection is the remaining gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Option (c): the cri task's r3 (CellSpursTaskArgument) points to different work
structures than the working RPCS3 SPU0 dump. Ours: r3=[00400000 006B4500 006B457C
006B6C00] where arg3@0x006B6C00 references SPURS internals (instance 0x40009F00,
taskset 0x40131000). RPCS3: [007A0000 3002EE44 3002ED00 005C3580] where
arg3@0x005C3580 is the cri decode buffer-descriptor table (sizes 0x180/0xC0/0x170 +
buffer ptrs 005C1xxx/30008A90) and arg0 points to float coefficients.

So the game's cri_mpv setup builds the task's work-args differently in our recomp:
the task gets pointers to SPURS internals instead of the movie decode buffers, so
it never finds real work and busy-spins. The remaining gap is upstream in the game
cri initialization that computes these args.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ctness batch (16 commits)

# Conflicts:
#	runtime/syscalls/sys_fs.c
…nsition matching the RPCS3 dump)

# Conflicts:
#	libs/system/cellSysutil.c
#	runtime/ppu/ppu_fs.cpp
discover_jump_tables chose the table-base register for `lwzx rD, rIdx, rBase`
as "first operand whose TOC slot reads back". Both operands are frequently
TOC-loaded, so an unrelated `lwz rIdx, d(r2)` earlier in the window reads back
a valid-but-wrong address and wins, and the table decodes from the wrong base.

The offset-table idiom always ends in `add rD, rOff, rBase` feeding mtctr, so
the base is unambiguous there. Prefer the lwzx operand that this add combines
with the (sign-extended) loaded offset; fall back to operand order otherwise.

Factored the offset-register set out as _extended_from() and reused it for both
the base disambiguation and the existing is_offset test (identical semantics).

flOw's app-loop state machine @0x89300 decoded its table from r9's global
0x10157F30 instead of r11's real base 0x00089304, so the switch fell through to
an unresolved bctr and unwound out of the main loop. With the fix flOw lifts
101 jump-table dispatchers / 1005 case targets (was 88 / 914) and drives its
app loop instead of exiting after one iteration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PhyreEngine titles (flOw) use the lv2 sys_fs syscalls directly, not the cellFs
HLE, so the existing YDKJ_FSDBG in ppu_fs.cpp never fires. Add FLOW_FSDBG to
sys_fs_read: fd, path, position, requested/actual bytes, first 4 bytes, LR.

In sys_tty_write's FLOW_PSSGTRACE hook, match "Mystery" too (flOw prints
"Mystery Numbers: %d, %d" once per app-loop iteration -- its loop heartbeat) and
call ppu_guest_callstack(): the guest back-chain reads 0 under the DRAIN/fragment
model, but the host-backtrace-to-guest-function mapper is reliable. This is what
surfaced the unresolved bctr fixed in the previous commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sp00nznet added a commit that referenced this pull request Aug 15, 2026
Folds every foldable open PR onto master: the #90/#82/#81/#79/#78 integration chain (fast-forward) plus canersaka's six fix PRs (#83-#88) cherry-picked with authorship preserved. Build clean, ctest 1/1.
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.

3 participants