[APU] Pace audio subsystem
This commit is contained in:
@@ -9,11 +9,14 @@
|
|||||||
|
|
||||||
#include "xenia/apu/audio_system.h"
|
#include "xenia/apu/audio_system.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
#include "xenia/apu/apu_flags.h"
|
#include "xenia/apu/apu_flags.h"
|
||||||
#include "xenia/apu/audio_driver.h"
|
#include "xenia/apu/audio_driver.h"
|
||||||
#include "xenia/apu/xma_decoder.h"
|
#include "xenia/apu/xma_decoder.h"
|
||||||
#include "xenia/base/assert.h"
|
#include "xenia/base/assert.h"
|
||||||
#include "xenia/base/byte_stream.h"
|
#include "xenia/base/byte_stream.h"
|
||||||
|
#include "xenia/base/clock.h"
|
||||||
#include "xenia/base/logging.h"
|
#include "xenia/base/logging.h"
|
||||||
#include "xenia/base/math.h"
|
#include "xenia/base/math.h"
|
||||||
#include "xenia/base/profiling.h"
|
#include "xenia/base/profiling.h"
|
||||||
@@ -35,13 +38,6 @@
|
|||||||
// and let the normal AudioSystem handling take it, to prevent duplicate
|
// and let the normal AudioSystem handling take it, to prevent duplicate
|
||||||
// implementations. They can be found in xboxkrnl_audio_xma.cc
|
// implementations. They can be found in xboxkrnl_audio_xma.cc
|
||||||
|
|
||||||
DEFINE_uint32(apu_max_queued_frames, 8,
|
|
||||||
"Allows changing max buffered audio frames to reduce audio "
|
|
||||||
"delay. Lowering this value might cause performance issues. "
|
|
||||||
"Value range: [4-64]",
|
|
||||||
"APU");
|
|
||||||
UPDATE_from_uint32(apu_max_queued_frames, 2024, 8, 31, 20, 64);
|
|
||||||
|
|
||||||
namespace xe {
|
namespace xe {
|
||||||
namespace apu {
|
namespace apu {
|
||||||
|
|
||||||
@@ -50,17 +46,13 @@ AudioSystem::AudioSystem(cpu::Processor* processor)
|
|||||||
processor_(processor),
|
processor_(processor),
|
||||||
worker_running_(false) {
|
worker_running_(false) {
|
||||||
std::memset(clients_, 0, sizeof(clients_));
|
std::memset(clients_, 0, sizeof(clients_));
|
||||||
queued_frames_ = std::clamp(cvars::apu_max_queued_frames,
|
|
||||||
static_cast<uint32_t>(kMinimumQueuedFrames),
|
|
||||||
static_cast<uint32_t>(kMaximumQueuedFrames));
|
|
||||||
|
|
||||||
for (size_t i = 0; i < kMaximumClientCount; ++i) {
|
for (size_t i = 0; i < kMaximumClientCount; ++i) {
|
||||||
client_semaphores_[i] = xe::threading::Semaphore::Create(0, queued_frames_);
|
client_semaphores_[i] =
|
||||||
wait_handles_[i] = client_semaphores_[i].get();
|
xe::threading::Semaphore::Create(0, kMaximumQueuedFrames);
|
||||||
}
|
}
|
||||||
shutdown_event_ = xe::threading::Event::CreateAutoResetEvent(false);
|
pending_work_event_ = xe::threading::Event::CreateAutoResetEvent(false);
|
||||||
assert_not_null(shutdown_event_);
|
assert_not_null(pending_work_event_);
|
||||||
wait_handles_[kMaximumClientCount] = shutdown_event_.get();
|
|
||||||
|
|
||||||
xma_decoder_ = std::make_unique<xe::apu::XmaDecoder>(processor_);
|
xma_decoder_ = std::make_unique<xe::apu::XmaDecoder>(processor_);
|
||||||
|
|
||||||
@@ -93,7 +85,8 @@ X_STATUS AudioSystem::Setup(kernel::KernelState* kernel_state) {
|
|||||||
worker_thread_->set_can_debugger_suspend(true);
|
worker_thread_->set_can_debugger_suspend(true);
|
||||||
worker_thread_->set_name("Audio Worker");
|
worker_thread_->set_name("Audio Worker");
|
||||||
worker_thread_->Create();
|
worker_thread_->Create();
|
||||||
|
// Set high priority for this thread for better pacing.
|
||||||
|
worker_thread_->SetPriority(24);
|
||||||
return X_STATUS_SUCCESS;
|
return X_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,56 +94,92 @@ void AudioSystem::WorkerThreadMain() {
|
|||||||
// Initialize driver and ringbuffer.
|
// Initialize driver and ringbuffer.
|
||||||
Initialize();
|
Initialize();
|
||||||
|
|
||||||
// Main run loop.
|
// The host mixer releases a client's semaphore on its own coarse cadence,
|
||||||
|
// but Xenos audio subsystem operates at 5.333ms interval (see
|
||||||
|
// xaudio2_audio_driver.cc) Interval scales inversely with guest_time_scalar.
|
||||||
|
// We therefore pace pumps to each client's next_pump_us deadline and use
|
||||||
|
// the semaphore only as back-pressure: a frame is submitted only if
|
||||||
|
// a host output slot is free, otherwise it is dropped (the host queue
|
||||||
|
// is full, so it is already well buffered).
|
||||||
while (worker_running_) {
|
while (worker_running_) {
|
||||||
// These handles signify the number of submitted samples. Once we reach
|
const uint64_t now = static_cast<uint64_t>(
|
||||||
// 64 samples, we wait until our audio backend releases a semaphore
|
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||||
// (signaling a sample has finished playing)
|
std::chrono::steady_clock::now().time_since_epoch())
|
||||||
auto result =
|
.count());
|
||||||
xe::threading::WaitAny(wait_handles_, xe::countof(wait_handles_), true);
|
|
||||||
if (result.first == xe::threading::WaitResult::kFailed) {
|
size_t client_index = kMaximumClientCount;
|
||||||
// TODO: Assert?
|
uint64_t earliest_pump_us = std::numeric_limits<uint64_t>::max();
|
||||||
continue;
|
uint32_t client_callback = 0;
|
||||||
|
uint32_t client_callback_arg = 0;
|
||||||
|
{
|
||||||
|
auto global_lock = global_critical_region_.Acquire();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < kMaximumClientCount; ++i) {
|
||||||
|
if (!clients_[i].in_use ||
|
||||||
|
clients_[i].next_pump_us >= earliest_pump_us) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
earliest_pump_us = clients_[i].next_pump_us;
|
||||||
|
client_index = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client_index != kMaximumClientCount) {
|
||||||
|
client_callback = clients_[client_index].callback;
|
||||||
|
client_callback_arg = clients_[client_index].wrapped_callback_arg;
|
||||||
|
|
||||||
|
const double scalar = xe::Clock::guest_time_scalar();
|
||||||
|
const uint64_t min_us =
|
||||||
|
scalar > 0.0 ? static_cast<uint64_t>(kAudioPumpInterval / scalar)
|
||||||
|
: kAudioPumpInterval;
|
||||||
|
clients_[client_index].next_pump_us =
|
||||||
|
(earliest_pump_us > now ? earliest_pump_us : now) + min_us;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.first == threading::WaitResult::kSuccess &&
|
// No clients yet: park until one registers or we're told to stop.
|
||||||
result.second == kMaximumClientCount) {
|
if (client_index == kMaximumClientCount) {
|
||||||
// Shutdown event signaled.
|
xe::threading::Wait(pending_work_event_.get(), true);
|
||||||
if (paused_) {
|
if (paused_) {
|
||||||
pause_fence_.Signal();
|
pause_fence_.Signal();
|
||||||
threading::Wait(resume_event_.get(), false);
|
xe::threading::Wait(resume_event_.get(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Number of clients pumped
|
// Pace to kAudioIntervalSlack ahead of the deadline.
|
||||||
bool pumped = false;
|
const uint64_t wake_target_us = earliest_pump_us > kAudioIntervalSlack
|
||||||
if (result.first == xe::threading::WaitResult::kSuccess) {
|
? earliest_pump_us - kAudioIntervalSlack
|
||||||
auto index = result.second;
|
: 0;
|
||||||
|
if (wake_target_us > now) {
|
||||||
auto global_lock = global_critical_region_.Acquire();
|
const std::chrono::milliseconds timeout((wake_target_us - now) / 1000);
|
||||||
uint32_t client_callback = clients_[index].callback;
|
auto result =
|
||||||
uint32_t client_callback_arg = clients_[index].wrapped_callback_arg;
|
xe::threading::Wait(pending_work_event_.get(), true, timeout);
|
||||||
global_lock.unlock();
|
if (result == xe::threading::WaitResult::kSuccess) {
|
||||||
|
if (paused_) {
|
||||||
if (client_callback) {
|
pause_fence_.Signal();
|
||||||
SCOPE_profile_cpu_i("apu", "xe::apu::AudioSystem->client_callback");
|
xe::threading::Wait(resume_event_.get(), false);
|
||||||
uint64_t args[] = {client_callback_arg};
|
}
|
||||||
processor_->Execute(worker_thread_->thread_state(), client_callback,
|
continue;
|
||||||
args, xe::countof(args));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pumped = true;
|
const uint64_t now_precise = static_cast<uint64_t>(
|
||||||
|
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch())
|
||||||
|
.count());
|
||||||
|
if (wake_target_us > now_precise) {
|
||||||
|
xe::threading::NanoSleepPrecise((wake_target_us - now_precise) * 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!worker_running_) {
|
// Submit only if the host has a free output slot;
|
||||||
break;
|
if (client_callback &&
|
||||||
}
|
xe::threading::Wait(client_semaphores_[client_index].get(), false,
|
||||||
|
std::chrono::milliseconds(0)) ==
|
||||||
if (!pumped) {
|
xe::threading::WaitResult::kSuccess) {
|
||||||
SCOPE_profile_cpu_i("apu", "Sleep");
|
SCOPE_profile_cpu_i("apu", "xe::apu::AudioSystem->client_callback");
|
||||||
xe::threading::Sleep(std::chrono::milliseconds(500));
|
uint64_t args[] = {client_callback_arg};
|
||||||
|
processor_->Execute(worker_thread_->thread_state(), client_callback, args,
|
||||||
|
xe::countof(args));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
worker_running_ = false;
|
worker_running_ = false;
|
||||||
@@ -173,7 +202,7 @@ void AudioSystem::Initialize() {}
|
|||||||
|
|
||||||
void AudioSystem::Shutdown() {
|
void AudioSystem::Shutdown() {
|
||||||
worker_running_ = false;
|
worker_running_ = false;
|
||||||
shutdown_event_->Set();
|
pending_work_event_->Set();
|
||||||
if (worker_thread_) {
|
if (worker_thread_) {
|
||||||
worker_thread_->Wait(0, 0, 0, nullptr);
|
worker_thread_->Wait(0, 0, 0, nullptr);
|
||||||
worker_thread_.reset();
|
worker_thread_.reset();
|
||||||
@@ -207,7 +236,7 @@ X_STATUS AudioSystem::RegisterClient(uint32_t callback, uint32_t callback_arg,
|
|||||||
assert_true(index >= 0);
|
assert_true(index >= 0);
|
||||||
|
|
||||||
auto client_semaphore = client_semaphores_[index].get();
|
auto client_semaphore = client_semaphores_[index].get();
|
||||||
auto ret = client_semaphore->Release(queued_frames_, nullptr);
|
auto ret = client_semaphore->Release(kMaximumQueuedFrames, nullptr);
|
||||||
assert_true(ret);
|
assert_true(ret);
|
||||||
|
|
||||||
AudioDriver* driver;
|
AudioDriver* driver;
|
||||||
@@ -225,7 +254,16 @@ X_STATUS AudioSystem::RegisterClient(uint32_t callback, uint32_t callback_arg,
|
|||||||
uint32_t ptr = memory()->SystemHeapAlloc(0x4);
|
uint32_t ptr = memory()->SystemHeapAlloc(0x4);
|
||||||
xe::store_and_swap<uint32_t>(memory()->TranslateVirtual(ptr), callback_arg);
|
xe::store_and_swap<uint32_t>(memory()->TranslateVirtual(ptr), callback_arg);
|
||||||
|
|
||||||
clients_[index] = {driver, callback, callback_arg, ptr, true};
|
clients_[index] = {};
|
||||||
|
clients_[index].driver = driver;
|
||||||
|
clients_[index].callback = callback;
|
||||||
|
clients_[index].callback_arg = callback_arg;
|
||||||
|
clients_[index].wrapped_callback_arg = ptr;
|
||||||
|
clients_[index].in_use = true;
|
||||||
|
|
||||||
|
// Wake the worker so it re-scans and starts pacing this client immediately.
|
||||||
|
pending_work_event_->Set();
|
||||||
|
|
||||||
XELOGI("AudioSystem::RegisterClient: client {} registered successfully",
|
XELOGI("AudioSystem::RegisterClient: client {} registered successfully",
|
||||||
index);
|
index);
|
||||||
|
|
||||||
@@ -330,10 +368,11 @@ bool AudioSystem::Restore(ByteStream* stream) {
|
|||||||
client.callback_arg = stream->Read<uint32_t>();
|
client.callback_arg = stream->Read<uint32_t>();
|
||||||
client.wrapped_callback_arg = stream->Read<uint32_t>();
|
client.wrapped_callback_arg = stream->Read<uint32_t>();
|
||||||
|
|
||||||
|
client.next_pump_us = 0;
|
||||||
client.in_use = true;
|
client.in_use = true;
|
||||||
|
|
||||||
auto client_semaphore = client_semaphores_[id].get();
|
auto client_semaphore = client_semaphores_[id].get();
|
||||||
auto ret = client_semaphore->Release(queued_frames_, nullptr);
|
auto ret = client_semaphore->Release(kMaximumQueuedFrames, nullptr);
|
||||||
assert_true(ret);
|
assert_true(ret);
|
||||||
|
|
||||||
AudioDriver* driver = nullptr;
|
AudioDriver* driver = nullptr;
|
||||||
@@ -359,8 +398,7 @@ void AudioSystem::Pause() {
|
|||||||
}
|
}
|
||||||
paused_ = true;
|
paused_ = true;
|
||||||
|
|
||||||
// Kind of a hack, but it works.
|
pending_work_event_->Set();
|
||||||
shutdown_event_->Set();
|
|
||||||
pause_fence_.Wait();
|
pause_fence_.Wait();
|
||||||
|
|
||||||
xma_decoder_->Pause();
|
xma_decoder_->Pause();
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
#define XENIA_APU_AUDIO_SYSTEM_H_
|
#define XENIA_APU_AUDIO_SYSTEM_H_
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <queue>
|
|
||||||
|
|
||||||
#include "xenia/base/mutex.h"
|
#include "xenia/base/mutex.h"
|
||||||
#include "xenia/base/threading.h"
|
#include "xenia/base/threading.h"
|
||||||
@@ -30,10 +29,9 @@ class XmaDecoder;
|
|||||||
|
|
||||||
class AudioSystem {
|
class AudioSystem {
|
||||||
public:
|
public:
|
||||||
// TODO(gibbed): respect XAUDIO2_MAX_QUEUED_BUFFERS somehow (ie min(64,
|
|
||||||
// XAUDIO2_MAX_QUEUED_BUFFERS))
|
|
||||||
static constexpr size_t kMinimumQueuedFrames = 4;
|
|
||||||
static constexpr size_t kMaximumQueuedFrames = 64;
|
static constexpr size_t kMaximumQueuedFrames = 64;
|
||||||
|
static constexpr uint32_t kAudioPumpInterval = 5333u;
|
||||||
|
static constexpr uint32_t kAudioIntervalSlack = 400u;
|
||||||
|
|
||||||
virtual ~AudioSystem();
|
virtual ~AudioSystem();
|
||||||
|
|
||||||
@@ -78,7 +76,6 @@ class AudioSystem {
|
|||||||
Memory* memory_ = nullptr;
|
Memory* memory_ = nullptr;
|
||||||
cpu::Processor* processor_ = nullptr;
|
cpu::Processor* processor_ = nullptr;
|
||||||
std::unique_ptr<XmaDecoder> xma_decoder_;
|
std::unique_ptr<XmaDecoder> xma_decoder_;
|
||||||
uint32_t queued_frames_;
|
|
||||||
|
|
||||||
std::atomic<bool> worker_running_ = {false};
|
std::atomic<bool> worker_running_ = {false};
|
||||||
kernel::object_ref<kernel::XHostThread> worker_thread_;
|
kernel::object_ref<kernel::XHostThread> worker_thread_;
|
||||||
@@ -87,6 +84,7 @@ class AudioSystem {
|
|||||||
static constexpr size_t kMaximumClientCount = 8;
|
static constexpr size_t kMaximumClientCount = 8;
|
||||||
struct {
|
struct {
|
||||||
AudioDriver* driver;
|
AudioDriver* driver;
|
||||||
|
uint64_t next_pump_us;
|
||||||
uint32_t callback;
|
uint32_t callback;
|
||||||
uint32_t callback_arg;
|
uint32_t callback_arg;
|
||||||
uint32_t wrapped_callback_arg;
|
uint32_t wrapped_callback_arg;
|
||||||
@@ -98,8 +96,7 @@ class AudioSystem {
|
|||||||
std::unique_ptr<xe::threading::Semaphore>
|
std::unique_ptr<xe::threading::Semaphore>
|
||||||
client_semaphores_[kMaximumClientCount];
|
client_semaphores_[kMaximumClientCount];
|
||||||
// Event is always there in case we have no clients.
|
// Event is always there in case we have no clients.
|
||||||
std::unique_ptr<xe::threading::Event> shutdown_event_;
|
std::unique_ptr<xe::threading::Event> pending_work_event_;
|
||||||
xe::threading::WaitHandle* wait_handles_[kMaximumClientCount + 1];
|
|
||||||
|
|
||||||
bool paused_ = false;
|
bool paused_ = false;
|
||||||
threading::Fence pause_fence_;
|
threading::Fence pause_fence_;
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ void SyncMemory();
|
|||||||
// Sleeps the current thread for at least as long as the given duration.
|
// Sleeps the current thread for at least as long as the given duration.
|
||||||
void Sleep(std::chrono::microseconds duration);
|
void Sleep(std::chrono::microseconds duration);
|
||||||
void NanoSleep(int64_t ns);
|
void NanoSleep(int64_t ns);
|
||||||
|
// Like NanoSleep but trades a brief busy-wait tail for sub-millisecond
|
||||||
|
// precision. Use only where wake-up jitter would miss a frame budget; the
|
||||||
|
// spin costs CPU.
|
||||||
|
void NanoSleepPrecise(int64_t ns);
|
||||||
template <typename Rep, typename Period>
|
template <typename Rep, typename Period>
|
||||||
void Sleep(std::chrono::duration<Rep, Period> duration) {
|
void Sleep(std::chrono::duration<Rep, Period> duration) {
|
||||||
Sleep(std::chrono::duration_cast<std::chrono::microseconds>(duration));
|
Sleep(std::chrono::duration_cast<std::chrono::microseconds>(duration));
|
||||||
|
|||||||
@@ -166,6 +166,36 @@ void Sleep(std::chrono::microseconds duration) {
|
|||||||
|
|
||||||
void NanoSleep(int64_t duration) { Sleep(std::chrono::nanoseconds(duration)); }
|
void NanoSleep(int64_t duration) { Sleep(std::chrono::nanoseconds(duration)); }
|
||||||
|
|
||||||
|
void NanoSleepPrecise(int64_t ns) {
|
||||||
|
#if XE_PLATFORM_MAC
|
||||||
|
// Darwin's nanosleep can oversleep by 100-500us under load. Land precisely
|
||||||
|
// on the deadline by using mach_wait_until for the bulk of the wait and
|
||||||
|
// busy-waiting the last ~200us.
|
||||||
|
if (ns <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
static const mach_timebase_info_data_t tb = [] {
|
||||||
|
mach_timebase_info_data_t i;
|
||||||
|
mach_timebase_info(&i);
|
||||||
|
return i;
|
||||||
|
}();
|
||||||
|
constexpr uint64_t kSpinTailNs = 200'000;
|
||||||
|
const uint64_t deadline =
|
||||||
|
mach_absolute_time() +
|
||||||
|
static_cast<uint64_t>((static_cast<__uint128_t>(ns) * tb.denom) /
|
||||||
|
tb.numer);
|
||||||
|
const uint64_t spin_tail = static_cast<uint64_t>(
|
||||||
|
(static_cast<__uint128_t>(kSpinTailNs) * tb.denom) / tb.numer);
|
||||||
|
if (deadline > mach_absolute_time() + spin_tail) {
|
||||||
|
mach_wait_until(deadline - spin_tail);
|
||||||
|
}
|
||||||
|
while (mach_absolute_time() < deadline) {
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
NanoSleep(ns);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(bwrsandman) Implement by allowing alert interrupts from IO operations
|
// TODO(bwrsandman) Implement by allowing alert interrupts from IO operations
|
||||||
thread_local bool alertable_state_ = false;
|
thread_local bool alertable_state_ = false;
|
||||||
SleepResult AlertableSleep(std::chrono::microseconds duration) {
|
SleepResult AlertableSleep(std::chrono::microseconds duration) {
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ void Sleep(std::chrono::microseconds duration) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NanoSleepPrecise(int64_t ns) { NanoSleep(ns); }
|
||||||
|
|
||||||
SleepResult AlertableSleep(std::chrono::microseconds duration) {
|
SleepResult AlertableSleep(std::chrono::microseconds duration) {
|
||||||
if (SleepEx(static_cast<DWORD>(duration.count() / 1000), true) ==
|
if (SleepEx(static_cast<DWORD>(duration.count() / 1000), true) ==
|
||||||
WAIT_IO_COMPLETION) {
|
WAIT_IO_COMPLETION) {
|
||||||
|
|||||||
Reference in New Issue
Block a user