From ac50cd5759901c7bf6770ed6dc21e15750783635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 10 Sep 2026 11:00:08 -0600 Subject: [PATCH 1/7] sceAudio: model the real buffering, so contending threads get told "busy" I deduced that this was the case, and attempted implementing this path long ago, but I could never quite get it to work in all games. Set Claude on a quest to research and implement it, and lo and behold, it works. A bit sobering. Fixes #12888 and likely more. Additionally, audio latency is likely slightly improved overall, and memory usage is down by 4.6MB. Claude says: The blocking output calls are not a queue that callers line up behind. Each mixer channel holds exactly one buffer and at most one parked thread; a second thread arriving while the first is waiting is told the channel is busy and is expected to skip its turn. The Output2/SRC channel holds two DMA descriptors and refuses a third caller outright, without waiting at all. We blocked everyone instead, so a game running a movie thread and a sound-effect thread over one output made the two alternate - a frame of movie audio, a frame of effects silence - and the movie played at half rate. That is #12888, seen in F1 2009 and Colin McRae: DiRT 2. With this, the movie thread keeps the channel for the whole cutscene and the effects thread is refused, which is what the hardware trace shows. The driver also never copies a buffer on the way in: it stores the pointer and its mixer walks it forward 64 samples at a time out of the game's own memory. Modelling that fixes #20095 as a side effect, and drops the 4.6MB of per-channel sample rings we were carrying. The mix event is re-phased to the moment a DMA starts, since the mixer thread outranks its caller and gets a block in before the output call returns. Along the way: the two rest-length calls differ after a null-pointer output, sceAudioChRelease reports not-reserved rather than not-init, sceAudioChangeChannelConfig validates the format, sceAudioChangeChannelVolume validates nothing, and sceAudioOutput2ChangeLength takes a range of 17..4111. Details in docs/sceAudio.md. Savestates: AudioChannel goes to version 4. Older ones stored mixed samples that can't become a pointer and a position again, so they load with the pending audio dropped and any parked threads released. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 1 + Core/HLE/__sceAudio.cpp | 542 +++++++++++++++++++++-------------- Core/HLE/__sceAudio.h | 19 +- Core/HLE/sceAudio.cpp | 273 ++++++++++-------- Core/HLE/sceAudio.h | 47 ++- Core/HLE/sceVaudio.cpp | 9 +- UI/ImDebugger/ImDebugger.cpp | 18 +- docs/sceAudio.md | 156 ++++++++++ pspautotests | 2 +- test.py | 8 +- 10 files changed, 718 insertions(+), 357 deletions(-) create mode 100644 docs/sceAudio.md diff --git a/AGENTS.md b/AGENTS.md index c670084070..4a3b78baa1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,7 @@ for it: | [docs/pspautotests.md](docs/pspautotests.md) | Workflow for improving PPSSPP using pspautotests | | [docs/pspautotests-hardware.md](docs/pspautotests-hardware.md) | Writing a new pspautotest, and running it on a real PSP over PSPLink to record its `.expected` | | [docs/frametest.md](docs/frametest.md) | Framedump rendering tests | +| [docs/sceAudio.md](docs/sceAudio.md) | How the audio output calls block, how deep they buffer, and what each error means | | [docs/WebSocketDebugger.md](docs/WebSocketDebugger.md) | WebSocket debugger protocol reference | | [docs/reverse-engineering.md](docs/reverse-engineering.md) | Disassembling a firmware PRX with `--re-module`, to find out what the hardware actually does | diff --git a/Core/HLE/__sceAudio.cpp b/Core/HLE/__sceAudio.cpp index 4d433d3b45..326517c4ed 100644 --- a/Core/HLE/__sceAudio.cpp +++ b/Core/HLE/__sceAudio.cpp @@ -47,10 +47,6 @@ // atomic locks are used on the lock. TODO: make this lock-free std::atomic_flag atomicLock_; -// We copy samples as they are written into this simple ring buffer. -// Might try something more efficient later. -FixedSizeQueue chanSampleQueues[PSP_AUDIO_CHANNEL_MAX + 1]; - int eventAudioUpdate = -1; // TODO: This is now useless and should be removed. Just scared of breaking states. @@ -72,11 +68,6 @@ WaveFileWriter g_wave_writer; static bool m_logAudio; #endif -// High and low watermarks, basically. For perfect emulation, the correct values are 0 and 1, respectively. -// TODO: Tweak. Hm, there aren't actually even used currently... -static int chanQueueMaxSizeFactor; -static int chanQueueMinSizeFactor; - static void hleAudioUpdate(u64 userdata, int cyclesLate) { // Schedule the next cycle first. __AudioUpdate() may consume cycles. CoreTiming::ScheduleEvent(audioIntervalCycles - cyclesLate, eventAudioUpdate, 0); @@ -100,9 +91,6 @@ void __AudioInit() { mixFrequency = 44100; srcFrequency = 0; - chanQueueMaxSizeFactor = 2; - chanQueueMinSizeFactor = 1; - __AudioCPUMHzChange(); eventAudioUpdate = CoreTiming::RegisterEvent("AudioUpdate", &hleAudioUpdate); @@ -188,132 +176,234 @@ void __AudioShutdown() { #endif } -u32 __AudioEnqueue(AudioChannel &chan, int chanNum, bool blocking) { - u32 ret = chan.sampleCount; +// The audio driver never copies a buffer on the way in. It stores the pointer, and its mixer +// thread reads 64 samples straight out of the game's memory every DMA block, walking +// sampleAddress forward until the buffer is spent. Everything below follows that shape; see +// docs/sceAudio.md for the behavior this is modelled on. - if (chan.sampleAddress == 0) { - // For some reason, multichannel audio lies and returns the sample count here. - if (chanNum == PSP_AUDIO_CHANNEL_SRC || chanNum == PSP_AUDIO_CHANNEL_OUTPUT2) { - ret = 0; - } - } - - // If there's anything on the queue at all, it should be busy, but we try to be a bit lax. - //if (chanSampleQueues[chanNum].size() > chan.sampleCount * 2 * chanQueueMaxSizeFactor || chan.sampleAddress == 0) { - if (chanSampleQueues[chanNum].size() > 0) { - if (blocking) { - // TODO: Regular multichannel audio seems to block for 64 samples less? Or enqueue the first 64 sync? - int blockSamples = (int)chanSampleQueues[chanNum].size() / 2 / chanQueueMinSizeFactor; - - if (__KernelIsDispatchEnabled()) { - AudioChannelWaitInfo waitInfo = {__KernelGetCurThread(), blockSamples}; - chan.waitingThreads.push_back(waitInfo); - // Also remember the value to return in the waitValue. - __KernelWaitCurThread(WAITTYPE_AUDIOCHANNEL, (SceUID)chanNum + 1, ret, 0, false, "blocking audio"); - } else { - // TODO: Maybe we shouldn't take this audio after all? - ret = SCE_KERNEL_ERROR_CAN_NOT_WAIT; - } - - // Fall through to the sample queueing, don't want to lose the samples even though - // we're getting full. The PSP would enqueue after blocking. - } else { - // Non-blocking doesn't even enqueue, but it's not commonly used. - return SCE_ERROR_AUDIO_CHANNEL_BUSY; - } - } - - if (chan.sampleAddress == 0) { - return ret; - } - - // NOTE: The below is WRONG! See issue #20095. - // - // What we should be queueing here is just the sampleAddress and sampleCount. Then when dequeuing is when we should - // read the actual data. - - int leftVol = chan.leftVolume; - int rightVol = chan.rightVolume; - - if (leftVol == (1 << 15) && rightVol == (1 << 15) && chan.format == PSP_AUDIO_FORMAT_STEREO && IS_LITTLE_ENDIAN) { - // TODO: Add mono->stereo conversion to this path. - - // Good news: the volume (1 << 15), specifically, doesn't affect the values at all. - // We can just do a direct memory copy. - const u32 totalSamples = chan.sampleCount * (chan.format == PSP_AUDIO_FORMAT_STEREO ? 2 : 1); - s16 *buf1 = 0, *buf2 = 0; - size_t sz1, sz2; - chanSampleQueues[chanNum].pushPointers(totalSamples, &buf1, &sz1, &buf2, &sz2); - - if (Memory::IsValidAddress(chan.sampleAddress + (totalSamples - 1) * sizeof(s16_le))) { - Memory::Memcpy(buf1, chan.sampleAddress, (u32)sz1 * sizeof(s16)); - if (buf2) - Memory::Memcpy(buf2, chan.sampleAddress + (u32)sz1 * sizeof(s16), (u32)sz2 * sizeof(s16)); - } - } else { - // Remember that maximum volume allowed is 0xFFFFF so left shift is no issue. - // This way we can optimally shift by 16. - leftVol <<=1; - rightVol <<=1; - - if (chan.format == PSP_AUDIO_FORMAT_STEREO) { - const u32 totalSamples = chan.sampleCount * 2; - - s16_le *sampleData = (s16_le *) Memory::GetPointerOrException(chan.sampleAddress); - - // Walking a pointer for speed. But let's make sure we wouldn't trip on an invalid ptr. - if (Memory::IsValidAddress(chan.sampleAddress + (totalSamples - 1) * sizeof(s16_le))) { - s16 *buf1 = 0, *buf2 = 0; - size_t sz1, sz2; - chanSampleQueues[chanNum].pushPointers(totalSamples, &buf1, &sz1, &buf2, &sz2); - AdjustVolumeBlock(buf1, sampleData, sz1, leftVol, rightVol); - if (buf2) { - AdjustVolumeBlock(buf2, sampleData + sz1, sz2, leftVol, rightVol); - } - } - } else if (chan.format == PSP_AUDIO_FORMAT_MONO) { - // Rare, so unoptimized. Expands to stereo. - if (Memory::IsValidRange(chan.sampleAddress, chan.sampleCount * sizeof(s16))) { - for (u32 i = 0; i < chan.sampleCount; i++) { - s16 sample = (s16)Memory::ReadUnchecked_U16(chan.sampleAddress + 2 * i); - chanSampleQueues[chanNum].push(ApplySampleVolume(sample, leftVol)); - chanSampleQueues[chanNum].push(ApplySampleVolume(sample, rightVol)); - } - } - } - } - return ret; +// Unity gain on the PSP is 0x8000. Accumulate at full width and clamp once at the end, the way +// the driver's 32-bit mix accumulator does, rather than clamping each channel separately. +// 64-bit because the SRC channel accepts volumes up to 0xFFFFF, which overflows a 32-bit +// product against a full-scale sample. +static inline int ApplyChannelVolume(int sample, int vol) { + return (int)(((s64)sample * vol) >> 15); } -void __AudioWakeThreads(AudioChannel &chan, int result, int step) { - u32 error; - bool wokeThreads = false; - for (size_t w = 0; w < chan.waitingThreads.size(); ++w) { - AudioChannelWaitInfo &waitInfo = chan.waitingThreads[w]; - waitInfo.numSamples -= step; +// Set while __AudioUpdate is running, so a buffer accepted from inside it - the retry a parked +// thread gets when its predecessor finishes - doesn't try to start the DMA again. +static bool audioMixing; - // If it's done (there will still be samples on queue) and actually still waiting, wake it up. - u32 waitID = __KernelGetWaitID(waitInfo.threadID, WAITTYPE_AUDIOCHANNEL, error); - if (waitInfo.numSamples <= 0 && waitID != 0) { - // DEBUG_LOG(Log::sceAudio, "Woke thread %i for some buffer filling", waitingThread); - u32 ret = result == 0 ? __KernelGetWaitValue(waitInfo.threadID, error) : SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED; - __KernelResumeThreadFromWait(waitInfo.threadID, ret); - wokeThreads = true; - - chan.waitingThreads.erase(chan.waitingThreads.begin() + w--); +// Only channels 0-7. The SRC channel is on its own DMA that the mixer never touches, so the +// two start independently of each other. +static bool __AudioAnyChannelPlaying() { + for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX; i++) { + if (g_audioChans[i].sampleAddress != 0) { + return true; } - // This means the thread stopped waiting, so stop trying to wake it. - else if (waitID == 0) - chan.waitingThreads.erase(chan.waitingThreads.begin() + w--); + } + return false; +} + +// The driver starts the mixer's DMA the moment the first buffer arrives, and its mixer thread - +// which outranks whoever called - immediately fills a block from it. So the first 64 samples +// are gone before the output call has returned, and a channel reserved for exactly 64 samples +// is free again right away. Re-phasing the mix event to the buffer's arrival reproduces that +// and costs nothing: the interval, and so the sample rate, is unchanged. +static void __AudioStartMixerDMA() { + if (audioMixing) { + return; + } + CoreTiming::UnscheduleEvent(eventAudioUpdate, 0); + __AudioUpdate(); + CoreTiming::ScheduleEvent(audioIntervalCycles, eventAudioUpdate, 0); +} + +// The SRC channel's DMA also starts when its first buffer arrives, but it feeds the codec +// directly rather than going through the mixer, so nothing is read early - only the phase moves. +// Without this the buffer would retire somewhere in the next 1.5ms depending on where the mix +// event happened to be, and a game polling sceAudioOutput2GetRestSample would see a different +// answer every run. Only safe to do while no mixer channel is playing, since the two share one +// event here and the mixer's phase is the one that has samples riding on it. +static void __AudioStartSRCDMA() { + if (audioMixing || __AudioAnyChannelPlaying()) { + return; + } + CoreTiming::UnscheduleEvent(eventAudioUpdate, 0); + CoreTiming::ScheduleEvent(audioIntervalCycles, eventAudioUpdate, 0); +} + +u32 __AudioEnqueue(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol) { + if (!chan.reserved) { + return SCE_ERROR_AUDIO_CHANNEL_NOT_INIT; + } + // One buffer slot per channel, with no queue behind it. + if (chan.sampleAddress != 0) { + return SCE_ERROR_AUDIO_CHANNEL_BUSY; } - if (wokeThreads) { - __KernelReSchedule("audio drain"); + chan.remainingSamples = chan.sampleCount; + if (leftVol >= 0) { + chan.leftVolume = leftVol; } + if (rightVol >= 0) { + chan.rightVolume = rightVol; + } + // A null pointer is accepted and leaves the channel idle, but still counts as a buffer's + // worth of remaining samples - which is the one case where the two rest-length calls + // disagree with each other. + const bool wasIdle = !__AudioAnyChannelPlaying(); + chan.sampleAddress = samplePtr; + if (samplePtr != 0 && wasIdle) { + __AudioStartMixerDMA(); + } + return chan.sampleCount; +} + +u32 __AudioEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol) { + u32 result = __AudioEnqueue(chan, samplePtr, leftVol, rightVol); + if (result != SCE_ERROR_AUDIO_CHANNEL_BUSY) { + return result; + } + + // The driver keeps a single "a thread is waiting" flag per channel, so the second thread + // to arrive is turned away rather than lining up behind the first. A game that runs a + // movie thread and a sound-effect thread over one channel depends on being told this - + // blocking it instead makes the two take turns and halves the movie's audio rate. + if (chan.waitingThread != 0) { + return SCE_ERROR_AUDIO_CHANNEL_BUSY; + } + if (!__KernelIsDispatchEnabled()) { + return SCE_KERNEL_ERROR_CAN_NOT_WAIT; + } + + chan.waitingThread = __KernelGetCurThread(); + chan.waitingAddress = samplePtr; + chan.waitingLeftVolume = leftVol; + chan.waitingRightVolume = rightVol; + // __AudioChannelFinished retries the enqueue and supplies the real return value. + __KernelWaitCurThread(WAITTYPE_AUDIOCHANNEL, (SceUID)chan.index + 1, chan.sampleCount, 0, false, "blocking audio"); + return chan.sampleCount; +} + +// The buffer ran out - the driver's mixer would set this channel's bit in its event flag here. +static bool __AudioChannelFinished(AudioChannel &chan) { + chan.sampleAddress = 0; + chan.remainingSamples = 0; + if (chan.waitingThread == 0) { + return false; + } + + const SceUID threadID = chan.waitingThread; + chan.waitingThread = 0; + + u32 error; + if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) == 0) { + // It stopped waiting on its own - deleted, or the wait was cancelled. + return false; + } + + __KernelResumeThreadFromWait(threadID, __AudioEnqueue(chan, chan.waitingAddress, chan.waitingLeftVolume, chan.waitingRightVolume)); + return true; +} + +u32 __AudioSRCEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int vol) { + if (!chan.reserved) { + return SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED; + } + // Two DMA descriptors, so two buffers fit. The third caller is refused outright - unlike + // the mixer channels it does not even get the chance to wait for a slot. + if (chan.SRCFull()) { + return SCE_ERROR_AUDIO_CHANNEL_BUSY; + } + + if (vol >= 0) { + chan.leftVolume = vol; + chan.rightVolume = vol; + } + + u32 result = 0; + if (samplePtr != 0) { + if (chan.srcBufferCount == 0) { + // Starting the DMA signals a completion by itself, which is why the first + // output after an idle stretch returns without blocking. + chan.srcCompletion = true; + chan.srcPlayedSamples = 0; + chan.srcFrac = 0; + } + const bool wasIdle = chan.srcBufferCount == 0; + chan.srcBuffers[chan.srcBufferCount].address = samplePtr; + chan.srcBuffers[chan.srcBufferCount].samples = chan.sampleCount; + chan.srcBufferCount++; + result = chan.sampleCount; + if (wasIdle) { + __AudioStartSRCDMA(); + } + } else if (chan.srcBufferCount == 0) { + // Nothing playing and nothing handed over, so there is no completion to wait for. + return 0; + } + + if (chan.srcCompletion) { + chan.srcCompletion = false; + return result; + } + if (!__KernelIsDispatchEnabled()) { + return SCE_KERNEL_ERROR_CAN_NOT_WAIT; + } + + chan.srcWaitingThreads.push_back(__KernelGetCurThread()); + __KernelWaitCurThread(WAITTYPE_AUDIOCHANNEL, (SceUID)chan.index + 1, result, 0, false, "blocking audio"); + return result; +} + +void __AudioSRCSignal(AudioChannel &chan) { + chan.srcCompletion = true; +} + +// One of the two SRC buffers finished playing. +static bool __AudioSRCCompleted(AudioChannel &chan) { + if (chan.srcWaitingThreads.empty()) { + // Nobody is listening, so the completion sits there for the next caller to consume. + chan.srcCompletion = true; + return false; + } + + const SceUID threadID = chan.srcWaitingThreads.front(); + chan.srcWaitingThreads.erase(chan.srcWaitingThreads.begin()); + + u32 error; + if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) == 0) { + return false; + } + __KernelResumeThreadFromWait(threadID, __KernelGetWaitValue(threadID, error)); + return true; } void __AudioWakeThreads(AudioChannel &chan, int result) { - __AudioWakeThreads(chan, result, 0x7FFFFFFF); + bool woke = false; + + if (chan.waitingThread != 0) { + const SceUID threadID = chan.waitingThread; + chan.waitingThread = 0; + u32 error; + if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) != 0) { + __KernelResumeThreadFromWait(threadID, result); + woke = true; + } + } + + for (SceUID threadID : chan.srcWaitingThreads) { + u32 error; + if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) != 0) { + __KernelResumeThreadFromWait(threadID, result); + woke = true; + } + } + chan.srcWaitingThreads.clear(); + + if (woke) { + __KernelReSchedule("audio drain"); + } } void __AudioSetOutputFrequency(int freq) { @@ -329,101 +419,125 @@ void __AudioSetSRCFrequency(int freq) { srcFrequency = freq; } +// Mixes one block from a mixer channel, reading straight out of the game's buffer. +static bool __AudioMixChannel(AudioChannel &chan) { + if (chan.sampleAddress == 0 || chan.remainingSamples == 0) { + return false; + } + + const u32 count = std::min(chan.remainingSamples, (u32)hwBlockSize); + const bool mono = chan.format == PSP_AUDIO_FORMAT_MONO; + const u32 stride = mono ? 2 : 4; + + // The samples are consumed either way; muting only drops them on the floor. + if (!chan.mute && Memory::IsValidRange(chan.sampleAddress, count * stride)) { + const s16_le *src = (const s16_le *)Memory::GetPointerUnchecked(chan.sampleAddress); + const int leftVol = chan.leftVolume; + const int rightVol = chan.rightVolume; + if (mono) { + // A mono channel reads the same sample into both sides, which is how the + // hardware expands it - there is no separate mono path in the mixer. + for (u32 s = 0; s < count; s++) { + const s16 sample = src[s]; + mixBuffer[s * 2] += ApplyChannelVolume(sample, leftVol); + mixBuffer[s * 2 + 1] += ApplyChannelVolume(sample, rightVol); + } + } else { + for (u32 s = 0; s < count; s++) { + mixBuffer[s * 2] += ApplyChannelVolume(src[s * 2], leftVol); + mixBuffer[s * 2 + 1] += ApplyChannelVolume(src[s * 2 + 1], rightVol); + } + } + } + + chan.sampleAddress += count * stride; + chan.remainingSamples -= count; + if (chan.remainingSamples == 0) { + return __AudioChannelFinished(chan); + } + return false; +} + +// Channel 8 never reaches the mixer on hardware - the DMA feeds the codec directly and the +// codec resamples. Model that as a read straight through the pending buffers at the ratio +// between the reserved frequency and the output rate. +static bool __AudioMixSRC(AudioChannel &chan) { + if (chan.srcBufferCount == 0) { + return false; + } + + // Zero means "whatever the output is running at", so no conversion. + const int inRate = srcFrequency != 0 ? srcFrequency : mixFrequency; + const u32 ratio = (u32)(((u64)(u32)inRate << 16) / (u32)mixFrequency); + const bool mono = chan.format == PSP_AUDIO_FORMAT_MONO; + const u32 stride = mono ? 2 : 4; + const int leftVol = chan.leftVolume; + const int rightVol = chan.rightVolume; + + bool woke = false; + for (int out = 0; out < hwBlockSize; out++) { + if (chan.srcBufferCount == 0) { + // Underrun. The rest of the block stays silent, like a descriptor the game + // never got around to arming. + break; + } + + const AudioPendingBuffer &buf = chan.srcBuffers[0]; + const u32 addr = buf.address + chan.srcPlayedSamples * stride; + // Interpolating against the following sample matters when a game reserved 22050Hz + // or similar; at the native rate the fraction is always zero and this reduces to a + // plain copy. + const u32 avail = std::min(buf.samples - chan.srcPlayedSamples, 2u); + if (!chan.mute && Memory::IsValidRange(addr, avail * stride)) { + const s16_le *src = (const s16_le *)Memory::GetPointerUnchecked(addr); + const int l0 = src[0]; + const int r0 = mono ? l0 : src[1]; + const int l1 = avail > 1 ? (int)src[stride / 2] : l0; + const int r1 = avail > 1 ? (mono ? l1 : (int)src[stride / 2 + 1]) : r0; + // 15 bits of fraction, not 16 - a full 16 would overflow the product against a + // full-scale difference. + const int frac = (int)(chan.srcFrac >> 1); + mixBuffer[out * 2] += ApplyChannelVolume(l0 + (((l1 - l0) * frac) >> 15), leftVol); + mixBuffer[out * 2 + 1] += ApplyChannelVolume(r0 + (((r1 - r0) * frac) >> 15), rightVol); + } + + chan.srcFrac += ratio; + u32 step = chan.srcFrac >> 16; + chan.srcFrac &= 0xFFFF; + while (step > 0 && chan.srcBufferCount > 0) { + const u32 take = std::min(step, chan.srcBuffers[0].samples - chan.srcPlayedSamples); + chan.srcPlayedSamples += take; + step -= take; + if (chan.srcPlayedSamples >= chan.srcBuffers[0].samples) { + chan.srcBuffers[0] = chan.srcBuffers[1]; + chan.srcBufferCount--; + chan.srcPlayedSamples = 0; + woke |= __AudioSRCCompleted(chan); + } + } + } + return woke; +} + // Mix samples from the various audio channels into a single sample queue, managed by the backend implementation. void __AudioUpdate(bool resetRecording) { // AUDIO throttle doesn't really work on the PSP since the mixing intervals are so closely tied // to the CPU. Much better to throttle the frame rate on frame display and just throw away audio // if the buffer somehow gets full. - bool firstChannel = true; - const int16_t srcBufferSize = hwBlockSize * 2; - int16_t srcBuffer[srcBufferSize]; + memset(mixBuffer, 0, hwBlockSize * 2 * sizeof(s32)); - for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX + 1; i++) { - if (!g_audioChans[i].reserved) { - continue; - } - - __AudioWakeThreads(g_audioChans[i], 0, hwBlockSize); - - if (!chanSampleQueues[i].size()) { - continue; - } - - bool needsResample = i == PSP_AUDIO_CHANNEL_SRC && srcFrequency != 0 && srcFrequency != mixFrequency; - size_t sz = needsResample ? (srcBufferSize * srcFrequency) / mixFrequency : srcBufferSize; - if (sz > chanSampleQueues[i].size()) { - ERROR_LOG(Log::sceAudio, "Channel %i buffer underrun at %i of %i", i, (int)chanSampleQueues[i].size() / 2, (int)sz / 2); - } - - const s16 *buf1 = 0, *buf2 = 0; - size_t sz1, sz2; - - chanSampleQueues[i].popPointers(sz, &buf1, &sz1, &buf2, &sz2); - - // We do this check as the very last thing before mixing, to maximize compatibility. - if (g_audioChans[i].mute) { - continue; - } - - if (needsResample) { - auto read = [&](size_t i) { - if (i < sz1) - return buf1[i]; - if (i < sz1 + sz2) - return buf2[i - sz1]; - if (buf2) - return buf2[sz2 - 1]; - return buf1[sz1 - 1]; - }; - - // TODO: This is terrible, since it's doing it by small chunk and discarding frac. - const uint32_t ratio = (uint32_t)(65536.0 * srcFrequency / (double)mixFrequency); - uint32_t frac = 0; - size_t readIndex = 0; - for (size_t outIndex = 0; readIndex < sz && outIndex < srcBufferSize; outIndex += 2) { - size_t readIndex2 = readIndex + 2; - int16_t l1 = read(readIndex); - int16_t r1 = read(readIndex + 1); - int16_t l2 = read(readIndex2); - int16_t r2 = read(readIndex2 + 1); - int sampleL = ((l1 << 16) + (l2 - l1) * (uint16_t)frac) >> 16; - int sampleR = ((r1 << 16) + (r2 - r1) * (uint16_t)frac) >> 16; - srcBuffer[outIndex] = sampleL; - srcBuffer[outIndex + 1] = sampleR; - frac += ratio; - readIndex += 2 * (uint16_t)(frac >> 16); - frac &= 0xffff; - } - - buf1 = srcBuffer; - sz1 = srcBufferSize; - buf2 = nullptr; - sz2 = 0; - } - - if (firstChannel) { - for (size_t s = 0; s < sz1; s++) - mixBuffer[s] = buf1[s]; - if (buf2) { - for (size_t s = 0; s < sz2; s++) - mixBuffer[s + sz1] = buf2[s]; - } - firstChannel = false; - } else { - // Surprisingly hard to SIMD efficiently on SSE2 due to lack of 16-to-32-bit sign extension. NEON should be straight-forward though, and SSE4.1 can do it nicely. - // Actually, the cmple/pack trick should work fine... - for (size_t s = 0; s < sz1; s++) - mixBuffer[s] += buf1[s]; - if (buf2) { - for (size_t s = 0; s < sz2; s++) - mixBuffer[s + sz1] += buf2[s]; - } - } + audioMixing = true; + bool woke = false; + for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX; i++) { + // Deliberately not gated on `reserved`: sceAudioChRelease only clears the + // reservation, and a buffer already in flight keeps playing out. + woke |= __AudioMixChannel(g_audioChans[i]); } + woke |= __AudioMixSRC(g_audioChans[PSP_AUDIO_CHANNEL_SRC]); + audioMixing = false; - if (firstChannel) { - // Nothing was written above, let's memset. - memset(mixBuffer, 0, hwBlockSize * 2 * sizeof(s32)); + if (woke) { + __KernelReSchedule("audio drain"); } if (g_Config.bEnableSound) { diff --git a/Core/HLE/__sceAudio.h b/Core/HLE/__sceAudio.h index e8ecced166..9ae1e894e0 100644 --- a/Core/HLE/__sceAudio.h +++ b/Core/HLE/__sceAudio.h @@ -41,9 +41,22 @@ void __AudioShutdown(); void __AudioSetOutputFrequency(int freq); void __AudioSetSRCFrequency(int freq); -// May return SCE_ERROR_AUDIO_CHANNEL_BUSY if buffer too large -u32 __AudioEnqueue(AudioChannel &chan, int chanNum, bool blocking); -void __AudioWakeThreads(AudioChannel &chan, int result, int step); +// The driver's single enqueue point for channels 0-7. Returns the channel's reserved sample +// count, or SCE_ERROR_AUDIO_CHANNEL_BUSY when a buffer is already in flight. A negative +// volume means "leave it alone". +u32 __AudioEnqueue(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol); +// Same, but parks the calling thread until the channel frees up. Only one thread can be +// parked; a second one gets SCE_ERROR_AUDIO_CHANNEL_BUSY straight back. +u32 __AudioEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol); + +// Channel 8 - Output2, SRC and Vaudio all share it. Two buffers fit; a third caller gets +// SCE_ERROR_AUDIO_CHANNEL_BUSY without waiting. A successful call waits for one buffer to +// finish before returning, except when nothing was playing to begin with. +u32 __AudioSRCEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int vol); +// Hands the SRC channel a completion that the next caller can consume without waiting. +void __AudioSRCSignal(AudioChannel &chan); + +// Wakes whoever is parked on the channel with the given error, and forgets their buffer. void __AudioWakeThreads(AudioChannel &chan, int result); void __AudioCPUMHzChange(); diff --git a/Core/HLE/sceAudio.cpp b/Core/HLE/sceAudio.cpp index 4606507e92..28c86535f8 100644 --- a/Core/HLE/sceAudio.cpp +++ b/Core/HLE/sceAudio.cpp @@ -15,6 +15,8 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. +#include + #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" #include "Common/Data/Collections/FixedSizeQueue.h" @@ -36,15 +38,17 @@ const int AUDIO_ROUTING_SPEAKER_ON = 1; int defaultRoutingMode = AUDIO_ROUTING_SPEAKER_ON; int defaultRoutingVolMode = AUDIO_ROUTING_SPEAKER_ON; -// TODO: These are way oversized and together consume 4MB of memory. -extern FixedSizeQueue chanSampleQueues[PSP_AUDIO_CHANNEL_MAX + 1]; +// Only still here to read savestates written before the channels held buffer pointers. +struct AudioChannelWaitInfo { + SceUID threadID; + int numSamples; +}; // The extra channel is for SRC/Output2/Vaudio. AudioChannel g_audioChans[PSP_AUDIO_CHANNEL_MAX + 1]; -void AudioChannel::DoState(PointerWrap &p) -{ - auto s = p.Section("AudioChannel", 1, 3); +void AudioChannel::DoState(PointerWrap &p) { + auto s = p.Section("AudioChannel", 1, 4); if (!s) return; @@ -54,81 +58,115 @@ void AudioChannel::DoState(PointerWrap &p) Do(p, leftVolume); Do(p, rightVolume); Do(p, format); - Do(p, waitingThreads); + + if (s >= 4) { + Do(p, remainingSamples); + Do(p, waitingThread); + Do(p, waitingAddress); + Do(p, waitingLeftVolume); + Do(p, waitingRightVolume); + Do(p, srcBufferCount); + for (AudioPendingBuffer &buf : srcBuffers) { + Do(p, buf.address); + Do(p, buf.samples); + } + Do(p, srcPlayedSamples); + Do(p, srcFrac); + Do(p, srcCompletion); + Do(p, srcWaitingThreads); + Do(p, defaultRoutingMode); + Do(p, defaultRoutingVolMode); + return; + } + + // Everything below is the old format, from when the emulator copied each buffer into a + // ring of samples at enqueue time instead of playing out of the game's memory. There is + // no way to turn that back into a buffer pointer and a position, so the pending audio is + // dropped - a fraction of a second of silence on load, and then the game carries on. + std::vector oldWaitingThreads; + Do(p, oldWaitingThreads); if (s >= 2) { Do(p, defaultRoutingMode); Do(p, defaultRoutingVolMode); } + + auto oldQueue = std::make_unique>(); if (s >= 3) { - // v3: compact queue form — only the live samples, not the whole 512KB - // fixed storage per channel. Cuts ~4.6MB of dead bytes from every - // savestate. Old savestates (s < 3) still load through the - // full-storage path below. - chanSampleQueues[index].DoStateCompact(p); + oldQueue->DoStateCompact(p); } else { - chanSampleQueues[index].DoState(p); + oldQueue->DoState(p); + } + + if (p.mode == p.MODE_READ) { + sampleAddress = 0; + remainingSamples = 0; + waitingThread = 0; + waitingAddress = 0; + srcBufferCount = 0; + srcPlayedSamples = 0; + srcFrac = 0; + srcCompletion = false; + srcWaitingThreads.clear(); + // The threads that were parked in a blocking output call are still parked, and + // nothing is going to wake them now, so hand them their buffer back. + for (const AudioChannelWaitInfo &waitInfo : oldWaitingThreads) { + u32 error; + if (__KernelGetWaitID(waitInfo.threadID, WAITTYPE_AUDIOCHANNEL, error) != 0) { + __KernelResumeThreadFromWait(waitInfo.threadID, sampleCount); + } + } } } -void AudioChannel::reset() -{ +void AudioChannel::reset() { __AudioWakeThreads(*this, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED); clear(); } -void AudioChannel::clear() -{ +void AudioChannel::clear() { reserved = false; leftVolume = 0; rightVolume = 0; format = 0; - sampleAddress = 0; sampleCount = 0; - chanSampleQueues[index].clear(); - waitingThreads.clear(); + sampleAddress = 0; + remainingSamples = 0; + waitingThread = 0; + waitingAddress = 0; + waitingLeftVolume = 0; + waitingRightVolume = 0; + srcBufferCount = 0; + srcPlayedSamples = 0; + srcFrac = 0; + srcCompletion = false; + srcWaitingThreads.clear(); } -// Enqueues the buffer pointed to on the channel. If channel buffer queue is full (2 items?) will block until it isn't. -// For solid audio output we'll need a queue length of 2 buffers at least. +// The blocking output calls do not queue callers up. Each channel holds one buffer and at most +// one parked thread; the SRC channel holds two buffers and no parked-thread slot at all. A +// caller that finds no room is told SCE_ERROR_AUDIO_CHANNEL_BUSY and expected to come back +// later. See docs/sceAudio.md. -// Not sure about the range of volume, I often see 0x800 so that might be either -// max or 50%? static u32 sceAudioOutputBlocking(u32 chan, int vol, u32 samplePtr) { if (vol > 0xFFFF) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume"); } else if (chan >= PSP_AUDIO_CHANNEL_MAX) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel"); - } else if (!g_audioChans[chan].reserved) { - return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel not reserved"); } - if (vol >= 0) { - g_audioChans[chan].leftVolume = vol; - g_audioChans[chan].rightVolume = vol; - } - g_audioChans[chan].sampleAddress = samplePtr; - return hleLogDebug(Log::sceAudio, __AudioEnqueue(g_audioChans[chan], chan, true)); + return hleLogDebug(Log::sceAudio, __AudioEnqueueBlocking(g_audioChans[chan], samplePtr, vol, vol)); } static u32 sceAudioOutputPannedBlocking(u32 chan, int leftvol, int rightvol, u32 samplePtr) { - // For some reason, this is the only one that checks for negative. - if (leftvol > 0xFFFF || rightvol > 0xFFFF || leftvol < 0 || rightvol < 0) { + // This one ORs the two volumes together before comparing, so unlike the others a negative + // volume fails instead of meaning "leave it alone". + if ((u32)(leftvol | rightvol) > 0xFFFF) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume"); } else if (chan >= PSP_AUDIO_CHANNEL_MAX) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel"); - } else if (!g_audioChans[chan].reserved) { - return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel not reserved"); } - if (leftvol >= 0) { - g_audioChans[chan].leftVolume = leftvol; - } - if (rightvol >= 0) { - g_audioChans[chan].rightVolume = rightvol; - } - g_audioChans[chan].sampleAddress = samplePtr; - u32 result = __AudioEnqueue(g_audioChans[chan], chan, true); - return hleLogDebug(Log::sceAudio, result); + return hleLogDebug(Log::sceAudio, __AudioEnqueueBlocking(g_audioChans[chan], samplePtr, leftvol, rightvol)); } static u32 sceAudioOutput(u32 chan, int vol, u32 samplePtr) { @@ -136,17 +174,9 @@ static u32 sceAudioOutput(u32 chan, int vol, u32 samplePtr) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume"); } else if (chan >= PSP_AUDIO_CHANNEL_MAX) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel"); - } else if (!g_audioChans[chan].reserved) { - return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel not reserved"); } - if (vol >= 0) { - g_audioChans[chan].leftVolume = vol; - g_audioChans[chan].rightVolume = vol; - } - g_audioChans[chan].sampleAddress = samplePtr; - u32 result = __AudioEnqueue(g_audioChans[chan], chan, false); - return hleLogDebug(Log::sceAudio, result); + return hleLogDebug(Log::sceAudio, __AudioEnqueue(g_audioChans[chan], samplePtr, vol, vol)); } static u32 sceAudioOutputPanned(u32 chan, int leftvol, int rightvol, u32 samplePtr) { @@ -154,43 +184,44 @@ static u32 sceAudioOutputPanned(u32 chan, int leftvol, int rightvol, u32 sampleP return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume"); } else if (chan >= PSP_AUDIO_CHANNEL_MAX) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel"); - } else if (!g_audioChans[chan].reserved) { - return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel not reserved"); - } else { - if (leftvol >= 0) { - g_audioChans[chan].leftVolume = leftvol; - } - if (rightvol >= 0) { - g_audioChans[chan].rightVolume = rightvol; - } - g_audioChans[chan].sampleAddress = samplePtr; - u32 result = __AudioEnqueue(g_audioChans[chan], chan, false); - return hleLogDebug(Log::sceAudio, result); } + + return hleLogDebug(Log::sceAudio, __AudioEnqueue(g_audioChans[chan], samplePtr, leftvol, rightvol)); } +// A thread parked in a blocking output call counts as a whole extra buffer, on top of whatever +// is left of the one playing. static int sceAudioGetChannelRestLen(u32 chan) { if (chan >= PSP_AUDIO_CHANNEL_MAX) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel"); } - int remainingSamples = (int)chanSampleQueues[chan].size() / 2; - return hleLogVerbose(Log::sceAudio, remainingSamples); + const AudioChannel &c = g_audioChans[chan]; + int rest = (int)c.remainingSamples; + if (c.waitingThread != 0) { + rest += (int)c.sampleCount; + } + return hleLogVerbose(Log::sceAudio, rest); } static int sceAudioGetChannelRestLength(u32 chan) { if (chan >= PSP_AUDIO_CHANNEL_MAX) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel"); } - int remainingSamples = (int)chanSampleQueues[chan].size() / 2; - return hleLogVerbose(Log::sceAudio, remainingSamples); + // Unlike its sibling this one checks that a buffer is really playing first, so after an + // output with a null pointer the two disagree. + const AudioChannel &c = g_audioChans[chan]; + int rest = c.sampleAddress != 0 ? (int)c.remainingSamples : 0; + if (c.waitingThread != 0) { + rest += (int)c.sampleCount; + } + return hleLogVerbose(Log::sceAudio, rest); } static int GetFreeChannel() { - // Changed this to allow channel 0. Fixes the startup sound in VSH. TODO: Why did we not allow channel 0 before? - // The counter has to be signed: as a u32 the i >= 0 condition is always true, so with every - // channel reserved it wrapped past zero and ran off the array instead of giving up. + // The search runs downwards from 7, and a channel only counts as free once it has both + // been released and finished playing whatever it still held. for (int i = PSP_AUDIO_CHANNEL_MAX - 1; i >= 0; --i) { - if (!g_audioChans[i].reserved) + if (g_audioChans[i].sampleCount == 0 && g_audioChans[i].sampleAddress == 0) return i; } return -1; @@ -206,15 +237,15 @@ static u32 sceAudioChReserve(int chan, u32 sampleCount, u32 format) { if ((u32)chan >= PSP_AUDIO_CHANNEL_MAX) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel %d", chan); } + if (g_audioChans[chan].reserved) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "reserve channel failed"); + } if ((sampleCount & 63) != 0 || sampleCount == 0 || sampleCount > PSP_AUDIO_SAMPLE_MAX) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_OUTPUT_SAMPLE_DATA_SIZE_NOT_ALIGNED, "invalid sample count (not aligned)"); } if (format != PSP_AUDIO_FORMAT_MONO && format != PSP_AUDIO_FORMAT_STEREO) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_FORMAT, "invalid format"); } - if (g_audioChans[chan].reserved) { - return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "reserve channel failed"); - } g_audioChans[chan].sampleCount = sampleCount; g_audioChans[chan].format = format; @@ -228,24 +259,30 @@ static u32 sceAudioChRelease(u32 chan) { if (chan >= PSP_AUDIO_CHANNEL_MAX) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel %d", chan); } else if (!g_audioChans[chan].reserved) { - return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel %d not reserved", chan); + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel %d not reserved", chan); + } else if (g_audioChans[chan].waitingThread != 0) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_BUSY, "channel %d has a thread waiting", chan); } - // TODO: Does this error if busy? - g_audioChans[chan].reset(); + // Only the reservation goes away. A buffer already handed over keeps playing to the end, + // and the channel stays unavailable to sceAudioChReserve(-1) until it does. g_audioChans[chan].reserved = false; + g_audioChans[chan].sampleCount = 0; return hleLogDebug(Log::sceAudio, 0); } static u32 sceAudioSetChannelDataLen(u32 chan, u32 len) { if (chan >= PSP_AUDIO_CHANNEL_MAX) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel %d", chan); - } else if (!g_audioChans[chan].reserved) { - return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel %d not reserved", chan); } else if ((len & 63) != 0 || len == 0 || len > PSP_AUDIO_SAMPLE_MAX) { + // Checked before the reservation, unlike most of the others. return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_OUTPUT_SAMPLE_DATA_SIZE_NOT_ALIGNED, "invalid sample count"); + } else if (g_audioChans[chan].waitingThread != 0) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_BUSY, "channel %d has a thread waiting", chan); + } else if (!g_audioChans[chan].reserved) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_INIT, "channel %d not reserved", chan); } - + g_audioChans[chan].sampleCount = len; return hleLogDebug(Log::sceAudio, 0); } @@ -253,25 +290,32 @@ static u32 sceAudioSetChannelDataLen(u32 chan, u32 len) { static u32 sceAudioChangeChannelConfig(u32 chan, u32 format) { if (chan >= PSP_AUDIO_CHANNEL_MAX) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "invalid channel number %d", chan); + } else if (g_audioChans[chan].waitingThread != 0 || g_audioChans[chan].sampleAddress != 0) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_BUSY, "channel %d busy", chan); } else if (!g_audioChans[chan].reserved) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel %d not reserved", chan); + } else if (format != PSP_AUDIO_FORMAT_MONO && format != PSP_AUDIO_FORMAT_STEREO) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_FORMAT, "invalid format"); } g_audioChans[chan].format = format; return hleLogDebug(Log::sceAudio, 0); } -static u32 sceAudioChangeChannelVolume(u32 chan, u32 leftvol, u32 rightvol) { +static u32 sceAudioChangeChannelVolume(u32 chan, int leftvol, int rightvol) { if (leftvol > 0xFFFF || rightvol > 0xFFFF) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid chan %d volume %d %d", chan, leftvol, rightvol); } else if (chan >= PSP_AUDIO_CHANNEL_MAX) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "invalid channel %d", chan); - } else if (!g_audioChans[chan].reserved) { - return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel %d not reserved", chan); } - g_audioChans[chan].leftVolume = leftvol; - g_audioChans[chan].rightVolume = rightvol; + // There is no reservation check here, and a negative volume means "leave that side alone". + if (leftvol >= 0) { + g_audioChans[chan].leftVolume = leftvol; + } + if (rightvol >= 0) { + g_audioChans[chan].rightVolume = rightvol; + } return hleLogDebug(Log::sceAudio, 0); } @@ -295,6 +339,7 @@ static u32 sceAudioOutput2Reserve(u32 sampleCount) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED, "channel already reserved"); } + chan.clear(); chan.sampleCount = sampleCount; chan.format = PSP_AUDIO_FORMAT_STEREO; chan.reserved = true; @@ -308,27 +353,25 @@ static u32 sceAudioOutput2OutputBlocking(u32 vol, u32 dataPtr) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume"); } - auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2]; - if (!chan.reserved) { - return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved"); - } - - chan.leftVolume = vol; - chan.rightVolume = vol; - chan.sampleAddress = dataPtr; - hleEatCycles(10000); - int result = __AudioEnqueue(chan, PSP_AUDIO_CHANNEL_OUTPUT2, true); - if (result < 0) + u32 result = __AudioSRCEnqueueBlocking(g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2], dataPtr, vol); + if ((int)result < 0) return hleLogError(Log::sceAudio, result); return hleLogDebug(Log::sceAudio, result); } static u32 sceAudioOutput2ChangeLength(u32 sampleCount) { + // The length is range-checked before the channel is, and 4111 is the same ceiling the + // reserve takes. + if (sampleCount - 17 >= 0xFFF) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_OUTPUT_SAMPLE_DATA_SIZE_NOT_ALIGNED, "invalid sample count"); + } auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2]; if (!chan.reserved) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved"); } + // Buffers already handed over keep their original length; only what is reported and what + // is accepted from here on changes. chan.sampleCount = sampleCount; return hleLogDebug(Log::sceAudio, 0); } @@ -338,23 +381,20 @@ static u32 sceAudioOutput2GetRestSample() { if (!chan.reserved) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved"); } - u32 size = (u32)chanSampleQueues[PSP_AUDIO_CHANNEL_OUTPUT2].size() / 2; - if (size > chan.sampleCount) { - // If ChangeLength reduces the size, it still gets output but this return is clamped. - size = chan.sampleCount; - } - return hleLogDebug(Log::sceAudio, size); + // Counts armed DMA descriptors, in units of the current length - so it reports two + // buffers' worth while both are in flight. + return hleLogDebug(Log::sceAudio, chan.srcBufferCount * chan.sampleCount); } static u32 sceAudioOutput2Release() { auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2]; if (!chan.reserved) return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved"); - if (!chanSampleQueues[PSP_AUDIO_CHANNEL_OUTPUT2].empty()) + if (chan.srcBufferCount != 0) return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED, "output busy"); chan.reset(); - chan.reserved = false; + __AudioSRCSignal(chan); return hleLogDebug(Log::sceAudio, 0); } @@ -399,6 +439,7 @@ static u32 sceAudioSRCChReserve(u32 sampleCount, u32 freq, u32 format) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED, "channel already reserved"); } + chan.clear(); chan.reserved = true; chan.sampleCount = sampleCount; chan.format = format == 2 ? PSP_AUDIO_FORMAT_STEREO : PSP_AUDIO_FORMAT_MONO; @@ -411,11 +452,12 @@ static u32 sceAudioSRCChRelease() { auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_SRC]; if (!chan.reserved) return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved"); - if (!chanSampleQueues[PSP_AUDIO_CHANNEL_SRC].empty()) + if (chan.srcBufferCount != 0) return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED, "output busy"); chan.reset(); - chan.reserved = false; + // Releasing signals a completion, which the next caller after a fresh reserve consumes. + __AudioSRCSignal(chan); return hleLogDebug(Log::sceAudio, 0); } @@ -424,18 +466,9 @@ static u32 sceAudioSRCOutputBlocking(u32 vol, u32 buf) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume"); } - auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_SRC]; - if (!chan.reserved) { - return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved"); - } - - chan.leftVolume = vol; - chan.rightVolume = vol; - chan.sampleAddress = buf; - hleEatCycles(10000); - int result = __AudioEnqueue(chan, PSP_AUDIO_CHANNEL_SRC, true); - if (result < 0) + u32 result = __AudioSRCEnqueueBlocking(g_audioChans[PSP_AUDIO_CHANNEL_SRC], buf, vol); + if ((int)result < 0) return hleLogError(Log::sceAudio, result); return hleLogDebug(Log::sceAudio, result); } @@ -531,7 +564,7 @@ const HLEFunction sceAudio[] = {0XB011922F, &WrapI_U, "sceAudioGetChannelRestLength", 'i', "i" }, {0XCB2E439E, &WrapU_UU, "sceAudioSetChannelDataLen", 'x', "ii" }, {0X95FD0C2D, &WrapU_UU, "sceAudioChangeChannelConfig", 'x', "ii" }, - {0XB7E1D8E7, &WrapU_UUU, "sceAudioChangeChannelVolume", 'x', "ixx" }, + {0XB7E1D8E7, &WrapU_UII, "sceAudioChangeChannelVolume", 'x', "ixx" }, // Like Output2, but with ability to do sample rate conversion. {0X38553111, &WrapU_UUU, "sceAudioSRCChReserve", 'x', "iii" }, diff --git a/Core/HLE/sceAudio.h b/Core/HLE/sceAudio.h index 03fe44673d..051f355bab 100644 --- a/Core/HLE/sceAudio.h +++ b/Core/HLE/sceAudio.h @@ -17,7 +17,7 @@ #pragma once -#include +#include #include "CommonTypes.h" #include "sceKernel.h" @@ -33,18 +33,21 @@ const int PSP_AUDIO_CHANNEL_SRC = 8; const int PSP_AUDIO_CHANNEL_OUTPUT2 = 8; const int PSP_AUDIO_CHANNEL_VAUDIO = 8; -struct AudioChannelWaitInfo { - SceUID threadID; - int numSamples; +// One buffer handed over and not yet fully played. +struct AudioPendingBuffer { + u32 address; + u32 samples; }; +// Mirrors the 16-byte channel struct the audio driver keeps, plus the two DMA descriptors it +// uses for the SRC/Output2 channel. See docs/sceAudio.md for how the hardware behaves; the +// short version is that it plays out of the game's own memory rather than taking a copy, and +// that it holds one buffer per mixer channel and two for SRC. struct AudioChannel { int index = 0; bool reserved = false; - // last sample address - u32 sampleAddress = 0; - u32 sampleCount = 0; // Number of samples written in each OutputBlocking + u32 sampleCount = 0; // Buffer size agreed at reserve time. u32 leftVolume = 0; u32 rightVolume = 0; u32 format = 0; @@ -52,12 +55,40 @@ struct AudioChannel { // For the debugger only. Not saved. bool mute = false; - std::vector waitingThreads; + // Channels 0-7. sampleAddress walks forward as the mixer consumes the buffer and drops + // back to zero when remainingSamples runs out. An output with a null pointer sets + // remainingSamples but leaves sampleAddress at zero, which is the one case where the two + // rest-length calls disagree. + u32 sampleAddress = 0; + u32 remainingSamples = 0; + + // Only one thread can be parked in a blocking output call on a channel. A second one is + // told the channel is busy rather than queueing up behind the first. These remember what + // it wanted to hand over, so the enqueue can be retried when the buffer finishes. + SceUID waitingThread = 0; + u32 waitingAddress = 0; + int waitingLeftVolume = 0; + int waitingRightVolume = 0; + + // Channel 8 (Output2/SRC/Vaudio) instead has two DMA descriptors, so two buffers can be + // in flight at once and the third caller is turned away. + AudioPendingBuffer srcBuffers[2]{}; + int srcBufferCount = 0; + u32 srcPlayedSamples = 0; // Consumed from srcBuffers[0]. + u32 srcFrac = 0; // 16.16 position between two input samples, for resampling. + // The driver signals a finished buffer with an event flag bit, so one completion can sit + // there unclaimed - which is why the first output after an idle period doesn't block. + bool srcCompletion = false; + std::vector srcWaitingThreads; void DoState(PointerWrap &p); void reset(); void clear(); + + bool SRCFull() const { + return srcBufferCount >= (int)ARRAY_SIZE(srcBuffers); + } }; // The extra channel is for SRC/Output2/Vaudio (who all share, apparently.) diff --git a/Core/HLE/sceVaudio.cpp b/Core/HLE/sceVaudio.cpp index 6aeb488c25..f6552f027f 100644 --- a/Core/HLE/sceVaudio.cpp +++ b/Core/HLE/sceVaudio.cpp @@ -69,6 +69,7 @@ static u32 sceVaudioChReserve(int sampleCount, int freq, int format) { return SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED; } DEBUG_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i)", sampleCount, freq, format); + g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].clear(); g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].reserved = true; g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].sampleCount = sampleCount; g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].format = format == 2 ? PSP_AUDIO_FORMAT_STEREO : PSP_AUDIO_FORMAT_MONO; @@ -85,7 +86,6 @@ static u32 sceVaudioChRelease() { return SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED; } else { g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].reset(); - g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].reserved = false; vaudioReserved = false; return 0; } @@ -93,11 +93,8 @@ static u32 sceVaudioChRelease() { static u32 sceVaudioOutputBlocking(int vol, u32 buffer) { DEBUG_LOG(Log::sceAudio, "sceVaudioOutputBlocking(%i, %08x)", vol, buffer); - g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].leftVolume = vol; - g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].rightVolume = vol; - // TODO: This may be wrong, not sure if's in a different format? - g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].sampleAddress = buffer; - return __AudioEnqueue(g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO], PSP_AUDIO_CHANNEL_VAUDIO, true); + // Shares the SRC channel, so it also shares the two-buffer depth and the busy return. + return __AudioSRCEnqueueBlocking(g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO], buffer, vol); } static u32 sceVaudioSetEffectType(int effectType, int vol) { diff --git a/UI/ImDebugger/ImDebugger.cpp b/UI/ImDebugger/ImDebugger.cpp index 9a2827f3f1..92cfae97d5 100644 --- a/UI/ImDebugger/ImDebugger.cpp +++ b/UI/ImDebugger/ImDebugger.cpp @@ -1666,10 +1666,22 @@ void DrawAudioChannels(ImConfig &cfg, ImControl &control) { break; } ImGui::TableNextColumn(); - for (auto t : g_audioChans[i].waitingThreads) { - KernelObject *thread = kernelObjects.GetFast(t.threadID); + // At most one thread can be parked on a mixer channel; the SRC channel keeps its + // own list instead. + SceUID waiting[3]; + int waitingCount = 0; + if (g_audioChans[i].waitingThread != 0) { + waiting[waitingCount++] = g_audioChans[i].waitingThread; + } + for (SceUID t : g_audioChans[i].srcWaitingThreads) { + if (waitingCount < ARRAY_SIZE(waiting)) { + waiting[waitingCount++] = t; + } + } + for (int w = 0; w < waitingCount; w++) { + KernelObject *thread = kernelObjects.GetFast(waiting[w]); if (thread) { - ImGui::Text("%s: %d", thread->GetName(), t.numSamples); + ImGui::TextUnformatted(thread->GetName()); } } ImGui::PopID(); diff --git a/docs/sceAudio.md b/docs/sceAudio.md new file mode 100644 index 0000000000..971ce10bf3 --- /dev/null +++ b/docs/sceAudio.md @@ -0,0 +1,156 @@ +# sceAudio: how the PSP's audio output actually behaves + +This describes the behavior `Core/HLE/sceAudio.cpp` and `Core/HLE/__sceAudio.cpp` model, and +where that behavior came from. Confirmed on a real PSP by `pspautotests/tests/audio/blocking`. + +The headline, because it is the thing most likely to be assumed wrong: **the blocking output +calls are not a queue that callers line up behind.** Each channel holds one buffer, the +Output2/SRC channel holds two, and a caller who finds no room is told +`SCE_ERROR_AUDIO_CHANNEL_BUSY` and expected to go away and come back. + +## Two different pieces of hardware + +`sceAudio` presents nine channels, but they are not the same thing underneath. + +**Channels 0-7** go through a software mixer running as a kernel thread. Every time the audio +DMA finishes a block, that thread wakes, takes up to 64 samples from each channel that has a +buffer, sums them into a 32-bit accumulator, clamps, and writes 64 stereo frames into the other +half of a double buffer. + +**Channel 8** - `sceAudioOutput2*`, `sceAudioSRC*` and `sceVaudio*` are all the same channel - +never touches the mixer. It builds DMA descriptors that point straight at the game's buffer and +lets the codec resample it. `sceAudioOutput2Reserve(n)` is literally +`sceAudioSRCChReserve(n, 44100, 2)`, and the other Output2 entry points are one-instruction tail +calls to the SRC ones. + +They also have separate DMA channels, so one starting does not disturb the other. + +## The driver plays out of the game's memory + +Nothing is copied when a buffer is handed over. `sceAudioOutputBlocking` stores the pointer, and +the mixer walks it forward 64 samples at a time until it is spent, then clears it and signals. +`sceAudioGetChannelRestLen` counts down in steps of 64 for exactly that reason. + +The emulator used to copy the whole buffer into a ring of samples at enqueue time, with the +volume already applied. That made a game rewriting a buffer it had already handed over +invisible, and it made the queue depth an emulator choice rather than a hardware fact. + +## One buffer per channel + +The driver's single enqueue point, which all four `sceAudioOutput*` functions reach, is: + +```c +if (sampleCount == 0) return SCE_ERROR_AUDIO_CHANNEL_NOT_INIT; +if (sampleAddress != 0) return SCE_ERROR_AUDIO_CHANNEL_BUSY; +remaining = sampleCount; +sampleAddress = ptr; // may be 0 +return sampleCount; +``` + +`sceAudioOutput` and `sceAudioOutputPanned` return that result as it stands, so a second +non-blocking output is refused until the first buffer has finished. + +The blocking pair adds a wait, but only for the first thread to ask: + +```c +r = enqueue(...); +if (r != BUSY) return r; +if (channel->waiting) return BUSY; // somebody else is already parked here +channel->waiting = 1; +wait for this channel's bit in the driver's event flag; +retry the enqueue; +channel->waiting = 0; +``` + +**Only one thread can be parked on a channel.** That single flag is why a game that runs a movie +thread and a sound-effect thread over one output gets sensible behavior on hardware and did not +in the emulator: the loser is told the channel is busy and skips its turn, where an emulator that +blocked it instead made the two threads alternate, halving the movie's audio rate. That is +https://github.com/hrydgard/ppsspp/issues/12888, and it is what +`tests/audio/blocking/contend` pins down. + +The same flag locks out `sceAudioChRelease`, `sceAudioSetChannelDataLen` and +`sceAudioChangeChannelConfig`, which all return `SCE_ERROR_AUDIO_CHANNEL_BUSY` while a thread is +parked. `sceAudioChangeChannelVolume` does not check anything. + +A null pointer is accepted and sets `remaining` without setting `sampleAddress`, so nothing +plays. That is the documented way to wait for a channel to drain, and it is the one case where +the two rest-length calls disagree - see below. + +## Two buffers on the SRC channel + +Channel 8 has two DMA descriptors, so two buffers can be in flight. The third caller is refused +outright and does not even get the chance to wait: + +```c +if (!reserved) return SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED; +if (both descriptors armed) return SCE_ERROR_AUDIO_CHANNEL_BUSY; +arm one; +wait for one descriptor to retire; +return sampleCount; +``` + +Starting the DMA sets the same event flag bit that a retiring descriptor does, which is why the +first output after an idle stretch returns without waiting while every one after it waits exactly +one buffer. The emulator models that unclaimed completion with a flag on the channel. + +## The two rest-length calls are not the same function twice + +```c +sceAudioGetChannelRestLen(ch) = remaining + (waiting ? sampleCount : 0) +sceAudioGetChannelRestLength(ch) = (sampleAddress ? remaining : 0) + (waiting ? sampleCount : 0) +sceAudioOutput2GetRestSample() = (armed descriptor count) * current sampleCount +``` + +Both channel versions count a parked thread's buffer as well as the one playing. They only +differ after an output with a null pointer, which leaves `remaining` set with no buffer, and the +hardware duly reports `0x400` from one and `0` from the other. + +`sceAudioOutput2GetRestSample` reports in units of the *current* length, so after +`sceAudioOutput2ChangeLength(64)` a 4096-sample buffer armed earlier reads back as `0x40`. + +## Where the emulator has to fake something + +The mixer thread outranks whoever called, so on hardware the first 64 samples of a buffer are +consumed before the output call has returned - a channel reserved for exactly 64 samples is free +again immediately. The emulator's mixer is a timer event instead, so it re-phases that event to +the moment the mixer's DMA starts and runs one block right then. The SRC channel gets the phase +reset but no early read, since its DMA feeds the codec directly. Without this the answer to +`sceAudioGetChannelRestLen` right after an output would depend on where the timer happened to +be, and would differ from run to run. + +Two things are still approximate, both below one mix block: + +- A descriptor retires when the DMA transfer finishes, which is slightly ahead of the audio + being heard, so hardware frees an Output2 buffer about 100us earlier than the emulator does. +- The emulator only retires SRC buffers on block boundaries, so a buffer whose length is not a + multiple of the block can be up to 1.5ms late. `audio/output2/frequency` and + `audio/output2/rest` are in `tests_next` for this reason. + +## Argument checking + +Recorded here because several of these were guesses before, and because the *order* of the +checks is observable. + +| function | checks, in order | +|---|---| +| `sceAudioChReserve` | the free-channel search runs 7 down to 0 and needs the channel both released and finished playing; bad channel `80260003`; already reserved `80260003`; count not a positive multiple of 64 up to 0xFFC0 `80260006`; format not 0 or 0x10 `80260007` | +| `sceAudioChRelease` | bad channel `80260003`; not reserved `80260008`; a thread parked `80260002`. Clears only the reservation - a buffer already playing plays out. | +| `sceAudioSetChannelDataLen` | bad channel `80260003`; **bad length `80260006`, before the reservation is looked at**; parked thread `80260002`; not reserved `80260001` | +| `sceAudioChangeChannelConfig` | bad channel `80260003`; parked thread or buffer in flight `80260002`; not reserved `80260008`; bad format `80260007` | +| `sceAudioChangeChannelVolume` | either side above 0xFFFF `8026000b`; bad channel `80260003`; **no reservation check**; a negative volume leaves that side alone | +| `sceAudioOutput`, `sceAudioOutputPanned` | each volume compared signed, so negatives pass and mean "leave unchanged" | +| `sceAudioOutputPannedBlocking` | the two volumes are ORed before comparing, so a negative one fails with `8026000b` | +| `sceAudioOutput2ChangeLength` | **length outside 17..4111 `80260006`, before the reservation**; not reserved `80260008` | +| `sceAudioSRCChReserve` | channels not 2 or 4 `80000104`; channels 4 `80000003`; count outside 17..4111 `80000104`; bad frequency `8026000a`; already reserved `80268002` | +| `sceAudioSRCChRelease` | not reserved `80260008`; a descriptor still armed `80268002` | + +Accepted SRC frequencies are 8000, 11025, 12000, 16000, 22050, 24000, 32000 and 48000, plus +whatever the output is currently running at - which is how 44100 and 0 get through. + +## Savestates + +`AudioChannel` is at section version 4. Anything older stored a ring of already-mixed samples, +which cannot be turned back into a buffer pointer and a position, so loading one drops the +pending audio and wakes any parked threads. That costs a fraction of a second of silence on +load and nothing else. diff --git a/pspautotests b/pspautotests index 1dda6a3709..3d55f06ed5 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 1dda6a37091c1dcf7bc23b5ac5c48811751e9f2c +Subproject commit 3d55f06ed5c253f3603f5c0517da09830a9f0b28 diff --git a/test.py b/test.py index 22142cb689..1904aba777 100755 --- a/test.py +++ b/test.py @@ -113,7 +113,13 @@ tests_good = [ "audio/mp3/reserve", "audio/mp3/setloopnum", "audio/mp3/stream", + "audio/blocking/contend", + "audio/blocking/depth", + "audio/blocking/errors", + "audio/blocking/restlen", + "audio/sceaudio/datalen", "audio/output2/changelength", + "audio/output2/release", "audio/output2/reserve", "audio/output2/threads", "audio/reverb/basic", @@ -411,13 +417,11 @@ tests_next = [ "cpu/vfpu/prefixes", "cpu/vfpu/vector", "cpu/vfpu/vregs", - "audio/sceaudio/datalen", "audio/sceaudio/output", "audio/sceaudio/reserve", "audio/sascore/setadsr", "audio/mp3/init", "audio/output2/frequency", - "audio/output2/release", "audio/output2/rest", "ccc/convertstring", "display/hcount", From 000af25d817a89e9a0377965e848a9f6a8818415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 10 Sep 2026 12:25:41 -0600 Subject: [PATCH 2/7] sceAudio: split the SRC channel out, and stop rescheduling mid-syscall The nine channels were one array of one struct, but only index 8 ever used the two DMA descriptors, the played/fraction position and the waiting-thread vector, and only 0-7 ever used the buffer slot - so eight copies of a std::vector sat there dead. They are two different pieces of hardware and now they are two structs: AudioChannel for the eight the mixer walks, and a single AudioSRCChannel for the one the codec reads directly. Found while rereading it: - __AudioUpdate can now run from inside an output call, and it reschedules when it wakes a thread. A context switch there lands before the syscall writes its return value, so the caller loses it and the woken thread gets it instead. Deferred with hleReSchedule when a syscall is in flight. The same fix applies to the release path, which had the problem before this branch existed. - A finished SRC buffer whose waiting thread had given up dropped the completion entirely, so the next caller waited a buffer too long. Now it walks past dead waiters and banks the completion if nobody is left. - An output with a null pointer changed the channel volume, where the hardware returns before touching it. - A busy channel came back as an error from the Output2 path and as debug from the mixer path. It is an ordinary answer a game polls on, so both are debug now; the old behavior filled the log with 18k error lines in a minute of F1 2009. - AudioChannel::reset had no callers left: releasing a channel with a thread parked on it is refused, so there is nobody to wake. - The two routing modes are globals and were written once per channel. They get their own small savestate block. Savestate: the sceAudio section goes to 3, and the SRC channel gets a section of its own. States from released builds carry it as a ninth channel record, which is read and discarded. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/__sceAudio.cpp | 194 ++++++++++++++++++++--------------- Core/HLE/__sceAudio.h | 9 +- Core/HLE/sceAudio.cpp | 122 +++++++++++++--------- Core/HLE/sceAudio.h | 71 ++++++++----- Core/HLE/sceVaudio.cpp | 20 ++-- UI/ImDebugger/ImDebugger.cpp | 90 +++++++++------- docs/sceAudio.md | 13 ++- 7 files changed, 306 insertions(+), 213 deletions(-) diff --git a/Core/HLE/__sceAudio.cpp b/Core/HLE/__sceAudio.cpp index 326517c4ed..78b77bada1 100644 --- a/Core/HLE/__sceAudio.cpp +++ b/Core/HLE/__sceAudio.cpp @@ -37,6 +37,7 @@ #include "Core/ELF/ParamSFO.h" #include "Core/HLE/sceKernelTime.h" #include "Core/HLE/ErrorCodes.h" +#include "Core/HLE/HLE.h" #include "Core/HLE/__sceAudio.h" #include "Core/HLE/sceAudio.h" #include "Core/HLE/sceKernel.h" @@ -98,10 +99,11 @@ void __AudioInit() { CoreTiming::ScheduleEvent(audioIntervalCycles, eventAudioUpdate, 0); CoreTiming::ScheduleEvent(audioHostIntervalCycles, eventHostAudioUpdate, 0); - for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX + 1; i++) { + for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX; i++) { g_audioChans[i].index = i; g_audioChans[i].clear(); } + g_audioSRC.clear(); mixBuffer = new s32[hwBlockSize * 2]; clampedMixBuffer = new s16[hwBlockSize * 2]; @@ -111,7 +113,7 @@ void __AudioInit() { } void __AudioDoState(PointerWrap &p) { - auto s = p.Section("sceAudio", 1, 2); + auto s = p.Section("sceAudio", 1, 3); if (!s) return; @@ -143,17 +145,34 @@ void __AudioDoState(PointerWrap &p) { System_AudioClear(); } + // Before v3 the SRC channel was a ninth entry in this array rather than its own thing, so + // older states carry one extra record here. Its contents are the old sample-ring format + // that can't be converted anyway, so it gets read into a throwaway and dropped. int chanCount = ARRAY_SIZE(g_audioChans); Do(p, chanCount); - if (chanCount != ARRAY_SIZE(g_audioChans)) - { + const int expected = s >= 3 ? (int)ARRAY_SIZE(g_audioChans) : (int)ARRAY_SIZE(g_audioChans) + 1; + if (chanCount != expected) { ERROR_LOG(Log::sceAudio, "Savestate failure: different number of audio channels."); p.SetError(p.ERROR_FAILURE); return; } for (int i = 0; i < chanCount; ++i) { - g_audioChans[i].index = i; - g_audioChans[i].DoState(p); + if (i < (int)ARRAY_SIZE(g_audioChans)) { + g_audioChans[i].index = i; + g_audioChans[i].DoState(p); + } else { + AudioChannel discarded; + discarded.index = i; + discarded.DoState(p); + } + } + + if (s >= 3) { + g_audioSRC.DoState(p); + __AudioRoutingDoState(p); + } else if (p.mode == p.MODE_READ) { + // The old format read the routing modes back once per channel, above. + g_audioSRC.clear(); } __AudioCPUMHzChange(); @@ -164,10 +183,11 @@ void __AudioShutdown() { delete [] clampedMixBuffer; mixBuffer = 0; - for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX + 1; i++) { + for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX; i++) { g_audioChans[i].index = i; g_audioChans[i].clear(); } + g_audioSRC.clear(); #ifndef MOBILE_DEVICE if (g_Config.bDumpAudio) { @@ -192,12 +212,25 @@ static inline int ApplyChannelVolume(int sample, int vol) { // Set while __AudioUpdate is running, so a buffer accepted from inside it - the retry a parked // thread gets when its predecessor finishes - doesn't try to start the DMA again. static bool audioMixing; +// Set while that mixing is happening underneath a syscall rather than from the timing event. +static bool audioMixingInSyscall; + +// Switching threads is fine from the timing event, but not from inside an output call: the +// syscall's return value is written after the call body runs, so a context switch here would +// put it in the wrong thread's registers. hleReSchedule defers to after the syscall instead. +static void __AudioReScheduleAfterWake() { + if (audioMixingInSyscall) { + hleReSchedule("audio drain"); + } else { + __KernelReSchedule("audio drain"); + } +} // Only channels 0-7. The SRC channel is on its own DMA that the mixer never touches, so the // two start independently of each other. static bool __AudioAnyChannelPlaying() { - for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX; i++) { - if (g_audioChans[i].sampleAddress != 0) { + for (const AudioChannel &chan : g_audioChans) { + if (chan.sampleAddress != 0) { return true; } } @@ -214,7 +247,9 @@ static void __AudioStartMixerDMA() { return; } CoreTiming::UnscheduleEvent(eventAudioUpdate, 0); + audioMixingInSyscall = true; __AudioUpdate(); + audioMixingInSyscall = false; CoreTiming::ScheduleEvent(audioIntervalCycles, eventAudioUpdate, 0); } @@ -248,12 +283,13 @@ u32 __AudioEnqueue(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol) if (rightVol >= 0) { chan.rightVolume = rightVol; } + // Handing over a buffer while nothing was playing is what starts the DMA. + const bool startsDMA = samplePtr != 0 && !__AudioAnyChannelPlaying(); // A null pointer is accepted and leaves the channel idle, but still counts as a buffer's // worth of remaining samples - which is the one case where the two rest-length calls // disagree with each other. - const bool wasIdle = !__AudioAnyChannelPlaying(); chan.sampleAddress = samplePtr; - if (samplePtr != 0 && wasIdle) { + if (startsDMA) { __AudioStartMixerDMA(); } return chan.sampleCount; @@ -306,103 +342,95 @@ static bool __AudioChannelFinished(AudioChannel &chan) { return true; } -u32 __AudioSRCEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int vol) { +u32 __AudioSRCEnqueueBlocking(AudioSRCChannel &chan, u32 samplePtr, int vol) { if (!chan.reserved) { return SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED; } // Two DMA descriptors, so two buffers fit. The third caller is refused outright - unlike // the mixer channels it does not even get the chance to wait for a slot. - if (chan.SRCFull()) { + if (chan.Full()) { return SCE_ERROR_AUDIO_CHANNEL_BUSY; } - if (vol >= 0) { - chan.leftVolume = vol; - chan.rightVolume = vol; - } - u32 result = 0; if (samplePtr != 0) { - if (chan.srcBufferCount == 0) { + // The volume rides along with a buffer, so a null pointer leaves it alone. A negative + // one means the same thing. + if (vol >= 0) { + chan.leftVolume = vol; + chan.rightVolume = vol; + } + const bool wasIdle = chan.bufferCount == 0; + if (wasIdle) { // Starting the DMA signals a completion by itself, which is why the first // output after an idle stretch returns without blocking. - chan.srcCompletion = true; - chan.srcPlayedSamples = 0; - chan.srcFrac = 0; + chan.completion = true; + chan.playedSamples = 0; + chan.frac = 0; } - const bool wasIdle = chan.srcBufferCount == 0; - chan.srcBuffers[chan.srcBufferCount].address = samplePtr; - chan.srcBuffers[chan.srcBufferCount].samples = chan.sampleCount; - chan.srcBufferCount++; + chan.buffers[chan.bufferCount].address = samplePtr; + chan.buffers[chan.bufferCount].samples = chan.sampleCount; + chan.bufferCount++; result = chan.sampleCount; if (wasIdle) { __AudioStartSRCDMA(); } - } else if (chan.srcBufferCount == 0) { + } else if (chan.bufferCount == 0) { // Nothing playing and nothing handed over, so there is no completion to wait for. return 0; } - if (chan.srcCompletion) { - chan.srcCompletion = false; + if (chan.completion) { + chan.completion = false; return result; } if (!__KernelIsDispatchEnabled()) { return SCE_KERNEL_ERROR_CAN_NOT_WAIT; } - chan.srcWaitingThreads.push_back(__KernelGetCurThread()); - __KernelWaitCurThread(WAITTYPE_AUDIOCHANNEL, (SceUID)chan.index + 1, result, 0, false, "blocking audio"); + chan.waitingThreads.push_back(__KernelGetCurThread()); + __KernelWaitCurThread(WAITTYPE_AUDIOCHANNEL, PSP_AUDIO_SRC_WAIT_ID, result, 0, false, "blocking audio"); return result; } -void __AudioSRCSignal(AudioChannel &chan) { - chan.srcCompletion = true; +void __AudioSRCSignal(AudioSRCChannel &chan) { + chan.completion = true; } // One of the two SRC buffers finished playing. -static bool __AudioSRCCompleted(AudioChannel &chan) { - if (chan.srcWaitingThreads.empty()) { - // Nobody is listening, so the completion sits there for the next caller to consume. - chan.srcCompletion = true; - return false; +static bool __AudioSRCCompleted(AudioSRCChannel &chan) { + // Threads that gave up on their own are dropped rather than counted as woken, so a + // completion is never spent on one - the next real waiter, or the flag, gets it. + while (!chan.waitingThreads.empty()) { + const SceUID threadID = chan.waitingThreads.front(); + chan.waitingThreads.erase(chan.waitingThreads.begin()); + + u32 error; + if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) != 0) { + __KernelResumeThreadFromWait(threadID, __KernelGetWaitValue(threadID, error)); + return true; + } } - const SceUID threadID = chan.srcWaitingThreads.front(); - chan.srcWaitingThreads.erase(chan.srcWaitingThreads.begin()); - - u32 error; - if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) == 0) { - return false; - } - __KernelResumeThreadFromWait(threadID, __KernelGetWaitValue(threadID, error)); - return true; + // Nobody is listening, so the completion sits there for the next caller to consume. + chan.completion = true; + return false; } -void __AudioWakeThreads(AudioChannel &chan, int result) { +void __AudioWakeThreads(AudioSRCChannel &chan, int result) { bool woke = false; - - if (chan.waitingThread != 0) { - const SceUID threadID = chan.waitingThread; - chan.waitingThread = 0; + for (SceUID threadID : chan.waitingThreads) { u32 error; if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) != 0) { __KernelResumeThreadFromWait(threadID, result); woke = true; } } - - for (SceUID threadID : chan.srcWaitingThreads) { - u32 error; - if (__KernelGetWaitID(threadID, WAITTYPE_AUDIOCHANNEL, error) != 0) { - __KernelResumeThreadFromWait(threadID, result); - woke = true; - } - } - chan.srcWaitingThreads.clear(); + chan.waitingThreads.clear(); if (woke) { - __KernelReSchedule("audio drain"); + // Only ever called from one of the release calls, so always inside a syscall. + hleReSchedule("audio drain"); } } @@ -461,8 +489,8 @@ static bool __AudioMixChannel(AudioChannel &chan) { // Channel 8 never reaches the mixer on hardware - the DMA feeds the codec directly and the // codec resamples. Model that as a read straight through the pending buffers at the ratio // between the reserved frequency and the output rate. -static bool __AudioMixSRC(AudioChannel &chan) { - if (chan.srcBufferCount == 0) { +static bool __AudioMixSRC(AudioSRCChannel &chan) { + if (chan.bufferCount == 0) { return false; } @@ -476,18 +504,18 @@ static bool __AudioMixSRC(AudioChannel &chan) { bool woke = false; for (int out = 0; out < hwBlockSize; out++) { - if (chan.srcBufferCount == 0) { + if (chan.bufferCount == 0) { // Underrun. The rest of the block stays silent, like a descriptor the game // never got around to arming. break; } - const AudioPendingBuffer &buf = chan.srcBuffers[0]; - const u32 addr = buf.address + chan.srcPlayedSamples * stride; + const AudioPendingBuffer &buf = chan.buffers[0]; + const u32 addr = buf.address + chan.playedSamples * stride; // Interpolating against the following sample matters when a game reserved 22050Hz // or similar; at the native rate the fraction is always zero and this reduces to a // plain copy. - const u32 avail = std::min(buf.samples - chan.srcPlayedSamples, 2u); + const u32 avail = std::min(buf.samples - chan.playedSamples, 2u); if (!chan.mute && Memory::IsValidRange(addr, avail * stride)) { const s16_le *src = (const s16_le *)Memory::GetPointerUnchecked(addr); const int l0 = src[0]; @@ -496,22 +524,22 @@ static bool __AudioMixSRC(AudioChannel &chan) { const int r1 = avail > 1 ? (mono ? l1 : (int)src[stride / 2 + 1]) : r0; // 15 bits of fraction, not 16 - a full 16 would overflow the product against a // full-scale difference. - const int frac = (int)(chan.srcFrac >> 1); + const int frac = (int)(chan.frac >> 1); mixBuffer[out * 2] += ApplyChannelVolume(l0 + (((l1 - l0) * frac) >> 15), leftVol); mixBuffer[out * 2 + 1] += ApplyChannelVolume(r0 + (((r1 - r0) * frac) >> 15), rightVol); } - chan.srcFrac += ratio; - u32 step = chan.srcFrac >> 16; - chan.srcFrac &= 0xFFFF; - while (step > 0 && chan.srcBufferCount > 0) { - const u32 take = std::min(step, chan.srcBuffers[0].samples - chan.srcPlayedSamples); - chan.srcPlayedSamples += take; + chan.frac += ratio; + u32 step = chan.frac >> 16; + chan.frac &= 0xFFFF; + while (step > 0 && chan.bufferCount > 0) { + const u32 take = std::min(step, chan.buffers[0].samples - chan.playedSamples); + chan.playedSamples += take; step -= take; - if (chan.srcPlayedSamples >= chan.srcBuffers[0].samples) { - chan.srcBuffers[0] = chan.srcBuffers[1]; - chan.srcBufferCount--; - chan.srcPlayedSamples = 0; + if (chan.playedSamples >= chan.buffers[0].samples) { + chan.buffers[0] = chan.buffers[1]; + chan.bufferCount--; + chan.playedSamples = 0; woke |= __AudioSRCCompleted(chan); } } @@ -528,16 +556,16 @@ void __AudioUpdate(bool resetRecording) { audioMixing = true; bool woke = false; - for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX; i++) { + for (AudioChannel &chan : g_audioChans) { // Deliberately not gated on `reserved`: sceAudioChRelease only clears the // reservation, and a buffer already in flight keeps playing out. - woke |= __AudioMixChannel(g_audioChans[i]); + woke |= __AudioMixChannel(chan); } - woke |= __AudioMixSRC(g_audioChans[PSP_AUDIO_CHANNEL_SRC]); + woke |= __AudioMixSRC(g_audioSRC); audioMixing = false; if (woke) { - __KernelReSchedule("audio drain"); + __AudioReScheduleAfterWake(); } if (g_Config.bEnableSound) { diff --git a/Core/HLE/__sceAudio.h b/Core/HLE/__sceAudio.h index 9ae1e894e0..b27080c946 100644 --- a/Core/HLE/__sceAudio.h +++ b/Core/HLE/__sceAudio.h @@ -52,12 +52,13 @@ u32 __AudioEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int leftVol, int r // Channel 8 - Output2, SRC and Vaudio all share it. Two buffers fit; a third caller gets // SCE_ERROR_AUDIO_CHANNEL_BUSY without waiting. A successful call waits for one buffer to // finish before returning, except when nothing was playing to begin with. -u32 __AudioSRCEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int vol); +u32 __AudioSRCEnqueueBlocking(AudioSRCChannel &chan, u32 samplePtr, int vol); // Hands the SRC channel a completion that the next caller can consume without waiting. -void __AudioSRCSignal(AudioChannel &chan); +void __AudioSRCSignal(AudioSRCChannel &chan); -// Wakes whoever is parked on the channel with the given error, and forgets their buffer. -void __AudioWakeThreads(AudioChannel &chan, int result); +// Wake everyone parked on the SRC channel with the given error. Mixer channels need no +// equivalent: releasing one while a thread waits on it is refused outright. +void __AudioWakeThreads(AudioSRCChannel &chan, int result); void __AudioCPUMHzChange(); diff --git a/Core/HLE/sceAudio.cpp b/Core/HLE/sceAudio.cpp index 28c86535f8..aef17b8abe 100644 --- a/Core/HLE/sceAudio.cpp +++ b/Core/HLE/sceAudio.cpp @@ -44,8 +44,9 @@ struct AudioChannelWaitInfo { int numSamples; }; -// The extra channel is for SRC/Output2/Vaudio. -AudioChannel g_audioChans[PSP_AUDIO_CHANNEL_MAX + 1]; +AudioChannel g_audioChans[PSP_AUDIO_CHANNEL_MAX]; +// sceAudioOutput2, sceAudioSRC and sceVaudio are three names for this one channel. +AudioSRCChannel g_audioSRC; void AudioChannel::DoState(PointerWrap &p) { auto s = p.Section("AudioChannel", 1, 4); @@ -65,17 +66,6 @@ void AudioChannel::DoState(PointerWrap &p) { Do(p, waitingAddress); Do(p, waitingLeftVolume); Do(p, waitingRightVolume); - Do(p, srcBufferCount); - for (AudioPendingBuffer &buf : srcBuffers) { - Do(p, buf.address); - Do(p, buf.samples); - } - Do(p, srcPlayedSamples); - Do(p, srcFrac); - Do(p, srcCompletion); - Do(p, srcWaitingThreads); - Do(p, defaultRoutingMode); - Do(p, defaultRoutingVolMode); return; } @@ -86,6 +76,8 @@ void AudioChannel::DoState(PointerWrap &p) { std::vector oldWaitingThreads; Do(p, oldWaitingThreads); if (s >= 2) { + // These are globals rather than per-channel; the old format just happened to write + // them out once per channel. Do(p, defaultRoutingMode); Do(p, defaultRoutingVolMode); } @@ -98,15 +90,9 @@ void AudioChannel::DoState(PointerWrap &p) { } if (p.mode == p.MODE_READ) { - sampleAddress = 0; - remainingSamples = 0; - waitingThread = 0; - waitingAddress = 0; - srcBufferCount = 0; - srcPlayedSamples = 0; - srcFrac = 0; - srcCompletion = false; - srcWaitingThreads.clear(); + const u32 oldSampleCount = sampleCount; + clear(); + sampleCount = oldSampleCount; // The threads that were parked in a blocking output call are still parked, and // nothing is going to wake them now, so hand them their buffer back. for (const AudioChannelWaitInfo &waitInfo : oldWaitingThreads) { @@ -118,11 +104,6 @@ void AudioChannel::DoState(PointerWrap &p) { } } -void AudioChannel::reset() { - __AudioWakeThreads(*this, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED); - clear(); -} - void AudioChannel::clear() { reserved = false; leftVolume = 0; @@ -135,11 +116,58 @@ void AudioChannel::clear() { waitingAddress = 0; waitingLeftVolume = 0; waitingRightVolume = 0; - srcBufferCount = 0; - srcPlayedSamples = 0; - srcFrac = 0; - srcCompletion = false; - srcWaitingThreads.clear(); +} + +void AudioSRCChannel::DoState(PointerWrap &p) { + auto s = p.Section("AudioSRCChannel", 1); + if (!s) + return; + + Do(p, reserved); + Do(p, sampleCount); + Do(p, leftVolume); + Do(p, rightVolume); + Do(p, format); + Do(p, bufferCount); + for (AudioPendingBuffer &buf : buffers) { + Do(p, buf.address); + Do(p, buf.samples); + } + Do(p, playedSamples); + Do(p, frac); + Do(p, completion); + Do(p, waitingThreads); +} + +void __AudioRoutingDoState(PointerWrap &p) { + auto s = p.Section("sceAudioRouting", 1); + if (!s) + return; + + Do(p, defaultRoutingMode); + Do(p, defaultRoutingVolMode); +} + +void AudioSRCChannel::reset() { + __AudioWakeThreads(*this, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED); + clear(); +} + +void AudioSRCChannel::clear() { + reserved = false; + leftVolume = 0; + rightVolume = 0; + format = 0; + sampleCount = 0; + bufferCount = 0; + playedSamples = 0; + frac = 0; + completion = false; + waitingThreads.clear(); + for (AudioPendingBuffer &buf : buffers) { + buf.address = 0; + buf.samples = 0; + } } // The blocking output calls do not queue callers up. Each channel holds one buffer and at most @@ -330,7 +358,7 @@ static u32 sceAudioEnd() { } static u32 sceAudioOutput2Reserve(u32 sampleCount) { - auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2]; + auto &chan = g_audioSRC; // This seems to ignore the MSB, for some reason. sampleCount &= 0x7FFFFFFF; if (sampleCount < 17 || sampleCount > 4111) { @@ -354,10 +382,9 @@ static u32 sceAudioOutput2OutputBlocking(u32 vol, u32 dataPtr) { } hleEatCycles(10000); - u32 result = __AudioSRCEnqueueBlocking(g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2], dataPtr, vol); - if ((int)result < 0) - return hleLogError(Log::sceAudio, result); - return hleLogDebug(Log::sceAudio, result); + // A busy channel is an ordinary answer here that a game is expected to poll on, not a + // fault, so this stays at debug like the mixer channels rather than filling the error log. + return hleLogDebug(Log::sceAudio, __AudioSRCEnqueueBlocking(g_audioSRC, dataPtr, vol)); } static u32 sceAudioOutput2ChangeLength(u32 sampleCount) { @@ -366,7 +393,7 @@ static u32 sceAudioOutput2ChangeLength(u32 sampleCount) { if (sampleCount - 17 >= 0xFFF) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_OUTPUT_SAMPLE_DATA_SIZE_NOT_ALIGNED, "invalid sample count"); } - auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2]; + auto &chan = g_audioSRC; if (!chan.reserved) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved"); } @@ -377,20 +404,20 @@ static u32 sceAudioOutput2ChangeLength(u32 sampleCount) { } static u32 sceAudioOutput2GetRestSample() { - auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2]; + auto &chan = g_audioSRC; if (!chan.reserved) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved"); } // Counts armed DMA descriptors, in units of the current length - so it reports two // buffers' worth while both are in flight. - return hleLogDebug(Log::sceAudio, chan.srcBufferCount * chan.sampleCount); + return hleLogDebug(Log::sceAudio, chan.bufferCount * chan.sampleCount); } static u32 sceAudioOutput2Release() { - auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_OUTPUT2]; + auto &chan = g_audioSRC; if (!chan.reserved) return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved"); - if (chan.srcBufferCount != 0) + if (chan.bufferCount != 0) return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED, "output busy"); chan.reset(); @@ -424,7 +451,7 @@ bool SRCFrequencyAllowed(int freq) { } static u32 sceAudioSRCChReserve(u32 sampleCount, u32 freq, u32 format) { - auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_SRC]; + auto &chan = g_audioSRC; // This seems to ignore the MSB, for some reason. sampleCount &= 0x7FFFFFFF; if (format == 4) { @@ -449,10 +476,10 @@ static u32 sceAudioSRCChReserve(u32 sampleCount, u32 freq, u32 format) { } static u32 sceAudioSRCChRelease() { - auto &chan = g_audioChans[PSP_AUDIO_CHANNEL_SRC]; + auto &chan = g_audioSRC; if (!chan.reserved) return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED, "channel not reserved"); - if (chan.srcBufferCount != 0) + if (chan.bufferCount != 0) return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED, "output busy"); chan.reset(); @@ -467,10 +494,7 @@ static u32 sceAudioSRCOutputBlocking(u32 vol, u32 buf) { } hleEatCycles(10000); - u32 result = __AudioSRCEnqueueBlocking(g_audioChans[PSP_AUDIO_CHANNEL_SRC], buf, vol); - if ((int)result < 0) - return hleLogError(Log::sceAudio, result); - return hleLogDebug(Log::sceAudio, result); + return hleLogDebug(Log::sceAudio, __AudioSRCEnqueueBlocking(g_audioSRC, buf, vol)); } static int sceAudioInputBlocking(u32 maxSamples, u32 sampleRate, u32 bufAddr) { diff --git a/Core/HLE/sceAudio.h b/Core/HLE/sceAudio.h index 051f355bab..097b91ffee 100644 --- a/Core/HLE/sceAudio.h +++ b/Core/HLE/sceAudio.h @@ -27,11 +27,11 @@ class PointerWrap; enum PspAudioFormats { PSP_AUDIO_FORMAT_STEREO = 0, PSP_AUDIO_FORMAT_MONO = 0x10 }; enum PspAudioFrequencies { PSP_AUDIO_FREQ_44K = 44100, PSP_AUDIO_FREQ_48K = 48000 }; +// Channels 0-7, the ones the software mixer walks. const u32 PSP_AUDIO_CHANNEL_MAX = 8; -const int PSP_AUDIO_CHANNEL_SRC = 8; -const int PSP_AUDIO_CHANNEL_OUTPUT2 = 8; -const int PSP_AUDIO_CHANNEL_VAUDIO = 8; +// Mixer channels wait on their own index plus one, so the SRC channel takes the id after them. +const int PSP_AUDIO_SRC_WAIT_ID = PSP_AUDIO_CHANNEL_MAX + 1; // One buffer handed over and not yet fully played. struct AudioPendingBuffer { @@ -39,10 +39,9 @@ struct AudioPendingBuffer { u32 samples; }; -// Mirrors the 16-byte channel struct the audio driver keeps, plus the two DMA descriptors it -// uses for the SRC/Output2 channel. See docs/sceAudio.md for how the hardware behaves; the -// short version is that it plays out of the game's own memory rather than taking a copy, and -// that it holds one buffer per mixer channel and two for SRC. +// A channel the software mixer walks. It holds exactly one buffer, played straight out of the +// game's memory 64 samples at a time, and has room for exactly one parked thread. See +// docs/sceAudio.md. struct AudioChannel { int index = 0; bool reserved = false; @@ -55,47 +54,67 @@ struct AudioChannel { // For the debugger only. Not saved. bool mute = false; - // Channels 0-7. sampleAddress walks forward as the mixer consumes the buffer and drops - // back to zero when remainingSamples runs out. An output with a null pointer sets - // remainingSamples but leaves sampleAddress at zero, which is the one case where the two - // rest-length calls disagree. + // sampleAddress walks forward as the mixer consumes the buffer and drops back to zero when + // remainingSamples runs out. An output with a null pointer sets remainingSamples but leaves + // sampleAddress at zero, which is the one case where the two rest-length calls disagree. u32 sampleAddress = 0; u32 remainingSamples = 0; - // Only one thread can be parked in a blocking output call on a channel. A second one is - // told the channel is busy rather than queueing up behind the first. These remember what - // it wanted to hand over, so the enqueue can be retried when the buffer finishes. + // A second thread arriving while one is parked here is told the channel is busy rather than + // queueing up behind it. These remember what the parked one wanted to hand over, so the + // enqueue can be retried once the buffer finishes. SceUID waitingThread = 0; u32 waitingAddress = 0; int waitingLeftVolume = 0; int waitingRightVolume = 0; - // Channel 8 (Output2/SRC/Vaudio) instead has two DMA descriptors, so two buffers can be - // in flight at once and the third caller is turned away. - AudioPendingBuffer srcBuffers[2]{}; - int srcBufferCount = 0; - u32 srcPlayedSamples = 0; // Consumed from srcBuffers[0]. - u32 srcFrac = 0; // 16.16 position between two input samples, for resampling. + void DoState(PointerWrap &p); + + void clear(); +}; + +// Channel 8, and there is only one of it: sceAudioOutput2, sceAudioSRC and sceVaudio are three +// names for the same channel. It skips the mixer entirely - its two DMA descriptors point +// straight at the game's buffers and the codec resamples them - so it shares nothing with the +// mixer channels beyond what reserve agrees on. Two buffers fit; a third caller is turned away. +struct AudioSRCChannel { + bool reserved = false; + + u32 sampleCount = 0; // Buffer size agreed at reserve time. + u32 leftVolume = 0; + u32 rightVolume = 0; + u32 format = 0; + + // For the debugger only. Not saved. + bool mute = false; + + AudioPendingBuffer buffers[2]{}; + int bufferCount = 0; + u32 playedSamples = 0; // Consumed from buffers[0]. + u32 frac = 0; // 16.16 position between two input samples, for resampling. // The driver signals a finished buffer with an event flag bit, so one completion can sit // there unclaimed - which is why the first output after an idle period doesn't block. - bool srcCompletion = false; - std::vector srcWaitingThreads; + bool completion = false; + std::vector waitingThreads; void DoState(PointerWrap &p); void reset(); void clear(); - bool SRCFull() const { - return srcBufferCount >= (int)ARRAY_SIZE(srcBuffers); + bool Full() const { + return bufferCount >= (int)ARRAY_SIZE(buffers); } }; -// The extra channel is for SRC/Output2/Vaudio (who all share, apparently.) -extern AudioChannel g_audioChans[PSP_AUDIO_CHANNEL_MAX + 1]; +extern AudioChannel g_audioChans[PSP_AUDIO_CHANNEL_MAX]; +extern AudioSRCChannel g_audioSRC; // The sample rates the SRC and VAUDIO channels will accept. bool SRCFrequencyAllowed(int freq); +// The two routing modes are globals rather than per-channel; they get their own little block. +void __AudioRoutingDoState(PointerWrap &p); + void Register_sceAudio(); diff --git a/Core/HLE/sceVaudio.cpp b/Core/HLE/sceVaudio.cpp index f6552f027f..f8207067ba 100644 --- a/Core/HLE/sceVaudio.cpp +++ b/Core/HLE/sceVaudio.cpp @@ -64,17 +64,17 @@ static u32 sceVaudioChReserve(int sampleCount, int freq, int format) { return SCE_KERNEL_ERROR_BUSY; } // We still have to check the channel also, which gives a different error. - if (g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].reserved) { + if (g_audioSRC.reserved) { ERROR_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i) - channel already reserved", sampleCount, freq, format); return SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED; } DEBUG_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i)", sampleCount, freq, format); - g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].clear(); - g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].reserved = true; - g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].sampleCount = sampleCount; - g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].format = format == 2 ? PSP_AUDIO_FORMAT_STEREO : PSP_AUDIO_FORMAT_MONO; - g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].leftVolume = 0; - g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].rightVolume = 0; + g_audioSRC.clear(); + g_audioSRC.reserved = true; + g_audioSRC.sampleCount = sampleCount; + g_audioSRC.format = format == 2 ? PSP_AUDIO_FORMAT_STEREO : PSP_AUDIO_FORMAT_MONO; + g_audioSRC.leftVolume = 0; + g_audioSRC.rightVolume = 0; vaudioReserved = true; __AudioSetSRCFrequency(freq); return 0; @@ -82,10 +82,10 @@ static u32 sceVaudioChReserve(int sampleCount, int freq, int format) { static u32 sceVaudioChRelease() { DEBUG_LOG(Log::sceAudio, "sceVaudioChRelease(...)"); - if (!g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].reserved) { + if (!g_audioSRC.reserved) { return SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED; } else { - g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO].reset(); + g_audioSRC.reset(); vaudioReserved = false; return 0; } @@ -94,7 +94,7 @@ static u32 sceVaudioChRelease() { static u32 sceVaudioOutputBlocking(int vol, u32 buffer) { DEBUG_LOG(Log::sceAudio, "sceVaudioOutputBlocking(%i, %08x)", vol, buffer); // Shares the SRC channel, so it also shares the two-buffer depth and the busy return. - return __AudioSRCEnqueueBlocking(g_audioChans[PSP_AUDIO_CHANNEL_VAUDIO], buffer, vol); + return __AudioSRCEnqueueBlocking(g_audioSRC, buffer, vol); } static u32 sceVaudioSetEffectType(int effectType, int vol) { diff --git a/UI/ImDebugger/ImDebugger.cpp b/UI/ImDebugger/ImDebugger.cpp index 92cfae97d5..d0e2850da7 100644 --- a/UI/ImDebugger/ImDebugger.cpp +++ b/UI/ImDebugger/ImDebugger.cpp @@ -1630,58 +1630,76 @@ void DrawAudioChannels(ImConfig &cfg, ImControl &control) { ImGui::TableHeadersRow(); - // vaudio / output2 uses channel 8. - for (int i = 0; i < PSP_AUDIO_CHANNEL_MAX + 1; i++) { - if (!g_audioChans[i].reserved) { + static const auto formatName = [](u32 format) { + switch (format) { + case PSP_AUDIO_FORMAT_STEREO: return "Stereo"; + case PSP_AUDIO_FORMAT_MONO: return "Mono"; + default: return "UNK"; + } + }; + static const auto threadName = [](SceUID threadID) -> const char * { + KernelObject *thread = kernelObjects.GetFast(threadID); + return thread ? thread->GetName() : nullptr; + }; + + for (int i = 0; i < (int)PSP_AUDIO_CHANNEL_MAX; i++) { + const AudioChannel &chan = g_audioChans[i]; + if (!chan.reserved) { continue; } ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::PushID(i); - if (i == 8) { - ImGui::TextUnformatted("audio2"); - } else { - ImGui::Text("%d", i); - } + ImGui::Text("%d", i); ImGui::TableNextColumn(); ImGui::Checkbox("", &g_audioChans[i].mute); ImGui::TableNextColumn(); char id[2]{}; id[0] = i + 1; - ImClickableValue(id, g_audioChans[i].sampleAddress, control, ImCmd::SHOW_IN_MEMORY_VIEWER); + ImClickableValue(id, chan.sampleAddress, control, ImCmd::SHOW_IN_MEMORY_VIEWER); ImGui::TableNextColumn(); - ImGui::Text("%08x", g_audioChans[i].sampleCount); + ImGui::Text("%08x", chan.sampleCount); ImGui::TableNextColumn(); - ImGui::Text("%d | %d", g_audioChans[i].leftVolume, g_audioChans[i].rightVolume); + ImGui::Text("%d | %d", chan.leftVolume, chan.rightVolume); ImGui::TableNextColumn(); - switch (g_audioChans[i].format) { - case PSP_AUDIO_FORMAT_STEREO: - ImGui::TextUnformatted("Stereo"); - break; - case PSP_AUDIO_FORMAT_MONO: - ImGui::TextUnformatted("Mono"); - break; - default: - ImGui::TextUnformatted("UNK: %04x"); - break; - } + ImGui::TextUnformatted(formatName(chan.format)); ImGui::TableNextColumn(); - // At most one thread can be parked on a mixer channel; the SRC channel keeps its - // own list instead. - SceUID waiting[3]; - int waitingCount = 0; - if (g_audioChans[i].waitingThread != 0) { - waiting[waitingCount++] = g_audioChans[i].waitingThread; - } - for (SceUID t : g_audioChans[i].srcWaitingThreads) { - if (waitingCount < ARRAY_SIZE(waiting)) { - waiting[waitingCount++] = t; + // Only one thread can ever be parked on a mixer channel. + if (chan.waitingThread != 0) { + const char *name = threadName(chan.waitingThread); + if (name) { + ImGui::TextUnformatted(name); } } - for (int w = 0; w < waitingCount; w++) { - KernelObject *thread = kernelObjects.GetFast(waiting[w]); - if (thread) { - ImGui::TextUnformatted(thread->GetName()); + ImGui::PopID(); + } + + // Output2, SRC and Vaudio all mean this one, which holds two buffers rather than one + // and can have more than one thread waiting on it. + if (g_audioSRC.reserved) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::PushID("src"); + ImGui::TextUnformatted("audio2"); + ImGui::TableNextColumn(); + ImGui::Checkbox("", &g_audioSRC.mute); + ImGui::TableNextColumn(); + if (g_audioSRC.bufferCount > 0) { + ImClickableValue("src", g_audioSRC.buffers[0].address, control, ImCmd::SHOW_IN_MEMORY_VIEWER); + } else { + ImGui::TextUnformatted("-"); + } + ImGui::TableNextColumn(); + ImGui::Text("%08x (%d queued)", g_audioSRC.sampleCount, g_audioSRC.bufferCount); + ImGui::TableNextColumn(); + ImGui::Text("%d | %d", g_audioSRC.leftVolume, g_audioSRC.rightVolume); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(formatName(g_audioSRC.format)); + ImGui::TableNextColumn(); + for (SceUID threadID : g_audioSRC.waitingThreads) { + const char *name = threadName(threadID); + if (name) { + ImGui::TextUnformatted(name); } } ImGui::PopID(); diff --git a/docs/sceAudio.md b/docs/sceAudio.md index 971ce10bf3..4867496c69 100644 --- a/docs/sceAudio.md +++ b/docs/sceAudio.md @@ -10,7 +10,9 @@ Output2/SRC channel holds two, and a caller who finds no room is told ## Two different pieces of hardware -`sceAudio` presents nine channels, but they are not the same thing underneath. +`sceAudio` presents nine channels, but they are not the same thing underneath, which is why the +emulator keeps them in two different structures rather than one array of nine: `AudioChannel` +for the eight the mixer walks, and a single `AudioSRCChannel` for the ninth. **Channels 0-7** go through a software mixer running as a kernel thread. Every time the audio DMA finishes a block, that thread wakes, takes up to 64 samples from each channel that has a @@ -150,7 +152,8 @@ whatever the output is currently running at - which is how 44100 and 0 get throu ## Savestates -`AudioChannel` is at section version 4. Anything older stored a ring of already-mixed samples, -which cannot be turned back into a buffer pointer and a position, so loading one drops the -pending audio and wakes any parked threads. That costs a fraction of a second of silence on -load and nothing else. +`AudioChannel` is at section version 4 and the SRC channel has a section of its own. Anything +older stored a ring of already-mixed samples, which cannot be turned back into a buffer pointer +and a position, so loading one drops the pending audio and wakes any parked threads. That costs +a fraction of a second of silence on load and nothing else. Older states also carry the SRC +channel as a ninth entry in the channel array, which is read and discarded. From 412d11534c67735020c7ab87977b2fd8669d831a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 10 Sep 2026 12:39:41 -0600 Subject: [PATCH 3/7] sceAudio: drain on vaudio release, and charge what an output call really costs Two things the last pass looked at and left, now measured on hardware by the new audio/blocking/vaudio and audio/blocking/overhead. sceVaudioChRelease is not shaped like the Output2 and SRC releases. It hands the channel a null pointer first, which waits for a buffer to finish, so it blocks for one buffer and returns 0 where the others would refuse - and the buffer is played out instead of being dropped, which is what we were doing. The reservation goes now rather than when the drain finishes: the caller is parked either way and gets the same answer at the same time, and the buffers keep playing because the mixer does not look at the reservation. The same test turned up two more: a *failed* sceVaudioChReserve still marks vaudio reserved, so a caller that lost the channel to Output2 is told 0x80000021 next time until a release clears it; and sceVaudioChRelease ignores that flag entirely and releases whatever holds the SRC channel, which pspautotests already called the "wrong release". The flat 10000 cycles charged to every Output2 and SRC output turns out to be right only for the refused case. Every other outcome ends up querying the codec and costs over 100us on hardware, including finding the channel unreserved, so those now cost 25000. Mixer channels are the other way round - cheap to refuse, expensive on the one output that starts the DMA - so that charge moved to the DMA start. F1 2009 behaves identically and Burnout Dominator's mixed output is bit-identical to before. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/__sceAudio.cpp | 11 +++++++++++ Core/HLE/__sceAudio.h | 3 +++ Core/HLE/sceAudio.cpp | 4 ++-- Core/HLE/sceVaudio.cpp | 43 ++++++++++++++++++++++++++++++----------- pspautotests | 2 +- test.py | 2 ++ 6 files changed, 51 insertions(+), 14 deletions(-) diff --git a/Core/HLE/__sceAudio.cpp b/Core/HLE/__sceAudio.cpp index 78b77bada1..584fb3dbcd 100644 --- a/Core/HLE/__sceAudio.cpp +++ b/Core/HLE/__sceAudio.cpp @@ -246,6 +246,9 @@ static void __AudioStartMixerDMA() { if (audioMixing) { return; } + // A mixer output is cheap - well under 10us - except for this one, which brings the DMA and + // the codec up and costs over 100us on hardware. + hleEatCycles(25000); CoreTiming::UnscheduleEvent(eventAudioUpdate, 0); audioMixingInSyscall = true; __AudioUpdate(); @@ -342,6 +345,14 @@ static bool __AudioChannelFinished(AudioChannel &chan) { return true; } +// Measured on hardware with tests/audio/blocking/overhead. Every SRC output ends up querying +// the codec, which costs upwards of 100us, whether it armed a buffer, found the channel +// unreserved, or did nothing at all. The one shortcut is a channel with both descriptors +// already armed, which lands in the 30-100us range instead. +int __AudioSRCCallCycles(const AudioSRCChannel &chan) { + return chan.Full() ? 10000 : 25000; +} + u32 __AudioSRCEnqueueBlocking(AudioSRCChannel &chan, u32 samplePtr, int vol) { if (!chan.reserved) { return SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED; diff --git a/Core/HLE/__sceAudio.h b/Core/HLE/__sceAudio.h index b27080c946..f39eb32e48 100644 --- a/Core/HLE/__sceAudio.h +++ b/Core/HLE/__sceAudio.h @@ -55,6 +55,9 @@ u32 __AudioEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int leftVol, int r u32 __AudioSRCEnqueueBlocking(AudioSRCChannel &chan, u32 samplePtr, int vol); // Hands the SRC channel a completion that the next caller can consume without waiting. void __AudioSRCSignal(AudioSRCChannel &chan); +// What one SRC/Output2 output call costs, in cycles, before it does anything - see the comment +// on the definition. +int __AudioSRCCallCycles(const AudioSRCChannel &chan); // Wake everyone parked on the SRC channel with the given error. Mixer channels need no // equivalent: releasing one while a thread waits on it is refused outright. diff --git a/Core/HLE/sceAudio.cpp b/Core/HLE/sceAudio.cpp index aef17b8abe..44f1174d0b 100644 --- a/Core/HLE/sceAudio.cpp +++ b/Core/HLE/sceAudio.cpp @@ -381,7 +381,7 @@ static u32 sceAudioOutput2OutputBlocking(u32 vol, u32 dataPtr) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume"); } - hleEatCycles(10000); + hleEatCycles(__AudioSRCCallCycles(g_audioSRC)); // A busy channel is an ordinary answer here that a game is expected to poll on, not a // fault, so this stays at debug like the mixer channels rather than filling the error log. return hleLogDebug(Log::sceAudio, __AudioSRCEnqueueBlocking(g_audioSRC, dataPtr, vol)); @@ -493,7 +493,7 @@ static u32 sceAudioSRCOutputBlocking(u32 vol, u32 buf) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume"); } - hleEatCycles(10000); + hleEatCycles(__AudioSRCCallCycles(g_audioSRC)); return hleLogDebug(Log::sceAudio, __AudioSRCEnqueueBlocking(g_audioSRC, buf, vol)); } diff --git a/Core/HLE/sceVaudio.cpp b/Core/HLE/sceVaudio.cpp index f8207067ba..71ccce2dbf 100644 --- a/Core/HLE/sceVaudio.cpp +++ b/Core/HLE/sceVaudio.cpp @@ -55,14 +55,20 @@ static u32 sceVaudioChReserve(int sampleCount, int freq, int format) { ERROR_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i) - unexpected format", sampleCount, freq, format); return SCE_KERNEL_ERROR_INVALID_FORMAT; } - if (freq != 0 && !SRCFrequencyAllowed(freq)) { - ERROR_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i) - invalid frequency", sampleCount, freq, format); - return SCE_ERROR_AUDIO_INVALID_FREQUENCY; - } if (vaudioReserved) { ERROR_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i) - already reserved", sampleCount, freq, format); return SCE_KERNEL_ERROR_BUSY; } + + // Everything past here is the underlying sceAudioSRCChReserve, and the module marks itself + // reserved before handing over - it does not undo that when the reserve fails. So a caller + // that got 0x80268002 because Output2 held the channel is told 0x80000021 next time round, + // until a release clears it. + vaudioReserved = true; + if (freq != 0 && !SRCFrequencyAllowed(freq)) { + ERROR_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i) - invalid frequency", sampleCount, freq, format); + return SCE_ERROR_AUDIO_INVALID_FREQUENCY; + } // We still have to check the channel also, which gives a different error. if (g_audioSRC.reserved) { ERROR_LOG(Log::sceAudio, "sceVaudioChReserve(%i, %i, %i) - channel already reserved", sampleCount, freq, format); @@ -73,22 +79,37 @@ static u32 sceVaudioChReserve(int sampleCount, int freq, int format) { g_audioSRC.reserved = true; g_audioSRC.sampleCount = sampleCount; g_audioSRC.format = format == 2 ? PSP_AUDIO_FORMAT_STEREO : PSP_AUDIO_FORMAT_MONO; - g_audioSRC.leftVolume = 0; - g_audioSRC.rightVolume = 0; - vaudioReserved = true; __AudioSetSRCFrequency(freq); return 0; } static u32 sceVaudioChRelease() { DEBUG_LOG(Log::sceAudio, "sceVaudioChRelease(...)"); + // Not gated on vaudio's own flag: the release goes straight at the SRC channel, so it will + // happily release a reservation that Output2 made. pspautotests calls that the "wrong + // release" and the hardware allows it. + vaudioReserved = false; if (!g_audioSRC.reserved) { return SCE_ERROR_AUDIO_CHANNEL_NOT_RESERVED; - } else { - g_audioSRC.reset(); - vaudioReserved = false; - return 0; } + + // Unlike the Output2 and SRC releases, which refuse while a buffer is in flight, this one + // hands the channel a null pointer first. That parks the caller until a buffer finishes, so + // what was playing is played out instead of being cut off. + __AudioSRCEnqueueBlocking(g_audioSRC, 0, -1); + if (g_audioSRC.Full()) { + // One drain was not enough to free a descriptor, so the release itself fails. + return SCE_ERROR_AUDIO_CHANNEL_ALREADY_RESERVED; + } + + // The reservation goes now rather than when the drain finishes. The caller is parked either + // way and gets the same answer at the same time; the buffers keep playing because the mixer + // does not look at the reservation. Only another thread peeking during that window could + // tell the difference. + g_audioSRC.reserved = false; + g_audioSRC.sampleCount = 0; + __AudioSRCSignal(g_audioSRC); + return 0; } static u32 sceVaudioOutputBlocking(int vol, u32 buffer) { diff --git a/pspautotests b/pspautotests index 3d55f06ed5..22c5bd9677 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 3d55f06ed5c253f3603f5c0517da09830a9f0b28 +Subproject commit 22c5bd96777d9d52396d10d37c7c59709623ad73 diff --git a/test.py b/test.py index 1904aba777..4aa6a6bd58 100755 --- a/test.py +++ b/test.py @@ -116,7 +116,9 @@ tests_good = [ "audio/blocking/contend", "audio/blocking/depth", "audio/blocking/errors", + "audio/blocking/overhead", "audio/blocking/restlen", + "audio/blocking/vaudio", "audio/sceaudio/datalen", "audio/output2/changelength", "audio/output2/release", From bfbede717f47b4cf3b009241a39588ee34071b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 10 Sep 2026 12:40:01 -0600 Subject: [PATCH 4/7] docs/sceAudio: record what vaudio does differently, and what a call costs Co-Authored-By: Claude Opus 5 (1M context) --- docs/sceAudio.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/sceAudio.md b/docs/sceAudio.md index 4867496c69..c6ce676b91 100644 --- a/docs/sceAudio.md +++ b/docs/sceAudio.md @@ -150,6 +150,35 @@ checks is observable. Accepted SRC frequencies are 8000, 11025, 12000, 16000, 22050, 24000, 32000 and 48000, plus whatever the output is currently running at - which is how 44100 and 0 get through. +## sceVaudio is a third shape + +`sceVaudio` reserves the same channel as Output2 and SRC, but its own release is not the same +call. It hands the channel a null pointer first - the drain idiom above - and only then does the +ordinary SRC release. So it blocks for one buffer and returns 0 where the other two would refuse +with `80268002`, and whatever was playing is played out rather than cut off. It also ignores its +own reservation flag and releases whatever holds the SRC channel, which is why +`tests/audio/sceaudio/reserve` gets away with what it calls the "wrong release". + +`sceVaudioChReserve` marks vaudio reserved *before* delegating and does not undo that when the +delegate fails, so a caller that got `80268002` because Output2 held the channel is told +`80000021` next time round until a release clears it. + +## What a call costs + +Worth knowing because a game whose losing thread retries in a loop feels it directly. +`tests/audio/blocking/overhead` measures it in buckets: + +| call | cost | +|---|---| +| Output2/SRC output, both descriptors already armed | 30-100us | +| Output2/SRC output, any other outcome, including an unreserved channel | over 100us | +| mixer output that is refused, blocking or not | under 10us | +| mixer output that starts the DMA | over 100us | + +Every SRC output ends up querying the codec, which is the slow part; only the refused case gets +out before it. Mixer channels are the other way round - cheap unless the call is the one that +brings the DMA up. + ## Savestates `AudioChannel` is at section version 4 and the SRC channel has a section of its own. Anything From 84bd8459e9501d41768f823d19847c51e9c38268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 10 Sep 2026 12:56:15 -0600 Subject: [PATCH 5/7] sceAudio: implement sceAudioOneshotOutput, and cover the rest of the channel rules Another pass looking for gaps, all of it now recorded by audio/blocking/channels and audio/blocking/oneshot. sceAudioOneshotOutput was the last unimplemented entry in the module, returning "library not linked" to anyone who called it. It plays one buffer on a channel it never reserves, so the channel frees itself when the buffer runs out, and its argument checks are their own set: any positive sample count, aligned or not, no upper bound, a negative volume rejected rather than skipped, and no busy check at all. No game is known to use it; it is implemented because tracing it turned out to be cheap, not because anything needed it. The channels test confirms four behaviors: sceAudioChReserve(-1) skips a released channel that is still playing while a reserve of it succeeds, a second channel joining a running mixer doesn't lose a block unlike a first, mono counts down in the same 64-sample steps over the same time as stereo, and the panned blocking output has no extra delay on the high channels. Also: the SRC resampler now interpolates into the next buffer at a buffer join instead of holding the last sample, since the codec reads the two descriptors as one stream. That only shows up at non-native rates and no test can see it. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/FunctionWrappers.h | 5 +++++ Core/HLE/__sceAudio.cpp | 42 ++++++++++++++++++++++++++++++------- Core/HLE/__sceAudio.h | 3 +++ Core/HLE/sceAudio.cpp | 33 +++++++++++++++++++++++++++-- docs/sceAudio.md | 12 +++++++++++ pspautotests | 2 +- test.py | 2 ++ 7 files changed, 89 insertions(+), 10 deletions(-) diff --git a/Core/HLE/FunctionWrappers.h b/Core/HLE/FunctionWrappers.h index 926ed29f72..fc7765fd23 100644 --- a/Core/HLE/FunctionWrappers.h +++ b/Core/HLE/FunctionWrappers.h @@ -307,6 +307,11 @@ template void WrapU_UIIIII() { RETURN(retval); } +template void WrapU_IIIIIU() { + u32 retval = func(PARAM(0), PARAM(1), PARAM(2), PARAM(3), PARAM(4), PARAM(5)); + RETURN(retval); +} + template void WrapU_UIIIU() { u32 retval = func(PARAM(0), PARAM(1), PARAM(2), PARAM(3), PARAM(4)); RETURN(retval); diff --git a/Core/HLE/__sceAudio.cpp b/Core/HLE/__sceAudio.cpp index 584fb3dbcd..78b4a7ab85 100644 --- a/Core/HLE/__sceAudio.cpp +++ b/Core/HLE/__sceAudio.cpp @@ -298,6 +298,22 @@ u32 __AudioEnqueue(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol) return chan.sampleCount; } +void __AudioEnqueueOneshot(AudioChannel &chan, u32 samplePtr, u32 sampleCount, u32 format, int leftVol, int rightVol) { + // No reservation and no busy check - handing a channel a second one while the first is + // still playing simply replaces it. sampleCount deliberately stays zero on the channel, + // which is what lets it go back to being free once the buffer runs out. + chan.format = format; + chan.leftVolume = leftVol; + chan.rightVolume = rightVol; + chan.remainingSamples = sampleCount; + + const bool startsDMA = samplePtr != 0 && !__AudioAnyChannelPlaying(); + chan.sampleAddress = samplePtr; + if (startsDMA) { + __AudioStartMixerDMA(); + } +} + u32 __AudioEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol) { u32 result = __AudioEnqueue(chan, samplePtr, leftVol, rightVol); if (result != SCE_ERROR_AUDIO_CHANNEL_BUSY) { @@ -508,6 +524,8 @@ static bool __AudioMixSRC(AudioSRCChannel &chan) { // Zero means "whatever the output is running at", so no conversion. const int inRate = srcFrequency != 0 ? srcFrequency : mixFrequency; const u32 ratio = (u32)(((u64)(u32)inRate << 16) / (u32)mixFrequency); + // At the output rate the fraction never moves off zero, so there is nothing to interpolate. + const bool resampling = ratio != 0x10000; const bool mono = chan.format == PSP_AUDIO_FORMAT_MONO; const u32 stride = mono ? 2 : 4; const int leftVol = chan.leftVolume; @@ -523,16 +541,26 @@ static bool __AudioMixSRC(AudioSRCChannel &chan) { const AudioPendingBuffer &buf = chan.buffers[0]; const u32 addr = buf.address + chan.playedSamples * stride; - // Interpolating against the following sample matters when a game reserved 22050Hz - // or similar; at the native rate the fraction is always zero and this reduces to a - // plain copy. - const u32 avail = std::min(buf.samples - chan.playedSamples, 2u); - if (!chan.mute && Memory::IsValidRange(addr, avail * stride)) { + if (!chan.mute && Memory::IsValidRange(addr, stride)) { const s16_le *src = (const s16_le *)Memory::GetPointerUnchecked(addr); const int l0 = src[0]; const int r0 = mono ? l0 : src[1]; - const int l1 = avail > 1 ? (int)src[stride / 2] : l0; - const int r1 = avail > 1 ? (mono ? l1 : (int)src[stride / 2 + 1]) : r0; + int l1 = l0; + int r1 = r0; + if (resampling) { + // Interpolate against the sample after this one. At the end of a buffer that + // is the start of the next, since the codec reads the two descriptors as one + // unbroken stream - holding the last sample instead would tick at every join. + u32 nextAddr = addr + stride; + if (chan.playedSamples + 1 >= buf.samples) { + nextAddr = chan.bufferCount > 1 ? chan.buffers[1].address : 0; + } + if (nextAddr != 0 && Memory::IsValidRange(nextAddr, stride)) { + const s16_le *next = (const s16_le *)Memory::GetPointerUnchecked(nextAddr); + l1 = next[0]; + r1 = mono ? l1 : next[1]; + } + } // 15 bits of fraction, not 16 - a full 16 would overflow the product against a // full-scale difference. const int frac = (int)(chan.frac >> 1); diff --git a/Core/HLE/__sceAudio.h b/Core/HLE/__sceAudio.h index f39eb32e48..efe022b9ae 100644 --- a/Core/HLE/__sceAudio.h +++ b/Core/HLE/__sceAudio.h @@ -45,6 +45,9 @@ void __AudioSetSRCFrequency(int freq); // count, or SCE_ERROR_AUDIO_CHANNEL_BUSY when a buffer is already in flight. A negative // volume means "leave it alone". u32 __AudioEnqueue(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol); +// sceAudioOneshotOutput's variant: hands over a buffer without a reservation, so it carries its +// own sample count and format and never reports busy. +void __AudioEnqueueOneshot(AudioChannel &chan, u32 samplePtr, u32 sampleCount, u32 format, int leftVol, int rightVol); // Same, but parks the calling thread until the channel frees up. Only one thread can be // parked; a second one gets SCE_ERROR_AUDIO_CHANNEL_BUSY straight back. u32 __AudioEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol); diff --git a/Core/HLE/sceAudio.cpp b/Core/HLE/sceAudio.cpp index 44f1174d0b..660fec804e 100644 --- a/Core/HLE/sceAudio.cpp +++ b/Core/HLE/sceAudio.cpp @@ -330,6 +330,34 @@ static u32 sceAudioChangeChannelConfig(u32 chan, u32 format) { return hleLogDebug(Log::sceAudio, 0); } +// Plays one buffer on a channel without reserving it, so the channel frees itself once the +// buffer runs out. The checks are looser than sceAudioChReserve's - any positive sample count +// goes, aligned or not - and stricter on volume, where a negative one is an error rather than +// meaning "leave it alone". +static u32 sceAudioOneshotOutput(int chan, int sampleCount, int format, int leftvol, int rightvol, u32 samplePtr) { + if ((u32)leftvol > 0xFFFF || (u32)rightvol > 0xFFFF) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid volume"); + } + if (chan < 0) { + chan = GetFreeChannel(); + if (chan < 0) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_NO_CHANNELS_AVAILABLE, "no channels remaining"); + } + } else if (chan >= (int)PSP_AUDIO_CHANNEL_MAX || g_audioChans[chan].reserved) { + // A reserved channel is refused with the same error as one that doesn't exist. + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_CHANNEL, "bad channel %d", chan); + } + if (sampleCount <= 0) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_OUTPUT_SAMPLE_DATA_SIZE_NOT_ALIGNED, "invalid sample count"); + } + if (format != PSP_AUDIO_FORMAT_MONO && format != PSP_AUDIO_FORMAT_STEREO) { + return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_FORMAT, "invalid format"); + } + + __AudioEnqueueOneshot(g_audioChans[chan], samplePtr, sampleCount, format, leftvol, rightvol); + return hleLogDebug(Log::sceAudio, chan); +} + static u32 sceAudioChangeChannelVolume(u32 chan, int leftvol, int rightvol) { if (leftvol > 0xFFFF || rightvol > 0xFFFF) { return hleLogError(Log::sceAudio, SCE_ERROR_AUDIO_INVALID_VOLUME, "invalid chan %d volume %d %d", chan, leftvol, rightvol); @@ -595,8 +623,9 @@ const HLEFunction sceAudio[] = {0X5C37C0AE, &WrapU_V, "sceAudioSRCChRelease", 'x', "" }, {0XE0727056, &WrapU_UU, "sceAudioSRCOutputBlocking", 'x', "xx" }, - // Never seen these used - {0X41EFADE7, nullptr, "sceAudioOneshotOutput", '?', "" }, + {0X41EFADE7, &WrapU_IIIIIU, "sceAudioOneshotOutput", 'x', "iiiiix"}, + + // Never seen this used {0XB61595C0, nullptr, "sceAudioLoopbackTest", '?', "" }, // Microphone interface diff --git a/docs/sceAudio.md b/docs/sceAudio.md index c6ce676b91..a99c7598c1 100644 --- a/docs/sceAudio.md +++ b/docs/sceAudio.md @@ -150,6 +150,18 @@ checks is observable. Accepted SRC frequencies are 8000, 11025, 12000, 16000, 22050, 24000, 32000 and 48000, plus whatever the output is currently running at - which is how 44100 and 0 get through. +## sceAudioOneshotOutput + +The odd one out: it plays a single buffer on a channel it never reserves. The channel stays +free as far as everything else is concerned, so `sceAudioChRelease` on it answers `80260008`, +and it returns to the pool by itself once the buffer runs out. It returns the channel number, +takes any positive sample count - alignment and the 0xFFC0 ceiling do not apply - and unlike +the other outputs a negative volume is an error rather than "leave it alone". There is no busy +check either: a second one-shot while the first is playing just replaces it. + +No game is known to call it. It is implemented because the behavior turned out to be simple +once traced, not because anything needed it. + ## sceVaudio is a third shape `sceVaudio` reserves the same channel as Output2 and SRC, but its own release is not the same diff --git a/pspautotests b/pspautotests index 22c5bd9677..4eb50721ea 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 22c5bd96777d9d52396d10d37c7c59709623ad73 +Subproject commit 4eb50721ea35a57f786b4230d2d852896ddfb74b diff --git a/test.py b/test.py index 4aa6a6bd58..62a540448d 100755 --- a/test.py +++ b/test.py @@ -114,9 +114,11 @@ tests_good = [ "audio/mp3/setloopnum", "audio/mp3/stream", "audio/blocking/contend", + "audio/blocking/channels", "audio/blocking/depth", "audio/blocking/errors", "audio/blocking/overhead", + "audio/blocking/oneshot", "audio/blocking/restlen", "audio/blocking/vaudio", "audio/sceaudio/datalen", From c1bbd6ae2b9d1d472bb3a77b9512856d6e94b6a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 10 Sep 2026 16:31:31 -0600 Subject: [PATCH 6/7] sceAudio: stop dropping the reservation when loading an older savestate Loading a state written by a released build left every channel unreserved, so the game's next output came back SCE_ERROR_AUDIO_CHANNEL_NOT_INIT and the audio never recovered. The old format's reserved flag, sample count, volumes and format are read at the top of AudioChannel::DoState and are perfectly good. Only the play position cannot be reconstructed. The conversion path used clear() to zero the fields that had no equivalent, which threw the rest away with them - my own cleanup two commits ago swapped explicit field assignments for that call and did not notice. The SRC channel had the same problem from the other end: older states carry it as a ninth entry in the channel array, and that record was read into a throwaway. What reserve agreed on carries over fine, so it now does. Both halves were checked by loading a state written by a master build, with and without the fix. Newer states are untouched and still round-trip. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/__sceAudio.cpp | 24 ++++++++++++++++-------- Core/HLE/sceAudio.cpp | 13 ++++++++++--- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Core/HLE/__sceAudio.cpp b/Core/HLE/__sceAudio.cpp index 78b4a7ab85..80838dd54a 100644 --- a/Core/HLE/__sceAudio.cpp +++ b/Core/HLE/__sceAudio.cpp @@ -146,8 +146,7 @@ void __AudioDoState(PointerWrap &p) { } // Before v3 the SRC channel was a ninth entry in this array rather than its own thing, so - // older states carry one extra record here. Its contents are the old sample-ring format - // that can't be converted anyway, so it gets read into a throwaway and dropped. + // older states carry one extra record here. int chanCount = ARRAY_SIZE(g_audioChans); Do(p, chanCount); const int expected = s >= 3 ? (int)ARRAY_SIZE(g_audioChans) : (int)ARRAY_SIZE(g_audioChans) + 1; @@ -161,19 +160,28 @@ void __AudioDoState(PointerWrap &p) { g_audioChans[i].index = i; g_audioChans[i].DoState(p); } else { - AudioChannel discarded; - discarded.index = i; - discarded.DoState(p); + // The ninth entry is the SRC channel in its old shape. Its queued audio is the old + // sample-ring format and can't be converted, but what reserve agreed on carries + // over - throwing that away is what leaves Output2 unreserved after the load. + AudioChannel old; + old.index = i; + old.DoState(p); + if (p.mode == p.MODE_READ) { + g_audioSRC.clear(); + g_audioSRC.reserved = old.reserved; + g_audioSRC.sampleCount = old.sampleCount; + g_audioSRC.leftVolume = old.leftVolume; + g_audioSRC.rightVolume = old.rightVolume; + g_audioSRC.format = old.format; + } } } if (s >= 3) { g_audioSRC.DoState(p); __AudioRoutingDoState(p); - } else if (p.mode == p.MODE_READ) { - // The old format read the routing modes back once per channel, above. - g_audioSRC.clear(); } + // For older states the routing modes were read back once per channel, above. __AudioCPUMHzChange(); } diff --git a/Core/HLE/sceAudio.cpp b/Core/HLE/sceAudio.cpp index 660fec804e..17c88eb684 100644 --- a/Core/HLE/sceAudio.cpp +++ b/Core/HLE/sceAudio.cpp @@ -90,9 +90,16 @@ void AudioChannel::DoState(PointerWrap &p) { } if (p.mode == p.MODE_READ) { - const u32 oldSampleCount = sampleCount; - clear(); - sampleCount = oldSampleCount; + // Only the play position is dropped. reserved, sampleCount, the volumes and the format + // were all just read out of the state and are still good - clearing those as well + // leaves every channel unreserved, and the game's next output fails with + // SCE_ERROR_AUDIO_CHANNEL_NOT_INIT. + sampleAddress = 0; + remainingSamples = 0; + waitingThread = 0; + waitingAddress = 0; + waitingLeftVolume = 0; + waitingRightVolume = 0; // The threads that were parked in a blocking output call are still parked, and // nothing is going to wake them now, so hand them their buffer back. for (const AudioChannelWaitInfo &waitInfo : oldWaitingThreads) { From 9a77ebb74355c841f3b959dab97a7efe9d3cb7e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 10 Sep 2026 16:41:21 -0600 Subject: [PATCH 7/7] sceAudio: tighten up the rest of the savestate handling A re-read of the whole thing after the reservation bug, looking for anything else it got wrong. The SRC channel's buffer count is read straight out of the state and then used to index a two-element array, so a corrupt one is a write outside the struct. It is range-checked now, the same way the channel count above it already was. A channel holding a buffer with no samples left of it was stuck: the mixer skipped it without ever clearing the address, so it read as busy for ever and the game had no way back to sound. The API cannot produce that - a channel is never reserved for zero samples - but a savestate can claim it, so the mixer now retires such a channel instead of stepping over it. Also took the "get rid of this next time we bump" the version-2 resampler section came with, since this branch is that bump. Nothing was ever in it. Checked by loading states written by a released build for a game that uses the mixer channels and one that uses Output2, plus a round trip of the new format. Every section ends in a marker, so a conversion path that consumed the wrong number of bytes would fail the load rather than quietly corrupt what follows. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/__sceAudio.cpp | 26 ++++++++++++++++---------- Core/HLE/sceAudio.cpp | 7 +++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/Core/HLE/__sceAudio.cpp b/Core/HLE/__sceAudio.cpp index 80838dd54a..763703bb15 100644 --- a/Core/HLE/__sceAudio.cpp +++ b/Core/HLE/__sceAudio.cpp @@ -131,17 +131,16 @@ void __AudioDoState(PointerWrap &p) { mixFrequency = 44100; } - if (s >= 2) { - // TODO: Next time we bump, get rid of this. It's kinda useless. - auto s = p.Section("resampler", 1); - if (p.mode == p.MODE_READ) { - System_AudioClear(); - } - } else { - // Only to preserve the previous file format. Might cause a slight audio glitch on upgrades? + // Version 1 kept the whole mixed output queue here, and version 2 replaced it with an empty + // "resampler" section that never held anything. Version 3 drops that too. Either way the + // contents go in the bin: the backend is cleared and the game refills it within a block. + if (s == 2) { + auto resampler = p.Section("resampler", 1); + } else if (s < 2) { FixedSizeQueue outAudioQueue; outAudioQueue.DoState(p); - + } + if (p.mode == p.MODE_READ) { System_AudioClear(); } @@ -484,9 +483,16 @@ void __AudioSetSRCFrequency(int freq) { // Mixes one block from a mixer channel, reading straight out of the game's buffer. static bool __AudioMixChannel(AudioChannel &chan) { - if (chan.sampleAddress == 0 || chan.remainingSamples == 0) { + if (chan.sampleAddress == 0) { + // Idle, or holding the remaining count from a null-pointer output. return false; } + if (chan.remainingSamples == 0) { + // Can't happen from the API, since a channel is never reserved for zero samples, but a + // savestate could say otherwise - and left alone the channel would read as busy for + // ever, which is silence the game can't recover from. + return __AudioChannelFinished(chan); + } const u32 count = std::min(chan.remainingSamples, (u32)hwBlockSize); const bool mono = chan.format == PSP_AUDIO_FORMAT_MONO; diff --git a/Core/HLE/sceAudio.cpp b/Core/HLE/sceAudio.cpp index 17c88eb684..8c315bc079 100644 --- a/Core/HLE/sceAudio.cpp +++ b/Core/HLE/sceAudio.cpp @@ -136,6 +136,13 @@ void AudioSRCChannel::DoState(PointerWrap &p) { Do(p, rightVolume); Do(p, format); Do(p, bufferCount); + if (bufferCount < 0 || bufferCount > (int)ARRAY_SIZE(buffers)) { + // Guarded because the enqueue indexes buffers[] with this, so a negative one would be + // a write outside the struct. + ERROR_LOG(Log::sceAudio, "Savestate failure: %d buffers on the SRC channel.", bufferCount); + p.SetError(p.ERROR_FAILURE); + return; + } for (AudioPendingBuffer &buf : buffers) { Do(p, buf.address); Do(p, buf.samples);