From c162eb3d74347bf9d2658649e6dea4fa43175b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 28 Sep 2026 09:36:52 -0600 Subject: [PATCH 1/3] sceAudiocodec: Match hardware setup, framing, errors and timing; fix Atrac3 polarity Checked against pspautotests audio/audiocodec, recorded on a PSP. - Atrac3+: at3Related selects headered (mpeg.prx) or raw (libatrac3plus) frames, instead of sniffing for the sync word. The header's size field is 10 bits, as the context's. Header errors 0x211/0x213, bad frames 0x20a, all returning SCE_AVCODEC_ERROR_INVALID_DATA with nothing read. - The first successfully decoded Atrac3+ frame, and the first two AAC frames, produce no output. Checked sample-for-sample against hardware. - Atrac3: the parameter at 0x28 selects the frame layout, as libatrac3plus.prx's table maps it. We used to read its low bit as a joint-stereo flag, which decoded mono (0x0F) streams as stereo garbage. AtracCtx2 had the table's fields swapped the same way. - at3_standalone's Atrac3 output was inverted relative to the PSP's (sceAtrac too). Negate the IMDCT scale. - CheckNeedMem sizes (AAC is 0x658c), codec 0x1004/0x1005, Init validation (AAC sample rate, Atrac3 parameter, Atrac3+ channels), and ReleaseEDRAM clearing edramAddr. - Every call that reaches the ME now blocks for its measured time, and decode time is modelled per codec and frame size. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HLE/AtracCtx2.cpp | 11 +- Core/HLE/ErrorCodes.h | 2 + Core/HLE/sceAudiocodec.cpp | 391 ++++++++++++++++++++++------------ ext/at3_standalone/atrac3.cpp | 4 +- test.py | 1 + 5 files changed, 272 insertions(+), 137 deletions(-) diff --git a/Core/HLE/AtracCtx2.cpp b/Core/HLE/AtracCtx2.cpp index ea87e407cd..2a403d84af 100644 --- a/Core/HLE/AtracCtx2.cpp +++ b/Core/HLE/AtracCtx2.cpp @@ -32,7 +32,7 @@ struct AT3BitrateMeta { u16 sampleSize; u8 dataByte; - u8 jointStereo; // I think? + u8 jointStereo; }; static const AT3BitrateMeta g_at3BitrateMeta[5] = { @@ -242,12 +242,13 @@ int InitContextFromTrackInfo(SceAtracContext *ctx, const TrackInfo *wave, u32 bu (ctx->codec).fmt.at3.formatByte2 = wave->tailFlag; return 0; } - // At3. Set up the hardware codec (hopefully we can correctly support this in sceAudiocodec and thus sceAtrac LLE in the future) - // This is not actually necessary since we don't use the actual hardware codec. + // At3. Set up the hardware codec parameter as libatrac3plus.prx's SetData (0880645c) does: + // keyed by frame size and the joint-stereo flag (sampleSizeMaybe, for Atrac3), it stores the + // data byte, as a word. We don't decode through it, but it's what the game sees. for (int counter = 4; counter >= 0; counter--) { if ((g_at3BitrateMeta[counter].sampleSize == (ctx->info).sampleSize) && - ((int)g_at3BitrateMeta[counter].dataByte == wave->sampleSizeMaybe)) { - (ctx->codec).fmt.at3.formatByte1 = (char)g_at3BitrateMeta[counter].jointStereo; + ((int)g_at3BitrateMeta[counter].jointStereo == wave->sampleSizeMaybe)) { + (ctx->codec).fmt.at3.formatByte1 = g_at3BitrateMeta[counter].dataByte; (ctx->codec).fmt.at3.formatByte2 = 0; (ctx->codec).fmt.at3.unk2a = 0; (ctx->codec).fmt.at3.unk2b = 0; diff --git a/Core/HLE/ErrorCodes.h b/Core/HLE/ErrorCodes.h index f15d353d5f..bb7635a490 100644 --- a/Core/HLE/ErrorCodes.h +++ b/Core/HLE/ErrorCodes.h @@ -500,6 +500,8 @@ enum PSPErrorCode : u32 { SCE_SAS_ERROR_NOT_INIT = 0x80420100, SCE_AVCODEC_ERROR_INVALID_DATA = 0x807f00fd, + // Name inferred: sceAudiocodec returns this for an unsupported codec or AAC sample rate. + SCE_AVCODEC_ERROR_UNSUPPORTED = 0x807f00ff, // These are inferred from behavior. SCE_REG_ERROR_MALLOC_FAILURE = 0x80082712, diff --git a/Core/HLE/sceAudiocodec.cpp b/Core/HLE/sceAudiocodec.cpp index 93de522240..11e3e498a0 100644 --- a/Core/HLE/sceAudiocodec.cpp +++ b/Core/HLE/sceAudiocodec.cpp @@ -18,6 +18,7 @@ #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" +#include "Common/Serialize/SerializeMap.h" #include "Core/HLE/HLE.h" #include "Core/HLE/FunctionWrappers.h" #include "Core/HLE/sceAudiocodec.h" @@ -38,6 +39,9 @@ std::map g_audioDecoderContexts; // frame size in the context, and at init time the input buffer is still empty, so for that path we // only learn it from the first frame - at which point the decoder has to be rebuilt to match. static std::map g_at3PlusFrameBytes; +// How many more successfully decoded frames will produce no output, per context (see the notes on +// Atrac3+ below). +static std::map g_primingFrames; static bool oldStateLoaded = false; @@ -66,42 +70,49 @@ static_assert(offsetof(SceAudiocodecCodec, allocMem) == 0x68); // // Bitrate Frame Size Byte 1 Byte 2 Channels // ----------------------------------------------------- -// 48kbps 0x118 0x24 0x22 1? // This hits "Frame data doesn't match channel configuration". -// 64kbps 0x178 (0x2e implied by the formula below) +// 48kbps 0x118 0x24 0x22 1 +// 64kbps 0x178 0x28 0x2e 2 // 96kbps? 0x230 0x28 0x45 2 // 128kbps 0x2E8 0x28 0x5c 2 // -// The frame size really is "Byte 2" * 8 + 8 - it holds for all three rows we have both numbers -// for, and libatrac3plus.prx writes 0x28/0x5c (the 128kbps row) into these two bytes at init as -// the worst case it sizes its EDRAM allocation against. So byte 2 is the hardware's own frame -// descriptor and reading it, as we do, is the right thing. +// The frame size is ((byte 1 & 3) << 8 | byte 2) * 8 + 8, and the channel count is bits 2-4 of +// byte 1 (AtracTrack checks the same bits). The 0x0FD0-sync header on PSMF frames carries the same +// two bytes. On hardware (pspautotests audio/audiocodec), a header claiming byte 1 = 0x29 made the +// decoder read 0x800 bytes more, so the two low bits of byte 1 really are size bits; a context of +// 28 5c on a 28 2e stream decodes fine and reports 744 bytes read, since the decoder only reads +// what the frame needs; 28 22 (too small) fails with err 0x214; and byte 1 = 0x20 (no channels) +// fails Init with err 0x202. // -// The channel guess below (bit 3 of byte 1) fits both data points we have and nothing else. +// at3Related (0x30) says whether frames carry that 8-byte header. mpeg.prx sets it and passes +// headered frames, libatrac3plus.prx clears it and strips the header. With it set, a frame without +// the 0F D0 sync fails with err 0x211, and one whose header disagrees with the context's channel +// count with err 0x213; with it clear, a header is taken for audio data, which fails to decode. +// +// Bitstream errors are 0x20a or 0x208 (junk and zeros give 0x20a; a real frame read from 4 bytes +// in can parse further and give 0x208). Neither is sticky: the next good frame decodes normally. +// Every failure returns SCE_AVCODEC_ERROR_INVALID_DATA with 0 bytes read and written. +// +// The first frame decoded successfully produces no output (dstBytesWritten 0); from then on +// frame n gives the samples ffmpeg's decoder gives for frame n, so it's dropped, not delayed. +// AAC drops two frames the same way. Atrac3 and MP3 drop none. // Atrac3 (0x1001) // // The Media Engine only ever sees the first 0x68 bytes of this structure - me_wrapper.prx does // sceKernelDcacheWritebackInvalidateRange(ctx, 0x68) before handing it over, and avcodec.prx -// validates the same range - so whatever the hardware uses to size an Atrac3 frame has to be in -// there. The only Atrac3-specific thing anything writes is the joint-stereo flag in byte 1. +// validates the same range. // -// Frame size data byte JointStereo? -// ------------------------------------------------- -// 0x180 0x04 0 -// 0x130 0x06 0 -// 0x0C0 0x0B 1 -// 0x0C0 0x0E 0 -// 0x098 0x0F 0 -// -// NOTE: sceAudiocodecDecode below hardcodes 384 (0x180) bytes per frame for Atrac3, which is only -// the first row. If a game ever drives Atrac3 through sceAudiocodec at one of the other sizes we -// will decode garbage. +// The parameter at 0x28 (a word) selects the frame layout. libatrac3plus.prx's SetData (0880645c) +// looks it up in a table keyed by frame size and the stream's joint-stereo flag, and the ME decodes +// by it alone. See at3Params below. Anything above 0x0F fails Init with err 0x186; a bad frame fails +// with err 0x182 and isn't sticky either. // AAC (0x1003) // ------------------------------------------------ // Sample rate is at offset 0x28. The input frame size is 0x609 when the byte at 0x2c is nonzero // and 0x600 when it is zero (avcodec.prx decodeUtility, case 0x1003); the output size comes from -// the byte at 0x2d and is 0x1000 or 0x2000. +// the byte at 0x2d and is 0x1000 or 0x2000. Setting the byte at 0x2c alone (to 1) fails Init on +// hardware, so something else has to go with it. // MP3 (0x1002) // ------------------------------------------------ @@ -113,51 +124,39 @@ static_assert(offsetof(SceAudiocodecCodec, allocMem) == 0x68); // 0x28 is not this frame's size - libmp3.prx writes 0x5A1 there once, the largest an MP3 frame // can ever be. Output is 0x1200 bytes for MPEG1 (1152 samples) and 0x900 otherwise (576). -void CalculateInputBytesAndChannelsAt3Plus(const SceAudiocodecCodec *ctx, int *inputBytes, int *channels, int *headerBytes = nullptr) { - *inputBytes = 0; - *channels = 2; - if (headerBytes) { - *headerBytes = 0; - } - - u8 formatByte1 = ctx->fmt.at3.formatByte1; - u8 formatByte2 = ctx->fmt.at3.formatByte2; - - // Atrac3+ frames inside a PSMF still carry their 8-byte header, starting with the 0x0FD0 sync - // word; libatrac3plus.prx strips it before handing the frame over, mpeg.prx leaves it on for - // the hardware to parse. So when the sync word is still there, take the size from the frame - // and step over the header, exactly as MpegDemux does on the HLE path. Bytes 2 and 3 are the - // same pair that ends up in the context, but with two more size bits in the first of them. - const u8 *frame = Memory::IsValidRange(ctx->inBuf, 4) ? Memory::GetPointerUnchecked(ctx->inBuf) : nullptr; - if (frame && frame[0] == 0x0F && frame[1] == 0xD0) { - formatByte1 = frame[2]; - formatByte2 = frame[3]; - if (headerBytes) { - *headerBytes = 8; - } - // The full size, unlike the context's, has two more high bits in the first byte. The - // 0x10 is the header plus the 8 the context's own formula adds. - *channels = (formatByte1 & 8) ? 2 : 1; - *inputBytes = (((formatByte1 & 0x03) << 8) | (formatByte2 * 8)) + 0x10 - 8; - return; - } - - // bit 3 of the first byte is the channel count. This is a guess, but it fits the data we have. - *channels = (formatByte1 & 8) ? 2 : 1; - // formatByte2 * 8 + 8 gives the frame size for every bitrate we have data for (0x118, 0x178, - // 0x230, 0x2E8), so use it for any other value as well rather than leaving inputBytes at 0, - // which the firmware never does and which would fail the decode outright. - *inputBytes = formatByte2 * 8 + 8; +// Atrac3+ frame size and channel count from a pair of format bytes (from the context, or from a +// PSMF frame header). +static void At3PlusFormat(u8 formatByte1, u8 formatByte2, int *inputBytes, int *channels) { + *channels = (formatByte1 >> 2) & 7; + *inputBytes = (((formatByte1 & 3) << 8) | formatByte2) * 8 + 8; } -// Atrac3 (0x1001). Unlike Atrac3+, the context doesn't carry a frame size - libatrac3plus.prx -// (and our mirror of it in AtracCtx2) writes only the joint-stereo flag into formatByte1 for -// Atrac3, so in general the size can't be recovered from the context alone. -// -// One case is unambiguous, though. Of the five frame sizes the hardware supports, exactly one is -// joint stereo (66kbps stereo, 0xC0 bytes), so that flag pins the size down by itself. Everything -// else falls back to the 132kbps stereo frame, which is what the old hardcoded 384 was. -static int Atrac3BytesPerFrameFromContext(const SceAudiocodecCodec *ctx, int *channels); +// For a decode (headerBytes non-null), also handles the frame header, and returns the ME error +// code for a frame that doesn't match the context, or 0. +static int CalculateInputBytesAndChannelsAt3Plus(const SceAudiocodecCodec *ctx, int *inputBytes, int *channels, int *headerBytes = nullptr) { + At3PlusFormat(ctx->fmt.at3.formatByte1, ctx->fmt.at3.formatByte2, inputBytes, channels); + if (!headerBytes) { + return 0; + } + *headerBytes = 0; + if (ctx->fmt.at3.at3Related == 0) { + return 0; + } + + const u8 *frame = Memory::IsValidRange(ctx->inBuf, 8) ? Memory::GetPointerUnchecked(ctx->inBuf) : nullptr; + if (!frame || frame[0] != 0x0F || frame[1] != 0xD0) { + return 0x211; + } + int frameChannels; + At3PlusFormat(frame[2], frame[3], inputBytes, &frameChannels); + if (frameChannels != *channels) { + return 0x213; + } + *headerBytes = 8; + return 0; +} + +static bool Atrac3LayoutFromContext(const SceAudiocodecCodec *ctx, int *bytesPerFrame, int *channels, bool *jointStereo); // The MPEG sample rates, indexed by [version][sampleRateIndex] exactly as the hardware's own // table in avcodec.prx does. Version is 0 = MPEG2, 1 = MPEG1, 2 = MPEG2.5. @@ -199,6 +198,7 @@ static bool removeDecoder(u32 ctxPtr) { delete it->second; g_audioDecoderContexts.erase(it); g_at3PlusFrameBytes.erase(ctxPtr); + g_primingFrames.erase(ctxPtr); return true; } return false; @@ -210,6 +210,7 @@ static void clearDecoders() { } g_audioDecoderContexts.clear(); g_at3PlusFrameBytes.clear(); + g_primingFrames.clear(); } void __AudioCodecInit() { @@ -224,6 +225,40 @@ void __AudioCodecShutdown() { clearDecoders(); } +// Everything below that reaches the ME blocks the caller while it answers. Measured at 222MHz +// (pspautotests audio/audiocodec/timing): +// CheckNeedMem GetEDRAM Init ReleaseEDRAM GetInfo +// Atrac3+ 147 56 646 55 82 +// Atrac3 80 56 210 56 3 +// MP3 79 57 517 57 82 +// AAC 57 57 230 55 3 +// A GetInfo of 3us didn't reach the ME. Calls that fail in the ME take time too; how long a failed +// decode takes isn't measured. +static int MECall(int result, int us) { + return hleDelayResult(result, "audiocodec", MEScheduleJob(PowerScaleFromDefaultClock(us))); +} + +static int InitUs(int codec) { + switch (codec) { + case PSP_CODEC_AT3PLUS: return 646; + case PSP_CODEC_AT3: return 210; + case PSP_CODEC_MP3: return 517; + default: return 230; + } +} + +// libmp4.prx puts the sample rate here. On hardware 22050 and 44100 are accepted, 0 and 12345 +// aren't; that the rest of the standard AAC rates are accepted is an assumption. 0 if not valid. +static int AacSampleRateFromContext(const SceAudiocodecCodec *ctx) { + static const int aacRates[] = { 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350 }; + for (int rate : aacRates) { + if ((int)ctx->fmt.aac.sampleRate == rate) { + return rate; + } + } + return 0; +} + // Creates a context's decoder from what the context holds. Init does this, and so does a state // load, since the decoders themselves aren't saved. static AudioDecoder *CreateDecoderForContext(u32 ctxPtr, PSPAudioType audioType) { @@ -231,6 +266,7 @@ static AudioDecoder *CreateDecoderForContext(u32 ctxPtr, PSPAudioType audioType) int bytesPerFrame = 0; int channels = 2; + int sampleRate = 44100; uint8_t extraData[14]{}; @@ -240,10 +276,8 @@ static AudioDecoder *CreateDecoderForContext(u32 ctxPtr, PSPAudioType audioType) break; case PSP_CODEC_AT3: { - // See AtracBase::CreateDecoder. The context only tells us whether the stream is joint - // stereo, which pins the frame size down in that one case - see the function. - bytesPerFrame = Atrac3BytesPerFrameFromContext(ctx, &channels); - bool jointStereo = IsAtrac3StreamJointStereo(PSP_CODEC_AT3, bytesPerFrame, channels); + bool jointStereo; + Atrac3LayoutFromContext(ctx, &bytesPerFrame, &channels, &jointStereo); // The only thing that changes are the jointStereo_ values. extraData[0] = 1; extraData[3] = channels << 3; @@ -252,6 +286,11 @@ static AudioDecoder *CreateDecoderForContext(u32 ctxPtr, PSPAudioType audioType) extraData[10] = 1; break; } + case PSP_CODEC_AAC: + if (AacSampleRateFromContext(ctx)) { + sampleRate = AacSampleRateFromContext(ctx); + } + break; default: break; } @@ -259,7 +298,7 @@ static AudioDecoder *CreateDecoderForContext(u32 ctxPtr, PSPAudioType audioType) // Create audio decoder for given audio codec and push it into AudioList INFO_LOG(Log::ME, "sceAudioDecoder: Creating codec with %04x frame size and %d channels, codec %04x", bytesPerFrame, channels, (int)audioType); // We send in extra data with all codec, most ignore it. - AudioDecoder *decoder = CreateAudioDecoder(audioType, 44100, channels, bytesPerFrame, extraData, sizeof(extraData)); + AudioDecoder *decoder = CreateAudioDecoder(audioType, sampleRate, channels, bytesPerFrame, extraData, sizeof(extraData)); decoder->SetCtxPtr(ctxPtr); g_audioDecoderContexts[ctxPtr] = decoder; g_at3PlusFrameBytes[ctxPtr] = bytesPerFrame; @@ -286,7 +325,9 @@ static int __AudioCodecInitCommon(u32 ctxPtr, int codec, bool mono) { ctx->magic = 0x5100601; ctx->err = 0; - // Special actions for some codecs. + int primingFrames = 0; + + // Special actions for some codecs, and what the hardware rejects. switch (audioType) { case PSP_CODEC_MP3: // Not seeing inited in Kurok (homebrew) @@ -294,16 +335,62 @@ static int __AudioCodecInitCommon(u32 ctxPtr, int codec, bool mono) { ctx->fmt.mp3.version = 9999; break; case PSP_CODEC_AAC: - // AAC / mp4 - // offsets 40-42 are a 24-bit LE number specifying the sample rate. It's 32000, 44100 or 48000. - // neededMem has been set to 0x18f20. + if (!AacSampleRateFromContext(ctx)) { + return MECall(hleLogError(Log::ME, SCE_AVCODEC_ERROR_UNSUPPORTED, "bad AAC sample rate %d", ctx->fmt.aac.sampleRate), InitUs(codec)); + } + primingFrames = 2; break; + case PSP_CODEC_AT3PLUS: + { + int bytesPerFrame, channels; + CalculateInputBytesAndChannelsAt3Plus(ctx, &bytesPerFrame, &channels); + if (channels == 0) { + ctx->err = 0x202; + return MECall(hleLogError(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "bad Atrac3+ format byte %02x", ctx->fmt.at3.formatByte1), InitUs(codec)); + } + primingFrames = 1; + break; + } + case PSP_CODEC_AT3: + { + int bytesPerFrame, channels; + bool jointStereo; + if (!Atrac3LayoutFromContext(ctx, &bytesPerFrame, &channels, &jointStereo)) { + ctx->err = 0x186; + return MECall(hleLogError(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "bad Atrac3 parameter %08x", *(const u32_le *)ctx->fmt.raw), InitUs(codec)); + } + break; + } default: break; } CreateDecoderForContext(ctxPtr, audioType); - return hleLogDebug(Log::ME, 0); + // Not in CreateDecoderForContext: a state load restores what's left of it instead. + g_primingFrames[ctxPtr] = primingFrames; + return MECall(hleLogDebug(Log::ME, 0), InitUs(codec)); +} + +// How long the ME takes over one frame, in microseconds at the default 222MHz clock. Fitted to +// sceAudiocodecDecode timed on a PSP (pspautotests audio/audiocodec/timing), which at 222MHz gave: +// Atrac3+ stereo: 2410us at 376 bytes/frame, 2990 at 744. Mono: 2126 at 744. +// Atrac3 stereo: 1138 at 0x180, 1063 at 0xC0 joint stereo. Mono: 685 at 0x98. +// MP3 MPEG1 (1152 samples): 2575 at 418 bytes, 2698 at 1045. MPEG2 (576): 1411 at 104, 1464 at 209. +// AAC-LC stereo 44.1kHz: 1651 at ~190 bytes, 1990 at ~373, 2042 at ~559. +// The mono Atrac3+ slope is a guess from its one data point. +static int EstimateDecodeUs(int codec, int channels, int frameBytes, const SceAudiocodecCodec *ctx) { + switch (codec) { + case PSP_CODEC_AT3PLUS: + return channels == 1 ? 1500 + frameBytes * 84 / 100 : 1815 + frameBytes * 158 / 100; + case PSP_CODEC_AT3: + return channels == 1 ? 685 : 970 + frameBytes * 44 / 100; + case PSP_CODEC_MP3: + return ctx->fmt.mp3.version == 1 ? 2492 + frameBytes / 5 : 1358 + frameBytes / 2; + case PSP_CODEC_AAC: + return 1400 + std::min(frameBytes, 400) * 16 / 10; + default: + return 0; + } } static int sceAudiocodecInit(u32 ctxPtr, int codec) { @@ -336,8 +423,16 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) { switch (codec) { case PSP_CODEC_AT3PLUS: - CalculateInputBytesAndChannelsAt3Plus(ctx, &bytesPerFrame, &channels, &headerBytes); + { + const int frameError = CalculateInputBytesAndChannelsAt3Plus(ctx, &bytesPerFrame, &channels, &headerBytes); + if (frameError) { + ctx->err = frameError; + ctx->srcBytesRead = 0; + ctx->dstBytesWritten = 0; + return MECall(hleLogWarning(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "Atrac3+ frame doesn't match the context: err %03x", frameError), 100); + } break; + } case PSP_CODEC_MP3: // Not srcBytesRead - that's an output field holding what the *previous* call consumed. // The hardware uses the bound at 0x28, which the caller also guarantees is readable at @@ -359,9 +454,12 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) { sampleRate = ctx->fmt.aac.sampleRate; break; case PSP_CODEC_AT3: - bytesPerFrame = Atrac3BytesPerFrameFromContext(ctx, &channels); + { + bool jointStereo; + Atrac3LayoutFromContext(ctx, &bytesPerFrame, &channels, &jointStereo); break; } + } // find a decoder in audioList auto decoder = findDecoder(ctxPtr); @@ -377,14 +475,18 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) { if (it == g_at3PlusFrameBytes.end() || it->second != bytesPerFrame) { // Only reachable when the context didn't carry a frame size at init - mpeg.prx. INFO_LOG(Log::ME, "sceAudiocodecDecode: Atrac3+ frame is %04x bytes, rebuilding decoder", bytesPerFrame); + // The hardware has one decoder throughout, so this doesn't restart the priming. + const int primingFrames = g_primingFrames[ctxPtr]; removeDecoder(ctxPtr); decoder = CreateAudioDecoder(audioType, 44100, channels, bytesPerFrame); decoder->SetCtxPtr(ctxPtr); g_audioDecoderContexts[ctxPtr] = decoder; g_at3PlusFrameBytes[ctxPtr] = bytesPerFrame; + g_primingFrames[ctxPtr] = primingFrames; } } + int decodeUs = 0; if (decoder) { // Use SimpleAudioDec to decode audio // Decode audio @@ -409,9 +511,24 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) { } bool result = decoder->Decode(inBuf, bytesPerFrame, &inDataConsumed, 2, outBuf, &outSamples); + if (!result && (codec == PSP_CODEC_AT3PLUS || codec == PSP_CODEC_AT3)) { + // What the hardware reports for a frame that doesn't decode (0x208 is also possible for + // Atrac3+, depending on how far into the frame the problem is). + ctx->err = codec == PSP_CODEC_AT3PLUS ? 0x20a : 0x182; + ctx->srcBytesRead = 0; + ctx->dstBytesWritten = 0; + return MECall(hleLogWarning(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "%s frame failed to decode", GetCodecName(codec)), 100); + } if (!result) { ctx->err = 0x20b; ERROR_LOG(Log::ME, "AudioCodec decode failed. Setting error to %08x", ctx->err); + } else { + ctx->err = 0; + auto priming = g_primingFrames.find(ctxPtr); + if (priming != g_primingFrames.end() && priming->second > 0) { + priming->second--; + outSamples = 0; + } } ctx->srcBytesRead = inDataConsumed + headerBytes; @@ -420,17 +537,8 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) { // reporting the sample count instead gave it a quarter of every frame, which played back // fast and metallic. The decoder always writes stereo 16-bit, whatever the source is. ctx->dstBytesWritten = outSamples * 2 * (int)sizeof(int16_t); - } - // The decode runs on the ME and takes real time. Measured on a PSP through the libraries that - // call this, so each includes their own work around the call: sceMpegAtracDecode of one ATRAC3+ - // frame (2048 samples) takes about 2.5ms (pspautotests video/mpeg/playertiming), and - // sceMp4AacDecode of one AAC-LC stereo frame (1024 samples) about 1.7ms (video/mp4/mp4timing). - // Other codecs aren't measured yet. - int decodeUs = 0; - if (audioType == PSP_CODEC_AT3PLUS) { - decodeUs = 2500; - } else if (audioType == PSP_CODEC_AAC) { - decodeUs = 1700; + + decodeUs = EstimateDecodeUs(codec, channels, inDataConsumed, ctx); } if (decodeUs > 0) { decodeUs = MEScheduleJob(PowerScaleFromDefaultClock(decodeUs)); @@ -493,6 +601,9 @@ static int sceAudiocodecGetInfo(u32 ctxPtr, int codec) { } } + if (codec == PSP_CODEC_MP3 || codec == PSP_CODEC_AT3PLUS) { + return MECall(hleLogInfo(Log::ME, 0, "codec=%s", GetCodecName(codec)), 82); + } return hleLogInfo(Log::ME, 0, "codec=%s", GetCodecName(codec)); } @@ -508,17 +619,16 @@ static int sceAudiocodecCheckNeedMem(u32 ctxPtr, int codec) { // Check for expected values. auto ctx = PSPPointer::Create(ctxPtr); // On game-owned heap, no need to allocate. + // The sizes are fixed per codec: on hardware they don't depend on the Atrac3+ format bytes, + // inited, or the AAC sample rate. switch (codec) { case 0x1000: - ctx->neededMem = 0x7bc0; - // avcodec.prx does no format check here at all, it just forwards to the ME. - // libatrac3plus writes 28 5c (the worst case it sizes EDRAM against). - // mpeg.prx writes the real frame's own header bytes. Let's just log if we find - // something unusual here, it might mean something. - if (ctx->fmt.at3.formatByte1 != 0x28 && ctx->fmt.at3.formatByte1 != 0x24) { - INFO_LOG(Log::ME, "sceAudiocodecCheckNeedMem: unfamiliar Atrac3+ format bytes %02x %02x", - ctx->fmt.at3.formatByte1, ctx->fmt.at3.formatByte2); + // The ME does look at the format bytes, though: both zero fails. + if (ctx->fmt.at3.formatByte1 == 0 && ctx->fmt.at3.formatByte2 == 0) { + ctx->err = 0x20f; + return MECall(hleLogError(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "no Atrac3+ format"), 147); } + ctx->neededMem = 0x7bc0; break; case 0x1001: ctx->neededMem = 0x3de0; @@ -528,15 +638,22 @@ static int sceAudiocodecCheckNeedMem(u32 ctxPtr, int codec) { break; case 0x1003: // Kosmodrones uses sceAudiocodec directly (no intermediate library). - ctx->neededMem = 0x18f20; - INFO_LOG(Log::ME, "CheckNeedMem for codec %04x: format %02x %02x", codec, ctx->fmt.at3.formatByte1, ctx->fmt.at3.formatByte2); + ctx->neededMem = 0x658c; break; + case 0x1004: + return hleLogError(Log::ME, SCE_AVCODEC_ERROR_UNSUPPORTED, "codec 1004"); + case 0x1005: + // What the hardware does, without asking the ME. Looks like an error code in the size field. + ctx->neededMem = 0x80000003; + ctx->err = 0; + return hleLogWarning(Log::ME, 0, "codec 1005"); } ctx->err = 0; ctx->magic = 0x5100601; - return hleLogInfo(Log::ME, 0, "%s: %x", GetCodecName(codec), ctx->neededMem); + static const int needMemUs[4] = { 147, 80, 79, 57 }; + return MECall(hleLogInfo(Log::ME, 0, "%s: %x", GetCodecName(codec), ctx->neededMem), needMemUs[codec - 0x1000]); } static int sceAudiocodecGetEDRAM(u32 ctxPtr, int codec) { @@ -552,7 +669,7 @@ static int sceAudiocodecGetEDRAM(u32 ctxPtr, int codec) { break; } ctx->edramAddr = (ctx->allocMem + 0x3f) & ~0x3f; // round up to 64 bytes. - return hleLogInfo(Log::ME, 0, "edram address set to %08x", ctx->edramAddr); + return MECall(hleLogInfo(Log::ME, 0, "edram address set to %08x", ctx->edramAddr), 56); } // One parameter, not two: the real module (audiocodec_260.prx, 080007c0) reads only a0 and sets @@ -562,10 +679,13 @@ static int sceAudiocodecGetEDRAM(u32 ctxPtr, int codec) { // CheckNeedMem/GetEDRAM to size the allocation and only creates a decoder if the stream turns out // to need one, so there is often nothing here to drop. static int sceAudiocodecReleaseEDRAM(u32 ctxPtr) { - if (removeDecoder(ctxPtr)) { - return hleLogInfo(Log::ME, 0); + if (Memory::IsValidRange(ctxPtr, sizeof(SceAudiocodecCodec))) { + PSPPointer::Create(ctxPtr)->edramAddr = 0; } - return hleLogDebug(Log::ME, 0, "no decoder for this context"); + if (removeDecoder(ctxPtr)) { + return MECall(hleLogInfo(Log::ME, 0), 56); + } + return MECall(hleLogDebug(Log::ME, 0, "no decoder for this context"), 56); } static int sceAudiocodecGetOutputBytes(u32 ctxPtr, int codec, u32 outBytesAddr) { @@ -587,40 +707,43 @@ static int sceAudiocodecGetOutputBytes(u32 ctxPtr, int codec, u32 outBytesAddr) return hleLogInfo(Log::ME, 0); } -struct At3HeaderMap { +// The Atrac3 parameter at 0x28, as libatrac3plus.prx's table at 08807f08 maps it from frame size +// and joint stereo. The hardware decoded 0x04, 0x0B and 0x0F streams as listed. +struct At3Param { + u8 param; u16 bytes; - u16 channels; + u8 channels; u8 jointStereo; - - bool Matches(int bytesPerFrame, int encodedChannels) const { - return this->bytes == bytesPerFrame && this->channels == encodedChannels; - } }; -// These should represent all possible supported bitrates (66, 104, and 132 for stereo.) -static const At3HeaderMap at3HeaderMap[] = { - { 0x00C0, 1, 0 }, // 132/2 (66) kbps mono - { 0x0098, 1, 0 }, // 105/2 (52.5) kbps mono - { 0x0180, 2, 0 }, // 132 kbps stereo - { 0x0130, 2, 0 }, // 105 kbps stereo - // At this size, stereo can only use joint stereo. - { 0x00C0, 2, 1 }, // 66 kbps stereo +static const At3Param at3Params[] = { + { 0x04, 0x0180, 2, 0 }, + { 0x06, 0x0130, 2, 0 }, + { 0x0B, 0x00C0, 2, 1 }, + { 0x0E, 0x00C0, 1, 0 }, + { 0x0F, 0x0098, 1, 0 }, }; -static int Atrac3BytesPerFrameFromContext(const SceAudiocodecCodec *ctx, int *channels) { - // AtracCtx2 puts the joint-stereo flag here for Atrac3, mirroring libatrac3plus. - const bool jointStereo = (ctx->fmt.at3.formatByte1 & 1) != 0; - if (jointStereo) { - for (size_t i = 0; i < ARRAY_SIZE(at3HeaderMap); ++i) { - if (at3HeaderMap[i].jointStereo) { - *channels = at3HeaderMap[i].channels; - return at3HeaderMap[i].bytes; - } +// Returns false for a parameter the hardware rejects. +static bool Atrac3LayoutFromContext(const SceAudiocodecCodec *ctx, int *bytesPerFrame, int *channels, bool *jointStereo) { + const u32 param = *(const u32_le *)ctx->fmt.raw; + for (const At3Param &p : at3Params) { + if (p.param == param) { + *bytesPerFrame = p.bytes; + *channels = p.channels; + *jointStereo = p.jointStereo != 0; + return true; } } - // 132kbps stereo - by far the most common, and what we assumed unconditionally before. + // Not in libatrac3plus.prx's table. Fall back to 132kbps stereo, the most common. + *bytesPerFrame = 0x180; *channels = 2; - return 0x180; + *jointStereo = false; + if (param > 0x0F) { + return false; + } + WARN_LOG(Log::ME, "Unknown Atrac3 parameter %02x", param); + return true; } bool IsAtrac3StreamJointStereo(int codecType, int bytesPerFrame, int channels) { @@ -629,9 +752,9 @@ bool IsAtrac3StreamJointStereo(int codecType, int bytesPerFrame, int channels) { return false; } - for (size_t i = 0; i < ARRAY_SIZE(at3HeaderMap); ++i) { - if (at3HeaderMap[i].Matches(bytesPerFrame, channels)) { - return at3HeaderMap[i].jointStereo; + for (const At3Param &p : at3Params) { + if (p.bytes == bytesPerFrame && p.channels == channels) { + return p.jointStereo != 0; } } @@ -656,7 +779,7 @@ void Register_sceAudiocodec() { } void __sceAudiocodecDoState(PointerWrap &p){ - auto s = p.Section("AudioList", 0, 2); + auto s = p.Section("AudioList", 0, 3); if (!s) { if (p.mode == PointerWrap::MODE_READ) { clearDecoders(); @@ -727,4 +850,10 @@ void __sceAudiocodecDoState(PointerWrap &p){ delete[] ctxPtr_; } } + + if (s >= 3) { + Do(p, g_primingFrames); + } else if (p.mode == PointerWrap::MODE_READ) { + g_primingFrames.clear(); + } } diff --git a/ext/at3_standalone/atrac3.cpp b/ext/at3_standalone/atrac3.cpp index 8c3f7f5664..186dcff6f7 100644 --- a/ext/at3_standalone/atrac3.cpp +++ b/ext/at3_standalone/atrac3.cpp @@ -902,7 +902,9 @@ ATRAC3Context *atrac3_alloc(int channels, int *block_align, const uint8_t *extra q->decoded_bytes_buffer = (uint8_t *)av_mallocz(FFALIGN(q->block_align, 4) + AV_INPUT_BUFFER_PADDING_SIZE); /* initialize the MDCT transform */ - if ((ret = ff_mdct_init(&q->mdct_ctx, 9, 1, 1.0 / 32768)) < 0) { + /* Negative scale: the PSP's decoder outputs the opposite polarity to ffmpeg's, in mono, + * stereo and joint stereo alike (compared on hardware through sceAudiocodec). */ + if ((ret = ff_mdct_init(&q->mdct_ctx, 9, 1, -1.0 / 32768)) < 0) { av_log(AV_LOG_ERROR, "Error initializing MDCT"); av_freep(&q->decoded_bytes_buffer); diff --git a/test.py b/test.py index ffd5dafbf7..9ddf5e38d5 100755 --- a/test.py +++ b/test.py @@ -129,6 +129,7 @@ tests_good = [ "audio/atrac/second/setbuffer", "audio/atrac/setdata", "audio/atrac/sas", + "audio/audiocodec/basic", "audio/mp3/checkneeded", "audio/mp3/getbitrate", "audio/mp3/getchannel", From c2bc2d9308a5bfc0591fe704a2b9b760a999260d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 28 Sep 2026 09:52:09 -0600 Subject: [PATCH 2/3] ME: Charge measured times for sceVideocodec calls and the rest of sceAudiocodec Measured on a PSP (pspautotests video/mp4/mp4timing, audio/audiocodec/timing): - sceVideocodec Open, GetEDRAM, GetVersion and ReleaseEDRAM take ~70-150us, Init ~26.6ms (sceMpegCreate is 27-28ms), Delete ~21ms (was 2ms), and Stop 132us with nothing held back. All go through the ME queue now. - Decodes that return no picture take as long as those that do; they were free. - Open reports the EDRAM the decoder needs (0x3c2c) at ctx+0x18, which mpeg.prx passes on to GetEDRAM. - sceAudiocodec: failed decodes (214/142/169us) and mono Atrac3+ init (524us). Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HLE/sceAudiocodec.cpp | 24 +++++++------- Core/HLE/sceVideocodec.cpp | 64 +++++++++++++++++++++++++++----------- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/Core/HLE/sceAudiocodec.cpp b/Core/HLE/sceAudiocodec.cpp index 11e3e498a0..d13a64e6b0 100644 --- a/Core/HLE/sceAudiocodec.cpp +++ b/Core/HLE/sceAudiocodec.cpp @@ -232,15 +232,16 @@ void __AudioCodecShutdown() { // Atrac3 80 56 210 56 3 // MP3 79 57 517 57 82 // AAC 57 57 230 55 3 -// A GetInfo of 3us didn't reach the ME. Calls that fail in the ME take time too; how long a failed -// decode takes isn't measured. +// A GetInfo of 3us didn't reach the ME. Initializing a mono Atrac3+ decoder (as InitMono does for +// libatrac3plus.prx's MOut functions) takes 524us. Failed decodes take 214us for an Atrac3+ +// bitstream error, 142us for a bad Atrac3+ frame header, and 169us for an Atrac3 bitstream error. static int MECall(int result, int us) { return hleDelayResult(result, "audiocodec", MEScheduleJob(PowerScaleFromDefaultClock(us))); } -static int InitUs(int codec) { +static int InitUs(int codec, const SceAudiocodecCodec *ctx) { switch (codec) { - case PSP_CODEC_AT3PLUS: return 646; + case PSP_CODEC_AT3PLUS: return ((ctx->fmt.at3.formatByte1 >> 2) & 7) == 1 ? 524 : 646; case PSP_CODEC_AT3: return 210; case PSP_CODEC_MP3: return 517; default: return 230; @@ -336,7 +337,7 @@ static int __AudioCodecInitCommon(u32 ctxPtr, int codec, bool mono) { break; case PSP_CODEC_AAC: if (!AacSampleRateFromContext(ctx)) { - return MECall(hleLogError(Log::ME, SCE_AVCODEC_ERROR_UNSUPPORTED, "bad AAC sample rate %d", ctx->fmt.aac.sampleRate), InitUs(codec)); + return MECall(hleLogError(Log::ME, SCE_AVCODEC_ERROR_UNSUPPORTED, "bad AAC sample rate %d", ctx->fmt.aac.sampleRate), InitUs(codec, ctx)); } primingFrames = 2; break; @@ -346,7 +347,7 @@ static int __AudioCodecInitCommon(u32 ctxPtr, int codec, bool mono) { CalculateInputBytesAndChannelsAt3Plus(ctx, &bytesPerFrame, &channels); if (channels == 0) { ctx->err = 0x202; - return MECall(hleLogError(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "bad Atrac3+ format byte %02x", ctx->fmt.at3.formatByte1), InitUs(codec)); + return MECall(hleLogError(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "bad Atrac3+ format byte %02x", ctx->fmt.at3.formatByte1), InitUs(codec, ctx)); } primingFrames = 1; break; @@ -357,7 +358,7 @@ static int __AudioCodecInitCommon(u32 ctxPtr, int codec, bool mono) { bool jointStereo; if (!Atrac3LayoutFromContext(ctx, &bytesPerFrame, &channels, &jointStereo)) { ctx->err = 0x186; - return MECall(hleLogError(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "bad Atrac3 parameter %08x", *(const u32_le *)ctx->fmt.raw), InitUs(codec)); + return MECall(hleLogError(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "bad Atrac3 parameter %08x", *(const u32_le *)ctx->fmt.raw), InitUs(codec, ctx)); } break; } @@ -368,7 +369,7 @@ static int __AudioCodecInitCommon(u32 ctxPtr, int codec, bool mono) { CreateDecoderForContext(ctxPtr, audioType); // Not in CreateDecoderForContext: a state load restores what's left of it instead. g_primingFrames[ctxPtr] = primingFrames; - return MECall(hleLogDebug(Log::ME, 0), InitUs(codec)); + return MECall(hleLogDebug(Log::ME, 0), InitUs(codec, ctx)); } // How long the ME takes over one frame, in microseconds at the default 222MHz clock. Fitted to @@ -377,7 +378,8 @@ static int __AudioCodecInitCommon(u32 ctxPtr, int codec, bool mono) { // Atrac3 stereo: 1138 at 0x180, 1063 at 0xC0 joint stereo. Mono: 685 at 0x98. // MP3 MPEG1 (1152 samples): 2575 at 418 bytes, 2698 at 1045. MPEG2 (576): 1411 at 104, 1464 at 209. // AAC-LC stereo 44.1kHz: 1651 at ~190 bytes, 1990 at ~373, 2042 at ~559. -// The mono Atrac3+ slope is a guess from its one data point. +// The mono Atrac3+ slope is a guess from its one data point. Content matters as well as size: the +// synthetic two-tone AAC in video/mp4 decodes about 15% faster than music at the same bitrate. static int EstimateDecodeUs(int codec, int channels, int frameBytes, const SceAudiocodecCodec *ctx) { switch (codec) { case PSP_CODEC_AT3PLUS: @@ -429,7 +431,7 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) { ctx->err = frameError; ctx->srcBytesRead = 0; ctx->dstBytesWritten = 0; - return MECall(hleLogWarning(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "Atrac3+ frame doesn't match the context: err %03x", frameError), 100); + return MECall(hleLogWarning(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "Atrac3+ frame doesn't match the context: err %03x", frameError), 142); } break; } @@ -517,7 +519,7 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) { ctx->err = codec == PSP_CODEC_AT3PLUS ? 0x20a : 0x182; ctx->srcBytesRead = 0; ctx->dstBytesWritten = 0; - return MECall(hleLogWarning(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "%s frame failed to decode", GetCodecName(codec)), 100); + return MECall(hleLogWarning(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "%s frame failed to decode", GetCodecName(codec)), codec == PSP_CODEC_AT3PLUS ? 214 : 169); } if (!result) { ctx->err = 0x20b; diff --git a/Core/HLE/sceVideocodec.cpp b/Core/HLE/sceVideocodec.cpp index a808925f26..d5c5be00d1 100644 --- a/Core/HLE/sceVideocodec.cpp +++ b/Core/HLE/sceVideocodec.cpp @@ -436,6 +436,24 @@ static void WriteTiledYCbCr(const u32 *buffers, const AvcDecoder &dec, int width } } +// Every call here that reaches the ME blocks while it answers. Measured at 222MHz in pspautotests +// video/mp4/mp4timing: Open, GetEDRAM, GetVersion and ReleaseEDRAM called directly, the rest through +// mpeg.prx's thin wrappers. sceMpegCreate (Open, Init, GetVersion, SetMemory) took 26.9-28.0ms, +// nearly all of it Init and SetMemory, which are charged together to Init here. sceMpegDelete took +// 21.2ms, and 35.9ms in a run that had stopped the decoder twice first. An sceMpegAvcDecodeStop with +// nothing held back took 132us (with three pictures to hand over, 4.9ms, the rest being their output). +static const int openUs = 96; +static const int getEdramUs = 146; +static const int getVersionUs = 94; +static const int releaseEdramUs = 66; +static const int initUs = 26600; +static const int stopUs = 132; +static const int deleteUs = 21000; + +static int MECall(int result, const char *reason, int us) { + return hleDelayResult(result, reason, MEScheduleJob(PowerScaleFromDefaultClock(us))); +} + static int sceVideocodecOpen(u32 ctxAddr, int type) { if (!Memory::IsValidRange(ctxAddr, 96)) { return hleLogError(Log::ME, -1, "bad context pointer"); @@ -445,7 +463,11 @@ static int sceVideocodecOpen(u32 ctxAddr, int type) { return hleLogError(Log::ME, -1, "built without ffmpeg, can't decode video"); } g_videocodecCtxs[ctxAddr].type = type; - return hleLogInfo(Log::ME, 0, "type %d", type); + if (type == 0) { + // The EDRAM the decoder needs, which mpeg.prx reads back and passes to GetEDRAM. + Memory::WriteUnchecked_U32(0x3c2c, ctxAddr + CTX_EDRAM_SIZE); + } + return MECall(hleLogInfo(Log::ME, 0, "type %d", type), "videocodec open", openUs); } static int sceVideocodecInit(u32 ctxAddr, int type) { @@ -458,7 +480,7 @@ static int sceVideocodecInit(u32 ctxAddr, int type) { vctx.decoder = new AvcDecoder(); vctx.frameCount = 0; vctx.type = type; - return hleLogInfo(Log::ME, 0, "type %d", type); + return MECall(hleLogInfo(Log::ME, 0, "type %d", type), "videocodec init", initUs); } // See g_meRam for why this doesn't come out of the game's memory. @@ -483,7 +505,7 @@ static int sceVideocodecGetEDRAM(u32 ctxAddr, int type) { // works in 64-byte grains, so the two only differ in what they mean, not in value. Memory::WriteUnchecked_U32(addr, ctxAddr + CTX_EDRAM); Memory::WriteUnchecked_U32(addr, ctxAddr + CTX_EDRAM_RAW); - return hleLogInfo(Log::ME, 0, "%u bytes at %08x in ME memory", size, addr); + return MECall(hleLogInfo(Log::ME, 0, "%u bytes at %08x in ME memory", size, addr), "videocodec getedram", getEdramUs); } static int sceVideocodecReleaseEDRAM(u32 ctxAddr) { @@ -502,7 +524,7 @@ static int sceVideocodecReleaseEDRAM(u32 ctxAddr) { } Memory::WriteUnchecked_U32(0, ctxAddr + CTX_EDRAM); Memory::WriteUnchecked_U32(0, ctxAddr + CTX_EDRAM_RAW); - return hleLogInfo(Log::ME, 0, "released %08x", token); + return MECall(hleLogInfo(Log::ME, 0, "released %08x", token), "videocodec releaseedram", releaseEdramUs); } static int sceVideocodecDecode(u32 ctxAddr, int type) { @@ -616,34 +638,38 @@ static int sceVideocodecDecode(u32 ctxAddr, int type) { // sceMpegAvcDecode (5.8ms) less sceMpegAvcCsc alone (2.4ms), in pspautotests // video/mpeg/playertiming. Movie players that present every decoded frame after a single // vblank wait rely on decode, colour conversion and blit adding up to more than a vblank. + // It takes as long when the decoder holds the picture back: the first sceMpegAvcDecode calls of + // a stream, which return none, took 4.2-5.4ms in video/mp4/mp4timing. Until a picture has told + // us the size, assume full screen. int delayUs = 0; - if (gotFrame && width > 0 && height > 0) { - delayUs = MEScheduleJob(PowerScaleFromDefaultClock((int)(3400LL * width * height / (480 * 272)))); + if (auBytes > 0) { + const int w = vctx.decoder->Width() > 0 ? vctx.decoder->Width() : 480; + const int h = vctx.decoder->Height() > 0 ? vctx.decoder->Height() : 272; + delayUs = MEScheduleJob(PowerScaleFromDefaultClock((int)(3400LL * w * h / (480 * 272)))); } if (delayUs > 0) { - return hleDelayResult(hleLogDebug(Log::ME, 0, "type %d, %d bytes -> frame %dx%d", - type, auBytes, width, height), "videocodec decode", delayUs); + return hleDelayResult(hleLogDebug(Log::ME, 0, "type %d, %d bytes -> %s %dx%d", + type, auBytes, gotFrame ? "frame" : "no frame yet", width, height), "videocodec decode", delayUs); } return hleLogDebug(Log::ME, 0, "type %d, %d bytes -> %s %dx%d", type, auBytes, gotFrame ? "frame" : "no frame yet", width, height); } -// Stopping or deleting the decoder is an ME round-trip and takes real time on hardware. Returning -// immediately is not correct, because a game can be relying on a thread of its own getting to run -// once more before it tears things down. Jak and Daxter deletes its video_sound_thread straight -// after sceVideocodecDelete without waiting for it to exit, and with no time passing here the audio -// thread never gets to deliver the wake that would let it exit - so the delete fails with -// NOT_DORMANT and the thread lives on, reading a context the game has already freed. -// One audio mix block is 64 samples at 44100Hz, about 1.45ms, so stay above that. -static const int videocodecTeardownDelayUs = 2000; +// Returning immediately from Stop or Delete is not correct, because a game can be relying on a +// thread of its own getting to run once more before it tears things down. Jak and Daxter deletes +// its video_sound_thread straight after sceVideocodecDelete without waiting for it to exit, and +// with no time passing here the audio thread never gets to deliver the wake that would let it exit +// - so the delete fails with NOT_DORMANT and the thread lives on, reading a context the game has +// already freed. One audio mix block is 64 samples at 44100Hz, about 1.45ms, which Delete's +// measured time is well above. static int sceVideocodecStop(u32 ctxAddr, int type) { auto it = g_videocodecCtxs.find(ctxAddr); if (it != g_videocodecCtxs.end() && it->second.decoder) { it->second.decoder->Flush(); } - return hleDelayResult(hleLogInfo(Log::ME, 0), "videocodec stop", videocodecTeardownDelayUs); + return MECall(hleLogInfo(Log::ME, 0), "videocodec stop", stopUs); } static int sceVideocodecDelete(u32 ctxAddr, int type) { @@ -652,7 +678,7 @@ static int sceVideocodecDelete(u32 ctxAddr, int type) { FreeContext(it->second); g_videocodecCtxs.erase(it); } - return hleDelayResult(hleLogInfo(Log::ME, 0), "videocodec delete", videocodecTeardownDelayUs); + return MECall(hleLogInfo(Log::ME, 0), "videocodec delete", deleteUs); } static int sceVideocodecGetVersion(u32 ctxAddr, int type) { @@ -661,7 +687,7 @@ static int sceVideocodecGetVersion(u32 ctxAddr, int type) { } // The value a real PSP returns, read with JpcspTrace. Memory::WriteUnchecked_U32(0x78, ctxAddr + CTX_VERSION); - return hleLogInfo(Log::ME, 0); + return MECall(hleLogInfo(Log::ME, 0), "videocodec getversion", getVersionUs); } static int sceVideocodecGetSEI(u32 ctxAddr, int type) { From 023ad93ed3b9827e16765f5092e3eb2a564a2d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 28 Sep 2026 12:16:12 -0600 Subject: [PATCH 3/3] sceVideocodec: Don't hold the ME for Init and Delete They take tens of milliseconds for the caller, but queueing that time on the shared ME timeline made the SAS mix wait behind them. In Jak and Daxter that held up the sound threads at the end of the first clip, so video_sound_thread got its last wake only after the game had deleted it (NOT_DORMANT), and the orphaned thread then read a freed context. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HLE/sceVideocodec.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Core/HLE/sceVideocodec.cpp b/Core/HLE/sceVideocodec.cpp index d5c5be00d1..c84ae1fa9b 100644 --- a/Core/HLE/sceVideocodec.cpp +++ b/Core/HLE/sceVideocodec.cpp @@ -454,6 +454,16 @@ static int MECall(int result, const char *reason, int us) { return hleDelayResult(result, reason, MEScheduleJob(PowerScaleFromDefaultClock(us))); } +// Init and Delete take tens of milliseconds for the caller, but they don't hold the ME for that +// long: queueing them there stalled the SAS mix behind them, and with it Jak and Daxter's sound +// threads, whose last wake to video_sound_thread then came after the game had deleted it +// (NOT_DORMANT, then the orphan reads a freed context). On hardware SAS keeps mixing through them: +// pspautotests audio/timing/meshare saw ~1.3ms SAS calls throughout a 39ms sceMpegCreate and a 32ms +// sceMpegDelete, the longest 2.2ms. +static int CallerWait(int result, const char *reason, int us) { + return hleDelayResult(result, reason, PowerScaleFromDefaultClock(us)); +} + static int sceVideocodecOpen(u32 ctxAddr, int type) { if (!Memory::IsValidRange(ctxAddr, 96)) { return hleLogError(Log::ME, -1, "bad context pointer"); @@ -480,7 +490,7 @@ static int sceVideocodecInit(u32 ctxAddr, int type) { vctx.decoder = new AvcDecoder(); vctx.frameCount = 0; vctx.type = type; - return MECall(hleLogInfo(Log::ME, 0, "type %d", type), "videocodec init", initUs); + return CallerWait(hleLogInfo(Log::ME, 0, "type %d", type), "videocodec init", initUs); } // See g_meRam for why this doesn't come out of the game's memory. @@ -678,7 +688,7 @@ static int sceVideocodecDelete(u32 ctxAddr, int type) { FreeContext(it->second); g_videocodecCtxs.erase(it); } - return MECall(hleLogInfo(Log::ME, 0), "videocodec delete", deleteUs); + return CallerWait(hleLogInfo(Log::ME, 0), "videocodec delete", deleteUs); } static int sceVideocodecGetVersion(u32 ctxAddr, int type) {