Audio/VolatileMem: Match hardware when a blocking call can't wait

From pspautotests intr/waits:

- sceAudioOutputBlocking sets the channel's waiting flag before its
  event flag wait, and when that wait fails at once (interrupts or
  dispatch disabled, or inside an interrupt) it returns the error
  without clearing the flag. The channel stays busy from then on, and
  can't be released.
- The SRC blocking output fails the same way even when a completion is
  already there, leaving the buffer armed.
- After a block that had samples in it, the mixer DMA is still playing
  it out, so a buffer arriving then isn't read early or restarts it.
- sceKernelVolatileMemLock only writes the fake address and size
  through pointers that are there, instead of faulting on NULL.

intr/waits now runs to the end; one scheduling marker still differs, from
async IO timing.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-29 09:19:32 -06:00
1 parent 5674c789ef
commit a74882b013
4 files changed
+59 -15

No files matched your search

+32 -10
View File
@@ -42,6 +42,7 @@
#include "Core/HLE/sceAudio.h"
#include "Core/HLE/sceKernel.h"
#include "Core/HLE/sceKernelThread.h"
#include "Core/HLE/sceKernelInterrupt.h"
#include "Core/Util/AudioFormat.h"
// Should be used to lock anything related to the outAudioQueue.
@@ -87,7 +88,12 @@ void __AudioCPUMHzChange() {
audioHostIntervalCycles = (int)(usToCycles(1000000ULL) * 512 / hwSampleRate);
}
// The mixer's DMA is still playing out the last block it was handed, which had samples in it.
// A buffer arriving then doesn't restart the DMA or get read early: it waits for the next block.
static bool mixerDMABusy;
void __AudioInit() {
mixerDMABusy = false;
System_AudioResetStatCounters();
mixFrequency = 44100;
srcFrequency = 0;
@@ -113,7 +119,7 @@ void __AudioInit() {
}
void __AudioDoState(PointerWrap &p) {
auto s = p.Section("sceAudio", 1, 3);
auto s = p.Section("sceAudio", 1, 4);
if (!s)
return;
@@ -180,6 +186,11 @@ void __AudioDoState(PointerWrap &p) {
g_audioSRC.DoState(p);
__AudioRoutingDoState(p);
}
if (s >= 4) {
Do(p, mixerDMABusy);
} else {
mixerDMABusy = false;
}
// For older states the routing modes were read back once per channel, above.
__AudioCPUMHzChange();
@@ -234,7 +245,7 @@ static void __AudioReScheduleAfterWake() {
}
// 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.
// two start independently of each other. See mixerDMABusy for the block after the last one.
static bool __AudioAnyChannelPlaying() {
for (const AudioChannel &chan : g_audioChans) {
if (chan.sampleAddress != 0) {
@@ -294,7 +305,7 @@ u32 __AudioEnqueue(AudioChannel &chan, u32 samplePtr, int leftVol, int rightVol)
chan.rightVolume = rightVol;
}
// Handing over a buffer while nothing was playing is what starts the DMA.
const bool startsDMA = samplePtr != 0 && !__AudioAnyChannelPlaying();
const bool startsDMA = samplePtr != 0 && !__AudioAnyChannelPlaying() && !mixerDMABusy;
// 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.
@@ -314,7 +325,7 @@ void __AudioEnqueueOneshot(AudioChannel &chan, u32 samplePtr, u32 sampleCount, u
chan.rightVolume = rightVol;
chan.remainingSamples = sampleCount;
const bool startsDMA = samplePtr != 0 && !__AudioAnyChannelPlaying();
const bool startsDMA = samplePtr != 0 && !__AudioAnyChannelPlaying() && !mixerDMABusy;
chan.sampleAddress = samplePtr;
if (startsDMA) {
__AudioStartMixerDMA();
@@ -334,8 +345,13 @@ u32 __AudioEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int leftVol, int r
if (chan.waitingThread != 0) {
return SCE_ERROR_AUDIO_CHANNEL_BUSY;
}
if (!__KernelIsDispatchEnabled()) {
return SCE_KERNEL_ERROR_CAN_NOT_WAIT;
// The driver sets the channel's waiting flag before its event flag wait, and if that wait
// fails at once (interrupts or dispatch disabled, inside an interrupt), it returns the error
// without clearing it. From then on the channel is busy for good: only a successful wait
// clears the flag, and release is refused while it's set. (audio.prx, sceAudioOutputBlocking.)
if (__IsInInterrupt() || !__KernelIsDispatchEnabled()) {
chan.waitingThread = AUDIO_WAITING_ABANDONED;
return __IsInInterrupt() ? SCE_KERNEL_ERROR_ILLEGAL_CONTEXT : SCE_KERNEL_ERROR_CAN_NOT_WAIT;
}
chan.waitingThread = __KernelGetCurThread();
@@ -351,7 +367,7 @@ u32 __AudioEnqueueBlocking(AudioChannel &chan, u32 samplePtr, int leftVol, int r
static bool __AudioChannelFinished(AudioChannel &chan) {
chan.sampleAddress = 0;
chan.remainingSamples = 0;
if (chan.waitingThread == 0) {
if (chan.waitingThread == 0 || chan.waitingThread == AUDIO_WAITING_ABANDONED) {
return false;
}
@@ -414,13 +430,18 @@ u32 __AudioSRCEnqueueBlocking(AudioSRCChannel &chan, u32 samplePtr, int vol) {
return 0;
}
if (chan.completion) {
chan.completion = false;
return result;
// The event flag wait refuses before looking at the flag, so even a completion that's
// already there isn't taken - the buffer stays armed and the error comes back.
if (__IsInInterrupt()) {
return SCE_KERNEL_ERROR_ILLEGAL_CONTEXT;
}
if (!__KernelIsDispatchEnabled()) {
return SCE_KERNEL_ERROR_CAN_NOT_WAIT;
}
if (chan.completion) {
chan.completion = false;
return result;
}
chan.waitingThreads.push_back(__KernelGetCurThread());
__KernelWaitCurThread(WAITTYPE_AUDIOCHANNEL, PSP_AUDIO_SRC_WAIT_ID, result, 0, false, "blocking audio");
@@ -608,6 +629,7 @@ void __AudioUpdate(bool resetRecording) {
memset(mixBuffer, 0, hwBlockSize * 2 * sizeof(s32));
audioMixing = true;
mixerDMABusy = __AudioAnyChannelPlaying();
bool woke = false;
for (AudioChannel &chan : g_audioChans) {
// Deliberately not gated on `reserved`: sceAudioChRelease only clears the
+3
View File
@@ -32,6 +32,8 @@ const u32 PSP_AUDIO_CHANNEL_MAX = 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;
// In AudioChannel::waitingThread: the flag was set by a wait that failed, and nobody will clear it.
const SceUID AUDIO_WAITING_ABANDONED = -1;
// One buffer handed over and not yet fully played.
struct AudioPendingBuffer {
@@ -63,6 +65,7 @@ struct AudioChannel {
// 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.
// AUDIO_WAITING_ABANDONED: see __AudioEnqueueBlocking().
SceUID waitingThread = 0;
u32 waitingAddress = 0;
int waitingLeftVolume = 0;
+10 -4
View File
@@ -433,16 +433,22 @@ static int sceKernelVolatileMemLock(int type, u32 paddr, u32 psize) {
case SCE_KERNEL_ERROR_CAN_NOT_WAIT:
{
WARN_LOG(Log::HLE, "sceKernelVolatileMemLock(%i, %08x, %08x): dispatch disabled", type, paddr, psize);
Memory::WriteOrException_U32(0x08400000, paddr);
Memory::WriteOrException_U32(0x00400000, psize);
// Only through pointers that are there: intr/waits passes NULL and gets just the error.
if (Memory::IsValid4AlignedAddress(paddr))
Memory::WriteUnchecked_U32(0x08400000, paddr);
if (Memory::IsValid4AlignedAddress(psize))
Memory::WriteUnchecked_U32(0x00400000, psize);
}
break;
case SCE_KERNEL_ERROR_ILLEGAL_CONTEXT:
{
WARN_LOG(Log::HLE, "sceKernelVolatileMemLock(%i, %08x, %08x): in interrupt", type, paddr, psize);
Memory::WriteOrException_U32(0x08400000, paddr);
Memory::WriteOrException_U32(0x00400000, psize);
// Only through pointers that are there: intr/waits passes NULL and gets just the error.
if (Memory::IsValid4AlignedAddress(paddr))
Memory::WriteUnchecked_U32(0x08400000, paddr);
if (Memory::IsValid4AlignedAddress(psize))
Memory::WriteUnchecked_U32(0x00400000, psize);
}
break;
+14 -1
View File
@@ -59,11 +59,19 @@ 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;
r = wait for this channel's bit in the driver's event flag;
if (r < 0) return r; // waiting is left set!
retry the enqueue;
channel->waiting = 0;
```
The error path is a real firmware quirk, pinned down by `tests/intr/waits`. The event flag wait
fails at once with interrupts or dispatch disabled (`800201a7`) or inside an interrupt
(`80020064`), and the driver returns that without clearing `waiting`. Nothing ever clears it
after that, so the channel stays busy for good, and `sceAudioChRelease` refuses to release it.
The SRC channel's wait fails the same way, and since the flag is never looked at, even a
completion that was already there isn't taken: the buffer stays armed and the error comes back.
**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
@@ -121,6 +129,11 @@ reset but no early read, since its DMA feeds the codec directly. Without this th
`sceAudioGetChannelRestLen` right after an output would depend on where the timer happened to
be, and would differ from run to run.
That only happens when the DMA is actually idle. After a block that had samples in it, the DMA
is still playing that block out, so a buffer handed over within it is not read early: it waits
for the next block like any other. (Also from `tests/intr/waits`, where a second 64-sample buffer
is still in the channel when the next call looks.)
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