diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt
index e925f90329..2770aee2dd 100644
--- a/Core/CMakeLists.txt
+++ b/Core/CMakeLists.txt
@@ -578,6 +578,8 @@ add_library(Core STATIC
HW/Display.h
HW/GpioMMIO.cpp
HW/GpioMMIO.h
+ HW/MeMMIO.cpp
+ HW/MeMMIO.h
HW/MMIO.cpp
HW/MMIO.h
HW/AvcDecoder.cpp
diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj
index c4d4d0734c..2348b5a217 100644
--- a/Core/Core.vcxproj
+++ b/Core/Core.vcxproj
@@ -494,6 +494,7 @@
+
@@ -867,6 +868,7 @@
+
diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters
index f82b5b8d60..70a1be4ee9 100644
--- a/Core/Core.vcxproj.filters
+++ b/Core/Core.vcxproj.filters
@@ -1177,6 +1177,9 @@
HW
+
+ HW
+
HLE\Libraries
@@ -2184,6 +2187,9 @@
HW
+
+ HW
+
HLE\Libraries
diff --git a/Core/HLE/ReplaceTables.cpp b/Core/HLE/ReplaceTables.cpp
index f1db1aad15..7b05326d93 100644
--- a/Core/HLE/ReplaceTables.cpp
+++ b/Core/HLE/ReplaceTables.cpp
@@ -38,6 +38,7 @@
#include "Core/HLE/ReplaceTables.h"
#include "Core/HLE/FunctionWrappers.h"
#include "Core/HLE/sceDisplay.h"
+#include "Core/HW/MMIO.h"
#include "GPU/Math3D.h"
#include "GPU/GPU.h"
@@ -119,6 +120,33 @@ static int Replace_ceilf(MIPSState *mips) {
// Should probably do JIT versions of this, possibly ones that only delegate
// large copies to a C function.
+// Kernel code copies Media Engine code to the boot page at 0xBFC00000, which the main CPU reaches
+// through MMIO. Byte by byte, forward like memmove when the destination is lower. Returns false if
+// neither side is MMIO.
+static bool CopyThroughMMIO(MIPSState *mips, u32 destPtr, u32 srcPtr, u32 bytes) {
+ if (!Memory::IsMMIOAccess(destPtr) && !Memory::IsMMIOAccess(srcPtr)) {
+ return false;
+ }
+ const bool backward = destPtr > srcPtr && destPtr < srcPtr + bytes;
+ for (u32 i = 0; i < bytes; ++i) {
+ const u32 offset = backward ? bytes - 1 - i : i;
+ const u32 src = srcPtr + offset;
+ const u32 dest = destPtr + offset;
+ u8 value = 0;
+ if (Memory::IsMMIOAccess(src)) {
+ value = (u8)MMIO::Read(mips, src, 1);
+ } else if (Memory::IsValidAddress(src)) {
+ value = Memory::ReadUnchecked_U8(src);
+ }
+ if (Memory::IsMMIOAccess(dest)) {
+ MMIO::Write(mips, dest, 1, value);
+ } else if (Memory::IsValidAddress(dest)) {
+ Memory::WriteUnchecked_U8(value, dest);
+ }
+ }
+ return true;
+}
+
static int Replace_memcpy(MIPSState *mips) {
u32 destPtr = PARAM(0);
u32 srcPtr = PARAM(1);
@@ -135,7 +163,7 @@ static int Replace_memcpy(MIPSState *mips) {
}
}
gpu->NotifyVideoCopy(destPtr, srcPtr, bytes);
- if (!skip && bytes != 0) {
+ if (!skip && bytes != 0 && !CopyThroughMMIO(mips, destPtr, srcPtr, bytes)) {
u8 *dst = Memory::GetPointerWriteRangeOrException(destPtr, bytes);
const u8 *src = Memory::GetPointerRangeOrException(srcPtr, bytes);
@@ -265,7 +293,7 @@ static int Replace_memcpy16(MIPSState *mips) {
}
}
gpu->NotifyVideoCopy(destPtr, srcPtr, bytes);
- if (!skip && bytes != 0) {
+ if (!skip && bytes != 0 && !CopyThroughMMIO(mips, destPtr, srcPtr, bytes)) {
u8 *dst = Memory::GetPointerWriteRangeOrException(destPtr, bytes);
const u8 *src = Memory::GetPointerRangeOrException(srcPtr, bytes);
if (dst && src) {
@@ -332,7 +360,7 @@ static int Replace_memmove(MIPSState *mips) {
}
}
gpu->NotifyVideoCopy(destPtr, srcPtr, bytes);
- if (!skip && bytes != 0) {
+ if (!skip && bytes != 0 && !CopyThroughMMIO(mips, destPtr, srcPtr, bytes)) {
u8 *dst = Memory::GetPointerWriteRangeOrException(destPtr, bytes);
const u8 *src = Memory::GetPointerRangeOrException(srcPtr, bytes);
if (dst && src) {
diff --git a/Core/HLE/sceKernelInterrupt.cpp b/Core/HLE/sceKernelInterrupt.cpp
index 66f60ba855..2d85e89970 100644
--- a/Core/HLE/sceKernelInterrupt.cpp
+++ b/Core/HLE/sceKernelInterrupt.cpp
@@ -30,6 +30,7 @@
#include "Core/HLE/FunctionWrappers.h"
#include "Core/HLE/ErrorCodes.h"
#include "Core/MIPS/MIPS.h"
+#include "Core/MIPS/MeCore.h"
#include "Core/Debugger/MemBlockInfo.h"
#include "Core/HLE/sceKernel.h"
@@ -132,6 +133,19 @@ static void sceKernelCpuResumeIntrWithSync(u32 enable)
bool IntrHandler::run(PendingInterrupt& pend)
{
+ if (pend.subintr == PSP_INTR_SUB_NONE) {
+ if (!baseEnabled_ || baseHandler_ == 0 || baseHandler_ == FIRMWARE_HANDLER) {
+ return false;
+ }
+ // As interruptman calls it (6.61): the interrupt, the arg, the interrupted pc.
+ DEBUG_LOG(Log::CPU, "Entering base interrupt handler %08x", baseHandler_);
+ currentMIPS->pc = baseHandler_;
+ currentMIPS->r[MIPS_REG_A0] = intrNumber;
+ currentMIPS->r[MIPS_REG_A1] = baseArg_;
+ currentMIPS->r[MIPS_REG_A2] = intState.savedCpu.pc;
+ return true;
+ }
+
SubIntrHandler *handler = get(pend.subintr);
if (!handler) {
WARN_LOG(Log::sceIntc, "Ignoring interrupt, already been released.");
@@ -155,7 +169,16 @@ void IntrHandler::copyArgsToCPU(PendingInterrupt& pend)
void IntrHandler::handleResult(PendingInterrupt& pend)
{
- //u32 result = currentMIPS->r[MIPS_REG_V0];
+ if (pend.subintr != PSP_INTR_SUB_NONE)
+ return;
+ // From interruptman (6.61): -1 is done, -2 disables the interrupt, and n >= 0 runs the handler's
+ // own sub-interrupt n, which nothing here registers.
+ const int result = (int)currentMIPS->r[MIPS_REG_V0];
+ if (result == -2) {
+ enableBase(false);
+ } else if (result >= 0) {
+ WARN_LOG(Log::sceIntc, "Base handler for interrupt %d asked for sub-interrupt %d", intrNumber, result);
+ }
}
SubIntrHandler* IntrHandler::add(int subIntrNum)
@@ -248,6 +271,11 @@ void __InterruptsInit()
inInterrupt = false;
for (int i = 0; i < (int)ARRAY_SIZE(intrHandlers); ++i)
intrHandlers[i] = new IntrHandler(i);
+ if (MeCore::g_meState) {
+ // me_wrapper's, in the firmware.
+ intrHandlers[PSP_MECODEC_INTR]->setBaseHandler(IntrHandler::FIRMWARE_HANDLER, 0);
+ intrHandlers[PSP_MECODEC_INTR]->enableBase(true);
+ }
intState.clear();
threadBeforeInterrupt = 0;
}
diff --git a/Core/HLE/sceKernelInterrupt.h b/Core/HLE/sceKernelInterrupt.h
index 02c2c407be..cbecadff4d 100644
--- a/Core/HLE/sceKernelInterrupt.h
+++ b/Core/HLE/sceKernelInterrupt.h
@@ -130,8 +130,8 @@ public:
SubIntrHandler *get(int subIntrNum);
void clear();
- // The handler a kernel driver registered for the whole interrupt (sceKernelRegisterIntrHandler).
- // Nothing raises those interrupts yet, so it's only kept.
+ // The handler a kernel driver registered for the whole interrupt (sceKernelRegisterIntrHandler),
+ // raised with PSP_INTR_SUB_NONE. Only the Media Engine raises one so far.
bool hasBaseHandler() const { return baseHandler_ != 0; }
void setBaseHandler(u32 handler, u32 arg) {
baseHandler_ = handler;
@@ -139,6 +139,8 @@ public:
baseEnabled_ = false;
}
void enableBase(bool enable) { baseEnabled_ = enable; }
+ // Stands in for a firmware driver's handler, which homebrew releases to register its own.
+ static constexpr u32 FIRMWARE_HANDLER = 1;
void DoState(PointerWrap &p);
diff --git a/Core/HW/MMIO.cpp b/Core/HW/MMIO.cpp
index ff263ce32f..e6ba500927 100644
--- a/Core/HW/MMIO.cpp
+++ b/Core/HW/MMIO.cpp
@@ -23,6 +23,7 @@
#include "Common/Serialize/SerializeFuncs.h"
#include "Core/HW/GpioMMIO.h"
#include "Core/HW/MMIO.h"
+#include "Core/HW/MeMMIO.h"
#include "Core/MemMap.h"
#include "Core/MIPS/MIPS.h"
@@ -93,6 +94,9 @@ void Init() {
GpioMMIO::Device(),
SysconSerialMMIO::Device(),
&g_bootPage,
+ MeMMIO::SysregDevice(),
+ MeMMIO::InterruptDevice(),
+ MeMMIO::MemProtectDevice(),
};
for (MMIODevice *device : g_devices) {
device->Reset();
diff --git a/Core/HW/MeMMIO.cpp b/Core/HW/MeMMIO.cpp
new file mode 100644
index 0000000000..ff41fe6f87
--- /dev/null
+++ b/Core/HW/MeMMIO.cpp
@@ -0,0 +1,181 @@
+// 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 "Core/HW/MMIO.h"
+#include "Core/HW/MeMMIO.h"
+#include "Core/MemMap.h"
+#include "Core/MIPS/MeCore.h"
+#include "Core/MIPS/MIPS.h"
+
+namespace MeMMIO {
+
+using MeCore::g_meState;
+
+static bool IsME(const MIPSState *mips) {
+ return mips && mips->IsMediaEngine();
+}
+
+class MeSysregDevice : public MMIODevice {
+public:
+ MeSysregDevice() : MMIODevice("ME sysreg", 0xBC100000, 0x100) {}
+
+ bool Read(MIPSState *mips, u32 offset, int size, u32 *value) override {
+ u32 *reg = Register(offset);
+ if (!reg) {
+ return false;
+ }
+ *value = *reg;
+ return true;
+ }
+
+ bool Write(MIPSState *mips, u32 offset, int size, u32 value) override {
+ if (!g_meState) {
+ return false;
+ }
+ switch (offset) {
+ case 0x44:
+ // Interrupts the other CPU.
+ if (value & 1) {
+ if (IsME(mips)) {
+ MeCore::InterruptMainCPU();
+ } else {
+ MeCore::RaiseInterrupt(MeCore::ME_INTR_SOFT);
+ }
+ }
+ return true;
+ case 0x48:
+ {
+ // Each CPU sets or clears its own bit, and can only set it while the other's is clear.
+ const u32 own = IsME(mips) ? 2 : 1;
+ const u32 other = own ^ 3;
+ if (!(value & own)) {
+ g_meState->mutex &= ~own;
+ } else if (!(g_meState->mutex & other)) {
+ g_meState->mutex |= own;
+ }
+ return true;
+ }
+ case 0x4C:
+ g_meState->resetFlags = value;
+ MeCore::SetReset((value & 4) != 0);
+ return true;
+ default:
+ if (u32 *reg = Register(offset)) {
+ *reg = value;
+ return true;
+ }
+ return false;
+ }
+ }
+
+ void Reset() override {}
+ void DoState(PointerWrap &p) override {}
+
+private:
+ static u32 *Register(u32 offset) {
+ if (!g_meState) {
+ return nullptr;
+ }
+ switch (offset) {
+ case 0x04: return &g_meState->nmiFlags;
+ case 0x40: return &g_meState->ramConfig;
+ case 0x48: return &g_meState->mutex;
+ case 0x4C: return &g_meState->resetFlags;
+ case 0x50: return &g_meState->busClock;
+ default: return nullptr;
+ }
+ }
+};
+
+class MeInterruptDevice : public MMIODevice {
+public:
+ MeInterruptDevice() : MMIODevice("ME interrupts", 0xBC300000, 0x10) {}
+
+ bool Read(MIPSState *mips, u32 offset, int size, u32 *value) override {
+ if (!g_meState || !IsME(mips)) {
+ return false;
+ }
+ switch (offset) {
+ case 0x0: *value = g_meState->intcRaised; return true;
+ case 0x8: *value = g_meState->intcEnabled; return true;
+ default: return false;
+ }
+ }
+
+ bool Write(MIPSState *mips, u32 offset, int size, u32 value) override {
+ if (!g_meState || !IsME(mips)) {
+ return false;
+ }
+ switch (offset) {
+ case 0x0:
+ // Writing a source's bit clears it.
+ g_meState->intcRaised &= ~value;
+ return true;
+ case 0x8:
+ g_meState->intcEnabled = value;
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ void Reset() override {}
+ void DoState(PointerWrap &p) override {}
+};
+
+class MeMemProtectDevice : public MMIODevice {
+public:
+ MeMemProtectDevice() : MMIODevice("ME memory protection", 0xBC000000, 0x50) {}
+
+ bool Read(MIPSState *mips, u32 offset, int size, u32 *value) override {
+ if (!g_meState || (offset & 3)) {
+ return false;
+ }
+ *value = g_meState->memProtect[offset / 4];
+ return true;
+ }
+
+ bool Write(MIPSState *mips, u32 offset, int size, u32 value) override {
+ if (!g_meState || (offset & 3)) {
+ return false;
+ }
+ g_meState->memProtect[offset / 4] = value;
+ return true;
+ }
+
+ void Reset() override {}
+ void DoState(PointerWrap &p) override {}
+};
+
+static MeSysregDevice g_sysreg;
+static MeInterruptDevice g_interrupts;
+static MeMemProtectDevice g_memProtect;
+
+MMIODevice *SysregDevice() {
+ return &g_sysreg;
+}
+
+MMIODevice *InterruptDevice() {
+ return &g_interrupts;
+}
+
+MMIODevice *MemProtectDevice() {
+ return &g_memProtect;
+}
+
+} // namespace MeMMIO
diff --git a/Core/HW/MeMMIO.h b/Core/HW/MeMMIO.h
new file mode 100644
index 0000000000..3f837fedad
--- /dev/null
+++ b/Core/HW/MeMMIO.h
@@ -0,0 +1,34 @@
+// 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 registers homebrew's Media Engine code uses to run the ME and talk to it. Their state is the
+// ME's (MeCore::MEState); without an ME they're left unhandled, as before.
+
+#pragma once
+
+class MMIODevice;
+
+namespace MeMMIO {
+
+// Reset, bus clock, soft interrupt and hardware mutex (0xBC100000).
+MMIODevice *SysregDevice();
+// The ME's interrupt controller (0xBC300000). The main CPU has its own there.
+MMIODevice *InterruptDevice();
+// Memory protection (0xBC000000), which the ME's startup code unlocks.
+MMIODevice *MemProtectDevice();
+
+} // namespace MeMMIO
diff --git a/Core/MIPS/IR/IRFrontend.cpp b/Core/MIPS/IR/IRFrontend.cpp
index 7cb3e1ecbb..3b153b7e9f 100644
--- a/Core/MIPS/IR/IRFrontend.cpp
+++ b/Core/MIPS/IR/IRFrontend.cpp
@@ -313,7 +313,7 @@ void IRFrontend::DoJit(u32 em_address, std::vector &instructions, u32 &m
js.hadBreakpoints = false;
js.blockWrotePrefixes = false;
js.inDelaySlot = false;
- js.kernelMode = Memory::IsKernelCodeAddress(em_address);
+ js.kernelMode = Memory::MayRunInKernelMode(em_address);
js.PrefixStart();
ir.Clear();
diff --git a/Core/MIPS/Interpreter.cpp b/Core/MIPS/Interpreter.cpp
index 6535a02d53..c2411a87a0 100644
--- a/Core/MIPS/Interpreter.cpp
+++ b/Core/MIPS/Interpreter.cpp
@@ -104,7 +104,7 @@ int MIPS_InterpretSingleStep(MIPSState *mips) {
// Hardware registers, for kernel code only.
static u32 ReadMMIO(MIPSState *mips, u32 addr, int size) {
// The Media Engine always runs in kernel mode.
- if (!mips->IsMediaEngine() && !Memory::IsKernelCodeAddress(mips->pc)) {
+ if (!mips->IsMediaEngine() && !Memory::MayRunInKernelMode(mips->pc)) {
Core_MemoryException(addr, size, mips->pc, MemoryExceptionType::READ_WORD, "Kernel mode only");
return MMIO::UNHANDLED_READ;
}
@@ -112,7 +112,7 @@ static u32 ReadMMIO(MIPSState *mips, u32 addr, int size) {
}
static void WriteMMIO(MIPSState *mips, u32 addr, int size, u32 value) {
- if (!mips->IsMediaEngine() && !Memory::IsKernelCodeAddress(mips->pc)) {
+ if (!mips->IsMediaEngine() && !Memory::MayRunInKernelMode(mips->pc)) {
Core_MemoryException(addr, size, mips->pc, MemoryExceptionType::WRITE_WORD, "Kernel mode only");
return;
}
@@ -757,8 +757,10 @@ namespace MIPSInt {
// calibration code of a nonzero seed/divisor. Tie it to CoreTiming instead;
// 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)(mips->IsMediaEngine() ? MeCore::GetTicks() : CoreTiming::GetTicks(mips));
+ if (mips->IsMediaEngine())
+ R(rt) = MeCore::ReadCop0(rd);
+ else if (rd == 9)
+ R(rt) = (u32)CoreTiming::GetTicks(mips);
else
R(rt) = mips->cop0[rd];
}
@@ -774,7 +776,10 @@ namespace MIPSInt {
break;
case 4: //mtc0
- mips->cop0[rd] = R(rt);
+ if (mips->IsMediaEngine())
+ MeCore::WriteCop0(rd, R(rt));
+ else
+ mips->cop0[rd] = R(rt);
break;
case 10: //rdpgpr
@@ -1040,6 +1045,16 @@ namespace MIPSInt {
PC += 4;
}
+ // Only the Media Engine runs code that halts. It sleeps until an interrupt.
+ void Int_Halt(MIPSState *mips, MIPSOpcode op) {
+ if (!mips->IsMediaEngine()) {
+ Core_ExecException(PC, PC, ExecExceptionType::ILLEGAL);
+ return;
+ }
+ PC += 4;
+ MeCore::Halt();
+ }
+
// The interrupt enable flag, shared with the HLE sceKernelCpuSuspendIntr/ResumeIntr, which on
// hardware are just mfic v0, $0; mtic zero, $0 and mtic a0, $0. Only $0 exists.
void Int_Special2(MIPSState *mips, MIPSOpcode op) {
diff --git a/Core/MIPS/Interpreter.h b/Core/MIPS/Interpreter.h
index c690b8b1bc..3ff319e854 100644
--- a/Core/MIPS/Interpreter.h
+++ b/Core/MIPS/Interpreter.h
@@ -48,6 +48,7 @@ namespace MIPSInt
void Int_FPUBranch(MIPSState *mips, MIPSOpcode op);
void Int_CallReplacement(MIPSState *mips, MIPSOpcode op);
void Int_Special2(MIPSState *mips, MIPSOpcode op);
+ void Int_Halt(MIPSState *mips, MIPSOpcode op);
void Int_Special3(MIPSState *mips, MIPSOpcode op);
void Int_Interrupt(MIPSState *mips, MIPSOpcode op);
void Int_Cache(MIPSState *mips, MIPSOpcode op);
diff --git a/Core/MIPS/InterpreterDispatch.cpp b/Core/MIPS/InterpreterDispatch.cpp
index ac87dcb217..11526c1a8e 100644
--- a/Core/MIPS/InterpreterDispatch.cpp
+++ b/Core/MIPS/InterpreterDispatch.cpp
@@ -391,6 +391,10 @@ int ExecInstruction(MIPSState *mips, MIPSOpcode op) {
case 28:
{
switch ((op.encoding >> 0) & 0x3f) {
+ // halt
+ case 0:
+ MIPSInt::Int_Halt(mips, op);
+ return 1;
// mfic, mtic
case 36:
case 38:
diff --git a/Core/MIPS/MIPSTables.cpp b/Core/MIPS/MIPSTables.cpp
index cc4f4f074c..223a1472ce 100644
--- a/Core/MIPS/MIPSTables.cpp
+++ b/Core/MIPS/MIPSTables.cpp
@@ -257,7 +257,7 @@ static const MIPSInstruction tableSpecial[64] = // 000000 ..... ..... ..... ....
// Theoretically should not hit these.
static const MIPSInstruction tableSpecial2[64] = // 011100 ..... ..... ..... ..... xxxxxx
{
- INSTR("halt", JITFUNC(Comp_Generic), Dis_Generic, 0, ENDS_BLOCK),
+ INSTR("halt", JITFUNC(Comp_Generic), Dis_Generic, Int_Halt, ENDS_BLOCK),
INVALID, INVALID, INVALID, INVALID, INVALID, INVALID, INVALID,
//8
INVALID_X_8,
diff --git a/Core/MIPS/MeCore.cpp b/Core/MIPS/MeCore.cpp
index 31142a2149..0c04be2911 100644
--- a/Core/MIPS/MeCore.cpp
+++ b/Core/MIPS/MeCore.cpp
@@ -15,36 +15,86 @@
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+#include
+
#include "Common/Log.h"
#include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Core/Config.h"
+#include "Core/Core.h"
#include "Core/CoreTiming.h"
+#include "Core/HLE/sceKernelInterrupt.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.
+// The ME's state after a reset, read on a PSP-1000 (a kernel-mode probe, not kept as a test: the
+// values left over from Sony's ME code aren't deterministic): COP0 register 22 is 1, which sends it
+// on to 0x40 from the reset vector; Status 0x20500404 (CU1, BEV, SR, IM2, ERL); the GPRs keep
+// whatever was in them, sp 0x801FFFE0 from Sony's code (top of the 2MB eDRAM); mfic 0; Count 0, and
+// it doesn't run, with ERL set or Status cleared.
+// Sysreg 0x40 read 0x20000002 and 0x50 0xD61D.
static const u32 ME_CPU_ID = 1;
+static const u32 RESET_STATUS = 0x20500404;
+static const u32 STATUS_RESET_BITS = 0x00500004;
+
+// Pending interrupts are checked between runs of this many cycles at most.
+static const int INTERRUPT_CHECK_CYCLES = 256;
+
+enum : u32 {
+ STATUS_IE = 0x01,
+ STATUS_EXL = 0x02,
+ STATUS_ERL = 0x04,
+ CAUSE_IP2 = 0x0400, // The interrupt controller.
+ CAUSE_IP7 = 0x8000, // Count reached Compare. Never on its own, since Count doesn't run.
+ CAUSE_IP_MASK = 0xFF00,
+ CAUSE_SOFT_IP = 0x0300,
+ CAUSE_EXCCODE_BD = 0x8000007C,
+};
MEState *g_meState = nullptr;
+static int meIntrEvent = -1;
// 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 MeIntrCallback(u64 userdata, int cyclesLate) {
+ __TriggerInterrupt(PSP_INTR_IMMEDIATE, PSP_MECODEC_INTR, PSP_INTR_SUB_NONE);
+}
+
+// Reset keeps the GPRs, and homebrew relies on that: mcidclan's samples run C on the stack Sony's
+// code left in sp.
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;
+ MIPSState &cpu = me->cpu;
+ cpu.cop0[30] = cpu.pc; // ErrorEPC
+ cpu.cop0[12] |= STATUS_RESET_BITS;
+ cpu.pc = Memory::BOOT_PAGE_ADDRESS;
+ cpu.nextPC = 0;
+ cpu.inDelaySlot = false;
+ cpu.llBit = 0;
+ cpu.cop0[9] = 0;
+ me->halted = false;
me->interruptEnable = 0;
+ me->intcRaised = 0;
+ me->intcEnabled = 0;
+}
+
+// What Sony's ME code leaves, which homebrew finds there.
+static void PowerOn(MEState *me) {
+ MIPSState &cpu = me->cpu;
+ cpu.InitRegisters();
+ cpu.cop0[12] = RESET_STATUS;
+ cpu.cop0[22] = ME_CPU_ID;
+ cpu.r[MIPS_REG_SP] = 0x80000000 + cpu.space->edramSize - 0x20;
+ me->ramConfig = 0x20000002;
+ me->busClock = 0xD61D;
+ // Held in reset, as the register says: a read-modify-write of it mustn't release the ME.
+ me->resetFlags = 4;
+ ResetCPU(me);
}
bool Init() {
@@ -58,7 +108,9 @@ bool Init() {
g_meState->cpu.space = space;
g_meState->cpu.noJit = true;
g_meState->tickBase = CoreTiming::GetTicks(&mipsr4k);
- ResetCPU(g_meState);
+ PowerOn(g_meState);
+ meIntrEvent = CoreTiming::RegisterEvent("MeIntr", &MeIntrCallback);
+ Memory::g_kernelModeAnywhere = true;
INFO_LOG(Log::ME, "Media Engine created, %d KB eDRAM", space->edramSize / 1024);
return true;
}
@@ -70,6 +122,7 @@ void Shutdown() {
delete g_meState;
g_meState = nullptr;
Memory::DestroyMESpace();
+ Memory::g_kernelModeAnywhere = false;
}
void DoState(PointerWrap &p) {
@@ -83,9 +136,13 @@ void DoState(PointerWrap &p) {
if (p.mode == PointerWrap::MODE_READ) {
if (!present) {
Shutdown();
- } else if (!g_meState && !Init()) {
- p.SetError(PointerWrap::ERROR_FAILURE);
- return;
+ } else if (!g_meState) {
+ if (!Init()) {
+ p.SetError(PointerWrap::ERROR_FAILURE);
+ return;
+ }
+ // Compiled for user mode.
+ mipsr4k.ClearJitCacheDeferred();
}
}
if (!g_meState) {
@@ -95,10 +152,50 @@ void DoState(PointerWrap &p) {
MEState *me = g_meState;
me->cpu.DoState(p);
Do(p, me->inReset);
+ Do(p, me->halted);
Do(p, me->tickBase);
Do(p, me->interruptEnable);
+ Do(p, me->intcRaised);
+ Do(p, me->intcEnabled);
+ Do(p, me->resetFlags);
+ Do(p, me->busClock);
+ Do(p, me->nmiFlags);
+ Do(p, me->ramConfig);
+ Do(p, me->mutex);
+ DoArray(p, me->memProtect, ARRAY_SIZE(me->memProtect));
DoArray(p, me->cpu.space->GetPointerUnchecked(0), me->cpu.space->edramSize);
DoArray(p, me->cpu.space->GetPointerUnchecked(0x04000000), me->cpu.space->localSize);
+ Do(p, meIntrEvent);
+ CoreTiming::RestoreRegisterEvent(meIntrEvent, "MeIntr", &MeIntrCallback);
+}
+
+static u32 PendingInterrupts(const MEState *me) {
+ u32 cause = me->cpu.cop0[13] & CAUSE_IP_MASK;
+ if (me->intcRaised & me->intcEnabled) {
+ cause |= CAUSE_IP2;
+ }
+ return cause;
+}
+
+// An unmasked interrupt wakes it from halt, and is taken if interrupts are enabled. Never in a
+// delay slot: the run loop only stops outside one.
+static void CheckInterrupts(MEState *me) {
+ MIPSState &cpu = me->cpu;
+ u32 &status = cpu.cop0[12];
+ if ((PendingInterrupts(me) & status & CAUSE_IP_MASK) == 0) {
+ return;
+ }
+ me->halted = false;
+ // mfic reads 0 after reset, and homebrew takes interrupts without mtic, so it doesn't gate them.
+ if ((status & (STATUS_IE | STATUS_EXL | STATUS_ERL)) != STATUS_IE) {
+ return;
+ }
+ // Interrupts go straight to EBase (COP0 25), with ExcCode 0.
+ cpu.cop0[14] = cpu.pc;
+ cpu.cop0[13] &= ~CAUSE_EXCCODE_BD;
+ status |= STATUS_EXL;
+ cpu.pc = cpu.cop0[25];
+ cpu.llBit = 0;
}
void RunFor(int cycles) {
@@ -110,8 +207,23 @@ void RunFor(int cycles) {
if (me->inReset) {
return;
}
- me->cpu.downcount += cycles;
- MIPSInterpret_RunDowncount(&me->cpu);
+ MIPSState &cpu = me->cpu;
+ cpu.downcount += cycles;
+ while (cpu.downcount >= 0 && coreState == CORE_RUNNING_CPU) {
+ CheckInterrupts(me);
+ if (me->halted) {
+ cpu.downcount = -1;
+ } else {
+ // Run a part, with the rest set aside so the clock stays right.
+ const int part = std::min(cpu.downcount, INTERRUPT_CHECK_CYCLES);
+ const int rest = cpu.downcount - part;
+ cpu.downcount = part;
+ me->tickBase -= rest;
+ MIPSInterpret_RunDowncount(&cpu);
+ cpu.downcount += rest;
+ me->tickBase += rest;
+ }
+ }
}
void SetReset(bool asserted) {
@@ -128,4 +240,44 @@ u64 GetTicks() {
return (u64)(g_meState->tickBase - g_meState->cpu.downcount);
}
+u32 ReadCop0(int reg) {
+ MEState *me = g_meState;
+ switch (reg) {
+ case 13:
+ return (me->cpu.cop0[13] & ~CAUSE_IP_MASK) | PendingInterrupts(me);
+ default:
+ return me->cpu.cop0[reg];
+ }
+}
+
+void WriteCop0(int reg, u32 value) {
+ MEState *me = g_meState;
+ switch (reg) {
+ case 11:
+ me->cpu.cop0[11] = value;
+ me->cpu.cop0[13] &= ~CAUSE_IP7;
+ break;
+ case 13:
+ me->cpu.cop0[13] = (me->cpu.cop0[13] & ~CAUSE_SOFT_IP) | (value & CAUSE_SOFT_IP);
+ break;
+ default:
+ me->cpu.cop0[reg] = value;
+ break;
+ }
+}
+
+void Halt() {
+ g_meState->halted = true;
+ // Ends the run, the rest of it spent halted.
+ g_meState->cpu.downcount = std::min(g_meState->cpu.downcount, -1);
+}
+
+void RaiseInterrupt(u32 sources) {
+ g_meState->intcRaised |= sources;
+}
+
+void InterruptMainCPU() {
+ CoreTiming::ScheduleEvent(0, meIntrEvent);
+}
+
} // namespace MeCore
diff --git a/Core/MIPS/MeCore.h b/Core/MIPS/MeCore.h
index e3ddf07368..537951cee9 100644
--- a/Core/MIPS/MeCore.h
+++ b/Core/MIPS/MeCore.h
@@ -29,14 +29,33 @@ class PointerWrap;
namespace MeCore {
+// The ME's interrupt controller's source for the main CPU's soft interrupt (0xBC100044).
+enum : u32 {
+ ME_INTR_SOFT = 0x80000000,
+};
+
struct MEState {
MIPSState cpu;
// Held in reset until the main CPU releases it.
bool inReset = true;
+ // Stopped by halt, until an interrupt it doesn't mask is raised.
+ bool halted = false;
// The ME's clock is tickBase - cpu.downcount.
s64 tickBase = 0;
// mfic/mtic's interrupt enable, which is per CPU.
u32 interruptEnable = 0;
+
+ // Its interrupt controller (0xBC300000): raised sources, and the ones that reach the CPU (IP2).
+ u32 intcRaised = 0;
+ u32 intcEnabled = 0;
+
+ // System registers (0xBC100000): those the ME code we run uses.
+ u32 resetFlags = 0; // 0x4C. Bit 2 holds the ME in reset.
+ u32 busClock = 0; // 0x50
+ u32 nmiFlags = 0; // 0x04
+ u32 ramConfig = 0; // 0x40. Bits 0-1: RAM size, 16 << n MB.
+ u32 mutex = 0; // 0x48. Bit 0 is the main CPU's, bit 1 the ME's.
+ u32 memProtect[20]{}; // 0xBC000000-0xBC00004C
};
// Null when the ME isn't emulated.
@@ -54,7 +73,17 @@ void DoState(PointerWrap &p);
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.
+// The ME's clock.
u64 GetTicks();
+// COP0 registers with behaviour on the ME: Compare (11), Cause (13).
+u32 ReadCop0(int reg);
+void WriteCop0(int reg, u32 value);
+void Halt();
+
+// Raises sources in the ME's interrupt controller.
+void RaiseInterrupt(u32 sources);
+// The ME interrupts the main CPU (it wrote 0xBC100044).
+void InterruptMainCPU();
+
} // namespace MeCore
diff --git a/Core/MemMap.cpp b/Core/MemMap.cpp
index 720d57c5b3..69ded52416 100644
--- a/Core/MemMap.cpp
+++ b/Core/MemMap.cpp
@@ -49,6 +49,7 @@ u8* base = nullptr;
u8 *blockShadow = nullptr;
AddressSpace g_mainSpace;
+bool g_kernelModeAnywhere = false;
static const size_t BLOCK_SHADOW_SIZE = (size_t)BLOCK_SHADOW_MASK + 1;
// The MemArena class
diff --git a/Core/MemMap.h b/Core/MemMap.h
index 627611034e..be2bea54a2 100644
--- a/Core/MemMap.h
+++ b/Core/MemMap.h
@@ -586,6 +586,17 @@ inline bool IsKernelCodeAddress(u32 address) {
return ((address & 0x0F800003) == 0x08000000) && address >= 0x08000100;
}
+// Homebrew that runs the Media Engine calls its own code in kernel mode, through a kernel module of
+// its own ("kcall"), so while the ME is emulated any code may access hardware registers.
+// HACK: This makes every block check its computed-address loads and stores for hardware registers.
+// Kernel mode should be part of the CPU's state instead, switched on while a kernel module's export
+// runs, with blocks checking that they were compiled for the mode they run in.
+extern bool g_kernelModeAnywhere;
+
+inline bool MayRunInKernelMode(u32 pc) {
+ return g_kernelModeAnywhere || IsKernelCodeAddress(pc);
+}
+
// I believe this is the same, but let's keep a separate function.
inline bool IsValidCLUTAddress(const u32 address) {
return IsValidTextureAddress(address);
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj
index 35bdb8bc70..a631a7493e 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj
+++ b/UWP/CoreUWP/CoreUWP.vcxproj
@@ -262,6 +262,7 @@
+
@@ -559,6 +560,7 @@
+
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters
index 7108a48265..530f196a30 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj.filters
+++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters
@@ -169,6 +169,7 @@
+
@@ -587,6 +588,7 @@
+
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index 2c0c6ff976..4554ccc1f4 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -657,6 +657,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Core/HW/Display.cpp \
$(SRC)/Core/HW/GpioMMIO.cpp \
$(SRC)/Core/HW/MMIO.cpp \
+ $(SRC)/Core/HW/MeMMIO.cpp \
$(SRC)/Core/HW/MemoryStick.cpp \
$(SRC)/Core/HW/MpegDemux.cpp.arm \
$(SRC)/Core/HW/AvcDecoder.cpp.arm \
diff --git a/libretro/Makefile.common b/libretro/Makefile.common
index 4181f46e4f..4ef0d59edd 100644
--- a/libretro/Makefile.common
+++ b/libretro/Makefile.common
@@ -849,6 +849,7 @@ SOURCES_CXX += \
$(COREDIR)/HW/Display.cpp \
$(COREDIR)/HW/GpioMMIO.cpp \
$(COREDIR)/HW/MMIO.cpp \
+ $(COREDIR)/HW/MeMMIO.cpp \
$(COREDIR)/HW/SimpleAudioDec.cpp \
$(COREDIR)/HW/Atrac3Standalone.cpp \
$(COREDIR)/HW/AsyncIOManager.cpp \
diff --git a/unittest/JitHarness.cpp b/unittest/JitHarness.cpp
index fac48cbeaa..9df6ce5890 100644
--- a/unittest/JitHarness.cpp
+++ b/unittest/JitHarness.cpp
@@ -758,7 +758,10 @@ bool TestMeCore() {
// Saved and loaded.
struct State {
- void DoState(PointerWrap &p) { MeCore::DoState(p); }
+ void DoState(PointerWrap &p) {
+ CoreTiming::DoState(p);
+ MeCore::DoState(p);
+ }
} state;
std::vector saved;
me->cpu.space->WriteUnchecked_U32(0xCAFE0001, 0x80001000);
@@ -784,6 +787,114 @@ bool TestMeCore() {
return success;
}
+// The main CPU releases the ME, which waits in halt for the main CPU's soft interrupt, and answers
+// each one with its own.
+bool TestMeInterrupts() {
+ 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;
+
+ auto assemble = [&](u32 addr, const std::vector &lines) {
+ for (const char *line : lines) {
+ std::string error;
+ if (!MipsAssembleOpcode(line, currentDebugMIPS, addr, &error)) {
+ printf("ERROR: %s: %s\n", line, error.c_str());
+ success = false;
+ }
+ addr += 4;
+ }
+ };
+ // Shared RAM, which the ME runs from too.
+ const u32 code = 0x08900000;
+ assemble(code, {
+ "lui k0, 0x0890",
+ "addiu k0, k0, 0x100",
+ "mtc0 k0, $25", // EBase: the handler
+ "lui k1, 0xBC30",
+ "lui k0, 0x8000",
+ "sw k0, 8(k1)", // the soft interrupt reaches the ME
+ "ori k0, zero, 0x401",
+ "mtc0 k0, $12", // IM2, IE
+ "nop",
+ "nop", // 0x08900024: halt, which armips doesn't know
+ "j 0x08900024",
+ "nop",
+ });
+ assemble(code + 0x100, {
+ "lui k1, 0xBC30",
+ "lui k0, 0x8000",
+ "sw k0, 0(k1)", // acknowledged
+ "lui k1, 0x0890",
+ "lw k0, 0x200(k1)",
+ "addiu k0, k0, 1",
+ "sw k0, 0x200(k1)",
+ "lui k1, 0xBC10",
+ "ori k0, zero, 1",
+ "sw k0, 0x44(k1)", // interrupts the main CPU
+ "eret",
+ });
+ Memory::WriteUnchecked_U32(0x70000000, code + 0x24);
+ // From the reset vector's 0x40.
+ u8 *page = Memory::GetBootPage();
+ const u32 jump[] = { MIPS_MAKE_LUI(MIPS_REG_T0, 0x0890), 0x01000008 /* jr t0 */, MIPS_MAKE_NOP() };
+ memcpy(page + 0x40, jump, sizeof(jump));
+ auto counter = [&]() { return Memory::ReadUnchecked_U32(code + 0x200); };
+
+ MMIO::Write(&mipsr4k, 0xBC10004C, 4, 0x04);
+ MMIO::Write(&mipsr4k, 0xBC10004C, 4, 0x00);
+ EXPECT_FALSE(me->inReset);
+ MeCore::RunFor(2000);
+ EXPECT_TRUE(me->halted);
+ EXPECT_EQ_HEX(me->cpu.pc, code + 0x28);
+ EXPECT_EQ_INT(counter(), 0);
+
+ for (int i = 1; i <= 2; ++i) {
+ MMIO::Write(&mipsr4k, 0xBC100044, 4, 1);
+ MeCore::RunFor(2000);
+ EXPECT_EQ_INT(counter(), i);
+ EXPECT_TRUE(me->halted);
+ EXPECT_EQ_HEX(me->intcRaised, 0);
+ EXPECT_EQ_HEX(me->cpu.cop0[12] & 2, 0);
+ }
+ EXPECT_TRUE(CoreTiming::GetScheduledEventsSummary().find("MeIntr") != std::string::npos);
+
+ // The mutex: each CPU only gets it while the other doesn't have it.
+ MMIO::Write(&mipsr4k, 0xBC100048, 4, 1);
+ MMIO::Write(&me->cpu, 0xBC100048, 4, 2);
+ EXPECT_EQ_HEX(MMIO::Read(&me->cpu, 0xBC100048, 4), 1);
+ MMIO::Write(&mipsr4k, 0xBC100048, 4, 0);
+ MMIO::Write(&me->cpu, 0xBC100048, 4, 2);
+ EXPECT_EQ_HEX(MMIO::Read(&mipsr4k, 0xBC100048, 4), 2);
+
+ // The main CPU has its own interrupt controller at the same address.
+ EXPECT_EQ_HEX(MMIO::Read(&mipsr4k, 0xBC300008, 4), MMIO::UNHANDLED_READ);
+ EXPECT_EQ_HEX(MMIO::Read(&me->cpu, 0xBC300008, 4), 0x80000000);
+
+ // Count doesn't run on the ME.
+ MeCore::WriteCop0(9, 1234);
+ MeCore::RunFor(1000);
+ EXPECT_EQ_INT(MeCore::ReadCop0(9), 1234);
+ EXPECT_TRUE(me->halted);
+
+ // Reset again.
+ MMIO::Write(&mipsr4k, 0xBC10004C, 4, 0x04);
+ EXPECT_TRUE(me->inReset);
+
+ 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 f48bff1b45..2c6efce620 100644
--- a/unittest/JitHarness.h
+++ b/unittest/JitHarness.h
@@ -24,6 +24,7 @@ bool TestCPUEret();
bool TestMMIO();
bool TestMESpace();
bool TestMeCore();
+bool TestMeInterrupts();
bool TestCPUEquivalence();
bool TestCPURoundingMode();
bool TestVFPUDotJit();
diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp
index dc8dcb48c5..ef503994b7 100644
--- a/unittest/UnitTest.cpp
+++ b/unittest/UnitTest.cpp
@@ -3737,6 +3737,7 @@ TestItem availableTests[] = {
TEST_ITEM(MMIO),
TEST_ITEM(MESpace),
TEST_ITEM(MeCore),
+ TEST_ITEM(MeInterrupts),
TEST_ITEM(CPUEquivalence),
TEST_ITEM(CPURoundingMode),
TEST_ITEM(VFPUDotJit),