Skip to content

[DRAFT] integrate/faithful-adopt-caner: SPU lifter faithful adoption + LBP SPU/SPURS + RSX textures - #82

Draft
sp00nznet wants to merge 207 commits into
ps3recompfrom
integrate/faithful-adopt-caner
Draft

[DRAFT] integrate/faithful-adopt-caner: SPU lifter faithful adoption + LBP SPU/SPURS + RSX textures#82
sp00nznet wants to merge 207 commits into
ps3recompfrom
integrate/faithful-adopt-caner

Conversation

@sp00nznet

@sp00nznet sp00nznet commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Attribution: this branch is a from-scratch re-derivation of the SPU decoder/lifter + runtime originally authored by @sp00nznet and @canersaka; credit for the underlying design belongs to them (their original commits stand on their own). sagemono authored the re-adoption. See CONTRIBUTORS.md.

DRAFT -- compile-verified, NOT yet re-lift/run verified. Do not merge before the validation below.

Folds sagemono/spu/faithful-adopt-caner (150 commits) on top of #81 (integrate/fold-2026-07-24). Brings a faithful re-adoption of caner's decode/lift work + LBP SPU/SPURS bring-up + RSX texture support (G8B8/DXT, deswizzle). -- @sagemono, adopting @canersaka's decode work.

Why this was hard (and the resolution principle)

The two lineages independently evolved the SAME subsystems (PPU/SPU lifter, FP/VMX handlers, SPU runtime, HLE) with DIFFERENT helper vocabularies -- e.g. lane-safe BE VMX floats via vrf/vstf (ours) vs ppu_vldf4/ppu_vstf4 (theirs); _mem_base rA=0 vs raw ctx->gpr[base]. Cherry-picking hunks across them would reference helpers defined on only one side. So this is a "which lineage is canonical" decision, not a hunk merge. 72 conflict hunks across 23 files.

Canonical = sagemono's faithful-adopt-caner (the deliberate, curated adoption). Resolution:

  • Lifter tools (ppu_lifter.py + spu_lifter/spu_disasm/gen_hle_nids): took theirs, then RE-APPLIED our jump-table-base fix (their discover_jump_tables had the identical "first TOC-loaded operand wins" bug that dropped flow's app-loop dispatcher).
  • SPU runtime + HLE (spu_context/dma/channels/workload, cellSpurs, cellSaveData, ppu_sysprx, ppu_loader): took theirs (canonical engine), but PRESERVED flow-critical ydkj functional bits: ydkj_host_bt re-added (ppu_fs.cpp calls it), ydkj_release_pending_threads unioned (sys_event.c calls it), park_on_empty_inmbox, TUNERFIX.
  • Unions where both added complementary code: sys_ppu_thread.c (ydkj threadgate + sagemono main-thread unique-id fix), lv2_register.c (both back-chain diagnostics), rsx_d3d12_backend.c (SYNTHESIZED: TRIANGLE_STRIP/FAN from theirs AND our VP-microcode guard), ppu_fs.cpp/sys_fs.c (superset of hdd0-overlay + dev_flash + USRDIR shortcut + mount-strip).

What IS verified

  • All 72 conflicts resolved; git clean.
  • ppu_recomp-independent runtime lib compiles clean (0 errors, ps3recomp_runtime.lib produced) -- so every C resolution is syntactically/type-correct.
  • All 4 lifter tools parse (ast.parse).

What is NOT verified (BLOCKERS before merge)

  • Cross-TU link: a static-lib archive does not link, so undefined externs between TUs are not caught here. Build a title exe against it to confirm.
  • Lifted-output correctness: taking sagemono's lifter changes the generated C. flow must be re-lifted (recomp_v6) and run; LBP must be run. A wrong synthesis in the FP/VMX/jump-table paths would be a silent miscompile invisible without running both games.

Recommend: re-lift flow -> build+run flow (ocean scene + SPU sim) AND run LBP, THEN mark ready.

🤖 Generated with Claude Code

sagemono and others added 30 commits July 10, 2026 17:42
The FP update forms (lfsu/lfdu/stfsu/stfdu) were lifted identically to
their non-update siblings: EA computed from rA+disp but never written
back to rA. gcc walks float arrays with `lfsu f,4(rN)`, so every later
access through rN silently re-read element 0. wave's _XyToPolar loaded
y via lfsu then x through the "advanced" base -- the lift computed
sqrt(x*x+x*x) instead of sqrt(x*x+y*y), turning the hue-palette disc
into a sqrt(2)-wide vertical band (the "square colour wheel").

Same class of bug fixed alongside: indexed FP update forms
(lfsux/lfdux/stfsux/stfdux) were unhandled entirely, indexed integer
update stores (stbux/sthux/stwux) lacked the writeback, and ldux/stdux
were missing from the indexed maps.

New mem_fp_update KAT section in torture_mem.c covers value + EA for
all of the above (they fail against the old lifter, pass now). The O0
switch-section noise in the torture run is the pre-existing
layout-sensitive jump-table lift bug shifting with the new guest code;
no update-form instruction occurs outside the new KATs in either
variant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The vblank ticker advanced its beat with GetTickCount64 + 16ms
integer steps = exactly 62.5Hz, which leaked into every flip-synced
title as 62-63 fps. Accumulate QPC ticks at qpf*1001/60000 instead:
long-run rate is the PS3's NTSC 60000/1001 Hz (titlebar now reads
59/60), independent of the host display. The ~1ms high-res drain
cadence is unchanged.

