Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions core/libs/prx/libSceAudioOut/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ target_link_libraries(libSceAudioOut PRIVATE libc libkernel SDL2-static)
configure_windows_unwind(libSceAudioOut)

if(BUILD_TESTING)
add_test_executable(audio_out_shutdown_tests tests/AudioOutShutdown.cpp src/AudioOut.cpp)
target_include_directories(audio_out_shutdown_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR})
target_compile_definitions(audio_out_shutdown_tests PRIVATE sceKernelGetProcessTime=AudioShutdownClock_nid_no_patch)
target_link_libraries(audio_out_shutdown_tests PRIVATE libc SDL2-static)
configure_windows_unwind(audio_out_shutdown_tests)
add_test(NAME audio_out_shutdown COMMAND audio_out_shutdown_tests)
set_tests_properties(audio_out_shutdown PROPERTIES TIMEOUT 10 ENVIRONMENT "SDL_AUDIODRIVER=dummy")
if(WIN32)
set_tests_properties(audio_out_shutdown PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(audio_out2_pad_mix_tests tests/AudioOut2PadMix.cpp src/AudioOut2PadMix.cpp)
target_include_directories(audio_out2_pad_mix_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(audio_out2_pad_mix_tests PRIVATE libc)
Expand Down
101 changes: 87 additions & 14 deletions core/libs/prx/libSceAudioOut/src/AudioOut.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <stdexcept>
#include <vector>
#include <prx/libc/include/General.hpp>
#include <prx/libc/include/Shutdown.hpp>

#include "SDL.h"
#include "SceTypes.hpp"
Expand Down Expand Up @@ -99,6 +100,28 @@ static std::mutex g_mutex;
static std::mutex g_outputMutex;
static Port g_ports[PORTS_MAX];
static bool g_sdlInitialized = false;
static std::atomic<bool> g_stopping{false};
static std::atomic<std::uint32_t> g_activeCalls{0};
static constexpr int INVALID_PORT = -2144993277;

class AudioCall {
bool admitted = false;
public:
AudioCall() {
if (g_stopping.load()) return;
g_activeCalls.fetch_add(1);
if (!g_stopping.load()) admitted = true;
else release();
}
AudioCall(const AudioCall&) = delete;
AudioCall& operator=(const AudioCall&) = delete;
~AudioCall() { if (admitted) release(); }
explicit operator bool() const { return admitted; }
private:
static void release() {
if (g_activeCalls.fetch_sub(1) == 1) g_activeCalls.notify_all();
}
};

static bool ensureSdlAudio() {
if (g_sdlInitialized) {
Expand Down Expand Up @@ -140,6 +163,27 @@ static void closeDevice(Port& port) {
port.spec = {};
}

static void shutdownAudio() {
g_stopping.store(true);
for (auto active = g_activeCalls.load(); active != 0; active = g_activeCalls.load())
g_activeCalls.wait(active);
std::lock_guard outputLock(g_outputMutex);
std::lock_guard lock(g_mutex);
for (auto& port : g_ports) {
closeDevice(port);
port = Port{};
}
if (g_sdlInitialized) {
SDL_QuitSubSystem(SDL_INIT_AUDIO);
g_sdlInitialized = false;
}
}

static const bool g_shutdownRegistered = [] {
LibcRegisterShutdown_nid_postfix(shutdownAudio);
return true;
}();

static constexpr std::uint32_t STD_8CH_MAP[8] = {0, 1, 2, 3, 6, 7, 4, 5};

static const void* prepareBuffer(const Port& port, const void* data, std::vector<std::uint8_t>& buf) {
Expand Down Expand Up @@ -192,23 +236,30 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector
return buf.data();
}

static void sleepUs(std::unique_lock<std::mutex>& lock, std::uint64_t us) {
static bool sleepUs(std::unique_lock<std::mutex>& lock, std::uint64_t us) {
const auto start = sceKernelGetProcessTime();
lock.unlock();
PreciseSleepUs(us);
while (!g_stopping.load()) {
const auto elapsed = sceKernelGetProcessTime() - start;
if (elapsed >= us) break;
PreciseSleepUs(std::min(us - elapsed, DRAIN_SLEEP_US));
}
lock.lock();
return !g_stopping.load();
}

static void queueAudio(Port& port, const void* data, std::unique_lock<std::mutex>& lock) {
static bool queueAudio(Port& port, const void* data, std::unique_lock<std::mutex>& lock) {
if (g_stopping.load()) return false;
if (data == nullptr) {
const std::uint64_t waitStart = sceKernelGetProcessTime();
while (SDL_GetQueuedAudioSize(port.device) > 0) {
if (sceKernelGetProcessTime() - waitStart > DRAIN_TIMEOUT_US) {
SDL_ClearQueuedAudio(port.device);
break;
}
sleepUs(lock, DRAIN_SLEEP_US);
if (!sleepUs(lock, DRAIN_SLEEP_US)) return false;
}
return;
return !g_stopping.load();
}

std::vector<std::uint8_t> prepareBuf;
Expand Down Expand Up @@ -256,25 +307,29 @@ static void queueAudio(Port& port, const void* data, std::unique_lock<std::mutex
SDL_ClearQueuedAudio(port.device);
break;
}
sleepUs(lock, DRAIN_SLEEP_US);
if (!sleepUs(lock, DRAIN_SLEEP_US)) return false;
}

if (g_stopping.load()) return false;
if (SDL_QueueAudio(port.device, queueData, queueSize) < 0) {
throw std::runtime_error(std::string("SDL_QueueAudio: ") + SDL_GetError());
}
// APS5_LOG_OUT("device=%u type=%d bytes=%u queued=%u", port.device, port.type, queueSize, SDL_GetQueuedAudioSize(port.device));
return true;
}

static void paceVirtualPort(Port& port, bool hasData, std::unique_lock<std::mutex>& lock) {
static bool paceVirtualPort(Port& port, bool hasData, std::unique_lock<std::mutex>& lock) {
if (g_stopping.load()) return false;
const std::uint64_t now = sceKernelGetProcessTime();
const auto queued = static_cast<std::int64_t>(port.virtualQueueEnd - now);
if (queued <= 0) port.virtualQueueEnd = now;
if (!hasData) {
if (queued > 0) sleepUs(lock, static_cast<std::uint64_t>(queued));
return;
if (queued > 0 && !sleepUs(lock, static_cast<std::uint64_t>(queued))) return false;
return !g_stopping.load();
}
if (queued > static_cast<std::int64_t>(TARGET_LATENCY_US)) sleepUs(lock, static_cast<std::uint64_t>(queued) - TARGET_LATENCY_US);
if (queued > static_cast<std::int64_t>(TARGET_LATENCY_US) && !sleepUs(lock, static_cast<std::uint64_t>(queued) - TARGET_LATENCY_US)) return false;
port.virtualQueueEnd += (1000000ULL * port.samplesNum) / port.freq;
return true;
}

static bool portTypeValid(int type) {
Expand All @@ -295,11 +350,15 @@ static Port* getPort(int handle) {
extern "C" {

int APS5_VABI sceAudioOutInit() {
const AudioCall call;
if (!call) return INVALID_PORT;
return 0;
}

int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len,
std::uint32_t freq, std::uint32_t param) {
const AudioCall call;
if (!call) return INVALID_PORT;
(void)userId;
if (!portTypeValid(type)) {
return -2144993270;
Expand Down Expand Up @@ -356,6 +415,8 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len
}

int APS5_VABI sceAudioOutClose(int handle) {
const AudioCall call;
if (!call) return INVALID_PORT;
std::lock_guard<std::mutex> outputLock(g_outputMutex);
std::lock_guard<std::mutex> lock(g_mutex);
Port* port = getPort(handle);
Expand All @@ -368,6 +429,8 @@ int APS5_VABI sceAudioOutClose(int handle) {
}

int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) {
const AudioCall call;
if (!call) return INVALID_PORT;
std::lock_guard<std::mutex> outputLock(g_outputMutex);
std::unique_lock<std::mutex> lock(g_mutex);
Port* port = getPort(handle);
Expand All @@ -376,15 +439,17 @@ int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) {
}

if (port->device == 0) {
paceVirtualPort(*port, ptr != nullptr, lock);
if (!paceVirtualPort(*port, ptr != nullptr, lock)) return INVALID_PORT;
} else {
queueAudio(*port, ptr, lock);
if (!queueAudio(*port, ptr, lock)) return INVALID_PORT;
}
if (ptr != nullptr) port->lastDataOutputTime = sceKernelGetProcessTime();
return static_cast<int>(port->samplesNum);
}

int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) {
const AudioCall call;
if (!call) return INVALID_PORT;
if (param == nullptr || num == 0) {
return -2144993276;
}
Expand All @@ -402,9 +467,9 @@ int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num)
for (std::uint32_t i = 0; i < num; i++) {
Port& port = *getPort(param[i].handle);
if (port.device == 0) {
paceVirtualPort(port, param[i].ptr != nullptr, lock);
if (!paceVirtualPort(port, param[i].ptr != nullptr, lock)) return INVALID_PORT;
} else {
queueAudio(port, param[i].ptr, lock);
if (!queueAudio(port, param[i].ptr, lock)) return INVALID_PORT;
}
}

Expand All @@ -417,6 +482,8 @@ int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num)
}

int APS5_VABI sceAudioOutSetVolume(int handle, std::uint32_t flag, int* vol) {
const AudioCall call;
if (!call) return INVALID_PORT;
if (vol == nullptr) {
return -2144993276;
}
Expand All @@ -443,6 +510,8 @@ int APS5_VABI sceAudioOutSetVolume(int handle, std::uint32_t flag, int* vol) {
}

int APS5_VABI sceAudioOutGetLastOutputTime(int handle, std::uint64_t* outputTime) {
const AudioCall call;
if (!call) return INVALID_PORT;
if (outputTime == nullptr) {
return -2144993276;
}
Expand All @@ -456,6 +525,8 @@ int APS5_VABI sceAudioOutGetLastOutputTime(int handle, std::uint64_t* outputTime
}

int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) {
const AudioCall call;
if (!call) return INVALID_PORT;
if (state == nullptr) {
return -2144993276;
}
Expand Down Expand Up @@ -517,6 +588,8 @@ int APS5_VABI sceAudioOutSysOpen() {
}

int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) {
const AudioCall call;
if (!call) return INVALID_PORT;
std::lock_guard<std::mutex> lock(g_mutex);
Port* port = getPort(handle);
if (port == nullptr) {
Expand Down
143 changes: 143 additions & 0 deletions core/libs/prx/libSceAudioOut/tests/AudioOutShutdown.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#include "SceTypes.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include "prx/libkernel/Time/include/Time.hpp"
#include <atomic>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <thread>
#include <vector>

extern "C" {
int APS5_VABI sceAudioOutInit();
int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t);
int APS5_VABI sceAudioOutClose(int);
int APS5_VABI sceAudioOutOutput(int, const void*);
int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam*, std::uint32_t);
int APS5_VABI sceAudioOutSetVolume(int, std::uint32_t, int*);
int APS5_VABI sceAudioOutGetLastOutputTime(int, std::uint64_t*);
int APS5_VABI sceAudioOutGetPortState(int, AudioOutPortState*);
int APS5_VABI sceAudioOutSetMixLevelPadSpk(int, int);
}

namespace {
constexpr int invalidPort = static_cast<int>(0x80260003);
constexpr int pending = std::numeric_limits<int>::max();
constexpr std::uint64_t poison = 0xa5a5a5a5a5a5a5a5ULL;
std::atomic<int> shutdownCallbacks{0};
std::atomic<bool> callbackEntered{false};
std::atomic<bool> releaseCallback{false};
std::atomic<bool> outputEntered{false};
thread_local bool traceOutput = false;
void Require(bool condition, const char* message) {
if (condition) return;
std::fprintf(stderr, "%s\n", message);
std::abort();
}
void CountShutdown() {
shutdownCallbacks.fetch_add(1);
callbackEntered.store(true);
callbackEntered.notify_all();
releaseCallback.wait(false);
}
}

extern "C" std::uint64_t APS5_VABI AudioShutdownClock_nid_no_patch() {
if (traceOutput) {
outputEntered.store(true);
outputEntered.notify_all();
}
return 1000;
}

int main() {
#ifdef _WIN32
_putenv_s("SDL_AUDIODRIVER", "dummy");
#else
setenv("SDL_AUDIODRIVER", "dummy", 1);
#endif
Require(sceAudioOutInit() == 0, "audio init failed");
const int physical = sceAudioOutOpen(0x10000000, 0, 0, 256, 48000, 0);
const int first = sceAudioOutOpen(0x10000000, 10, 0, 48000, 48000, 0);
const int second = sceAudioOutOpen(0x10000000, 10, 0, 48000, 48000, 0);
Require(physical > 0 && first > 0 && second > 0, "audio ports failed to open");
std::vector<std::int16_t> samples(48000, 0);
Require(sceAudioOutOutput(physical, samples.data()) == 256, "dummy-device output failed");
Require(sceAudioOutOutput(first, samples.data()) == 48000, "first queue prefill failed");
Require(sceAudioOutOutput(second, samples.data()) == 48000, "second queue prefill failed");
std::atomic<int> outputResult{pending};
std::atomic<int> outputsResult{pending};
std::thread output([&] {
traceOutput = true;
outputResult.store(sceAudioOutOutput(first, nullptr));
});
outputEntered.wait(false);
std::atomic<bool> stopQueries{false};
std::atomic<unsigned> accepted{0}, rejected{0};
std::thread queries([&] {
while (!stopQueries.load()) {
std::uint64_t time = poison;
const int rc = sceAudioOutGetLastOutputTime(first, &time);
if (rc == 0) {
accepted.fetch_add(1);
accepted.notify_all();
}
else {
Require(rc == invalidPort && time == poison, "racing query touched rejected output");
rejected.fetch_add(1);
rejected.notify_all();
}
std::this_thread::yield();
}
});
accepted.wait(0);
Require(accepted.load() > 0, "query loop did not overlap admitted audio output");
Require(outputResult.load() == pending, "prefilled virtual output was not blocked");
std::thread outputs([&] {
AudioOutOutputParam param{second, nullptr};
outputsResult.store(sceAudioOutOutputs(&param, 1));
});
LibcRegisterShutdown_nid_postfix(CountShutdown);
std::thread shutdown1([] { LibcRunShutdown_nid_postfix(); });
callbackEntered.wait(false);
std::atomic<bool> secondShutdownEntered{false};
std::thread shutdown2([&] {
secondShutdownEntered.store(true);
secondShutdownEntered.notify_all();
LibcRunShutdown_nid_postfix();
});
secondShutdownEntered.wait(false);
releaseCallback.store(true);
releaseCallback.notify_all();
shutdown1.join();
shutdown2.join();
output.join();
outputs.join();
Require(outputResult.load() == invalidPort && outputsResult.load() == invalidPort, "pending output was not cancelled");
rejected.wait(0);
stopQueries.store(true);
queries.join();
Require(rejected.load() > 0, "query loop did not exercise shutdown rejection");
LibcRunShutdown_nid_postfix();
Require(shutdownCallbacks.load() == 1, "shutdown callbacks ran more than once");

AudioOutPortState state;
std::memset(&state, 0xa5, sizeof(state));
const auto oldState = state;
std::uint64_t time = poison;
int volume[8]{};
AudioOutOutputParam param{first, samples.data()};
Require(sceAudioOutInit() == invalidPort, "late init accepted");
Require(sceAudioOutOpen(0, -1, -1, 0, 0, 0xffffffffu) == invalidPort, "late open reached argument validation");
Require(sceAudioOutClose(physical) == invalidPort, "late close accepted");
Require(sceAudioOutOutput(first, samples.data()) == invalidPort, "late output accepted");
Require(sceAudioOutOutputs(&param, 1) == invalidPort && sceAudioOutOutputs(nullptr, 0) == invalidPort, "late outputs accepted");
Require(sceAudioOutSetVolume(first, 0xffu, volume) == invalidPort && sceAudioOutSetVolume(first, 0xffu, nullptr) == invalidPort, "late volume accepted");
Require(sceAudioOutGetLastOutputTime(first, &time) == invalidPort && time == poison, "late time query touched output");
Require(sceAudioOutGetLastOutputTime(first, nullptr) == invalidPort, "late null time query reached validation");
Require(sceAudioOutGetPortState(first, &state) == invalidPort && std::memcmp(&state, &oldState, sizeof(state)) == 0, "late state query touched output");
Require(sceAudioOutGetPortState(first, nullptr) == invalidPort, "late null state query reached validation");
Require(sceAudioOutSetMixLevelPadSpk(first, -1) == invalidPort, "late mix call reached validation");
}
Loading