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 +#include + +#include "Common/Serialize/Serializer.h" +#include "Common/Serialize/SerializeFuncs.h" +#include "Common/Serialize/SerializeMap.h" +#include "Common/Swap.h" +#include "Core/HLE/HLE.h" +#include "Core/HLE/FunctionWrappers.h" +#include "Core/HLE/sceMpeg.h" +#include "Core/HLE/sceMpegbase.h" +#include "Core/HLE/sceVideocodec.h" +#include "Core/MemMapHelpers.h" +#include "GPU/ge_constants.h" + +// The PES payloads gathered by sceMpegBasePESpacketCopy, keyed by the destination each was +// copied to. It carries audio as well as video - the destination is what tells them apart - so +// sceVideocodec has to ask for the one matching the address it was handed. +static std::map> g_pesPackets; +// Set by sceMpegBaseCscInit / sceMpegBaseCscSetPixelMode, and used when the caller passes 0. +static int g_mpegBaseBufferWidth = 512; +static int g_mpegBasePixelMode = GE_CMODE_32BIT_ABGR8888; + +void __MpegBaseInit() { + // None of this survives a boot on hardware. + g_pesPackets.clear(); + g_mpegBaseBufferWidth = 512; + g_mpegBasePixelMode = GE_CMODE_32BIT_ABGR8888; +} + +void __MpegBaseDoState(PointerWrap &p) { + auto s = p.Section("sceMpegbase", 0, 1); + if (!s) { + return; + } + // The pixel mode decides both the colour packing and the bytes per pixel of the output, and a + // game sets it once per movie rather than per frame - so without it here, a state resumed + // mid-movie converted at the default until the next sceMpegBaseCscInit, which may never come. + Do(p, g_mpegBaseBufferWidth); + Do(p, g_mpegBasePixelMode); + // A state can land between the copy and the decode that consumes it. + Do(p, g_pesPackets); +} + +// p pointing to a SceMpegLLI structure consists of video frame blocks. +static u32 sceMpegBasePESpacketCopy(u32 p) +{ + int nBlocks = 0; + auto lli = PSPPointer::Create(p); + while (lli.IsValid()) { + nBlocks++; + if (lli->Next == 0) { + // Last block + break; + } + ++lli; + } + MpegSetPmpVideoSource(p, nBlocks); + + // On hardware this is the DMA that moves the PES payload into the Media Engine's own memory, + // after which mpeg.prx hands sceVideocodecDecode an ME-side address we have no way to read. + // Since the copy is ours, gather the blocks here instead and let sceVideocodec decode from + // this - see MpegBaseTakePESPacket. + lli = PSPPointer::Create(p); + u32 dest = 0; + std::vector gathered; + for (int i = 0; i < nBlocks && lli.IsValid(); i++) { + if (i == 0) { + dest = lli->pDst; + } + // The list is game-supplied, so check the span validity before taking a pointer to it. + const u8 *src = (lli->iSize > 0 && Memory::IsValidRange(lli->pSrc, lli->iSize)) + ? Memory::GetTypedPointerRange(lli->pSrc, lli->iSize) : nullptr; + if (src) { + gathered.insert(gathered.end(), src, src + lli->iSize); + // Audio payloads land in main memory, and mpeg.prx hands that same address to + // sceAudiocodecDecode as its input, so for those the copy has to really happen. + // Video goes to a Media Engine address that isn't mapped for us: the gather above + // is what stands in for it there. + if (Memory::IsValidRange(lli->pDst, lli->iSize)) { + Memory::MemcpyUnchecked(lli->pDst, src, lli->iSize); + } + } + ++lli; + } + if (dest != 0) { + g_pesPackets[dest] = std::move(gathered); + } + + DEBUG_LOG(Log::Mpeg, "sceMpegBasePESpacketCopy(%08x), %d block(s) -> %08x, %d bytes", + p, nBlocks, dest, dest ? (int)g_pesPackets[dest].size() : 0); + return 0; +} + +std::vector MpegBaseTakePESPacket(u32 dest) { + auto it = g_pesPackets.find(dest); + if (it == g_pesPackets.end()) { + return std::vector(); + } + std::vector packet = std::move(it->second); + g_pesPackets.erase(it); + return packet; +} + + +// --- sceMpegbase colour conversion --------------------------------------------------------- +// +// mpeg.prx hands the ME's decoded output to these to be converted to RGB. The descriptor it +// passes is 48 bytes, which matches the range mpegbase.prx bounds-checks before using it. +// +// The dimensions appear twice. Death Jr. has the plain macroblock counts in both pairs, but +// Thrillville's psmfplayer path has them shifted up by 8 in the first pair and plain in the +// second, so the second is the one to trust. The four luma buffers are at 0x20; the chroma ones +// aren't in here at all (see the recovery in MpegBaseCscRange). +struct SceMp4AvcCscStruct { + s32_le scaledHeight; // 0x00 height << 8 on some paths, macroblocks on others - unused + s32_le scaledWidth; // 0x04 + s32_le mode0; // 0x08 + s32_le mode1; // 0x0c + s32_le height; // 0x10 in macroblocks + s32_le width; // 0x14 in macroblocks + s32_le unk18; // 0x18 + s32_le unk1c; // 0x1c + u32_le buffer[4]; // 0x20 luma only +}; +static_assert(sizeof(SceMp4AvcCscStruct) == 0x30); + + +// The ME doesn't write plain planar YCbCr. The layout below was established by analysing +// sceMpegBaseYCrCbCopy output on a real PSP (documented in JPCSP's sceVideocodec), and it uses +// all eight buffers in the descriptor: +// +// The image is divided into vertical bands 32 pixels wide, each split into two 16-pixel halves. +// Luma is one byte per pixel, and which buffer a row lands in depends on whether it is even or +// odd: +// buffer0: left half, even rows buffer1: right half, even rows +// buffer2: left half, odd rows buffer3: right half, odd rows +// Within a buffer, rows are stored 16 bytes at a time, band by band, top to bottom. +// +// Chroma is one (Cb,Cr) byte pair per 2x2 pixel square, so in chroma coordinates the bands are +// 16 wide with 8-pixel halves, and the same even/odd split applies: +// buffer4: left half, even chroma rows buffer5: left half, odd chroma rows +// buffer6: right half, even chroma rows buffer7: right half, odd chroma rows +// +// Untangling it into plain planes costs one pass per frame, which keeps the conversion below +// readable and is not where the time goes. +static bool ReadTiledYCbCr(const u32 *buffers, int width, int height, + std::vector &luma, std::vector &cb, std::vector &cr) { + const int width2 = width >> 1; + const int height2 = height >> 1; + + int sizes[8]; + VideocodecFrameBufferLayout(width, height, sizes, nullptr); + const int *ySize = sizes; + const int *cSize = sizes + 4; + + // These are addresses in the Media Engine's memory, not in PSP RAM, so they resolve through + // sceVideocodec rather than through Memory::. A descriptor that was never filled in holds + // small integers instead, and those simply aren't in the ME's range. + const u8 *y[4] = {}; + const u8 *c[4] = {}; + for (int i = 0; i < 4; i++) { + if (ySize[i] > 0) { + y[i] = VideocodecMEPointer(buffers[i], ySize[i]); + if (!y[i]) { + return false; + } + } + if (cSize[i] > 0) { + c[i] = VideocodecMEPointer(buffers[4 + i], cSize[i]); + if (!c[i]) { + return false; + } + } + } + + luma.assign((size_t)width * height, 0); + cb.assign((size_t)width2 * height2, 128); + cr.assign((size_t)width2 * height2, 128); + + // Luma: four buffers, keyed by (left/right half of the band, even/odd row). + for (int b = 0; b < 4; b++) { + if (!y[b]) { + continue; + } + const int xOffset = (b & 1) ? 16 : 0; + const int yStart = (b >> 1) ? 1 : 0; + int j = 0; + for (int bandX = xOffset; bandX < width; bandX += 32) { + const int run = std::min(16, width - bandX); + for (int row = yStart; row < height; row += 2, j += 16) { + if (run <= 0 || j + run > ySize[b]) { + continue; + } + memcpy(&luma[(size_t)row * width + bandX], y[b] + j, run); + } + } + } + + // Chroma: same shape in half-resolution coordinates, with interleaved Cb/Cr pairs. + for (int b = 0; b < 4; b++) { + if (!c[b]) { + continue; + } + const int xOffset = (b >> 1) ? 8 : 0; + const int yStart = (b & 1) ? 1 : 0; + int j = 0; + for (int bandX = xOffset; bandX < width2; bandX += 16) { + for (int row = yStart; row < height2; row += 2) { + for (int k = 0; k < 8; k++, j += 2) { + const int x = bandX + k; + if (x >= width2 || j + 1 >= cSize[b]) { + continue; + } + const size_t i = (size_t)row * width2 + x; + cb[i] = c[b][j]; + cr[i] = c[b][j + 1]; + } + } + } + } + return true; +} + +static u32 YCbCrToPixel(int y, int cbv, int crv, int pixelMode) { + const int c = y - 16, d = cbv - 128, e = crv - 128; + int r = (298 * c + 409 * e + 128) >> 8; + int g = (298 * c - 100 * d - 208 * e + 128) >> 8; + int b = (298 * c + 516 * d + 128) >> 8; + r = std::min(255, std::max(0, r)); + g = std::min(255, std::max(0, g)); + b = std::min(255, std::max(0, b)); + switch (pixelMode) { + case GE_CMODE_16BIT_BGR5650: + return ((b >> 3) << 11) | ((g >> 2) << 5) | (r >> 3); + case GE_CMODE_16BIT_ABGR5551: + return (1 << 15) | ((b >> 3) << 10) | ((g >> 3) << 5) | (r >> 3); + case GE_CMODE_16BIT_ABGR4444: + return (0xF << 12) | ((b >> 4) << 8) | ((g >> 4) << 4) | (r >> 4); + default: + return 0xFF000000 | (b << 16) | (g << 8) | r; + } +} + +// The shared body of sceMpegBaseCscAvc and sceMpegBaseCscAvcRange - the former is just the +// latter over the whole frame. +static int MpegBaseCscRange(u32 bufferRGB, u32 cscAddr, int bufferWidth, + int rangeX, int rangeY, int rangeWidth, int rangeHeight) { + auto csc = PSPPointer::Create(cscAddr); + if (!csc.IsValid()) { + return hleLogError(Log::Mpeg, -1, "bad csc struct pointer"); + } + if (bufferWidth == 0) { + bufferWidth = g_mpegBaseBufferWidth; + } + + const int width = csc->width << 4; + const int height = csc->height << 4; + if (width <= 0 || height <= 0 || width > 1024 || height > 1024) { + return hleLogError(Log::Mpeg, -1, "unreasonable frame size %dx%d", width, height); + } + if (rangeWidth <= 0 || rangeHeight <= 0) { + return hleLogDebug(Log::Mpeg, 0, "empty range"); + } + rangeWidth = std::min(rangeWidth, width - rangeX); + rangeHeight = std::min(rangeHeight, height - rangeY); + // The output buffer is sized from the stride below, so a row can't be wider than one. Every + // game seen so far passes a stride comfortably wider than the frame (512 for 480), but nothing + // guarantees it, and writing a wider row than we measured would run off the end of the buffer. + rangeWidth = std::min(rangeWidth, bufferWidth); + if (rangeX < 0 || rangeY < 0 || rangeWidth <= 0 || rangeHeight <= 0) { + return hleLogError(Log::Mpeg, -1, "range outside the frame"); + } + + // The descriptor only carries the four luma buffers. Recover the full set from the allocation + // sceVideocodec handed out - both ends of that are ours. + u32 buffers[8]{}; + for (int i = 0; i < 4; i++) { + buffers[i] = csc->buffer[i]; + } + VideocodecGetFrameBuffers(csc->buffer[0], buffers); + + std::vector luma, cb, cr; + if (!ReadTiledYCbCr(buffers, width, height, luma, cb, cr)) { + return hleLogError(Log::Mpeg, -1, "YCbCr buffers not readable"); + } + + const int bpp = g_mpegBasePixelMode == GE_CMODE_32BIT_ABGR8888 ? 4 : 2; + const u32 destSize = (u32)(rangeHeight * bufferWidth * bpp); + if (!Memory::IsValidRange(bufferRGB, destSize)) { + return hleLogError(Log::Mpeg, -1, "output buffer not writable"); + } + u8 *dest = Memory::GetTypedPointerWriteRange(bufferRGB, destSize); + if (!dest) { + return hleLogError(Log::Mpeg, -1, "output buffer not writable"); + } + + const int width2 = width >> 1; + for (int y = 0; y < rangeHeight; y++) { + const int sy = rangeY + y; + for (int x = 0; x < rangeWidth; x++) { + const int sx = rangeX + x; + const int ci = (sy >> 1) * width2 + (sx >> 1); + const u32 pixel = YCbCrToPixel(luma[sy * width + sx], cb[ci], cr[ci], g_mpegBasePixelMode); + if (bpp == 4) { + memcpy(dest + (y * bufferWidth + x) * 4, &pixel, 4); + } else { + const u16 p16 = (u16)pixel; + memcpy(dest + (y * bufferWidth + x) * 2, &p16, 2); + } + } + } + NotifyMemInfo(MemBlockFlags::WRITE, bufferRGB, destSize, "MpegBaseCsc"); + return hleLogDebug(Log::Mpeg, 0, "%dx%d at %d,%d -> %08x stride %d", + rangeWidth, rangeHeight, rangeX, rangeY, bufferRGB, bufferWidth); +} + +static int sceMpegBaseCscInit(int bufferWidth) { + g_mpegBaseBufferWidth = bufferWidth ? bufferWidth : 512; + return hleLogInfo(Log::Mpeg, 0, "bufferWidth %d", bufferWidth); +} + +// Sets the output pixel format for the conversions below. The official name isn't known - this one +// says what it does. Inside mpegbase.prx it stores the value and hands it to the DMACPLUS colour +// conversion hardware, and mpeg.prx gets it by indexing a table of {1, 2, 3, 0} with the pixel mode +// the game passed to sceMpegAvcDecodeMode. So the numbering is its own, not the GE's. +static int sceMpegBaseCscSetPixelMode(int pixelMode) { + static const int toGeMode[4] = { + GE_CMODE_32BIT_ABGR8888, + GE_CMODE_16BIT_BGR5650, + GE_CMODE_16BIT_ABGR5551, + GE_CMODE_16BIT_ABGR4444, + }; + if (pixelMode < 0 || pixelMode >= (int)ARRAY_SIZE(toGeMode)) { + return hleLogError(Log::Mpeg, -1, "bad pixel mode %d", pixelMode); + } + g_mpegBasePixelMode = toGeMode[pixelMode]; + return hleLogInfo(Log::Mpeg, 0, "pixel mode %d -> GE mode %d", pixelMode, g_mpegBasePixelMode); +} + +static int sceMpegBaseCscAvc(u32 bufferRGB, u32 unknown, int bufferWidth, u32 cscAddr) { + auto csc = PSPPointer::Create(cscAddr); + if (!csc.IsValid()) { + return hleLogError(Log::Mpeg, -1, "bad csc struct pointer"); + } + return MpegBaseCscRange(bufferRGB, cscAddr, bufferWidth, 0, 0, csc->width << 4, csc->height << 4); +} + +static u32 sceMpegBaseCscAvcRange(u32 bufferRGB, u32 unknown, u32 rangeAddr, int bufferWidth, u32 cscAddr) { + if (!Memory::IsValidRange(rangeAddr, 16)) { + return hleLogError(Log::Mpeg, -1, "bad range pointer"); + } + // Also in macroblocks. + const int rangeX = Memory::ReadUnchecked_U32(rangeAddr + 0) << 4; + const int rangeY = Memory::ReadUnchecked_U32(rangeAddr + 4) << 4; + const int rangeWidth = Memory::ReadUnchecked_U32(rangeAddr + 8) << 4; + const int rangeHeight = Memory::ReadUnchecked_U32(rangeAddr + 12) << 4; + return MpegBaseCscRange(bufferRGB, cscAddr, bufferWidth, rangeX, rangeY, rangeWidth, rangeHeight); +} + +// Copies one 48-byte YCrCb descriptor over another. The hardware copies the buffers themselves +// when told to; games seen so far only use it to move the descriptor around, so that is all we +// do until something needs more. +static int sceMpegBaseYCrCbCopy(u32 dstAddr, u32 srcAddr, int flags) { + if (!Memory::IsValidRange(dstAddr, sizeof(SceMp4AvcCscStruct)) || + !Memory::IsValidRange(srcAddr, sizeof(SceMp4AvcCscStruct))) { + return hleLogError(Log::Mpeg, -1, "bad descriptor pointer"); + } + Memory::Memcpy(dstAddr, srcAddr, (u32)sizeof(SceMp4AvcCscStruct), "MpegBaseYCrCbCopy"); + return hleLogDebug(Log::Mpeg, 0, "flags %08x - descriptor copied, buffers left alone", flags); +} + +const HLEFunction sceMpegbase[] = +{ + {0XBEA18F91, &WrapU_U, "sceMpegBasePESpacketCopy", 'x', "x" }, + {0X492B5E4B, &WrapI_I, "sceMpegBaseCscInit", 'i', "i" }, + {0X0530BE4E, &WrapI_I, "sceMpegbase_0530BE4E", 'i', "i" }, + {0X91929A21, &WrapI_UUIU, "sceMpegBaseCscAvc", 'i', "xxix" }, + {0X304882E1, &WrapU_UUUIU, "sceMpegBaseCscAvcRange", 'x', "xxxix" }, + {0X7AC0321A, &WrapI_UUI, "sceMpegBaseYCrCbCopy", 'i', "xxi" } +}; + +void Register_sceMpegbase() +{ + RegisterHLEModule("sceMpegbase", ARRAY_SIZE(sceMpegbase), sceMpegbase); +}; diff --git a/Core/HLE/sceMpegbase.h b/Core/HLE/sceMpegbase.h new file mode 100644 index 0000000000..6bf855d464 --- /dev/null +++ b/Core/HLE/sceMpegbase.h @@ -0,0 +1,37 @@ +// 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/. + +#pragma once + +#include + +#include "Common/CommonTypes.h" + +class PointerWrap; + +void Register_sceMpegbase(); + +// Called per boot, from __MpegInit. +void __MpegBaseInit(); + +void __MpegBaseDoState(PointerWrap &p); + +// Takes the PES payload sceMpegBasePESpacketCopy gathered for a given destination. On hardware that +// copy lands in Media Engine memory, which sceVideocodec would then read back; we keep it here +// instead. The payload is moved out and dropped from the table, so each one is decoded once. +// Empty if nothing was copied to that address. +std::vector MpegBaseTakePESPacket(u32 dest); diff --git a/Core/HLE/sceUtility.cpp b/Core/HLE/sceUtility.cpp index 3f16d40a3d..21be540051 100644 --- a/Core/HLE/sceUtility.cpp +++ b/Core/HLE/sceUtility.cpp @@ -137,11 +137,23 @@ static void LoadFirmwareModules(const char *library, const FirmwareModule *modul } } -// libmp3.prx imports nothing but the kernel and sceAudiocodec, which we have. -static void NotifyLoadStatusMp3(int state, u32 loadAddr, u32 totalSize) { +// mpeg.prx needs sceVideocodec, sceMpegbase and sceAudiocodec from us, all of which we implement, +// so the module itself is the only thing that has to come from somewhere real. +static void NotifyLoadStatusMpegBase(int state, u32 loadAddr, u32 totalSize) { // The effective flags, not the raw setting: those also account for the compat flags, for a // firmware dump that isn't there, and for the boundary a savestate restored - resolving // imports one way and loading modules the other is how a game ends up with neither. + if (state != 1 || !(GetEffectiveDisableHLEFlags() & DisableHLEFlags::sceMpeg)) { + return; + } + static const FirmwareModule modules[] = { + { "flash0:/kd/mpeg.prx", "sceMpeg_library" }, + }; + LoadFirmwareModules("sceMpeg", modules, ARRAY_SIZE(modules)); +} + +// libmp3.prx imports nothing but the kernel and sceAudiocodec, which we have. +static void NotifyLoadStatusMp3(int state, u32 loadAddr, u32 totalSize) { if (state != 1 || !(GetEffectiveDisableHLEFlags() & DisableHLEFlags::sceMp3)) { return; } @@ -203,6 +215,7 @@ static void NotifyLoadStatusAtrac(int state, u32 loadAddr, u32 totalSize) { static DisableHLEFlags UtilityModuleLibraryFlag(u32 module) { switch (module) { case 0x302: return DisableHLEFlags::sceAtrac; // av_atrac3plus + case 0x303: return DisableHLEFlags::sceMpeg; // av_mpegbase case 0x304: return DisableHLEFlags::sceMp3; // av_mp3 case 0x308: return DisableHLEFlags::sceMp4; // av_mp4 default: return (DisableHLEFlags)0; @@ -235,7 +248,7 @@ static const ModuleLoadInfo moduleLoadInfo[] = { // The size varies a bit per version, from about 0x3C00 to 0x4500 bytes. We could make a lookup table... // Changing this breaks some bad cheats though.. ModuleLoadInfo(0x302, 0x00008000, "av_atrac3plus", atrac3PlusModuleDeps, &NotifyLoadStatusAtrac), - ModuleLoadInfo(0x303, 0x0000c000, "av_mpegbase", mpegBaseModuleDeps), + ModuleLoadInfo(0x303, 0x0000c000, "av_mpegbase", mpegBaseModuleDeps, &NotifyLoadStatusMpegBase), ModuleLoadInfo(0x304, 0x00004000, "av_mp3", &NotifyLoadStatusMp3), ModuleLoadInfo(0x305, 0x0000a300, "av_vaudio"), ModuleLoadInfo(0x306, 0x00004000, "av_aac"), diff --git a/Core/HLE/sceVideocodec.cpp b/Core/HLE/sceVideocodec.cpp new file mode 100644 index 0000000000..877b8c1635 --- /dev/null +++ b/Core/HLE/sceVideocodec.cpp @@ -0,0 +1,654 @@ +// 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/. + +// sceVideocodec - the H.264 decoding interface the Media Engine exposes. +// +// This exists so that flash0:/kd/mpeg.prx can be run in place of our sceMpeg HLE: mpeg.prx needs +// only sceVideocodec, sceMpegbase and sceAudiocodec from us, and the other two we already have. +// The point is to have a reference to compare the HLE against, so it aims to behave like the +// hardware rather than to be the fastest way to get pixels on screen. +// +// Behaviour cross-checked against JPCSP, whose description of the buffer layout was established +// by looking at sceMpegBaseYCrCbCopy output on a real PSP. + +#include +#include +#include + +#include "Common/Serialize/Serializer.h" +#include "Common/Serialize/SerializeFuncs.h" +#include "Core/HLE/ErrorCodes.h" +#include "Core/HLE/HLE.h" +#include "Core/HLE/FunctionWrappers.h" +#include "Core/HLE/sceVideocodec.h" +#include "Core/Util/BlockAllocator.h" +#include "Core/HLE/sceMpeg.h" +#include "Core/HLE/sceMpegbase.h" +#include "Core/HW/AvcDecoder.h" +#include "Core/MemMap.h" +#include "Core/MIPS/MIPS.h" + +// The context the caller hands us is 96 bytes. The offsets below are what mpeg.prx actually +// reads and writes; anything not listed here it doesn't look at. +enum { + CTX_MAGIC = 0, // 0x05100601, same marker sceAudiocodec's context carries + CTX_VERSION = 4, // GetVersion writes 0x78 here + CTX_STATUS = 8, + CTX_MEM = 12, + CTX_OUT_INFO = 16, // pointer to the 108-byte result descriptor + CTX_EDRAM = 20, + CTX_EDRAM_SIZE = 24, + CTX_AU_DATA = 36, // the access unit to decode + CTX_AU_SIZE = 40, + CTX_YUV_STRUCT = 44, // type 0: pointer to the eight output buffers + CTX_EDRAM_RAW = 92, +}; + +// Fields of the descriptor at CTX_OUT_INFO that mpeg.prx reads back. +enum { + OUT_DATA = 0, + OUT_SIZE = 4, + OUT_UNK8 = 8, + OUT_UNK12 = 12, + OUT_CONSUMED = 44, + OUT_WIDTH = 48, + OUT_HEIGHT = 52, + OUT_FRAME_READY = 60, // 2 when a frame came out, 1 when it didn't + OUT_UNK64 = 64, + OUT_UNK72 = 72, + OUT_TIMESTAMP = 76, + OUT_FPS = 80, + OUT_BUFFER_Y = 84, + OUT_BUFFER_CR = 88, + OUT_BUFFER_CB = 92, + OUT_WIDTH_Y = 96, + OUT_WIDTH_CR = 100, + OUT_WIDTH_CB = 104, +}; + +// A game can have more than one of these open at once - Silent Hill Origins runs two, one that +// owns the EDRAM and one that does the decoding - so everything here is per context. +struct VideocodecCtx { + AvcDecoder *decoder = nullptr; + int type = 0; + int frameCount = 0; + // What sceVideocodecGetEDRAM handed out, as an address in g_meRam. + u32 edram = 0; + // The frame buffers the ME reported back, also in g_meRam - see PublishFrameBuffers. + u32 frameBuffers = 0; + u32 frameBuffersSize = 0; + int frameBufferWidth = 0; + int frameBufferHeight = 0; +}; + +static std::map g_videocodecCtxs; + +// The Media Engine's own 2MB of embedded DRAM, modelled as memory of ours. +// +// The main CPU cannot address it. mpeg.prx asks for a block with sceVideocodecGetEDRAM, keeps the value +// and hands it back, and never dereferences it; the frame buffers the ME reports back live in here +// too, which is why sceVideocodecSetMemory is given a frame size rather than a buffer - 480, 272 +// and a count of 2 for a full-screen movie, with nowhere for the caller to say where to put them. +// +// The addresses handed out are offsets into g_meRam, based well outside anything PSP RAM maps so +// that a stray dereference faults where it happens instead of quietly reading the game's memory. +// Being outside PSP RAM, the contents aren't in the memory a savestate captures either, so the +// block and its allocator go in __VideocodecDoState. +static const u32 ME_EDRAM_BASE = 0xC0000000; +static const u32 ME_EDRAM_SIZE = 2 * 1024 * 1024; +static std::vector g_meRam; +static BlockAllocator g_meAlloc(64); + +// The 2MB is only committed once something asks for a piece of it, so a game that never plays a +// video pays nothing for this and its savestates don't carry it. +static void MEEnsureRam() { + if (g_meRam.size() != ME_EDRAM_SIZE) { + g_meRam.assign(ME_EDRAM_SIZE, 0); + g_meAlloc.Init(ME_EDRAM_BASE, ME_EDRAM_SIZE, false); + } +} + +u8 *VideocodecMEPointer(u32 addr, u32 size) { + if (addr < ME_EDRAM_BASE || size > ME_EDRAM_SIZE || g_meRam.size() != ME_EDRAM_SIZE) { + return nullptr; + } + const u32 offset = addr - ME_EDRAM_BASE; + if (offset > ME_EDRAM_SIZE - size) { + return nullptr; + } + return g_meRam.data() + offset; +} + +static void FreeContext(VideocodecCtx &ctx) { + delete ctx.decoder; + ctx.decoder = nullptr; + if (ctx.edram) { + g_meAlloc.Free(ctx.edram); + ctx.edram = 0; + } + if (ctx.frameBuffers) { + g_meAlloc.Free(ctx.frameBuffers); + ctx.frameBuffers = 0; + } +} + +// freeMemory is false when loading a savestate: the state carries its own g_meAlloc, so the blocks +// these contexts were holding belong to a world that no longer exists and freeing them would be +// freeing someone else's memory. +static void ClearContexts(bool freeMemory) { + for (auto &[addr, ctx] : g_videocodecCtxs) { + if (freeMemory) { + FreeContext(ctx); + } else { + delete ctx.decoder; + ctx.decoder = nullptr; + } + } + g_videocodecCtxs.clear(); +} + +void __VideocodecInit() { + // Nothing to free: a boot starts with a fresh allocator. + g_videocodecCtxs.clear(); + g_meRam.clear(); + g_meRam.shrink_to_fit(); + g_meAlloc.Shutdown(); +} + +void __VideocodecShutdown() { + ClearContexts(true); +} + +void __VideocodecDoState(PointerWrap &p) { + auto s = p.Section("sceVideocodec", 0, 1); + if (!s) { + return; + } + + // The decoders themselves aren't serializable - a savestate resumes with fresh ones, which + // costs at most the frames up to the next keyframe. The frame buffer allocations do have to + // come back, or we'd lose track of memory the restored allocator still has handed out. + // + // If we in the future directly integrate with a h.264 decoder, it might be actually possible + // to serialize the internal states. But 100% accurate savestates during cutscene playback are + // not really that important. + int count = (int)g_videocodecCtxs.size(); + Do(p, count); + if (p.mode == p.MODE_READ) { + ClearContexts(false); + for (int i = 0; i < count; i++) { + u32 addr = 0; + VideocodecCtx ctx; + Do(p, addr); + Do(p, ctx.type); + Do(p, ctx.frameCount); + Do(p, ctx.frameBuffers); + Do(p, ctx.frameBuffersSize); + Do(p, ctx.frameBufferWidth); + Do(p, ctx.frameBufferHeight); + Do(p, ctx.edram); + g_videocodecCtxs[addr] = std::move(ctx); + } + } else { + for (auto &[addr, ctx] : g_videocodecCtxs) { + u32 a = addr; + Do(p, a); + Do(p, ctx.type); + Do(p, ctx.frameCount); + Do(p, ctx.frameBuffers); + Do(p, ctx.frameBuffersSize); + Do(p, ctx.frameBufferWidth); + Do(p, ctx.frameBufferHeight); + Do(p, ctx.edram); + } + } + + // The Media Engine's memory and who holds what of it. Empty until a video plays, and then it + // is the one copy - the contexts above only carry addresses into it. + Do(p, g_meRam); + g_meAlloc.DoState(p); +} + +u32 VideocodecFrameBufferLayout(int width, int height, int sizes[8], u32 offsets[8]) { + // buffer0/2 take the odd band out when the width isn't a multiple of 32. + const int lumaLeft = ((width + 16) >> 5) * (height >> 1) * 16; + const int lumaRight = (width >> 5) * (height >> 1) * 16; + const int local[8] = { + lumaLeft, lumaRight, lumaLeft, lumaRight, + lumaLeft >> 1, lumaLeft >> 1, lumaRight >> 1, lumaRight >> 1, + }; + u32 total = 0; + for (int i = 0; i < 8; i++) { + if (sizes) { + sizes[i] = local[i]; + } + if (offsets) { + offsets[i] = total; + } + total += (local[i] + 63) & ~63; + } + return total; +} + +// The descriptor mpeg.prx passes in is empty: on hardware the ME owns the frame buffers, and +// reports where it put them. So allocate them here and fill the descriptor in the shape +// sceMpegBaseCscAvc expects - dimensions in macroblocks, then the eight buffer addresses. +static bool PublishFrameBuffers(VideocodecCtx &vctx, u32 structAddr, int width, int height, u32 buffers[8]) { + u32 offsets[8]; + const u32 total = VideocodecFrameBufferLayout(width, height, nullptr, offsets); + if (total == 0) { + return false; + } + + if (vctx.frameBuffers && (width != vctx.frameBufferWidth || height != vctx.frameBufferHeight)) { + g_meAlloc.Free(vctx.frameBuffers); + vctx.frameBuffers = 0; + } + if (!vctx.frameBuffers) { + MEEnsureRam(); + u32 size = total; + vctx.frameBuffers = g_meAlloc.Alloc(size, false, "VideocodecFrame"); + if (vctx.frameBuffers == (u32)-1) { + vctx.frameBuffers = 0; + ERROR_LOG(Log::ME, "sceVideocodec: no room in ME memory for %d bytes of frame buffers", total); + return false; + } + vctx.frameBuffersSize = total; + vctx.frameBufferWidth = width; + vctx.frameBufferHeight = height; + INFO_LOG(Log::ME, "sceVideocodec: %d bytes of frame buffers at %08x for %dx%d", + total, vctx.frameBuffers, width, height); + } + + for (int i = 0; i < 8; i++) { + buffers[i] = vctx.frameBuffers + offsets[i]; + } + + if (!Memory::IsValidRange(structAddr, 48)) { + return false; + } + Memory::WriteUnchecked_U32(height >> 4, structAddr + 0); // macroblocks + Memory::WriteUnchecked_U32(width >> 4, structAddr + 4); + for (int i = 0; i < 8; i++) { + Memory::WriteUnchecked_U32(buffers[i], structAddr + 16 + i * 4); + } + return true; +} + +void VideocodecGetCtxInfo(std::vector *infos) { + infos->clear(); + for (const auto &[addr, ctx] : g_videocodecCtxs) { + VideocodecCtxInfo info; + info.ctxAddr = addr; + info.type = ctx.type; + info.hasDecoder = ctx.decoder != nullptr; + info.frameCount = ctx.frameCount; + // Hardware keeps the token in the context struct, so that's where we read it back from too. + info.edramToken = ctx.edram; + info.edramSize = ctx.edram ? g_meAlloc.GetBlockSizeFromAddress(ctx.edram) : 0; + info.frameBuffers = ctx.frameBuffers; + info.frameBuffersSize = ctx.frameBuffersSize; + info.width = ctx.frameBufferWidth; + info.height = ctx.frameBufferHeight; + infos->push_back(info); + } +} + +bool VideocodecGetFrameBuffers(u32 firstBuffer, u32 buffers[8]) { + // sceMpegbase only has the first of the eight addresses, so find whose allocation it is. + const VideocodecCtx *found = nullptr; + for (const auto &[addr, ctx] : g_videocodecCtxs) { + if (ctx.frameBuffers && ctx.frameBuffers == firstBuffer) { + found = &ctx; + break; + } + } + if (!found) { + return false; + } + u32 offsets[8]; + VideocodecFrameBufferLayout(found->frameBufferWidth, found->frameBufferHeight, nullptr, offsets); + for (int i = 0; i < 8; i++) { + buffers[i] = found->frameBuffers + offsets[i]; + } + return true; +} + +// Writes the decoded frame into the eight buffers the hardware uses. The image is in 32-pixel +// vertical bands split into two 16-pixel halves, and which buffer a row lands in depends on +// whether it is even or odd. This is the exact inverse of ReadTiledYCbCr in sceMpeg.cpp, which +// is what reads it back out - see the comment there for the full layout. +static void WriteTiledYCbCr(const u32 *buffers, const AvcDecoder &dec, int width, int height) { + const int width2 = width >> 1; + const int height2 = height >> 1; + + const u8 *srcY = dec.Plane(0); + const u8 *srcCb = dec.Plane(1); + const u8 *srcCr = dec.Plane(2); + const int strideY = dec.Stride(0); + const int strideCb = dec.Stride(1); + const int strideCr = dec.Stride(2); + if (!srcY || !srcCb || !srcCr) { + return; + } + + int sizes[8]; + VideocodecFrameBufferLayout(width, height, sizes, nullptr); + const int *ySize = sizes; + const int *cSize = sizes + 4; + + for (int b = 0; b < 4; b++) { + if (ySize[b] <= 0) { + continue; + } + u8 *dst = VideocodecMEPointer(buffers[b], ySize[b]); + if (!dst) { + continue; + } + const int xOffset = (b & 1) ? 16 : 0; + const int yStart = (b >> 1) ? 1 : 0; + int j = 0; + for (int bandX = xOffset; bandX < width; bandX += 32) { + const int run = std::min(16, width - bandX); + for (int row = yStart; row < height; row += 2, j += 16) { + if (run <= 0 || j + run > ySize[b]) { + continue; + } + memcpy(dst + j, srcY + (size_t)row * strideY + bandX, run); + } + } + } + + for (int b = 0; b < 4; b++) { + if (cSize[b] <= 0) { + continue; + } + u8 *dst = VideocodecMEPointer(buffers[4 + b], cSize[b]); + if (!dst) { + continue; + } + const int xOffset = (b >> 1) ? 8 : 0; + const int yStart = (b & 1) ? 1 : 0; + int j = 0; + for (int bandX = xOffset; bandX < width2; bandX += 16) { + for (int row = yStart; row < height2; row += 2) { + for (int k = 0; k < 8; k++, j += 2) { + const int x = bandX + k; + if (x >= width2 || j + 1 >= cSize[b]) { + continue; + } + dst[j] = srcCb[(size_t)row * strideCb + x]; + dst[j + 1] = srcCr[(size_t)row * strideCr + x]; + } + } + } + } +} + +static int sceVideocodecOpen(u32 ctxAddr, int type) { + if (!Memory::IsValidRange(ctxAddr, 96)) { + return hleLogError(Log::ME, -1, "bad context pointer"); + } + Memory::WriteUnchecked_U32(0x05100601, ctxAddr + CTX_MAGIC); + if (!AvcDecoder::IsAvailable()) { + 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); +} + +static int sceVideocodecInit(u32 ctxAddr, int type) { + if (!Memory::IsValidRange(ctxAddr, 96)) { + return hleLogError(Log::ME, -1, "bad context pointer"); + } + Memory::WriteUnchecked_U32(Memory::ReadUnchecked_U32(ctxAddr + CTX_EDRAM) + 8, ctxAddr + CTX_MEM); + VideocodecCtx &vctx = g_videocodecCtxs[ctxAddr]; + delete vctx.decoder; + vctx.decoder = new AvcDecoder(); + vctx.frameCount = 0; + vctx.type = type; + return hleLogInfo(Log::ME, 0, "type %d", type); +} + +// See g_meRam for why this doesn't come out of the game's memory. +static int sceVideocodecGetEDRAM(u32 ctxAddr, int type) { + if (!Memory::IsValidRange(ctxAddr, 96)) { + return hleLogError(Log::ME, -1, "bad context pointer"); + } + // The firmware refuses rather than replacing one it already handed out, and a game that asks + // twice would otherwise leave the first block with nothing pointing at it. + if (Memory::ReadUnchecked_U32(ctxAddr + CTX_EDRAM_RAW) != 0) { + return hleLogError(Log::ME, SCE_MPEG_ERROR_AVC_INVALID_VALUE, "context already has EDRAM"); + } + // Rounding as the firmware does it - the OR really is an OR, so every size ends in 0x3F. + u32 size = (Memory::ReadUnchecked_U32(ctxAddr + CTX_EDRAM_SIZE) + 63) | 0x3F; + MEEnsureRam(); + const u32 addr = g_meAlloc.Alloc(size, false, "VideocodecEDRAM"); + if (addr == (u32)-1) { + return hleLogError(Log::ME, SCE_MPEG_ERROR_AVC_INVALID_VALUE, "no room in ME memory for %u bytes", size); + } + g_videocodecCtxs[ctxAddr].edram = addr; + // Both fields as hardware fills them: the raw value and the 64-byte-aligned one. The allocator + // 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); +} + +static int sceVideocodecReleaseEDRAM(u32 ctxAddr) { + if (!Memory::IsValidRange(ctxAddr, 96)) { + return hleLogError(Log::ME, -1, "bad context pointer"); + } + // Whether this context has one is recorded in the context struct, as on hardware. + const u32 token = Memory::ReadUnchecked_U32(ctxAddr + CTX_EDRAM_RAW); + if (!token) { + return hleLogError(Log::ME, SCE_MPEG_ERROR_AVC_INVALID_VALUE, "context has no EDRAM"); + } + auto it = g_videocodecCtxs.find(ctxAddr); + if (it != g_videocodecCtxs.end() && it->second.edram) { + g_meAlloc.Free(it->second.edram); + it->second.edram = 0; + } + Memory::WriteUnchecked_U32(0, ctxAddr + CTX_EDRAM); + Memory::WriteUnchecked_U32(0, ctxAddr + CTX_EDRAM_RAW); + return hleLogInfo(Log::ME, 0, "released %08x", token); +} + +static int sceVideocodecDecode(u32 ctxAddr, int type) { + if (!Memory::IsValidRange(ctxAddr, 96)) { + return hleLogError(Log::ME, -1, "bad context pointer"); + } + if (type != 0 && type != 1) { + return hleLogError(Log::ME, -1, "unknown type %d", type); + } + // Only Open and Init create contexts. Keying off whatever address Decode is handed would let + // a game that never opens one accumulate decoders that nothing ever deletes. + auto ctxIter = g_videocodecCtxs.find(ctxAddr); + if (ctxIter == g_videocodecCtxs.end()) { + return hleLogError(Log::ME, -1, "decode on a context that was never opened"); + } + VideocodecCtx &vctx = ctxIter->second; + if (!vctx.decoder) { + vctx.decoder = new AvcDecoder(); + } + + const u32 auAddr = Memory::ReadUnchecked_U32(ctxAddr + CTX_AU_DATA); + const int auSize = (int)Memory::ReadUnchecked_U32(ctxAddr + CTX_AU_SIZE); + const u32 outAddr = Memory::ReadUnchecked_U32(ctxAddr + CTX_OUT_INFO); + Memory::WriteUnchecked_U32(0, ctxAddr + CTX_STATUS); + + if (!Memory::IsValidRange(outAddr, 108)) { + return hleLogError(Log::ME, -1, "bad output descriptor"); + } + + // The access unit address mpeg.prx passes is in Media Engine space, which we can't read - + // on hardware sceMpegBasePESpacketCopy DMA'd the data there. That copy is ours, so use what + // it gathered instead, and fall back to main memory for any caller that points at it + // directly. + bool gotFrame = false; + const u8 *au = nullptr; + int auBytes = 0; + // Owns the gathered payload for as long as au points into it. + std::vector pes; + if (auSize > 0 && Memory::IsValidRange(auAddr, auSize)) { + au = Memory::GetTypedPointerRange(auAddr, auSize); + auBytes = auSize; + } else { + // Take the payload copied to this exact address - the same call carries audio too. + pes = MpegBaseTakePESPacket(auAddr); + if (!pes.empty()) { + au = pes.data(); + auBytes = (int)pes.size(); + } + } + if (au && auBytes > 0) { + gotFrame = vctx.decoder->Decode(au, auBytes); + } + + const int width = gotFrame ? vctx.decoder->Width() : 0; + const int height = gotFrame ? vctx.decoder->Height() : 0; + + auto out32 = [outAddr](int offset, u32 value) { + Memory::WriteUnchecked_U32(value, outAddr + offset); + }; + + // Only the type 1 path fills the descriptor in. For type 0 the YCbCr descriptor sits just + // 0x40 bytes after this one - mpeg.prx allocates them adjacently - so writing the type 1 + // fields here scribbles over the buffer addresses the colour conversion is about to read. + // For type 0 the frame isn't announced until the buffers holding it have been published - + // see below. Saying "one image decoded" and then failing to allocate would have mpeg.prx + // convert from whatever the descriptor pointed at last. + bool published = false; + if (type == 0) { + out32(8, width); + out32(12, height); + out32(28, 1); + } else { + out32(OUT_DATA, auAddr); + out32(OUT_SIZE, auSize); + out32(OUT_UNK12, 0x40); + out32(OUT_CONSUMED, auSize); + out32(OUT_WIDTH, width); + out32(OUT_HEIGHT, height); + out32(OUT_FRAME_READY, gotFrame ? 2 : 1); + out32(OUT_UNK64, 1); + out32(OUT_UNK72, (u32)-1); + out32(OUT_TIMESTAMP, vctx.frameCount * 0x64); + out32(OUT_FPS, 2997); + } + + if (gotFrame) { + vctx.frameCount++; + if (type == 0) { + const u32 yuvStructAddr = Memory::ReadUnchecked_U32(ctxAddr + CTX_YUV_STRUCT); + if (Memory::IsValidRange(yuvStructAddr, 8 * 4)) { + u32 buffers[8]; + if (PublishFrameBuffers(vctx, yuvStructAddr, width, height, buffers)) { + WriteTiledYCbCr(buffers, *vctx.decoder, width, height); + published = true; + } + } else { + WARN_LOG(Log::ME, "sceVideocodecDecode: type 0 without a usable buffer list"); + } + } else { + // Type 1 hands back plain planar YUV, so just point at the decoder's own planes - + // nothing in the descriptor is read until the caller copies from them. + out32(OUT_WIDTH_Y, width); + out32(OUT_WIDTH_CR, width / 2); + out32(OUT_WIDTH_CB, width / 2); + } + } + + if (type == 0) { + out32(32, published ? 1 : 0); // images decoded - mpeg.prx won't convert without this + out32(36, published ? 0 : 1); + } + + return hleLogDebug(Log::ME, 0, "type %d, %d bytes -> %s %dx%d", + type, auBytes, gotFrame ? "frame" : "no frame yet", width, height); +} + +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 hleLogInfo(Log::ME, 0); +} + +static int sceVideocodecDelete(u32 ctxAddr, int type) { + auto it = g_videocodecCtxs.find(ctxAddr); + if (it != g_videocodecCtxs.end()) { + FreeContext(it->second); + g_videocodecCtxs.erase(it); + } + return hleLogInfo(Log::ME, 0); +} + +static int sceVideocodecGetVersion(u32 ctxAddr, int type) { + if (!Memory::IsValidRange(ctxAddr, 96)) { + return hleLogError(Log::ME, -1, "bad context pointer"); + } + // The value a real PSP returns, read with JpcspTrace. + Memory::WriteUnchecked_U32(0x78, ctxAddr + CTX_VERSION); + return hleLogInfo(Log::ME, 0); +} + +static int sceVideocodecGetSEI(u32 ctxAddr, int type) { + return hleLogWarning(Log::ME, 0, "UNIMPL"); +} + +static int sceVideocodecScanHeader(u32 ctxAddr, int type) { + return hleLogWarning(Log::ME, 0, "UNIMPL"); +} + +static int sceVideocodecGetFrameCrop(u32 ctxAddr, int type) { + return hleLogWarning(Log::ME, 0, "UNIMPL"); +} + +static int sceVideocodecSetMemory(u32 ctxAddr, int type) { + return hleLogDebug(Log::ME, 0); +} + +static int sceVideocodec_893B32B1(u32 ctxAddr, int type) { + return hleLogWarning(Log::ME, 0, "UNIMPL"); +} + +static int sceVideocodec_D95C24D5(u32 ctxAddr, int type) { + return hleLogWarning(Log::ME, 0, "UNIMPL"); +} + +const HLEFunction sceVideocodec[] = { + {0XC01EC829, &WrapI_UI, "sceVideocodecOpen", 'i', "xi"}, + {0X2D31F5B1, &WrapI_UI, "sceVideocodecGetEDRAM", 'i', "xi"}, + {0X17099F0A, &WrapI_UI, "sceVideocodecInit", 'i', "xi"}, + {0XDBA273FA, &WrapI_UI, "sceVideocodecDecode", 'i', "xi"}, + {0X4F160BF4, &WrapI_U, "sceVideocodecReleaseEDRAM", 'i', "x" }, + {0X745A7B7A, &WrapI_UI, "sceVideocodecSetMemory", 'i', "xi"}, + {0X2F385E7F, &WrapI_UI, "sceVideocodecScanHeader", 'i', "xi"}, + {0X307E6E1C, &WrapI_UI, "sceVideocodecDelete", 'i', "xi"}, + {0XA2F0564E, &WrapI_UI, "sceVideocodecStop", 'i', "xi"}, + {0X17CF7D2C, &WrapI_UI, "sceVideocodecGetFrameCrop", 'i', "xi"}, + {0X26927D19, &WrapI_UI, "sceVideocodecGetVersion", 'i', "xi"}, + {0X627B7D42, &WrapI_UI, "sceVideocodecGetSEI", 'i', "xi"}, + {0X893B32B1, &WrapI_UI, "sceVideocodec_893B32B1", 'i', "xi"}, + {0XD95C24D5, &WrapI_UI, "sceVideocodec_D95C24D5", 'i', "xi"}, +}; + +void Register_sceVideocodec() { + RegisterHLEModule("sceVideocodec", ARRAY_SIZE(sceVideocodec), sceVideocodec); +} diff --git a/Core/HLE/sceVideocodec.h b/Core/HLE/sceVideocodec.h new file mode 100644 index 0000000000..0bb19e3216 --- /dev/null +++ b/Core/HLE/sceVideocodec.h @@ -0,0 +1,65 @@ +// 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/. + +#pragma once + +#include + +#include "Common/CommonTypes.h" + +class PointerWrap; + +void __VideocodecInit(); +void __VideocodecShutdown(); +void __VideocodecDoState(PointerWrap &p); + +void Register_sceVideocodec(); + +// The state of each open decoder, for the debugger. A game can have several - Silent Hill Origins +// runs one context for the EDRAM and another for the decoding. +struct VideocodecCtxInfo { + u32 ctxAddr; + int type; + bool hasDecoder; + int frameCount; + // The token sceVideocodecGetEDRAM handed back, and how much it stands for. Not an address - + // the block is ours, not the game's, the way the Media Engine's memory is on hardware. + u32 edramToken; + u32 edramSize; + u32 frameBuffers; + u32 frameBuffersSize; + int width; + int height; +}; +void VideocodecGetCtxInfo(std::vector *infos); + +// A host pointer into the Media Engine's memory, or null if the range isn't in it. The frame +// buffers and the EDRAM block both live there, so anything reading them goes through this rather +// than through Memory:: - the ME's memory is not part of PSP RAM. +u8 *VideocodecMEPointer(u32 addr, u32 size); + +// mpeg.prx copies only the four luma buffers into the descriptor it hands sceMpegBaseCscAvc. +// Both ends of that are ours, so the conversion can recover the other four from the allocation +// they came from. Returns false if `firstBuffer` isn't one we handed out. +bool VideocodecGetFrameBuffers(u32 firstBuffer, u32 buffers[8]); + +// How the eight buffers a frame is delivered in are sized and laid out, in the order the +// descriptor lists them: four luma (left/right half of a 32-pixel band, even/odd rows) then four +// chroma. Everything that writes, reads or allocates them has to agree, so it lives in one place. +// `offsets` is each buffer's start within a single allocation, 64-byte aligned; either array may +// be null. Returns the total allocation size. +u32 VideocodecFrameBufferLayout(int width, int height, int sizes[8], u32 offsets[8]); diff --git a/Core/HW/AvcDecoder.cpp b/Core/HW/AvcDecoder.cpp new file mode 100644 index 0000000000..568adf750f --- /dev/null +++ b/Core/HW/AvcDecoder.cpp @@ -0,0 +1,194 @@ +// 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/. + +#include "ppsspp_config.h" + +#include "Common/Log.h" +#include "Core/HW/AvcDecoder.h" + +#ifdef USE_FFMPEG +extern "C" { +#include "libavcodec/avcodec.h" +#include "libavutil/imgutils.h" +} +#include "Core/FFMPEGCompat.h" +#endif // USE_FFMPEG + +AvcDecoder::AvcDecoder() { +#ifdef USE_FFMPEG + AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); + if (!codec) { + ERROR_LOG(Log::ME, "AvcDecoder: no H.264 decoder in this ffmpeg build"); + return; + } + codecCtx_ = avcodec_alloc_context3(codec); + if (!codecCtx_) { + ERROR_LOG(Log::ME, "AvcDecoder: couldn't allocate a codec context"); + return; + } + // The PSP hands us whole access units, so no parser is needed and no truncation is expected. + codecCtx_->flags = 0; + codecCtx_->flags2 = 0; + if (avcodec_open2(codecCtx_, codec, nullptr) < 0) { + ERROR_LOG(Log::ME, "AvcDecoder: couldn't open the H.264 decoder"); + avcodec_free_context(&codecCtx_); + codecCtx_ = nullptr; + return; + } + frame_ = av_frame_alloc(); + if (!frame_) { + avcodec_free_context(&codecCtx_); + codecCtx_ = nullptr; + } +#endif +} + +AvcDecoder::~AvcDecoder() { +#ifdef USE_FFMPEG + if (frame_) { + av_frame_free(&frame_); + } + if (codecCtx_) { + avcodec_free_context(&codecCtx_); + } +#endif +} + +bool AvcDecoder::IsAvailable() { +#ifdef USE_FFMPEG + return true; +#else + return false; +#endif +} + +void AvcDecoder::Flush() { + haveFrame_ = false; +#ifdef USE_FFMPEG + if (codecCtx_) { + avcodec_flush_buffers(codecCtx_); + } + if (frame_) { + av_frame_unref(frame_); + } +#endif +} + +bool AvcDecoder::Decode(const u8 *data, int size) { + haveFrame_ = false; +#ifdef USE_FFMPEG + if (!codecCtx_ || !frame_ || !data || size <= 0) { + return false; + } + + // ffmpeg reads a little past the end of a packet, so it gets a padded copy rather than the + // caller's buffer. A plain stack AVPacket, like the rest of the emulator uses - the heap + // packet API isn't in the ffmpeg headers every target builds against. + packetBuf_.resize((size_t)size + AV_INPUT_BUFFER_PADDING_SIZE); + memcpy(packetBuf_.data(), data, size); + memset(packetBuf_.data() + size, 0, AV_INPUT_BUFFER_PADDING_SIZE); + + AVPacket packet; + av_init_packet(&packet); + packet.data = packetBuf_.data(); + packet.size = size; + + av_frame_unref(frame_); + +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 48, 101) + int err = avcodec_send_packet(codecCtx_, &packet); + if (err == AVERROR(EAGAIN)) { + // The decoder has frames waiting and won't take more input until one is taken out. It did + // not consume the packet, so drain a frame and hand it the same packet again rather than + // dropping the access unit. + err = avcodec_receive_frame(codecCtx_, frame_); + if (err < 0) { + WARN_LOG(Log::ME, "AvcDecoder: send_packet wanted a frame out but none came (%d)", err); + return false; + } + const int resend = avcodec_send_packet(codecCtx_, &packet); + if (resend < 0) { + WARN_LOG(Log::ME, "AvcDecoder: send_packet failed on retry (%d)", resend); + return false; + } + // A frame is already in hand from the drain above - fall through to the format check. + } else { + if (err < 0) { + WARN_LOG(Log::ME, "AvcDecoder: send_packet failed (%d)", err); + return false; + } + + err = avcodec_receive_frame(codecCtx_, frame_); + if (err == AVERROR(EAGAIN)) { + // Normal: H.264 reorders, so the decoder wants more input before it gives a frame back. + return false; + } + if (err < 0) { + WARN_LOG(Log::ME, "AvcDecoder: receive_frame failed (%d)", err); + return false; + } + } +#else + int gotFrame = 0; + int err = avcodec_decode_video2(codecCtx_, frame_, &gotFrame, &packet); + if (err < 0) { + WARN_LOG(Log::ME, "AvcDecoder: decode_video2 failed (%d)", err); + return false; + } + if (!gotFrame) { + // Normal: H.264 reorders, so the decoder wants more input before it gives a frame back. + return false; + } +#endif + + // Everything downstream indexes the three planes as 8-bit 4:2:0, which is all the PSP's + // encoder produced. Anything else would be read as if it were, so say so and drop the frame + // rather than converting garbage. + if (frame_->format != AV_PIX_FMT_YUV420P && frame_->format != AV_PIX_FMT_YUVJ420P) { + WARN_LOG(Log::ME, "AvcDecoder: unexpected pixel format %d, ignoring the frame", frame_->format); + return false; + } + width_ = frame_->width; + height_ = frame_->height; + haveFrame_ = width_ > 0 && height_ > 0; + return haveFrame_; +#else + return false; +#endif +} + +const u8 *AvcDecoder::Plane(int index) const { +#ifdef USE_FFMPEG + if (!haveFrame_ || !frame_ || index < 0 || index >= 3) { + return nullptr; + } + return frame_->data[index]; +#else + return nullptr; +#endif +} + +int AvcDecoder::Stride(int index) const { +#ifdef USE_FFMPEG + if (!haveFrame_ || !frame_ || index < 0 || index >= 3) { + return 0; + } + return frame_->linesize[index]; +#else + return 0; +#endif +} diff --git a/Core/HW/AvcDecoder.h b/Core/HW/AvcDecoder.h new file mode 100644 index 0000000000..4b088a042e --- /dev/null +++ b/Core/HW/AvcDecoder.h @@ -0,0 +1,72 @@ +// 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/. + +#pragma once + +#include + +#include "Common/CommonTypes.h" + +struct AVCodecContext; +struct AVFrame; + +// A plain "hand it one access unit, get a frame back" H.264 decoder. +// +// This exists for sceVideocodec, which is a low level interface to the PSP Media Engine. +// PPSSPP's MediaEngine can't serve that - it is built around sceMpeg's own model, with the +// PSMF demuxer, the ringbuffer and its own frame pacing wrapped around the decoder, so this +// is a second path in addition to that. Implementing the old path around this might happen later. +// +// Output is whatever pixel format the decoder produced, normally YUV420P; the caller converts. +class AvcDecoder { +public: + AvcDecoder(); + ~AvcDecoder(); + + // Not copyable. + AvcDecoder(const AvcDecoder &) = delete; + AvcDecoder &operator=(const AvcDecoder &) = delete; + + // Decodes one access unit. Returns true when a frame came out - H.264 reorders, so a frame + // is not produced for every unit fed in, and that is not an error. + bool Decode(const u8 *data, int size); + + // Throws away any decoder state, for a seek or a new stream. + void Flush(); + + bool HasFrame() const { return haveFrame_; } + int Width() const { return width_; } + int Height() const { return height_; } + + // Only meaningful while HasFrame(). Planes are Y, Cb, Cr; strides are in bytes and are not + // necessarily the width. + const u8 *Plane(int index) const; + int Stride(int index) const; + + // True if we were built without ffmpeg, in which case nothing decodes. + static bool IsAvailable(); + +private: + AVCodecContext *codecCtx_ = nullptr; + AVFrame *frame_ = nullptr; + // The access unit, copied so we can pad it - ffmpeg reads a little past the end of a packet. + // Kept between calls so the usual case doesn't allocate. + std::vector packetBuf_; + bool haveFrame_ = false; + int width_ = 0; + int height_ = 0; +}; diff --git a/UI/ImDebugger/ImDebugger.cpp b/UI/ImDebugger/ImDebugger.cpp index d0e2850da7..634d521db1 100644 --- a/UI/ImDebugger/ImDebugger.cpp +++ b/UI/ImDebugger/ImDebugger.cpp @@ -47,6 +47,7 @@ #include "Core/HLE/sceAtrac.h" #include "Core/HLE/sceAudio.h" #include "Core/HLE/sceAudiocodec.h" +#include "Core/HLE/sceVideocodec.h" #include "Core/HLE/sceMp3.h" #include "Core/HLE/AtracCtx.h" #include "Core/HLE/sceSas.h" @@ -1577,6 +1578,39 @@ void DrawMediaDecodersView(ImConfig &cfg, ImControl &control) { } } + std::vector videoCtxs; + VideocodecGetCtxInfo(&videoCtxs); + if (ImGui::CollapsingHeaderWithCount("sceVideocodec", (int)videoCtxs.size(), ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::BeginTable("videocodecs", 7, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersH)) { + ImGui::TableSetupColumn("CtxAddr", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Frames", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Decoder", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("EDRAM", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Frame buffers", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableHeadersRow(); + for (const VideocodecCtxInfo &ctx : videoCtxs) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%08x", ctx.ctxAddr); + ImGui::TableNextColumn(); + ImGui::Text("H.264 (%d)", ctx.type); + ImGui::TableNextColumn(); + ImGui::Text("%dx%d", ctx.width, ctx.height); + ImGui::TableNextColumn(); + ImGui::Text("%d", ctx.frameCount); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(ctx.hasDecoder ? "open" : "-"); + ImGui::TableNextColumn(); + ImGui::Text("%08x (%d)", ctx.edramToken, ctx.edramSize); + ImGui::TableNextColumn(); + ImGui::Text("%08x (%d)", ctx.frameBuffers, ctx.frameBuffersSize); + } + ImGui::EndTable(); + } + } + ImGui::End(); } diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index b94187585b..6e74a6f739 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -219,7 +219,9 @@ + + @@ -257,6 +259,7 @@ + @@ -502,7 +505,9 @@ + + @@ -539,6 +544,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index 05b1873f8f..69ae74e6fe 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -127,7 +127,9 @@ + + @@ -164,6 +166,7 @@ + @@ -522,6 +525,9 @@ + + + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index d7c5912df0..63cff285ec 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -614,6 +614,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/HW/GpioMMIO.cpp \ $(SRC)/Core/HW/MemoryStick.cpp \ $(SRC)/Core/HW/MpegDemux.cpp.arm \ + $(SRC)/Core/HW/AvcDecoder.cpp.arm \ $(SRC)/Core/HW/MediaEngine.cpp.arm \ $(SRC)/Core/HW/SasAudio.cpp.arm \ $(SRC)/Core/HW/SasReverb.cpp.arm \ @@ -741,7 +742,9 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/HLE/sceKernelThread.cpp.arm \ $(SRC)/Core/HLE/sceKernelTime.cpp \ $(SRC)/Core/HLE/sceKernelVTimer.cpp \ + $(SRC)/Core/HLE/sceVideocodec.cpp \ $(SRC)/Core/HLE/sceMpeg.cpp \ + $(SRC)/Core/HLE/sceMpegbase.cpp \ $(SRC)/Core/HLE/sceMd5.cpp \ $(SRC)/Core/HLE/sceMp4.cpp \ $(SRC)/Core/HLE/sceAac.cpp \ diff --git a/libretro/Makefile.common b/libretro/Makefile.common index c0af28cc5b..fa6897fa45 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -801,7 +801,9 @@ SOURCES_CXX += \ $(COREDIR)/HLE/sceKernelThread.cpp \ $(COREDIR)/HLE/sceKernelTime.cpp \ $(COREDIR)/HLE/sceKernelVTimer.cpp \ + $(COREDIR)/HLE/sceVideocodec.cpp \ $(COREDIR)/HLE/sceMpeg.cpp \ + $(COREDIR)/HLE/sceMpegbase.cpp \ $(COREDIR)/HLE/sceNet.cpp \ $(COREDIR)/HLE/sceNet_lib.cpp \ $(COREDIR)/HLE/NetAdhocCommon.cpp \ @@ -847,6 +849,7 @@ SOURCES_CXX += \ $(COREDIR)/HW/SimpleAudioDec.cpp \ $(COREDIR)/HW/Atrac3Standalone.cpp \ $(COREDIR)/HW/AsyncIOManager.cpp \ + $(COREDIR)/HW/AvcDecoder.cpp \ $(COREDIR)/HW/MediaEngine.cpp \ $(COREDIR)/HW/MpegDemux.cpp \ $(COREDIR)/HW/MemoryStick.cpp \