From 7c6f9d18b8514d347d268912b53f5d7a44196ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 7 Sep 2026 16:41:46 -0600 Subject: [PATCH 01/14] sceMpegbase: implement the colour conversion functions Five of the six functions in this module were registered as nullptr, so any caller reaching them got nothing. mpeg.prx needs all of them, and this is the first of the pieces required before the real module can run in place of our sceMpeg HLE. The module gets its own file at the same time. It was a tail of sceMpeg.cpp, which is long enough already, and everything below is new code for it. sceMpegBaseCscAvc and sceMpegBaseCscAvcRange convert the ME's decoded output to RGB. The 48-byte descriptor holds the frame size in macroblocks at 0x10 and 0x14 - it appears at 0x00/0x04 too, but scaled differently depending on which path built it, so the second pair is the one every caller agrees on - and the four luma buffers at 0x20. What they point at is not planar: luma arrives as 16-pixel-wide vertical strips alternating between buffers on every other line, and chroma as Cb/Cr pairs interleaved across four more. All eight are untangled into planes once per frame before converting, which keeps the pixel loop readable. sceMpegBaseCscInit carries the default buffer width for callers that pass zero. sceMpegBaseCscSetPixelMode (0x0530BE4E - the official name isn't known) carries the output format, in mpegbase's own numbering rather than the GE's: mpeg.prx gets it by indexing a table of {1, 2, 3, 0} with the pixel mode the game gave sceMpegAvcDecodeMode, making 0 ABGR8888 and 2 ABGR5551. Read as a GE mode it turned Thrillville's black into blue. sceMpegBaseYCrCbCopy copies the descriptor but not the buffers it points at, and says so in its log - nothing seen so far needs more, and inventing the buffer copy without something to test it against seemed worse than a note. Behaviour is cross-checked against JPCSP, which implements all of these. Co-Authored-By: Claude Opus 5 (1M context) --- Core/CMakeLists.txt | 2 + Core/Core.vcxproj | 2 + Core/Core.vcxproj.filters | 6 + Core/HLE/HLETables.cpp | 1 + Core/HLE/sceMpeg.cpp | 47 +--- Core/HLE/sceMpeg.h | 14 +- Core/HLE/sceMpegbase.cpp | 343 ++++++++++++++++++++++++++++ Core/HLE/sceMpegbase.h | 23 ++ UWP/CoreUWP/CoreUWP.vcxproj | 2 + UWP/CoreUWP/CoreUWP.vcxproj.filters | 2 + android/jni/Android.mk | 1 + libretro/Makefile.common | 1 + 12 files changed, 402 insertions(+), 42 deletions(-) create mode 100644 Core/HLE/sceMpegbase.cpp create mode 100644 Core/HLE/sceMpegbase.h diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index a283e7c935..c6cf1f5a80 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -496,6 +496,8 @@ add_library(Core STATIC HLE/sceKernelVTimer.h HLE/sceMpeg.cpp HLE/sceMpeg.h + HLE/sceMpegbase.cpp + HLE/sceMpegbase.h HLE/sceNet.cpp HLE/sceNet.h HLE/sceNet_lib.cpp diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index b1cbe3f35e..bdda9d35a3 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -638,6 +638,7 @@ + @@ -1151,6 +1152,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 8bb8462455..5133900c30 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -258,6 +258,9 @@ HLE\Libraries + + HLE\Libraries + HLE\Libraries @@ -1581,6 +1584,9 @@ HLE\Libraries + + HLE\Libraries + HLE\Libraries diff --git a/Core/HLE/HLETables.cpp b/Core/HLE/HLETables.cpp index 94e15495e9..39df6a3f78 100644 --- a/Core/HLE/HLETables.cpp +++ b/Core/HLE/HLETables.cpp @@ -57,6 +57,7 @@ #include "sceNetAdhocMatching.h" #include "sceNp.h" #include "sceMpeg.h" +#include "sceMpegbase.h" #include "sceOpenPSID.h" #include "sceResmgr.h" #include "sceP3da.h" diff --git a/Core/HLE/sceMpeg.cpp b/Core/HLE/sceMpeg.cpp index 3418883434..9ca6ef58c9 100644 --- a/Core/HLE/sceMpeg.cpp +++ b/Core/HLE/sceMpeg.cpp @@ -23,6 +23,7 @@ #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/HLE/HLE.h" @@ -121,13 +122,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 +348,7 @@ private: }; void __MpegInit() { + __MpegBaseInit(); isMpegInit = false; mpegLibVersion = 0x010A; streamIdGen = 1; @@ -2322,37 +2321,3 @@ void Register_sceMpeg() } // This function is currently only been used for PMP videos -// p pointing to a SceMpegLLI structure consists of video frame blocks. -static u32 sceMpegBasePESpacketCopy(u32 p) -{ - pmp_videoSource = p; - pmp_nBlocks = 0; - - auto lli = PSPPointer::Create(p); - while (lli.IsValid()) { - pmp_nBlocks++; - // lli.Next ==0 for last block - if (lli->Next == 0){ - break; - } - ++lli; - } - - DEBUG_LOG(Log::Mpeg, "sceMpegBasePESpacketCopy(%08x), received %d block(s)", pmp_videoSource, pmp_nBlocks); - return 0; -} - -const HLEFunction sceMpegbase[] = -{ - {0XBEA18F91, &WrapU_U, "sceMpegBasePESpacketCopy", 'x', "x" }, - {0X492B5E4B, nullptr, "sceMpegBaseCscInit", '?', "" }, - {0X0530BE4E, nullptr, "sceMpegbase_0530BE4E", '?', "" }, - {0X91929A21, nullptr, "sceMpegBaseCscAvc", '?', "" }, - {0X304882E1, nullptr, "sceMpegBaseCscAvcRange", '?', "" }, - {0X7AC0321A, nullptr, "sceMpegBaseYCrCbCopy", '?', "" } -}; - -void Register_sceMpegbase() -{ - RegisterHLEModule("sceMpegbase", ARRAY_SIZE(sceMpegbase), sceMpegbase); -}; diff --git a/Core/HLE/sceMpeg.h b/Core/HLE/sceMpeg.h index db969b6292..9692a92176 100644 --- a/Core/HLE/sceMpeg.h +++ b/Core/HLE/sceMpeg.h @@ -139,7 +139,19 @@ void __MpegLoadModule(int version, u32 crc); void Register_sceMpeg(); -void Register_sceMpegbase(); +// One block of the scatter-gather list sceMpegBasePESpacketCopy walks. Shared because the PMP +// video path reads the same list back. +struct SceMpegLLI { + u32 pSrc; + u32 pDst; + u32 Next; + int iSize; +}; + +// sceMpegBasePESpacketCopy hands the list it just walked to the PMP path, which decodes from it. +// The two values live in sceMpeg.cpp rather than with the copy itself because __VideoPmpDoState +// serializes them as part of sceMpeg's savestate section. +void MpegSetPmpVideoSource(u32 addr, int blocks); void __VideoPmpInit(); void __VideoPmpDoState(PointerWrap &p); diff --git a/Core/HLE/sceMpegbase.cpp b/Core/HLE/sceMpegbase.cpp new file mode 100644 index 0000000000..9c048172e6 --- /dev/null +++ b/Core/HLE/sceMpegbase.cpp @@ -0,0 +1,343 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +// sceMpegbase - the Media Engine's colour space conversion, and the DMA that feeds it. +// +// mpeg.prx drives these directly, so they have to be real for the firmware module to run in place +// of our sceMpeg HLE. + +#include + +#include "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/MemMapHelpers.h" +#include "GPU/ge_constants.h" + +// 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_mpegBaseBufferWidth = 512; + g_mpegBasePixelMode = GE_CMODE_32BIT_ABGR8888; +} + +// p pointing to a SceMpegLLI structure consists of video frame blocks. +static u32 sceMpegBasePESpacketCopy(u32 p) +{ + int nBlocks = 0; + auto lli = PSPPointer::Create(p); + while (lli.IsValid()) { + nBlocks++; + // lli.Next ==0 for last block + if (lli->Next == 0){ + break; + } + ++lli; + } + MpegSetPmpVideoSource(p, nBlocks); + + DEBUG_LOG(Log::Mpeg, "sceMpegBasePESpacketCopy(%08x), received %d block(s)", p, nBlocks); + return 0; +} + + +// --- 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 is exactly 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 planar YCbCr. The layout below was established by analysing +// sceMpegBaseYCrCbCopy output on a real PSP (documented in JPCSP's sceVideocodec), and it uses +// all eight buffers in the descriptor: +// +// The image is divided into vertical bands 32 pixels wide, each split into two 16-pixel halves. +// Luma is one byte per pixel, and which buffer a row lands in depends on whether it is even or +// odd: +// buffer0: left half, even rows buffer1: right half, even rows +// buffer2: left half, odd rows buffer3: right half, odd rows +// Within a buffer, rows are stored 16 bytes at a time, band by band, top to bottom. +// +// Chroma is one (Cb,Cr) byte pair per 2x2 pixel square, so in chroma coordinates the bands are +// 16 wide with 8-pixel halves, and the same even/odd split applies: +// buffer4: left half, even chroma rows buffer5: left half, odd chroma rows +// buffer6: right half, even chroma rows buffer7: right half, odd chroma rows +// +// Untangling it into plain planes costs one pass per frame, which keeps the conversion below +// readable and is not where the time goes. +static bool ReadTiledYCbCr(const u32 *buffers, int width, int height, + std::vector &luma, std::vector &cb, std::vector &cr) { + const int width2 = width >> 1; + const int height2 = height >> 1; + + // buffer0/2 take the odd band out when the width isn't a multiple of 32. + const int lumaSizeLeft = ((width + 16) >> 5) * (height >> 1) * 16; + const int lumaSizeRight = (width >> 5) * (height >> 1) * 16; + const int chromaSizeLeft = lumaSizeLeft >> 1; + const int chromaSizeRight = lumaSizeRight >> 1; + + const u8 *y[4] = {}; + const u8 *c[4] = {}; + const int ySize[4] = { lumaSizeLeft, lumaSizeRight, lumaSizeLeft, lumaSizeRight }; + const int cSize[4] = { chromaSizeLeft, chromaSizeLeft, chromaSizeRight, chromaSizeRight }; + for (int i = 0; i < 4; i++) { + if (ySize[i] > 0) { + if (!Memory::IsValidRange(buffers[i], ySize[i])) { + return false; + } + y[i] = Memory::GetTypedPointerRange(buffers[i], ySize[i]); + } + if (cSize[i] > 0) { + if (!Memory::IsValidRange(buffers[4 + i], cSize[i])) { + return false; + } + c[i] = Memory::GetTypedPointerRange(buffers[4 + i], cSize[i]); + } + if ((ySize[i] > 0 && !y[i]) || (cSize[i] > 0 && !c[i])) { + return false; + } + } + + luma.assign((size_t)width * height, 0); + cb.assign((size_t)width2 * height2, 128); + cr.assign((size_t)width2 * height2, 128); + + // Luma: four buffers, keyed by (left/right half of the band, even/odd row). + for (int b = 0; b < 4; b++) { + if (!y[b]) { + continue; + } + const int xOffset = (b & 1) ? 16 : 0; + const int yStart = (b >> 1) ? 1 : 0; + int j = 0; + for (int bandX = xOffset; bandX < width; bandX += 32) { + const int run = std::min(16, width - bandX); + for (int row = yStart; row < height; row += 2, j += 16) { + if (run <= 0 || j + run > ySize[b]) { + continue; + } + memcpy(&luma[(size_t)row * width + bandX], y[b] + j, run); + } + } + } + + // Chroma: same shape in half-resolution coordinates, with interleaved Cb/Cr pairs. + for (int b = 0; b < 4; b++) { + if (!c[b]) { + continue; + } + const int xOffset = (b >> 1) ? 8 : 0; + const int yStart = (b & 1) ? 1 : 0; + int j = 0; + for (int bandX = xOffset; bandX < width2; bandX += 16) { + for (int row = yStart; row < height2; row += 2) { + for (int k = 0; k < 8; k++, j += 2) { + const int x = bandX + k; + if (x >= width2 || j + 1 >= cSize[b]) { + continue; + } + const size_t i = (size_t)row * width2 + x; + cb[i] = c[b][j]; + cr[i] = c[b][j + 1]; + } + } + } + } + return true; +} + +static u32 YCbCrToPixel(int y, int cbv, int crv, int pixelMode) { + const int c = y - 16, d = cbv - 128, e = crv - 128; + int r = (298 * c + 409 * e + 128) >> 8; + int g = (298 * c - 100 * d - 208 * e + 128) >> 8; + int b = (298 * c + 516 * d + 128) >> 8; + r = std::min(255, std::max(0, r)); + g = std::min(255, std::max(0, g)); + b = std::min(255, std::max(0, b)); + switch (pixelMode) { + case GE_CMODE_16BIT_BGR5650: + return ((b >> 3) << 11) | ((g >> 2) << 5) | (r >> 3); + case GE_CMODE_16BIT_ABGR5551: + return (1 << 15) | ((b >> 3) << 10) | ((g >> 3) << 5) | (r >> 3); + case GE_CMODE_16BIT_ABGR4444: + return (0xF << 12) | ((b >> 4) << 8) | ((g >> 4) << 4) | (r >> 4); + default: + return 0xFF000000 | (b << 16) | (g << 8) | r; + } +} + +// The shared body of sceMpegBaseCscAvc and sceMpegBaseCscAvcRange - the former is just the +// latter over the whole frame. +static int MpegBaseCscRange(u32 bufferRGB, u32 cscAddr, int bufferWidth, + int rangeX, int rangeY, int rangeWidth, int rangeHeight) { + auto csc = PSPPointer::Create(cscAddr); + if (!csc.IsValid()) { + return hleLogError(Log::Mpeg, -1, "bad csc struct pointer"); + } + if (bufferWidth == 0) { + bufferWidth = g_mpegBaseBufferWidth; + } + + const int width = csc->width << 4; + const int height = csc->height << 4; + if (width <= 0 || height <= 0 || width > 1024 || height > 1024) { + return hleLogError(Log::Mpeg, -1, "unreasonable frame size %dx%d", width, height); + } + if (rangeWidth <= 0 || rangeHeight <= 0) { + return hleLogDebug(Log::Mpeg, 0, "empty range"); + } + rangeWidth = std::min(rangeWidth, width - rangeX); + rangeHeight = std::min(rangeHeight, height - rangeY); + // The output buffer is sized from the stride below, so a row can't be wider than one. Every + // game seen so far passes a stride comfortably wider than the frame (512 for 480), but nothing + // guarantees it, and writing a wider row than we measured would run off the end of the buffer. + rangeWidth = std::min(rangeWidth, bufferWidth); + if (rangeX < 0 || rangeY < 0 || rangeWidth <= 0 || rangeHeight <= 0) { + return hleLogError(Log::Mpeg, -1, "range outside the frame"); + } + + u32 buffers[8]{}; + for (int i = 0; i < 4; i++) { + buffers[i] = csc->buffer[i]; + } + + std::vector luma, cb, cr; + if (!ReadTiledYCbCr(buffers, width, height, luma, cb, cr)) { + return hleLogError(Log::Mpeg, -1, "YCbCr buffers not readable"); + } + + const int bpp = g_mpegBasePixelMode == GE_CMODE_32BIT_ABGR8888 ? 4 : 2; + const u32 destSize = (u32)(rangeHeight * bufferWidth * bpp); + if (!Memory::IsValidRange(bufferRGB, destSize)) { + return hleLogError(Log::Mpeg, -1, "output buffer not writable"); + } + u8 *dest = Memory::GetTypedPointerWriteRange(bufferRGB, destSize); + if (!dest) { + return hleLogError(Log::Mpeg, -1, "output buffer not writable"); + } + + const int width2 = width >> 1; + for (int y = 0; y < rangeHeight; y++) { + const int sy = rangeY + y; + for (int x = 0; x < rangeWidth; x++) { + const int sx = rangeX + x; + const int ci = (sy >> 1) * width2 + (sx >> 1); + const u32 pixel = YCbCrToPixel(luma[sy * width + sx], cb[ci], cr[ci], g_mpegBasePixelMode); + if (bpp == 4) { + memcpy(dest + (y * bufferWidth + x) * 4, &pixel, 4); + } else { + const u16 p16 = (u16)pixel; + memcpy(dest + (y * bufferWidth + x) * 2, &p16, 2); + } + } + } + NotifyMemInfo(MemBlockFlags::WRITE, bufferRGB, destSize, "MpegBaseCsc"); + return hleLogDebug(Log::Mpeg, 0, "%dx%d at %d,%d -> %08x stride %d", + rangeWidth, rangeHeight, rangeX, rangeY, bufferRGB, bufferWidth); +} + +static int sceMpegBaseCscInit(int bufferWidth) { + g_mpegBaseBufferWidth = bufferWidth ? bufferWidth : 512; + return hleLogInfo(Log::Mpeg, 0, "bufferWidth %d", bufferWidth); +} + +// Sets the output pixel format for the conversions below. The official name isn't known - this one +// says what it does. Inside mpegbase.prx it stores the value and hands it to the DMACPLUS colour +// conversion hardware, and mpeg.prx gets it by indexing a table of {1, 2, 3, 0} with the pixel mode +// the game passed to sceMpegAvcDecodeMode. So the numbering is its own, not the GE's. +static int sceMpegBaseCscSetPixelMode(int pixelMode) { + static const int toGeMode[4] = { + GE_CMODE_32BIT_ABGR8888, + GE_CMODE_16BIT_BGR5650, + GE_CMODE_16BIT_ABGR5551, + GE_CMODE_16BIT_ABGR4444, + }; + if (pixelMode < 0 || pixelMode >= (int)ARRAY_SIZE(toGeMode)) { + return hleLogError(Log::Mpeg, -1, "bad pixel mode %d", pixelMode); + } + g_mpegBasePixelMode = toGeMode[pixelMode]; + return hleLogInfo(Log::Mpeg, 0, "pixel mode %d -> GE mode %d", pixelMode, g_mpegBasePixelMode); +} + +static int sceMpegBaseCscAvc(u32 bufferRGB, u32 unknown, int bufferWidth, u32 cscAddr) { + auto csc = PSPPointer::Create(cscAddr); + if (!csc.IsValid()) { + return hleLogError(Log::Mpeg, -1, "bad csc struct pointer"); + } + return MpegBaseCscRange(bufferRGB, cscAddr, bufferWidth, 0, 0, csc->width << 4, csc->height << 4); +} + +static u32 sceMpegBaseCscAvcRange(u32 bufferRGB, u32 unknown, u32 rangeAddr, int bufferWidth, u32 cscAddr) { + if (!Memory::IsValidRange(rangeAddr, 16)) { + return hleLogError(Log::Mpeg, -1, "bad range pointer"); + } + // Also in macroblocks. + const int rangeX = Memory::ReadUnchecked_U32(rangeAddr + 0) << 4; + const int rangeY = Memory::ReadUnchecked_U32(rangeAddr + 4) << 4; + const int rangeWidth = Memory::ReadUnchecked_U32(rangeAddr + 8) << 4; + const int rangeHeight = Memory::ReadUnchecked_U32(rangeAddr + 12) << 4; + return MpegBaseCscRange(bufferRGB, cscAddr, bufferWidth, rangeX, rangeY, rangeWidth, rangeHeight); +} + +// Copies one 48-byte YCrCb descriptor over another. The hardware copies the buffers themselves +// when told to; games seen so far only use it to move the descriptor around, so that is all we +// do until something needs more. +static int sceMpegBaseYCrCbCopy(u32 dstAddr, u32 srcAddr, int flags) { + if (!Memory::IsValidRange(dstAddr, sizeof(SceMp4AvcCscStruct)) || + !Memory::IsValidRange(srcAddr, sizeof(SceMp4AvcCscStruct))) { + return hleLogError(Log::Mpeg, -1, "bad descriptor pointer"); + } + Memory::Memcpy(dstAddr, srcAddr, (u32)sizeof(SceMp4AvcCscStruct), "MpegBaseYCrCbCopy"); + return hleLogDebug(Log::Mpeg, 0, "flags %08x - descriptor copied, buffers left alone", flags); +} + +const HLEFunction sceMpegbase[] = +{ + {0XBEA18F91, &WrapU_U, "sceMpegBasePESpacketCopy", 'x', "x" }, + {0X492B5E4B, &WrapI_I, "sceMpegBaseCscInit", 'i', "i" }, + {0X0530BE4E, &WrapI_I, "sceMpegbase_0530BE4E", 'i', "i" }, + {0X91929A21, &WrapI_UUIU, "sceMpegBaseCscAvc", 'i', "xxix" }, + {0X304882E1, &WrapU_UUUIU, "sceMpegBaseCscAvcRange", 'x', "xxxix" }, + {0X7AC0321A, &WrapI_UUI, "sceMpegBaseYCrCbCopy", 'i', "xxi" } +}; + +void Register_sceMpegbase() +{ + RegisterHLEModule("sceMpegbase", ARRAY_SIZE(sceMpegbase), sceMpegbase); +}; diff --git a/Core/HLE/sceMpegbase.h b/Core/HLE/sceMpegbase.h new file mode 100644 index 0000000000..98fbad71bd --- /dev/null +++ b/Core/HLE/sceMpegbase.h @@ -0,0 +1,23 @@ +// 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 + +void Register_sceMpegbase(); + +// Called per boot, from __MpegInit. +void __MpegBaseInit(); diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index b94187585b..9bda778ca2 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -220,6 +220,7 @@ + @@ -503,6 +504,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index 05b1873f8f..555c850fcd 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -128,6 +128,7 @@ + @@ -522,6 +523,7 @@ + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index d7c5912df0..1a0d219d2c 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -742,6 +742,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/HLE/sceKernelTime.cpp \ $(SRC)/Core/HLE/sceKernelVTimer.cpp \ $(SRC)/Core/HLE/sceMpeg.cpp \ + $(SRC)/Core/HLE/sceMpegbase.cpp \ $(SRC)/Core/HLE/sceMd5.cpp \ $(SRC)/Core/HLE/sceMp4.cpp \ $(SRC)/Core/HLE/sceAac.cpp \ diff --git a/libretro/Makefile.common b/libretro/Makefile.common index c0af28cc5b..a47b086c88 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -802,6 +802,7 @@ SOURCES_CXX += \ $(COREDIR)/HLE/sceKernelTime.cpp \ $(COREDIR)/HLE/sceKernelVTimer.cpp \ $(COREDIR)/HLE/sceMpeg.cpp \ + $(COREDIR)/HLE/sceMpegbase.cpp \ $(COREDIR)/HLE/sceNet.cpp \ $(COREDIR)/HLE/sceNet_lib.cpp \ $(COREDIR)/HLE/NetAdhocCommon.cpp \ From 3276cdf65f402f5825fbebde9c78bd4982f4f4f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 7 Sep 2026 16:56:29 -0600 Subject: [PATCH 02/14] Add AvcDecoder, a plain access-unit-in, frame-out H.264 decoder sceVideocodec is the interface the Media Engine really exposes: the caller owns the buffers and hands over one access unit at a time. MediaEngine can't serve that shape - 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, separate path rather than a refactor of code every game that plays video depends on. Merging the two is worth doing once sceVideocodec has earned its keep. Nothing calls it yet; sceVideocodec is the next piece. Co-Authored-By: Claude Opus 5 (1M context) --- Core/CMakeLists.txt | 2 + Core/Core.vcxproj | 2 + Core/Core.vcxproj.filters | 6 + Core/HW/AvcDecoder.cpp | 187 ++++++++++++++++++++++++++++ Core/HW/AvcDecoder.h | 73 +++++++++++ UWP/CoreUWP/CoreUWP.vcxproj | 2 + UWP/CoreUWP/CoreUWP.vcxproj.filters | 2 + android/jni/Android.mk | 1 + libretro/Makefile.common | 1 + 9 files changed, 276 insertions(+) create mode 100644 Core/HW/AvcDecoder.cpp create mode 100644 Core/HW/AvcDecoder.h diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index c6cf1f5a80..24b7c17d4d 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -597,6 +597,8 @@ add_library(Core STATIC HW/Display.h HW/GpioMMIO.cpp HW/GpioMMIO.h + HW/AvcDecoder.cpp + HW/AvcDecoder.h HW/MediaEngine.cpp HW/MediaEngine.h HW/MpegDemux.cpp diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index bdda9d35a3..b051ba598a 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -672,6 +672,7 @@ + @@ -1189,6 +1190,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 5133900c30..16ebfaddd9 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -357,6 +357,9 @@ HLE\Libraries + + HW + HW @@ -1692,6 +1695,9 @@ HLE\Libraries + + HW + HW diff --git a/Core/HW/AvcDecoder.cpp b/Core/HW/AvcDecoder.cpp new file mode 100644 index 0000000000..b87bb611be --- /dev/null +++ b/Core/HW/AvcDecoder.cpp @@ -0,0 +1,187 @@ +// 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 + + width_ = frame_->width; + height_ = frame_->height; + haveFrame_ = width_ > 0 && height_ > 0; + return haveFrame_; +#else + return false; +#endif +} + +const u8 *AvcDecoder::Plane(int index) const { +#ifdef USE_FFMPEG + if (!haveFrame_ || !frame_ || index < 0 || index >= 3) { + return nullptr; + } + return frame_->data[index]; +#else + return nullptr; +#endif +} + +int AvcDecoder::Stride(int index) const { +#ifdef USE_FFMPEG + if (!haveFrame_ || !frame_ || index < 0 || index >= 3) { + return 0; + } + return frame_->linesize[index]; +#else + return 0; +#endif +} diff --git a/Core/HW/AvcDecoder.h b/Core/HW/AvcDecoder.h new file mode 100644 index 0000000000..35cf1f7078 --- /dev/null +++ b/Core/HW/AvcDecoder.h @@ -0,0 +1,73 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include + +#include "Common/CommonTypes.h" + +struct AVCodecContext; +struct AVFrame; + +// A plain "hand it one access unit, get a frame back" H.264 decoder. +// +// This exists for sceVideocodec, which is the interface the Media Engine really exposes: the +// caller owns the buffers and hands over one access unit at a time. 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 deliberately a second, separate path +// rather than a refactor of code every game that plays video depends on. Merging the two is +// worth doing once sceVideocodec has proven itself. +// +// Output is whatever pixel format the decoder produced, normally YUV420P; the caller converts. +class AvcDecoder { +public: + AvcDecoder(); + ~AvcDecoder(); + + AvcDecoder(const AvcDecoder &) = delete; + AvcDecoder &operator=(const AvcDecoder &) = delete; + + // Decodes one access unit. Returns true when a frame came out - H.264 reorders, so a frame + // is not produced for every unit fed in, and that is not an error. + bool Decode(const u8 *data, int size); + + // Throws away any decoder state, for a seek or a new stream. + void Flush(); + + bool HasFrame() const { return haveFrame_; } + int Width() const { return width_; } + int Height() const { return height_; } + + // Only meaningful while HasFrame(). Planes are Y, Cb, Cr; strides are in bytes and are not + // necessarily the width. + const u8 *Plane(int index) const; + int Stride(int index) const; + + // True if we were built without ffmpeg, in which case nothing decodes. + static bool IsAvailable(); + +private: + AVCodecContext *codecCtx_ = nullptr; + AVFrame *frame_ = nullptr; + // The access unit, copied so we can pad it - ffmpeg reads a little past the end of a packet. + // Kept between calls so the usual case doesn't allocate. + std::vector packetBuf_; + bool haveFrame_ = false; + int width_ = 0; + int height_ = 0; +}; diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index 9bda778ca2..291e398f04 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -258,6 +258,7 @@ + @@ -541,6 +542,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index 555c850fcd..e800daa7e1 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -165,6 +165,7 @@ + @@ -524,6 +525,7 @@ + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 1a0d219d2c..72b9440a41 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -614,6 +614,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/HW/GpioMMIO.cpp \ $(SRC)/Core/HW/MemoryStick.cpp \ $(SRC)/Core/HW/MpegDemux.cpp.arm \ + $(SRC)/Core/HW/AvcDecoder.cpp.arm \ $(SRC)/Core/HW/MediaEngine.cpp.arm \ $(SRC)/Core/HW/SasAudio.cpp.arm \ $(SRC)/Core/HW/SasReverb.cpp.arm \ diff --git a/libretro/Makefile.common b/libretro/Makefile.common index a47b086c88..d865b0f66c 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -848,6 +848,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 \ From 184c2cb4ea0605eb1093678cb5504757cdaf4c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 7 Sep 2026 17:25:49 -0600 Subject: [PATCH 03/14] Implement sceVideocodec, the ME's H.264 decoding interface The last piece mpeg.prx needs from us. It decodes through AvcDecoder and writes the result where sceMpegbase expects to read it: for type 0, the eight-buffer tiled layout, produced here as the exact inverse of the reader added with the colour conversion. Type 1 hands back plain planar YUV instead. The descriptor mpeg.prx passes in is empty - on hardware the Media Engine owns the frame buffers and reports back where it put them - so they are allocated here and their addresses filled in, and sceMpegbase can recover the full set from that allocation afterwards. The list is only read as addresses once it holds addresses: a caller that leaves small integers there used to take us straight into a memory exception. Both those buffers and the block sceVideocodecGetEDRAM hands out live in a model of the ME's own 2MB of embedded DRAM rather than in either PSP partition, with a BlockAllocator carving it up. The CPU can't reach ME memory on hardware: mpeg.prx keeps the EDRAM value and hands it back without ever dereferencing it, and it has no way to say where the frame buffers should go - sceVideocodecSetMemory is given a frame size and a buffer count, 480, 272 and 2 for a full-screen movie. Taking either from the game would push its own allocations around or spend memory a real PSP never spends, so sceMpegbase reads the frame through a host pointer into that block instead of through Memory::. A game can hold several contexts at once - Silent Hill Origins runs two, one that owns the EDRAM and one that does every decode - so the decoder, the frame buffers and the EDRAM address are per context, keyed by context address the way sceAudiocodec keys its decoders. The EDRAM itself the firmware tracks in the caller's context struct and nowhere else, including refusing a second request rather than replacing the first, as videocodec_260.prx shows, so we do the same. Registered at the end of RegisterAllModules, since a savestate stores the syscall opcode encoding that order. GetSEI, ScanHeader, GetFrameCrop and the two unnamed NIDs return 0 and log - mpeg.prx calls them but nothing yet shows what they need to return, and guessing seemed worse than being loud about it. Untested: nothing calls this until mpeg.prx is loaded, which is the next step. Co-Authored-By: Claude Opus 5 (1M context) --- Core/CMakeLists.txt | 2 + Core/Core.vcxproj | 2 + Core/Core.vcxproj.filters | 6 + Core/HLE/HLETables.cpp | 3 + Core/HLE/sceKernel.cpp | 4 + Core/HLE/sceMpegbase.cpp | 15 +- Core/HLE/sceVideocodec.cpp | 581 ++++++++++++++++++++++++++++ Core/HLE/sceVideocodec.h | 33 ++ UWP/CoreUWP/CoreUWP.vcxproj | 2 + UWP/CoreUWP/CoreUWP.vcxproj.filters | 2 + android/jni/Android.mk | 1 + libretro/Makefile.common | 1 + 12 files changed, 645 insertions(+), 7 deletions(-) create mode 100644 Core/HLE/sceVideocodec.cpp create mode 100644 Core/HLE/sceVideocodec.h diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index 24b7c17d4d..c42305224a 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -494,6 +494,8 @@ 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 diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index b051ba598a..c39cf6b058 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -637,6 +637,7 @@ + @@ -1152,6 +1153,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 16ebfaddd9..b0e07da334 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -255,6 +255,9 @@ HLE\Libraries + + HLE\Libraries + HLE\Libraries @@ -1587,6 +1590,9 @@ HLE\Libraries + + HLE\Libraries + HLE\Libraries diff --git a/Core/HLE/HLETables.cpp b/Core/HLE/HLETables.cpp index 39df6a3f78..44d672aebe 100644 --- a/Core/HLE/HLETables.cpp +++ b/Core/HLE/HLETables.cpp @@ -91,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', ""}, @@ -329,6 +330,8 @@ void RegisterAllModules() { Register_sceVshBridge(); Register_sceResmgr(); + Register_sceVideocodec(); + // add new modules here. // Not ready to enable this due to apparent softlocks in Patapon 3. diff --git a/Core/HLE/sceKernel.cpp b/Core/HLE/sceKernel.cpp index 623b412289..aeb6fdb928 100644 --- a/Core/HLE/sceKernel.cpp +++ b/Core/HLE/sceKernel.cpp @@ -45,6 +45,7 @@ #include "sceAtrac.h" #include "sceAudio.h" #include "sceAudiocodec.h" +#include "sceVideocodec.h" #include "sceCcc.h" #include "sceCtrl.h" #include "sceDisplay.h" @@ -158,6 +159,7 @@ void __KernelInit() __HeapInit(); __DmacInit(); __AudioCodecInit(); + __VideocodecInit(); __VideoPmpInit(); __UsbGpsInit(); __UsbCamInit(); @@ -197,6 +199,7 @@ void __KernelShutdown() __UsbGpsShutdown(); __AudioCodecShutdown(); + __VideocodecShutdown(); __VideoPmpShutdown(); __AACShutdown(); __NetAdhocShutdown(); @@ -284,6 +287,7 @@ void __KernelDoState(PointerWrap &p) __JpegDoState(p); __Mp3DoState(p); __MpegDoState(p); + __VideocodecDoState(p); __NetDoState(p); __NetAdhocDoState(p); __PowerDoState(p); diff --git a/Core/HLE/sceMpegbase.cpp b/Core/HLE/sceMpegbase.cpp index 9c048172e6..b55a31f507 100644 --- a/Core/HLE/sceMpegbase.cpp +++ b/Core/HLE/sceMpegbase.cpp @@ -27,6 +27,7 @@ #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" @@ -116,21 +117,21 @@ static bool ReadTiledYCbCr(const u32 *buffers, int width, int height, const u8 *c[4] = {}; const int ySize[4] = { lumaSizeLeft, lumaSizeRight, lumaSizeLeft, lumaSizeRight }; const int cSize[4] = { chromaSizeLeft, chromaSizeLeft, chromaSizeRight, chromaSizeRight }; + // 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. for (int i = 0; i < 4; i++) { if (ySize[i] > 0) { - if (!Memory::IsValidRange(buffers[i], ySize[i])) { + y[i] = VideocodecMEPointer(buffers[i], ySize[i]); + if (!y[i]) { return false; } - y[i] = Memory::GetTypedPointerRange(buffers[i], ySize[i]); } if (cSize[i] > 0) { - if (!Memory::IsValidRange(buffers[4 + i], cSize[i])) { + c[i] = VideocodecMEPointer(buffers[4 + i], cSize[i]); + if (!c[i]) { return false; } - c[i] = Memory::GetTypedPointerRange(buffers[4 + i], cSize[i]); - } - if ((ySize[i] > 0 && !y[i]) || (cSize[i] > 0 && !c[i])) { - return false; } } diff --git a/Core/HLE/sceVideocodec.cpp b/Core/HLE/sceVideocodec.cpp new file mode 100644 index 0000000000..f4ec3b3150 --- /dev/null +++ b/Core/HLE/sceVideocodec.cpp @@ -0,0 +1,581 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +// sceVideocodec - the H.264 decoding interface the Media Engine exposes. +// +// This exists so that flash0:/kd/mpeg.prx can be run in place of our sceMpeg HLE: mpeg.prx needs +// only sceVideocodec, sceMpegbase and sceAudiocodec from us, and the other two we already have. +// The point is to have a reference to compare the HLE against, so it aims to behave like the +// hardware rather than to be the fastest way to get pixels on screen. +// +// Behaviour cross-checked against JPCSP, whose description of the buffer layout was established +// by looking at sceMpegBaseYCrCbCopy output on a real PSP. + +#include +#include +#include + +#include "Common/Serialize/Serializer.h" +#include "Common/Serialize/SerializeFuncs.h" +#include "Core/HLE/ErrorCodes.h" +#include "Core/HLE/HLE.h" +#include "Core/HLE/FunctionWrappers.h" +#include "Core/HLE/sceVideocodec.h" +#include "Core/Util/BlockAllocator.h" +#include "Core/HW/AvcDecoder.h" +#include "Core/MemMap.h" +#include "Core/MIPS/MIPS.h" + +// The context the caller hands us is 96 bytes. The offsets below are what mpeg.prx actually +// reads and writes; anything not listed here it doesn't look at. +enum { + CTX_MAGIC = 0, // 0x05100601, same marker sceAudiocodec's context carries + CTX_VERSION = 4, // GetVersion writes 0x78 here + CTX_STATUS = 8, + CTX_MEM = 12, + CTX_OUT_INFO = 16, // pointer to the 108-byte result descriptor + CTX_EDRAM = 20, + CTX_EDRAM_SIZE = 24, + CTX_AU_DATA = 36, // the access unit to decode + CTX_AU_SIZE = 40, + CTX_YUV_STRUCT = 44, // type 0: pointer to the eight output buffers + CTX_EDRAM_RAW = 92, +}; + +// Fields of the descriptor at CTX_OUT_INFO that mpeg.prx reads back. +enum { + OUT_DATA = 0, + OUT_SIZE = 4, + OUT_UNK8 = 8, + OUT_UNK12 = 12, + OUT_CONSUMED = 44, + OUT_WIDTH = 48, + OUT_HEIGHT = 52, + OUT_FRAME_READY = 60, // 2 when a frame came out, 1 when it didn't + OUT_UNK64 = 64, + OUT_UNK72 = 72, + OUT_TIMESTAMP = 76, + OUT_FPS = 80, + OUT_BUFFER_Y = 84, + OUT_BUFFER_CR = 88, + OUT_BUFFER_CB = 92, + OUT_WIDTH_Y = 96, + OUT_WIDTH_CR = 100, + OUT_WIDTH_CB = 104, +}; + +// A game can have more than one of these open at once - Silent Hill Origins runs two, one that +// owns the EDRAM and one that does the decoding - so everything here is per context. +struct VideocodecCtx { + AvcDecoder *decoder = nullptr; + int type = 0; + int frameCount = 0; + // What sceVideocodecGetEDRAM handed out, as an address in g_meRam. + u32 edram = 0; + // The frame buffers the ME reported back, also in g_meRam - see PublishFrameBuffers. + u32 frameBuffers = 0; + u32 frameBuffersSize = 0; + int frameBufferWidth = 0; + int frameBufferHeight = 0; +}; + +static std::map g_videocodecCtxs; + +// The Media Engine's own 2MB of embedded DRAM, modelled as memory of ours. +// +// The 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. +// +// So none of it may come out of the game's partitions: taking it from user memory would push a +// game's own allocations around, and from kernel memory would spend memory a real PSP never does. +// The addresses handed out are offsets into g_meRam, based well outside anything PSP RAM maps so +// that a stray dereference faults where it happens instead of quietly reading the game's memory. +// Being outside PSP RAM, the contents aren't in the memory a savestate captures either, so the +// block and its allocator go in __VideocodecDoState. +static const u32 ME_EDRAM_BASE = 0xC0000000; +static const u32 ME_EDRAM_SIZE = 2 * 1024 * 1024; +static std::vector g_meRam; +static BlockAllocator g_meAlloc(64); + +// The 2MB is only committed once something asks for a piece of it, so a game that never plays a +// video pays nothing for this and its savestates don't carry it. +static void MEEnsureRam() { + if (g_meRam.size() != ME_EDRAM_SIZE) { + g_meRam.assign(ME_EDRAM_SIZE, 0); + g_meAlloc.Init(ME_EDRAM_BASE, ME_EDRAM_SIZE, false); + } +} + +u8 *VideocodecMEPointer(u32 addr, u32 size) { + if (addr < ME_EDRAM_BASE || size > ME_EDRAM_SIZE || g_meRam.size() != ME_EDRAM_SIZE) { + return nullptr; + } + const u32 offset = addr - ME_EDRAM_BASE; + if (offset > ME_EDRAM_SIZE - size) { + return nullptr; + } + return g_meRam.data() + offset; +} + +static void FreeContext(VideocodecCtx &ctx) { + delete ctx.decoder; + ctx.decoder = nullptr; + if (ctx.edram) { + g_meAlloc.Free(ctx.edram); + ctx.edram = 0; + } + if (ctx.frameBuffers) { + g_meAlloc.Free(ctx.frameBuffers); + ctx.frameBuffers = 0; + } +} + +// freeMemory is false when loading a savestate: the state carries its own g_meAlloc, so the blocks +// these contexts were holding belong to a world that no longer exists and freeing them would be +// freeing someone else's memory. +static void ClearContexts(bool freeMemory) { + for (auto &[addr, ctx] : g_videocodecCtxs) { + if (freeMemory) { + FreeContext(ctx); + } else { + delete ctx.decoder; + ctx.decoder = nullptr; + } + } + g_videocodecCtxs.clear(); +} + +void __VideocodecInit() { + // Nothing to free: a boot starts with a fresh allocator. + g_videocodecCtxs.clear(); + g_meRam.clear(); + g_meRam.shrink_to_fit(); + g_meAlloc.Shutdown(); +} + +void __VideocodecShutdown() { + ClearContexts(true); + g_meRam.clear(); + g_meRam.shrink_to_fit(); + g_meAlloc.Shutdown(); +} + +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. + 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.edram); + Do(p, ctx.frameBuffers); + Do(p, ctx.frameBuffersSize); + Do(p, ctx.frameBufferWidth); + Do(p, ctx.frameBufferHeight); + g_videocodecCtxs[addr] = 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.edram); + Do(p, ctx.frameBuffers); + Do(p, ctx.frameBuffersSize); + Do(p, ctx.frameBufferWidth); + Do(p, ctx.frameBufferHeight); + } + } + + // 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); +} + +// 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]) { + const int lumaLeft = ((width + 16) >> 5) * (height >> 1) * 16; + const int lumaRight = (width >> 5) * (height >> 1) * 16; + const int sizes[8] = { + lumaLeft, lumaRight, lumaLeft, lumaRight, + lumaLeft >> 1, lumaLeft >> 1, lumaRight >> 1, lumaRight >> 1, + }; + + u32 total = 0; + for (int i = 0; i < 8; i++) { + total += (sizes[i] + 63) & ~63; + } + 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); + } + + u32 addr = vctx.frameBuffers; + for (int i = 0; i < 8; i++) { + buffers[i] = addr; + addr += (sizes[i] + 63) & ~63; + } + + 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; +} + +// 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; + } + + const int lumaSizeLeft = ((width + 16) >> 5) * (height >> 1) * 16; + const int lumaSizeRight = (width >> 5) * (height >> 1) * 16; + const int ySize[4] = { lumaSizeLeft, lumaSizeRight, lumaSizeLeft, lumaSizeRight }; + const int cSize[4] = { lumaSizeLeft >> 1, lumaSizeLeft >> 1, lumaSizeRight >> 1, lumaSizeRight >> 1 }; + + 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. The firmware keeps the block in +// the caller's context and nowhere else, so we do too - see ppsspp-re, modules/sceVideocodec. +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"); + } + + bool gotFrame = false; + if (auSize > 0 && Memory::IsValidRange(auAddr, auSize)) { + const u8 *au = Memory::GetTypedPointerRange(auAddr, auSize); + if (au) { + gotFrame = vctx.decoder->Decode(au, auSize); + } + } + + 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. + if (type == 0) { + out32(8, width); + out32(12, height); + out32(28, 1); + out32(32, gotFrame ? 1 : 0); // images decoded - mpeg.prx won't convert without this + out32(36, gotFrame ? 0 : 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); + } + } 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); + } + } + + return hleLogDebug(Log::ME, 0, "type %d, %d bytes -> %s %dx%d", + type, auSize, gotFrame ? "frame" : "no frame yet", width, height); +} + +static int sceVideocodecStop(u32 ctxAddr, int type) { + auto it = g_videocodecCtxs.find(ctxAddr); + if (it != g_videocodecCtxs.end() && it->second.decoder) { + it->second.decoder->Flush(); + } + return hleLogInfo(Log::ME, 0); +} + +static int sceVideocodecDelete(u32 ctxAddr, int type) { + auto it = g_videocodecCtxs.find(ctxAddr); + if (it != g_videocodecCtxs.end()) { + FreeContext(it->second); + g_videocodecCtxs.erase(it); + } + return hleLogInfo(Log::ME, 0); +} + +static int sceVideocodecGetVersion(u32 ctxAddr, int type) { + if (!Memory::IsValidRange(ctxAddr, 96)) { + return hleLogError(Log::ME, -1, "bad context pointer"); + } + // The value a real PSP returns, read with JpcspTrace. + Memory::WriteUnchecked_U32(0x78, ctxAddr + CTX_VERSION); + return hleLogInfo(Log::ME, 0); +} + +static int sceVideocodecGetSEI(u32 ctxAddr, int type) { + return hleLogWarning(Log::ME, 0, "UNIMPL"); +} + +static int sceVideocodecScanHeader(u32 ctxAddr, int type) { + return hleLogWarning(Log::ME, 0, "UNIMPL"); +} + +static int sceVideocodecGetFrameCrop(u32 ctxAddr, int type) { + return hleLogWarning(Log::ME, 0, "UNIMPL"); +} + +static int sceVideocodecSetMemory(u32 ctxAddr, int type) { + return hleLogDebug(Log::ME, 0); +} + +static int sceVideocodec_893B32B1(u32 ctxAddr, int type) { + return hleLogWarning(Log::ME, 0, "UNIMPL"); +} + +static int sceVideocodec_D95C24D5(u32 ctxAddr, int type) { + return hleLogWarning(Log::ME, 0, "UNIMPL"); +} + +const HLEFunction sceVideocodec[] = { + {0XC01EC829, &WrapI_UI, "sceVideocodecOpen", 'i', "xi"}, + {0X2D31F5B1, &WrapI_UI, "sceVideocodecGetEDRAM", 'i', "xi"}, + {0X17099F0A, &WrapI_UI, "sceVideocodecInit", 'i', "xi"}, + {0XDBA273FA, &WrapI_UI, "sceVideocodecDecode", 'i', "xi"}, + {0X4F160BF4, &WrapI_U, "sceVideocodecReleaseEDRAM", 'i', "x" }, + {0X745A7B7A, &WrapI_UI, "sceVideocodecSetMemory", 'i', "xi"}, + {0X2F385E7F, &WrapI_UI, "sceVideocodecScanHeader", 'i', "xi"}, + {0X307E6E1C, &WrapI_UI, "sceVideocodecDelete", 'i', "xi"}, + {0XA2F0564E, &WrapI_UI, "sceVideocodecStop", 'i', "xi"}, + {0X17CF7D2C, &WrapI_UI, "sceVideocodecGetFrameCrop", 'i', "xi"}, + {0X26927D19, &WrapI_UI, "sceVideocodecGetVersion", 'i', "xi"}, + {0X627B7D42, &WrapI_UI, "sceVideocodecGetSEI", 'i', "xi"}, + {0X893B32B1, &WrapI_UI, "sceVideocodec_893B32B1", 'i', "xi"}, + {0XD95C24D5, &WrapI_UI, "sceVideocodec_D95C24D5", 'i', "xi"}, +}; + +void Register_sceVideocodec() { + RegisterHLEModule("sceVideocodec", ARRAY_SIZE(sceVideocodec), sceVideocodec); +} diff --git a/Core/HLE/sceVideocodec.h b/Core/HLE/sceVideocodec.h new file mode 100644 index 0000000000..fbb06e9f11 --- /dev/null +++ b/Core/HLE/sceVideocodec.h @@ -0,0 +1,33 @@ +// 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 "Common/CommonTypes.h" + +class PointerWrap; + +void __VideocodecInit(); +void __VideocodecShutdown(); +void __VideocodecDoState(PointerWrap &p); + +void Register_sceVideocodec(); + +// 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); diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index 291e398f04..6e74a6f739 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -219,6 +219,7 @@ + @@ -504,6 +505,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index e800daa7e1..69ae74e6fe 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -127,6 +127,7 @@ + @@ -525,6 +526,7 @@ + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 72b9440a41..63cff285ec 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -742,6 +742,7 @@ 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 \ diff --git a/libretro/Makefile.common b/libretro/Makefile.common index d865b0f66c..fa6897fa45 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -801,6 +801,7 @@ 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 \ From ba975d290841a1a6658f6ada4a872c301ec67f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 7 Sep 2026 18:50:51 -0600 Subject: [PATCH 04/14] sceMpegbase: recover the chroma buffers the descriptor doesn't carry mpeg.prx copies only the four luma buffers into the descriptor it passes to sceMpegBaseCscAvc, at +0x20, and keeps a stride where the chroma ones would be - so the conversion had nothing to read. Both ends of this are ours, so the full set is recovered from the allocation sceVideocodec handed out. Death Jr. now runs the whole chain: 504 frames decoded by real mpeg.prx through sceVideocodec, and 505 colour conversions into the game's display buffers at 480x272, no exceptions. The picture is still black, and the cause is now upstream of all of this: the decoder itself returns blank frames, so what we feed it isn't the video elementary stream. The first access unit being 162 bytes was the early sign. sceMpegBasePESpacketCopy gathers the LLI blocks it is given, and that is evidently not the whole story - PES headers, or blocks it isn't counting. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/sceMpegbase.cpp | 3 +++ Core/HLE/sceVideocodec.cpp | 27 +++++++++++++++++++++++++++ Core/HLE/sceVideocodec.h | 4 ++++ 3 files changed, 34 insertions(+) diff --git a/Core/HLE/sceMpegbase.cpp b/Core/HLE/sceMpegbase.cpp index b55a31f507..7fe02ae97d 100644 --- a/Core/HLE/sceMpegbase.cpp +++ b/Core/HLE/sceMpegbase.cpp @@ -233,10 +233,13 @@ static int MpegBaseCscRange(u32 bufferRGB, u32 cscAddr, int bufferWidth, return hleLogError(Log::Mpeg, -1, "range outside the frame"); } + // The descriptor only carries the four luma buffers. Recover the full set from the allocation + // sceVideocodec handed out - both ends of that are ours. u32 buffers[8]{}; for (int i = 0; i < 4; i++) { buffers[i] = csc->buffer[i]; } + VideocodecGetFrameBuffers(csc->buffer[0], buffers); std::vector luma, cb, cr; if (!ReadTiledYCbCr(buffers, width, height, luma, cb, cr)) { diff --git a/Core/HLE/sceVideocodec.cpp b/Core/HLE/sceVideocodec.cpp index f4ec3b3150..dfcfa120d7 100644 --- a/Core/HLE/sceVideocodec.cpp +++ b/Core/HLE/sceVideocodec.cpp @@ -278,6 +278,33 @@ static bool PublishFrameBuffers(VideocodecCtx &vctx, u32 structAddr, int width, return true; } +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; + } + const int width = found->frameBufferWidth, height = found->frameBufferHeight; + const int lumaLeft = ((width + 16) >> 5) * (height >> 1) * 16; + const int lumaRight = (width >> 5) * (height >> 1) * 16; + const int sizes[8] = { + lumaLeft, lumaRight, lumaLeft, lumaRight, + lumaLeft >> 1, lumaLeft >> 1, lumaRight >> 1, lumaRight >> 1, + }; + u32 addr = found->frameBuffers; + for (int i = 0; i < 8; i++) { + buffers[i] = addr; + addr += (sizes[i] + 63) & ~63; + } + 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 diff --git a/Core/HLE/sceVideocodec.h b/Core/HLE/sceVideocodec.h index fbb06e9f11..37486744b7 100644 --- a/Core/HLE/sceVideocodec.h +++ b/Core/HLE/sceVideocodec.h @@ -31,3 +31,7 @@ void Register_sceVideocodec(); // 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]); From 00770764a73bd50df0e8edef6d2d74777c9bc155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 7 Sep 2026 17:42:01 -0600 Subject: [PATCH 05/14] Run the real mpeg.prx in place of our sceMpeg HLE Add flash0:/kd/mpeg.prx to the sceUtility module swap, hung off av_mpegbase, so it loads when DisableHLEFlags::sceMpeg is set. Games that ship their own sceMpeg_library on the disc never get here and just use theirs. The piece that was missing to make it work: the access unit address mpeg.prx passes to sceVideocodecDecode is in Media Engine space, which we can't read. On hardware sceMpegBasePESpacketCopy DMA'd the payload there first. That copy is ours, so it now gathers the blocks and sceVideocodec decodes from those, falling back to main memory for any caller that points at it directly. The gather is keyed by destination, because that call carries the audio payload too - video to an ME address, audio to main memory - and handing an ATRAC packet to the H.264 decoder gets you nothing. With that, video/mpeg/basic gets a 144x80 frame out of real Sony code driving our sceVideocodec through ffmpeg. The test still fails overall, as it did before this branch - it is in tests_next. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/sceMpeg.cpp | 10 ++++++++++ Core/HLE/sceMpegbase.cpp | 40 +++++++++++++++++++++++++++++++++++++- Core/HLE/sceMpegbase.h | 9 +++++++++ Core/HLE/sceUtility.cpp | 19 +++++++++++++++--- Core/HLE/sceVideocodec.cpp | 24 +++++++++++++++++++---- 5 files changed, 94 insertions(+), 8 deletions(-) diff --git a/Core/HLE/sceMpeg.cpp b/Core/HLE/sceMpeg.cpp index 9ca6ef58c9..a31617efe0 100644 --- a/Core/HLE/sceMpeg.cpp +++ b/Core/HLE/sceMpeg.cpp @@ -26,6 +26,7 @@ #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" @@ -348,6 +349,15 @@ private: }; void __MpegInit() { + // We used to load flash0's mpeg.prx here, before the game module, on the theory that a game + // calling sceMpegInit without asking sceUtility for the AV module first would otherwise find + // nothing to resolve against. In practice a game either ships its own sceMpeg_library on the + // disc (Death Jr., Pursuit Force) or asks sceUtility (Thrillville), and both of those paths + // arrive in time on their own - while loading it here costs 33KB at the top of user memory + // for nothing. Stacks allocate from the top, so that pushed Pursuit Force's main thread stack + // down by exactly that much and left the largest free block just under the 256KB it wanted, + // which failed the boot outright. If a game turns up that really does need it early, make the + // load conditional on the game not providing its own rather than bringing this back. __MpegBaseInit(); isMpegInit = false; mpegLibVersion = 0x010A; diff --git a/Core/HLE/sceMpegbase.cpp b/Core/HLE/sceMpegbase.cpp index 7fe02ae97d..cfcac524b3 100644 --- a/Core/HLE/sceMpegbase.cpp +++ b/Core/HLE/sceMpegbase.cpp @@ -20,6 +20,7 @@ // mpeg.prx drives these directly, so they have to be real for the firmware module to run in place // of our sceMpeg HLE. +#include #include #include "Common/Swap.h" @@ -31,12 +32,18 @@ #include "Core/MemMapHelpers.h" #include "GPU/ge_constants.h" +// The PES payloads gathered by sceMpegBasePESpacketCopy, keyed by the destination each was +// copied to. It carries audio as well as video - the destination is what tells them apart - so +// sceVideocodec has to ask for the one matching the address it was handed. +static std::map> g_pesPackets; + // Set by sceMpegBaseCscInit / sceMpegBaseCscSetPixelMode, and used when the caller passes 0. static int g_mpegBaseBufferWidth = 512; static int g_mpegBasePixelMode = GE_CMODE_32BIT_ABGR8888; void __MpegBaseInit() { // None of this survives a boot on hardware. + g_pesPackets.clear(); g_mpegBaseBufferWidth = 512; g_mpegBasePixelMode = GE_CMODE_32BIT_ABGR8888; } @@ -56,10 +63,41 @@ static u32 sceMpegBasePESpacketCopy(u32 p) } MpegSetPmpVideoSource(p, nBlocks); - DEBUG_LOG(Log::Mpeg, "sceMpegBasePESpacketCopy(%08x), received %d block(s)", 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 MpegBaseGetPESPacket. + lli = PSPPointer::Create(p); + u32 dest = 0; + std::vector gathered; + for (int i = 0; i < nBlocks && lli.IsValid(); i++) { + if (i == 0) { + dest = lli->pDst; + } + // The list is game-supplied, so check the span before taking a pointer to it: the range + // accessors raise a memory exception rather than returning null, and a malformed block + // shouldn't fault the game when the intent here is to skip it. + const u8 *src = (lli->iSize > 0 && Memory::IsValidRange(lli->pSrc, lli->iSize)) + ? Memory::GetTypedPointerRange(lli->pSrc, lli->iSize) : nullptr; + if (src) { + gathered.insert(gathered.end(), src, src + lli->iSize); + } + ++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; } +const std::vector *MpegBaseGetPESPacket(u32 dest) { + auto it = g_pesPackets.find(dest); + return it == g_pesPackets.end() ? nullptr : &it->second; +} + // --- sceMpegbase colour conversion --------------------------------------------------------- // diff --git a/Core/HLE/sceMpegbase.h b/Core/HLE/sceMpegbase.h index 98fbad71bd..50334d5d77 100644 --- a/Core/HLE/sceMpegbase.h +++ b/Core/HLE/sceMpegbase.h @@ -17,7 +17,16 @@ #pragma once +#include + +#include "Common/CommonTypes.h" + void Register_sceMpegbase(); // Called per boot, from __MpegInit. void __MpegBaseInit(); + +// 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. +// Returns nullptr if nothing was copied to that address. +const std::vector *MpegBaseGetPESPacket(u32 dest); diff --git a/Core/HLE/sceUtility.cpp b/Core/HLE/sceUtility.cpp index 3f16d40a3d..21be540051 100644 --- a/Core/HLE/sceUtility.cpp +++ b/Core/HLE/sceUtility.cpp @@ -137,11 +137,23 @@ static void LoadFirmwareModules(const char *library, const FirmwareModule *modul } } -// libmp3.prx imports nothing but the kernel and sceAudiocodec, which we have. -static void NotifyLoadStatusMp3(int state, u32 loadAddr, u32 totalSize) { +// mpeg.prx needs sceVideocodec, sceMpegbase and sceAudiocodec from us, all of which we implement, +// so the module itself is the only thing that has to come from somewhere real. +static void NotifyLoadStatusMpegBase(int state, u32 loadAddr, u32 totalSize) { // The effective flags, not the raw setting: those also account for the compat flags, for a // firmware dump that isn't there, and for the boundary a savestate restored - resolving // imports one way and loading modules the other is how a game ends up with neither. + if (state != 1 || !(GetEffectiveDisableHLEFlags() & DisableHLEFlags::sceMpeg)) { + return; + } + static const FirmwareModule modules[] = { + { "flash0:/kd/mpeg.prx", "sceMpeg_library" }, + }; + LoadFirmwareModules("sceMpeg", modules, ARRAY_SIZE(modules)); +} + +// libmp3.prx imports nothing but the kernel and sceAudiocodec, which we have. +static void NotifyLoadStatusMp3(int state, u32 loadAddr, u32 totalSize) { if (state != 1 || !(GetEffectiveDisableHLEFlags() & DisableHLEFlags::sceMp3)) { return; } @@ -203,6 +215,7 @@ static void NotifyLoadStatusAtrac(int state, u32 loadAddr, u32 totalSize) { static DisableHLEFlags UtilityModuleLibraryFlag(u32 module) { switch (module) { case 0x302: return DisableHLEFlags::sceAtrac; // av_atrac3plus + case 0x303: return DisableHLEFlags::sceMpeg; // av_mpegbase case 0x304: return DisableHLEFlags::sceMp3; // av_mp3 case 0x308: return DisableHLEFlags::sceMp4; // av_mp4 default: return (DisableHLEFlags)0; @@ -235,7 +248,7 @@ static const ModuleLoadInfo moduleLoadInfo[] = { // The size varies a bit per version, from about 0x3C00 to 0x4500 bytes. We could make a lookup table... // Changing this breaks some bad cheats though.. ModuleLoadInfo(0x302, 0x00008000, "av_atrac3plus", atrac3PlusModuleDeps, &NotifyLoadStatusAtrac), - ModuleLoadInfo(0x303, 0x0000c000, "av_mpegbase", mpegBaseModuleDeps), + ModuleLoadInfo(0x303, 0x0000c000, "av_mpegbase", mpegBaseModuleDeps, &NotifyLoadStatusMpegBase), ModuleLoadInfo(0x304, 0x00004000, "av_mp3", &NotifyLoadStatusMp3), ModuleLoadInfo(0x305, 0x0000a300, "av_vaudio"), ModuleLoadInfo(0x306, 0x00004000, "av_aac"), diff --git a/Core/HLE/sceVideocodec.cpp b/Core/HLE/sceVideocodec.cpp index dfcfa120d7..15c83f5e08 100644 --- a/Core/HLE/sceVideocodec.cpp +++ b/Core/HLE/sceVideocodec.cpp @@ -35,6 +35,8 @@ #include "Core/HLE/HLE.h" #include "Core/HLE/FunctionWrappers.h" #include "Core/HLE/sceVideocodec.h" +#include "Core/HLE/sceMpeg.h" +#include "Core/HLE/sceMpegbase.h" #include "Core/Util/BlockAllocator.h" #include "Core/HW/AvcDecoder.h" #include "Core/MemMap.h" @@ -473,13 +475,27 @@ static int sceVideocodecDecode(u32 ctxAddr, int type) { 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; if (auSize > 0 && Memory::IsValidRange(auAddr, auSize)) { - const u8 *au = Memory::GetTypedPointerRange(auAddr, auSize); - if (au) { - gotFrame = vctx.decoder->Decode(au, auSize); + au = Memory::GetTypedPointerRange(auAddr, auSize); + auBytes = auSize; + } else { + // Ask for the payload copied to this exact address - the same call carries audio too. + const std::vector *pes = MpegBaseGetPESPacket(auAddr); + if (pes && !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; @@ -533,7 +549,7 @@ static int sceVideocodecDecode(u32 ctxAddr, int type) { } return hleLogDebug(Log::ME, 0, "type %d, %d bytes -> %s %dx%d", - type, auSize, gotFrame ? "frame" : "no frame yet", width, height); + type, auBytes, gotFrame ? "frame" : "no frame yet", width, height); } static int sceVideocodecStop(u32 ctxAddr, int type) { From 9c4173fe510b1f70c64a71ca8eeb975c4ea07ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 7 Sep 2026 17:58:41 -0600 Subject: [PATCH 06/14] sceAudiocodec: drop a format check CheckNeedMem doesn't actually do avcodec.prx's sceAudiocodecCheckNeedMem validates the codec id and the pointer, sets the magic and forwards to the ME. It never looks at the format bytes, and the caller isn't obliged to have filled them in yet - the real mpeg.prx calls it with both still zero, and our invented check stopped its audio dead with SCE_AVCODEC_ERROR_INVALID_DATA. Now just a warning when they aren't the pair libatrac3plus writes, which is still worth knowing about. Found by running the real mpeg.prx against our sceAudiocodec, which is exactly what that reference is for. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/sceAudiocodec.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Core/HLE/sceAudiocodec.cpp b/Core/HLE/sceAudiocodec.cpp index 49782e5232..60e9ed883d 100644 --- a/Core/HLE/sceAudiocodec.cpp +++ b/Core/HLE/sceAudiocodec.cpp @@ -394,9 +394,13 @@ static int sceAudiocodecCheckNeedMem(u32 ctxPtr, int codec) { switch (codec) { case 0x1000: ctx->neededMem = 0x7bc0; + // avcodec.prx does no format check here at all - it just forwards to the ME - and the + // caller isn't obliged to have filled these in yet. mpeg.prx calls this with both bytes + // still zero, and rejecting that stopped its audio dead. Only worth a note when they + // aren't the pair libatrac3plus writes. 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); + WARN_LOG(Log::ME, "sceAudiocodecCheckNeedMem: unfamiliar Atrac3+ format bytes %02x %02x", + ctx->fmt.at3.formatByte1, ctx->fmt.at3.formatByte2); } break; case 0x1001: From ad7cfefd49c23d6e510933580690d0606033d036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 7 Sep 2026 21:30:30 -0600 Subject: [PATCH 07/14] sceMpegbase/sceAudiocodec: make audio work with the real mpeg.prx Three bugs stacked on top of each other, all of them silent - the decoder happily returned 2048 samples of digital silence 878 times per movie. sceMpegBasePESpacketCopy gathered the scatter-gather blocks but never performed the copy. That was fine for video, whose destination is Media Engine memory we can't write anyway and which we intercept instead, but audio is copied into main memory and mpeg.prx then hands that same address to sceAudiocodecDecode as its input. It was reading a buffer nothing had written. With real data arriving, the Atrac3+ frames turned out to still carry their 8-byte PSMF header - libatrac3plus.prx strips it before calling us, mpeg.prx leaves it on for the hardware to parse. Detect the 0x0FD0 sync word and step over it, taking the frame size from the header the way MpegDemux already does on the HLE path. Finally, rebuild the decoder if the frame size only becomes known at the first decode: the context has no size in it at init time, and a decoder built for a zero-byte frame decodes nothing. Also corrects a comment claiming mpeg.prx passes zeroed Atrac3+ format bytes. It doesn't - they were zero because of the missing copy above. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/sceAudiocodec.cpp | 68 ++++++++++++++++++++++++++++++++------ Core/HLE/sceMpegbase.cpp | 7 ++++ 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/Core/HLE/sceAudiocodec.cpp b/Core/HLE/sceAudiocodec.cpp index 60e9ed883d..8d47834eaa 100644 --- a/Core/HLE/sceAudiocodec.cpp +++ b/Core/HLE/sceAudiocodec.cpp @@ -31,6 +31,10 @@ // g_audioDecoderContexts is to store current playing audios. std::map g_audioDecoderContexts; +// The Atrac3+ frame size each decoder in the map above was created for. mpeg.prx doesn't put the +// frame size in the context, and at init time the input buffer is still empty, so for that path we +// only learn it from the first frame - at which point the decoder has to be rebuilt to match. +static std::map g_at3PlusFrameBytes; static bool oldStateLoaded = false; @@ -108,13 +112,38 @@ static_assert(offsetof(SceAudiocodecCodec, allocMem) == 0x68); // 0x28 is not this frame's size - libmp3.prx writes 0x5A1 there once, the largest an MP3 frame // can ever be. Output is 0x1200 bytes for MPEG1 (1152 samples) and 0x900 otherwise (576). -void CalculateInputBytesAndChannelsAt3Plus(const SceAudiocodecCodec *ctx, int *inputBytes, int *channels) { +void CalculateInputBytesAndChannelsAt3Plus(const SceAudiocodecCodec *ctx, int *inputBytes, int *channels, int *headerBytes = nullptr) { *inputBytes = 0; *channels = 2; + if (headerBytes) { + *headerBytes = 0; + } - int size = ctx->fmt.at3.formatByte2 * 8 + 8; + u8 formatByte1 = ctx->fmt.at3.formatByte1; + u8 formatByte2 = ctx->fmt.at3.formatByte2; + + // Atrac3+ frames inside a PSMF still carry their 8-byte header, starting with the 0x0FD0 sync + // word; libatrac3plus.prx strips it before handing the frame over, mpeg.prx leaves it on for + // the hardware to parse. So when the sync word is still there, take the size from the frame + // and step over the header, exactly as MpegDemux does on the HLE path. Bytes 2 and 3 are the + // same pair that ends up in the context, but with two more size bits in the first of them. + const u8 *frame = Memory::IsValidRange(ctx->inBuf, 4) ? Memory::GetPointerUnchecked(ctx->inBuf) : nullptr; + if (frame && frame[0] == 0x0F && frame[1] == 0xD0) { + formatByte1 = frame[2]; + formatByte2 = frame[3]; + if (headerBytes) { + *headerBytes = 8; + } + // The full size, unlike the context's, has two more high bits in the first byte. The + // 0x10 is the header plus the 8 the context's own formula adds. + *channels = (formatByte1 & 8) ? 2 : 1; + *inputBytes = (((formatByte1 & 0x03) << 8) | (formatByte2 * 8)) + 0x10 - 8; + return; + } + + int size = formatByte2 * 8 + 8; // No idea if this is accurate, this is just a guess... - if (ctx->fmt.at3.formatByte1 & 8) { + if (formatByte1 & 8) { *channels = 2; } else { *channels = 1; @@ -180,6 +209,7 @@ static bool removeDecoder(u32 ctxPtr) { if (it != g_audioDecoderContexts.end()) { delete it->second; g_audioDecoderContexts.erase(it); + g_at3PlusFrameBytes.erase(ctxPtr); return true; } return false; @@ -190,6 +220,7 @@ static void clearDecoders() { delete decoder; } g_audioDecoderContexts.clear(); + g_at3PlusFrameBytes.clear(); } void __AudioCodecInit() { @@ -262,6 +293,7 @@ static int __AudioCodecInitCommon(u32 ctxPtr, int codec, bool mono) { AudioDecoder *decoder = CreateAudioDecoder(audioType, 44100, channels, bytesPerFrame, extraData, sizeof(extraData)); decoder->SetCtxPtr(ctxPtr); g_audioDecoderContexts[ctxPtr] = decoder; + g_at3PlusFrameBytes[ctxPtr] = bytesPerFrame; return hleLogDebug(Log::ME, 0); } @@ -291,10 +323,11 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) { int bytesPerFrame = 0; int channels = 2; int sampleRate = 0; + int headerBytes = 0; switch (codec) { case PSP_CODEC_AT3PLUS: - CalculateInputBytesAndChannelsAt3Plus(ctx, &bytesPerFrame, &channels); + CalculateInputBytesAndChannelsAt3Plus(ctx, &bytesPerFrame, &channels, &headerBytes); break; case PSP_CODEC_MP3: // Not srcBytesRead - that's an output field holding what the *previous* call consumed. @@ -328,6 +361,19 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) { g_audioDecoderContexts[ctxPtr] = decoder; } + if (decoder && codec == PSP_CODEC_AT3PLUS && bytesPerFrame > 0) { + auto it = g_at3PlusFrameBytes.find(ctxPtr); + if (it == g_at3PlusFrameBytes.end() || it->second != bytesPerFrame) { + // Only reachable when the context didn't carry a frame size at init - mpeg.prx. + INFO_LOG(Log::ME, "sceAudiocodecDecode: Atrac3+ frame is %04x bytes, rebuilding decoder", bytesPerFrame); + removeDecoder(ctxPtr); + decoder = CreateAudioDecoder(audioType, 44100, channels, bytesPerFrame); + decoder->SetCtxPtr(ctxPtr); + g_audioDecoderContexts[ctxPtr] = decoder; + g_at3PlusFrameBytes[ctxPtr] = bytesPerFrame; + } + } + if (decoder) { // Use SimpleAudioDec to decode audio // Decode audio @@ -338,13 +384,13 @@ 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); + bool result = decoder->Decode(Memory::GetPointerOrException(ctx->inBuf + headerBytes), 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); @@ -395,11 +441,11 @@ static int sceAudiocodecCheckNeedMem(u32 ctxPtr, int codec) { case 0x1000: ctx->neededMem = 0x7bc0; // avcodec.prx does no format check here at all - it just forwards to the ME - and the - // caller isn't obliged to have filled these in yet. mpeg.prx calls this with both bytes - // still zero, and rejecting that stopped its audio dead. Only worth a note when they - // aren't the pair libatrac3plus writes. - if (ctx->fmt.at3.formatByte1 != 0x28 || ctx->fmt.at3.formatByte2 != 0x5c) { - WARN_LOG(Log::ME, "sceAudiocodecCheckNeedMem: unfamiliar Atrac3+ format bytes %02x %02x", + // caller isn't obliged to have filled these in yet, so this stays a note. libatrac3plus + // writes 28 5c (the worst case it sizes EDRAM against); mpeg.prx writes the real frame's + // own header bytes, so anything with bit 3 of the first byte is ordinary. + if (ctx->fmt.at3.formatByte1 != 0x28 && ctx->fmt.at3.formatByte1 != 0x24) { + DEBUG_LOG(Log::ME, "sceAudiocodecCheckNeedMem: unfamiliar Atrac3+ format bytes %02x %02x", ctx->fmt.at3.formatByte1, ctx->fmt.at3.formatByte2); } break; diff --git a/Core/HLE/sceMpegbase.cpp b/Core/HLE/sceMpegbase.cpp index cfcac524b3..bd96e28654 100644 --- a/Core/HLE/sceMpegbase.cpp +++ b/Core/HLE/sceMpegbase.cpp @@ -81,6 +81,13 @@ static u32 sceMpegBasePESpacketCopy(u32 p) ? Memory::GetTypedPointerRange(lli->pSrc, lli->iSize) : nullptr; if (src) { gathered.insert(gathered.end(), src, src + lli->iSize); + // Audio payloads land in main memory, and mpeg.prx hands that same address to + // sceAudiocodecDecode as its input - so for those the copy has to really happen. + // Video goes to a Media Engine address that isn't mapped for us; the gather above + // is what stands in for it there. + if (Memory::IsValidRange(lli->pDst, lli->iSize)) { + Memory::MemcpyUnchecked(lli->pDst, src, lli->iSize); + } } ++lli; } From 9dfce3ec2f7f86d10788de4754e8f358a71c2c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 12 Sep 2026 11:49:06 -0600 Subject: [PATCH 08/14] ImDebugger: list the sceVideocodec contexts alongside sceAudiocodec Same shape as the audio table: one row per open context, with its EDRAM block and frame buffer allocation. Both are in ME memory, so the addresses shown are in that space, not in PSP RAM. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/sceVideocodec.cpp | 18 ++++++++++++++++++ Core/HLE/sceVideocodec.h | 28 ++++++++++++++++++++++++++++ UI/ImDebugger/ImDebugger.cpp | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/Core/HLE/sceVideocodec.cpp b/Core/HLE/sceVideocodec.cpp index 15c83f5e08..48482d1905 100644 --- a/Core/HLE/sceVideocodec.cpp +++ b/Core/HLE/sceVideocodec.cpp @@ -280,6 +280,24 @@ static bool PublishFrameBuffers(VideocodecCtx &vctx, u32 structAddr, int width, return true; } +void VideocodecGetCtxInfo(std::vector *infos) { + infos->clear(); + for (const auto &[addr, ctx] : g_videocodecCtxs) { + VideocodecCtxInfo info; + info.ctxAddr = addr; + info.type = ctx.type; + info.hasDecoder = ctx.decoder != nullptr; + info.frameCount = ctx.frameCount; + 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; diff --git a/Core/HLE/sceVideocodec.h b/Core/HLE/sceVideocodec.h index 37486744b7..0bb19e3216 100644 --- a/Core/HLE/sceVideocodec.h +++ b/Core/HLE/sceVideocodec.h @@ -17,6 +17,8 @@ #pragma once +#include + #include "Common/CommonTypes.h" class PointerWrap; @@ -27,11 +29,37 @@ void __VideocodecDoState(PointerWrap &p); void Register_sceVideocodec(); +// The state of each open decoder, for the debugger. A game can have several - Silent Hill Origins +// runs one context for the EDRAM and another for the decoding. +struct VideocodecCtxInfo { + u32 ctxAddr; + int type; + bool hasDecoder; + int frameCount; + // The token sceVideocodecGetEDRAM handed back, and how much it stands for. Not an address - + // the block is ours, not the game's, the way the Media Engine's memory is on hardware. + u32 edramToken; + u32 edramSize; + u32 frameBuffers; + u32 frameBuffersSize; + int width; + int height; +}; +void VideocodecGetCtxInfo(std::vector *infos); + // A host pointer into the Media Engine's memory, or null if the range isn't in it. The frame // buffers and the EDRAM block both live there, so anything reading them goes through this rather // than through Memory:: - the ME's memory is not part of PSP RAM. u8 *VideocodecMEPointer(u32 addr, u32 size); + // mpeg.prx copies only the four luma buffers into the descriptor it hands sceMpegBaseCscAvc. // Both ends of that are ours, so the conversion can recover the other four from the allocation // they came from. Returns false if `firstBuffer` isn't one we handed out. bool VideocodecGetFrameBuffers(u32 firstBuffer, u32 buffers[8]); + +// How the eight buffers a frame is delivered in are sized and laid out, in the order the +// descriptor lists them: four luma (left/right half of a 32-pixel band, even/odd rows) then four +// chroma. Everything that writes, reads or allocates them has to agree, so it lives in one place. +// `offsets` is each buffer's start within a single allocation, 64-byte aligned; either array may +// be null. Returns the total allocation size. +u32 VideocodecFrameBufferLayout(int width, int height, int sizes[8], u32 offsets[8]); diff --git a/UI/ImDebugger/ImDebugger.cpp b/UI/ImDebugger/ImDebugger.cpp index d0e2850da7..634d521db1 100644 --- a/UI/ImDebugger/ImDebugger.cpp +++ b/UI/ImDebugger/ImDebugger.cpp @@ -47,6 +47,7 @@ #include "Core/HLE/sceAtrac.h" #include "Core/HLE/sceAudio.h" #include "Core/HLE/sceAudiocodec.h" +#include "Core/HLE/sceVideocodec.h" #include "Core/HLE/sceMp3.h" #include "Core/HLE/AtracCtx.h" #include "Core/HLE/sceSas.h" @@ -1577,6 +1578,39 @@ void DrawMediaDecodersView(ImConfig &cfg, ImControl &control) { } } + std::vector videoCtxs; + VideocodecGetCtxInfo(&videoCtxs); + if (ImGui::CollapsingHeaderWithCount("sceVideocodec", (int)videoCtxs.size(), ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::BeginTable("videocodecs", 7, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersH)) { + ImGui::TableSetupColumn("CtxAddr", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Frames", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Decoder", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("EDRAM", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("Frame buffers", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableHeadersRow(); + for (const VideocodecCtxInfo &ctx : videoCtxs) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%08x", ctx.ctxAddr); + ImGui::TableNextColumn(); + ImGui::Text("H.264 (%d)", ctx.type); + ImGui::TableNextColumn(); + ImGui::Text("%dx%d", ctx.width, ctx.height); + ImGui::TableNextColumn(); + ImGui::Text("%d", ctx.frameCount); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(ctx.hasDecoder ? "open" : "-"); + ImGui::TableNextColumn(); + ImGui::Text("%08x (%d)", ctx.edramToken, ctx.edramSize); + ImGui::TableNextColumn(); + ImGui::Text("%08x (%d)", ctx.frameBuffers, ctx.frameBuffersSize); + } + ImGui::EndTable(); + } + } + ImGui::End(); } From 779f9eb81107b1ff5692770f8d8391cfb94ecace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 9 Sep 2026 13:53:38 -0600 Subject: [PATCH 09/14] sceAudiocodec: check the whole Atrac3+ frame is readable before decoding it For a frame that still has its PSMF header, the length comes out of the header itself, so it's only as trustworthy as the stream - up to 2816 bytes read from a pointer where only the first four had been checked. Validate the span, and use IsValidRange rather than a range accessor, which raises a memory exception instead of returning null. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/sceAudiocodec.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Core/HLE/sceAudiocodec.cpp b/Core/HLE/sceAudiocodec.cpp index 8d47834eaa..fa07931d6e 100644 --- a/Core/HLE/sceAudiocodec.cpp +++ b/Core/HLE/sceAudiocodec.cpp @@ -384,7 +384,20 @@ static int sceAudiocodecDecode(u32 ctxPtr, int codec) { int16_t *outBuf = (int16_t *)Memory::GetPointerWriteOrException(ctx->outBuf); - bool result = decoder->Decode(Memory::GetPointerOrException(ctx->inBuf + headerBytes), 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); From 435d446c378f61f2f6b9617b73d25d0075964ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 9 Sep 2026 14:01:13 -0600 Subject: [PATCH 10/14] sceVideocodec/sceMpegbase: stop holding on to contexts and payloads that are done Decode looked its context up with operator[], so a game that never opened one would still get an entry and a decoder, and only Delete ever removes those. Require the context to exist instead. The gathered PES payloads were likewise kept for the whole boot, so the first decode of a movie could be handed the last packet of the previous one if the address came round again. Hand each payload out once. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/sceMpegbase.cpp | 14 ++++++++++---- Core/HLE/sceMpegbase.h | 9 +++++---- Core/HLE/sceVideocodec.cpp | 12 +++++++----- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Core/HLE/sceMpegbase.cpp b/Core/HLE/sceMpegbase.cpp index bd96e28654..82d5bcc0d7 100644 --- a/Core/HLE/sceMpegbase.cpp +++ b/Core/HLE/sceMpegbase.cpp @@ -36,7 +36,6 @@ // copied to. It carries audio as well as video - the destination is what tells them apart - so // sceVideocodec has to ask for the one matching the address it was handed. static std::map> g_pesPackets; - // Set by sceMpegBaseCscInit / sceMpegBaseCscSetPixelMode, and used when the caller passes 0. static int g_mpegBaseBufferWidth = 512; static int g_mpegBasePixelMode = GE_CMODE_32BIT_ABGR8888; @@ -66,7 +65,7 @@ static u32 sceMpegBasePESpacketCopy(u32 p) // 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 MpegBaseGetPESPacket. + // this - see MpegBaseTakePESPacket. lli = PSPPointer::Create(p); u32 dest = 0; std::vector gathered; @@ -100,9 +99,16 @@ static u32 sceMpegBasePESpacketCopy(u32 p) return 0; } -const std::vector *MpegBaseGetPESPacket(u32 dest) { +std::vector MpegBaseTakePESPacket(u32 dest) { auto it = g_pesPackets.find(dest); - return it == g_pesPackets.end() ? nullptr : &it->second; + if (it == g_pesPackets.end()) { + return std::vector(); + } + // Handed over, not lent: leaving it behind meant the first decode of the next movie could pick + // up the last packet of the previous one, if the address came round again. + std::vector packet = std::move(it->second); + g_pesPackets.erase(it); + return packet; } diff --git a/Core/HLE/sceMpegbase.h b/Core/HLE/sceMpegbase.h index 50334d5d77..e1898a667d 100644 --- a/Core/HLE/sceMpegbase.h +++ b/Core/HLE/sceMpegbase.h @@ -26,7 +26,8 @@ void Register_sceMpegbase(); // Called per boot, from __MpegInit. void __MpegBaseInit(); -// 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. -// Returns nullptr if nothing was copied to that address. -const std::vector *MpegBaseGetPESPacket(u32 dest); +// Takes the PES payload sceMpegBasePESpacketCopy gathered for a given destination. On hardware that +// copy lands in Media Engine memory, which sceVideocodec would then read back; we keep it here +// instead. The payload is moved out and dropped from the table, so each one is decoded once. +// Empty if nothing was copied to that address. +std::vector MpegBaseTakePESPacket(u32 dest); diff --git a/Core/HLE/sceVideocodec.cpp b/Core/HLE/sceVideocodec.cpp index 48482d1905..3c9315d985 100644 --- a/Core/HLE/sceVideocodec.cpp +++ b/Core/HLE/sceVideocodec.cpp @@ -500,15 +500,17 @@ static int sceVideocodecDecode(u32 ctxAddr, int type) { bool gotFrame = false; const u8 *au = nullptr; int auBytes = 0; + // Owns the gathered payload for as long as au points into it. + std::vector pes; if (auSize > 0 && Memory::IsValidRange(auAddr, auSize)) { au = Memory::GetTypedPointerRange(auAddr, auSize); auBytes = auSize; } else { - // Ask for the payload copied to this exact address - the same call carries audio too. - const std::vector *pes = MpegBaseGetPESPacket(auAddr); - if (pes && !pes->empty()) { - au = pes->data(); - auBytes = (int)pes->size(); + // 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) { From b4b8e2e7332c77e9aeb7d9df26c5e778974f9db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 9 Sep 2026 14:04:26 -0600 Subject: [PATCH 11/14] AvcDecoder: Check the pixel format AvcDecoder handed out frame_->data[0..2] without looking at the pixel format, and everything downstream indexes them as 8-bit 4:2:0. Check it, and drop the frame instead of converting whatever else turned up. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/HLETables.cpp | 1 - Core/HW/AvcDecoder.cpp | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Core/HLE/HLETables.cpp b/Core/HLE/HLETables.cpp index 44d672aebe..eddb1b028b 100644 --- a/Core/HLE/HLETables.cpp +++ b/Core/HLE/HLETables.cpp @@ -329,7 +329,6 @@ void RegisterAllModules() { Register_sceChkreg(); Register_sceVshBridge(); Register_sceResmgr(); - Register_sceVideocodec(); // add new modules here. diff --git a/Core/HW/AvcDecoder.cpp b/Core/HW/AvcDecoder.cpp index b87bb611be..568adf750f 100644 --- a/Core/HW/AvcDecoder.cpp +++ b/Core/HW/AvcDecoder.cpp @@ -155,6 +155,13 @@ bool AvcDecoder::Decode(const u8 *data, int size) { } #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; From 9422cb48999984637b6d882cae3840c867d9c548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 10 Sep 2026 17:21:23 -0600 Subject: [PATCH 12/14] sceMpegbase: save its state The output pixel mode decides both the colour packing and the bytes per pixel of the conversion, and a game sets it once per movie rather than per frame - so a state resumed mid-movie converted at the default until the next sceMpegBaseCscInit, which may never come. The gathered PES payloads go in too, since a state can land between the copy and the decode that consumes it. The section is optional (minimum version 0), so states written before it still load. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/sceKernel.cpp | 2 ++ Core/HLE/sceMpegbase.cpp | 17 +++++++++++++++++ Core/HLE/sceMpegbase.h | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/Core/HLE/sceKernel.cpp b/Core/HLE/sceKernel.cpp index aeb6fdb928..86241a7239 100644 --- a/Core/HLE/sceKernel.cpp +++ b/Core/HLE/sceKernel.cpp @@ -45,6 +45,7 @@ #include "sceAtrac.h" #include "sceAudio.h" #include "sceAudiocodec.h" +#include "sceMpegbase.h" #include "sceVideocodec.h" #include "sceCcc.h" #include "sceCtrl.h" @@ -287,6 +288,7 @@ void __KernelDoState(PointerWrap &p) __JpegDoState(p); __Mp3DoState(p); __MpegDoState(p); + __MpegBaseDoState(p); __VideocodecDoState(p); __NetDoState(p); __NetAdhocDoState(p); diff --git a/Core/HLE/sceMpegbase.cpp b/Core/HLE/sceMpegbase.cpp index 82d5bcc0d7..d1b91ac80b 100644 --- a/Core/HLE/sceMpegbase.cpp +++ b/Core/HLE/sceMpegbase.cpp @@ -23,6 +23,9 @@ #include #include +#include "Common/Serialize/Serializer.h" +#include "Common/Serialize/SerializeFuncs.h" +#include "Common/Serialize/SerializeMap.h" #include "Common/Swap.h" #include "Core/HLE/HLE.h" #include "Core/HLE/FunctionWrappers.h" @@ -47,6 +50,20 @@ void __MpegBaseInit() { 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) { diff --git a/Core/HLE/sceMpegbase.h b/Core/HLE/sceMpegbase.h index e1898a667d..6bf855d464 100644 --- a/Core/HLE/sceMpegbase.h +++ b/Core/HLE/sceMpegbase.h @@ -21,11 +21,15 @@ #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. From 6110a27b7360fbd679eebea37bbc520818127eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 12 Sep 2026 11:49:49 -0600 Subject: [PATCH 13/14] sceVideocodec: one layout helper for the eight frame buffers Four places were each computing the same sizes and 64-byte-aligned offsets: the allocation, the recovery sceMpegbase uses, and the readers on both sides. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/HLE.cpp | 4 +- Core/HLE/sceMpeg.cpp | 2 - Core/HLE/sceMpegbase.cpp | 15 +++---- Core/HLE/sceVideocodec.cpp | 86 +++++++++++++++++++++----------------- 4 files changed, 55 insertions(+), 52 deletions(-) diff --git a/Core/HLE/HLE.cpp b/Core/HLE/HLE.cpp index cdc7d40ed8..1e3566ed00 100644 --- a/Core/HLE/HLE.cpp +++ b/Core/HLE/HLE.cpp @@ -340,13 +340,13 @@ static void hleDelayResultFinish(u64 userdata, int cycleslate) { // Which files need to be present for a disable-hle-flag to be honoured. // // Two shapes end up here. sceMp4 because libmp4.prx and mp4msv.prx are firmware libraries no game -// ships, so without a dump there is nothing to run at all. sceFont because the module is on the +// ships, so without firmware there is nothing to run at all. sceFont because the module is on the // disc like any other but reads its fonts from flash0:/font with nothing to fall back on. Either // way the HLE is the only thing that can serve, so the flag comes off. // // sceMpeg, sceMp3 and sceAtrac are deliberately not here: plenty of discs carry their own copy // (Death Jr. has MPEG.PRX and LIBATRAC3PLUS.PRX under PSP_GAME/USRDIR/MODULES), and dropping the -// flag for want of a dump would replace a perfectly good disc module with our HLE. Those check for +// flag for want of firmware would replace a perfectly good disc module with our HLE. Those check for // a real module at the point they would load one, and warn there if neither source has it. static void CheckDisableHLEAvailability() { g_unavailableDisableFlags = (DisableHLEFlags)0; diff --git a/Core/HLE/sceMpeg.cpp b/Core/HLE/sceMpeg.cpp index a31617efe0..aee3d989fd 100644 --- a/Core/HLE/sceMpeg.cpp +++ b/Core/HLE/sceMpeg.cpp @@ -2329,5 +2329,3 @@ void Register_sceMpeg() { RegisterHLEModule("sceMpeg", ARRAY_SIZE(sceMpeg), sceMpeg); } - -// This function is currently only been used for PMP videos diff --git a/Core/HLE/sceMpegbase.cpp b/Core/HLE/sceMpegbase.cpp index d1b91ac80b..f091165df2 100644 --- a/Core/HLE/sceMpegbase.cpp +++ b/Core/HLE/sceMpegbase.cpp @@ -175,19 +175,16 @@ static bool ReadTiledYCbCr(const u32 *buffers, int width, int height, const int width2 = width >> 1; const int height2 = height >> 1; - // buffer0/2 take the odd band out when the width isn't a multiple of 32. - const int lumaSizeLeft = ((width + 16) >> 5) * (height >> 1) * 16; - const int lumaSizeRight = (width >> 5) * (height >> 1) * 16; - const int chromaSizeLeft = lumaSizeLeft >> 1; - const int chromaSizeRight = lumaSizeRight >> 1; + int sizes[8]; + VideocodecFrameBufferLayout(width, height, sizes, nullptr); + const int *ySize = sizes; + const int *cSize = sizes + 4; - const u8 *y[4] = {}; - const u8 *c[4] = {}; - const int ySize[4] = { lumaSizeLeft, lumaSizeRight, lumaSizeLeft, lumaSizeRight }; - const int cSize[4] = { chromaSizeLeft, chromaSizeLeft, chromaSizeRight, chromaSizeRight }; // 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]); diff --git a/Core/HLE/sceVideocodec.cpp b/Core/HLE/sceVideocodec.cpp index 3c9315d985..fdff36ebd7 100644 --- a/Core/HLE/sceVideocodec.cpp +++ b/Core/HLE/sceVideocodec.cpp @@ -35,9 +35,9 @@ #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/Util/BlockAllocator.h" #include "Core/HW/AvcDecoder.h" #include "Core/MemMap.h" #include "Core/MIPS/MIPS.h" @@ -173,9 +173,6 @@ void __VideocodecInit() { void __VideocodecShutdown() { ClearContexts(true); - g_meRam.clear(); - g_meRam.shrink_to_fit(); - g_meAlloc.Shutdown(); } void __VideocodecDoState(PointerWrap &p) { @@ -197,12 +194,12 @@ void __VideocodecDoState(PointerWrap &p) { Do(p, addr); Do(p, ctx.type); Do(p, ctx.frameCount); - Do(p, ctx.edram); Do(p, ctx.frameBuffers); Do(p, ctx.frameBuffersSize); Do(p, ctx.frameBufferWidth); Do(p, ctx.frameBufferHeight); - g_videocodecCtxs[addr] = ctx; + Do(p, ctx.edram); + g_videocodecCtxs[addr] = std::move(ctx); } } else { for (auto &[addr, ctx] : g_videocodecCtxs) { @@ -210,11 +207,11 @@ void __VideocodecDoState(PointerWrap &p) { Do(p, a); Do(p, ctx.type); Do(p, ctx.frameCount); - Do(p, ctx.edram); Do(p, ctx.frameBuffers); Do(p, ctx.frameBuffersSize); Do(p, ctx.frameBufferWidth); Do(p, ctx.frameBufferHeight); + Do(p, ctx.edram); } } @@ -224,21 +221,33 @@ void __VideocodecDoState(PointerWrap &p) { 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]) { - const int lumaLeft = ((width + 16) >> 5) * (height >> 1) * 16; - const int lumaRight = (width >> 5) * (height >> 1) * 16; - const int sizes[8] = { - lumaLeft, lumaRight, lumaLeft, lumaRight, - lumaLeft >> 1, lumaLeft >> 1, lumaRight >> 1, lumaRight >> 1, - }; - - u32 total = 0; - for (int i = 0; i < 8; i++) { - total += (sizes[i] + 63) & ~63; - } + u32 offsets[8]; + const u32 total = VideocodecFrameBufferLayout(width, height, nullptr, offsets); if (total == 0) { return false; } @@ -263,10 +272,8 @@ static bool PublishFrameBuffers(VideocodecCtx &vctx, u32 structAddr, int width, total, vctx.frameBuffers, width, height); } - u32 addr = vctx.frameBuffers; for (int i = 0; i < 8; i++) { - buffers[i] = addr; - addr += (sizes[i] + 63) & ~63; + buffers[i] = vctx.frameBuffers + offsets[i]; } if (!Memory::IsValidRange(structAddr, 48)) { @@ -288,6 +295,7 @@ void VideocodecGetCtxInfo(std::vector *infos) { 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; @@ -310,17 +318,10 @@ bool VideocodecGetFrameBuffers(u32 firstBuffer, u32 buffers[8]) { if (!found) { return false; } - const int width = found->frameBufferWidth, height = found->frameBufferHeight; - const int lumaLeft = ((width + 16) >> 5) * (height >> 1) * 16; - const int lumaRight = (width >> 5) * (height >> 1) * 16; - const int sizes[8] = { - lumaLeft, lumaRight, lumaLeft, lumaRight, - lumaLeft >> 1, lumaLeft >> 1, lumaRight >> 1, lumaRight >> 1, - }; - u32 addr = found->frameBuffers; + u32 offsets[8]; + VideocodecFrameBufferLayout(found->frameBufferWidth, found->frameBufferHeight, nullptr, offsets); for (int i = 0; i < 8; i++) { - buffers[i] = addr; - addr += (sizes[i] + 63) & ~63; + buffers[i] = found->frameBuffers + offsets[i]; } return true; } @@ -343,10 +344,10 @@ static void WriteTiledYCbCr(const u32 *buffers, const AvcDecoder &dec, int width return; } - const int lumaSizeLeft = ((width + 16) >> 5) * (height >> 1) * 16; - const int lumaSizeRight = (width >> 5) * (height >> 1) * 16; - const int ySize[4] = { lumaSizeLeft, lumaSizeRight, lumaSizeLeft, lumaSizeRight }; - const int cSize[4] = { lumaSizeLeft >> 1, lumaSizeLeft >> 1, lumaSizeRight >> 1, lumaSizeRight >> 1 }; + 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) { @@ -421,8 +422,7 @@ static int sceVideocodecInit(u32 ctxAddr, int type) { return hleLogInfo(Log::ME, 0, "type %d", type); } -// See g_meRam for why this doesn't come out of the game's memory. The firmware keeps the block in -// the caller's context and nowhere else, so we do too - see ppsspp-re, modules/sceVideocodec. +// 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"); @@ -527,12 +527,14 @@ static int sceVideocodecDecode(u32 ctxAddr, int type) { // 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); - out32(32, gotFrame ? 1 : 0); // images decoded - mpeg.prx won't convert without this - out32(36, gotFrame ? 0 : 1); } else { out32(OUT_DATA, auAddr); out32(OUT_SIZE, auSize); @@ -555,6 +557,7 @@ static int sceVideocodecDecode(u32 ctxAddr, int type) { 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"); @@ -568,6 +571,11 @@ static int sceVideocodecDecode(u32 ctxAddr, int type) { } } + 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); } From 10a9d7bb317892bbf14880503c7825c22a741a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 14 Sep 2026 10:35:32 -0600 Subject: [PATCH 14/14] De-claude some overly verbose comments --- Core/HLE/sceAudiocodec.cpp | 10 +++++----- Core/HLE/sceMpeg.cpp | 9 --------- Core/HLE/sceMpegbase.cpp | 18 +++++++----------- Core/HLE/sceVideocodec.cpp | 8 +++++--- Core/HW/AvcDecoder.h | 11 +++++------ 5 files changed, 22 insertions(+), 34 deletions(-) diff --git a/Core/HLE/sceAudiocodec.cpp b/Core/HLE/sceAudiocodec.cpp index fa07931d6e..5306340605 100644 --- a/Core/HLE/sceAudiocodec.cpp +++ b/Core/HLE/sceAudiocodec.cpp @@ -453,12 +453,12 @@ static int sceAudiocodecCheckNeedMem(u32 ctxPtr, int codec) { switch (codec) { case 0x1000: ctx->neededMem = 0x7bc0; - // avcodec.prx does no format check here at all - it just forwards to the ME - and the - // caller isn't obliged to have filled these in yet, so this stays a note. libatrac3plus - // writes 28 5c (the worst case it sizes EDRAM against); mpeg.prx writes the real frame's - // own header bytes, so anything with bit 3 of the first byte is ordinary. + // 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) { - DEBUG_LOG(Log::ME, "sceAudiocodecCheckNeedMem: unfamiliar Atrac3+ format bytes %02x %02x", + INFO_LOG(Log::ME, "sceAudiocodecCheckNeedMem: unfamiliar Atrac3+ format bytes %02x %02x", ctx->fmt.at3.formatByte1, ctx->fmt.at3.formatByte2); } break; diff --git a/Core/HLE/sceMpeg.cpp b/Core/HLE/sceMpeg.cpp index aee3d989fd..99c295a757 100644 --- a/Core/HLE/sceMpeg.cpp +++ b/Core/HLE/sceMpeg.cpp @@ -349,15 +349,6 @@ private: }; void __MpegInit() { - // We used to load flash0's mpeg.prx here, before the game module, on the theory that a game - // calling sceMpegInit without asking sceUtility for the AV module first would otherwise find - // nothing to resolve against. In practice a game either ships its own sceMpeg_library on the - // disc (Death Jr., Pursuit Force) or asks sceUtility (Thrillville), and both of those paths - // arrive in time on their own - while loading it here costs 33KB at the top of user memory - // for nothing. Stacks allocate from the top, so that pushed Pursuit Force's main thread stack - // down by exactly that much and left the largest free block just under the 256KB it wanted, - // which failed the boot outright. If a game turns up that really does need it early, make the - // load conditional on the game not providing its own rather than bringing this back. __MpegBaseInit(); isMpegInit = false; mpegLibVersion = 0x010A; diff --git a/Core/HLE/sceMpegbase.cpp b/Core/HLE/sceMpegbase.cpp index f091165df2..ecbca8e016 100644 --- a/Core/HLE/sceMpegbase.cpp +++ b/Core/HLE/sceMpegbase.cpp @@ -71,8 +71,8 @@ static u32 sceMpegBasePESpacketCopy(u32 p) auto lli = PSPPointer::Create(p); while (lli.IsValid()) { nBlocks++; - // lli.Next ==0 for last block - if (lli->Next == 0){ + if (lli->Next == 0) { + // Last block break; } ++lli; @@ -90,16 +90,14 @@ static u32 sceMpegBasePESpacketCopy(u32 p) if (i == 0) { dest = lli->pDst; } - // The list is game-supplied, so check the span before taking a pointer to it: the range - // accessors raise a memory exception rather than returning null, and a malformed block - // shouldn't fault the game when the intent here is to skip it. + // The list is game-supplied, so check the span validity before taking a pointer to it. const u8 *src = (lli->iSize > 0 && Memory::IsValidRange(lli->pSrc, lli->iSize)) ? Memory::GetTypedPointerRange(lli->pSrc, lli->iSize) : nullptr; if (src) { gathered.insert(gathered.end(), src, src + lli->iSize); // Audio payloads land in main memory, and mpeg.prx hands that same address to - // sceAudiocodecDecode as its input - so for those the copy has to really happen. - // Video goes to a Media Engine address that isn't mapped for us; the gather above + // 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); @@ -121,8 +119,6 @@ std::vector MpegBaseTakePESPacket(u32 dest) { if (it == g_pesPackets.end()) { return std::vector(); } - // Handed over, not lent: leaving it behind meant the first decode of the next movie could pick - // up the last packet of the previous one, if the address came round again. std::vector packet = std::move(it->second); g_pesPackets.erase(it); return packet; @@ -132,7 +128,7 @@ std::vector MpegBaseTakePESPacket(u32 dest) { // --- 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 is exactly the range mpegbase.prx bounds-checks before using 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 @@ -152,7 +148,7 @@ struct SceMp4AvcCscStruct { static_assert(sizeof(SceMp4AvcCscStruct) == 0x30); -// The ME doesn't write planar YCbCr. The layout below was established by analysing +// 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: // diff --git a/Core/HLE/sceVideocodec.cpp b/Core/HLE/sceVideocodec.cpp index fdff36ebd7..877b8c1635 100644 --- a/Core/HLE/sceVideocodec.cpp +++ b/Core/HLE/sceVideocodec.cpp @@ -99,13 +99,11 @@ static std::map g_videocodecCtxs; // The Media Engine's own 2MB of embedded DRAM, modelled as memory of ours. // -// The CPU cannot address it. mpeg.prx asks for a block with sceVideocodecGetEDRAM, keeps the value +// 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. // -// So none of it may come out of the game's partitions: taking it from user memory would push a -// game's own allocations around, and from kernel memory would spend memory a real PSP never does. // 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 @@ -184,6 +182,10 @@ void __VideocodecDoState(PointerWrap &p) { // 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) { diff --git a/Core/HW/AvcDecoder.h b/Core/HW/AvcDecoder.h index 35cf1f7078..4b088a042e 100644 --- a/Core/HW/AvcDecoder.h +++ b/Core/HW/AvcDecoder.h @@ -26,12 +26,10 @@ struct AVFrame; // A plain "hand it one access unit, get a frame back" H.264 decoder. // -// This exists for sceVideocodec, which is the interface the Media Engine really exposes: the -// caller owns the buffers and hands over one access unit at a time. 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 deliberately a second, separate path -// rather than a refactor of code every game that plays video depends on. Merging the two is -// worth doing once sceVideocodec has proven itself. +// 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 { @@ -39,6 +37,7 @@ public: AvcDecoder(); ~AvcDecoder(); + // Not copyable. AvcDecoder(const AvcDecoder &) = delete; AvcDecoder &operator=(const AvcDecoder &) = delete;