Skip to content

Fix post-#91 master: clang-cl build, two missed guest-call sites, dropped SPU park - #92

Open
sp00nznet wants to merge 11 commits into
masterfrom
fix/clang-cl-build
Open

Fix post-#91 master: clang-cl build, two missed guest-call sites, dropped SPU park#92
sp00nznet wants to merge 11 commits into
masterfrom
fix/clang-cl-build

Conversation

@sp00nznet

@sp00nznet sp00nznet commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Four fixes needed to build and run a real title (the Rubber Ducky port) against
master after #91. Two are build breaks, two are behavioural regressions the
fold itself introduced.

1. clang-cl build breaks (pre-existing)

  • getenv without <stdlib.h> in libs/spurs/cellSpurs.c, libs/video/rsx_commands.c,
    runtime/syscalls/sys_timer.c. clang-cl errors on the implicit declaration; MSVC
    only warns (C4013) and then silently truncates the returned char* to int
    a live 64-bit bug on the MSVC path, not clang pedantry.
  • Stale local extern for spu_run_lifted_job_abi in runtime/spu/spu_workload.c,
    conflicting with the static inline in spu_lifted_job.h that the same file
    already includes.

Both were fixed locally in the Rubber Ducky checkout months ago (its README lists
them under "Research-branch fixes carried in this port") and never pushed upstream.

2. Two guest-call sites #86's r3–r10 widening missed

#86 widened ppu_guest_call / the g_ps3_guest_caller hook to nine arguments and
updated every call site on its base. Two sites added later by the integration chain
were not on that base:

  • runtime/ppu/ppu_hle.cpp:407ppu_guest_call(iopd, desc, 1, 8, 0x398)
  • lbp/main.cpp:174harness_guest_caller plus a local 5-param ps3_guest_caller_fn
    typedef and extern. This one is worse than a compile error: it installed a
    five-parameter function as the hook, so every nine-argument call through
    g_ps3_guest_caller was an ABI mismatch.

These did not show up in #91's verification because ppu_hle.cpp and lbp/main.cpp
are per-title files — not part of ps3recomp_runtime or the sync_stress target.

3. park_on_empty_inmbox dropped by the runtime/spu consolidation

eeff394 added spu_context.park_on_empty_inmbox, letting a persistent-worker raw
SPU run synchronously (full init + ready-mailbox handshake, then park) instead of on
an async host thread racing the PPU. 6dd8524 ("fold: align runtime/spu with
sagemono's consolidated structure") dropped both the field and the rchcnt behaviour,
silently un-fixing the determinism it was written for. Restored into the consolidated
spu_channels.c / spu_context.h.

The same commit's RD_FORCE_ADDR read-override diagnostic in ppu_loader.cpp is
still missing; left out here since it is debug-only.

Verification

  • ps3recomp_runtime.lib links clean under clang-cl 21.1.8 (Ninja, Release).
  • The Rubber Ducky port re-lifts, builds (22.4 MB exe) and runs against this branch,
    reaching the same point as its last known-good build.

🤖 Generated with Claude Code

sp00nznet and others added 3 commits August 15, 2026 03:18
Two pre-existing breakages that only MSVC tolerated. Both were found and fixed
locally in the Rubber Ducky port's checkout months ago (its README lists them
under "research-branch fixes carried in this port") but never pushed upstream,
so a fresh clang-cl build of master still fails.

1. cellSpurs.c, rsx_commands.c and sys_timer.c call getenv() without <stdlib.h>.
   clang-cl makes the implicit declaration an error; MSVC only warns (C4013) and
   then silently truncates the returned char* to int -- a real 64-bit bug on the
   MSVC path too, not just a clang pedantry.

2. spu_workload.c re-declared spu_run_lifted_job_abi as a local extern, which
   conflicts with the static inline that spu_lifted_job.h (already included at
   the top of the file) provides. Dropped the redeclaration.

Verified: clang-cl + Ninja Release build of ps3recomp_runtime is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#86 widened ppu_guest_call / the g_ps3_guest_caller hook from r3-r6 to r3-r10
and updated every call site that existed on its base. Two sites added later by
the integration chain were not on that base and were left at five arguments:

  runtime/ppu/ppu_hle.cpp:407   ppu_guest_call(iopd, desc, 1, 8, 0x398)
  lbp/main.cpp:174              harness_guest_caller + its local typedef/extern

