Merge pull request #22286 from hrydgard/real-mpeg-prx

Implement sceVideocodec, run sceMpeg as LLE on top (controlled by DisableHLE)
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-09-14 17:23:01 -06:00
commit 03bb3e20fe
21 files changed
+1631 -58

No files matched your search

+6
View File
@@ -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
+6
View File
@@ -637,7 +637,9 @@
<ClCompile Include="HLE\sceKernelThread.cpp" />
<ClCompile Include="HLE\sceKernelTime.cpp" />
<ClCompile Include="HLE\sceKernelVTimer.cpp" />
<ClCompile Include="HLE\sceVideocodec.cpp" />
<ClCompile Include="HLE\sceMpeg.cpp" />
<ClCompile Include="HLE\sceMpegbase.cpp" />
<ClCompile Include="HLE\sceNet.cpp" />
<ClCompile Include="HLE\sceNet_lib.cpp" />
<ClCompile Include="HLE\NetAdhocCommon.cpp" />
@@ -671,6 +673,7 @@
<ClCompile Include="HLE\sceVaudio.cpp" />
<ClCompile Include="HLE\sceVshBridge.cpp" />
<ClCompile Include="HLE\__sceAudio.cpp" />
<ClCompile Include="HW\AvcDecoder.cpp" />
<ClCompile Include="HW\MediaEngine.cpp" />
<ClCompile Include="HW\MemoryStick.cpp" />
<ClCompile Include="HW\MpegDemux.cpp" />
@@ -1150,7 +1153,9 @@
<ClInclude Include="HLE\sceKernelSemaphore.h" />
<ClInclude Include="HLE\sceKernelThread.h" />
<ClInclude Include="HLE\sceKernelTime.h" />
<ClInclude Include="HLE\sceVideocodec.h" />
<ClInclude Include="HLE\sceMpeg.h" />
<ClInclude Include="HLE\sceMpegbase.h" />
<ClInclude Include="HLE\sceNet.h" />
<ClInclude Include="HLE\sceNet_lib.h" />
<ClInclude Include="HLE\NetAdhocCommon.h" />
@@ -1187,6 +1192,7 @@
<ClInclude Include="HLE\ThreadQueueList.h" />
<ClInclude Include="HLE\__sceAudio.h" />
<ClInclude Include="HW\BufferQueue.h" />
<ClInclude Include="HW\AvcDecoder.h" />
<ClInclude Include="HW\MediaEngine.h" />
<ClInclude Include="HW\MpegDemux.h" />
<ClInclude Include="HW\SasAudio.h" />
+18
View File
@@ -255,9 +255,15 @@
<ClCompile Include="HLE\sceJpeg.cpp">
<Filter>HLE\Libraries</Filter>
</ClCompile>
<ClCompile Include="HLE\sceVideocodec.cpp">
<Filter>HLE\Libraries</Filter>
</ClCompile>
<ClCompile Include="HLE\sceMpeg.cpp">
<Filter>HLE\Libraries</Filter>
</ClCompile>
<ClCompile Include="HLE\sceMpegbase.cpp">
<Filter>HLE\Libraries</Filter>
</ClCompile>
<ClCompile Include="HLE\sceMd5.cpp">
<Filter>HLE\Libraries</Filter>
</ClCompile>
@@ -354,6 +360,9 @@
<ClCompile Include="HLE\sceImpose.cpp">
<Filter>HLE\Libraries</Filter>
</ClCompile>
<ClCompile Include="HW\AvcDecoder.cpp">
<Filter>HW</Filter>
</ClCompile>
<ClCompile Include="HW\MediaEngine.cpp">
<Filter>HW</Filter>
</ClCompile>
@@ -1581,6 +1590,12 @@
<ClInclude Include="HLE\sceMpeg.h">
<Filter>HLE\Libraries</Filter>
</ClInclude>
<ClInclude Include="HLE\sceVideocodec.h">
<Filter>HLE\Libraries</Filter>
</ClInclude>
<ClInclude Include="HLE\sceMpegbase.h">
<Filter>HLE\Libraries</Filter>
</ClInclude>
<ClInclude Include="HLE\sceMd5.h">
<Filter>HLE\Libraries</Filter>
</ClInclude>
@@ -1686,6 +1701,9 @@
<ClInclude Include="HLE\sceImpose.h">
<Filter>HLE\Libraries</Filter>
</ClInclude>
<ClInclude Include="HW\AvcDecoder.h">
<Filter>HW</Filter>
</ClInclude>
<ClInclude Include="HW\MediaEngine.h">
<Filter>HW</Filter>
</ClInclude>
+2 -2
View File
@@ -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;
+3
View File
@@ -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.
+72 -9
View File
@@ -31,6 +31,10 @@
// g_audioDecoderContexts is to store current playing audios.
std::map<u32, AudioDecoder *> 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<u32, int> 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:
+6
View File
@@ -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);
+7 -43
View File
@@ -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<SceMpegLLI>::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>, "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);
};
+13 -1
View File
@@ -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);
+408
View File
@@ -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 <map>
#include <vector>
#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<u32, std::vector<u8>> 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<SceMpegLLI>::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<SceMpegLLI>::Create(p);
u32 dest = 0;
std::vector<u8> 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<u8>(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<u8> MpegBaseTakePESPacket(u32 dest) {
auto it = g_pesPackets.find(dest);
if (it == g_pesPackets.end()) {
return std::vector<u8>();
}
std::vector<u8> 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<u8> &luma, std::vector<u8> &cb, std::vector<u8> &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<SceMp4AvcCscStruct>::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<u8> 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<u8>(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<SceMp4AvcCscStruct>::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>, "sceMpegBasePESpacketCopy", 'x', "x" },
{0X492B5E4B, &WrapI_I<sceMpegBaseCscInit>, "sceMpegBaseCscInit", 'i', "i" },
{0X0530BE4E, &WrapI_I<sceMpegBaseCscSetPixelMode>, "sceMpegbase_0530BE4E", 'i', "i" },
{0X91929A21, &WrapI_UUIU<sceMpegBaseCscAvc>, "sceMpegBaseCscAvc", 'i', "xxix" },
{0X304882E1, &WrapU_UUUIU<sceMpegBaseCscAvcRange>, "sceMpegBaseCscAvcRange", 'x', "xxxix" },
{0X7AC0321A, &WrapI_UUI<sceMpegBaseYCrCbCopy>, "sceMpegBaseYCrCbCopy", 'i', "xxi" }
};
void Register_sceMpegbase()
{
RegisterHLEModule("sceMpegbase", ARRAY_SIZE(sceMpegbase), sceMpegbase);
};
+37
View File
@@ -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 <vector>
#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<u8> MpegBaseTakePESPacket(u32 dest);
+16 -3
View File
@@ -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"),
+654
View File
@@ -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 <algorithm>
#include <map>
#include <vector>
#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<u32, VideocodecCtx> 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<u8> 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<VideocodecCtxInfo> *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<u8> pes;
if (auSize > 0 && Memory::IsValidRange(auAddr, auSize)) {
au = Memory::GetTypedPointerRange<u8>(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>, "sceVideocodecOpen", 'i', "xi"},
{0X2D31F5B1, &WrapI_UI<sceVideocodecGetEDRAM>, "sceVideocodecGetEDRAM", 'i', "xi"},
{0X17099F0A, &WrapI_UI<sceVideocodecInit>, "sceVideocodecInit", 'i', "xi"},
{0XDBA273FA, &WrapI_UI<sceVideocodecDecode>, "sceVideocodecDecode", 'i', "xi"},
{0X4F160BF4, &WrapI_U<sceVideocodecReleaseEDRAM>, "sceVideocodecReleaseEDRAM", 'i', "x" },
{0X745A7B7A, &WrapI_UI<sceVideocodecSetMemory>, "sceVideocodecSetMemory", 'i', "xi"},
{0X2F385E7F, &WrapI_UI<sceVideocodecScanHeader>, "sceVideocodecScanHeader", 'i', "xi"},
{0X307E6E1C, &WrapI_UI<sceVideocodecDelete>, "sceVideocodecDelete", 'i', "xi"},
{0XA2F0564E, &WrapI_UI<sceVideocodecStop>, "sceVideocodecStop", 'i', "xi"},
{0X17CF7D2C, &WrapI_UI<sceVideocodecGetFrameCrop>, "sceVideocodecGetFrameCrop", 'i', "xi"},
{0X26927D19, &WrapI_UI<sceVideocodecGetVersion>, "sceVideocodecGetVersion", 'i', "xi"},
{0X627B7D42, &WrapI_UI<sceVideocodecGetSEI>, "sceVideocodecGetSEI", 'i', "xi"},
{0X893B32B1, &WrapI_UI<sceVideocodec_893B32B1>, "sceVideocodec_893B32B1", 'i', "xi"},
{0XD95C24D5, &WrapI_UI<sceVideocodec_D95C24D5>, "sceVideocodec_D95C24D5", 'i', "xi"},
};
void Register_sceVideocodec() {
RegisterHLEModule("sceVideocodec", ARRAY_SIZE(sceVideocodec), sceVideocodec);
}
+65
View File
@@ -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 <vector>
#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<VideocodecCtxInfo> *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]);
+194
View File
@@ -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
}
+72
View File
@@ -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 <vector>
#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<u8> packetBuf_;
bool haveFrame_ = false;
int width_ = 0;
int height_ = 0;
};
+34
View File
@@ -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<VideocodecCtxInfo> 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();
}
+6
View File
@@ -219,7 +219,9 @@
<ClInclude Include="..\..\Core\HLE\sceMp3.h" />
<ClInclude Include="..\..\Core\HLE\sceMp4.h" />
<ClInclude Include="..\..\Core\HLE\sceAac.h" />
<ClInclude Include="..\..\Core\HLE\sceVideocodec.h" />
<ClInclude Include="..\..\Core\HLE\sceMpeg.h" />
<ClInclude Include="..\..\Core\HLE\sceMpegbase.h" />
<ClInclude Include="..\..\Core\HLE\sceMt19937.h" />
<ClInclude Include="..\..\Core\HLE\sceNet.h" />
<ClInclude Include="..\..\Core\HLE\sceNet_lib.h" />
@@ -257,6 +259,7 @@
<ClInclude Include="..\..\Core\HW\Camera.h" />
<ClInclude Include="..\..\Core\HW\Display.h" />
<ClInclude Include="..\..\Core\HW\GpioMMIO.h" />
<ClInclude Include="..\..\Core\HW\AvcDecoder.h" />
<ClInclude Include="..\..\Core\HW\MediaEngine.h" />
<ClInclude Include="..\..\Core\HW\MemoryStick.h" />
<ClInclude Include="..\..\Core\HW\MpegDemux.h" />
@@ -502,7 +505,9 @@
<ClCompile Include="..\..\Core\HLE\sceMp3.cpp" />
<ClCompile Include="..\..\Core\HLE\sceMp4.cpp" />
<ClCompile Include="..\..\Core\HLE\sceAac.cpp" />
<ClCompile Include="..\..\Core\HLE\sceVideocodec.cpp" />
<ClCompile Include="..\..\Core\HLE\sceMpeg.cpp" />
<ClCompile Include="..\..\Core\HLE\sceMpegbase.cpp" />
<ClCompile Include="..\..\Core\HLE\sceMt19937.cpp" />
<ClCompile Include="..\..\Core\HLE\sceNet.cpp" />
<ClCompile Include="..\..\Core\HLE\sceNet_lib.cpp" />
@@ -539,6 +544,7 @@
<ClCompile Include="..\..\Core\HW\Camera.cpp" />
<ClCompile Include="..\..\Core\HW\Display.cpp" />
<ClCompile Include="..\..\Core\HW\GpioMMIO.cpp" />
<ClCompile Include="..\..\Core\HW\AvcDecoder.cpp" />
<ClCompile Include="..\..\Core\HW\MediaEngine.cpp" />
<ClCompile Include="..\..\Core\HW\MemoryStick.cpp" />
<ClCompile Include="..\..\Core\HW\MpegDemux.cpp" />
+6
View File
@@ -127,7 +127,9 @@
<ClCompile Include="..\..\Core\HLE\sceMp3.cpp" />
<ClCompile Include="..\..\Core\HLE\sceMp4.cpp" />
<ClCompile Include="..\..\Core\HLE\sceAac.cpp" />
<ClCompile Include="..\..\Core\HLE\sceVideocodec.cpp" />
<ClCompile Include="..\..\Core\HLE\sceMpeg.cpp" />
<ClCompile Include="..\..\Core\HLE\sceMpegbase.cpp" />
<ClCompile Include="..\..\Core\HLE\sceMt19937.cpp" />
<ClCompile Include="..\..\Core\HLE\sceNet.cpp" />
<ClCompile Include="..\..\Core\HLE\sceNet_lib.cpp" />
@@ -164,6 +166,7 @@
<ClCompile Include="..\..\Core\HW\Camera.cpp" />
<ClCompile Include="..\..\Core\HW\Display.cpp" />
<ClCompile Include="..\..\Core\HW\GpioMMIO.cpp" />
<ClCompile Include="..\..\Core\HW\AvcDecoder.cpp" />
<ClCompile Include="..\..\Core\HW\MediaEngine.cpp" />
<ClCompile Include="..\..\Core\HW\MemoryStick.cpp" />
<ClCompile Include="..\..\Core\HW\MpegDemux.cpp" />
@@ -522,6 +525,9 @@
<ClInclude Include="..\..\Core\HLE\sceAtrac.h" />
<ClInclude Include="..\..\Core\HLE\sceAudio.h" />
<ClInclude Include="..\..\Core\HLE\sceAudiocodec.h" />
<ClInclude Include="..\..\Core\HLE\sceMpegbase.h" />
<ClInclude Include="..\..\Core\HLE\sceVideocodec.h" />
<ClInclude Include="..\..\Core\HW\AvcDecoder.h" />
<ClInclude Include="..\..\Core\HLE\sceAudioRouting.h" />
<ClInclude Include="..\..\Core\HLE\sceCcc.h" />
<ClInclude Include="..\..\Core\HLE\sceChnnlsv.h" />
+3
View File
@@ -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 \
+3
View File
@@ -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 \