rsx backend: TEX_SAVE=1 dumps the first few converted ARGB texture
uploads as rgb+alpha BMP pairs -- ground truth for "is the guest
texture wrong or the sampling wrong" (this is what proved wave's
colour wheel was baked wrong by the guest, not mis-sampled: alpha
inside-region was a vertical band, not a disc).

Co-Authored-By: Claude Fable 5 <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>
read_vp_vertex passed the raw vertex-array offset to cellGcmResolveOffset,
but the NV4097_SET_VERTEX_DATA_ARRAY_OFFSET register carries the context-DMA
location in bit 31: 0 = LOCAL (VRAM), 1 = MAIN (IO-mapped system memory).
DeferredShading is the first sample to put its meshes in the main heap, so
every attrib offset was 0x80xxxxxx -- resolveOffset computed page 0x80x,
missed the IO table, and read garbage VRAM (vertices came back as -7.992 =>
degenerate => empty G-buffer, nothing rasterized).

Strip bit 31 and resolve MAIN via cellGcmResolveLocated; local offsets keep
the old path, so no change for wave/gcmcube/cellmark/vkcube (all VRAM verts).
DeferredShading geometry now rasterizes (was entirely absent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SET_VERTEX_DATA_ARRAY_FORMAT parser dropped bits [31:16] -- the vertex
frequency divisor -- and SET_FREQUENCY_DIVIDER_OPERATION (0x1FC0) was
unhandled, so instanced geometry was fetched as if every attribute advanced
per-vertex. DeferredShading's cube rings confirmed it: attrib0/attrib2
(the 24-vertex cube mesh) have freq=24 with the divider-op MODULO bit set
(repeat the mesh per instance), and attrib9 (per-instance transform) has
freq=72 in DIVIDE mode (advance once per instance). Reading attrib9 per
vertex gave every instance a garbage transform.

read_vp_vertex now maps the fetch index through the divisor: freq>1 uses
vertex%freq (MODULO) or vertex/freq (DIVIDE) per the per-attribute op bit;
freq 0/1 stays per-vertex, so the non-instanced samples (cellmark/wave/
gcmcube, all freq=0 -- verified) are unaffected.

Instancing infrastructure only: DeferredShading's cubes are still blocked
downstream by garbage G-buffer MVP constants (separate issue).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A title that double-buffers the display (DeferredShading clears both 0x0 and
0x440000 per frame, plus a HUD pass) issues several display-buffer clears per
real frame. d3d12_clear treated every one as a frame boundary and presented
the accumulated batch, so the 1-2-draw intermediates strobed black between
the full ~190-draw frames -- violent black/white flicker.

Anchor the boundary to the FLIP: a display clear only presents once a
cellGcmSetFlip has landed since the last present, and only when the batch is
a substantial fraction of the running-max frame size (so an intermediate that
arrives before the full frame's clear keeps accumulating instead of
presenting alone). Titles that flip once per frame (cellmark/gcmcube, both
verified 14/14) and titles that never call SetFlipCommand (fc stays 0) keep
the old clear-only heuristic -- no behavior change for them.

Also: VP_NOFREQ instancing kill-switch, FLIP_DBG trace env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vmrghw/vmrglw and the byte/halfword variants were unhandled -> silent
no-ops. DeferredShading's Vectormath::Aos::Matrix4 assembles its
projection rows with vmrghw (sgSceneEnvironment::setCameraProjection),
so the projection matrix vector kept whatever stale VRAM bytes were in
the register -- the guest uploaded 0xC0FFC0FF (a VRAM pointer, = -7.99f)
into the transform constants, so every G-buffer mesh transformed
offscreen and the instanced cube scene was empty (only the baseplate
survived, via the garbage-projection fallback).

Merge interleaves the HIGH (elements 0..n/2-1) or LOW half of vA and vB
per big-endian element order (byte 0 = element 0, matching vsldoi/
vspltw); temps guard vD aliasing vA/vB (`vmrghw v0,v0,v13`). With it the
transform constants come out a correct perspective matrix (0.97, 1.73,
-1.02, -2.02, -1.0) and the scene geometry submits (prim=6 strips).

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>
Capstone decodes the VA-form vector FMAs as [vD, vA, vC, vB], so ops[2]
is the multiplicand (vC) and ops[3] is the addend (vB). The lifter had
these swapped, computing vA*vB+vC instead of vA*vC+vB, corrupting every
VMX matrix multiply (Vectormath::Aos::Matrix4).

DeferredShading's transformHierarchy world*view multiply produced huge
1e37 model-eye matrices as a result. After the fix the transform
constants are clean (garbage slots 40+ -> 0), with no regression
(gcmcube 10/12, vkcube 11/12, cellmark text intact).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Purpose-built RSX frame capture ("rsxcap"): RSX_CAP=start[:count[:stride]]
snapshots whole frames to <RSX_CAP_DIR>/frame_NN/ -- a text manifest of every
op in submission order (surface targets, viewport, FP/VP, textures, colour
mask, blend) plus a BMP of the backbuffer and every offscreen colour RT used
that frame, captured atomically. Also dumps each geometry draw's VP constant
bank (candidate MVP) for transform debugging. Replaces cross-referencing draw
lists across separate runs, which is unreliable once a title cycles scenes.
current_rt_off gains an RT_OFFDBG surface-routing trace.

cmask: RenderTargetWriteMask used (cmask ? cmask : 0xF), turning a guest's
explicit all-channels-off mask (DeferredShading's depth-only shadow pass) into
write-all, so the depth pass splattered fragment colour onto its target. cmask
is already 0xF by default when unset, so a literal 0 is genuine -- honour it.

Capture confirmed DeferredShading's G-buffer geometry (142 MRT draws to
0xCC0000) rasterizes zero fragments even under FP_FORCE (camera transform is
off-screen) while the light-space shadow pass draws fine; c[0..3] of those
draws is zero with real constants up to slot 467.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Frame capture now dumps the full set of non-zero VP constant slots for the
first offscreen geometry draw (not just c[0..3]), which pinpointed the projection
block at c[256..259]. TCONST_DBG gains a slot 254..260 filter (TCONST_ALL for the
old first-64 behaviour) so the projection upload is visible past the early flood.

Together these traced DeferredShading's black screen to the camera projection:
c[256].x/c[257].y are uploaded correctly (0.9743/1.7321) for the first ~10 frames
then collapse to 0.0667 for both axes in steady state -- a guest-side miscompute
in setCameraProjection (values arrive wrong at the TCONST upload, so not a backend
bug), squashing all geometry to a sub-pixel centre strip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… repair

Three lifter-correctness fixes surfaced by the DeferredShading (first VMX
matrix-math title) and LittleBigPlanet (first retail game) bring-ups.

1. VMX float byte-order. ctx->vr holds RAW big-endian guest bytes (lvx is a
   plain 16-byte memcpy). Value-interpreting ops read (float*)&ctx->vr, which
   reads BE bytes as host little-endian -> a BE 1.0 (0x3F800000) becomes the
   denormal 0x0000803F. Added ppu_vldf4/ppu_vstf4/ppu_vldu4/ppu_vstu4 helpers
   (per-lane bswap) and rewrote every value op: FMA, add/sub/mul, FP compares,
   recip/rsqrt/floor, int<->float convert, splat-imm, word shifts. Byte/word
   MOVE ops (vperm, vmrgh*, vsldoi, vspltw, vsel, bitwise) are endian-correct
   on the raw bytes and stay untouched.

2. FMA operand order (vmaddfp/vnmsubfp). The lifter uses ppu_disasm.py, whose
   VA-form printer emits ENCODING-field order "vD, vA, vB, vC" -- ops[2] is the
   addend (vB), ops[3] the multiplicand (vC). The prior fix assumed capstone's
   ISA order (vD,vA,vC,vB) and swapped them, so the Vectormath dot-product
   square `vmaddfp vD,vA,v0zero,vA` (= vA*vA+0) degenerated into a copy
   (vA*0+vA) -> len^2 collapsed -> rsqrt -> NaN view matrix -> black screen.
   Byte-proven: 0x1e1a8 = 0x11AC4B2E enc(vD=13,vA=12,vB=9,vC=12). Corrected to
   vb=ops[2], vc=ops[3] (d = vA*vC + vB).

3. Fall-through gap repair. IDA exports sometimes truncate a function's end
   mid-body on a non-terminator (LBP malloc wrapper 0x5E7808 exported 8 bytes
   long, real body 48). The fragment falls through, and with no function at the
   end address the emit fallback trampolined to the next listed function --
   silently skipping the tail (the `bl mspace_malloc`), so malloc returned NULL
   -> operator new threw bad_alloc -> terminate -> abort at boot. New pass: for
   any bound whose last insn is not b/ba/blr/bctr/rfi and whose end has no
   function, synthesize a tail fn [end, next_start), to fixpoint. Systemic fix
   for the fragmented/r1-drift class (obviates cellmark's hand-patches).

Verified: DeferredShading renders its 3D scene; LBP boots through system init
to the render window; cellmark clean re-lift runs + draws text with no
hand-patches; gcmcube/wave/vkcube use no VMX float value ops (no-op for them).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…le strips

Driven by DeferredShading's G-buffer deferred renderer (screenshots showed a
flat-lavender normals view, blown-out light passes, and missing tentacle
meshes vs the Sony reference).

- MRT C/D binding. SET_SURFACE_COLOR_TARGET 0x17/0x1F (MRT2/MRT3 = A+B+C[+D])
  bound only A+B before, so the G-buffer's normal/position targets were never
  written -> normals view = the untouched clear colour and lighting read
  garbage. rt_off2 -> rt_mrt[3] threaded through the draw record, current_rt_off,
  the RT warm-up, the PSO (nrt via dr_num_rts), the submit loop (OMSetRenderTargets
  up to 4), ordered clears, the sampled-RT transition guard, and rsxcap.

- Guest blend factors/equation. The PSO hardcoded straight alpha; deferred's
  light accumulation is additive (ONE,ONE), so each spot light blew the frame
  to white. rsx_blend_key() packs the guest SFACTOR/DFACTOR/EQUATION (GL enums)
  into D3D12 blend enums as a PSO key, with a legacy straight-alpha fallback
  when factors are never programmed (dbgfont text).

- Indexed TRIANGLE_STRIP/FAN (prim 6/7) were skipped -> the spline-tube meshes
  (octopus tentacles) never drew. Expand CPU-side to a triangle list
  (upload_strip_vp_indexed; CullMode is NONE so strip winding parity is moot).

- RTT_DUMP now skips empty init frames so PNG-decode-heavy titles don't burn
  the capture budget before the first real draw.

Co-Authored-By: Claude Opus 4.8 (1M context) <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>
Diagnostics used to root-cause the LBP boot flow (all env-gated or bounded, no
cost when off):

- ppu_log_host_chain(): resolves the host call stack to guest function addrs
  via the lifted dispatch table (function_table). The direct-call model leaves
  guest LR slots stale, so a host backtrace mapped back to guest addresses is
  the only reliable caller chain -- this is what pinned the boot state-machine
  stage that starves (sub_439E50) and the abort caller.
- PS3_CALLTRACE=N (ppu_loader): log the first N indirect calls (target + r3/r4
  + tid) -- vtable/callback/job-body dispatch visibility.
- POLLSITE (sys_timer): dump the guest chain of the 1ms poll site.
- sys_timer usleep trace gains lr/cia; sys_semaphore_post gains a [WAKE] trace;
  sys_lwmutex create/destroy counters (LWM_COUNT).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion)