The lbp one is the worse of the two: it re-declared ps3_guest_caller_fn locally
with five parameters and installed a five-parameter function as the hook, so a
nine-argument call through g_ps3_guest_caller was an ABI mismatch rather than a
compile error.

Neither showed up in the #91 verification because ppu_hle.cpp and lbp/main.cpp
are per-title files, not part of the ps3recomp_runtime library or the
sync_stress target. Found by building the Rubber Ducky port against master.

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

eeff394 added spu_context.park_on_empty_inmbox: a rchcnt(SPU_RdInMbox) that
finds the inbound mailbox empty halts the SPU instead of returning 0, so a
persistent-worker raw SPU can run SYNCHRONOUSLY -- full init + ready-mailbox
handshake, then park -- with no async host thread racing the PPU.

6dd8524 ("fold: align runtime/spu with sagemono's consolidated structure")
dropped both the field and the rchcnt behaviour. That silently un-fixed the
Rubber Ducky AsyncCopy SPU, whose determinism it was written for (that port
went from a run-to-run-varying abort to a stable ~5160-line run because of it),
and left the port failing to compile against master.

Restored into the consolidated spu_channels.c/spu_context.h. The same commit's
ppu_loader diagnostics (RD_FORCE_ADDR read override) are still missing; left
out here since they are debug-only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sp00nznet sp00nznet changed the title build: fix the clang-cl runtime build (implicit getenv + stale extern) Fix post-#91 master: clang-cl build, two missed guest-call sites, dropped SPU park Aug 15, 2026
…fix)

6bc76ae restored three of the fixes the faithful-adopt merge dropped when it
took sagemono's ppu_lifter.py wholesale (~835 lines of the ydkj side discarded).
This is a fourth, from 27982e0.

discover_jump_tables anchors on an indexed `lwzx rEnt,rIdx,rBase`. gcc's 64-bit
offset-table idiom materialises the element address first:

    sldi  rIdx, idx, 2          # rldicr rIdx,idx,2,61
    lwz   rBase, d(r2)
    add   rT, rIdx, rBase
    lwz   rEnt, 0(rT)           # <-- no lwzx
    extsw rEnt, rEnt
    add   r0, rEnt, rBase
    mtctr r0 ; bctr

With no lwzx in the window, discovery bailed and the switch lifted to a bare
ps3_indirect_call -- at runtime an "unresolved indirect call" that returns
without dispatching. 27982e0 synthesised the equivalent lwzx operands from the
`add rT,idx,base; lwz rEnt,0(rT)` pair so the existing base/offset/count logic
applies unchanged; that block is restored verbatim.

On Rubber Ducky (8530 functions, unchanged detection):
  jump-table dispatchers  204 -> 252   (+48, matching 27982e0's original +48)
  case targets           2042 -> 2458
and _jsCountFloatsInCgType's 110-case switch -- in the middle of the Cg shader
loader that port is blocked on -- decodes again.

Also decode lvxl/stvxl as lvx/stvx (identical apart from a cache LRU hint);
they were emitting a silent no-op `.word` at 4 sites in the same binary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sp00nznet

Copy link
Copy Markdown
Owner Author

Result on Rubber Ducky: the Cg shader wall is gone

The 4th dropped fix (dee21c4, restoring 27982e0's sldi+add+lwz jump-table idiom) clears the wall that port had been stuck on since v0.18.

_jsCountFloatsInCgType's 110-case switch sits inside the Cg shader loader. Without the idiom, discover_jump_tables bailed (JT_DEBUG showed lwzx=None), the bctr lifted to a bare ps3_indirect_call, and at runtime it logged unresolved indirect call -> 0x00143DBC and returned without dispatching — so _jsCreatePushBuffer got a garbage program layout and Cg rejected the handle.

Lift: 204 → 252 dispatchers, 2042 → 2458 case targets (detection unchanged at 8530 functions, so this is purely codegen).

Run, same binary and assets:

before after
unresolved indirect calls 4 0
.vpo shaders loaded 1 8 (every shader)
meshes read 3 7
Started solver thread no yes
abort cg_particlefluid.cpp:101 g_paramProjection none

It now runs to the 150s bound instead of aborting. New park point is TclMultiSim::initialize+0x78sys_event_queue_receive(q=9, timeout=0), waiting on an SPU thread group that reports no fallback for any of 1 thread(s); instantly completed — an unregistered SPU workload, a different and much later wall.

sp00nznet and others added 7 commits August 15, 2026 09:46
… restore RD_FORCE_ADDR

Three fixes found driving the Rubber Ducky demo past its Cg wall.

1. spu_dispatch_frame_by_queue seeded the per-frame work-descriptor EA into the
   SPU's inbound mailbox, on the assumption that a persistent-worker sim SPU
   reads it with `rdch SPU_RdInMbox`. At least one image uses `rchcnt
   SPU_RdInMbox` the OTHER way round: the solver's send-result helper (LS 0xC370)
   returns EBUSY when the inbox is NON-empty and only falls through to
   `wrch WrOutMbox; stop 0x110` when it is EMPTY. Seeding made that check fail
   every pass, so the worker never signalled completion and spun -- 3,000,000+
   interpreted steps with no progress, and the PPU (which dispatches
   synchronously on its own thread) deadlocked against it.

   Default is now no seed; RD_SPU_FRAME_MBOX=1 restores the old behaviour for an
   image that really does rdch its work EA.

2. group_start reported "no fallback for any of N thread(s)" whenever it spawned
   no host threads -- but `instant` counts BOTH no-fallback threads AND threads
   the interpreter ran to completion synchronously. A perfectly working
   interpreted run therefore logged as "no fallback", which is how the Rubber
   Ducky sim SPUs looked broken while they were in fact running. Count the two
   separately and say which.

3. The frame path never logged a completion line, so a hung re-run was
   indistinguishable from one that returned. Mirrored the group_start print.

Also restores eeff394's RD_FORCE_ADDR/RD_FORCE_VAL read override in vm_read32
(dropped by the same consolidation as park_on_empty_inmbox), and gives the
read32 [HOTREAD] spin print the guest cia/lr that the read8 one already had --
that is what named TsimpleTexture::load and _jsFlipCallbackFunction here.

Verified on Rubber Ducky: the solver's per-frame dispatch now completes
(stop=0x110, 531 insns) instead of spinning, a second sim SPU thread comes up
behind it, and the demo advances past TclMultiSim::initialize.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The transient-open retry exists for a real Windows failure mode (an AV/indexer
share lock on a file that DOES exist), but it had no gate: any failed read-only
open burned 3000 x Sleep(1) before giving up. Sleep(1) is 1-15ms, so a path that
simply does not exist cost 3-45 SECONDS of dead wall-clock per probe.

That is what made the Rubber Ducky demo look wedged at "Loading cviewer scene":
libxml2 probes /etc/xml/catalog there, which is absent by design, and every
probe stalled the guest's main thread for tens of seconds.

ENOENT is never transient. A share-lock/handle-pressure failure reports
EACCES/EBUSY, so the recovery this loop was written for still works.

Also adds an (YDKJ_MBOXTRACE-gated) trace of the SPU->PPU mailbox delivery
DECISION, not just successful deliveries -- a per-title hook that drops a
mailbox class was otherwise indistinguishable from an SPU that never wrote one.

Verified on Rubber Ducky: the scene finishes loading (all wall/tub/towel
textures + lightmaps), "init done", and the demo enters its frame loop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NV4097_SET_VERTEX_DATA_ARRAY_FORMAT packs [3:0]=type, [7:4]=size,
[15:8]=stride, [31:16]=frequency. Size 0 means "this array is off"; the parser
was keying `enabled` off `type != 0` instead.

PSGL writes a bare type word (0x00000002 = float32, size 0, stride 0) into every
array slot it is NOT using, so 13 of the 16 slots came back enabled with zero
components and zero stride. The input layout was then built from a mostly
degenerate attribute set.

Confirmed against the Rubber Ducky demo's own FIFO (YDKJ_RSXTRACE): slots 0/2/13
carry real formats (0x00012032 = float32 x3, stride 32) while 1,3,4,5,6,7...
carry 0x00000002. With the fix the layouts the draws actually use are
  attrib[0] float32 x3 stride=12, attrib[2] float32 x3 stride=12,
  attrib[13] float32 x3 stride=52
and the fetched positions are real mesh coordinates (-1.45, 0.0059, 1.49 ...)
rather than a degenerate set.

Also makes three fixed debug caps env-tunable, because each was spent entirely on
boot-time traffic and so never showed the draws worth looking at:
  YDKJ_RSXTRACE=<N>   method trace depth (was a fixed 250)
  RSX_VTXDBG=<N>      vertex-layout prints (was a fixed 5)
  VTX_POS=<N>         first fetched position per draw (new; distinguishes
                      "vertex array resolved to the wrong memory" from
                      "the shader is wrong", which look identical otherwise)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-on to a0bca37, from bisecting a title that draws valid geometry to a
correct render target and produces zero fragments.

  TCONST_MAX=<N>  raises the transform-constant trace cap (was a fixed 64, all
                  spent on boot-time uploads, so the constants the real draws
                  use were never seen). With it: this title's MVP lands at
                  c260..c263, well outside the slot 12..30 window the default
                  filter watches.
  VP_MVP=<N>      dumps the per-draw constant bank AS THE SHADER WILL SEE IT,
                  read back from the mapped vp_cb slot. A snapshot taken from a
                  stale rsx_state is indistinguishable from a broken vertex
                  program without this -- both give zero fragments.

Neither changes behaviour; both are off unless the env var is set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`render_frame` gates the ENTIRE vertex-program draw pass on s_d3d.vp_ready:

    if (s_d3d.vp_ready && s_d3d.draw_count > 0) { ... draw every is_vp record ... }

but vp_ready is only set by compile_vp(), whose trigger ran at frame end and read
s_d3d.current_rsx_state->vp_ucode_bytes. By then that state no longer carries the
microcode, so for a title that only ever compiles per-draw vertex shaders the base
pipeline was never built -- and every is_vp draw was silently dropped. Draws
recorded, constants snapshotted, per-draw VS compiled and cached, and not one of
them submitted to the GPU. From outside this is indistinguishable from a broken
vertex program: valid vertex data, valid MVP, correct render target, zero
fragments.

Build it from vp_get_vs() instead, which runs at RECORD time with a live state
that definitely has microcode. Also drops the trigger's "microcode size differs
from last compile" clause: while !vp_ready there is nothing to be stale against.

On the Rubber Ducky demo this is the difference between a boot that stalls after
two simulation frames and a continuous frame loop -- 8096 presents carrying 287
draws each, where before the pass never executed.

Adds VP_BYPASS=1 (replace the guest transform with a direct attribute-0 -> clip
space map) to separate "the transform is wrong" from "the geometry never reaches
raster", which otherwise look identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A mis-decoded NV4097_SET_COLOR_MASK makes geometry rasterize correctly and write
nothing, which from the outside is identical to the draw never happening. Env-
gated override so that hypothesis can be tested in one run instead of inferred.

(Tested on the Rubber Ducky demo: not the cause there -- its draws record
cmask 0xF from color_mask 0x01010101 -- but the check cost one run instead of a
reading session, and the next blank-output title gets it for free.)

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

current_rt_off() classified any surface whose colour offset was not one of the
registered cellGcmSetDisplayBuffer offsets as OFFSCREEN. But SetDisplayBuffer
only registers the buffers a FLIP may point at; a guest is free to render into a
different surface of the same size and flip to it later, and PSGL does exactly
that -- the Rubber Ducky demo registers 0xDF80000 / 0xDBC0000 and renders every
frame to 0xE340000.

The consequence was total, and invisible from every downstream check:

  [PRESENTGATE] records=29 onscreen=0 offscreen=29 has_display=0 -> render_frame=SKIPPED

has_display gates render_frame(), which is what submits ALL recorded geometry.
With every record misfiled as offscreen it was never called, so not one draw
reached the GPU -- while draw records, vertex uploads, constant snapshots and
per-draw shader compiles all completed normally. Vertex data, input layout, MVP
constants, depth state, cull mode and colour mask each verify as correct in that
state, which is what makes this so hard to see: the whole pipeline is right and
nothing is drawn.

With RT_DISPLAY_BY_SIZE=1 the same run submits its draws
(`[VPSUBMIT] draw[0] verts=128 pso=guest-fp rt=0`) and the demo's geometry
rasterizes to the backbuffer for the first time.

Env-gated rather than unconditional: keying "is this the display" off the clip
size is a heuristic, and a title that renders a full-size intermediate surface
would be misfiled the other way. The real fix is to track the surface the guest
flips to; this makes the failure reproducible and gives titles a way through in
the meantime.

Also adds VP_SUBMIT=<N>: prints the present-time gate decision and the first N
DrawInstanced calls the VP pass actually issues. "Records exist" and "draws were
submitted" are different claims -- conflating them cost this investigation
several wrong turns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant