fix Skate 3 audio crackle
This commit is contained in:
parent
5d932d1cc8
commit
6e34b9a745
10 changed files with 889 additions and 66 deletions
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <stack>
|
||||
|
|
@ -35,6 +36,14 @@ class SDLAudioDriver : public AudioDriver {
|
|||
static void SDLCallback(void* userdata, SDL_AudioStream* stream, int additional_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;
|
||||
|
||||
SDL_AudioStream* sdl_stream_ = nullptr;
|
||||
|
|
@ -49,6 +58,13 @@ class SDLAudioDriver : public AudioDriver {
|
|||
std::queue<float*> frames_queued_ = {};
|
||||
std::stack<float*> frames_unused_ = {};
|
||||
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
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include <array>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <tuple>
|
||||
|
||||
#include <rex/kernel.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};
|
||||
|
||||
class XmaContext {
|
||||
|
|
@ -230,13 +237,20 @@ class XmaContext {
|
|||
|
||||
kPacketInfo GetPacketInfo(uint8_t* packet, uint32_t frame_offset);
|
||||
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,
|
||||
uint32_t current_input_packet_count);
|
||||
uint32_t GetNextPacketReadOffset(uint8_t* buffer, uint32_t next_packet_index,
|
||||
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);
|
||||
|
||||
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 UpdateLoopStatus(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 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;
|
||||
std::unique_ptr<rex::thread::Event> work_completion_event_;
|
||||
|
||||
|
|
@ -266,6 +289,10 @@ class XmaContext {
|
|||
AVCodecContext* av_context_ = 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)
|
||||
std::array<uint8_t, kBytesPerPacketData * 2> input_buffer_;
|
||||
// First byte contains bit offset information
|
||||
|
|
@ -280,6 +307,13 @@ class XmaContext {
|
|||
// Loop subframe precision state
|
||||
uint8_t loop_frame_output_limit_ = 0;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -199,11 +199,19 @@ struct FPSCRRegister {
|
|||
inline uint32_t getcsr() noexcept { return Platform::getcsr(); }
|
||||
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 {
|
||||
if (!vmx_mode) {
|
||||
fpu_csr = getcsr();
|
||||
Platform::InitHostExceptions(fpu_csr);
|
||||
fpu_csr &= ~FlushMask;
|
||||
fpu_csr |= FlushMask;
|
||||
if (vmx_csr == 0) {
|
||||
vmx_csr = FlushMask | Platform::GuestToHost[kRoundNearest];
|
||||
Platform::InitHostExceptions(vmx_csr);
|
||||
|
|
@ -216,7 +224,7 @@ struct FPSCRRegister {
|
|||
inline void storeFromGuest(uint32_t value) noexcept {
|
||||
fpu_csr &= ~RoundMaskVal;
|
||||
fpu_csr |= Platform::GuestToHost[value & kRoundMask];
|
||||
fpu_csr &= ~FlushMask;
|
||||
fpu_csr |= FlushMask;
|
||||
Platform::InitHostExceptions(fpu_csr);
|
||||
csr = fpu_csr;
|
||||
vmx_mode = false;
|
||||
|
|
@ -254,7 +262,7 @@ struct FPSCRRegister {
|
|||
inline void InitHost() noexcept {
|
||||
fpu_csr = getcsr();
|
||||
Platform::InitHostExceptions(fpu_csr);
|
||||
fpu_csr &= ~FlushMask;
|
||||
fpu_csr |= FlushMask;
|
||||
vmx_csr = FlushMask | Platform::GuestToHost[kRoundNearest];
|
||||
Platform::InitHostExceptions(vmx_csr);
|
||||
csr = fpu_csr;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
#include <simde/x86/avx.h>
|
||||
#include <simde/x86/avx2.h>
|
||||
#include <simde/x86/fma.h>
|
||||
#include <simde/x86/sse.h>
|
||||
#include <simde/x86/sse4.1.h>
|
||||
|
||||
|
|
|
|||
|
|
@ -22,17 +22,22 @@
|
|||
#include <rex/memory/ring_buffer.h>
|
||||
#include <rex/stream.h>
|
||||
#include <rex/string/buffer.h>
|
||||
#include <rex/platform/fpscr.h>
|
||||
#include <rex/system/thread_state.h>
|
||||
#include <rex/thread.h>
|
||||
#include <rex/cvar.h>
|
||||
|
||||
REXCVAR_DEFINE_INT32(
|
||||
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, "
|
||||
"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);
|
||||
|
||||
namespace {
|
||||
|
|
@ -92,7 +97,7 @@ AudioSystem::AudioSystem(runtime::FunctionDispatcher* function_dispatcher)
|
|||
|
||||
queued_frames_ = std::min(
|
||||
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) {
|
||||
client_semaphores_[i] = rex::thread::Semaphore::Create(0, queued_frames_);
|
||||
|
|
@ -189,6 +194,18 @@ void AudioSystem::WorkerThreadMain() {
|
|||
uint64_t args[] = {client_callback_arg};
|
||||
function_dispatcher_->Execute(worker_thread_->thread_state(), client_callback, 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) {
|
||||
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();
|
||||
assert_true(index < kMaximumClientCount);
|
||||
assert_true(clients_[index].driver != NULL);
|
||||
|
||||
(clients_[index].driver)->SubmitFrame(samples_ptr);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include <rex/assert.h>
|
||||
#include <rex/audio/conversion.h>
|
||||
|
|
@ -25,6 +27,30 @@
|
|||
|
||||
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 {
|
||||
|
||||
SDLAudioDriver::SDLAudioDriver(memory::Memory* memory, rex::thread::Semaphore* semaphore)
|
||||
|
|
@ -45,6 +71,12 @@ bool SDLAudioDriver::Initialize() {
|
|||
// Set app name for audio device identification
|
||||
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)) {
|
||||
REXAPU_ERROR("SDL_InitSubSystem(SDL_INIT_AUDIO) failed: {}", SDL_GetError());
|
||||
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);
|
||||
return;
|
||||
}
|
||||
// Grant credits deferred by the real-time pacer once enough time has passed.
|
||||
driver->ReleasePacedCredits(0);
|
||||
while (additional_amount > 0) {
|
||||
static uint32_t sdl_callback_count = 0;
|
||||
float* buffer = nullptr;
|
||||
|
|
@ -216,12 +250,44 @@ void SDLAudioDriver::SDLCallback(void* userdata, SDL_AudioStream* stream, int ad
|
|||
break;
|
||||
}
|
||||
|
||||
auto ret = driver->semaphore_->Release(1, nullptr);
|
||||
assert_true(ret);
|
||||
driver->ReleasePacedCredits(1);
|
||||
additional_amount -= len;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
|
|
@ -11,10 +11,12 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <tuple>
|
||||
|
||||
#include <rex/audio/xma/context.h>
|
||||
#include <rex/audio/xma/decoder.h>
|
||||
#include <rex/audio/xma/helpers.h>
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/dbg.h>
|
||||
#include <rex/logging.h>
|
||||
#include <rex/memory/ring_buffer.h>
|
||||
|
|
@ -36,6 +38,10 @@ extern "C" {
|
|||
// Credits for most of this code goes to:
|
||||
// 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 {
|
||||
|
||||
using stream::BitStream;
|
||||
|
|
@ -93,19 +99,35 @@ int XmaContext::Setup(uint32_t id, memory::Memory* memory, uint32_t guest_ptr) {
|
|||
}
|
||||
|
||||
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()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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());
|
||||
XMA_CONTEXT_DATA data(context_ptr);
|
||||
const XMA_CONTEXT_DATA initial_data = data;
|
||||
|
||||
if (!data.output_buffer_valid) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
memory::RingBuffer output_rb = PrepareOutputRingBuffer(&data);
|
||||
|
|
@ -113,7 +135,7 @@ bool XmaContext::Work() {
|
|||
// Consume-only context: no input, just drain remaining subframes.
|
||||
if (data.IsConsumeOnlyContext()) {
|
||||
if (current_frame_remaining_subframes_ == 0) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
Consume(&output_rb, &data);
|
||||
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_) {
|
||||
StoreContextMerged(data, initial_data, context_ptr);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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_1_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_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;
|
||||
loop_frame_output_limit_ = 0;
|
||||
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() {
|
||||
|
|
@ -265,25 +309,51 @@ uint32_t XmaContext::GetAmountOfBitsToRead(uint32_t remaining_stream_bits, uint3
|
|||
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,
|
||||
uint32_t current_input_packet_count) {
|
||||
if (next_packet_index < current_input_packet_count) {
|
||||
return memory()->TranslatePhysical(data->GetCurrentInputBufferAddress()) +
|
||||
next_packet_index * kBytesPerPacket;
|
||||
}
|
||||
|
||||
const uint8_t next_buffer_index = data->current_buffer ^ 1;
|
||||
if (!data->IsInputBufferValid(next_buffer_index)) {
|
||||
const kPacketHandle packet =
|
||||
GetPacketHandle(data, data->current_buffer, next_packet_index, current_input_packet_count);
|
||||
if (!packet.is_valid_) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const uint32_t next_buffer_address = data->GetInputBufferAddress(next_buffer_index);
|
||||
if (!next_buffer_address) {
|
||||
REXAPU_ERROR("XmaContext {}: Buffer marked valid but has null pointer!", id());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return memory()->TranslatePhysical(next_buffer_address);
|
||||
const uint32_t buffer_address =
|
||||
data->GetInputBufferAddress(static_cast<uint8_t>(packet.buffer_index_));
|
||||
return memory()->TranslatePhysical(buffer_address) + packet.packet_index_ * kBytesPerPacket;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
const uint32_t output_capacity = data->output_buffer_block_count * 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -378,27 +477,58 @@ kPacketInfo XmaContext::GetPacketInfo(uint8_t* packet, uint32_t frame_offset) {
|
|||
|
||||
void XmaContext::StoreContextMerged(const XMA_CONTEXT_DATA& data,
|
||||
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;
|
||||
fresh.output_buffer_write_offset = data.output_buffer_write_offset;
|
||||
const auto cas_update = [&](size_t dword_idx, uint32_t mask, uint32_t value) {
|
||||
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) {
|
||||
fresh.input_buffer_0_valid = 0;
|
||||
mask0 |= 1u << 20;
|
||||
}
|
||||
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) {
|
||||
fresh.output_buffer_valid = 0;
|
||||
cas_update(1, 0x80000000u, 0);
|
||||
}
|
||||
|
||||
fresh.input_buffer_read_offset = data.input_buffer_read_offset;
|
||||
fresh.error_status = data.error_status;
|
||||
fresh.current_buffer = data.current_buffer;
|
||||
fresh.output_buffer_read_offset = data.output_buffer_read_offset;
|
||||
// DWORD 2: input_buffer_read_offset (0-25) + error_status (26-30).
|
||||
cas_update(2, 0x7FFFFFFFu, d[2] & 0x7FFFFFFFu);
|
||||
|
||||
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) {
|
||||
|
|
@ -556,6 +686,12 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) {
|
|||
}
|
||||
|
||||
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);
|
||||
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.
|
||||
if (skip_count == 0xFF) {
|
||||
const uint32_t next_packet_index_skip = packet_index + 1;
|
||||
uint32_t next_input_offset =
|
||||
GetNextPacketReadOffset(current_input_buffer, packet_index + 1, current_input_packet_count);
|
||||
if (next_input_offset == kBitsPerPacketHeader) {
|
||||
GetNextPacketReadOffset(data, next_packet_index_skip, current_input_packet_count);
|
||||
if (next_packet_index_skip >= current_input_packet_count ||
|
||||
next_input_offset == kBitsPerPacketHeader) {
|
||||
SwapInputBuffer(data);
|
||||
}
|
||||
data->input_buffer_read_offset = next_input_offset;
|
||||
|
|
@ -673,10 +811,14 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) {
|
|||
}
|
||||
|
||||
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) {
|
||||
SwapInputBuffer(data);
|
||||
if (data->IsAnyInputBufferValid()) {
|
||||
next_input_offset = xma::GetPacketFrameOffset(
|
||||
memory()->TranslatePhysical(data->GetCurrentInputBufferAddress()));
|
||||
|
|
@ -690,6 +832,494 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) {
|
|||
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,
|
||||
uint8_t* output_buffer) {
|
||||
// Loop through every sample, convert and drop it into the output array.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
|
||||
*/
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include <rex/audio/xma/context.h>
|
||||
#include <rex/audio/xma/decoder.h>
|
||||
#include <rex/cvar.h>
|
||||
|
|
@ -17,6 +19,7 @@
|
|||
#include <rex/perf/counter.h>
|
||||
#include <rex/math.h>
|
||||
#include <rex/memory/ring_buffer.h>
|
||||
#include <rex/platform/fpscr.h>
|
||||
#include <rex/string/buffer.h>
|
||||
#include <rex/system/function_dispatcher.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_->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;
|
||||
}
|
||||
|
|
@ -159,8 +168,11 @@ void XmaDecoder::WorkerThreadMain() {
|
|||
if (did_work) {
|
||||
continue;
|
||||
}
|
||||
// No work done this iteration, block until signaled.
|
||||
rex::thread::Wait(work_event_.get(), false);
|
||||
// No work done this iteration. Contexts stay enabled until the game
|
||||
// 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!"
|
||||
// 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.
|
||||
uint32_t base_context_id = (r - XmaRegister::Context0Kick) * 32;
|
||||
uint32_t kicked_value = value;
|
||||
for (int i = 0; value && i < 32; ++i, value >>= 1) {
|
||||
if (value & 1) {
|
||||
uint32_t context_id = base_context_id + i;
|
||||
auto& context = contexts_[context_id];
|
||||
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();
|
||||
// Block until the worker finishes, so the game sees updated context data.
|
||||
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();
|
||||
}
|
||||
|
||||
if (decode_csr != guest_csr) {
|
||||
rex::platform::FPSCRPlatform::setcsr(guest_csr);
|
||||
}
|
||||
} else if (r >= XmaRegister::Context0Lock && r <= XmaRegister::Context9Lock) {
|
||||
// Context lock command.
|
||||
|
|
|
|||
|
|
@ -81,10 +81,13 @@ bool build_vmulfp128(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.println(
|
||||
"\tsimde_mm_store_ps({}.f32, simde_mm_add_ps("
|
||||
"simde_mm_mul_ps(simde_mm_load_ps({}.f32), simde_mm_load_ps({}.f32)), "
|
||||
"\tsimde_mm_store_ps({}.f32, simde_mm_fmadd_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[1]), ctx.v(ctx.insn.operands[2]),
|
||||
ctx.v(ctx.insn.operands[3]));
|
||||
|
|
@ -92,13 +95,17 @@ bool build_vmaddfp(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.println(
|
||||
"\tsimde_mm_store_ps({}.f32, simde_mm_sub_ps("
|
||||
"simde_mm_load_ps({}.f32), "
|
||||
"simde_mm_mul_ps(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[2]));
|
||||
"\tsimde_mm_store_ps({}.f32, simde_mm_fnmadd_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[1]), ctx.v(ctx.insn.operands[2]),
|
||||
ctx.v(ctx.insn.operands[3]));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -823,14 +823,19 @@ void XThread::SetPriority(int32_t increment) {
|
|||
auto kthread = guest_object<X_KTHREAD>();
|
||||
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;
|
||||
if (increment > 0x22) {
|
||||
if (increment >= 15) {
|
||||
target_priority = rex::thread::ThreadPriority::kHighest;
|
||||
} else if (increment > 0x11) {
|
||||
} else if (increment >= 1) {
|
||||
target_priority = rex::thread::ThreadPriority::kAboveNormal;
|
||||
} else if (increment < -0x22) {
|
||||
} else if (increment <= -15) {
|
||||
target_priority = rex::thread::ThreadPriority::kLowest;
|
||||
} else if (increment < -0x11) {
|
||||
} else if (increment <= -1) {
|
||||
target_priority = rex::thread::ThreadPriority::kBelowNormal;
|
||||
} else {
|
||||
target_priority = rex::thread::ThreadPriority::kNormal;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue