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) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-09-10 17:03:07 -06:00
1 parent c1bbd6ae2b
commit 9a77ebb743
2 files changed
+23 -10

No files matched your search

+16 -10
View File
@@ -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<s16, 512 * 16> 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;
+7
View File
@@ -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);