diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index a32134aefc..e925f90329 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -634,6 +634,8 @@ add_library(Core STATIC MIPS/MIPSVFPUUtils.h MIPS/MIPSTracer.cpp MIPS/MIPSTracer.h + MIPS/MeCore.cpp + MIPS/MeCore.h MIPS/JitCommon/JitCommon.cpp MIPS/JitCommon/JitCommon.h MIPS/JitCommon/JitProfiler.cpp diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 960c093909..44cf315d16 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -210,6 +210,7 @@ static const CommandLineParam g_autoParams[] = { {POFF(dumpCompat), CmdParamType::Bool, "dump-compat", '\0', "Apply the game's compat.ini settings when replaying a GE frame dump (off by default)", CmdLineMode::Headless}, {POFF(hardwareTransform), CmdParamType::Bool, "hardware-transform", '\0', "Transform vertices on the GPU (default true); false transforms them on the CPU", CmdLineMode::Headless}, {POFF(vertexJit), CmdParamType::Bool, "vertex-jit", '\0', "Decode vertices with the JIT where the host can (default; =false for the C++ decoder)", CmdLineMode::Headless}, + {POFF(emulateMediaEngine), CmdParamType::Bool, "me-core", '\0', "Emulate the Media Engine for homebrew that runs code on it (default false)"}, {POFF(shaderCache), CmdParamType::Bool, "shader-cache", '\0', "Load and save the per-game shader cache (default true); =false to time shader compiles like a first boot"}, {POFF(disableGPUFeatures), CmdParamType::String, "disable-gpu-features", '\0', "Act as if the GPU lacked these, to test the fallbacks: comma-separated clip-distance, cull-distance, depth-clamp, framebuffer-fetch, dual-source-blend", CmdLineMode::Headless}, {POFF(depthFilenameSave), CmdParamType::String, "depth-save", '\0', "Save the depth buffer to FILE along with the screenshot (u32 width, u32 height, u16 depths; float depths if FILE ends in .f32)", CmdLineMode::Headless}, @@ -568,6 +569,10 @@ void CommandLineOptions::ApplyToConfig() const { g_Config.bHardwareTransform = hardwareTransform.value(); g_Config.DoNotSaveSetting(&g_Config.bHardwareTransform); } + if (emulateMediaEngine.has_value()) { + g_Config.bEmulateMediaEngine = emulateMediaEngine.value(); + g_Config.DoNotSaveSetting(&g_Config.bEmulateMediaEngine); + } if (shaderCache.has_value()) { g_Config.bShaderCache = shaderCache.value(); g_Config.DoNotSaveSetting(&g_Config.bShaderCache); diff --git a/Core/CmdLine.h b/Core/CmdLine.h index 7e7a68043a..b80bfa0651 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -205,6 +205,7 @@ struct CommandLineOptions { std::optional hardwareTransform; std::optional vertexJit; std::optional shaderCache; + std::optional emulateMediaEngine; // Headless: comma-separated clip-distance, cull-distance, depth-clamp, framebuffer-fetch, dual-source-blend. std::optional disableGPUFeatures; std::optional screenshotRenderTarget; diff --git a/Core/Config.cpp b/Core/Config.cpp index 15318c5edc..04fa406654 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -440,6 +440,7 @@ static const ConfigSetting cpuSettings[] = { ConfigSetting("SeparateSASThread", SETTING(g_Config, bSeparateSASThread), &DefaultSasThread, CfgFlag::PER_GAME | CfgFlag::REPORT), ConfigSetting("IOTimingMethod", SETTING(g_Config, iIOTimingMethod), IOTIMING_FAST, CfgFlag::PER_GAME | CfgFlag::REPORT), ConfigSetting("FastMemoryAccess", SETTING(g_Config, bFastMemory), true, CfgFlag::PER_GAME), + ConfigSetting("EmulateMediaEngine", SETTING(g_Config, bEmulateMediaEngine), false, CfgFlag::REPORT), ConfigSetting("FunctionReplacements", SETTING(g_Config, bFuncReplacements), true, CfgFlag::PER_GAME | CfgFlag::REPORT), ConfigSetting("HideSlowWarnings", SETTING(g_Config, bHideSlowWarnings), false, CfgFlag::DEFAULT), ConfigSetting("HideStateWarnings", SETTING(g_Config, bHideStateWarnings), false, CfgFlag::DEFAULT), diff --git a/Core/Config.h b/Core/Config.h index 2dac232622..16c7d9f65d 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -233,6 +233,8 @@ public: bool bFastMemory; int iCpuCore; + // Run homebrew's Media Engine code (Core/MIPS/MeCore.h). Global for now, in developer settings. + bool bEmulateMediaEngine; bool bCheckForNewVersion; bool bWow64WarningDismissed; bool bForceLagSync; diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 37493b9955..c4d4d0734c 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -718,6 +718,7 @@ + @@ -1043,6 +1044,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 468cdc841f..f82b5b8d60 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -129,6 +129,9 @@ MIPS + + MIPS + MIPS @@ -1422,6 +1425,9 @@ MIPS + + MIPS + MIPS diff --git a/Core/CoreTiming.cpp b/Core/CoreTiming.cpp index 73f0c87984..8b06f48617 100644 --- a/Core/CoreTiming.cpp +++ b/Core/CoreTiming.cpp @@ -33,6 +33,7 @@ #include "Core/HLE/sceKernelThread.h" #include "Core/HLE/__sceAudio.h" #include "Core/MIPS/MIPS.h" +#include "Core/MIPS/MeCore.h" static const int initialHz = 222000000; int CPU_HZ = 222000000; @@ -413,8 +414,16 @@ void ProcessEvents() { } } +// The Media Engine catches up with the main CPU. +static void RunOtherCPUs(int cycles) { + if (MeCore::g_meState) { + MeCore::RunFor(cycles); + } +} + void ForceCheck(MIPSState *mips) { int cyclesExecuted = slicelength - mips->downcount; + RunOtherCPUs(cyclesExecuted); globalTimer += cyclesExecuted; // This will cause us to check for new events immediately. mips->downcount = -1; @@ -429,6 +438,7 @@ void ForceCheck(MIPSState *mips) { void Advance(MIPSState *mips) { PROFILE_THIS_SCOPE("advance"); int cyclesExecuted = slicelength - mips->downcount; + RunOtherCPUs(cyclesExecuted); globalTimer += cyclesExecuted; mips->downcount = slicelength; diff --git a/Core/HW/MMIO.cpp b/Core/HW/MMIO.cpp index c8f552213c..ff263ce32f 100644 --- a/Core/HW/MMIO.cpp +++ b/Core/HW/MMIO.cpp @@ -61,8 +61,22 @@ public: return true; } + // Both CPUs reset to the start of the page. The firmware leaves code there that sends the ME + // (whose COP0 register 22 is nonzero) on to 0x40, where homebrew puts its ME code (read on a + // PSP-1000 under PSPLink). The main CPU's half isn't needed, it never resets under HLE. void Reset() override { - memset(Memory::GetBootPage(), 0, Memory::BOOT_PAGE_SIZE); + u8 *page = Memory::GetBootPage(); + memset(page, 0, Memory::BOOT_PAGE_SIZE); + const u32 code[] = { + 0x4002B000, // mfc0 v0, $22 + 0x1440000E, // bnez v0, 0xBFC00040 + 0x00000000, // nop + 0x1000FFFF, // b . + 0x00000000, // nop + }; + for (size_t i = 0; i < ARRAY_SIZE(code); ++i) { + ((u32_le *)page)[i] = code[i]; + } } void DoState(PointerWrap &p) override { diff --git a/Core/MIPS/Interpreter.cpp b/Core/MIPS/Interpreter.cpp index c3835c5246..1cfe2f326d 100644 --- a/Core/MIPS/Interpreter.cpp +++ b/Core/MIPS/Interpreter.cpp @@ -28,6 +28,7 @@ #include "Core/CoreTiming.h" #include "Core/MemMap.h" #include "Core/MIPS/MIPS.h" +#include "Core/MIPS/MeCore.h" #include "Core/MIPS/MIPSCodeUtils.h" #include "Core/MIPS/Interpreter.h" #include "Core/MIPS/MIPSTables.h" @@ -102,7 +103,8 @@ int MIPS_InterpretSingleStep(MIPSState *mips) { // Hardware registers, for kernel code only. static u32 ReadMMIO(MIPSState *mips, u32 addr, int size) { - if (!Memory::IsKernelCodeAddress(mips->pc)) { + // The Media Engine always runs in kernel mode. + if (!mips->space->IsMediaEngine() && !Memory::IsKernelCodeAddress(mips->pc)) { Core_MemoryException(addr, size, mips->pc, MemoryExceptionType::READ_WORD, "Kernel mode only"); return MMIO::UNHANDLED_READ; } @@ -110,7 +112,7 @@ static u32 ReadMMIO(MIPSState *mips, u32 addr, int size) { } static void WriteMMIO(MIPSState *mips, u32 addr, int size, u32 value) { - if (!Memory::IsKernelCodeAddress(mips->pc)) { + if (!mips->space->IsMediaEngine() && !Memory::IsKernelCodeAddress(mips->pc)) { Core_MemoryException(addr, size, mips->pc, MemoryExceptionType::WRITE_WORD, "Kernel mode only"); return; } @@ -173,13 +175,19 @@ namespace MIPSInt { break; default: - DEBUG_LOG(Log::CPU, "cache instruction affecting %08x : function %i", addr, func); + VERBOSE_LOG(Log::CPU, "cache instruction affecting %08x : function %i", addr, func); } PC += 4; } void Int_Syscall(MIPSState *mips, MIPSOpcode op) { + if (mips->space->IsMediaEngine()) { + // There's no HLE on the ME. TODO: Its exception vector. + ERROR_LOG(Log::ME, "syscall on the Media Engine at %08x", mips->pc); + Core_ExecException(mips->pc, mips->pc, ExecExceptionType::ILLEGAL); + return; + } const u32 syscallPC = mips->pc - 4; // Need to pre-move PC, as CallSyscall may result in a rescheduling! // To do this neater, we'll need a little generated kernel loop that syscall can jump to and then RFI from @@ -750,7 +758,7 @@ namespace MIPSInt { // still not real COP0 semantics (no Compare-triggered interrupt), just a // closer approximation. See docs/VSHBootInvestigation.md "Attempt 9". if (rd == 9) - R(rt) = (u32)CoreTiming::GetTicks(mips); + R(rt) = (u32)(mips->space->IsMediaEngine() ? MeCore::GetTicks() : CoreTiming::GetTicks(mips)); else R(rt) = mips->cop0[rd]; } @@ -1038,10 +1046,16 @@ namespace MIPSInt { int rt = _RT; switch (op & 0x3F) { case 36: // mfic - if (rt != 0) + if (rt != 0 && mips->space->IsMediaEngine()) + R(rt) = MeCore::g_meState->interruptEnable; + else if (rt != 0) R(rt) = __InterruptsEnabled() ? 1 : 0; break; case 38: // mtic + if (mips->space->IsMediaEngine()) { + MeCore::g_meState->interruptEnable = R(rt) & 1; + break; + } // Only bit 0 counts: mtic 2 disables. Pending interrupts run at the next scheduling point, // not right away as with sceKernelCpuResumeIntr. if (R(rt) & 1) diff --git a/Core/MIPS/MIPS.cpp b/Core/MIPS/MIPS.cpp index cbcb311e9c..a79eece7e6 100644 --- a/Core/MIPS/MIPS.cpp +++ b/Core/MIPS/MIPS.cpp @@ -322,6 +322,10 @@ void MIPSState::Shutdown() { } void MIPSState::Reset() { + if (noJit) { + InitRegisters(); + return; + } Shutdown(); Init(); } diff --git a/Core/MIPS/MIPS.h b/Core/MIPS/MIPS.h index 85d07a08d9..38bbf5077e 100644 --- a/Core/MIPS/MIPS.h +++ b/Core/MIPS/MIPS.h @@ -325,6 +325,8 @@ public: MIPSComp::JitInterface *jit = nullptr; // What this CPU sees of memory. The main CPU's is Memory::g_mainSpace. const Memory::AddressSpace *space = nullptr; + // Interpreter only, and Reset() leaves the main CPU's replacement hooks alone (the Media Engine). + bool noJit = false; }; // Access control ('private') break standard layout. Let's not allow it, to avoid excessive diff --git a/Core/MIPS/MIPSTables.cpp b/Core/MIPS/MIPSTables.cpp index cb5335345a..c951abf1cf 100644 --- a/Core/MIPS/MIPSTables.cpp +++ b/Core/MIPS/MIPSTables.cpp @@ -1189,6 +1189,18 @@ struct IRBlockState { bool skipNext = false; }; +// The instruction after the branch at pc, as this CPU sees it. The Media Engine's code can be where +// the main CPU has no memory. +static MIPSOpcode ReadDelaySlot(const MIPSState *mips, u32 pc) { + if (!mips->space->IsMediaEngine()) { + return Memory::Read_Instruction(pc + 4); + } + if (!mips->space->IsValid4AlignedAddress(pc + 4)) { + return MIPSOpcode(0); + } + return MIPSOpcode(mips->space->ReadUnchecked_U32(pc + 4)); +} + // After running op from pc, whether the block is over. In a delay slot, the caller runs on anyway. static inline bool StepIRBlock(IRBlockState &b, const MIPSState *mips, MIPSOpcode op, u32 pc) { if (b.endAfterNext) { @@ -1220,7 +1232,7 @@ static inline bool StepIRBlock(IRBlockState &b, const MIPSState *mips, MIPSOpcod } const u32 enc = op.encoding; - if (((enc >> 26) == 0x14 || (enc >> 26) == 0x15) && (enc & 0xFFFF) == 1 && (Memory::Read_Instruction(pc + 4).encoding & 0xFC00003F) == 0x0D) { + if (((enc >> 26) == 0x14 || (enc >> 26) == 0x15) && (enc & 0xFFFF) == 1 && (ReadDelaySlot(mips, pc).encoding & 0xFC00003F) == 0x0D) { // beql/bnel over a break: BranchRSRTComp drops both and goes on. b.numInstructions++; b.skipNext = mips->inDelaySlot; @@ -1236,7 +1248,7 @@ static inline bool StepIRBlock(IRBlockState &b, const MIPSState *mips, MIPSOpcod return true; } // Not taken: the delay slot runs next, as an ordinary instruction. - const MIPSInfo delaySlotInfo = MIPSGetInfo(Memory::Read_Instruction(pc + 4)); + const MIPSInfo delaySlotInfo = MIPSGetInfo(ReadDelaySlot(mips, pc)); if (IR_CONTINUE_BRANCHES && IsContinuableBranch(op) && (delaySlotInfo & (IS_JUMP | IS_CONDBRANCH)) == 0) { b.continueAfterNext = true; } else { @@ -1472,6 +1484,12 @@ int MIPSInterpret_RunUntil(MIPSState *mips, u64 globalTicks) { return 1; } +void MIPSInterpret_RunDowncount(MIPSState *mips) { + ApplyHostRoundingMode(mips); + RunUntilDowncountZeroFast(mips); + RestoreHostRoundingMode(); +} + const char *MIPSGetName(MIPSOpcode op) { static const char * const noname = "unk"; const MIPSInstruction *instr = MIPSGetInstruction(op); diff --git a/Core/MIPS/MIPSTables.h b/Core/MIPS/MIPSTables.h index 1c4a422b79..6991e4b8eb 100644 --- a/Core/MIPS/MIPSTables.h +++ b/Core/MIPS/MIPSTables.h @@ -135,6 +135,8 @@ void MIPSDisAsm(MIPSOpcode op, u32 pc, char *out, size_t outSize, bool tabsToSpa MIPSInfo MIPSGetInfo(MIPSOpcode op); void MIPSInterpret(MIPSState *mips, MIPSOpcode op); //only for those rare ones int MIPSInterpret_RunUntil(MIPSState *mips, u64 globalTicks); +// Runs until the downcount goes negative, without CoreTiming or breakpoints. For the Media Engine. +void MIPSInterpret_RunDowncount(MIPSState *mips); MIPSInterpretFunc MIPSGetInterpretFunc(MIPSOpcode op); // MIPSInterpret for the JIT backends' interpreter fallback (CompIR_Interpret), which calls it from diff --git a/Core/MIPS/MeCore.cpp b/Core/MIPS/MeCore.cpp new file mode 100644 index 0000000000..31142a2149 --- /dev/null +++ b/Core/MIPS/MeCore.cpp @@ -0,0 +1,131 @@ +// Copyright (c) 2012- 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 "Common/Log.h" +#include "Common/Serialize/Serializer.h" +#include "Common/Serialize/SerializeFuncs.h" +#include "Core/Config.h" +#include "Core/CoreTiming.h" +#include "Core/MemMap.h" +#include "Core/MIPS/MeCore.h" +#include "Core/MIPS/MIPSTables.h" + +namespace MeCore { + +// COP0 register 22, which the reset vector tests to send the ME on to 0x40 (the main CPU's reads 0). +// Only known to be nonzero. +static const u32 ME_CPU_ID = 1; + +MEState *g_meState = nullptr; + +// Booting the VSH counts as homebrew: vshmain.prx loads as an ELF. +bool Wanted(bool homebrew) { + return homebrew && g_Config.bEmulateMediaEngine; +} + +// To the state reset leaves it in, with its clock running on. +static void ResetCPU(MEState *me) { + const u64 ticks = GetTicks(); + me->cpu.InitRegisters(); + me->cpu.cop0[22] = ME_CPU_ID; + me->cpu.pc = Memory::BOOT_PAGE_ADDRESS; + me->tickBase = ticks; + me->interruptEnable = 0; +} + +bool Init() { + _dbg_assert_(!g_meState); + Memory::AddressSpace *space = Memory::CreateMESpace(); + if (!space) { + WARN_LOG(Log::ME, "No Media Engine address space on this platform, not emulating the ME"); + return false; + } + g_meState = new MEState(); + g_meState->cpu.space = space; + g_meState->cpu.noJit = true; + g_meState->tickBase = CoreTiming::GetTicks(&mipsr4k); + ResetCPU(g_meState); + INFO_LOG(Log::ME, "Media Engine created, %d KB eDRAM", space->edramSize / 1024); + return true; +} + +void Shutdown() { + if (!g_meState) { + return; + } + delete g_meState; + g_meState = nullptr; + Memory::DestroyMESpace(); +} + +void DoState(PointerWrap &p) { + auto s = p.Section("MeCore", 1); + if (!s) { + return; + } + + bool present = g_meState != nullptr; + Do(p, present); + if (p.mode == PointerWrap::MODE_READ) { + if (!present) { + Shutdown(); + } else if (!g_meState && !Init()) { + p.SetError(PointerWrap::ERROR_FAILURE); + return; + } + } + if (!g_meState) { + return; + } + + MEState *me = g_meState; + me->cpu.DoState(p); + Do(p, me->inReset); + Do(p, me->tickBase); + Do(p, me->interruptEnable); + DoArray(p, me->cpu.space->GetPointerUnchecked(0), me->cpu.space->edramSize); + DoArray(p, me->cpu.space->GetPointerUnchecked(0x04000000), me->cpu.space->localSize); +} + +void RunFor(int cycles) { + MEState *me = g_meState; + if (cycles <= 0) { + return; + } + me->tickBase += cycles; + if (me->inReset) { + return; + } + me->cpu.downcount += cycles; + MIPSInterpret_RunDowncount(&me->cpu); +} + +void SetReset(bool asserted) { + MEState *me = g_meState; + if (asserted) { + me->inReset = true; + } else if (me->inReset) { + ResetCPU(me); + me->inReset = false; + } +} + +u64 GetTicks() { + return (u64)(g_meState->tickBase - g_meState->cpu.downcount); +} + +} // namespace MeCore diff --git a/Core/MIPS/MeCore.h b/Core/MIPS/MeCore.h new file mode 100644 index 0000000000..e3ddf07368 --- /dev/null +++ b/Core/MIPS/MeCore.h @@ -0,0 +1,60 @@ +// Copyright (c) 2012- 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/. + +// The Media Engine, the PSP's second Allegrex. The firmware runs its codecs on it, and homebrew +// whatever it likes. It runs in its own address space (Memory::CreateMESpace()), on the +// interpreter, a slice behind the main CPU: CoreTiming runs it for the cycles the main CPU has just +// used. Only emulated for homebrew, when enabled; commercial games get the HLE codecs. + +#pragma once + +#include "Common/CommonTypes.h" +#include "Core/MIPS/MIPS.h" + +class PointerWrap; + +namespace MeCore { + +struct MEState { + MIPSState cpu; + // Held in reset until the main CPU releases it. + bool inReset = true; + // The ME's clock is tickBase - cpu.downcount. + s64 tickBase = 0; + // mfic/mtic's interrupt enable, which is per CPU. + u32 interruptEnable = 0; +}; + +// Null when the ME isn't emulated. +extern MEState *g_meState; + +// Whether the config and this boot (homebrew) want the ME. +bool Wanted(bool homebrew); +// Creates the ME, in reset. Fails on platforms without an ME address space. +bool Init(); +void Shutdown(); +// Creates or removes the ME to match the state. +void DoState(PointerWrap &p); + +// Runs the ME for the cycles the main CPU has just run. +void RunFor(int cycles); +// Asserting reset stops the ME. Releasing it starts the ME at the reset vector. +void SetReset(bool asserted); +// Count (COP0 register 9) on the ME. +u64 GetTicks(); + +} // namespace MeCore diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index 57662ed013..c0acc25371 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -65,6 +65,7 @@ #include "Core/RetroAchievements.h" #include "HW/MemoryStick.h" #include "Core/HW/MMIO.h" +#include "Core/MIPS/MeCore.h" #ifndef MOBILE_DEVICE #include "Core/AVIDump.h" @@ -211,7 +212,7 @@ int g_screenshotFailures; if (gpu) gpu->FlushPendingDrawing(); - auto s = p.Section("SaveStart", 1, 4); + auto s = p.Section("SaveStart", 1, 5); if (!s) return; @@ -274,6 +275,11 @@ int g_screenshotFailures; } else if (p.mode == p.MODE_READ) { MMIO::Init(); } + if (s >= 5) { + MeCore::DoState(p); + } else if (p.mode == p.MODE_READ) { + MeCore::Shutdown(); + } if (p.mode == p.MODE_READ && p.error == PointerWrap::ERROR_FAILURE) { // RAM may be loaded and the JIT not told, or the other way round. Keep nothing. diff --git a/Core/System.cpp b/Core/System.cpp index 1590da74b5..289a2897d3 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -61,6 +61,7 @@ #include "Core/HLE/sceUtility.h" #include "Core/HW/Display.h" #include "Core/HW/MMIO.h" +#include "Core/MIPS/MeCore.h" #include "Core/Config.h" #include "Core/Core.h" #include "Core/Util/PathUtil.h" @@ -383,6 +384,7 @@ static bool CPU_Init(FileLoader *fileLoader, IdentifiedFileType type, std::strin std::string geDumpDiscID; std::string gameTitle = "Unidentified PSP title"; + bool homebrew = false; switch (type) { case IdentifiedFileType::PSP_ISO: @@ -437,7 +439,7 @@ static bool CPU_Init(FileLoader *fileLoader, IdentifiedFileType type, std::strin // would, and the program would then overwrite its own heap and stacks. // Homebrew is identified by the MG ("memory stick game") category, or no PARAM.SFO at all. // (g_paramSFO only gets a few keys copied over for a PBP, so the category is in the raw copy.) - const bool homebrew = !hasSFO || g_paramSFORaw.GetValueString("CATEGORY") == "MG"; + homebrew = !hasSFO || g_paramSFORaw.GetValueString("CATEGORY") == "MG"; if (homebrew && Memory::g_PSPModel != PSP_MODEL_FAT && Memory::g_MemorySize == Memory::RAM_NORMAL_SIZE) { INFO_LOG(Log::Loader, "Homebrew PBP: mapping the PSP-2000 extra RAM, outside the user partition"); Memory::g_MemorySize = Memory::RAM_DOUBLE_SIZE; @@ -446,6 +448,7 @@ static bool CPU_Init(FileLoader *fileLoader, IdentifiedFileType type, std::strin break; } case IdentifiedFileType::PSP_ELF: + homebrew = true; if (Memory::g_PSPModel != PSP_MODEL_FAT) { INFO_LOG(Log::Loader, "ELF, using full PSP-2000 memory access"); Memory::g_MemorySize = Memory::RAM_DOUBLE_SIZE; @@ -554,6 +557,9 @@ static bool CPU_Init(FileLoader *fileLoader, IdentifiedFileType type, std::strin CoreTiming::Init(&mipsr4k); MMIO::Init(); + if (MeCore::Wanted(homebrew)) { + MeCore::Init(); + } DisplayHWInit(); @@ -678,6 +684,7 @@ void CPU_Shutdown(bool success) { Replacement_Shutdown(); CoreTiming::Shutdown(); + MeCore::Shutdown(); MMIO::Shutdown(); __KernelShutdown(); HLEShutdown(); diff --git a/UI/DeveloperToolsScreen.cpp b/UI/DeveloperToolsScreen.cpp index 53efdc1793..d2fb09a7ad 100644 --- a/UI/DeveloperToolsScreen.cpp +++ b/UI/DeveloperToolsScreen.cpp @@ -160,6 +160,8 @@ void DeveloperToolsScreen::CreateGeneralTab(UI::LinearLayout *list) { // Only the interpreter raises these, but don't hide it behind the CPU core choice - it'd just // look like the setting had vanished. list->Add(new CheckBox(&g_Config.bEnableFPUExceptionTraps, dev->T("Enable FPU exceptions"))); + // Experimental, so not translated yet. + list->Add(new CheckBox(&g_Config.bEmulateMediaEngine, dev->T("Enable ME emulation for homebrew"))); list->Add(new Choice(dev->T("JIT debug tools")))->OnClick.Handle(this, &DeveloperToolsScreen::OnJitDebugTools); list->Add(new CheckBox(&g_Config.bShowDeveloperMenu, dev->T("Show in-game developer menu"))); diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index cc57a3adcf..35bdb8bc70 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -310,6 +310,7 @@ + @@ -622,6 +623,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index 382b5f7d77..7108a48265 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -233,6 +233,7 @@ + @@ -633,6 +634,7 @@ + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index d843963663..2c0c6ff976 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -568,6 +568,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/MIPS/MIPSCodeUtils.cpp.arm \ $(SRC)/Core/MIPS/MIPSDebugInterface.cpp \ $(SRC)/Core/MIPS/MIPSTracer.cpp \ + $(SRC)/Core/MIPS/MeCore.cpp \ $(SRC)/Core/MIPS/IR/IRAnalysis.cpp \ $(SRC)/Core/MIPS/IR/IRFrontend.cpp \ $(SRC)/Core/MIPS/IR/IRJit.cpp \ diff --git a/libretro/Makefile.common b/libretro/Makefile.common index ab5fc9171d..4181f46e4f 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -890,6 +890,7 @@ SOURCES_CXX += \ $(COREDIR)/MIPS/MIPSStackWalk.cpp \ $(COREDIR)/MIPS/MIPSVFPUUtils.cpp \ $(COREDIR)/MIPS/MIPSTracer.cpp \ + $(COREDIR)/MIPS/MeCore.cpp \ $(COREDIR)/MemFault.cpp \ $(COREDIR)/MemMap.cpp \ $(COREDIR)/MemMapFunctions.cpp \ diff --git a/unittest/JitHarness.cpp b/unittest/JitHarness.cpp index 5ebea65466..fac48cbeaa 100644 --- a/unittest/JitHarness.cpp +++ b/unittest/JitHarness.cpp @@ -33,6 +33,7 @@ #include "Core/MIPS/JitCommon/JitCommon.h" #include "Core/MIPS/JitCommon/JitBlockCache.h" #include "Core/MIPS/Interpreter.h" +#include "Core/MIPS/MeCore.h" #include "Core/MIPS/MIPSCodeUtils.h" #include "Core/MIPS/MIPSDebugInterface.h" #include "Core/MIPS/MIPSAsm.h" @@ -714,6 +715,75 @@ bool TestMESpace() { return success; } +bool TestMeCore() { + Memory::g_PSPModel = PSP_MODEL_FAT; + SetupJitHarness(); + MMIO::Init(); + bool success = true; + + if (!MeCore::Init()) { + printf("No Media Engine on this platform, skipping\n"); + MMIO::Shutdown(); + DestroyJitHarness(); + return true; + } + MeCore::MEState *me = MeCore::g_meState; + + // The classic homebrew ME code at 0xBFC00040 (pspsdk me/basic): count at 0xBFC00060, forever. + const u32 code[] = { + MIPS_MAKE_LUI(MIPS_REG_T1, 0xBFC0), + MIPS_MAKE_LW(MIPS_REG_T0, MIPS_REG_T1, 0x60), + MIPS_MAKE_ADDIU(MIPS_REG_T0, MIPS_REG_T0, 1), + (0x2Bu << 26) | (MIPS_REG_T1 << 21) | (MIPS_REG_T0 << 16) | 0x60, // sw t0, 0x60(t1) + MIPS_MAKE_B(-4), + MIPS_MAKE_NOP(), + }; + u8 *page = Memory::GetBootPage(); + memcpy(page + 0x40, code, sizeof(code)); + auto counter = [&]() { return *(u32_le *)(page + 0x60); }; + + // Held in reset, its clock still runs. + MeCore::RunFor(1000); + EXPECT_EQ_INT(counter(), 0); + EXPECT_EQ_INT((int)MeCore::GetTicks(), 1000); + + // Released, through the reset stub to 0x40. + MeCore::SetReset(false); + EXPECT_EQ_HEX(me->cpu.pc, 0xBFC00000); + MeCore::RunFor(1000); + const u32 count = counter(); + EXPECT_TRUE(count > 100); + EXPECT_TRUE(me->cpu.pc >= 0xBFC00040 && me->cpu.pc < 0xBFC00058); + EXPECT_TRUE(MeCore::GetTicks() >= 2000 && MeCore::GetTicks() < 2010); + + // Saved and loaded. + struct State { + void DoState(PointerWrap &p) { MeCore::DoState(p); } + } state; + std::vector saved; + me->cpu.space->WriteUnchecked_U32(0xCAFE0001, 0x80001000); + const u32 pc = me->cpu.pc; + EXPECT_EQ_INT(CChunkFileReader::MeasureAndSavePtr(state, &saved), CChunkFileReader::ERROR_NONE); + me->cpu.space->WriteUnchecked_U32(0, 0x80001000); + MeCore::RunFor(100); + std::string error; + EXPECT_EQ_INT(CChunkFileReader::LoadPtr(saved.data(), saved.size(), state, &error), CChunkFileReader::ERROR_NONE); + me = MeCore::g_meState; + EXPECT_EQ_HEX(me->cpu.pc, pc); + EXPECT_EQ_HEX(me->cpu.space->ReadUnchecked_U32(0x80001000), 0xCAFE0001); + + // Back in reset, it stops. + MeCore::SetReset(true); + const u32 stopped = counter(); + MeCore::RunFor(1000); + EXPECT_EQ_INT(counter(), stopped); + + MeCore::Shutdown(); + MMIO::Shutdown(); + DestroyJitHarness(); + return success; +} + // Differential fuzzing of the CPU cores. Random straight-line code (ALU, FPU, VFPU, loads and // stores) runs from a random register state on each core, and every core has to end up with the // interpreter's state, NaN payloads aside. diff --git a/unittest/JitHarness.h b/unittest/JitHarness.h index 2ba9ff4bdd..f48bff1b45 100644 --- a/unittest/JitHarness.h +++ b/unittest/JitHarness.h @@ -23,6 +23,7 @@ bool TestJitReplacementHooks(); bool TestCPUEret(); bool TestMMIO(); bool TestMESpace(); +bool TestMeCore(); bool TestCPUEquivalence(); bool TestCPURoundingMode(); bool TestVFPUDotJit(); diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 01760c3796..dc8dcb48c5 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -3736,6 +3736,7 @@ TestItem availableTests[] = { TEST_ITEM(CPUEret), TEST_ITEM(MMIO), TEST_ITEM(MESpace), + TEST_ITEM(MeCore), TEST_ITEM(CPUEquivalence), TEST_ITEM(CPURoundingMode), TEST_ITEM(VFPUDotJit),