diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt
index a283e7c935..c42305224a 100644
--- a/Core/CMakeLists.txt
+++ b/Core/CMakeLists.txt
@@ -494,8 +494,12 @@ add_library(Core STATIC
HLE/sceKernelTime.h
HLE/sceKernelVTimer.cpp
HLE/sceKernelVTimer.h
+ HLE/sceVideocodec.cpp
+ HLE/sceVideocodec.h
HLE/sceMpeg.cpp
HLE/sceMpeg.h
+ HLE/sceMpegbase.cpp
+ HLE/sceMpegbase.h
HLE/sceNet.cpp
HLE/sceNet.h
HLE/sceNet_lib.cpp
@@ -595,6 +599,8 @@ add_library(Core STATIC
HW/Display.h
HW/GpioMMIO.cpp
HW/GpioMMIO.h
+ HW/AvcDecoder.cpp
+ HW/AvcDecoder.h
HW/MediaEngine.cpp
HW/MediaEngine.h
HW/MpegDemux.cpp
diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj
index b1cbe3f35e..c39cf6b058 100644
--- a/Core/Core.vcxproj
+++ b/Core/Core.vcxproj
@@ -637,7 +637,9 @@
+
+
@@ -671,6 +673,7 @@
+
@@ -1150,7 +1153,9 @@
+
+
@@ -1187,6 +1192,7 @@
+
diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters
index 8bb8462455..b0e07da334 100644
--- a/Core/Core.vcxproj.filters
+++ b/Core/Core.vcxproj.filters
@@ -255,9 +255,15 @@
HLE\Libraries
+
+ HLE\Libraries
+
HLE\Libraries
+
+ HLE\Libraries
+
HLE\Libraries
@@ -354,6 +360,9 @@
HLE\Libraries
+
+ HW
+
HW
@@ -1581,6 +1590,12 @@
HLE\Libraries
+
+ HLE\Libraries
+
+
+ HLE\Libraries
+
HLE\Libraries
@@ -1686,6 +1701,9 @@
HLE\Libraries
+
+ HW
+
HW
diff --git a/Core/HLE/HLE.cpp b/Core/HLE/HLE.cpp
index cdc7d40ed8..1e3566ed00 100644
--- a/Core/HLE/HLE.cpp
+++ b/Core/HLE/HLE.cpp
@@ -340,13 +340,13 @@ static void hleDelayResultFinish(u64 userdata, int cycleslate) {
// Which files need to be present for a disable-hle-flag to be honoured.
//
// Two shapes end up here. sceMp4 because libmp4.prx and mp4msv.prx are firmware libraries no game
-// ships, so without a dump there is nothing to run at all. sceFont because the module is on the
+// ships, so without firmware there is nothing to run at all. sceFont because the module is on the
// disc like any other but reads its fonts from flash0:/font with nothing to fall back on. Either
// way the HLE is the only thing that can serve, so the flag comes off.
//
// sceMpeg, sceMp3 and sceAtrac are deliberately not here: plenty of discs carry their own copy
// (Death Jr. has MPEG.PRX and LIBATRAC3PLUS.PRX under PSP_GAME/USRDIR/MODULES), and dropping the
-// flag for want of a dump would replace a perfectly good disc module with our HLE. Those check for
+// flag for want of firmware would replace a perfectly good disc module with our HLE. Those check for
// a real module at the point they would load one, and warn there if neither source has it.
static void CheckDisableHLEAvailability() {
g_unavailableDisableFlags = (DisableHLEFlags)0;
diff --git a/Core/HLE/HLETables.cpp b/Core/HLE/HLETables.cpp
index 94e15495e9..eddb1b028b 100644
--- a/Core/HLE/HLETables.cpp
+++ b/Core/HLE/HLETables.cpp
@@ -57,6 +57,7 @@
#include "sceNetAdhocMatching.h"
#include "sceNp.h"
#include "sceMpeg.h"
+#include "sceMpegbase.h"
#include "sceOpenPSID.h"
#include "sceResmgr.h"
#include "sceP3da.h"
@@ -90,6 +91,7 @@
#include "sceNetResolver.h"
// #include "sceNp2.h"
#include "sceNet_lib.h"
+#include "sceVideocodec.h"
static const HLEFunction FakeSysCalls[] = {
{NID_THREADRETURN, __KernelReturnFromThread, "__KernelReturnFromThread", 'x', ""},
@@ -327,6 +329,7 @@ void RegisterAllModules() {
Register_sceChkreg();
Register_sceVshBridge();
Register_sceResmgr();
+ Register_sceVideocodec();
// add new modules here.
diff --git a/Core/HLE/sceAudiocodec.cpp b/Core/HLE/sceAudiocodec.cpp
index 49782e5232..5306340605 100644
--- a/Core/HLE/sceAudiocodec.cpp
+++ b/Core/HLE/sceAudiocodec.cpp
@@ -31,6 +31,10 @@
// g_audioDecoderContexts is to store current playing audios.
std::map g_audioDecoderContexts;
+// The Atrac3+ frame size each decoder in the map above was created for. mpeg.prx doesn't put the
+// 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;
static bool oldStateLoaded = false;
@@ -108,13 +112,38 @@ 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) {
+void CalculateInputBytesAndChannelsAt3Plus(const SceAudiocodecCodec *ctx, int *inputBytes, int *channels, int *headerBytes = nullptr) {
*inputBytes = 0;
*channels = 2;
+ if (headerBytes) {
+ *headerBytes = 0;
+ }
- int size = ctx->fmt.at3.formatByte2 * 8 + 8;
+ 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;
+ }
+
+ int size = formatByte2 * 8 + 8;
// No idea if this is accurate, this is just a guess...
- if (ctx->fmt.at3.formatByte1 & 8) {
+ if (formatByte1 & 8) {
*channels = 2;
} else {
*channels = 1;
@@ -180,6 +209,7 @@ static bool removeDecoder(u32 ctxPtr) {
if (it != g_audioDecoderContexts.end()) {
delete it->second;
g_audioDecoderContexts.erase(it);
+ g_at3PlusFrameBytes.erase(ctxPtr);
return true;
}
return false;
@@ -190,6 +220,7 @@ static void clearDecoders() {
delete decoder;
}
g_audioDecoderContexts.clear();
+ g_at3PlusFrameBytes.clear();
}
void __AudioCodecInit() {
@@ -262,6 +293,7 @@ static int __AudioCodecInitCommon(u32 ctxPtr, int codec, bool mono) {
AudioDecoder *decoder = CreateAudioDecoder(audioType, 44100, channels, bytesPerFrame, extraData, sizeof(extraData));
decoder->SetCtxPtr(ctxPtr);
g_audioDecoderContexts[ctxPtr] = decoder;
+ g_at3PlusFrameBytes[ctxPtr] = bytesPerFrame;
return hleLogDebug(Log::ME, 0);
}
@@ -291,10 +323,11 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) {
int bytesPerFrame = 0;
int channels = 2;
int sampleRate = 0;
+ int headerBytes = 0;
switch (codec) {
case PSP_CODEC_AT3PLUS:
- CalculateInputBytesAndChannelsAt3Plus(ctx, &bytesPerFrame, &channels);
+ CalculateInputBytesAndChannelsAt3Plus(ctx, &bytesPerFrame, &channels, &headerBytes);
break;
case PSP_CODEC_MP3:
// Not srcBytesRead - that's an output field holding what the *previous* call consumed.
@@ -328,6 +361,19 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) {
g_audioDecoderContexts[ctxPtr] = decoder;
}
+ if (decoder && codec == PSP_CODEC_AT3PLUS && bytesPerFrame > 0) {
+ auto it = g_at3PlusFrameBytes.find(ctxPtr);
+ 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);
+ removeDecoder(ctxPtr);
+ decoder = CreateAudioDecoder(audioType, 44100, channels, bytesPerFrame);
+ decoder->SetCtxPtr(ctxPtr);
+ g_audioDecoderContexts[ctxPtr] = decoder;
+ g_at3PlusFrameBytes[ctxPtr] = bytesPerFrame;
+ }
+ }
+
if (decoder) {
// Use SimpleAudioDec to decode audio
// Decode audio
@@ -338,13 +384,26 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) {
int16_t *outBuf = (int16_t *)Memory::GetPointerWriteOrException(ctx->outBuf);
- bool result = decoder->Decode(Memory::GetPointerOrException(ctx->inBuf), bytesPerFrame, &inDataConsumed, 2, outBuf, &outSamples);
+ // For Atrac3+ in a PSMF the length came out of the frame's own header, so it's only as
+ // trustworthy as the stream - check the whole span before handing it to the decoder
+ // rather than just the first byte, which is all GetPointerOrException would look at.
+ // IsValidRange first: the range accessors raise a memory exception rather than returning
+ // null, and a stream that lies about its length shouldn't fault the game.
+ const u32 inAddr = ctx->inBuf + headerBytes;
+ const u8 *inBuf = (bytesPerFrame > 0 && Memory::IsValidRange(inAddr, bytesPerFrame))
+ ? Memory::GetPointerUnchecked(inAddr) : nullptr;
+ if (!inBuf) {
+ ctx->err = 0x20b;
+ return hleLogError(Log::ME, 0, "%d bytes at %08x isn't readable", bytesPerFrame, inAddr);
+ }
+
+ bool result = decoder->Decode(inBuf, bytesPerFrame, &inDataConsumed, 2, outBuf, &outSamples);
if (!result) {
ctx->err = 0x20b;
ERROR_LOG(Log::ME, "AudioCodec decode failed. Setting error to %08x", ctx->err);
}
- ctx->srcBytesRead = inDataConsumed;
+ ctx->srcBytesRead = inDataConsumed + headerBytes;
ctx->dstSamplesWritten = outSamples;
}
return hleLogDebug(Log::ME, 0, "codec %s sampleRate: %d bytesPerFrame: %d channels: %d", GetCodecName(codec), sampleRate, bytesPerFrame, channels);
@@ -394,9 +453,13 @@ static int sceAudiocodecCheckNeedMem(u32 ctxPtr, int codec) {
switch (codec) {
case 0x1000:
ctx->neededMem = 0x7bc0;
- if (ctx->fmt.at3.formatByte1 != 0x28 || ctx->fmt.at3.formatByte2 != 0x5c) {
- ctx->err = 0x20f;
- return hleLogError(Log::ME, SCE_AVCODEC_ERROR_INVALID_DATA, "Bad format values: %02x %02x", ctx->fmt.at3.formatByte1, ctx->fmt.at3.formatByte2);
+ // 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);
}
break;
case 0x1001:
diff --git a/Core/HLE/sceKernel.cpp b/Core/HLE/sceKernel.cpp
index 623b412289..86241a7239 100644
--- a/Core/HLE/sceKernel.cpp
+++ b/Core/HLE/sceKernel.cpp
@@ -45,6 +45,8 @@
#include "sceAtrac.h"
#include "sceAudio.h"
#include "sceAudiocodec.h"
+#include "sceMpegbase.h"
+#include "sceVideocodec.h"
#include "sceCcc.h"
#include "sceCtrl.h"
#include "sceDisplay.h"
@@ -158,6 +160,7 @@ void __KernelInit()
__HeapInit();
__DmacInit();
__AudioCodecInit();
+ __VideocodecInit();
__VideoPmpInit();
__UsbGpsInit();
__UsbCamInit();
@@ -197,6 +200,7 @@ void __KernelShutdown()
__UsbGpsShutdown();
__AudioCodecShutdown();
+ __VideocodecShutdown();
__VideoPmpShutdown();
__AACShutdown();
__NetAdhocShutdown();
@@ -284,6 +288,8 @@ void __KernelDoState(PointerWrap &p)
__JpegDoState(p);
__Mp3DoState(p);
__MpegDoState(p);
+ __MpegBaseDoState(p);
+ __VideocodecDoState(p);
__NetDoState(p);
__NetAdhocDoState(p);
__PowerDoState(p);
diff --git a/Core/HLE/sceMpeg.cpp b/Core/HLE/sceMpeg.cpp
index 3418883434..99c295a757 100644
--- a/Core/HLE/sceMpeg.cpp
+++ b/Core/HLE/sceMpeg.cpp
@@ -23,8 +23,10 @@
#include "Common/Serialize/SerializeMap.h"
#include "Common/Swap.h"
#include "Core/HLE/sceMpeg.h"
+#include "Core/HLE/sceMpegbase.h"
#include "Core/HLE/sceKernelModule.h"
#include "Core/HLE/sceKernelThread.h"
+#include "Core/Config.h"
#include "Core/HLE/HLE.h"
#include "Core/HLE/FunctionWrappers.h"
#include "Core/HLE/ErrorCodes.h"
@@ -121,13 +123,10 @@ static AVPixelFormat pmp_want_pix_fmt;
#endif
-struct SceMpegLLI
-{
- u32 pSrc;
- u32 pDst;
- u32 Next;
- int iSize;
-};
+void MpegSetPmpVideoSource(u32 addr, int blocks) {
+ pmp_videoSource = addr;
+ pmp_nBlocks = blocks;
+}
void SceMpegAu::read(u32 addr) {
Memory::Memcpy(this, addr, sizeof(*this), "SceMpegAu");
@@ -350,6 +349,7 @@ private:
};
void __MpegInit() {
+ __MpegBaseInit();
isMpegInit = false;
mpegLibVersion = 0x010A;
streamIdGen = 1;
@@ -2320,39 +2320,3 @@ void Register_sceMpeg()
{
RegisterHLEModule("sceMpeg", ARRAY_SIZE(sceMpeg), sceMpeg);
}
-
-// This function is currently only been used for PMP videos
-// p pointing to a SceMpegLLI structure consists of video frame blocks.
-static u32 sceMpegBasePESpacketCopy(u32 p)
-{
- pmp_videoSource = p;
- pmp_nBlocks = 0;
-
- auto lli = PSPPointer::Create(p);
- while (lli.IsValid()) {
- pmp_nBlocks++;
- // lli.Next ==0 for last block
- if (lli->Next == 0){
- break;
- }
- ++lli;
- }
-
- DEBUG_LOG(Log::Mpeg, "sceMpegBasePESpacketCopy(%08x), received %d block(s)", pmp_videoSource, pmp_nBlocks);
- return 0;
-}
-
-const HLEFunction sceMpegbase[] =
-{
- {0XBEA18F91, &WrapU_U, "sceMpegBasePESpacketCopy", 'x', "x" },
- {0X492B5E4B, nullptr, "sceMpegBaseCscInit", '?', "" },
- {0X0530BE4E, nullptr, "sceMpegbase_0530BE4E", '?', "" },
- {0X91929A21, nullptr, "sceMpegBaseCscAvc", '?', "" },
- {0X304882E1, nullptr, "sceMpegBaseCscAvcRange", '?', "" },
- {0X7AC0321A, nullptr, "sceMpegBaseYCrCbCopy", '?', "" }
-};
-
-void Register_sceMpegbase()
-{
- RegisterHLEModule("sceMpegbase", ARRAY_SIZE(sceMpegbase), sceMpegbase);
-};
diff --git a/Core/HLE/sceMpeg.h b/Core/HLE/sceMpeg.h
index db969b6292..9692a92176 100644
--- a/Core/HLE/sceMpeg.h
+++ b/Core/HLE/sceMpeg.h
@@ -139,7 +139,19 @@ void __MpegLoadModule(int version, u32 crc);
void Register_sceMpeg();
-void Register_sceMpegbase();
+// One block of the scatter-gather list sceMpegBasePESpacketCopy walks. Shared because the PMP
+// video path reads the same list back.
+struct SceMpegLLI {
+ u32 pSrc;
+ u32 pDst;
+ u32 Next;
+ int iSize;
+};
+
+// sceMpegBasePESpacketCopy hands the list it just walked to the PMP path, which decodes from it.
+// The two values live in sceMpeg.cpp rather than with the copy itself because __VideoPmpDoState
+// serializes them as part of sceMpeg's savestate section.
+void MpegSetPmpVideoSource(u32 addr, int blocks);
void __VideoPmpInit();
void __VideoPmpDoState(PointerWrap &p);
diff --git a/Core/HLE/sceMpegbase.cpp b/Core/HLE/sceMpegbase.cpp
new file mode 100644
index 0000000000..ecbca8e016
--- /dev/null
+++ b/Core/HLE/sceMpegbase.cpp
@@ -0,0 +1,408 @@
+// Copyright (c) 2026- PPSSPP Project.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 2.0 or later versions.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License 2.0 for more details.
+
+// A copy of the GPL 2.0 should have been included with the program.
+// If not, see http://www.gnu.org/licenses/
+
+// Official git repository and contact information can be found at
+// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+
+// sceMpegbase - the Media Engine's colour space conversion, and the DMA that feeds it.
+//
+// mpeg.prx drives these directly, so they have to be real for the firmware module to run in place
+// of our sceMpeg HLE.
+
+#include