From e7219e4bc4fe34aba7d1d49aff1032d9f021d665 Mon Sep 17 00:00:00 2001 From: toxinexx Date: Wed, 14 Oct 2026 20:37:49 +0200 Subject: [PATCH] fix(libSceAudioOut): park guest audio calls at exit before the port tables 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 (#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. --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 14 ++ core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 12 +- .../libSceAudioOut/src/AudioOut2Context.cpp | 19 ++- .../libSceAudioOut/src/AudioOut2Internal.hpp | 3 +- .../prx/libSceAudioOut/src/AudioOut2Port.cpp | 22 ++- .../libSceAudioOut/src/AudioOutShutdown.cpp | 34 ++++ .../libSceAudioOut/src/AudioOutShutdown.hpp | 12 ++ .../libSceAudioOut/tests/AudioOutShutdown.cpp | 155 ++++++++++++++++++ 8 files changed, 260 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceAudioOut/src/AudioOutShutdown.cpp create mode 100644 core/libs/prx/libSceAudioOut/src/AudioOutShutdown.hpp create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutShutdown.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index b29adfce52e..33093cc0110 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(libSceAudioOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOutSdl.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOutShutdown.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Context.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2PadMix.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Port.cpp @@ -135,4 +136,17 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_timing PROPERTIES RUN_SERIAL TRUE ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out_shutdown_tests tests/AudioOutShutdown.cpp) + target_include_directories(audio_out_shutdown_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_shutdown_tests PRIVATE libSceAudioOut libkernel libc) + configure_windows_unwind(audio_out_shutdown_tests) + add_test(NAME audio_out2_shutdown COMMAND audio_out_shutdown_tests --audio-out2) + add_test(NAME audio_out2_waiting_push_shutdown COMMAND audio_out_shutdown_tests --audio-out2-waiting-push) + add_test(NAME audio_out2_advance_shutdown COMMAND audio_out_shutdown_tests --audio-out2-advance) + add_test(NAME audio_out_shutdown COMMAND audio_out_shutdown_tests --audio-out) + set_tests_properties(audio_out2_shutdown audio_out2_waiting_push_shutdown audio_out2_advance_shutdown audio_out_shutdown PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(audio_out2_shutdown audio_out2_waiting_push_shutdown audio_out2_advance_shutdown audio_out_shutdown PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 206466563d1..2f63a8fb034 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -15,8 +15,10 @@ #include "SDL.h" #include "SceTypes.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libSceAudioOut/include/AudioOutSdl.hpp" +#include "AudioOutShutdown.hpp" static constexpr int PORT_TYPE_MAIN = 0; static constexpr int PORT_TYPE_BGM = 1; @@ -383,7 +385,8 @@ int APS5_VABI sceAudioOutClose(int handle) { return 0; } -int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) { +int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) try { + const AudioOutCall call; std::lock_guard outputLock(g_outputMutex); std::unique_lock lock(g_mutex); Port* port = getPort(handle); @@ -398,13 +401,16 @@ int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) { } if (ptr != nullptr) port->lastDataOutputTime = sceKernelGetProcessTime(); return static_cast(port->samplesNum); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) { +int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) try { if (param == nullptr || num == 0) { return -2144993276; } + const AudioOutCall call; std::lock_guard outputLock(g_outputMutex); std::unique_lock lock(g_mutex); @@ -430,6 +436,8 @@ int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) } return static_cast(first.samplesNum); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceAudioOutSetVolume(int handle, std::uint32_t flag, int* vol) { diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 081850a76cf..74a4a2b1c46 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -13,7 +13,9 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "AudioOut2Internal.hpp" +#include "AudioOutShutdown.hpp" #include "prx/libSceAudioOut/include/AudioOutSdl.hpp" #include "prx/libScePad/include/PadState.hpp" @@ -256,7 +258,8 @@ static void TraceSummary(AudioOut2Context& context, Clock::time_point now) { extern "C" { -int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) { +int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) try { + const AudioOutCall call; auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::lock_guard lock(context->lock); @@ -278,6 +281,8 @@ int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) { context->summaryAdvances++; if (context->advances <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Advance ctx %p (pushes so far %llu)\n", AudioOut2TraceSeconds(), static_cast(context), static_cast(context->pushes)); return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam* params, void* buffer, size_t buffer_size, AudioOut2ContextHandle* ctx) { @@ -295,7 +300,8 @@ int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam* params, voi return 0; } -int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) { +int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) try { + const AudioOutCall call; auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; AUDIOOUT2_TRACE("t=%.3f ContextDestroy ctx %p after %llu pushes\n", AudioOut2TraceSeconds(), static_cast(context), static_cast(context->pushes)); @@ -309,6 +315,8 @@ int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) { AudioOut2ReleasePorts(*context); delete context; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint32_t* queue_level, uint32_t* available_queue) { @@ -324,7 +332,8 @@ int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint3 return 0; } -int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t blocking) { +int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t blocking) try { + const AudioOutCall call; auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::unique_lock lock(context->lock); @@ -344,7 +353,7 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block } if (context->advancedMode && context->advancedGrains.empty()) { if (!blocking) throw std::runtime_error("AudioOut2: a non-blocking push without a rendered grain is not implemented"); - context->grainReady.wait(lock, [&] { return !context->advancedGrains.empty(); }); + if (!context->grainReady.wait(lock, LibcShutdownToken_nid_postfix(), [&] { return !context->advancedGrains.empty(); })) throw ProcessShutdown{}; now = Clock::now(); Drain(*context, now); } @@ -390,6 +399,8 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block } TraceSummary(*context, now); return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceAudioOut2ContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size) { diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 8832eb873a3..c58bd4c5426 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -91,7 +92,7 @@ struct AudioOut2Context { std::deque output; bool priming = true; bool advancedMode = false; - std::condition_variable grainReady; + std::condition_variable_any grainReady; std::deque advancedGrains; std::deque pendingGrains; std::deque pendingSince; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index cc72dbbc161..14c9bc2c635 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -10,8 +10,10 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "AudioOut2Internal.hpp" #include "AudioOut2PadMix.hpp" +#include "AudioOutShutdown.hpp" static constexpr std::uint16_t OUTPUT_MAIN = 1; static constexpr std::int16_t VOLUME_MAX = 127; @@ -178,7 +180,8 @@ static void TraceAttribute(AudioOut2PortHandle handle, const AudioOut2Port& port extern "C" { -int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port) { +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port) try { + const AudioOutCall call; if (!ctx) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; if (!params || !port) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; const auto channels = (params->data_format >> FORMAT_CHANNELS_SHIFT) & FORMAT_CHANNELS_MASK; @@ -219,18 +222,24 @@ int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2 AudioOut2TraceSeconds(), static_cast(ctx), static_cast(*port), params->port_type, params->data_format, entry.channels, params->sampling_freq, params->flags, static_cast(params->user_handle)); return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle port) { +int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle port) try { + const AudioOutCall call; std::lock_guard lock(g_portsLock); auto* entry = FromHandle(port); if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; AUDIOOUT2_TRACE("t=%.3f PortDestroy port %llu (data sets %llu)\n", AudioOut2TraceSeconds(), static_cast(port), static_cast(entry->dataSets)); *entry = AudioOut2Port{}; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) { +int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) try { + const AudioOutCall call; std::lock_guard lock(g_portsLock); const auto* entry = FromHandle(port); if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; @@ -240,9 +249,12 @@ int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortSt state->num_channels = static_cast(entry->channels); state->volume = VOLUME_MAX; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num) { +int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num) try { + const AudioOutCall call; std::lock_guard lock(g_portsLock); auto* entry = FromHandle(port); if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; @@ -268,6 +280,8 @@ int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const Audi if (traces < ATTRIBUTE_TRACE_FULL || traces % ATTRIBUTE_TRACE_EVERY == 0) TraceAttribute(port, *entry, attribute, verdict); } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.cpp b/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.cpp new file mode 100644 index 00000000000..d980fd22fc5 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.cpp @@ -0,0 +1,34 @@ +#include +#include +#include "prx/libc/include/Shutdown.hpp" +#include "AudioOutShutdown.hpp" + +namespace { + +std::atomic inside{0}; +std::atomic stopping{false}; + +void Leave() { + if (inside.fetch_sub(1) == 1) inside.notify_all(); +} + +void Stop() { + stopping.store(true); + for (auto count = inside.load(); count != 0; count = inside.load()) inside.wait(count); +} + +} + +AudioOutCall::AudioOutCall() { + static const bool registered = (LibcRegisterShutdown_nid_postfix(Stop), true); + static_cast(registered); + inside.fetch_add(1); + if (stopping.load() || LibcShutdownToken_nid_postfix().stop_requested()) { + Leave(); + throw ProcessShutdown{}; + } +} + +AudioOutCall::~AudioOutCall() { + Leave(); +} diff --git a/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.hpp b/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.hpp new file mode 100644 index 00000000000..39af8943ab4 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.hpp @@ -0,0 +1,12 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUTSHUTDOWN_HPP +#define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUTSHUTDOWN_HPP + +class AudioOutCall final { +public: + AudioOutCall(); + ~AudioOutCall(); + AudioOutCall(const AudioOutCall&) = delete; + AudioOutCall& operator=(const AudioOutCall&) = delete; +}; + +#endif diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutShutdown.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutShutdown.cpp new file mode 100644 index 00000000000..dd5390753d1 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutShutdown.cpp @@ -0,0 +1,155 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); +int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); +int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle, std::uint32_t); +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); +int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle, const AudioOut2Attribute*, std::uint32_t); +int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOutOutput(int, const void*); +} + +namespace { + +constexpr std::uint32_t grain = 256; +constexpr std::uint32_t frequency = 48000; +constexpr std::uint16_t portTypeMain = 0; +constexpr std::uint32_t attributeData = 0; +constexpr std::uint32_t formatStereoFloat = 2u << 8; +constexpr int user = 0x10000000; +constexpr std::uint32_t formatS16Stereo = 1; +constexpr std::uint64_t startedOutputs = 3; +constexpr auto settle = std::chrono::milliseconds(100); +constexpr auto exitLimit = std::chrono::seconds(3); + +std::atomic outputs{0}; +std::vector pcm(static_cast(grain) * 2, 0.25f); + +void Fail(const char* message) { + std::fprintf(stderr, "%s\n", message); + std::fflush(stderr); + std::_Exit(1); +} + +void RequireParked() { + const auto before = outputs.load(); + std::this_thread::sleep_for(settle); + if (outputs.load() != before) Fail("the mixer thread kept outputting audio after the audio shutdown"); + std::puts("the mixer thread was parked before the audio tables were destroyed"); + std::fflush(stdout); +} + +void DelayExit() { + std::this_thread::sleep_for(settle); +} + +void ReportExit() { + std::puts("the exit finished while a push waited for an advance"); + std::fflush(stdout); +} + +void SetData(AudioOut2PortHandle port) { + const void* pointer = pcm.data(); + const AudioOut2Attribute attribute{attributeData, 0, &pointer, sizeof(pointer)}; + if (sceAudioOut2PortSetAttributes(port, &attribute, 1) != 0) Fail("a port attribute set returned an error"); +} + +void CreatePort(AudioOut2ContextHandle& context, AudioOut2PortHandle& port) { + AudioOut2ContextParam params{}; + if (sceAudioOut2ContextResetParam(¶ms) != 0) Fail("context parameters were not reset"); + params.num_grains = grain; + if (sceAudioOut2ContextCreate(¶ms, nullptr, 0, &context) != 0) Fail("context was not created"); + AudioOut2PortParam portParams{}; + portParams.port_type = portTypeMain; + portParams.data_format = formatStereoFloat; + portParams.sampling_freq = frequency; + if (sceAudioOut2PortCreate(context, &portParams, &port) != 0) Fail("port was not created"); +} + +void PushAudioOut2() { + AudioOut2ContextHandle context = 0; + AudioOut2PortHandle port = 0; + CreatePort(context, port); + std::thread([context, port] { + for (;;) { + SetData(port); + if (sceAudioOut2ContextPush(context, 1) != 0) Fail("a push during exit returned an error"); + outputs++; + } + }).detach(); +} + +void WaitInPushAudioOut2() { + AudioOut2ContextHandle context = 0; + AudioOut2PortHandle port = 0; + CreatePort(context, port); + SetData(port); + if (sceAudioOut2ContextAdvance(context) != 0) Fail("an advance returned an error"); + if (sceAudioOut2ContextPush(context, 1) != 0) Fail("a push returned an error"); + std::thread([context] { + outputs++; + sceAudioOut2ContextPush(context, 1); + Fail("a push waiting for an advance returned during exit"); + }).detach(); +} + +void AdvanceAtExit() { + AudioOut2ContextHandle context = 0; + AudioOut2PortHandle port = 0; + CreatePort(context, port); + SetData(port); + LibcRegisterShutdown_nid_postfix(DelayExit); + LibcRequestExit_nid_postfix(0); + sceAudioOut2ContextAdvance(context); + Fail("an advance after the exit request returned"); +} + +void OutputAudioOut() { + const int port = sceAudioOutOpen(user, 0, 0, grain, frequency, formatS16Stereo); + if (port <= 0) Fail("port was not opened"); + static std::vector data(static_cast(grain) * 2, 0x1000); + std::thread([port] { + for (;;) { + if (sceAudioOutOutput(port, data.data()) < 0) Fail("an output during exit returned an error"); + outputs++; + } + }).detach(); +} + +} + +int main(int argc, char** argv) { +#ifdef _WIN32 + _putenv_s("SDL_AUDIODRIVER", "dummy"); +#else + setenv("SDL_AUDIODRIVER", "dummy", 1); +#endif + const bool waiting = argc == 2 && std::strcmp(argv[1], "--audio-out2-waiting-push") == 0; + if (argc == 2 && std::strcmp(argv[1], "--audio-out2") == 0) PushAudioOut2(); + else if (argc == 2 && std::strcmp(argv[1], "--audio-out") == 0) OutputAudioOut(); + else if (waiting) WaitInPushAudioOut2(); + else if (argc == 2 && std::strcmp(argv[1], "--audio-out2-advance") == 0) AdvanceAtExit(); + else Fail("usage: audio_out_shutdown_tests --audio-out2 | --audio-out2-waiting-push | --audio-out2-advance | --audio-out"); + while (outputs.load() < (waiting ? 1 : startedOutputs)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (waiting) std::this_thread::sleep_for(settle); + std::thread([] { + std::this_thread::sleep_for(exitLimit); + Fail("the exit did not finish"); + }).detach(); + if (std::atexit(waiting ? ReportExit : RequireParked) != 0) Fail("atexit registration failed"); + LibcRequestExit_nid_postfix(0); + LibcAwaitExit_nid_postfix(); +}