fix AudioOut pacing

Co-authored-by: Stefanos Costa <stefanos.costa1999@gmail.com>
This commit is contained in:
nmzik
2026-08-05 02:17:25 +02:00
co-authored by Stefanos Costa
parent f06d7e6084
commit f380d69b9d
4 changed files with 164 additions and 11 deletions
+19 -5
View File
@@ -82,6 +82,7 @@ public:
Id AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format); Id AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format);
bool AudioOutClose(Id handle); bool AudioOutClose(Id handle);
bool AudioOutValid(Id handle); bool AudioOutValid(Id handle);
bool AudioOutHasDevice(Id handle);
bool AudioOutSetVolume(Id handle, uint32_t bitflag, const int* volume); bool AudioOutSetVolume(Id handle, uint32_t bitflag, const int* volume);
uint32_t AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking = true); uint32_t AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking = true);
bool AudioOutGetStatus(Id handle, int* type, int* channels_num); bool AudioOutGetStatus(Id handle, int* type, int* channels_num);
@@ -152,6 +153,10 @@ void AudioOutClose(int handle) {
} }
} }
bool AudioOutHasDevice(int handle) {
return g_audio != nullptr && handle > 0 && g_audio->AudioOutHasDevice(Audio::Id(handle));
}
uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) { uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) {
if (g_audio == nullptr || params == nullptr || num == 0) { if (g_audio == nullptr || params == nullptr || num == 0) {
return 0; return 0;
@@ -451,6 +456,13 @@ bool Audio::AudioOutValid(Id handle) {
m_out_ports[handle.GetId()].used); m_out_ports[handle.GetId()].used);
} }
bool Audio::AudioOutHasDevice(Id handle) {
Common::LockGuard lock(m_mutex);
return (handle.GetId() >= 0 && handle.GetId() < OUT_PORTS_MAX &&
m_out_ports[handle.GetId()].used && m_out_ports[handle.GetId()].audio_device != 0);
}
bool Audio::AudioOutGetStatus(Id handle, int* type, int* channels_num) { bool Audio::AudioOutGetStatus(Id handle, int* type, int* channels_num) {
Common::LockGuard lock(m_mutex); Common::LockGuard lock(m_mutex);
@@ -516,16 +528,18 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
max_wait_time = (wait_time > max_wait_time ? wait_time : max_wait_time); max_wait_time = (wait_time > max_wait_time ? wait_time : max_wait_time);
} }
bool all_ports_have_device = true; bool any_port_has_device = false;
for (uint32_t i = 0; i < num; i++) { for (uint32_t i = 0; i < num; i++) {
if (m_out_ports[params[i].handle.GetId()].audio_device == 0) { if (m_out_ports[params[i].handle.GetId()].audio_device != 0) {
all_ports_have_device = false; any_port_has_device = true;
break; break;
} }
} }
// Device-backed ports are paced by the SDL queue above. // One real output device is enough to pace the whole synchronized batch. Applying the fallback
if (blocking && max_wait_time != 0 && !all_ports_have_device) { // when a vibration port is present would rate-limit the device-backed ports to exactly 1x and
// prevent their SDL queues from building an underrun cushion.
if (blocking && max_wait_time != 0 && !any_port_has_device) {
Common::Thread::SleepMicro(max_wait_time); Common::Thread::SleepMicro(max_wait_time);
} }
+1
View File
@@ -26,6 +26,7 @@ static constexpr int OUT_PORTS_MAX = 32;
int AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format); int AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format);
void AudioOutClose(int handle); void AudioOutClose(int handle);
bool AudioOutHasDevice(int handle);
uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking = true); uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking = true);
} // namespace Libs::Audio::AudioInternal } // namespace Libs::Audio::AudioInternal
+21 -2
View File
@@ -336,6 +336,17 @@ static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle p
return nullptr; return nullptr;
} }
static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
Common::LockGuard lock(g_audioout2_port_mutex);
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
state.pcm_data != nullptr && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
return true;
}
}
return false;
}
static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) { static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) {
std::vector<AudioInternal::OutputParam> params; std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX); params.reserve(AudioInternal::OUT_PORTS_MAX);
@@ -493,15 +504,23 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc
uint32_t sleep_micros = audioout2_grain_micros(512); uint32_t sleep_micros = audioout2_grain_micros(512);
for (;;) { for (;;) {
// Only a synchronous submission carrying PCM to a real device can rely on the SDL queue for
// pacing. Async pushes must retain queue-depth backpressure, and a handle without PCM (or a
// vibration/failed-open handle) has no downstream operation that can block this call.
const bool use_device_clock =
blocking != 0 && audioout2_context_has_queueable_device(ctx);
g_audioout2_context_mutex.Lock(); g_audioout2_context_mutex.Lock();
if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) { if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) {
audioout2_update_context_locked(state); audioout2_update_context_locked(state);
sleep_micros = audioout2_grain_micros(state->num_grains); sleep_micros = audioout2_grain_micros(state->num_grains);
if (state->queued < state->queue_depth) { if (state->queued < state->queue_depth || use_device_clock) {
if (state->queued == 0) { if (state->queued == 0) {
state->last_update = LibKernel::KernelGetProcessTime(); state->last_update = LibKernel::KernelGetProcessTime();
} }
state->queued++; if (state->queued < state->queue_depth) {
state->queued++;
}
g_audioout2_context_mutex.Unlock(); g_audioout2_context_mutex.Unlock();
audioout2_queue_context_audio(ctx, blocking != 0); audioout2_queue_context_audio(ctx, blocking != 0);
return OK; return OK;
+123 -4
View File
@@ -19,6 +19,8 @@ namespace AudioOut2 = Libs::Audio::AudioOut2;
std::mutex g_device_mutex; std::mutex g_device_mutex;
std::condition_variable g_device_cv; std::condition_variable g_device_cv;
std::vector<int> g_live_devices; std::vector<int> g_live_devices;
std::vector<int> g_device_backed_handles;
std::vector<bool> g_output_blocking;
int g_next_device = 1; int g_next_device = 1;
int g_open_waiters = 0; int g_open_waiters = 0;
bool g_block_opens = false; bool g_block_opens = false;
@@ -61,6 +63,17 @@ struct PortState {
uint64_t reserved[6]; uint64_t reserved[6];
}; };
struct Attribute {
uint32_t attribute_id;
int32_t reserved;
const void* value;
size_t value_size;
};
struct Pcm {
const void* data;
};
const auto* AsParam(const PortParam* param) { const auto* AsParam(const PortParam* param) {
return reinterpret_cast<const AudioOut2::AudioOut2PortParam*>(param); return reinterpret_cast<const AudioOut2::AudioOut2PortParam*>(param);
} }
@@ -73,6 +86,10 @@ auto* AsState(PortState* state) {
return reinterpret_cast<AudioOut2::AudioOut2PortState*>(state); return reinterpret_cast<AudioOut2::AudioOut2PortState*>(state);
} }
const auto* AsAttribute(const Attribute* attribute) {
return reinterpret_cast<const AudioOut2::AudioOut2Attribute*>(attribute);
}
PortParam MakeParam(uint32_t data_format = 0x200) { PortParam MakeParam(uint32_t data_format = 0x200) {
PortParam param {}; PortParam param {};
param.data_format = data_format; param.data_format = data_format;
@@ -80,9 +97,9 @@ PortParam MakeParam(uint32_t data_format = 0x200) {
return param; return param;
} }
AudioOut2::AudioOut2ContextHandle CreateContext() { AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4) {
ContextParam param {}; ContextParam param {};
param.queue_depth = 4; param.queue_depth = queue_depth;
param.num_grains = 512; param.num_grains = 512;
AudioOut2::AudioOut2ContextHandle context = 0; AudioOut2::AudioOut2ContextHandle context = 0;
Check(AudioOut2::AudioOut2ContextCreate(AsParam(&param), nullptr, 0, &context) == OK, Check(AudioOut2::AudioOut2ContextCreate(AsParam(&param), nullptr, 0, &context) == OK,
@@ -112,6 +129,23 @@ int LiveDeviceCount() {
return static_cast<int>(g_live_devices.size()); return static_cast<int>(g_live_devices.size());
} }
void SetPcm(AudioOut2::AudioOut2PortHandle port, const void* data) {
const Pcm pcm {data};
const Attribute attribute {0, 0, &pcm, sizeof(pcm)};
Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) == OK,
"setting PCM failed");
}
void ResetOutputCalls() {
std::lock_guard lock(g_device_mutex);
g_output_blocking.clear();
}
std::vector<bool> OutputCalls() {
std::lock_guard lock(g_device_mutex);
return g_output_blocking;
}
void TestSlotReuse() { void TestSlotReuse() {
const auto context = CreateContext(); const auto context = CreateContext();
const auto param = MakeParam(); const auto param = MakeParam();
@@ -202,14 +236,83 @@ void TestContextDestroyCancelsPendingCreate() {
Check(LiveDeviceCount() == 0, "cancelled port create leaked a device"); Check(LiveDeviceCount() == 0, "cancelled port create leaked a device");
} }
void TestSynchronousDevicePushBypassesModelledQueue() {
const auto context = CreateContext(1);
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"device port create failed");
uint32_t pcm[512] {};
SetPcm(port, pcm);
ResetOutputCalls();
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "first sync push failed");
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK,
"device-paced sync push was blocked by modelled queue");
const auto calls = OutputCalls();
Check(calls.size() == 2, "sync pushes did not reach the device backend");
Check(calls[0] && calls[1], "sync pushes lost their blocking mode");
AudioOut2::AudioOut2PortDestroy(port);
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestAsynchronousDevicePushKeepsQueueBounded() {
const auto context = CreateContext(1);
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"device port create failed");
uint32_t pcm[512] {};
SetPcm(port, pcm);
ResetOutputCalls();
Check(AudioOut2::AudioOut2ContextPush(context, 0) == OK, "first async push failed");
Check(AudioOut2::AudioOut2ContextPush(context, 0) != OK,
"full async queue accepted another buffer");
const auto calls = OutputCalls();
Check(calls.size() == 1 && !calls[0], "rejected async push reached the device backend");
uint32_t queued = 0;
uint32_t available = 0;
Check(AudioOut2::AudioOut2ContextGetQueueLevel(context, &queued, &available) == OK,
"queue-level query failed");
Check(queued == 1 && available == 0, "async queue level does not match accepted pushes");
AudioOut2::AudioOut2PortDestroy(port);
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestHandleWithoutPcmDoesNotBypassQueue() {
const auto context = CreateContext(1);
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle port = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &port) == OK,
"device port create failed");
ResetOutputCalls();
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "empty sync push failed");
Check(AudioOut2::AudioOut2ContextPush(context, 0) != OK,
"handle without PCM bypassed queue backpressure");
Check(OutputCalls().empty(), "empty push reached the device backend");
AudioOut2::AudioOut2PortDestroy(port);
AudioOut2::AudioOut2ContextDestroy(context);
}
} // namespace } // namespace
namespace Libs::Audio::AudioInternal { namespace Libs::Audio::AudioInternal {
int AudioOutOpen(int /*type*/, uint32_t /*samples_num*/, uint32_t /*freq*/, Format /*format*/) { int AudioOutOpen(int type, uint32_t /*samples_num*/, uint32_t /*freq*/, Format /*format*/) {
std::unique_lock lock(g_device_mutex); std::unique_lock lock(g_device_mutex);
const int handle = g_next_device++; const int handle = g_next_device++;
g_live_devices.push_back(handle); g_live_devices.push_back(handle);
if (type != 10) {
g_device_backed_handles.push_back(handle);
}
g_open_waiters++; g_open_waiters++;
g_device_cv.notify_all(); g_device_cv.notify_all();
g_device_cv.wait(lock, []() { return !g_block_opens; }); g_device_cv.wait(lock, []() { return !g_block_opens; });
@@ -222,9 +325,22 @@ void AudioOutClose(int handle) {
if (it != g_live_devices.end()) { if (it != g_live_devices.end()) {
g_live_devices.erase(it); g_live_devices.erase(it);
} }
const auto device_it =
std::find(g_device_backed_handles.begin(), g_device_backed_handles.end(), handle);
if (device_it != g_device_backed_handles.end()) {
g_device_backed_handles.erase(device_it);
}
} }
uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool /*blocking*/) { bool AudioOutHasDevice(int handle) {
std::lock_guard lock(g_device_mutex);
return std::find(g_device_backed_handles.begin(), g_device_backed_handles.end(), handle) !=
g_device_backed_handles.end();
}
uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool blocking) {
std::lock_guard lock(g_device_mutex);
g_output_blocking.push_back(blocking);
return 0; return 0;
} }
@@ -244,6 +360,9 @@ int main() {
TestFullTableRecovers(); TestFullTableRecovers();
TestConcurrentCreates(); TestConcurrentCreates();
TestContextDestroyCancelsPendingCreate(); TestContextDestroyCancelsPendingCreate();
TestSynchronousDevicePushBypassesModelledQueue();
TestAsynchronousDevicePushKeepsQueueBounded();
TestHandleWithoutPcmDoesNotBypassQueue();
std::printf("AudioOut2PortTests: all cases passed\n"); std::printf("AudioOut2PortTests: all cases passed\n");
return 0; return 0;
} }