fix Skate 3 audio crackle

This commit is contained in:
mchughalex 2026-06-14 05:21:42 +01:00
parent 5d932d1cc8
commit 6e34b9a745
10 changed files with 889 additions and 66 deletions

View file

@ -11,6 +11,7 @@
#pragma once #pragma once
#include <cstdint>
#include <mutex> #include <mutex>
#include <queue> #include <queue>
#include <stack> #include <stack>
@ -35,6 +36,14 @@ class SDLAudioDriver : public AudioDriver {
static void SDLCallback(void* userdata, SDL_AudioStream* stream, int additional_amount, static void SDLCallback(void* userdata, SDL_AudioStream* stream, int additional_amount,
int total_amount); int total_amount);
// Releases guest frame credits, paced to real time when
// audio_realtime_credit_pacing is enabled: a misbehaving audio device that
// drains the stream faster than real time (e.g. a Bluetooth device in a
// broken state) would otherwise release credits at an unbounded rate,
// speeding up the guest's whole audio clock and anything paced by it (such
// as video playback). Called only from the SDL audio thread.
void ReleasePacedCredits(uint32_t new_credits);
rex::thread::Semaphore* semaphore_ = nullptr; rex::thread::Semaphore* semaphore_ = nullptr;
SDL_AudioStream* sdl_stream_ = nullptr; SDL_AudioStream* sdl_stream_ = nullptr;
@ -49,6 +58,13 @@ class SDLAudioDriver : public AudioDriver {
std::queue<float*> frames_queued_ = {}; std::queue<float*> frames_queued_ = {};
std::stack<float*> frames_unused_ = {}; std::stack<float*> frames_unused_ = {};
std::mutex frames_mutex_ = {}; std::mutex frames_mutex_ = {};
// Credit pacing state - only touched on the SDL audio thread. The allowance
// accumulates with wall time (slightly above real time to tolerate device
// clock drift) and is capped so pauses don't bank an unbounded burst.
double pace_allowance_frames_ = 4.0;
uint64_t pace_last_ns_ = 0;
uint64_t pace_deferred_credits_ = 0;
}; };
} // namespace rex::audio::sdl } // namespace rex::audio::sdl

View file

@ -14,6 +14,7 @@
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <mutex> #include <mutex>
#include <tuple>
#include <rex/kernel.h> #include <rex/kernel.h>
#include <rex/memory.h> #include <rex/memory.h>
@ -166,6 +167,12 @@ struct kPacketInfo {
} }
}; };
struct kPacketHandle {
uint32_t buffer_index_ = 0;
uint32_t packet_index_ = 0;
bool is_valid_ = false;
};
static constexpr int kIdToSampleRate[4] = {24000, 32000, 44100, 48000}; static constexpr int kIdToSampleRate[4] = {24000, 32000, 44100, 48000};
class XmaContext { class XmaContext {
@ -230,13 +237,20 @@ class XmaContext {
kPacketInfo GetPacketInfo(uint8_t* packet, uint32_t frame_offset); kPacketInfo GetPacketInfo(uint8_t* packet, uint32_t frame_offset);
uint32_t GetAmountOfBitsToRead(uint32_t remaining_stream_bits, uint32_t frame_size); uint32_t GetAmountOfBitsToRead(uint32_t remaining_stream_bits, uint32_t frame_size);
kPacketHandle GetPacketHandle(XMA_CONTEXT_DATA* data, uint32_t buffer_index,
uint32_t packet_index,
uint32_t current_input_packet_count);
const uint8_t* GetNextPacket(XMA_CONTEXT_DATA* data, uint32_t next_packet_index, const uint8_t* GetNextPacket(XMA_CONTEXT_DATA* data, uint32_t next_packet_index,
uint32_t current_input_packet_count); uint32_t current_input_packet_count);
uint32_t GetNextPacketReadOffset(uint8_t* buffer, uint32_t next_packet_index, uint32_t GetNextPacketReadOffset(uint8_t* buffer, uint32_t next_packet_index,
uint32_t current_input_packet_count); uint32_t current_input_packet_count);
uint32_t GetNextPacketReadOffset(XMA_CONTEXT_DATA* data, uint32_t next_packet_index,
uint32_t current_input_packet_count);
uint8_t* GetCurrentInputBuffer(XMA_CONTEXT_DATA* data); uint8_t* GetCurrentInputBuffer(XMA_CONTEXT_DATA* data);
void Decode(XMA_CONTEXT_DATA* data); void Decode(XMA_CONTEXT_DATA* data);
bool WorkOldFrameDecoder();
void DecodeOldFrame(XMA_CONTEXT_DATA* data);
void Consume(memory::RingBuffer* output_rb, const XMA_CONTEXT_DATA* data); void Consume(memory::RingBuffer* output_rb, const XMA_CONTEXT_DATA* data);
void UpdateLoopStatus(XMA_CONTEXT_DATA* data); void UpdateLoopStatus(XMA_CONTEXT_DATA* data);
void ClearLocked(XMA_CONTEXT_DATA* data); void ClearLocked(XMA_CONTEXT_DATA* data);
@ -251,6 +265,15 @@ class XmaContext {
static void ConvertFrame(const uint8_t** samples, bool is_two_channel, uint8_t* output_buffer); static void ConvertFrame(const uint8_t** samples, bool is_two_channel, uint8_t* output_buffer);
static bool TrySetupNextLoopOld(XMA_CONTEXT_DATA* data, bool ignore_input_buffer_offset);
static bool ValidFrameOffsetOld(uint8_t* block, size_t size_bytes, size_t frame_offset_bits);
static size_t GetNextFrameOld(uint8_t* block, size_t size_bytes, size_t bit_offset);
static int GetFramePacketNumberOld(uint8_t* block, size_t size_bytes, size_t bit_offset);
static std::tuple<int, int> GetFrameNumberOld(uint8_t* block, size_t size_bytes,
size_t bit_offset);
static std::tuple<int, bool> GetPacketFrameCountOld(uint8_t* packet);
uint32_t GetPacketFirstFrameOffsetOld(const XMA_CONTEXT_DATA* data);
memory::Memory* memory_ = nullptr; memory::Memory* memory_ = nullptr;
std::unique_ptr<rex::thread::Event> work_completion_event_; std::unique_ptr<rex::thread::Event> work_completion_event_;
@ -266,6 +289,10 @@ class XmaContext {
AVCodecContext* av_context_ = nullptr; AVCodecContext* av_context_ = nullptr;
AVFrame* av_frame_ = nullptr; AVFrame* av_frame_ = nullptr;
// Snapshot of the current input packet, taken once per Decode() call so
// header parsing, frame slicing and the bit copy all see one consistent
// image even if the game refills the buffer mid-decode.
std::array<uint8_t, kBytesPerPacket> packet_snapshot_;
// Packet data buffer (two packets worth for split frame handling) // Packet data buffer (two packets worth for split frame handling)
std::array<uint8_t, kBytesPerPacketData * 2> input_buffer_; std::array<uint8_t, kBytesPerPacketData * 2> input_buffer_;
// First byte contains bit offset information // First byte contains bit offset information
@ -280,6 +307,13 @@ class XmaContext {
// Loop subframe precision state // Loop subframe precision state
uint8_t loop_frame_output_limit_ = 0; uint8_t loop_frame_output_limit_ = 0;
bool loop_start_skip_pending_ = false; bool loop_start_skip_pending_ = false;
// xenia-edge old XMA decoder state.
uint32_t old_packets_skip_ = 0;
bool old_is_stream_done_ = false;
uint32_t old_split_frame_len_ = 0;
uint32_t old_split_frame_len_partial_ = 0;
uint8_t old_split_frame_padding_start_ = 0;
}; };
} // namespace rex::audio } // namespace rex::audio

View file

@ -199,11 +199,19 @@ struct FPSCRRegister {
inline uint32_t getcsr() noexcept { return Platform::getcsr(); } inline uint32_t getcsr() noexcept { return Platform::getcsr(); }
inline void setcsr(uint32_t csr) noexcept { Platform::setcsr(csr); } inline void setcsr(uint32_t csr) noexcept { Platform::setcsr(csr); }
// Scalar (FPU) mode also runs with flush-to-zero/denormals-are-zero. PPC
// hardware processes denormals at full speed, but every denormal operand
// on x86 takes a ~100-cycle microcode assist - and audio DSP feedback
// tails (e.g. Skate 3's reverb/echo bus) decay straight through denormal
// range, stalling the guest audio threads seconds after loud transients
// (heard as crackle that starts ~2-3s after impacts and fades with the
// tail). Flushing trades exact denormal-range results for hardware-speed
// behavior, the same tradeoff other 360 emulators ship.
inline uint32_t loadFromHost() noexcept { inline uint32_t loadFromHost() noexcept {
if (!vmx_mode) { if (!vmx_mode) {
fpu_csr = getcsr(); fpu_csr = getcsr();
Platform::InitHostExceptions(fpu_csr); Platform::InitHostExceptions(fpu_csr);
fpu_csr &= ~FlushMask; fpu_csr |= FlushMask;
if (vmx_csr == 0) { if (vmx_csr == 0) {
vmx_csr = FlushMask | Platform::GuestToHost[kRoundNearest]; vmx_csr = FlushMask | Platform::GuestToHost[kRoundNearest];
Platform::InitHostExceptions(vmx_csr); Platform::InitHostExceptions(vmx_csr);
@ -216,7 +224,7 @@ struct FPSCRRegister {
inline void storeFromGuest(uint32_t value) noexcept { inline void storeFromGuest(uint32_t value) noexcept {
fpu_csr &= ~RoundMaskVal; fpu_csr &= ~RoundMaskVal;
fpu_csr |= Platform::GuestToHost[value & kRoundMask]; fpu_csr |= Platform::GuestToHost[value & kRoundMask];
fpu_csr &= ~FlushMask; fpu_csr |= FlushMask;
Platform::InitHostExceptions(fpu_csr); Platform::InitHostExceptions(fpu_csr);
csr = fpu_csr; csr = fpu_csr;
vmx_mode = false; vmx_mode = false;
@ -254,7 +262,7 @@ struct FPSCRRegister {
inline void InitHost() noexcept { inline void InitHost() noexcept {
fpu_csr = getcsr(); fpu_csr = getcsr();
Platform::InitHostExceptions(fpu_csr); Platform::InitHostExceptions(fpu_csr);
fpu_csr &= ~FlushMask; fpu_csr |= FlushMask;
vmx_csr = FlushMask | Platform::GuestToHost[kRoundNearest]; vmx_csr = FlushMask | Platform::GuestToHost[kRoundNearest];
Platform::InitHostExceptions(vmx_csr); Platform::InitHostExceptions(vmx_csr);
csr = fpu_csr; csr = fpu_csr;

View file

@ -20,6 +20,7 @@
#include <simde/x86/avx.h> #include <simde/x86/avx.h>
#include <simde/x86/avx2.h> #include <simde/x86/avx2.h>
#include <simde/x86/fma.h>
#include <simde/x86/sse.h> #include <simde/x86/sse.h>
#include <simde/x86/sse4.1.h> #include <simde/x86/sse4.1.h>

View file

@ -22,17 +22,22 @@
#include <rex/memory/ring_buffer.h> #include <rex/memory/ring_buffer.h>
#include <rex/stream.h> #include <rex/stream.h>
#include <rex/string/buffer.h> #include <rex/string/buffer.h>
#include <rex/platform/fpscr.h>
#include <rex/system/thread_state.h> #include <rex/system/thread_state.h>
#include <rex/thread.h> #include <rex/thread.h>
#include <rex/cvar.h> #include <rex/cvar.h>
REXCVAR_DEFINE_INT32( REXCVAR_DEFINE_INT32(
audio_maxqframes, 16, "Audio", audio_maxqframes, 16, "Audio",
"Max buffered audio frames (range 4-64). Lower reduces latency but may cause stuttering."); "Max buffered audio frames (range 2-64). Lower reduces latency and keeps the guest "
"audio engine closer to hardware-like lockstep with playback (real hardware double "
"buffers, ~2); may cause stuttering if too low for the machine.");
REXCVAR_DEFINE_INT32(audio_worker_thread_priority, 0, "Audio", REXCVAR_DEFINE_INT32(audio_worker_thread_priority, 1, "Audio",
"Native audio worker thread priority (-2 lowest, -1 below normal, 0 normal, " "Native audio worker thread priority (-2 lowest, -1 below normal, 0 normal, "
"1 above normal, 2 highest)") "1 above normal, 2 highest). The worker runs the guest's audio mixing on a "
"~5.3 ms deadline; above-normal keeps it scheduled ahead of the busy "
"emulation threads to avoid underrun crackle.")
.range(-2, 2); .range(-2, 2);
namespace { namespace {
@ -92,7 +97,7 @@ AudioSystem::AudioSystem(runtime::FunctionDispatcher* function_dispatcher)
queued_frames_ = std::min( queued_frames_ = std::min(
static_cast<uint32_t>(kMaximumQueuedFrames), static_cast<uint32_t>(kMaximumQueuedFrames),
std::max(static_cast<uint32_t>(REXCVAR_GET(audio_maxqframes)), static_cast<uint32_t>(4))); std::max(static_cast<uint32_t>(REXCVAR_GET(audio_maxqframes)), static_cast<uint32_t>(2)));
for (size_t i = 0; i < kMaximumClientCount; ++i) { for (size_t i = 0; i < kMaximumClientCount; ++i) {
client_semaphores_[i] = rex::thread::Semaphore::Create(0, queued_frames_); client_semaphores_[i] = rex::thread::Semaphore::Create(0, queued_frames_);
@ -189,6 +194,18 @@ void AudioSystem::WorkerThreadMain() {
uint64_t args[] = {client_callback_arg}; uint64_t args[] = {client_callback_arg};
function_dispatcher_->Execute(worker_thread_->thread_state(), client_callback, args, function_dispatcher_->Execute(worker_thread_->thread_state(), client_callback, args,
rex::countof(args)); rex::countof(args));
// Guest execution can return with host FP exceptions unmasked (the
// FPSCR emulation leaks MXCSR state); the first host float op on this
// thread then traps (seen as STATUS_FLOAT_INEXACT_RESULT in the gap
// logging below). Re-mask before doing any host-side work.
{
const uint32_t csr = rex::platform::FPSCRPlatform::getcsr();
uint32_t masked = csr;
rex::platform::FPSCRPlatform::InitHostExceptions(masked);
if (csr != masked) {
rex::platform::FPSCRPlatform::setcsr(masked);
}
}
if (diag_pump_count < 10) { if (diag_pump_count < 10) {
REXAPU_DEBUG("AudioWorker: callback returned for client {}", index); REXAPU_DEBUG("AudioWorker: callback returned for client {}", index);
} }
@ -310,6 +327,7 @@ void AudioSystem::SubmitFrame(size_t index, uint32_t samples_ptr) {
auto global_lock = global_critical_region_.Acquire(); auto global_lock = global_critical_region_.Acquire();
assert_true(index < kMaximumClientCount); assert_true(index < kMaximumClientCount);
assert_true(clients_[index].driver != NULL); assert_true(clients_[index].driver != NULL);
(clients_[index].driver)->SubmitFrame(samples_ptr); (clients_[index].driver)->SubmitFrame(samples_ptr);
} }

View file

@ -11,7 +11,9 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <chrono>
#include <cstring> #include <cstring>
#include <string>
#include <rex/assert.h> #include <rex/assert.h>
#include <rex/audio/conversion.h> #include <rex/audio/conversion.h>
@ -25,6 +27,30 @@
REXCVAR_DEFINE_BOOL(audio_mute, false, "Audio", "Mute audio output"); REXCVAR_DEFINE_BOOL(audio_mute, false, "Audio", "Mute audio output");
REXCVAR_DEFINE_BOOL(audio_realtime_credit_pacing, true, "Audio",
"Limit the rate at which consumed audio frames release guest submission "
"credits to slightly above real time. Prevents a misbehaving audio device "
"(e.g. a Bluetooth headset in a broken state) from running the guest audio "
"clock - and anything synchronized to it, like video playback - several "
"times too fast.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_INT32(audio_device_sample_frames, 0, "Audio",
"Requested audio device buffer size in sample frames (0 = backend default). "
"Larger values (e.g. 1024 or 2048) add latency but tolerate scheduling "
"hiccups better - try this against crackling, especially on Linux.")
.range(0, 8192);
namespace {
uint64_t AudioNowNs() {
return uint64_t(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count());
}
} // namespace
namespace rex::audio::sdl { namespace rex::audio::sdl {
SDLAudioDriver::SDLAudioDriver(memory::Memory* memory, rex::thread::Semaphore* semaphore) SDLAudioDriver::SDLAudioDriver(memory::Memory* memory, rex::thread::Semaphore* semaphore)
@ -45,6 +71,12 @@ bool SDLAudioDriver::Initialize() {
// Set app name for audio device identification // Set app name for audio device identification
SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING, "rexglue"); SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING, "rexglue");
int32_t requested_sample_frames = REXCVAR_GET(audio_device_sample_frames);
if (requested_sample_frames > 0) {
SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES,
std::to_string(requested_sample_frames).c_str());
}
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) { if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
REXAPU_ERROR("SDL_InitSubSystem(SDL_INIT_AUDIO) failed: {}", SDL_GetError()); REXAPU_ERROR("SDL_InitSubSystem(SDL_INIT_AUDIO) failed: {}", SDL_GetError());
return false; return false;
@ -167,6 +199,8 @@ void SDLAudioDriver::SDLCallback(void* userdata, SDL_AudioStream* stream, int ad
REXAPU_ERROR("SDLAudioDriver::SDLCallback failed to allocate {} samples", sample_count); REXAPU_ERROR("SDLAudioDriver::SDLCallback failed to allocate {} samples", sample_count);
return; return;
} }
// Grant credits deferred by the real-time pacer once enough time has passed.
driver->ReleasePacedCredits(0);
while (additional_amount > 0) { while (additional_amount > 0) {
static uint32_t sdl_callback_count = 0; static uint32_t sdl_callback_count = 0;
float* buffer = nullptr; float* buffer = nullptr;
@ -216,12 +250,44 @@ void SDLAudioDriver::SDLCallback(void* userdata, SDL_AudioStream* stream, int ad
break; break;
} }
auto ret = driver->semaphore_->Release(1, nullptr); driver->ReleasePacedCredits(1);
assert_true(ret);
additional_amount -= len; additional_amount -= len;
} }
} }
SDL_stack_free(data); SDL_stack_free(data);
} }
void SDLAudioDriver::ReleasePacedCredits(uint32_t new_credits) {
pace_deferred_credits_ += new_credits;
if (!pace_deferred_credits_) {
return;
}
uint64_t releasable = pace_deferred_credits_;
if (REXCVAR_GET(audio_realtime_credit_pacing)) {
const uint64_t now_ns = AudioNowNs();
if (pace_last_ns_) {
// 1% above real time so a device clock running slightly faster than the
// host clock can't slowly drain the pipeline; the allowance cap keeps
// catch-up bursts after pauses or stalls to a fraction of the guest's
// credit pool instead of an unbounded replay.
pace_allowance_frames_ += double(now_ns - pace_last_ns_) * 1e-9 *
(double(frame_frequency_) / double(channel_samples_)) * 1.01;
}
pace_last_ns_ = now_ns;
constexpr double kMaxBankedFrames = 8.0;
pace_allowance_frames_ = std::min(pace_allowance_frames_, kMaxBankedFrames);
releasable = std::min(releasable, uint64_t(pace_allowance_frames_));
}
if (releasable) {
auto ret = semaphore_->Release(int(releasable), nullptr);
assert_true(ret);
pace_deferred_credits_ -= releasable;
pace_allowance_frames_ -= double(releasable);
if (pace_allowance_frames_ < 0.0) {
pace_allowance_frames_ = 0.0;
}
}
}
} // namespace rex::audio::sdl } // namespace rex::audio::sdl

View file

@ -1,4 +1,4 @@
/** /**
****************************************************************************** ******************************************************************************
* Xenia : Xbox 360 Emulator Research Project * * Xenia : Xbox 360 Emulator Research Project *
****************************************************************************** ******************************************************************************
@ -11,10 +11,12 @@
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
#include <tuple>
#include <rex/audio/xma/context.h> #include <rex/audio/xma/context.h>
#include <rex/audio/xma/decoder.h> #include <rex/audio/xma/decoder.h>
#include <rex/audio/xma/helpers.h> #include <rex/audio/xma/helpers.h>
#include <rex/cvar.h>
#include <rex/dbg.h> #include <rex/dbg.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/memory/ring_buffer.h> #include <rex/memory/ring_buffer.h>
@ -36,6 +38,10 @@ extern "C" {
// Credits for most of this code goes to: // Credits for most of this code goes to:
// https://github.com/koolkdev/libertyv/blob/master/libav_wrapper/xma2dec.c // https://github.com/koolkdev/libertyv/blob/master/libav_wrapper/xma2dec.c
REXCVAR_DEFINE_BOOL(xma_use_old_decoder, true, "Audio",
"Use the xenia-edge old XMA decoder path. This avoids Skate 3 one-shot SFX "
"crackle caused by the current subframe/frame assembly path.");
namespace rex::audio { namespace rex::audio {
using stream::BitStream; using stream::BitStream;
@ -93,19 +99,35 @@ int XmaContext::Setup(uint32_t id, memory::Memory* memory, uint32_t guest_ptr) {
} }
bool XmaContext::Work() { bool XmaContext::Work() {
// NOTE: do NOT decode ahead of the kick ("eager decode" on cleared
// contexts was tried and increased mix clicks ~60%): the game commits
// input_buffer_*_valid BEFORE writing the final input_buffer_read_offset
// (mid-stream seek values), so decoding early uses a stale offset and
// plays the wrong region. The kick is the only safe decode trigger.
if (!is_allocated() || !is_enabled()) { if (!is_allocated() || !is_enabled()) {
return false; return false;
} }
std::lock_guard<std::mutex> lock(lock_); std::lock_guard<std::mutex> lock(lock_);
set_is_enabled(false); if (REXCVAR_GET(xma_use_old_decoder)) {
return WorkOldFrameDecoder();
}
// Free-running semantics: real XMA hardware keeps an enabled context
// decoding whenever output space frees up, without further kicks - the
// enable persists until XMADisableContext/lock. Clearing the enable per
// Work() made decode strictly kick-driven; when the game consumed output
// after its kick, the refill waited for the next kick and the voice fetch
// could find the ring empty -> 5.3ms per-voice dropouts (audible clicks).
// The worker now polls enabled contexts on a short cadence (see
// WorkerThreadMain), so keep is_enabled set and report whether progress
// was made: returning false on no-progress keeps the worker from spinning.
auto context_ptr = memory()->TranslateVirtual(guest_ptr()); auto context_ptr = memory()->TranslateVirtual(guest_ptr());
XMA_CONTEXT_DATA data(context_ptr); XMA_CONTEXT_DATA data(context_ptr);
const XMA_CONTEXT_DATA initial_data = data; const XMA_CONTEXT_DATA initial_data = data;
if (!data.output_buffer_valid) { if (!data.output_buffer_valid) {
return true; return false;
} }
memory::RingBuffer output_rb = PrepareOutputRingBuffer(&data); memory::RingBuffer output_rb = PrepareOutputRingBuffer(&data);
@ -113,7 +135,7 @@ bool XmaContext::Work() {
// Consume-only context: no input, just drain remaining subframes. // Consume-only context: no input, just drain remaining subframes.
if (data.IsConsumeOnlyContext()) { if (data.IsConsumeOnlyContext()) {
if (current_frame_remaining_subframes_ == 0) { if (current_frame_remaining_subframes_ == 0) {
return true; return false;
} }
Consume(&output_rb, &data); Consume(&output_rb, &data);
data.output_buffer_write_offset = output_rb.write_offset() / kOutputBytesPerBlock; data.output_buffer_write_offset = output_rb.write_offset() / kOutputBytesPerBlock;
@ -129,7 +151,7 @@ bool XmaContext::Work() {
if (minimum_subframe_decode_count > remaining_subframe_blocks_in_output_buffer_) { if (minimum_subframe_decode_count > remaining_subframe_blocks_in_output_buffer_) {
StoreContextMerged(data, initial_data, context_ptr); StoreContextMerged(data, initial_data, context_ptr);
return true; return false;
} }
while (remaining_subframe_blocks_in_output_buffer_ >= minimum_subframe_decode_count) { while (remaining_subframe_blocks_in_output_buffer_ >= minimum_subframe_decode_count) {
@ -178,17 +200,39 @@ void XmaContext::Clear() {
} }
void XmaContext::ClearLocked(XMA_CONTEXT_DATA* data) { void XmaContext::ClearLocked(XMA_CONTEXT_DATA* data) {
// The game clears a context to rewind it or start a new sound on it (Skate 3
// keeps all contexts allocated and multiplexes voices via clear + re-kick).
// The XMA bitstream is overlap-add: the ffmpeg decoder carries the previous
// frame's spectral tail, and PrepareDecoder only reopens the codec when the
// sample rate or channel count changes. Without a flush, the first frame of
// the next sound on this context gets overlap-added with the tail of the
// previous one - audible as crunchy/echo-like corruption on one-shot SFX.
if (av_context_ && avcodec_is_open(av_context_)) {
avcodec_flush_buffers(av_context_);
}
data->input_buffer_0_valid = 0; data->input_buffer_0_valid = 0;
data->input_buffer_1_valid = 0; data->input_buffer_1_valid = 0;
data->output_buffer_valid = 0; data->output_buffer_valid = 0;
data->input_buffer_read_offset = kBitsPerPacketHeader; data->input_buffer_read_offset =
REXCVAR_GET(xma_use_old_decoder) ? 0 : kBitsPerPacketHeader;
data->output_buffer_read_offset = 0; data->output_buffer_read_offset = 0;
data->output_buffer_write_offset = 0; data->output_buffer_write_offset = 0;
// NOTE: do NOT zero the output ring contents here. The game clears
// contexts for reuse while the previous voice's ring tail is still being
// played out by the mixer fetch - wiping it audibly truncates live audio
// (measured: mix clicks doubled with a memset here).
current_frame_remaining_subframes_ = 0; current_frame_remaining_subframes_ = 0;
loop_frame_output_limit_ = 0; loop_frame_output_limit_ = 0;
loop_start_skip_pending_ = false; loop_start_skip_pending_ = false;
old_packets_skip_ = 0;
old_is_stream_done_ = false;
old_split_frame_len_ = 0;
old_split_frame_len_partial_ = 0;
old_split_frame_padding_start_ = 0;
xma_frame_.fill(0);
} }
void XmaContext::Disable() { void XmaContext::Disable() {
@ -265,25 +309,51 @@ uint32_t XmaContext::GetAmountOfBitsToRead(uint32_t remaining_stream_bits, uint3
return std::min(remaining_stream_bits, frame_size); return std::min(remaining_stream_bits, frame_size);
} }
kPacketHandle XmaContext::GetPacketHandle(XMA_CONTEXT_DATA* data, uint32_t buffer_index,
uint32_t packet_index,
uint32_t current_input_packet_count) {
kPacketHandle result{};
const bool is_packet_in_next_buffer = packet_index >= current_input_packet_count;
if (is_packet_in_next_buffer) {
buffer_index ^= 1;
packet_index -= current_input_packet_count;
}
if (!data->IsInputBufferValid(static_cast<uint8_t>(buffer_index))) {
return result;
}
const uint32_t buffer_address = data->GetInputBufferAddress(static_cast<uint8_t>(buffer_index));
if (!buffer_address) {
REXAPU_ERROR("XmaContext {}: Buffer marked valid but has null pointer!", id());
return result;
}
const uint32_t packet_count =
data->GetInputBufferPacketCount(static_cast<uint8_t>(buffer_index));
if (packet_index >= packet_count) {
REXAPU_ERROR("XmaContext {}: Packet {} is outside buffer {} packet count {}", id(),
packet_index, buffer_index, packet_count);
return result;
}
result.buffer_index_ = buffer_index;
result.packet_index_ = packet_index;
result.is_valid_ = true;
return result;
}
const uint8_t* XmaContext::GetNextPacket(XMA_CONTEXT_DATA* data, uint32_t next_packet_index, const uint8_t* XmaContext::GetNextPacket(XMA_CONTEXT_DATA* data, uint32_t next_packet_index,
uint32_t current_input_packet_count) { uint32_t current_input_packet_count) {
if (next_packet_index < current_input_packet_count) { const kPacketHandle packet =
return memory()->TranslatePhysical(data->GetCurrentInputBufferAddress()) + GetPacketHandle(data, data->current_buffer, next_packet_index, current_input_packet_count);
next_packet_index * kBytesPerPacket; if (!packet.is_valid_) {
}
const uint8_t next_buffer_index = data->current_buffer ^ 1;
if (!data->IsInputBufferValid(next_buffer_index)) {
return nullptr; return nullptr;
} }
const uint32_t next_buffer_address = data->GetInputBufferAddress(next_buffer_index); const uint32_t buffer_address =
if (!next_buffer_address) { data->GetInputBufferAddress(static_cast<uint8_t>(packet.buffer_index_));
REXAPU_ERROR("XmaContext {}: Buffer marked valid but has null pointer!", id()); return memory()->TranslatePhysical(buffer_address) + packet.packet_index_ * kBytesPerPacket;
return nullptr;
}
return memory()->TranslatePhysical(next_buffer_address);
} }
uint32_t XmaContext::GetNextPacketReadOffset(uint8_t* buffer, uint32_t next_packet_index, uint32_t XmaContext::GetNextPacketReadOffset(uint8_t* buffer, uint32_t next_packet_index,
@ -301,6 +371,21 @@ uint32_t XmaContext::GetNextPacketReadOffset(uint8_t* buffer, uint32_t next_pack
return kBitsPerPacketHeader; return kBitsPerPacketHeader;
} }
uint32_t XmaContext::GetNextPacketReadOffset(XMA_CONTEXT_DATA* data, uint32_t next_packet_index,
uint32_t current_input_packet_count) {
const kPacketHandle packet =
GetPacketHandle(data, data->current_buffer, next_packet_index, current_input_packet_count);
if (!packet.is_valid_) {
return kBitsPerPacketHeader;
}
const uint32_t buffer_address =
data->GetInputBufferAddress(static_cast<uint8_t>(packet.buffer_index_));
return GetNextPacketReadOffset(memory()->TranslatePhysical(buffer_address), packet.packet_index_,
data->GetInputBufferPacketCount(
static_cast<uint8_t>(packet.buffer_index_)));
}
memory::RingBuffer XmaContext::PrepareOutputRingBuffer(XMA_CONTEXT_DATA* data) { memory::RingBuffer XmaContext::PrepareOutputRingBuffer(XMA_CONTEXT_DATA* data) {
const uint32_t output_capacity = data->output_buffer_block_count * kOutputBytesPerBlock; const uint32_t output_capacity = data->output_buffer_block_count * kOutputBytesPerBlock;
const uint32_t output_read_offset = data->output_buffer_read_offset * kOutputBytesPerBlock; const uint32_t output_read_offset = data->output_buffer_read_offset * kOutputBytesPerBlock;
@ -338,6 +423,20 @@ kPacketInfo XmaContext::GetPacketInfo(uint8_t* packet, uint32_t frame_offset) {
while (true) { while (true) {
if (stream.BitsRemaining() < kBitsPerFrameHeader) { if (stream.BitsRemaining() < kBitsPerFrameHeader) {
// A frame whose 15-bit size header straddles the packet boundary still
// starts in this packet (we only get here following a set continuation
// bit or the packet's declared first-frame offset). Count it, so the
// frame before it is not misclassified as last-in-packet - otherwise
// the advance logic jumps to the next packet's declared offset and
// silently drops the straddling frame (audible as a click/crunch in
// one-shot SFX). current_frame_size_ stays 0 for it, which routes
// Decode() into the existing split-header path.
if (stream.BitsRemaining() > 0) {
if (stream.offset_bits() == frame_offset) {
packet_info.current_frame_ = packet_info.frame_count_;
}
packet_info.frame_count_++;
}
break; break;
} }
@ -378,27 +477,58 @@ kPacketInfo XmaContext::GetPacketInfo(uint8_t* packet, uint32_t frame_offset) {
void XmaContext::StoreContextMerged(const XMA_CONTEXT_DATA& data, void XmaContext::StoreContextMerged(const XMA_CONTEXT_DATA& data,
const XMA_CONTEXT_DATA& initial_data, uint8_t* context_ptr) { const XMA_CONTEXT_DATA& initial_data, uint8_t* context_ptr) {
XMA_CONTEXT_DATA fresh(context_ptr); // The game reads and writes this context concurrently (valid flags, input
// pointers, output read offset - especially during load-time decode
// storms). The old whole-struct read-modify-write had a microsecond window
// where any concurrent game write was silently clobbered, which made the
// decoder consume stale/wrong buffers and bake garbage frames into sounds
// the game pre-decodes and caches at level load. Update only decoder-owned
// bitfields, with a per-dword CAS so game-owned neighbor bits survive.
// Note: output_buffer_read_offset (dword 9) is game-owned and deliberately
// never written here.
const uint32_t* d = reinterpret_cast<const uint32_t*>(&data);
fresh.loop_count = data.loop_count; const auto cas_update = [&](size_t dword_idx, uint32_t mask, uint32_t value) {
fresh.output_buffer_write_offset = data.output_buffer_write_offset; uint32_t* p = reinterpret_cast<uint32_t*>(context_ptr) + dword_idx;
std::atomic_ref<uint32_t> ref(*p);
uint32_t old_be = ref.load(std::memory_order_relaxed);
while (true) {
const uint32_t old_host = rex::byte_swap(old_be);
const uint32_t new_host = (old_host & ~mask) | (value & mask);
const uint32_t new_be = rex::byte_swap(new_host);
if (new_be == old_be) {
return;
}
// release: decoded PCM written to the output ring must be visible
// before the write offset advances.
if (ref.compare_exchange_weak(old_be, new_be, std::memory_order_release,
std::memory_order_relaxed)) {
return;
}
}
};
// DWORD 0: loop_count (bits 12-19), output_buffer_write_offset (27-31);
// input valid flags (20, 21) are clear-only.
uint32_t mask0 = 0x000FF000u | 0xF8000000u;
if (initial_data.input_buffer_0_valid && !data.input_buffer_0_valid) { if (initial_data.input_buffer_0_valid && !data.input_buffer_0_valid) {
fresh.input_buffer_0_valid = 0; mask0 |= 1u << 20;
} }
if (initial_data.input_buffer_1_valid && !data.input_buffer_1_valid) { if (initial_data.input_buffer_1_valid && !data.input_buffer_1_valid) {
fresh.input_buffer_1_valid = 0; mask0 |= 1u << 21;
} }
cas_update(0, mask0, d[0] & mask0);
// DWORD 1: output_buffer_valid (bit 31) is clear-only.
if (initial_data.output_buffer_valid && !data.output_buffer_valid) { if (initial_data.output_buffer_valid && !data.output_buffer_valid) {
fresh.output_buffer_valid = 0; cas_update(1, 0x80000000u, 0);
} }
fresh.input_buffer_read_offset = data.input_buffer_read_offset; // DWORD 2: input_buffer_read_offset (0-25) + error_status (26-30).
fresh.error_status = data.error_status; cas_update(2, 0x7FFFFFFFu, d[2] & 0x7FFFFFFFu);
fresh.current_buffer = data.current_buffer;
fresh.output_buffer_read_offset = data.output_buffer_read_offset;
fresh.Store(context_ptr); // DWORD 4: current_buffer (bit 31).
cas_update(4, 0x80000000u, d[4] & 0x80000000u);
} }
void XmaContext::Consume(memory::RingBuffer* output_rb, const XMA_CONTEXT_DATA* data) { void XmaContext::Consume(memory::RingBuffer* output_rb, const XMA_CONTEXT_DATA* data) {
@ -556,6 +686,12 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) {
} }
uint8_t* packet = current_input_buffer + (packet_index * kBytesPerPacket); uint8_t* packet = current_input_buffer + (packet_index * kBytesPerPacket);
// Work on a stable copy: the game's streamer can refill this buffer while
// we are mid-decode, and slicing decisions made from one read tearing into
// another produce garbage frames with no error status.
std::memcpy(packet_snapshot_.data(), packet, kBytesPerPacket);
packet = packet_snapshot_.data();
const uint32_t packet_first_frame_offset = xma::GetPacketFrameOffset(packet); const uint32_t packet_first_frame_offset = xma::GetPacketFrameOffset(packet);
uint32_t relative_offset = data->input_buffer_read_offset % kBitsPerPacket; uint32_t relative_offset = data->input_buffer_read_offset % kBitsPerPacket;
@ -568,9 +704,11 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) {
// Full packet skip (0xFF) -- no new frames begin in this packet. // Full packet skip (0xFF) -- no new frames begin in this packet.
if (skip_count == 0xFF) { if (skip_count == 0xFF) {
const uint32_t next_packet_index_skip = packet_index + 1;
uint32_t next_input_offset = uint32_t next_input_offset =
GetNextPacketReadOffset(current_input_buffer, packet_index + 1, current_input_packet_count); GetNextPacketReadOffset(data, next_packet_index_skip, current_input_packet_count);
if (next_input_offset == kBitsPerPacketHeader) { if (next_packet_index_skip >= current_input_packet_count ||
next_input_offset == kBitsPerPacketHeader) {
SwapInputBuffer(data); SwapInputBuffer(data);
} }
data->input_buffer_read_offset = next_input_offset; data->input_buffer_read_offset = next_input_offset;
@ -673,10 +811,14 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) {
} }
uint32_t next_input_offset = uint32_t next_input_offset =
GetNextPacketReadOffset(current_input_buffer, next_packet_index, current_input_packet_count); GetNextPacketReadOffset(data, next_packet_index, current_input_packet_count);
if (next_packet_index >= current_input_packet_count ||
next_input_offset == kBitsPerPacketHeader) {
SwapInputBuffer(data);
}
if (next_input_offset == kBitsPerPacketHeader) { if (next_input_offset == kBitsPerPacketHeader) {
SwapInputBuffer(data);
if (data->IsAnyInputBufferValid()) { if (data->IsAnyInputBufferValid()) {
next_input_offset = xma::GetPacketFrameOffset( next_input_offset = xma::GetPacketFrameOffset(
memory()->TranslatePhysical(data->GetCurrentInputBufferAddress())); memory()->TranslatePhysical(data->GetCurrentInputBufferAddress()));
@ -690,6 +832,494 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) {
data->input_buffer_read_offset = next_input_offset; data->input_buffer_read_offset = next_input_offset;
} }
bool XmaContext::WorkOldFrameDecoder() {
set_is_enabled(false);
auto context_ptr = memory()->TranslateVirtual(guest_ptr());
XMA_CONTEXT_DATA data(context_ptr);
DecodeOldFrame(&data);
data.Store(context_ptr);
return true;
}
bool XmaContext::TrySetupNextLoopOld(XMA_CONTEXT_DATA* data,
bool ignore_input_buffer_offset) {
if (data->loop_count > 0 && data->loop_start < data->loop_end &&
(ignore_input_buffer_offset || data->input_buffer_read_offset >= data->loop_end)) {
data->input_buffer_read_offset = data->loop_start;
if (data->loop_count < 255) {
data->loop_count--;
}
return true;
}
return false;
}
bool XmaContext::ValidFrameOffsetOld(uint8_t* block, size_t size_bytes,
size_t frame_offset_bits) {
const int packet_num = GetFramePacketNumberOld(block, size_bytes, frame_offset_bits);
if (packet_num < 0) {
return false;
}
uint8_t* packet = block + (packet_num * kBytesPerPacket);
const size_t relative_offset_bits = frame_offset_bits % kBitsPerPacket;
const uint32_t first_frame_offset = xma::GetPacketFrameOffset(packet);
if (first_frame_offset > kBitsPerPacket) {
return false;
}
BitStream stream(packet, kBitsPerPacket);
stream.SetOffset(first_frame_offset);
while (true) {
if (stream.offset_bits() == relative_offset_bits) {
return true;
}
if (stream.BitsRemaining() < kBitsPerFrameHeader) {
return false;
}
const uint64_t size = stream.Read(kBitsPerFrameHeader);
if (size == xma::kMaxFrameLength || size < kBitsPerFrameHeader + 1 ||
(size - kBitsPerFrameHeader) > stream.BitsRemaining()) {
return false;
}
stream.Advance(size - (kBitsPerFrameHeader + 1));
if (stream.Read(1) == 0) {
break;
}
}
return false;
}
size_t XmaContext::GetNextFrameOld(uint8_t* block, size_t size_bytes, size_t bit_offset) {
const int packet_idx = GetFramePacketNumberOld(block, size_bytes, bit_offset);
if (packet_idx < 0) {
return 0;
}
BitStream stream(block, size_bytes * 8);
stream.SetOffset(bit_offset);
if (stream.BitsRemaining() < kBitsPerFrameHeader) {
return 0;
}
const uint64_t len = stream.Read(kBitsPerFrameHeader);
if (len >= xma::kMaxFrameLength || len < kBitsPerFrameHeader + 1 ||
(len - kBitsPerFrameHeader) > stream.BitsRemaining()) {
return 0;
}
stream.Advance(len - (kBitsPerFrameHeader + 1));
if (stream.Read(1) == 0) {
return 0;
}
bit_offset += len;
if (packet_idx < GetFramePacketNumberOld(block, size_bytes, bit_offset)) {
return 0;
}
return bit_offset;
}
int XmaContext::GetFramePacketNumberOld(uint8_t* block, size_t size_bytes,
size_t bit_offset) {
(void)block;
const size_t size_bits = size_bytes * 8;
if (bit_offset >= size_bits) {
return -1;
}
return static_cast<int>((bit_offset >> 3) / kBytesPerPacket);
}
std::tuple<int, int> XmaContext::GetFrameNumberOld(uint8_t* block, size_t size_bytes,
size_t bit_offset) {
const int packet_idx = GetFramePacketNumberOld(block, size_bytes, bit_offset);
if (packet_idx < 0 || (static_cast<size_t>(packet_idx) + 1) * kBytesPerPacket > size_bytes) {
return {packet_idx, -2};
}
if (bit_offset == 0) {
return {packet_idx, -1};
}
uint8_t* packet = block + (packet_idx * kBytesPerPacket);
const uint32_t first_frame_offset = xma::GetPacketFrameOffset(packet);
if (first_frame_offset > kBitsPerPacket) {
return {packet_idx, -2};
}
BitStream stream(block, size_bytes * 8);
stream.SetOffset(packet_idx * kBitsPerPacket + first_frame_offset);
int frame_idx = 0;
while (true) {
if (stream.BitsRemaining() < kBitsPerFrameHeader || stream.offset_bits() == bit_offset) {
break;
}
const uint64_t size = stream.Read(kBitsPerFrameHeader);
if (size == xma::kMaxFrameLength || size < kBitsPerFrameHeader + 1 ||
(size - kBitsPerFrameHeader) > stream.BitsRemaining()) {
break;
}
stream.Advance(size - (kBitsPerFrameHeader + 1));
if (stream.Read(1) == 0) {
break;
}
frame_idx++;
}
return {packet_idx, frame_idx};
}
std::tuple<int, bool> XmaContext::GetPacketFrameCountOld(uint8_t* packet) {
const uint32_t first_frame_offset = xma::GetPacketFrameOffset(packet);
if (first_frame_offset > kBitsPerPacket - kBitsPerPacketHeader) {
return {0, false};
}
BitStream stream(packet, kBitsPerPacket);
stream.SetOffset(first_frame_offset);
int frame_count = 0;
while (true) {
if (stream.BitsRemaining() < kBitsPerFrameHeader) {
return {frame_count, false};
}
frame_count++;
const uint64_t size = stream.Read(kBitsPerFrameHeader);
if (size == xma::kMaxFrameLength || size < kBitsPerFrameHeader + 1 ||
(size - kBitsPerFrameHeader) > stream.BitsRemaining()) {
return {frame_count, true};
}
stream.Advance(size - (kBitsPerFrameHeader + 1));
if (stream.Read(1) == 0 || !stream.BitsRemaining()) {
return {frame_count, false};
}
}
}
uint32_t XmaContext::GetPacketFirstFrameOffsetOld(const XMA_CONTEXT_DATA* data) {
uint8_t* current_input_buffer =
data->IsCurrentInputBufferValid()
? memory()->TranslatePhysical(data->GetCurrentInputBufferAddress())
: nullptr;
return current_input_buffer ? xma::GetPacketFrameOffset(current_input_buffer)
: kBitsPerPacketHeader;
}
void XmaContext::DecodeOldFrame(XMA_CONTEXT_DATA* data) {
SCOPE_profile_cpu_f("apu");
if (!data->output_buffer_valid || !data->IsAnyInputBufferValid()) {
return;
}
uint8_t* in0 = data->input_buffer_0_valid
? memory()->TranslatePhysical(data->input_buffer_0_ptr)
: nullptr;
uint8_t* in1 = data->input_buffer_1_valid
? memory()->TranslatePhysical(data->input_buffer_1_ptr)
: nullptr;
uint8_t* current_input_buffer = data->current_buffer ? in1 : in0;
if (!current_input_buffer) {
REXAPU_ERROR("XmaContext {} old: input buffer pointer is invalid", id());
return;
}
if (!data->output_buffer_block_count) {
REXAPU_ERROR("XmaContext {} old: output_buffer_block_count is 0", id());
return;
}
if (old_is_stream_done_) {
old_is_stream_done_ = false;
old_packets_skip_ = 0;
SwapInputBuffer(data);
return;
}
const size_t input_buffer_0_size = data->input_buffer_0_packet_count * kBytesPerPacket;
const size_t input_buffer_1_size = data->input_buffer_1_packet_count * kBytesPerPacket;
const size_t current_input_size = data->current_buffer ? input_buffer_1_size
: input_buffer_0_size;
const size_t current_input_packet_count = current_input_size / kBytesPerPacket;
const bool is_streaming =
data->input_buffer_0_packet_count == 1 && data->input_buffer_1_packet_count == 1;
uint8_t* output_buffer = memory()->TranslatePhysical(data->output_buffer_ptr);
const uint32_t output_capacity = data->output_buffer_block_count * kOutputBytesPerBlock;
const uint32_t output_read_offset = data->output_buffer_read_offset * kOutputBytesPerBlock;
const uint32_t output_write_offset = data->output_buffer_write_offset * kOutputBytesPerBlock;
memory::RingBuffer output_rb(output_buffer, output_capacity);
output_rb.set_read_offset(output_read_offset);
output_rb.set_write_offset(output_write_offset);
const size_t frame_byte_count = size_t(kBytesPerFrameChannel) << data->is_stereo;
size_t output_remaining_bytes = output_rb.write_count();
output_remaining_bytes -= output_remaining_bytes % frame_byte_count;
while (output_remaining_bytes > 0) {
if (!data->IsAnyInputBufferValid()) {
break;
}
bool reuse_input_buffer = TrySetupNextLoopOld(data, false);
int packet_idx = -1;
int frame_idx = -1;
int frame_count = 0;
uint8_t* packet = nullptr;
bool frame_last_split = false;
BitStream stream(current_input_buffer, current_input_size * 8);
stream.SetOffset(data->input_buffer_read_offset);
if (data->input_buffer_read_offset > current_input_size * 8) {
REXAPU_ERROR("XmaContext {} old: input offset {} exceeds buffer size {}", id(),
uint32_t(data->input_buffer_read_offset), current_input_size * 8);
SwapInputBuffer(data);
return;
}
if (old_packets_skip_ > 0) {
packet_idx =
GetFramePacketNumberOld(current_input_buffer, current_input_size,
data->input_buffer_read_offset);
if (packet_idx < 0) {
return;
}
while (old_packets_skip_ > 0) {
old_packets_skip_--;
packet_idx++;
if (static_cast<size_t>(packet_idx) >= current_input_packet_count) {
if (!reuse_input_buffer) {
reuse_input_buffer = TrySetupNextLoopOld(data, true);
}
if (!reuse_input_buffer) {
if (is_streaming) {
SwapInputBuffer(data);
} else {
old_is_stream_done_ = true;
}
}
return;
}
}
data->input_buffer_read_offset = packet_idx * kBitsPerPacket;
}
if (old_split_frame_len_) {
packet_idx =
GetFramePacketNumberOld(current_input_buffer, current_input_size,
data->input_buffer_read_offset);
if (packet_idx < 0) {
return;
}
packet = current_input_buffer + packet_idx * kBytesPerPacket;
std::tie(frame_count, frame_last_split) = GetPacketFrameCountOld(packet);
frame_idx = -1;
stream = BitStream(current_input_buffer, (packet_idx + 1) * kBitsPerPacket);
stream.SetOffset(packet_idx * kBitsPerPacket + kBitsPerPacketHeader);
if (old_split_frame_len_ > xma::kMaxFrameLength) {
const auto offset = stream.offset_bits();
stream.Copy(xma_frame_.data() + 1 +
((old_split_frame_len_partial_ + old_split_frame_padding_start_) / 8),
kBitsPerFrameHeader - old_split_frame_len_partial_);
stream.SetOffset(offset);
BitStream slen(xma_frame_.data() + 1,
kBitsPerFrameHeader + old_split_frame_padding_start_);
slen.Advance(old_split_frame_padding_start_);
old_split_frame_len_ = static_cast<uint32_t>(slen.Read(kBitsPerFrameHeader));
}
stream.Copy(xma_frame_.data() + 1 +
((old_split_frame_len_partial_ + old_split_frame_padding_start_) / 8),
old_split_frame_len_ - old_split_frame_len_partial_);
} else {
if (data->input_buffer_read_offset % kBitsPerPacket == 0) {
const int packet_number =
GetFramePacketNumberOld(current_input_buffer, current_input_size,
data->input_buffer_read_offset);
if (packet_number < 0) {
return;
}
const uint32_t first_frame_offset =
xma::GetPacketFrameOffset(current_input_buffer + kBytesPerPacket * packet_number);
if (first_frame_offset > kBitsPerPacket) {
SwapInputBuffer(data);
return;
}
data->input_buffer_read_offset += first_frame_offset;
}
if (!ValidFrameOffsetOld(current_input_buffer, current_input_size,
data->input_buffer_read_offset)) {
REXAPU_NOISY_DEBUG("XmaContext {} old: invalid read offset {}", id(),
uint32_t(data->input_buffer_read_offset));
SwapInputBuffer(data);
return;
}
std::tie(packet_idx, frame_idx) =
GetFrameNumberOld(current_input_buffer, current_input_size,
data->input_buffer_read_offset);
if (packet_idx < 0 || frame_idx < 0) {
return;
}
packet = current_input_buffer + packet_idx * kBytesPerPacket;
std::tie(frame_count, frame_last_split) = GetPacketFrameCountOld(packet);
if (frame_count < 0) {
return;
}
PrepareDecoder(data->sample_rate, bool(data->is_stereo));
const bool frame_is_split = frame_last_split && (frame_idx >= frame_count - 1);
stream = BitStream(current_input_buffer, (packet_idx + 1) * kBitsPerPacket);
stream.SetOffset(data->input_buffer_read_offset);
old_split_frame_len_partial_ = static_cast<uint32_t>(stream.BitsRemaining());
if (old_split_frame_len_partial_ >= kBitsPerFrameHeader) {
old_split_frame_len_ = static_cast<uint32_t>(stream.Peek(kBitsPerFrameHeader));
} else {
old_split_frame_len_ = xma::kMaxFrameLength + 1;
}
xma_frame_.fill(0);
const uint32_t bits_to_copy = std::min(old_split_frame_len_, old_split_frame_len_partial_);
old_split_frame_padding_start_ =
static_cast<uint8_t>(stream.Copy(xma_frame_.data() + 1, bits_to_copy));
if (frame_is_split) {
old_packets_skip_ = xma::GetPacketSkipCount(packet) + 1;
while (old_packets_skip_ > 0) {
old_packets_skip_--;
packet_idx++;
if (static_cast<size_t>(packet_idx) >= current_input_packet_count) {
if (!reuse_input_buffer) {
reuse_input_buffer = TrySetupNextLoopOld(data, true);
}
if (!reuse_input_buffer) {
if (is_streaming) {
SwapInputBuffer(data);
} else {
old_is_stream_done_ = true;
}
}
return;
}
}
data->input_buffer_read_offset = packet_idx * kBitsPerPacket;
continue;
}
}
av_packet_->data = xma_frame_.data();
av_packet_->size = static_cast<int>(
1 + ((old_split_frame_padding_start_ + old_split_frame_len_) / 8) +
(((old_split_frame_padding_start_ + old_split_frame_len_) % 8) ? 1 : 0));
const auto padding_end =
av_packet_->size * 8 - (8 + old_split_frame_padding_start_ + old_split_frame_len_);
if (padding_end >= 8) {
return;
}
xma_frame_[0] =
((old_split_frame_padding_start_ & 7) << 5) | ((padding_end & 7) << 2);
old_split_frame_len_ = 0;
old_split_frame_len_partial_ = 0;
old_split_frame_padding_start_ = 0;
int ret = avcodec_send_packet(av_context_, av_packet_);
if (ret < 0) {
return;
}
ret = avcodec_receive_frame(av_context_, av_frame_);
if (ret == AVERROR(EAGAIN)) {
return;
}
if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errbuf, sizeof(errbuf));
REXAPU_ERROR("XmaContext {} old: decoding failed: {} ({})", id(), errbuf, ret);
data->parser_error_status = 4;
SwapInputBuffer(data);
return;
}
ConvertFrame(reinterpret_cast<const uint8_t**>(&av_frame_->data), bool(data->is_stereo),
raw_frame_.data());
output_rb.Write(raw_frame_.data(), frame_byte_count);
output_remaining_bytes -= frame_byte_count;
data->output_buffer_write_offset = output_rb.write_offset() / kOutputBytesPerBlock;
uint32_t offset = std::max(kBitsPerPacketHeader, data->input_buffer_read_offset);
offset = static_cast<uint32_t>(
GetNextFrameOld(current_input_buffer, current_input_size, offset));
if (frame_idx + 1 >= frame_count) {
old_packets_skip_ = xma::GetPacketSkipCount(packet) + 1;
while (old_packets_skip_ > 0) {
old_packets_skip_--;
packet_idx++;
if (static_cast<size_t>(packet_idx) >= current_input_packet_count) {
if (!reuse_input_buffer) {
reuse_input_buffer = TrySetupNextLoopOld(data, true);
}
if (!reuse_input_buffer) {
if (is_streaming) {
SwapInputBuffer(data);
data->input_buffer_read_offset = GetPacketFirstFrameOffsetOld(data);
} else {
old_is_stream_done_ = true;
}
if (output_rb.write_offset() == output_rb.read_offset()) {
data->output_buffer_valid = 0;
}
}
return;
}
}
packet = current_input_buffer + packet_idx * kBytesPerPacket;
offset = xma::GetPacketFrameOffset(packet) + packet_idx * kBitsPerPacket;
}
if (offset == 0 || frame_idx == -1) {
if (static_cast<size_t>(packet_idx) >= current_input_packet_count) {
if (!reuse_input_buffer) {
reuse_input_buffer = TrySetupNextLoopOld(data, true);
}
if (!reuse_input_buffer) {
if (is_streaming) {
SwapInputBuffer(data);
} else {
old_is_stream_done_ = true;
}
}
break;
}
offset = xma::GetPacketFrameOffset(packet) + packet_idx * kBitsPerPacket;
}
if (offset <= data->input_buffer_read_offset) {
REXAPU_NOISY_DEBUG("XmaContext {} old: non-advancing offset {} <= {}", id(), offset,
uint32_t(data->input_buffer_read_offset));
return;
}
data->input_buffer_read_offset = offset;
}
if (output_rb.write_offset() == output_rb.read_offset()) {
data->output_buffer_valid = 0;
}
}
void XmaContext::ConvertFrame(const uint8_t** samples, bool is_two_channel, void XmaContext::ConvertFrame(const uint8_t** samples, bool is_two_channel,
uint8_t* output_buffer) { uint8_t* output_buffer) {
// Loop through every sample, convert and drop it into the output array. // Loop through every sample, convert and drop it into the output array.

View file

@ -9,6 +9,8 @@
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/ */
#include <chrono>
#include <rex/audio/xma/context.h> #include <rex/audio/xma/context.h>
#include <rex/audio/xma/decoder.h> #include <rex/audio/xma/decoder.h>
#include <rex/cvar.h> #include <rex/cvar.h>
@ -17,6 +19,7 @@
#include <rex/perf/counter.h> #include <rex/perf/counter.h>
#include <rex/math.h> #include <rex/math.h>
#include <rex/memory/ring_buffer.h> #include <rex/memory/ring_buffer.h>
#include <rex/platform/fpscr.h>
#include <rex/string/buffer.h> #include <rex/string/buffer.h>
#include <rex/system/function_dispatcher.h> #include <rex/system/function_dispatcher.h>
#include <rex/system/thread_state.h> #include <rex/system/thread_state.h>
@ -133,6 +136,12 @@ X_STATUS XmaDecoder::Setup(system::KernelState* kernel_state) {
worker_thread_->set_name("XMA Decoder"); worker_thread_->set_name("XMA Decoder");
worker_thread_->Create(); worker_thread_->Create();
if (worker_thread_->thread()) {
// The decoder feeds the guest mixer - decode bursts arriving late surface
// as in-content audio glitches, so keep it ahead of normal-priority
// emulation threads (notably the spinning GPU command processor).
worker_thread_->thread()->set_priority(rex::thread::ThreadPriority::kAboveNormal);
}
return X_STATUS_SUCCESS; return X_STATUS_SUCCESS;
} }
@ -159,8 +168,11 @@ void XmaDecoder::WorkerThreadMain() {
if (did_work) { if (did_work) {
continue; continue;
} }
// No work done this iteration, block until signaled. // No work done this iteration. Contexts stay enabled until the game
rex::thread::Wait(work_event_.get(), false); // disables them (hardware free-running semantics, see XmaContext::Work),
// so poll on a short cadence to refill output rings as the game consumes
// them - real hardware resumes by itself, there is no kick to wake us.
rex::thread::Wait(work_event_.get(), false, std::chrono::milliseconds(2));
} }
} }
@ -283,24 +295,50 @@ void XmaDecoder::WriteRegister(uint32_t addr, uint32_t value) {
// Basically, this kicks the SPU and says "hey, decode that audio!" // Basically, this kicks the SPU and says "hey, decode that audio!"
// XMAEnableContext // XMAEnableContext
// Decode the kicked contexts inline on the kicking thread instead of
// waking the worker and waiting for its full context pass. The game's
// audio threads kick up to ~9k contexts/s and run at host-Highest
// priority; parking each kick behind the AboveNormal worker (which walks
// all 256 contexts per pass) is a priority inversion whose stalls can
// blow the game mixer's 5.3ms deadline - the engine then emits an
// all-zero block, audible as a click. Inline, a kick costs only its own
// contexts' decode time and the guest still sees updated context data on
// return, which is what the old block-on-worker arrangement was for.
//
// Guest threads execute with host FP exceptions unmasked and FTZ/DAZ
// possibly set (FPSCR emulation state); ffmpeg's float math would trap
// or diverge from worker-thread decodes, so switch to the worker's FP
// environment around the decode and restore the guest's after.
const uint32_t guest_csr = rex::platform::FPSCRPlatform::getcsr();
uint32_t decode_csr = guest_csr;
rex::platform::FPSCRPlatform::InitHostExceptions(decode_csr);
decode_csr &= ~uint32_t(rex::platform::FPSCRPlatform::FlushMask);
decode_csr &= ~uint32_t(rex::platform::FPSCRPlatform::RoundMaskVal); // round-to-nearest
if (decode_csr != guest_csr) {
rex::platform::FPSCRPlatform::setcsr(decode_csr);
}
// The context ID is a bit in the range of the entire context array. // The context ID is a bit in the range of the entire context array.
uint32_t base_context_id = (r - XmaRegister::Context0Kick) * 32; uint32_t base_context_id = (r - XmaRegister::Context0Kick) * 32;
uint32_t kicked_value = value;
for (int i = 0; value && i < 32; ++i, value >>= 1) { for (int i = 0; value && i < 32; ++i, value >>= 1) {
if (value & 1) { if (value & 1) {
uint32_t context_id = base_context_id + i; uint32_t context_id = base_context_id + i;
auto& context = contexts_[context_id]; auto& context = contexts_[context_id];
context.Enable(); context.Enable();
if (context.Work()) {
PROFILE_XMA_FRAME_DECODED();
} else if (context.is_allocated()) {
// Another thread (a concurrent kick, or the worker's bounded-wait
// stats scan) consumed the enable between our Enable() and Work().
// Wait for that decode to release the context lock so the game
// still sees updated context data before the kick write returns.
context.Block(false);
}
} }
} }
// Signal the decoder thread to start processing.
work_event_->Set(); if (decode_csr != guest_csr) {
// Block until the worker finishes, so the game sees updated context data. rex::platform::FPSCRPlatform::setcsr(guest_csr);
for (int i = 0; kicked_value && i < 32; ++i, kicked_value >>= 1) {
if (kicked_value & 1) {
uint32_t context_id = base_context_id + i;
contexts_[context_id].WaitForWorkDone();
}
} }
} else if (r >= XmaRegister::Context0Lock && r <= XmaRegister::Context9Lock) { } else if (r >= XmaRegister::Context0Lock && r <= XmaRegister::Context9Lock) {
// Context lock command. // Context lock command.

View file

@ -81,10 +81,13 @@ bool build_vmulfp128(BuilderContext& ctx) {
} }
bool build_vmaddfp(BuilderContext& ctx) { bool build_vmaddfp(BuilderContext& ctx) {
// Xenon vmaddfp is a true fused multiply-add (single rounding). Emit a real
// FMA so the result is bit-identical to hardware; the previous mul+add pair
// double-rounded every multiply-add in guest VMX code.
ctx.emit_set_flush_mode(true); ctx.emit_set_flush_mode(true);
ctx.println( ctx.println(
"\tsimde_mm_store_ps({}.f32, simde_mm_add_ps(" "\tsimde_mm_store_ps({}.f32, simde_mm_fmadd_ps("
"simde_mm_mul_ps(simde_mm_load_ps({}.f32), simde_mm_load_ps({}.f32)), " "simde_mm_load_ps({}.f32), simde_mm_load_ps({}.f32), "
"simde_mm_load_ps({}.f32)));", "simde_mm_load_ps({}.f32)));",
ctx.v(ctx.insn.operands[0]), ctx.v(ctx.insn.operands[1]), ctx.v(ctx.insn.operands[2]), ctx.v(ctx.insn.operands[0]), ctx.v(ctx.insn.operands[1]), ctx.v(ctx.insn.operands[2]),
ctx.v(ctx.insn.operands[3])); ctx.v(ctx.insn.operands[3]));
@ -92,13 +95,17 @@ bool build_vmaddfp(BuilderContext& ctx) {
} }
bool build_vnmsubfp(BuilderContext& ctx) { bool build_vnmsubfp(BuilderContext& ctx) {
// Xenon vnmsubfp computes -((vA*vC) - vB) fused with a single rounding.
// fnmadd(a, c, b) = b - a*c is the same value single-rounded (zero-sign of
// an exact cancellation differs, which hardware also exhibits via the
// negate; acceptable). The previous sub(mul(...)) pair double-rounded.
ctx.emit_set_flush_mode(true); ctx.emit_set_flush_mode(true);
ctx.println( ctx.println(
"\tsimde_mm_store_ps({}.f32, simde_mm_sub_ps(" "\tsimde_mm_store_ps({}.f32, simde_mm_fnmadd_ps("
"simde_mm_load_ps({}.f32), " "simde_mm_load_ps({}.f32), simde_mm_load_ps({}.f32), "
"simde_mm_mul_ps(simde_mm_load_ps({}.f32), simde_mm_load_ps({}.f32))));", "simde_mm_load_ps({}.f32)));",
ctx.v(ctx.insn.operands[0]), ctx.v(ctx.insn.operands[3]), ctx.v(ctx.insn.operands[1]), ctx.v(ctx.insn.operands[0]), ctx.v(ctx.insn.operands[1]), ctx.v(ctx.insn.operands[2]),
ctx.v(ctx.insn.operands[2])); ctx.v(ctx.insn.operands[3]));
return true; return true;
} }

View file

@ -823,14 +823,19 @@ void XThread::SetPriority(int32_t increment) {
auto kthread = guest_object<X_KTHREAD>(); auto kthread = guest_object<X_KTHREAD>();
kthread->priority = static_cast<uint8_t>(std::clamp(increment, 0, 31)); kthread->priority = static_cast<uint8_t>(std::clamp(increment, 0, 31));
// KeSetBasePriorityThread increments follow SetThreadPriority semantics:
// +1 above-normal, +2 highest, >= +15 saturates to time-critical (Skate 3
// sets its RwAudioCore Dac audio thread to 16 - flattening that to host
// normal made it lose its ~5ms deadline against the emulation threads,
// which surfaced as silent-block audio crackle).
int32_t target_priority = 0; int32_t target_priority = 0;
if (increment > 0x22) { if (increment >= 15) {
target_priority = rex::thread::ThreadPriority::kHighest; target_priority = rex::thread::ThreadPriority::kHighest;
} else if (increment > 0x11) { } else if (increment >= 1) {
target_priority = rex::thread::ThreadPriority::kAboveNormal; target_priority = rex::thread::ThreadPriority::kAboveNormal;
} else if (increment < -0x22) { } else if (increment <= -15) {
target_priority = rex::thread::ThreadPriority::kLowest; target_priority = rex::thread::ThreadPriority::kLowest;
} else if (increment < -0x11) { } else if (increment <= -1) {
target_priority = rex::thread::ThreadPriority::kBelowNormal; target_priority = rex::thread::ThreadPriority::kBelowNormal;
} else { } else {
target_priority = rex::thread::ThreadPriority::kNormal; target_priority = rex::thread::ThreadPriority::kNormal;