build_spu_workloads.py generalizes the flOw-hardcoded gen_spu_workloads.py
into a title-agnostic tool: for a directory of SPU ELFs it lifts each image
under a per-image C symbol prefix (so dozens of images -- each defining
spu_func_*/spu_recomp_register/a static function table -- link without
collisions), rewrites the lifter's depth-sensitive ../../runtime/spu/*.h
includes to bare includes (resolved via a build -I path, so the lifted tree
is location-independent), and emits one registration C file that registers
every image with the runtime workload registry by FNV-1a-64 content
fingerprint (byte-identical to runtime/spu/spu_workload.c) under a distinct
image id, with an optional startup constructor.

Proven on LittleBigPlanet's 23 embedded SPU images: all lift + syntax-check
clean and link into the title exe (+5.5MB). cellSpurs AddWorkload/CreateTask/
RunJobChain dispatch these by fingerprint once un-gated.

Co-Authored-By: Claude Opus 4.8 <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>
Two systemic PPC->C lifter correctness fixes (both benefit every port),
plus the matching runtime helpers and env-gated debug tooling.

1. Capped-tail continuation (the LBP "Pool possibly corrupt" abort storm).
   A gap/mid-entry tail lifted past _MAX_MID_TAIL (0x6000) ends mid-stream
   on a non-terminator with fallthrough_to = <cut addr>, and no function
   covers that address. The emit fallback then chained to the NEXT function
   in ADDRESS order -- for an interior join that is a *backwards* teleport.
   LBP: sub_32CC18 (0x69BC bytes, absent from the IDA function export) was
   gap-lifted and capped at 0x332C18; every string-build/reset join teleported
   back into func_0032F154, re-running the shrink-to-SSO reset WITHOUT its
   cap>15 free-guard -> HIBYTE(heap ptr)=0 truncated a live 0x47xxxxxx pointer
   to 0x00xxxxxx -> free(garbage) -> "doesn't belong to pool 32768" x1328 ->
   all engine threads hang.
   Fix: _register_continuation() promotes a dangling capped-tail continuation
   into branch_targets (window-guarded) so a later mid-function pass lifts a
   real func_<cont>; the emit fallback now chains to next-in-order ONLY when it
   starts exactly at fallthrough_to, else emits a halt + lift-time WARNING
   (halting a thread is strictly safer than teleporting it). Verified: LBP
   pool-corrupt count 1328 -> 0, PT-detector clean.

