Skip to content

fix(libSceAudioOut): reopen the output device the host stopped - #13694

Merged
adriadam10 merged 13 commits into
boykopovar:mainfrom
MarcZX10:fix/host-device-changes
Oct 31, 2026
Merged

adriadam10 merged 13 commits into
boykopovar:mainfrom
MarcZX10:fix/host-device-changes

Conversation

@MarcZX10

Copy link
Copy Markdown
Contributor

What

An AudioOut port stayed silent for the rest of a title after the host stopped the device it played through. SDL disables an opened device when its backend reports the device lost, and it does not enable it again: SDL_OpenedAudioDeviceDisconnected in src/audio/SDL_audio.c sets enabled to 0, and only opening a device sets it back to 1 (bundled 3rdparty/SDL2 at 4b69833bc). SDL_GetAudioDeviceStatus reports such a device as SDL_AUDIO_STOPPED. AnyPS5 never read that status for a port's default output or controller sound card, so the output calls kept returning success while the audio went nowhere.

This PR checks the status before a port's device is used and reopens the device when it is stopped:

  • the default output (sceAudioOutOpen with the main/default route) reopens through openDevice;
  • the controller sound card of a vibration port reopens through openVibrationDevice.

A failed reopen leaves the port with no device, so it runs as a virtual port, as a port opened with no output does today. Such a port is not retried later. The guest-visible API is unchanged: no export, return value or argument changes. The change is host-side only.

Base: upstream/main at 7ceda1326c, merged into the branch in 714d7e40e6. Earlier merges in the history: 9b6c28b8bd (of 50843f45d9), 2bdcad1490 (of ac551a129a), 01ffa65535 (of f25279c0) and 376d9b6fc2 (of b852c46c7d). All merges are clean.

Eight commits on top of upstream/main, plus the merges:

  • fix(libSceAudioOut): reopen the default output after the host stops it
  • test(libSceAudioOut): cover the default output reopen after a host stop
  • fix(libSceAudioOut): reopen a controller sound card the host stopped
  • test(libSceAudioOut): cover the controller sound card reopen after a host stop
  • docs(libSceAudioOut): record the host output reopen as debt
  • docs(libSceAudioOut): record the AudioOut2 main output as debt
  • test(libSceAudioOut): answer the device status query in two harnesses
  • test(libSceAudioOut): keep the queue target check in the default open harness

The status query stub in the two harnesses makes the fake device IDs of the port-lock and resampling tests report as playing, which matches the upstream behaviour those tests were written against. The default open harness keeps its queue target check in every mode except the disconnect mode, which removes the default output on purpose.

