mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-16 16:00:30 +02:00
A JIT only switched modes around syscalls, and only took the mode into account in conversions, once it had compiled a ctc1 (hasSetRounding). A mode from an interpreted ctc1, a savestate made on another core, a core switch or a thread switch went unnoticed, and HLE then ran in the guest's mode. UpdateFCR31() now sets the flag when fcr31 isn't the default, and clears the block cache at the next safe point, since blocks compiled before don't switch either. A new JIT gets an UpdateFCR31() too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
455 lines
15 KiB
C++
455 lines
15 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.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);
|
|
}
|
|
|
|
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);
|
|
ir.Write(IROp::Interpret, 0, 0, 0, op.encoding);
|
|
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.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
|