2. Atomic stwcx./stdcx. store-conditional. The old codegen was a plain
   conditional write that always succeeded and never validated the reservation
   value, so lock-free sequences (free-list CAS, refcounts) lost concurrent
   updates. Now calls ppu_stwcx32/ppu_stdcx64 (runtime), which do an atomic CAS
   on the raw big-endian guest word. Correct for both compare-swap and
   read-modify-write.

Torture suite (3793 KATs x -O0/-O2) A/B-verified regression-free: the failing
set (stmw, u64 var-shift, sw_dense jump tables, setjmp) is byte-identical with
and without these changes.

Debug tooling in ppu_loader.cpp is all env-gated / error-string-gated and
harmless when idle (PT persistent-truncation detector, pool/OOM host-chain
trace, value/addr watchpoints).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cellGameContentPermit always returned /dev_hdd0/game/<title>, so a disc
title (BootCheck type=1) that permits its boot content got the game-data
dir instead of the disc. LBP then hunted for data.farc on /dev_hdd0
(ENOENT), every resource load failed, and the loader stalled forever
polling its completion semaphore (sem=7).

Model a check session: BootCheck / DataCheck record whether the checked
content is disc (CELL_GAME_GAMETYPE_DISC) or game-data; the following
ContentPermit reports that content's paths -- /dev_bdvd/PS3_GAME[/USRDIR]
for disc, /dev_hdd0/game/<dir>[/USRDIR] otherwise -- and only ensures the
game-data dirs exist for the non-disc case.

