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) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-28 16:57:41 -06:00
1 parent c162eb3d74
commit c2bc2d9308
2 files changed
+58 -30

No files matched your search

+13 -11
View File
@@ -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;
+45 -19
View File
@@ -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) {