Skip to content

SotC: unblock asset streaming, honour GNM fast clears, and play LLE audio - #15

Open
Force67 wants to merge 2 commits into
masterfrom
sotc/ingame-audio-render
Open

SotC: unblock asset streaming, honour GNM fast clears, and play LLE audio#15
Force67 wants to merge 2 commits into
masterfrom
sotc/ingame-audio-render

Conversation

@Force67

@Force67 Force67 commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Fourteen commits from a Shadow of the Colossus bring-up session. Three land real
functionality; the rest are the diagnostics that found them, plus findings written
down where the next person will look.

The one that unblocked everything

kern/umtx: publish UMUTEX_CONTESTED. MUTEX_WAIT slept without ever writing
the contested bit. FreeBSD's _do_lock_normal does exactly that, for exactly this
reason: "Set the contested bit so that a release in user space knows to use the
system call for unlock."
Without it libthr's inline atomic_cmpset_rel_32(id -> UNOWNED) always succeeds, the owner never traps, no MUTEX_WAKE is ever issued,
and our waiter escapes only via the 2 ms safety poll — 15,708 failed re-acquires on
one JobSystem mutex. The three release-side ops were wrong in the matching way, so
op 6, op 18/22 and CV_WAIT's mutex release are fixed together against the real
kernel source.

Frame rate went ~6 -> ~20 fps and SotC reached in-world for the first time: 33,023
distinct asset opens, whole world streamed, no synchronisation stall left.

Rendering

Honour GNM's fast-clear rect. SotC clears with a RECT_LIST draw carrying no
pixel shader and no vertex attributes, colour in CB_COLORn_CLEAR_WORD0/1. The
lazy-clear heuristic requires float positions and attributes so it could never
match, and the draw was rasterised as a no-op — every target ran loadOp=LOAD
forever. The fingerprint was unambiguous: the world RT held exactly the frame
index
(100.0 at f100) in 2 distinct texels across 2,073,600. Compute was a victim
faithfully processing a saturated input, not the culprit.

v_cvt_pk_u8_f32 + the cbuffer planning cap. Two independent reasons a pixel
shader was refused translation, which made the command processor skip the draw —
so the world meshes' depth pre-pass was submitted every frame while the shaded
colour pass of the same meshes never reached the backend. Declined shaders 23 -> 2,
dropped draws 32/400 -> 6/400, six-MRT G-buffer populated for the first time.

Scene RT went from 2 distinct values to 5,869, and fps ~21 -> ~41.

Audio

Host the system audio daemon. The real libSceAudioOut needs no ioctl device.
It creates a control shm plus one region per port and speaks a one-block-deep
handshake: submit() returns BUSY unless a token is zero, and the mixer thread
then parks on sceAudioOutMix<pid> forever. We hosted no consumer, so exactly one
block was ever written. kern/ps4/audio_daemon.cpp now takes the block, clears the
token, grants the event-flag bit and pushes to the existing SDL sink, paced to real
time.

Verified against Isaac, the only title on hand with a non-zero signal: the LLE path
delivers PCM bit-identical to the HLE shim — ~3.1M aligned frames over 65 s,
zero mismatching frames, reproduced across two independent runs at 1.00x real time.
Two unrelated code paths converging on identical bytes rules out wrong channel
count, format, stride, endianness, duplication, loss and tearing at once.