Result: data.farc (180 MB) is found and opened, blurayguids.map resolves,
and the loader advances into resource loading.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cellFsLseek used fseek/ftell (long = 32-bit on Windows), truncating
offsets in files > 2 GB. Use _fseeki64/_ftelli64 (POSIX fseeko/ftello).

Extend the guest-allocator error trace (sys_tty_write path) to also fire
on "out of memory on request" so the host->guest call chain is dumped when
the game's AllocatorPlatform reports an allocation failure.

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

Two more PPC->C lifter correctness fixes, both surfaced bringing up LBP's
resource loader (each benefits every port).

1. Callee-save memory-snapshot misfire on an address-escaped frame slot.
   The robust callee-save pass, for a pure mid-function tail-entry, assumes a
   slot it never itself stores to still holds the original saved value at entry
   and rewrites `ld rN,off(r1)` -> `rN = _cs_N` (snapshot at entry). But a slot
   whose ADDRESS was taken (`addi rN,r1,off`) and passed to a call is written by
   that CALLEE (an out-param) -- invisible to the store scan -- so the `ld` after
   the call is a LIVE reload, not a restore. Snapshotting it resurrected stale
   pre-call stack garbage. LBP loc_4A52B0: `addi r5,r1,var_80; bl <fstat>` writes
   the file size to var_80, then `ld r28,var_80` reloads it; the snapshot gave
   r28 a stale stack address, so a std::vector grow requested 2x garbage ->
   "AllocatorPlatform out of memory on request size -805238272" and the loader
   stalled. Fix: `_CS_ADDR_OF_RE` collects offsets whose address is computed into
   a register; the memory-snapshot branch skips them (the genuine load stays).

