Merge pull request #22252 from hrydgard/scemp4-real-prx

Run the real libmp4.prx/mp4msv.prx instead of our sceMp4 HLE, optionally. Fixes Speedball
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-09-08 10:54:50 -06:00
commit 1fc823681f
8 files changed
+144 -10

No files matched your search

+19 -7
View File
@@ -28,12 +28,16 @@ for it:
4. **Don't write code on `master`.** When asked to make a code change while on `master`, create an
appropriately named branch first (`git checkout -b some-descriptive-name`) and do the work there.
If you're already on a topic branch, just keep working on it.
5. **Most files in this repo are CRLF** - `.vcxproj`, `.vcxproj.filters`, `android/jni/Android.mk`,
`libretro/Makefile.common`, `AGENTS.md`, and much of the source. If you patch one with a script, read *and*
write with `newline=''`; reading with Python's default universal-newline translation and writing with
`newline=''` silently converts the whole file, turning a two-line addition into a 5000-line diff. Check
`git diff --stat` before committing - a whole-file rewrite is obvious there and invisible in the editor.
Prefer the Edit tool, which does exact string replacement and can't do this.
5. **Never assume a file's line endings - preserve whatever is on disk.** Which ending a file has
depends on where it was checked out: on Windows everything is auto-checked-out as CRLF, while a
Linux checkout leaves files as they are stored, so the same file (`.vcxproj`, `.vcxproj.filters`,
`android/jni/Android.mk`, `libretro/Makefile.common`, this file, much of the source) is CRLF in one
working copy and LF in another. Don't hardcode either, and don't "fix" a file's endings to match
what a doc claims. If you patch one with a script, read *and* write with `newline=''`, which keeps
whatever was there; reading with Python's default universal-newline translation and writing with
`newline=''` silently converts the whole file, turning a two-line addition into a 5000-line diff.
Check `git diff --stat` before committing - a whole-file rewrite is obvious there and invisible in
the editor. Prefer the Edit tool, which does exact string replacement and can't do this.
6. **Don't feed Python to `bash -c` via a heredoc when the code contains backslashes.** The Git Bash / MinGW
layer strips one level of backslash escaping on the way in, *even with a quoted delimiter* (`<<'PY'`), which
normally suppresses all substitution. So the script Python receives is not the one you wrote:
@@ -61,6 +65,13 @@ for it:
1. For HLE, CPU, GPU, timing, threading, and memory changes, call out regression risks explicitly.
2. Consider savestate compatibility when changing serialized state.
3. **Never insert an entry into the middle of an `HLEFunction` array.** A savestate stores the
syscall opcode, which encodes the entry's *index* in that array - so inserting anywhere but the
end silently repoints every later entry, and old savestates start calling the wrong function.
This applies to adding a *single* function to an *existing* module, which is when it is easiest
to forget: put it last in the array even when alphabetical or NID order would put it elsewhere,
and even when the array is otherwise tidily sorted. The same rule governs the order of
`Register_*()` calls in `Core/HLE/HLETables.cpp` - new modules go at the very end.
## Build and validation
@@ -174,7 +185,8 @@ things silently if ignored:
never inserted alphabetically among the existing `Register_*()` calls.
- **New entries in an existing module's function table go at the very end of that array too** - a
savestate captures the syscall opcode encoding the entry's array index, so shifting later entries
makes old savestates call the wrong function.
makes old savestates call the wrong function. See Core Safety Checks above: this holds for any
edit to any `HLEFunction` array, not just when adding a module.
Also: a new `.cpp`/`.c` file has to be added to **seven** build files (CMake, Core.vcxproj + filters,
the two UWP projects, `android/jni/Android.mk`, `libretro/Makefile.common`); headers to the first five.
+7
View File
@@ -217,6 +217,7 @@ static const CommandLineParam g_autoParams[] = {
{POFF(debuggerPort), CmdParamType::Int, "debugger", '\0', "Enable the WebSocket debugger on this port (0 = pick automatically); see docs/WebSocketDebugger.md"},
{POFF(autoSaveLoadSymbols), CmdParamType::Bool, "auto-save-load-symbols", '\0', "Auto save/load per-module and per-game symbol files (see bAutoSaveLoadSymbols)", CmdLineMode::Both},
{POFF(bootVSH), CmdParamType::Bool, "vsh", '\0', "Boot the VSH (requires files dumped from a PSP in the flash0 directory)"},
{POFF(disableHLE), CmdParamType::Int, "disable-hle", '\0', "Bitmask of libraries to run the real firmware module for instead of our HLE", CmdLineMode::Both},
{POFF(memReadAction), CmdParamType::Enum, "memread", '\0', "Set the action for memory read exceptions", CmdLineMode::Both, g_ExceptionActionValues, ARRAY_SIZE(g_ExceptionActionValues)},
{POFF(memWriteAction), CmdParamType::Enum, "memwrite", '\0', "Set the action for memory write exceptions", CmdLineMode::Both, g_ExceptionActionValues, ARRAY_SIZE(g_ExceptionActionValues)},
{POFF(breakAction), CmdParamType::Enum, "break", '\0', "Set the action for break exceptions", CmdLineMode::Both, g_ExceptionActionValues, ARRAY_SIZE(g_ExceptionActionValues)},
@@ -544,6 +545,12 @@ void CommandLineOptions::ApplyToConfig() const {
g_Config.DoNotSaveSetting(&g_Config.bAutoSaveLoadSymbols);
}
if (disableHLE.has_value()) {
// DoNotSaveSetting so a per-game config can't quietly put the HLE back.
g_Config.iDisableHLE = disableHLE.value();
g_Config.DoNotSaveSetting(&g_Config.iDisableHLE);
}
if (logLevel.has_value()) {
g_logManager.SetAllLogLevels(logLevel.value());
}
+4
View File
@@ -78,6 +78,10 @@ struct CommandLineOptions {
std::optional<std::string> unpackUpdaterModel;
std::optional<std::string> unpackUpdaterFilter;
// Bitmask of DisableHLEFlags: run the real firmware module instead of our HLE for those
// libraries. Needs a firmware dump under the NAND directory.
std::optional<int> disableHLE;
std::optional<int> memReadAction;
std::optional<int> memWriteAction;
std::optional<int> breakAction;
+3 -1
View File
@@ -164,7 +164,9 @@ enum class DisableHLEFlags : int {
sceMp3 = (1 << 5),
sceParseHttp = (1 << 6),
sceCcc = (1 << 7), // character conversion library.
Count = 8,
// Swaps in flash0:/kd/libmp4.prx and mp4msv.prx, which then decode through our sceAudiocodec.
sceMp4 = (1 << 8),
Count = 9,
// TODO: Some of the networking libraries may be interesting candidates, like HTTP.
};
ENUM_CLASS_BITOPS(DisableHLEFlags);
+32
View File
@@ -22,6 +22,9 @@
#include "Common/Math/CrossSIMD.h"
#include "Common/File/FileUtil.h"
#include "Common/Data/Text/I18n.h"
#include "Common/System/OSD.h"
#include "Common/Profiler/Profiler.h"
#include "Common/Log.h"
@@ -153,6 +156,9 @@ static const HLEModuleMeta g_moduleMeta[] = {
{"scePsmfPlayer", "scePsmfPlayer", DisableHLEFlags::scePsmfPlayer},
{"sceSAScore", "sceSasCore"},
{"sceCcc_Library", "sceCcc", DisableHLEFlags::sceCcc},
// libmp4.prx needs 41 functions from mp4msv.prx, so the two only make sense swapped together.
{"sceMp4_library", "sceMp4", DisableHLEFlags::sceMp4},
{"mp4msv_module", "mp4msv", DisableHLEFlags::sceMp4},
{"SceParseHTTPheader_Library", "sceParseHttp", DisableHLEFlags::sceParseHttp},
{"SceParseURI_Library"},
// Guessing these names
@@ -198,6 +204,12 @@ DisableHLEFlags AlwaysDisableHLEFlags() {
}
// Process compat flags.
// Flags the user asked for that we can't honour this boot, because the firmware modules they
// need aren't in the NAND directory. Subtracted in GetDisableHLEFlags so that a missing dump
// leaves the HLE in place rather than handing the game unresolved imports, which is much worse
// than our stubs. Recomputed per boot, since the dump can appear between runs.
static DisableHLEFlags g_unavailableDisableFlags = (DisableHLEFlags)0;
static DisableHLEFlags GetDisableHLEFlags() {
DisableHLEFlags flags = (DisableHLEFlags)g_Config.iDisableHLE | AlwaysDisableHLEFlags();
if (PSP_CoreParameter().compat.flags().DisableHLESceFont) {
@@ -208,6 +220,8 @@ static DisableHLEFlags GetDisableHLEFlags() {
}
flags &= ~(DisableHLEFlags)g_Config.iForceEnableHLE;
// Anything whose firmware module isn't actually present stays HLE'd.
flags &= ~g_unavailableDisableFlags;
return flags;
}
@@ -262,7 +276,25 @@ static void hleDelayResultFinish(u64 userdata, int cycleslate) {
WARN_LOG(Log::HLE, "Someone else woke up HLE-blocked thread %d?", threadID);
}
// Which firmware files a flag needs before it can be honoured.
static void CheckDisableHLEAvailability() {
g_unavailableDisableFlags = (DisableHLEFlags)0;
if ((DisableHLEFlags)g_Config.iDisableHLE & DisableHLEFlags::sceMp4) {
const Path kd = g_Config.nandRootDirectory / "flash0" / "kd";
if (!File::Exists(kd / "libmp4.prx") || !File::Exists(kd / "mp4msv.prx")) {
g_unavailableDisableFlags |= DisableHLEFlags::sceMp4;
ERROR_LOG(Log::HLE, "Asked to run the real sceMp4, but %s doesn't have libmp4.prx and "
"mp4msv.prx - keeping the HLE.", kd.c_str());
auto sy = GetI18NCategory(I18NCat::SYSTEM);
g_OSD.Show(OSDType::MESSAGE_WARNING,
sy->T("Real sceMp4 needs a firmware dump in the NAND folder - using HLE instead"), 6.0f);
}
}
}
void HLEInit() {
CheckDisableHLEAvailability();
RegisterAllModules();
g_stackSize = 0;
delayedResultEvent = CoreTiming::RegisterEvent("HLEDelayedResult", hleDelayResultFinish);
+29
View File
@@ -2713,6 +2713,34 @@ struct GetModuleIdByAddressArg
SceUID result;
};
// ModuleMgrForUser_D2FBC957. Looks up the gp value of whichever module contains an address, which
// is how a library that is handed function pointers from another module can call them: MIPS code
// needs the callee's gp in place. libmp4.prx uses it on the three callbacks it is given.
// Named after what it does; the official name isn't known.
static u32 sceKernelGetModuleGPByAddress(u32 addr, u32 gpPtr) {
if (!Memory::IsValidAddress(gpPtr)) {
return hleLogError(Log::sceModule, SCE_KERNEL_ERROR_ILLEGAL_ADDR, "bad gp pointer");
}
u32 gp = 0;
bool found = false;
kernelObjects.Iterate<PSPModule>([&](int id, PSPModule *module) -> bool {
const u32 start = module->memoryBlockAddr, size = module->memoryBlockSize;
if (start != 0 && start <= addr && start + size > addr) {
gp = module->nm.gp_value;
found = true;
return false;
}
return true;
});
if (!found) {
return hleLogError(Log::sceModule, SCE_KERNEL_ERROR_UNKNOWN_MODULE, "no module at %08x", addr);
}
Memory::WriteUnchecked_U32(gp, gpPtr);
return hleLogDebug(Log::sceModule, 0, "gp=%08x", gp);
}
static u32 sceKernelGetModuleIdByAddress(u32 moduleAddr)
{
GetModuleIdByAddressArg state;
@@ -3028,6 +3056,7 @@ const HLEFunction ModuleMgrForUser[] = {
{0XF2D8D1B4, &WrapU_CUU<sceKernelLoadModuleNpDrm>, "sceKernelLoadModuleNpDrm", 'x', "sxx" },
{0XE4C4211C, nullptr, "sceKernelLoadModuleWithBlockOffset", '?', "" },
{0XFBE27467, nullptr, "sceKernelLoadModuleByIDWithBlockOffset", '?', "" },
{0XD2FBC957, &WrapU_UU<sceKernelGetModuleGPByAddress>, "sceKernelGetModuleGPByAddress", 'x', "xx" },
};
const HLEFunction ModuleMgrForKernel[] = {
+44 -1
View File
@@ -85,6 +85,49 @@ static void NotifyLoadStatusAvcodec(int state, u32 loadAddr, u32 totalSize) {
JpegNotifyLoadStatus(state);
}
// The MP4 libraries are a good candidate for running the real thing: libmp4.prx needs only two
// functions from sceAudiocodec (Init and Decode) plus ordinary kernel calls, and mp4msv.prx - the
// 41 functions libmp4 leans on - imports nothing at all. So with a firmware dump present, the pair
// can be loaded for real and left to decode through our sceAudiocodec HLE.
static SceUID g_mp4RealModules[2] = { 0, 0 };
static void NotifyLoadStatusMp4(int state, u32 loadAddr, u32 totalSize) {
if (!((DisableHLEFlags)g_Config.iDisableHLE & DisableHLEFlags::sceMp4)) {
return;
}
if (state == 1) {
// mp4msv first - libmp4 imports from it, and an import can only resolve to a module that
// is already loaded.
static const char *const paths[2] = {
"flash0:/kd/mp4msv.prx",
"flash0:/kd/libmp4.prx",
};
for (int i = 0; i < 2; i++) {
if (g_mp4RealModules[i]) {
continue;
}
std::string error;
SceUID id = KernelLoadModule(paths[i], &error);
if (id < 0) {
ERROR_LOG(Log::sceUtility, "sceMp4 HLE is disabled, but %s wouldn't load (%s) - "
"the game will get unresolved imports", paths[i], error.c_str());
return;
}
int result = __KernelStartModule(id, 0, 0, 0, nullptr, nullptr);
if (result < 0) {
ERROR_LOG(Log::sceUtility, "Failed to start %s (%08x)", paths[i], result);
return;
}
g_mp4RealModules[i] = id;
INFO_LOG(Log::sceUtility, "Loaded the real %s", paths[i]);
}
} else if (state == -1) {
g_mp4RealModules[0] = 0;
g_mp4RealModules[1] = 0;
}
}
static void NotifyLoadStatusAtrac(int state, u32 loadAddr, u32 totalSize) {
if (state == 1) {
// If HLE of sceAtrac is disabled, things will break!
@@ -139,7 +182,7 @@ static const ModuleLoadInfo moduleLoadInfo[] = {
ModuleLoadInfo(0x305, 0x0000a300, "av_vaudio"),
ModuleLoadInfo(0x306, 0x00004000, "av_aac"),
ModuleLoadInfo(0x307, 0x00000000, "av_g729"),
ModuleLoadInfo(0x308, 0x0003c000, "av_mp4", mp4ModuleDeps),
ModuleLoadInfo(0x308, 0x0003c000, "av_mp4", mp4ModuleDeps, &NotifyLoadStatusMp4),
ModuleLoadInfo(0x3fe, 0x00000000, "me_stuff"),
ModuleLoadInfo(0x3ff, 0x00000000, "me_core"), // ME Core?
ModuleLoadInfo(0x400, 0x0000c000, "np_common"),
+6 -1
View File
@@ -727,13 +727,18 @@ int main(int argc, const char* argv[]) {
g_Config.iReverbVolume = VOLUMEHI_FULL;
g_Config.internalDataDirectory.clear();
g_Config.bUseOldAtrac = oldAtrac;
g_Config.iForceEnableHLE = 0xFFFFFFFF; // Run all modules as HLE. We don't have anything to load in this context.
g_Config.bSkipDeadbeefFilling = false;
// ApplyToConfig() has the final say, applied after RestoreDefaults() and the headless
// overrides above, so a matching command line flag always wins.
cmdLineOptions.ApplyToConfig();
// Run all modules as HLE - a headless run normally has no firmware to load them from. An
// explicit --disable-hle means the caller does have a dump and wants the real thing, so leave
// the modules they asked for alone.
g_Config.iForceEnableHLE = 0xFFFFFFFF & ~g_Config.iDisableHLE;
// This looks contradictory to the above. But, this preserves the old test behavior which apparently ran the JIT for the CPU
// but ended up running software vertex decoding due to the setting in g_Config. Yeah, it's a mess.
CPUCore cpuCore = CPUCore::JIT;