Debt: docs/dev/debt/libSceAudioOut.md records the host reopen (the console's behaviour is not measured) and a gap this PR does not fix: a context's main host device in AudioOut2 is still opened once and not reopened.

Not fixed, observed by reading only: VK_ERROR_SURFACE_LOST_KHR is not handled in VulkanDevice. check() in core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp throws on any non-success result except VK_ERROR_DEVICE_LOST, and presentLoop in core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp calls std::terminate on a std::exception. No headless seam exists to reproduce it, so it is not changed here.

Tested

Linux x86_64 (Intel Xeon E5-1620 v2, 8 threads), GCC 12, Ninja, -DBUILD_TESTING=ON, incremental build of the 30 AudioOut targets (the 27 audio_out* and libSceAudioOut targets plus the three audio_out_host_* tests of upstream main). Head tested: 714d7e40e6. The full runs were first done on 2bdcad1490 (base ac551a129a). The later merges (01ffa65535, 376d9b6fc2, 714d7e40e6) were each followed by an incremental build of the changed files and ctest -R audio_out, which gave 61 of 61 with identical test names every time. Between f25279c0 and 7ceda1326c upstream made no net change to any file under core/libs/prx/libSceAudioOut, to the libScePad, libc or libkernel sources, or to docs/dev/debt/libSceAudioOut.md; the other upstream changes are in other libraries (for example InventoryExports.hpp headers and the libSceJxr CMake file).

  • ctest -R audio_out on 2bdcad1490: 61 of 61 pass (repeated on 01ffa65535, see above). That is the 56 existing tests, the 3 upstream host tests, and the 2 new disconnect tests audio_out_default_open_disconnect and audio_out_vibration_device_disconnect.
  • Baseline on the base of the first full runs (ac551a129a): the eight branch-only files (CMakeLists.txt, src/AudioOut.cpp, the five AudioOut tests, the debt doc) set to their upstream/main versions, same build tree, ctest -R audio_out: 59 of 59 pass. Per test, the names are identical to the branch run except the two disconnect tests. So the defaults are unchanged. The files were then restored from the branch and checked byte for byte.
  • Negative control A (pre-fix harnesses): AudioOutPortLocks.cpp and AudioOutResampling.cpp from de0b4c4163, before the status stub. audio_out_independent_output, audio_out_independent_close, audio_out_same_port_close and audio_out_resampling abort: "AudioOut port locks: unknown device polled" (three times) and "resampling discarded the impulse response at the grain boundary". With the stub from 4e06acd31a they pass. The stub removes no assertion.
  • Negative control B (upstream AudioOut.cpp with the branch tests): audio_out_default_open_disconnect fails with "the removed default output was not replaced"; audio_out_vibration_device_disconnect fails with "the stopped controller sound card was not reopened". Both fail at the first assertion that depends on the reopen. The branch file was restored and both pass again. The later assertions of those two modes (queue into the live device, close count) follow the first failing assertion and were not controlled separately.
  • Negative control C (queue target): a scratch-only change in the build tree that queues to port.device + 1. With the restored default open harness, audio_out_default_open_disconnect fails with "wrong queue written". With the loosened harness from 4e06acd31a it fails with "output was queued to the removed default output". No existing default open mode calls sceAudioOutOutput, so their results do not depend on this check. The clone was not modified by the control.
  • python3 tools/env_reference.py --check: the same single failure on a clean upstream/main export and on the branch (APS5_PAD_SPEAKER_FALLBACK_DB has a row in docs/dev/ENVIRONMENT.md but the code no longer reads it), a known baseline failure. The branch adds no new failure.
  • python3 tools/check_conventions.py --base upstream/main: all conventions checks passed.
  • git diff --check upstream/main HEAD: clean.
  • python3 tools/pr_test_quality.py --json on the five changed AudioOut test files: five findings of kind possible-unregistered-cpp-test. These are a path-format false positive: all five are registered in core/libs/prx/libSceAudioOut/CMakeLists.txt with paths relative to that directory (tests/AudioOutBufferSize.cpp etc.), and all five run in ctest.
  • Overlap, python3 tools/pr_overlap.py --branch fix/host-device-changes: exit 2 (fails closed). The tool reports 10 open PRs sharing files with the branch, and could not compare three of them because they conflict with main: feat(aot): consolidate native ARM64 compiler, runtime and consumers #5021, feat(pad): stream vibration ports to a Bluetooth DualSense as haptics reports #9154, feat(libSceVideoOut): opt-in host pause with a key or on focus loss #12863. The rest of the report is in the tool output. Manual comparison of the three, against the PR metadata from GitHub:
  • Also named, checked with gh: fix(libSceAudioOut): return from a blocking AudioOut2 push once the queue has room #12954 (fix(libSceAudioOut): return from a blocking AudioOut2 push once the queue has room) is MERGED and touches AudioOut2Context.cpp and the debt doc. feat(libSceAudioOut): host volume and mute hotkeys, opt-in mute on focus loss #12569 (host volume and mute) is MERGED and is part of current main. fix(agc): keep presenting after a SUBOPTIMAL present #9881, feat(videoout): add ANYPS5_FPS_LIMIT to cap the rate the window presents #12536, feat(libSceVideoOut): limit the guest display to the host resolution #11948 are OPEN and touch no AudioOut file. feat(libSceVideoOut): validate ANYPS5_FULLSCREEN, restore the windowed size and hide the cursor in fullscreen #12930 is OPEN and touches only docs/dev/debt/libSceVideoOut.md.

NOT RUN: the rest of the ctest suite (only audio_out* was run), Windows (WASAPI default-device path), macOS, real devices (no Bluetooth controller, no audio hotplug), console, GPUs and the Xvfb VUID run (no presentation code changed), performance.

Checklist

  • Based on current main; no other open PR implements the same functions
    (based on 7ceda1326c, tested and overlap-checked on it; main had moved to 813496001e (two commits, none changing a file this PR touches, merge clean) when the PR was opened; the overlap comparison above found no other open PR with the same device-status logic)
  • 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
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

SDL marks an opened device as disabled when the backend reports it lost
(SDL_OpenedAudioDeviceDisconnected in src/audio/SDL_audio.c of the
bundled 3rdparty/SDL2 at 4b69833bc). The flag is set back only when a
device is opened again, so a disabled device stays silent until it is
closed, and SDL_GetAudioDeviceStatus reports SDL_AUDIO_STOPPED.

AudioOut never checked that status for a port's default output, so
after the output went away the title's audio stayed silent for the rest
of its run while every output call still returned success.

Before a default-output port is used, reopen its device when its status
is stopped. A failed reopen leaves the port with no device, so it runs
as a virtual port, as a port opened with no output does today. Such a
port is not retried later.
The default-open harness gains a disconnect mode. It opens the main port
on the default output, reports that device as stopped, and writes one
buffer. The output must open a replacement device, queue into it and
never queue into the stopped one, then close both devices with the port.

On a tree without the reopen the mode fails at "the removed default
output was not replaced".
A vibration port plays through the controller's own sound card, such as
a DualSense over Bluetooth. When the backend disables that device, SDL
keeps the port's device disabled until it is closed (see the default
output fix), so the port's haptic audio stopped for the rest of the run.