2. Jump-table discovery under-decoded, and the emit dropped out-of-range cases.
   (a) Count detection took the nearest `cmpwi` to the bctr, which grabbed an
   unrelated sibling-block compare (`cmpwi r9,0` / a per-case `cmpwi r7,1`) ->
   count 0/1 -> only the default case decoded. Now it matches compares of the
   RAW INDEX register (traced back through the index shift) and takes the LARGEST
   immediate (the bounds check; per-case tests use smaller values).
   (b) The bctr emit only kept switch arms with `func.start<=case<func.end`, so
   cases past an IDA-truncated function end were silently dropped -> the runtime
   bctr hit an unlifted address. Now out-of-range cases are tail-called as their
   own func_<case> and registered so the mid-function pass lifts them.
   LBP sub_422A40 (a 0x2A-case "GMTb" resource dispatcher, cases past its
   truncated end 0x422BEC) decoded 1 case -> `unresolved indirect call 0x422CA0`;
   now decodes all 42 and dispatches.

Torture suite (3793 KATs x -O0/-O2) regression-free: failing set (stmw, u64
var-shift, sw_dense, setjmp) byte-identical to baseline -- notably sw_dense
(the jump-table KAT) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…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>
The store-conditional helpers were a bare value-CAS, which is ABA-vulnerable:
for a lock-free free-list pop (lwarx head; next=*head; stwcx next,head), a
concurrent pop+push that restores head's old value lets a stale stwcx succeed,
publishing a dangling next -> heap corruption. Real PPC loses the reservation
on ANY store to the granule, so that stale stwcx would fail.

Model that: threads register their ppu_context in a reservation set
(ppu_resv_register: host thread proc + the main entry thread); a SUCCESSFUL
stwcx/stdcx breaks every other thread's reservation on that word by clobbering
its reserve_addr to an impossible value (bit 32 set), so its own
reserve_addr==ea guard fails and it retries. Recheck+CAS+break are serialized
under address-sharded spinlocks (1024 slots by 16-byte block; a single global
lock convoyed the LBP loader's workers), and the caller's own reservation is
re-validated inside the critical section -- exact PPC semantics for
stwcx-vs-stwcx races.

PPU_RESV_STORE=1 additionally breaks reservations on plain vm_write32/64
stores to a reserved word (full granule semantics; default off keeps the hot
store path to one predictable branch). PPU_RESV_OFF=1 reverts to the bare CAS
for A/B diagnosis. RESV_DIAG=1 flags stwcx from unregistered contexts.

Fixes LBP's nondeterministic loader heap corruption (OOM aborts + hangs from
a corrupted lock-free free list).