Also here

  • IPMI imported from ps5-minecraft-ipmi, plus a SceUserService handler and the
    fix for manager op 594 (a client event-flag wait answered as "success" made
    sceUserServiceGetEvent leak libkernel's 16 MiB arena until bad_alloc).
    libSceUserService runs LLE.
  • DELTA_LLE / DELTA_HLE to pick HLE or the real firmware module per library,
    with the measured caveats recorded next to it.
  • Guest fixed VA ranges claimed before the host can take them.
  • Host threads named from guest sys_mname stack tags, so a wait probe names the
    subsystem instead of a numeric tid.

Not fixed, and written down rather than guessed at

SotC still presents black. It is not a compositing bug: the guest paints a
fullscreen curtain whose vertex colour really is 0xff000000 in guest memory, and
out = ui.rgb + (1-ui.a)*scene.rgb is then pinned to zero. Dropping that one draw
takes the flip buffer from 1 distinct colour to 908 — a grey field, not a picture.

The largest silent correctness hole behind that is now named and documented in
gcn_spirv.cc: v_interp_mov_f32 is silently dropped for the P10 and P20
selectors
— 356 sites across 33 of SotC's 56 pixel shaders emit zero SPIR-V and
leave the destination register zero, with no warning, so the shader still reports
verdict=ok. Fixing it needs the per-vertex parameters
(VK_KHR_fragment_shader_barycentric). Left documented because the cheap interim
step flips ~33 shaders from silently-wrong to declined and that could not be
measured here.

Vertex-ring exhaustion is also real and measured (735 declines, all the VB ring,
2.5x the per-slot budget) — DELTA_GPU_VBRING_MB sizes it at runtime. It changes
nothing on screen, which is recorded so nobody re-runs it.

Default path throughout: no crash, same 18 pre-existing module-load errors. Every
new knob is env-gated and off by default.

Force67 added 2 commits July 30, 2026 19:51
Instrumentation from the black-frame investigation, all env-gated and default
off, kept because each one answered a question that had been guessed at:

  DELTA_GPU_VTXTRACE_RT=<hex>[+_AFTER]  decode and print the first vertex's
    attributes for draws into one render target. This is what proved the
    fullscreen quad SotC paints over its scene really does carry colour
    0xff000000 in GUEST memory -- a black curtain the emulator is reproducing
    faithfully, not a decode error. Its position attribute decodes to an exact
    (-1,-1,0) NDC quad from the same fetch, so the V# resolution is sound too.
  DELTA_GPU_SKIP_PS=<hex>   drop every draw using one pixel shader. Dropping the
    curtain's PS (0x804340f700) takes the flip buffer from 1 distinct colour to
    908 -- an end-to-end causal proof of which single draw blacks the frame.
  DELTA_GPU_RINGHWM         per-frame high-water marks for the vertex, index,
    uniform and storage upload rings.
  DELTA_GPU_VBRING_MB=<n>   size the vertex ring at runtime.

On the ring: exhaustion is real and now measured. Up to 735 declines in ~30
frames, every one of them the vertex ring (never IB/UBO/SBO), pegged at
8,272,244 of 8,388,608 bytes -- kVbRing is 16 MB but is halved per frame slot --
with single declined uploads up to 2.42 MB for an 86,790-index mesh. Raising it
drives declines to exactly zero and reveals 20,723 KB of genuine per-frame
vertex demand, 2.5x the per-slot budget.

It changes nothing on screen, and that is worth recording: the scene RT is
bit-for-bit identical with and without it (distinct=3606 min=0.069 max=0.836
mean=0.412 either way). So this is a real capacity bug that was starving real
draws, and it is NOT the black frame. The default is left at 16 MB rather than
raised on a hunch, since the only measured effect so far is memory.

Default path unchanged: 20 fps, no crash, the same 18 pre-existing module-load
errors.
VSRC (bits [7:0]) selects which parameter v_interp_mov_f32 reads: 0 = P10,
1 = P20, 2 = P0. Only P0 is handled. A P10 or P20 site falls through the gate,
emits zero SPIR-V body words and leaves the destination VGPR at its zero
initialiser -- and because it never calls WarnUnsupported, the shader still
reports verdict=ok with zero declines and zero unsupported ops while quietly
computing from a zero.

That combination is why it went unnoticed: every audit this session reported the
pixel shaders as translating cleanly. DELTA_GPU_SHAUDIT measures the real blast
radius on SotC -- 356 sites across 33 of its 56 unique pixel shaders -- and it is
exactly the failure shape observed downstream, where shaders that "translate
fine" write constants or zero into their targets.

Only documented, not fixed. A correct fix needs the per-vertex parameters, since
P10 = P1 - P0 and P20 = P2 - P0 at the provoking triangle: declare the Location
PerVertexKHR (VK_KHR_fragment_shader_barycentric) and emit the subtraction. The
cheap interim step is to route these through WarnUnsupported so the shader is
declined rather than drawn wrong, but that flips ~33 shaders from silently-wrong
to declined and the consequences could not be measured in this session -- so it
is written down rather than guessed at.
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