Skip to content

fix(libSceAudioOut): park guest audio calls at exit before the port tables are destroyed - #6000

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
isnisk:fix/audio-out-shutdown
Oct 14, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
isnisk:fix/audio-out-shutdown

Conversation

@isnisk

@isnisk isnisk commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

What

Fixes #5864. libSceAudioOut registered no shutdown callback, so a guest mixer thread kept calling sceAudioOut2ContextPush while exit ran the static destructors, and mixed from the freed g_ports table (SIGSEGV in AudioOut2MixPorts in the report).

  • The exports that read the AudioOut2 port table or output audio hold a guard: sceAudioOut2ContextAdvance, sceAudioOut2ContextPush, sceAudioOut2ContextDestroy, sceAudioOut2PortCreate, sceAudioOut2PortDestroy, sceAudioOut2PortGetState, sceAudioOut2PortSetAttributes, sceAudioOutOutput and sceAudioOutOutputs.
  • Once exit is requested, a guarded call parks its thread with LibcAwaitExit_nid_postfix, as the AGC entry points do since 5550ca6, instead of returning an error the title might retry.
  • The library's shutdown callback (registered on the first guarded call) waits until no guarded call is still inside, so no guest thread reads the tables when their destructors run.
  • A blocking push that waits for a grain from sceAudioOut2ContextAdvance (added in bb4ec48) also wakes on the shutdown token: grainReady becomes a condition_variable_any. Without that, the callback would wait forever for a push whose advance is parked.

The SDL devices are not closed at shutdown: the device callback reads only its heap-allocated context, which the exit does not free.

Relation to #5971: it gives the AudioOut2 port table process lifetime, and its debt entry records that the host does not stop the guest threads. This PR stops them. The two change different lines of AudioOut2Port.cpp and do not depend on each other.

Tested

Ubuntu 26.04 under WSL2, GCC 15.2.0, Release, SDL_AUDIODRIVER=dummy, on this branch rebased on 73efd9407.

  • New audio_out_shutdown_tests (four ctest cases): a thread keeps pushing (AudioOut2) or outputting (AudioOut) while another thread requests exit; an atexit handler, which runs after the shutdown callbacks and before the library's static destructors, checks that the thread has stopped. A third case has a push waiting for an advance during exit and checks that the exit finishes; a fourth calls sceAudioOut2ContextAdvance after the exit request and checks that it does not return.
  • Without the change all four fail (the mixer thread kept outputting audio after the audio shutdown, the exit did not finish, an advance after the exit request returned); with it they pass, 50 repetitions each.
  • All 16 audio_out* tests pass; no compiler warnings; check_test_registration.py and check_conventions.py pass.

Not tested: Windows (no MinGW toolchain here), the full test suite (nothing else links libSceAudioOut), a title run. The reporter of #5864 offered to run Hades II on this branch.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none (fixes a crash at exit seen on Hades II PPSA36082 Hades II (PPSA36082) #1965, see Audio: guest audio thread mixes from freed memory while the process exits #5864)
  • GPUs tested: n/a
  • AI-assisted: yes (Claude Code)

🤖 Generated with Claude Code

…ables are destroyed

The library registered no shutdown callback, so a guest mixer thread kept calling sceAudioOut2ContextPush while exit ran the static destructors and mixed from the freed port table (boykopovar#5864). Exports that read the port tables or output audio now hold a guard: once exit starts they park the thread with LibcAwaitExit_nid_postfix, and the shutdown callback waits for the calls already inside. A push waiting for a grain from sceAudioOut2ContextAdvance also wakes on the shutdown token, so the callback does not wait for an advance that is parked.
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
#5965 core/libs/prx/libSceAudioOut/CMakeLists.txt L137
#5995 core/libs/prx/libSceAudioOut/src/AudioOut.cpp L386-387, L407-408

@mrfelek

mrfelek commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Regression: after this PR the Peace Walker launcher exits before it starts the game (LoadExec hand-over)

Title: METAL GEAR SOLID: Peace Walker (PPSA18216), Master Collection launcher, sceSystemServiceLoadExec of /app0/mgspw/mgspw.elf
System: Linux, RTX 3080 Ti (NVIDIA 615.71.09)

What happens

On main d92a8da (this PR is in it), pressing through the launcher menu ends with the hand-over arguments and then the process dies:

ExitAndStartNewProject Path = /app0/mgspw/mgspw.elf
ExitAndStartNewProject Args = -region eu -lan en -selfregion NA -resolution 0 -upscale 0 -launcherpath /app0/eboot.bin -fsroot /app0/mgspw/
Arg 0 = /app0/mgspw/mgspw.elf
...
Arg 14 = /app0/mgspw/
[libc] unhandled exception '15ProcessShutdown' thrown from 0x...
terminate called after throwing an instance of 'ProcessShutdown'

The converted game executable (mgspw.mgspw.elf.elf) is never started. It starts normally when I run it directly. On main b72378b, a few hours earlier, the same hand-over worked: the log continued with the new process's HostCpu.cpp line.

Check

The only change between those two that touches shutdown is this PR. With e7219e4 reverted on top of d92a8da (plus some unrelated AGC PRs), the same launcher run hands over and the game starts (two HostCpu.cpp lines in the log, one per process).

What I did not check

I did not run plain main without my AGC PRs, and I have not read the audio code beyond this PR's description. My guess is that LoadExec runs the library shutdown before fexecve, and a guest thread's guarded audio call then parks on LibcAwaitExit and ends up throwing ProcessShutdown through guest frames. That would end the process before the exec. It is a guess, not something I traced.

AI-assisted; the runs are from my machine.

@isnisk

isnisk commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

@mrfelek thanks, your guess is right, and it's this PR's fault. sceSystemServiceLoadExec runs the library shutdown without marking an exit as requested. Since this PR, a guest audio call after the shutdown token is set goes to LibcAwaitExit_nid_postfix, which parks the thread only once an exit is requested and otherwise throws ProcessShutdown back into the title. The launcher's mixer thread pushes every few milliseconds, so it reaches that during the hand-over, before the fexecve. I assumed the library shutdown only runs on exit and didn't check LoadExec.

#6237 fixes it at the source: LoadExec starts the shutdown the way exit does, so the thread parks until the process is replaced (the AGC entry points had the same gap, which is how #6237 found it on MGS4). I reproduced your stop with a test that pushes AudioOut2 audio across a LoadExec: on main 489466d3f 0 of 10 runs relaunch, all with your '15ProcessShutdown' terminate; with #6237 merged on top, 30 of 30 do. Details on #6237. A Peace Walker run on #6237 would confirm it on the title.

AI-assisted: yes (Claude Code).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Audio: guest audio thread mixes from freed memory while the process exits

3 participants