Add a developer setting to disable individual HLE modules, allowing them to be loaded properly.

Some games survive with a loaded sceAtrac, and start talking to
sceAudioCodec instead, the underlying library, though unsuccessfully
since it's not properly implemented yet.
This commit is contained in:
Henrik Rydgård 2025-04-01 20:52:12 +02:00
parent 5e18576f59
commit 343ca2600a
9 changed files with 210 additions and 128 deletions

View file

@ -238,6 +238,7 @@ static const ConfigSetting generalSettings[] = {
ConfigSetting("GameListScrollPosition", &g_Config.fGameListScrollPosition, 0.0f, CfgFlag::DEFAULT),
ConfigSetting("DebugOverlay", &g_Config.iDebugOverlay, 0, CfgFlag::DONT_SAVE),
ConfigSetting("DefaultTab", &g_Config.iDefaultTab, 0, CfgFlag::DEFAULT),
ConfigSetting("DisableHLEFlags", &g_Config.iDisableHLE, 0, CfgFlag::PER_GAME),
ConfigSetting("ScreenshotMode", &g_Config.iScreenshotMode, 0, CfgFlag::DEFAULT),
ConfigSetting("ScreenshotsAsPNG", &g_Config.bScreenshotsAsPNG, false, CfgFlag::PER_GAME),

View file

@ -139,6 +139,7 @@ public:
bool bLoadPlugins;
int iAskForExitConfirmationAfterSeconds;
int iUIScaleFactor; // In 8ths of powers of two.
int iDisableHLE;
int iScreenRotation; // The rotation angle of the PPSSPP UI. Only supported on Android and possibly other mobile platforms.
int iInternalScreenRotation; // The internal screen rotation angle. Useful for vertical SHMUPs and similar.

View file

@ -116,6 +116,21 @@ enum class RestoreSettingsBits : int {
};
ENUM_CLASS_BITOPS(RestoreSettingsBits);
// Modules that are candidates for disabling HLE of.
enum class DisableHLEFlags : int {
sceFont = (1 << 0),
sceAtrac = (1 << 1),
scePsmf = (1 << 2),
scePsmfPlayer = (1 << 3),
sceMpeg = (1 << 4),
sceMp3 = (1 << 5),
sceJpeg = (1 << 6),
sceParseHttp = (1 << 7),
Count = 8,
// TODO: Some of the networking libraries may be interesting candidates, like HTTP.
};
ENUM_CLASS_BITOPS(DisableHLEFlags);
std::string GPUBackendToString(GPUBackend backend);
GPUBackend GPUBackendFromString(std::string_view backend);

View file

@ -680,7 +680,6 @@
<ClCompile Include="Font\PGF.cpp" />
<ClCompile Include="HDRemaster.cpp" />
<ClCompile Include="HLE\HLE.cpp">
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">MaxSpeed</Optimization>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">MaxSpeed</Optimization>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">MaxSpeed</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Default</BasicRuntimeChecks>

View file

@ -113,6 +113,111 @@ static std::vector<HLEMipsCallInfo> enqueuedMipsCalls;
// Does need to be saved, referenced by the stack and owned.
static std::vector<PSPAction *> mipsCallActions;
// Modules that games try to load from disk, that we should not load normally. Instead we just HLE hook them.
// TODO: Merge with moduleDB?
static const HLEModuleMeta g_moduleMeta[] = {
{"sceATRAC3plus_Library", "sceAtrac3plus", DisableHLEFlags::sceAtrac},
{"sceFont_Library", "sceLibFont", DisableHLEFlags::sceFont},
{"SceFont_Library", "sceLibFont", DisableHLEFlags::sceFont},
{"SceFont_Library", "sceLibFttt", DisableHLEFlags::sceFont},
{"SceHttp_Library", "sceHttp"},
{"sceMpeg_library", "sceMpeg", DisableHLEFlags::sceMpeg},
{"sceNetAdhocctl_Library"},
{"sceNetAdhocDownload_Library"},
{"sceNetAdhocMatching_Library"},
{"sceNetApDialogDummy_Library"},
{"sceNetAdhoc_Library"},
{"sceNetApctl_Library"},
{"sceNetInet_Library"},
{"sceNetResolver_Library"},
{"sceNet_Library"},
{"sceNetAdhoc_Library"},
{"sceNetAdhocAuth_Service"},
{"sceNetAdhocctl_Library"},
{"sceNetIfhandle_Service"},
{"sceSsl_Module"},
{"sceDEFLATE_Library"},
{"sceMD5_Library"},
{"sceMemab"},
{"sceAvcodec_driver"},
{"sceAudiocodec_Driver"},
{"sceAudiocodec"},
{"sceVideocodec_Driver"},
{"sceVideocodec"},
{"sceMpegbase_Driver"},
{"sceMpegbase"},
{"scePsmf_library", "scePsmf", DisableHLEFlags::scePsmf},
{"scePsmfP_library", "scePsmfPlayer", DisableHLEFlags::scePsmfPlayer},
{"scePsmfPlayer", "scePsmfPlayer", DisableHLEFlags::scePsmfPlayer},
{"sceSAScore", "sceSasCore"},
{"sceCcc_Library", "sceCcc"},
{"SceParseHTTPheader_Library", "sceParseHttp", DisableHLEFlags::sceParseHttp},
{"SceParseURI_Library"},
// Guessing these names
{"sceJpeg", "sceJpeg", DisableHLEFlags::sceJpeg},
{"sceJpeg_library", "sceJpeg", DisableHLEFlags::sceJpeg},
{"sceJpeg_Library", "sceJpeg", DisableHLEFlags::sceJpeg},
};
const HLEModuleMeta *GetHLEModuleMeta(std::string_view modname) {
for (size_t i = 0; i < ARRAY_SIZE(g_moduleMeta); i++) {
if (equalsNoCase(modname, g_moduleMeta[i].modname)) {
return &g_moduleMeta[i];
}
}
return nullptr;
}
const HLEModuleMeta *GetHLEModuleMetaByFlag(DisableHLEFlags flag) {
for (size_t i = 0; i < ARRAY_SIZE(g_moduleMeta); i++) {
if (g_moduleMeta[i].disableFlag == flag) {
return &g_moduleMeta[i];
}
}
return nullptr;
}
const HLEModuleMeta *GetHLEModuleMetaByImport(std::string_view importModuleName) {
for (size_t i = 0; i < ARRAY_SIZE(g_moduleMeta); i++) {
if (g_moduleMeta[i].importName && equalsNoCase(importModuleName, g_moduleMeta[i].importName)) {
return &g_moduleMeta[i];
}
}
return nullptr;
}
// Note: name is the modname from prx, not the export module name!
bool ShouldHLEModule(std::string_view modname, bool *wasDisabled) {
if (wasDisabled) {
*wasDisabled = false;
}
const HLEModuleMeta *meta = GetHLEModuleMeta(modname);
if (!meta) {
return false;
}
bool disabled = meta->disableFlag & (DisableHLEFlags)g_Config.iDisableHLE;
if (disabled) {
if (wasDisabled) {
*wasDisabled = true;
}
return false;
}
return true;
}
bool ShouldHLEModuleByImportName(std::string_view name) {
// Check our special metadata lookup. Should probably be merged with the main one.
const HLEModuleMeta *meta = GetHLEModuleMetaByImport(name);
if (meta) {
bool disabled = meta->disableFlag & (DisableHLEFlags)g_Config.iDisableHLE;
return !disabled;
}
// Otherwise, just fall back to the regular db. If it's in there, we should HLE it.
return GetHLEModuleByName(name) != nullptr;
}
static void hleDelayResultFinish(u64 userdata, int cycleslate) {
u32 error;
SceUID threadID = (SceUID) userdata;
@ -278,7 +383,7 @@ const char *GetHLEFuncName(int moduleIndex, int func) {
u32 GetSyscallOp(std::string_view moduleName, u32 nib) {
// Special case to hook up bad imports.
if (moduleName.empty()) {
return (0x03FFFFCC); // invalid syscall
return 0x03FFFFCC; // invalid syscall
}
int modindex = GetHLEModuleIndex(moduleName);
@ -292,15 +397,10 @@ u32 GetSyscallOp(std::string_view moduleName, u32 nib) {
}
} else {
ERROR_LOG(Log::HLE, "Unknown module %.*s!", (int)moduleName.size(), moduleName.data());
return 0x03FFFFCC; // invalid syscall
return 0x03FFFFCC; // invalid syscall (invalid mod index and func index..)
}
}
bool FuncImportIsSyscall(std::string_view module, u32 nib)
{
return GetHLEFunc(module, nib) != nullptr;
}
void WriteFuncStub(u32 stubAddr, u32 symAddr)
{
// Note that this should be J not JAL, as otherwise control will return to the stub..
@ -317,7 +417,7 @@ void WriteFuncMissingStub(u32 stubAddr, u32 nid)
Memory::Write_U32(GetSyscallOp("", nid), stubAddr + 4);
}
bool WriteSyscall(std::string_view moduleName, u32 nib, u32 address)
bool WriteHLESyscall(std::string_view moduleName, u32 nib, u32 address)
{
if (nib == 0)
{

View file

@ -25,6 +25,7 @@
#include "Common/CommonTypes.h"
#include "Common/Log.h"
#include "Core/MIPS/MIPS.h"
#include "Core/ConfigValues.h"
#ifdef _MSC_VER
#pragma warning (error: 4834) // discarding return value of function with 'nodiscard' attribute
@ -97,8 +98,21 @@ struct Syscall {
#define RETURN64(n) {u64 RETURN64_tmp = n; currentMIPS->r[MIPS_REG_V0] = RETURN64_tmp & 0xFFFFFFFF; currentMIPS->r[MIPS_REG_V1] = RETURN64_tmp >> 32;}
#define RETURNF(fl) currentMIPS->f[0] = fl
struct HLEModuleMeta {
// This is the modname (name from the PRX header). Probably, we should really blacklist on the module names of the exported symbol metadata.
const char *modname;
const char *importName; // Technically a module can export functions with different module names, but doesn't seem to happen.
DisableHLEFlags disableFlag;
};
const HLEModuleMeta *GetHLEModuleMetaByFlag(DisableHLEFlags flag);
const HLEModuleMeta *GetHLEModuleMeta(std::string_view modname);
bool ShouldHLEModule(std::string_view modname, bool *wasDisabled = nullptr);
bool ShouldHLEModuleByImportName(std::string_view importModuleName);
const char *GetHLEFuncName(std::string_view module, u32 nib);
const char *GetHLEFuncName(int module, int func);
const HLEModule *GetHLEModuleByName(std::string_view name);
const HLEFunction *GetHLEFunc(std::string_view module, u32 nib);
int GetHLEFuncIndexByNib(int moduleIndex, u32 nib);
int GetHLEModuleIndex(std::string_view modulename);
@ -160,8 +174,7 @@ void HLEInit();
void HLEDoState(PointerWrap &p);
void HLEShutdown();
u32 GetSyscallOp(std::string_view module, u32 nib);
bool FuncImportIsSyscall(std::string_view module, u32 nib);
bool WriteSyscall(std::string_view module, u32 nib, u32 address);
bool WriteHLESyscall(std::string_view module, u32 nib, u32 address);
void CallSyscall(MIPSOpcode op);
void WriteFuncStub(u32 stubAddr, u32 symAddr);
void WriteFuncMissingStub(u32 stubAddr, u32 nid);

View file

@ -52,12 +52,11 @@
#include "Core/FileSystems/MetaFileSystem.h"
#include "Core/Util/BlockAllocator.h"
#include "Core/CoreTiming.h"
#include "Core/ConfigValues.h"
#include "Core/PSPLoaders.h"
#include "Core/System.h"
#include "Core/MemMapHelpers.h"
#include "Core/Debugger/SymbolMap.h"
#include "Core/MIPS/MIPS.h"
#include "Core/HLE/sceKernel.h"
#include "Core/HLE/sceKernelModule.h"
#include "Core/HLE/sceKernelThread.h"
@ -103,32 +102,6 @@ static const char * const lieAboutSuccessModules[] = {
"flash0:/kd/pspnet_resolver.prx",
};
// Modules to not load. TODO: Look into loosening this a little (say sceFont).
static const char * const blacklistedModules[] = {
"sceATRAC3plus_Library",
"sceFont_Library",
"SceFont_Library",
"SceHttp_Library",
"sceMpeg_library",
"sceNetAdhocctl_Library",
"sceNetAdhocDownload_Library",
"sceNetAdhocMatching_Library",
"sceNetApDialogDummy_Library",
"sceNetAdhoc_Library",
"sceNetApctl_Library",
"sceNetInet_Library",
"sceNetResolver_Library",
"sceNet_Library",
"sceNetAdhoc_Library",
"sceNetAdhocAuth_Service",
"sceNetAdhocctl_Library",
"sceNetIfhandle_Service",
"sceSsl_Module",
"sceDEFLATE_Library",
"sceMD5_Library",
"sceMemab",
};
const char *NativeModuleStatusToString(NativeModuleStatus status) {
switch (status) {
case MODULE_STATUS_STARTING: return "STARTING";
@ -167,6 +140,34 @@ struct ModuleInfo {
char name[28];
};
struct PspLibStubEntry {
u32_le name;
u16_le version;
u16_le flags;
u8 size;
u8 numVars;
u16_le numFuncs;
// each symbol has an associated nid; nidData is a pointer
// (in .rodata.sceNid section) to an array of longs, one
// for each function, which identifies the function whose
// address is to be inserted.
//
// The hash is the first 4 bytes of a SHA-1 hash of the function
// name. (Represented as a little-endian long, so the order
// of the bytes is reversed.)
u32_le nidData;
// the address of the function stubs where the function address jumps
// should be filled in
u32_le firstSymAddr;
// Optional, this is where var relocations are.
// They use the format: u32 addr, u32 nid, ...
// WARNING: May have garbage if size < 6.
u32_le varData;
// Not sure what this is yet, assume garbage for now.
// TODO: Tales of the World: Radiant Mythology 2 has something here?
u32_le extra;
};
PSPModule::~PSPModule() {
if (memoryBlockAddr) {
// If it's either below user memory, or using a high kernel bit, it's in kernel.
@ -639,14 +640,26 @@ void UnexportVarSymbol(const VarSymbolExport &var) {
}
}
static bool FuncImportIsHLE(std::string_view module, u32 nid) {
// TODO: Take into account whether HLE is enabled for the module.
// Also, this needs to be more efficient.
if (!ShouldHLEModuleByImportName(module)) {
return false;
}
return GetHLEFunc(module, nid) != nullptr;
}
void ImportFuncSymbol(const FuncSymbolImport &func, bool reimporting, const char *importingModule) {
// Prioritize HLE implementations.
// TODO: Or not?
if (FuncImportIsSyscall(func.moduleName, func.nid)) {
bool shouldHLE = ShouldHLEModuleByImportName(func.moduleName);
// Prioritize HLE implementations, if we should HLE this.
if (shouldHLE && GetHLEFunc(func.moduleName, func.nid)) {
if (reimporting && Memory::Read_Instruction(func.stubAddr + 4) != GetSyscallOp(func.moduleName, func.nid)) {
WARN_LOG(Log::Loader, "Reimporting updated syscall %s", GetHLEFuncName(func.moduleName, func.nid));
}
WriteSyscall(func.moduleName, func.nid, func.stubAddr);
// TODO: There's some double lookup going on here (we already did the lookup in GetHLEFunc above).
WriteHLESyscall(func.moduleName, func.nid, func.stubAddr);
currentMIPS->InvalidateICache(func.stubAddr, 8);
if (g_Config.bPreloadFunctions) {
MIPSAnalyst::PrecompileFunction(func.stubAddr, 8);
@ -678,21 +691,21 @@ void ImportFuncSymbol(const FuncSymbolImport &func, bool reimporting, const char
}
// It hasn't been exported yet, but hopefully it will later. Check if we know about it through HLE.
const bool isKnownHLEModule = GetHLEModuleIndex(func.moduleName) != -1;
if (isKnownHLEModule) {
if (shouldHLE) {
// We used to report this, but I don't think it's very interesting anymore.
WARN_LOG(Log::Loader, "Unknown syscall from known HLE module '%s': 0x%08x (import for '%s')", func.moduleName, func.nid, importingModule);
} else {
INFO_LOG(Log::Loader, "Function (%s,%08x) unresolved in '%s', storing for later resolving", func.moduleName, func.nid, importingModule);
}
if (isKnownHLEModule || !reimporting) {
if (shouldHLE || !reimporting) {
WriteFuncMissingStub(func.stubAddr, func.nid);
currentMIPS->InvalidateICache(func.stubAddr, 8);
}
}
void ExportFuncSymbol(const FuncSymbolExport &func) {
if (FuncImportIsSyscall(func.moduleName, func.nid)) {
if (FuncImportIsHLE(func.moduleName, func.nid)) {
// HLE covers this already - let's ignore the function.
WARN_LOG(Log::Loader, "Ignoring func export %s/%08x, already implemented in HLE.", func.moduleName, func.nid);
return;
@ -720,7 +733,7 @@ void ExportFuncSymbol(const FuncSymbolExport &func) {
}
void UnexportFuncSymbol(const FuncSymbolExport &func) {
if (FuncImportIsSyscall(func.moduleName, func.nid)) {
if (FuncImportIsHLE(func.moduleName, func.nid)) {
// Oops, HLE covers this.
return;
}
@ -769,72 +782,7 @@ void PSPModule::Cleanup() {
}
}
static bool IsHLEVersionedModule(const char *name) {
// TODO: Only some of these are currently known to be versioned.
// Potentially only sceMpeg_library matters.
// For now, we're just reporting version numbers.
for (size_t i = 0; i < ARRAY_SIZE(blacklistedModules); i++) {
if (!strncmp(name, blacklistedModules[i], 28)) {
return true;
}
}
static const char *otherModules[] = {
"sceAvcodec_driver",
"sceAudiocodec_Driver",
"sceAudiocodec",
"sceVideocodec_Driver",
"sceVideocodec",
"sceMpegbase_Driver",
"sceMpegbase",
"scePsmf_library",
"scePsmfP_library",
"scePsmfPlayer",
"sceSAScore",
"sceCcc_Library",
"SceParseHTTPheader_Library",
"SceParseURI_Library",
// Guessing.
"sceJpeg",
"sceJpeg_library",
"sceJpeg_Library",
};
for (size_t i = 0; i < ARRAY_SIZE(otherModules); i++) {
if (!strncmp(name, otherModules[i], 28)) {
return true;
}
}
return false;
}
static bool KernelImportModuleFuncs(PSPModule *module, u32 *firstImportStubAddr, bool reimporting) {
struct PspLibStubEntry {
u32_le name;
u16_le version;
u16_le flags;
u8 size;
u8 numVars;
u16_le numFuncs;
// each symbol has an associated nid; nidData is a pointer
// (in .rodata.sceNid section) to an array of longs, one
// for each function, which identifies the function whose
// address is to be inserted.
//
// The hash is the first 4 bytes of a SHA-1 hash of the function
// name. (Represented as a little-endian long, so the order
// of the bytes is reversed.)
u32_le nidData;
// the address of the function stubs where the function address jumps
// should be filled in
u32_le firstSymAddr;
// Optional, this is where var relocations are.
// They use the format: u32 addr, u32 nid, ...
// WARNING: May have garbage if size < 6.
u32_le varData;
// Not sure what this is yet, assume garbage for now.
// TODO: Tales of the World: Radiant Mythology 2 has something here?
u32_le extra;
};
// Can't run - we didn't keep track of the libstub entry.
if (module->libstub == 0) {
return false;
@ -1099,7 +1047,8 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
head = (const PSP_Header *)ptr;
devkitVersion = head->devkitversion;
if (IsHLEVersionedModule(head->modname)) {
bool wasDisabled;
if (ShouldHLEModule(head->modname, &wasDisabled)) {
int ver = (head->module_ver_hi << 8) | head->module_ver_lo;
INFO_LOG(Log::sceModule, "Loading module %s with version %04x, devkit %08x, crc %x", head->modname, ver, head->devkitversion, module->crc);
@ -1108,6 +1057,9 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
fakeLoadedModule = true;
}
if (wasDisabled) {
g_OSD.Show(OSDType::MESSAGE_WARNING, StringFromFormat("HLE for %s has been manually disabled", head->modname));
}
const u8 *in = ptr;
const auto isGzip = head->comp_attribute & 1;
// Kind of odd.
@ -1310,16 +1262,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
char moduleName[29] = {0};
strncpy(moduleName, modinfo->name, ARRAY_SIZE(module->nm.name));
// Check for module blacklist - we don't allow games to load these modules from disc
// as we have HLE implementations and the originals won't run in the emu because they
// directly access hardware or for other reasons.
for (u32 i = 0; i < ARRAY_SIZE(blacklistedModules); i++) {
if (strncmp(modinfo->name, blacklistedModules[i], ARRAY_SIZE(modinfo->name)) == 0) {
module->isFake = true;
}
}
if (!module->isFake && module->memoryBlockAddr != 0) {
if (module->memoryBlockAddr != 0) {
g_symbolMap->AddModule(moduleName, module->memoryBlockAddr, module->memoryBlockSize);
}
@ -1415,6 +1358,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
}
// Look at the exports, too.
// TODO: Add them to the symbol map!
struct PspLibEntEntry {
u32_le name; /* ent's name (module name) address */
@ -1500,7 +1444,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
func.nid = nid;
func.symAddr = exportAddr;
if (ent->name == 0) {
WARN_LOG_REPORT(Log::HLE, "Exporting func from syslib export: %08x", nid);
WARN_LOG(Log::HLE, "Exporting func from syslib export: %08x", nid);
}
module->ExportFunc(func);
}
@ -1587,7 +1531,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
delete [] newptr;
if (!reportedModule && IsHLEVersionedModule(modinfo->name)) {
if (!reportedModule && ShouldHLEModule(modinfo->name)) {
INFO_LOG(Log::sceModule, "Loading module %s with version %04x, devkit %08x", modinfo->name, modinfo->moduleVersion, devkitVersion);
if (!strcmp(modinfo->name, "sceMpeg_library")) {

View file

@ -746,7 +746,7 @@ static void __KernelWriteFakeSysCall(u32 nid, u32 *ptr, u32 &pos)
{
*ptr = pos;
pos += 8;
WriteSyscall("FakeSysCalls", nid, *ptr);
WriteHLESyscall("FakeSysCalls", nid, *ptr);
MIPSAnalyst::PrecompileFunction(*ptr, 8);
}
@ -1931,7 +1931,7 @@ int __KernelStartThread(SceUID threadToStartID, int argSize, u32 argBlockPtr, bo
// At the bottom of those 64 bytes, the return syscall and ra is written.
// Test Drive Unlimited actually depends on it being in the correct place.
WriteSyscall("FakeSysCalls", NID_THREADRETURN, sp);
WriteHLESyscall("FakeSysCalls", NID_THREADRETURN, sp);
Memory::Write_U32(MIPS_MAKE_B(-1), sp + 8);
Memory::Write_U32(MIPS_MAKE_NOP(), sp + 12);

View file

@ -1995,6 +1995,15 @@ void DeveloperToolsScreen::CreateViews() {
list->Add(new BitCheckBox(&g_Config.iDumpFileTypes, (int)DumpFileType::PRX, dev->T("PRX")));
list->Add(new BitCheckBox(&g_Config.iDumpFileTypes, (int)DumpFileType::Atrac3, dev->T("Atrac3/3+")));
list->Add(new ItemHeader("Disable HLE (experimental! Not expected to work yet)"));
for (int i = 0; i < (int)DisableHLEFlags::Count; i++) {
DisableHLEFlags flag = (DisableHLEFlags)(1 << i);
const HLEModuleMeta *meta = GetHLEModuleMetaByFlag(flag);
if (meta) {
list->Add(new BitCheckBox(&g_Config.iDisableHLE, (int)flag, meta->modname));
}
}
#if !PPSSPP_PLATFORM(ANDROID) && !PPSSPP_PLATFORM(IOS) && !PPSSPP_PLATFORM(SWITCH)
list->Add(new ItemHeader(dev->T("MIPSTracer")));