Files
ppsspp/Core/MIPS/IR/IRFrontend.cpp
T
Henrik RydgårdandClaude Opus 5.5 e52cc38866 ME: Reset, interrupts and the other registers homebrew runs the ME with
Sysreg (0xBC100000) gets the ME's reset bit (0x4C bit 2), the soft
interrupt (0x44, which interrupts the other CPU: the ME's controller from
the main CPU, PSP_MECODEC_INTR from the ME), the hardware mutex (0x48,
each CPU sets its own bit only while the other's is clear) and the clock
and config registers it writes. The ME's interrupt controller (0xBC300000,
write-1-to-clear, mask at 8) answers the ME only. All of it is the ME's
state and stays unhandled without an ME.

On the ME: interrupts to EBase with EPC and EXL, halt sleeping until an
unmasked interrupt, and Count, which doesn't run: it stays 0 across 10ms
with ERL set after reset and with Status cleared, and reads back what was
written. Reset keeps the GPRs and sets Status ERL/BEV/SR, which homebrew
relies on: mcidclan's samples run C on the stack Sony's ME code left. The
values are from a kernel probe on a PSP-1000 (sp 0x801FFFE0, COP0 22 = 1,
Status 0x20500404, mfic 0, which doesn't gate interrupts).

The main CPU runs base interrupt handlers the way interruptman calls them
(a0 intno, a1 arg, a2 interrupted pc; -2 disables), and interrupt 31 has
a stand-in for me_wrapper's handler, which homebrew releases first.

While the ME is emulated, any main CPU code may access hardware registers:
homebrew calls its own code in kernel mode through a kcall module. This
is a stopgap (g_kernelModeAnywhere, marked HACK): kernel mode should be
part of the CPU's state, switched on while a kernel module's export runs.
The memcpy/memmove replacements copy through MMIO when an end is a
register address, for code copied to the boot page.

mcidclan's me-soft-interrupt sample now runs, counting each interrupt in
both directions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 09:33:26 +02:00

484 lines
17 KiB
C++

// 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/Debugger/Breakpoints.h"
#include "Core/Debugger/SymbolMap.h"
#include "Core/Reporting.h"
#include "Core/HLE/ReplaceTables.h"
#include "Core/MemMap.h"
#include "Core/MIPS/MIPSCodeUtils.h"
#include "Core/MIPS/MIPSTables.h"
#include "Core/MIPS/IR/IRFrontend.h"
#include "Core/MIPS/IR/IRRegCache.h"
#include "Core/MIPS/IR/IRPassSimplify.h"
#include "Core/MIPS/IR/IRInterpreter.h"
#include "Core/MIPS/MIPSTracer.h"
#include <iterator>
namespace MIPSComp {
IRFrontend::IRFrontend(bool startDefaultPrefix) {
js.startDefaultPrefix = startDefaultPrefix;
js.hasSetRounding = false;
// The debugger sets this so that "go" on a breakpoint will actually... go.
// But if they reset, we can end up hitting it by mistake, since it's based on PC and ticks.
g_breakpoints.ClearSkipFirst();
}
bool IRFrontend::DoState(PointerWrap &p) {
auto s = p.Section("Jit", 1, 2);
if (!s)
return false;
const bool oldStartDefaultPrefix = js.startDefaultPrefix;
const bool oldHasSetRounding = js.hasSetRounding;
const u8 oldLastSetRounding = js.lastSetRounding;
Do(p, js.startDefaultPrefix);
if (s >= 2) {
Do(p, js.hasSetRounding);
if (p.mode == p.MODE_READ) {
js.lastSetRounding = 0;
}
} else {
js.hasSetRounding = 1;
}
// The debugger sets this so that "go" on a breakpoint will actually... go.
// But if they reset, we can end up hitting it by mistake, since it's based on PC and ticks.
g_breakpoints.ClearSkipFirst();
if (p.mode != PointerWrap::MODE_READ) {
return false;
}
// Blocks only switch rounding modes if hasSetRounding was set when they were compiled, and
// assume default prefixes at their start if startDefaultPrefix was.
if ((js.hasSetRounding && !oldHasSetRounding) || (!js.startDefaultPrefix && oldStartDefaultPrefix)) {
return true;
}
// The other way round, the blocks already compiled just do more than they need to. Keep the
// flags that way, or a loaded hasSetRounding of false would have UpdateFCR31() throw them all
// away for a rounding mode they handle fine. lastSetRounding says they were compiled with the
// rounding checks, so the next compile doesn't throw them away to add them either.
js.hasSetRounding = js.hasSetRounding || oldHasSetRounding;
js.startDefaultPrefix = js.startDefaultPrefix && oldStartDefaultPrefix;
js.lastSetRounding = oldLastSetRounding;
return false;
}
bool IRFrontend::UpdateFCR31(u32 fcr31) {
// fcr31 can reach a non-default mode without a compiled ctc1 (an interpreted one, a savestate,
// a core switch, a thread switch). Blocks compiled before that don't switch modes around
// syscalls or conversions, so the caller throws them away.
if ((fcr31 & 0x01000003) && !js.hasSetRounding) {
js.hasSetRounding = true;
js.lastSetRounding = true;
return true;
}
return false;
}
void IRFrontend::FlushAll() {
FlushPrefixV();
}
void IRFrontend::FlushPrefixV() {
if (js.startDefaultPrefix && !js.blockWrotePrefixes && js.HasNoPrefix()) {
// They started default, we never modified in memory, and they're default now.
// No reason to modify memory. This is common at end of blocks. Just clear dirty.
js.prefixSFlag = (JitState::PrefixState)(js.prefixSFlag & ~JitState::PREFIX_DIRTY);
js.prefixTFlag = (JitState::PrefixState)(js.prefixTFlag & ~JitState::PREFIX_DIRTY);
js.prefixDFlag = (JitState::PrefixState)(js.prefixDFlag & ~JitState::PREFIX_DIRTY);
return;
}
if ((js.prefixSFlag & JitState::PREFIX_DIRTY) != 0) {
ir.Write(IROp::SetCtrlVFPU, VFPU_CTRL_SPREFIX, 0, 0, js.prefixS);
js.prefixSFlag = (JitState::PrefixState) (js.prefixSFlag & ~JitState::PREFIX_DIRTY);
}
if ((js.prefixTFlag & JitState::PREFIX_DIRTY) != 0) {
ir.Write(IROp::SetCtrlVFPU, VFPU_CTRL_TPREFIX, 0, 0, js.prefixT);
js.prefixTFlag = (JitState::PrefixState) (js.prefixTFlag & ~JitState::PREFIX_DIRTY);
}
if ((js.prefixDFlag & JitState::PREFIX_DIRTY) != 0) {
ir.Write(IROp::SetCtrlVFPU, VFPU_CTRL_DPREFIX, 0, 0, js.prefixD);
js.prefixDFlag = (JitState::PrefixState) (js.prefixDFlag & ~JitState::PREFIX_DIRTY);
}
// If we got here, we must've written prefixes to memory in this block.
js.blockWrotePrefixes = true;
}
void IRFrontend::EatInstruction(MIPSOpcode op) {
MIPSInfo info = MIPSGetInfo(op);
if (info & DELAYSLOT) {
ERROR_LOG_REPORT_ONCE(ateDelaySlot, Log::JIT, "Ate a branch op.");
}
if (js.inDelaySlot) {
ERROR_LOG_REPORT_ONCE(ateInDelaySlot, Log::JIT, "Ate an instruction inside a delay slot.");
}
CheckBreakpoint(GetCompilerPC() + 4);
js.numInstructions++;
js.compilerPC += 4;
js.downcountAmount += MIPSGetInstructionCycleEstimate(op);
}
void IRFrontend::CompileDelaySlot() {
js.inDelaySlot = true;
CheckBreakpoint(GetCompilerPC() + 4);
MIPSOpcode op = GetOffsetInstruction(1);
MIPSCompileOp(op, this);
js.inDelaySlot = false;
}
bool IRFrontend::CheckRounding(u32 blockAddress) {
bool cleanSlate = false;
if (js.hasSetRounding && !js.lastSetRounding) {
WARN_LOG(Log::JIT, "Detected rounding mode usage, rebuilding jit with checks");
// Won't loop, since hasSetRounding is only ever set to 1.
js.lastSetRounding = js.hasSetRounding;
cleanSlate = true;
}
// Drat. The VFPU hit an uneaten prefix at the end of a block.
if (js.startDefaultPrefix && js.MayHavePrefix()) {
WARN_LOG_REPORT(Log::JIT, "An uneaten prefix at end of block for %08x", blockAddress);
logBlocks = 1;
js.LogPrefix();
// Let's try that one more time. We won't get back here because we toggled the value.
js.startDefaultPrefix = false;
cleanSlate = true;
}
return cleanSlate;
}
void IRFrontend::Comp_ReplacementFunc(MIPSOpcode op) {
// Memory never holds these: if it does, the game is executing data. Let the interpreter raise it.
if (Memory::Read_Instruction(GetCompilerPC()).encoding == op.encoding) {
// The interpreter checks memory at PC, which Comp_Generic doesn't set here.
FlushAll();
ir.Write(IROp::SetPCConst, 0, 0, 0, GetCompilerPC());
Comp_Generic(op);
return;
}
int index = op.encoding & MIPS_REPLACEMENT_INDEX_MASK;
const ReplacementTableEntry *entry = GetReplacementFunc(index);
if (!entry) {
ERROR_LOG(Log::HLE, "Invalid replacement op %08x", op.encoding);
return;
}
u32 funcSize = g_symbolMap->GetFunctionSize(GetCompilerPC());
bool disabled = (entry->flags & REPFLAG_DISABLED) != 0;
if (!disabled && funcSize != SymbolMap::INVALID_ADDRESS && funcSize > sizeof(u32)) {
// We don't need to disable hooks, the code will still run.
if ((entry->flags & (REPFLAG_HOOKENTER | REPFLAG_HOOKEXIT)) == 0) {
// Any breakpoint at the func entry was already tripped, so we can still run the replacement.
// That's a common case - just to see how often the replacement hits.
disabled = g_breakpoints.RangeContainsBreakPoint(GetCompilerPC() + sizeof(u32), funcSize - sizeof(u32));
}
}
if (disabled) {
MIPSCompileOp(Memory::Read_Instruction(GetCompilerPC()), this);
} else if (entry->replaceFunc) {
FlushAll();
RestoreRoundingMode();
ir.Write(IROp::SetPCConst, 0, 0, 0, GetCompilerPC());
ir.Write(IROp::CallReplacement, IRTEMP_0, 0, 0, index);
if (entry->flags & (REPFLAG_HOOKENTER | REPFLAG_HOOKEXIT)) {
// Compile the original instruction at this address. We ignore cycles for hooks.
ApplyRoundingMode();
MIPSCompileOp(Memory::Read_Instruction(GetCompilerPC()), this);
} else {
ApplyRoundingMode();
// If IRTEMP_0 was set to 1, it means the replacement needs to run again (sliced.)
// This is necessary for replacements that take a lot of cycles.
ir.Write(IROp::Downcount, 0, 0, 0, js.downcountAmount);
ir.Write(IROp::ExitToConstIfNeq, 0, IRTEMP_0, MIPS_REG_ZERO, GetCompilerPC());
ir.Write(IROp::ExitToReg, 0, MIPS_REG_RA, 0);
js.compiling = false;
}
} else {
ERROR_LOG(Log::HLE, "Replacement function %s has neither jit nor regular impl", entry->name);
}
}
void IRFrontend::Comp_Generic(MIPSOpcode op) {
FlushAll();
const MIPSInfo info = MIPSGetInfo(op);
// Interpreted ops run in the guest's rounding mode. A syscall is host code (like a compiled one),
// and only ctc1 to fcr31 changes the mode, so only those need it switched.
const bool isSyscall = (info & IS_SYSCALL) != 0;
const bool setsMode = MIPS_IS_CTC1_FCR31(op.encoding);
if (isSyscall || setsMode)
RestoreRoundingMode(setsMode);
const bool endsBlock = (info & ENDS_BLOCK) != 0;
if (endsBlock) {
ir.Write(IROp::Downcount, 0, 0, 0, js.downcountAmount);
js.downcountAmount = 0;
}
ir.Write(IROp::Interpret, 0, 0, 0, op.encoding);
if (endsBlock) {
ir.Write(IROp::ExitToPC);
js.compiling = false;
return;
}
if (setsMode)
UpdateRoundingMode();
if (isSyscall || setsMode)
ApplyRoundingMode(setsMode);
if ((info & IS_VFPU) != 0 && (info & VFPU_NO_PREFIX) == 0) {
// If it does eat them, it'll happen in MIPSCompileOp().
if ((info & OUT_EAT_PREFIX) == 0)
js.PrefixUnknown();
// Even if DISABLE'd, we want to set this flag so we overwrite.
if ((info & OUT_VFPU_PREFIX) != 0)
js.blockWrotePrefixes = true;
}
}
// Destroys SCRATCH2
void IRFrontend::RestoreRoundingMode(bool force) {
// If the game has never set an interesting rounding mode, we can safely skip this.
if (force || js.hasSetRounding) {
ir.Write(IROp::RestoreRoundingMode);
}
}
// Destroys SCRATCH1 and SCRATCH2
void IRFrontend::ApplyRoundingMode(bool force) {
// If the game has never set an interesting rounding mode, we can safely skip this.
if (force || js.hasSetRounding) {
ir.Write(IROp::ApplyRoundingMode);
}
}
// Destroys SCRATCH1 and SCRATCH2
void IRFrontend::UpdateRoundingMode() {
// We must set js.hasSetRounding at compile time, or this block will use the wrong rounding mode.
js.hasSetRounding = true;
ir.Write(IROp::UpdateRoundingMode);
}
void IRFrontend::Comp_DoNothing(MIPSOpcode op) {
}
int IRFrontend::Replace_fabsf() {
Crash();
return 0;
}
u32 IRFrontend::GetCompilerPC() {
return js.compilerPC;
}
MIPSOpcode IRFrontend::GetOffsetInstruction(int offset) {
const u32 addr = GetCompilerPC() + 4 * offset;
return Memory::IsValid4AlignedAddress(addr) ? ReadExecutedOp(addr) : Memory::Read_Instruction(addr);
}
void IRFrontend::DoJit(u32 em_address, std::vector<IRInst> &instructions, u32 &mipsBytes) {
js.cancel = false;
js.blockStart = em_address;
js.compilerPC = em_address;
js.downcountAmount = 0;
js.compiling = true;
js.hadBreakpoints = false;
js.blockWrotePrefixes = false;
js.inDelaySlot = false;
js.kernelMode = Memory::MayRunInKernelMode(em_address);
js.PrefixStart();
ir.Clear();
ir.Reserve(64); // Estimate a reasonable number of IR instructions per block
js.numInstructions = 0;
js.continuedBranches = 0;
while (js.compiling) {
// Jit breakpoints are quite fast, so let's do them in release too.
CheckBreakpoint(GetCompilerPC());
MIPSOpcode inst = ReadExecutedOp(GetCompilerPC());
js.downcountAmount += MIPSGetInstructionCycleEstimate(inst);
MIPSCompileOp(inst, this);
js.compilerPC += 4;
js.numInstructions++;
}
if (js.cancel) {
// Clear the instructions to signal this was not compiled.
ir.Clear();
}
mipsBytes = js.compilerPC - em_address;
IRWriter simplified;
IRWriter *code = &ir;
if (!js.hadBreakpoints) {
std::vector<IRPassFunc> passes{
&ApplyMemoryValidation,
&RemoveLoadStoreLeftRight,
&OptimizeFPMoves,
&PropagateConstants,
&ResolveKernelMemory,
&PurgeTemps,
&ReduceVec4Flush,
&OptimizeLoadsAfterStores,
// &ReorderLoadStore,
// &MergeLoadStore,
// &ThreeOpToTwoOp,
};
if (opts.optimizeForInterpreter) {
// Add special passes here.
passes.push_back(&OptimizeForInterpreter);
}
if (IRApplyPasses(passes.data(), passes.size(), ir, simplified, opts))
logBlocks = 1;
code = &simplified;
//if (ir.GetInstructions().size() >= 24)
// logBlocks = 1;
}
if (!mipsTracer.tracing_enabled) {
instructions = code->GetInstructions();
}
else {
std::vector<IRInst> block_instructions = code->GetInstructions();
instructions.reserve(block_instructions.size() + 2); // +2 for Downcount and LogIRBlock
// The first instruction is "Downcount"
instructions.push_back(block_instructions.front());
// prepare_block() fills in the constant, unless it fails to record the block.
instructions.push_back({ IROp::LogIRBlock, {0}, 0, 0, INVALID_TRACE_INDEX });
std::copy(block_instructions.begin() + 1, block_instructions.end(), std::back_inserter(instructions));
}
if (logBlocks > 0 && dontLogBlocks == 0) {
char temp2[256];
NOTICE_LOG(Log::JIT, "=============== mips %08x ===============", em_address);
for (u32 cpc = em_address; cpc != GetCompilerPC(); cpc += 4) {
temp2[0] = 0;
MIPSDisAsm(Memory::ReadUnchecked_Instruction(cpc), cpc, temp2, sizeof(temp2), true);
NOTICE_LOG(Log::JIT, "M: %08x %s", cpc, temp2);
}
}
if (logBlocks > 0 && dontLogBlocks == 0) {
NOTICE_LOG(Log::JIT, "=============== Original IR (%d instructions) ===============", (int)ir.GetInstructions().size());
for (size_t i = 0; i < ir.GetInstructions().size(); i++) {
char buf[256];
DisassembleIR(buf, sizeof(buf), ir.GetInstructions()[i]);
NOTICE_LOG(Log::JIT, "%s", buf);
}
NOTICE_LOG(Log::JIT, "=============== end =================");
}
if (logBlocks > 0 && dontLogBlocks == 0) {
NOTICE_LOG(Log::JIT, "=============== IR (%d instructions) ===============", (int)code->GetInstructions().size());
for (size_t i = 0; i < code->GetInstructions().size(); i++) {
char buf[256];
DisassembleIR(buf, sizeof(buf), code->GetInstructions()[i]);
NOTICE_LOG(Log::JIT, "%s", buf);
}
NOTICE_LOG(Log::JIT, "=============== end =================");
}
if (logBlocks > 0)
logBlocks--;
if (dontLogBlocks > 0)
dontLogBlocks--;
}
void IRFrontend::CheckBreakpoint(u32 addr) {
if (g_breakpoints.NeedsBreakCheckAt(addr)) {
FlushAll();
// Can't skip this even at the start of a block, might impact block linking.
ir.Write(IROp::SetPCConst, 0, 0, 0, GetCompilerPC());
RestoreRoundingMode();
// At this point, downcount HAS the delay slot, but not the instruction itself.
int downcountOffset = 0;
if (js.inDelaySlot) {
MIPSOpcode branchOp = Memory::ReadUnchecked_Instruction(GetCompilerPC());
MIPSOpcode delayOp = Memory::ReadUnchecked_Instruction(addr);
downcountOffset = -MIPSGetInstructionCycleEstimate(delayOp);
if ((MIPSGetInfo(branchOp) & LIKELY) != 0) {
// Okay, we're in a likely branch. Also negate the branch cycles.
downcountOffset += -MIPSGetInstructionCycleEstimate(branchOp);
}
}
int downcountAmount = js.downcountAmount + downcountOffset;
if (downcountAmount != 0) {
ir.Write(IROp::Downcount, 0, 0, 0, downcountAmount);
}
// Note that this means downcount can't be metadata on the block.
js.downcountAmount = -downcountOffset;
ir.Write(IROp::Breakpoint, 0, 0, 0, addr);
ApplyRoundingMode();
js.hadBreakpoints = true;
}
}
void IRFrontend::CheckMemoryBreakpoint(int rs, int offset) {
if (g_breakpoints.HasMemChecks()) {
FlushAll();
// Can't skip this even at the start of a block, might impact block linking.
ir.Write(IROp::SetPCConst, 0, 0, 0, GetCompilerPC());
RestoreRoundingMode();
// At this point, downcount HAS the delay slot, but not the instruction itself.
int downcountOffset = 0;
if (js.inDelaySlot) {
// We assume delay slot in compilerPC + 4.
MIPSOpcode branchOp = Memory::ReadUnchecked_Instruction(GetCompilerPC());
MIPSOpcode delayOp = Memory::ReadUnchecked_Instruction(GetCompilerPC() + 4);
downcountOffset = -MIPSGetInstructionCycleEstimate(delayOp);
if ((MIPSGetInfo(branchOp) & LIKELY) != 0) {
// Okay, we're in a likely branch. Also negate the branch cycles.
downcountOffset += -MIPSGetInstructionCycleEstimate(branchOp);
}
}
int downcountAmount = js.downcountAmount + downcountOffset;
if (downcountAmount != 0)
ir.Write(IROp::Downcount, 0, 0, 0, downcountAmount);
// Note that this means downcount can't be metadata on the block.
js.downcountAmount = -downcountOffset;
ir.Write(IROp::MemoryCheck, js.inDelaySlot ? 4 : 0, rs, 0, offset);
ApplyRoundingMode();
js.hadBreakpoints = true;
}
}
} // namespace