Before a vibration port's device is used, close it and open the
controller sound card again when its status is stopped. A failed reopen
leaves the port with no device, so its output runs as a virtual port,
as a vibration port opened with no controller card does today. Such a
port is not retried later.
…host stop

The vibration harness gains a disconnect mode. It opens a vibration port
on the controller sound card, reports that card as stopped, and writes
one buffer. The output must open the card again, queue the PCM into the
new device and never into the stopped one, then close both devices with
the port.

On a tree without the reopen the mode fails at "the stopped controller
sound card was not reopened".
The reopen of a stopped host output has no console counterpart to test
against, so its guest-visible behaviour is recorded as unmeasured.
A context's main host device is not reopened when the host stops it, a
gap the branch does not fix. The console's behaviour is unmeasured.
The output path now asks SDL for the status of a port's device. The port
lock and resampling harnesses link the shared library with fake device
IDs, and without a stub the status query reaches the real SDL, which
reports an unknown ID as stopped. The port then reopened its device and
the harnesses' expectations about the calls they see broke.

Both harnesses answer the query as playing, as the other AudioOut
harnesses that stub the device functions do.
Bring the branch up to the current upstream main. The only conflict was the
AudioOut debt doc, where both sides added a Functional entry; both are kept.
… harness

The default open harness counted SDL_QueueAudio calls by target and no
longer required the device id, so a queue to the wrong device passed in
every mode. Keep the requirement; it is lifted only while the disconnect
mode has removed the default output, where the counts check the target.
No existing mode queues audio, so their results are unchanged.
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5021 core/libs/prx/libSceAudioOut/CMakeLists.txt
docs/dev/debt/libSceAudioOut.md
PR 12765 docs/dev/debt/libSceAudioOut.md
PR 13781 core/libs/prx/libSceAudioOut/src/AudioOut.cpp
docs/dev/debt/libSceAudioOut.md

@Noxy-HNR

Noxy-HNR commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Windows data point for 714d7e40e6: Windows 11 (build 26300), Intel Core Ultra 7 255HX, WinLibs MinGW-w64 GCC 15.2.0, Release, BUILD_TESTING=ON. All 57 audio_out_* tests pass on this PR, including the two new disconnect modes, audio_out_default_open_disconnect and audio_out_vibration_device_disconnect. On its base 7ceda1326c, built the same way, the other 52 of its 55 audio_out_* tests pass; I did not run the last three there (audio_out_host_volume, audio_out_host_gain and audio_out_host_volume_state), which pass on this PR.

@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the reopen in reopenStoppedDevice (called under the port locks before both output paths), the two new disconnect tests (each asserts the reopen before anything else, so they fail without the change), the status stubs added to the other harnesses, and the debt entries; Windows and Conventions are green, with a third-party Windows run of all audio_out_* tests; merged.

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.

3 participants