Also env-gated debug tooling: ALLOCPROBE (insane-size allocation probe with
guest-stack + host-chain attribution), LBP_BREADCRUMB (per-tid last
indirect-call + count table, dumped from the semaphore wait path so it fires
during hangs), ppu_dbg4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sys_lwmutex_lock was a no-op stamp ("boot is single-threaded") -- once a title
spins up worker threads, every lwmutex-protected structure races; LBP's
dlmalloc mspace behind its big-allocator fell apart and reported OOM on tiny
requests with 100+ MB free. Each guest lwmutex now maps to a host
CRITICAL_SECTION (recursive, blocking) keyed by guest address in an
open-addressed table; trylock returns EBUSY on contention; lwcond_wait
releases the paired lwmutex, sleeps briefly and reacquires (poll-style: guests
re-check their predicate loops; signals stay no-ops). Contended locks log
[LWM-BLOCK]/[LWM-GOT] (bounded; uncapped inside a g_nd_inpump probe window,
default 0 -- a title's diagnostic code may pulse it around a suspect wait).

sys_ppu_thread_get_id wrote a fixed id of 1, breaking every
am-I-the-designated-thread check in multithreaded titles -- LBP's job system
routes work by thread identity, so its network-init queues were never
serviced and boot parked forever. Return the caller's real thread id.

hle_ppu_thread_create/exit now store the syscall's return into r3: the
ctx-aware dispatch doesn't propagate handler returns, so r3 kept the incoming
&tid out-pointer and guest wrappers (LBP sub_52613C) read nonzero as "create
failed" and handed tid 0 back to their callers.

sys_net offline model: LBP's net tick (sub_11A864) drains its UDP socket with
non-blocking recvfrom until it returns -1 (EWOULDBLOCK on empty socket).
These NIDs were unresolved, and the unresolved-NID default of r3=0 reads as
"received a 0-byte packet": the drain loop spun forever holding the
net-manager lwmutex, wedging boot at the Network init node. recvfrom/recv/
recvmsg now return -1 with errno EWOULDBLOCK (kept in a guest scratch cell,
since _sys_net_errno_loc returns a pointer the game dereferences); poll and
select report 0 ready. NIDs from PSL1GHT libnet exports.

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>
…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>
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>
…orensics

- The sys_process_exit back-chain dump now always fires for a NONZERO exit
  code (LBP's rare loader-thread abort is a race we can't reproduce on
  demand); clean code=0 exits stay quiet, FLOW_EXITCHAIN still forces all.

- The hang watchdog's stack scan trusted VirtualQuery blindly and read
  through guard pages / a mid-create thread's garbage Rsp; the diagnostic AV
  was then caught by the crash filter, killing otherwise-healthy runs. Skip
  the scan unless the region is committed and readable.

- Semaphore forensics (all env-gated): SEMTID adds thread ids to
  [WAIT]/[WAKE] and lifts the print cap; SEMCHAIN dumps guest call chains at
  the sem=7 poll / sem=3 post; LBP_BREADCRUMB dumps the per-tid
  indirect-call table from the wait path (fires during hangs); and
  lbp_hang_census / lbp_unstick_once (POKESEM/POKE7) read LBP's
  resource-manager queue depths out of guest memory and cross-check each
  queue's semaphore, to prove or disprove lost-wakeup hypotheses from the
  watchdog.

- sys_ppu_thread_create's log line now includes the new tid.

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

The CRITICAL_SECTION-backed lwmutex (3002f18) enforced Windows CS semantics:
only the owning thread may release. Guest code doesn't play by that rule --
LBP's job system passes lwmutex ownership between threads, and threads exit
while holding. One cross-thread LeaveCriticalSection silently no-ops, the CS
stays owned forever, and the next EnterCriticalSection parks that thread for
good. In practice this parked LBP's bringup thread at a random point a few
seconds into the Network init node (whichever lock it touched first after
the poison), which masqueraded as a network-init hang for a long chain of
plausible-but-wrong theories (busy-flag stick, frozen guest clock, starved
quiesce timeout, stuck async component -- each eliminated by measurement:
the clock is 1:1, the compare is correct, the state machine and present path
are healthy right up until they touch the lock). The watchdog's "last HLE
call = 0x1573DC3F" (sys_lwmutex_lock) plus every thread sitting in ntdll was
the giveaway.

Replace the CS with a binary semaphore, which any thread may release:
- lock: recursive re-lock via the guest owner/recur stamps (race-free: only
  the holder ever writes owner=self), else try-wait then block; the
  [LWM-BLOCK]/[LWM-GOT] contention probes stay.
- trylock: 0-timeout wait, EBUSY on contention, recursion honored.
- unlock: recur>1 counts down; final release clears the guest fields and
  posts the semaphore -- from whichever thread calls it. Over-release of a
  free mutex fails harmlessly at max count 1.
- create: force-signals the slot so a recreate at a reused guest address
  can't inherit a dead holder's locked state.
- lwcond_wait: save/clear the guest fields, release, sleep, reacquire,
  restore (poll-style wait, signalers stay no-ops).

Also register the rest of LBP's 13 sys_net imports so none fall to the
unresolved-NID default: netInitializeNetworkEx/netBind/netSetSockOpt/
netClose/netFinalizeNetwork/netFreethreadContext return 0, netSocket hands
out distinct fds (the default gave every socket fd 0, aliasing them all),
netSendTo reports the full length sent (RPCS3-offline oracle: packets vanish
into the void), netGetHostByName returns NULL (DNS down).

Verified: with this fix LBP's Network init node completes deterministically
(2/2 runs) -- the state machine ticks continuously, the game's own 30-second
offline quiesce timeout fires right on schedule, and the boot walker
advances past Network into a previously-unreached boot phase (DetermineMain
-> NGfx -> NHUD -> Input -> LoadingScreen -> GameData) with a live render
loop drawing UI quads. The old DebugVariable wild-pointer crash is also gone
(same lock corruption, different victim).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sagemono and others added 27 commits July 24, 2026 08:11
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>
…l NID table

gen/ppu_hle_nids.cpp is the NID->handler table; an unregistered import
silently fakes CELL_OK with untouched out-params. After the file was lost it
was regenerated with only cellFs/cellGcmSys/cellSysmodule (3 modules, 110
handlers), so the game called cellSysutilCheckCallback unregistered, got a
faked CELL_OK, and its boot diverged: the loading threads exited early,
cellSpursInitialize was never reached, and the loader spun (finish#3).

Regenerate with all 88 PRX modules the runtime implements (1032 handlers).
cellSysutil / sceNp / cellUserInfo / cellGame / ... now resolve and LBP boots
through to the SPURS workload + WWS job-manager path again (27x AddWorkload,
wid=12, farc asset loads, no early loading-thread exit).

- gen_hle_nids.py: add --all, which discovers every PRX module compiled into
  ps3recomp_runtime rather than a hand-typed list that can silently drop a
  whole library. --all reproduces the identical 1032-handler set.
- track gen/ppu_hle_nids.cpp. Its siblings gen/jobmods_register.c and
  gen/spu_workloads.c are already tracked; this one never was, which is why a
  stray `git clean` deleted only it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two LBP WWS physics jobmods (e0A40, e0A20) embed a zlib-inflate error-string
table plus float const tables after their code, inside the executable segment.
Those data bytes decode as in-range branches (br/brsl/brhz) whose targets seed
spurious "functions", which then disassemble the tail as code -- 97 `.word`
(undecodable) each, 194 unsupported total. Reachability from the entry can't
bound it (the image entry is an indirect dispatch stub), so the boundary is
supplied per image.

- spu_lifter.py: --code-end ADDR drops any auto-detected function starting
  at/after ADDR and caps any function's end there, leaving the tail as data.
- lift_jobmods.py: per-module CODE_END (e0A40=0xAE68, e0A20=0xAE48 -- the real
  code ends at the last unconditional `br`, located by disassembly).

Re-lift is 0 unsupported across all 89 modules (was 194).

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>
… of caner's decode + LBP SPU/SPURS + RSX textures

# Conflicts:
#	libs/audio/cellAudio.c
#	libs/spurs/cellSpurs.c
#	libs/spurs/spurs_pm.c
#	libs/system/cellGame.c
#	libs/system/cellSaveData.c
#	libs/system/cellSysutil.c
#	libs/video/rsx_d3d12_backend.c
#	runtime/ppu/ppu_fs.cpp
#	runtime/ppu/ppu_hle.cpp
#	runtime/ppu/ppu_loader.cpp
#	runtime/ppu/ppu_sysprx.cpp
#	runtime/spu/spu_channels.c
#	runtime/spu/spu_context.h
#	runtime/spu/spu_dma.h
#	runtime/spu/spu_workload.c
#	runtime/syscalls/lv2_register.c
#	runtime/syscalls/sys_event.c
#	runtime/syscalls/sys_fs.c
#	runtime/syscalls/sys_ppu_thread.c
#	tools/gen_hle_nids.py
#	tools/ppu_lifter.py
#	tools/spu_disasm.py
#	tools/spu_lifter.py
…struct

sagemono's boot_main.cpp added a sys_ppu_thread.h include (->lv2_syscall_table.h
->ppu_context.h) after the lifted ppu_recomp.h already defined struct ppu_context,
causing a redefinition when a title is built with a lifter whose header uses
#pragma once (no PPU_CONTEXT_H). Claim the guard right after ppu_recomp.h.
Take sagemono's version of every runtime/spu file and drop the base-only
interpreter split (spu_interp.c/.h/_tables.inc, spu_fn_registry.c/.h + selftests)
that sagemono consolidated into spu_channels.c. Re-add ppu_guest_callstack (kept
ydkj diagnostics reference it) and drop the ydkj ppu_in_guest_callback re-entrancy
guard in ppu_gcm_pump (no guest-call-depth counter in sagemono's runtime; the
local guard holds). Makes the merged runtime link when a title builds against it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR #82 (faithful-adopt-caner) is a from-scratch re-derivation of the SPU
decoder/lifter + runtime that sp00nz and Caner Saka originally authored. Their
original commit history stands on its own; this credits them as the originators
of the underlying design that sagemono's branch faithfully re-adopts, so the
attribution is preserved when the re-adoption folds in.

Co-Authored-By: sp00nz <sp00nz@gmail.com>
Co-Authored-By: Caner Saka <c33.saka@gmail.com>
Co-Authored-By: sagemono <sewshee@tuta.io>
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