mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-14 01:59:34 +01:00
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:
21 files changed
+1631
-58
No files matched your search
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
@@ -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"),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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]);
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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 \
|
||||
|
||||
Reference in new issue
Block a user