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) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5 authored and Henrik Rydgård committed 2026-09-12 12:53:13 -06:00
1 parent ba975d2908
commit 00770764a7
5 files changed
+94 -8

No files matched your search

+10
View File
@@ -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;
+39 -1
View File
@@ -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 <map>
#include <vector>
#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<u32, std::vector<u8>> g_pesPackets;
// Set by sceMpegBaseCscInit / sceMpegBaseCscSetPixelMode, and used when the caller passes 0.
static int g_mpegBaseBufferWidth = 512;
static int g_mpegBasePixelMode = GE_CMODE_32BIT_ABGR8888;
void __MpegBaseInit() {
// None of this survives a boot on hardware.
g_pesPackets.clear();
g_mpegBaseBufferWidth = 512;
g_mpegBasePixelMode = GE_CMODE_32BIT_ABGR8888;
}
@@ -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<SceMpegLLI>::Create(p);
u32 dest = 0;
std::vector<u8> gathered;
for (int i = 0; i < nBlocks && lli.IsValid(); i++) {
if (i == 0) {
dest = lli->pDst;
}
// The list is game-supplied, so check the span 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<u8>(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<u8> *MpegBaseGetPESPacket(u32 dest) {
auto it = g_pesPackets.find(dest);
return it == g_pesPackets.end() ? nullptr : &it->second;
}
// --- sceMpegbase colour conversion ---------------------------------------------------------
//
+9
View File
@@ -17,7 +17,16 @@
#pragma once
#include <vector>
#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<u8> *MpegBaseGetPESPacket(u32 dest);
+16 -3
View File
@@ -137,11 +137,23 @@ static void LoadFirmwareModules(const char *library, const FirmwareModule *modul
}
}
// libmp3.prx imports nothing but the kernel and sceAudiocodec, which we have.
static void NotifyLoadStatusMp3(int state, u32 loadAddr, u32 totalSize) {
// mpeg.prx needs sceVideocodec, sceMpegbase and sceAudiocodec from us, all of which we implement,
// so the module itself is the only thing that has to come from somewhere real.
static void NotifyLoadStatusMpegBase(int state, u32 loadAddr, u32 totalSize) {
// The effective flags, not the raw setting: those also account for the compat flags, for a
// firmware dump that isn't there, and for the boundary a savestate restored - resolving
// imports one way and loading modules the other is how a game ends up with neither.
if (state != 1 || !(GetEffectiveDisableHLEFlags() & DisableHLEFlags::sceMpeg)) {
return;
}
static const FirmwareModule modules[] = {
{ "flash0:/kd/mpeg.prx", "sceMpeg_library" },
};
LoadFirmwareModules("sceMpeg", modules, ARRAY_SIZE(modules));
}
// libmp3.prx imports nothing but the kernel and sceAudiocodec, which we have.
static void NotifyLoadStatusMp3(int state, u32 loadAddr, u32 totalSize) {
if (state != 1 || !(GetEffectiveDisableHLEFlags() & DisableHLEFlags::sceMp3)) {
return;
}
@@ -203,6 +215,7 @@ static void NotifyLoadStatusAtrac(int state, u32 loadAddr, u32 totalSize) {
static DisableHLEFlags UtilityModuleLibraryFlag(u32 module) {
switch (module) {
case 0x302: return DisableHLEFlags::sceAtrac; // av_atrac3plus
case 0x303: return DisableHLEFlags::sceMpeg; // av_mpegbase
case 0x304: return DisableHLEFlags::sceMp3; // av_mp3
case 0x308: return DisableHLEFlags::sceMp4; // av_mp4
default: return (DisableHLEFlags)0;
@@ -235,7 +248,7 @@ static const ModuleLoadInfo moduleLoadInfo[] = {
// The size varies a bit per version, from about 0x3C00 to 0x4500 bytes. We could make a lookup table...
// Changing this breaks some bad cheats though..
ModuleLoadInfo(0x302, 0x00008000, "av_atrac3plus", atrac3PlusModuleDeps, &NotifyLoadStatusAtrac),
ModuleLoadInfo(0x303, 0x0000c000, "av_mpegbase", mpegBaseModuleDeps),
ModuleLoadInfo(0x303, 0x0000c000, "av_mpegbase", mpegBaseModuleDeps, &NotifyLoadStatusMpegBase),
ModuleLoadInfo(0x304, 0x00004000, "av_mp3", &NotifyLoadStatusMp3),
ModuleLoadInfo(0x305, 0x0000a300, "av_vaudio"),
ModuleLoadInfo(0x306, 0x00004000, "av_aac"),
+20 -4
View File
@@ -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<u8>(auAddr, auSize);
if (au) {
gotFrame = vctx.decoder->Decode(au, auSize);
au = Memory::GetTypedPointerRange<u8>(auAddr, auSize);
auBytes = auSize;
} else {
// Ask for the payload copied to this exact address - the same call carries audio too.
const std::vector<u8> *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) {