mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-14 23:10:33 +02:00
The JITs used to find a block by overwriting its first instruction with an "emuhack" holding the offset into the code space, and replacement functions installed their own emuhack the same way. Anything that read code (savestates, the debugger, memory dumps, cheats, the function hasher, games reading their own code) had to translate them back. Blocks now live in a block shadow: one u32 per guest instruction, indexed by the PC masked with BLOCK_SHADOW_MASK (1GB on 64-bit, folding the kernel and uncached mirrors; 256MB on 32-bit, iOS and Switch, where address space is short), with the low bits cleared so a misaligned PC can't read across two entries. Only the ranges under memory views are committed, so a bad PC still faults in the dispatcher, and the shadow is reserved before the memory map so it can't land in a gap of the 4GB window. Entries hold full 32-bit offsets, lifting the 16MB limit. The dispatchers on every backend, and the IR interpreter's loop, look blocks up there. A hooked instruction without a compiled block gets the JIT's hook value in its entry: the offset of the dispatcher's compile path (1 for the interpreters, the IR arena keeping that offset empty), so dispatching there compiles a block, and the frontends substitute the hook. Compiling replaces the entry with the block's, and destroying the block puts the hook value back. The hook value is rewritten when the JIT changes or regenerates its dispatcher. Hooks live in a map keyed by the masked address, and are dropped when the game overwrites the hooked instruction, or when an icache invalidation finds the function's hash (immediates masked, as when matching it) changed. Those checks run when queued invalidations are processed, since cheats and the debugger queue them before writing. Installing and removing hooks invalidates deferred, as it happens in HLE calls that can run inside a block. The cache instruction goes through MIPSState, so it drops changed hooks too. With clean memory, the save/clear/restore around savestates, memory reads and dumps goes away, as do the source-range invalidations in the memcpy replacements, sceDmac and the cheats. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
432 lines
14 KiB
C++
432 lines
14 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/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.SetSkipFirst(0);
|
|
}
|
|
|
|
void IRFrontend::DoState(PointerWrap &p) {
|
|
auto s = p.Section("Jit", 1, 2);
|
|
if (!s)
|
|
return;
|
|
|
|
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.SetSkipFirst(0);
|
|
}
|
|
|
|
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_EMUHACK_VALUE_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();
|
|
ir.Write(IROp::Interpret, 0, 0, 0, op.encoding);
|
|
const MIPSInfo info = MIPSGetInfo(op);
|
|
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.initialBlockSize = 0;
|
|
js.nextExit = 0;
|
|
js.downcountAmount = 0;
|
|
js.curBlock = nullptr;
|
|
js.compiling = true;
|
|
js.hadBreakpoints = false;
|
|
js.blockWrotePrefixes = false;
|
|
js.inDelaySlot = false;
|
|
js.kernelMode = Memory::IsKernelCodeAddress(em_address);
|
|
|
|
js.PrefixStart();
|
|
ir.Clear();
|
|
ir.Reserve(64); // Estimate a reasonable number of IR instructions per block
|
|
|
|
js.numInstructions = 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,
|
|
&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
|