Files
ppsspp/Core/MIPS/ARM/ArmJit.cpp
T
Henrik RydgårdandClaude Opus 5.5 6d122f416e Keep JIT block entries and replacement hooks out of PSP memory
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>
2026-10-11 10:33:30 +02:00

781 lines
24 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 "ppsspp_config.h"
#if PPSSPP_ARCH(ARM)
#include "Common/Profiler/Profiler.h"
#include "Common/Log.h"
#include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Core/Reporting.h"
#include "Core/Config.h"
#include "Core/Core.h"
#include "Core/CoreTiming.h"
#include "Core/Debugger/Breakpoints.h"
#include "Core/Debugger/SymbolMap.h"
#include "Core/MemMap.h"
#include "Core/MIPS/MIPS.h"
#include "Core/MIPS/MIPSAnalyst.h"
#include "Core/MIPS/MIPSCodeUtils.h"
#include "Core/MIPS/Interpreter.h"
#include "Core/MIPS/MIPSTables.h"
#include "Core/HLE/ReplaceTables.h"
#include "Core/MIPS/ARM/ArmRegCache.h"
#include "Core/MIPS/ARM/ArmRegCacheFPU.h"
#include "ArmRegCache.h"
#include "ArmJit.h"
#include "CPUDetect.h"
#include "ext/disarm.h"
using namespace ArmJitConstants;
void DisassembleArm(const u8 *data, int size) {
char temp[256];
for (int i = 0; i < size; i += 4) {
const u32 *codePtr = (const u32 *)(data + i);
u32 inst = codePtr[0];
u32 next = (i < size - 4) ? codePtr[1] : 0;
// MAGIC SPECIAL CASE for MOVW/MOVT readability!
if ((inst & 0x0FF00000) == 0x03000000 && (next & 0x0FF00000) == 0x03400000) {
u32 low = ((inst & 0x000F0000) >> 4) | (inst & 0x0FFF);
u32 hi = ((next & 0x000F0000) >> 4) | (next & 0x0FFF);
int reg0 = (inst & 0x0000F000) >> 12;
int reg1 = (next & 0x0000F000) >> 12;
if (reg0 == reg1) {
snprintf(temp, sizeof(temp), "%08x MOV32 %s, %04x%04x", (u32)inst, ArmRegName(reg0), hi, low);
INFO_LOG(Log::JIT, "A: %s", temp);
i += 4;
continue;
}
}
ArmDis((u32)codePtr, inst, temp, sizeof(temp), true);
INFO_LOG(Log::JIT, "A: %s", temp);
}
}
static u32 JitBreakpoint(uint32_t addr) {
// Should we skip this breakpoint?
if (g_breakpoints.CheckSkipFirst() == currentMIPS->pc || g_breakpoints.CheckSkipFirst() == addr)
return 0;
BreakAction result = g_breakpoints.ExecBreakPoint(addr);
if ((result & BREAK_ACTION_PAUSE) == 0)
return 0;
return 1;
}
static u32 JitMemCheck(u32 pc) {
if (g_breakpoints.CheckSkipFirst() == currentMIPS->pc)
return 0;
// Note: pc may be the delay slot.
const auto op = Memory::Read_Instruction(pc);
s32 offset = SignExtend16ToS32(op & 0xFFFF);
if (MIPSGetInfo(op) & IS_VFPU)
offset &= 0xFFFC;
u32 addr = currentMIPS->r[MIPS_GET_RS(op)] + offset;
g_breakpoints.ExecOpMemCheck(addr, pc);
return coreState == CORE_RUNNING_CPU || coreState == CORE_NEXTFRAME ? 0 : 1;
}
namespace MIPSComp {
using namespace ArmGen;
using namespace ArmJitConstants;
ArmJit::ArmJit(MIPSState *mipsState) : blocks(mipsState, this), gpr(mipsState, &js, &jo), fpr(mipsState, &js, &jo), mips_(mipsState) {
logBlocks = 0;
dontLogBlocks = 0;
blocks.Init();
gpr.SetEmitter(this);
fpr.SetEmitter(this);
AllocCodeSpace(1024 * 1024 * 16); // 32MB is the absolute max because that's what an ARM branch instruction can reach, backwards and forwards.
GenerateFixedCode();
INFO_LOG(Log::JIT, "ARM JIT initialized: %lld MB of code space", (long long)(GetSpaceLeft() / (1024 * 1024)));
js.startDefaultPrefix = mips_->HasDefaultPrefix();
// 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);
}
ArmJit::~ArmJit() {
}
void ArmJit::DoState(PointerWrap &p)
{
auto s = p.Section("Jit", 1, 2);
if (!s)
return;
Do(p, js.startDefaultPrefix);
if (p.mode == PointerWrap::MODE_READ && !js.startDefaultPrefix) {
WARN_LOG(Log::CPU, "Jit: An uneaten prefix was previously detected. Jitting in unknown-prefix mode.");
}
if (s >= 2) {
Do(p, js.hasSetRounding);
if (p.mode == PointerWrap::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 ArmJit::UpdateFCR31() {
}
void ArmJit::FlushAll()
{
gpr.FlushAll();
fpr.FlushAll();
FlushPrefixV();
}
void ArmJit::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) {
gpr.SetRegImm(SCRATCHREG1, js.prefixS);
STR(SCRATCHREG1, CTXREG, offsetof(MIPSState, vfpuCtrl[VFPU_CTRL_SPREFIX]));
js.prefixSFlag = (JitState::PrefixState) (js.prefixSFlag & ~JitState::PREFIX_DIRTY);
}
if ((js.prefixTFlag & JitState::PREFIX_DIRTY) != 0) {
gpr.SetRegImm(SCRATCHREG1, js.prefixT);
STR(SCRATCHREG1, CTXREG, offsetof(MIPSState, vfpuCtrl[VFPU_CTRL_TPREFIX]));
js.prefixTFlag = (JitState::PrefixState) (js.prefixTFlag & ~JitState::PREFIX_DIRTY);
}
if ((js.prefixDFlag & JitState::PREFIX_DIRTY) != 0) {
gpr.SetRegImm(SCRATCHREG1, js.prefixD);
STR(SCRATCHREG1, CTXREG, offsetof(MIPSState, vfpuCtrl[VFPU_CTRL_DPREFIX]));
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 ArmJit::ClearCache()
{
blocks.Clear();
ClearCodeSpace(0);
GenerateFixedCode();
// The compile path may have moved, and replacement hooks' block shadow entries point to it.
Replacement_SetBlockShadowHook(GetBlockShadowHookValue());
}
void ArmJit::InvalidateCacheAt(u32 em_address, int length) {
if (blocks.RangeMayHaveBlocks(em_address, em_address + length)) {
blocks.InvalidateICache(em_address, length);
}
}
void ArmJit::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.");
}
CheckJitBreakpoint(GetCompilerPC() + 4, 0);
js.numInstructions++;
js.compilerPC += 4;
js.downcountAmount += MIPSGetInstructionCycleEstimate(op);
}
void ArmJit::CompileDelaySlot(int flags) {
// Need to offset the downcount which was already incremented for the branch + delay slot.
CheckJitBreakpoint(GetCompilerPC() + 4, -2);
// preserve flag around the delay slot! Maybe this is not always necessary on ARM where
// we can (mostly) control whether we set the flag or not. Of course, if someone puts an slt in to the
// delay slot, we're screwed.
if (flags & DELAYSLOT_SAFE)
MRS(R8); // Save flags register. R8 is preserved through function calls and is not allocated.
js.inDelaySlot = true;
MIPSOpcode op = GetOffsetInstruction(1);
MIPSCompileOp(op, this);
js.inDelaySlot = false;
if (flags & DELAYSLOT_FLUSH)
FlushAll();
if (flags & DELAYSLOT_SAFE)
_MSR(true, false, R8); // Restore flags register
}
void ArmJit::Compile(u32 em_address) {
PROFILE_THIS_SCOPE("jitc");
if (!Memory::IsValid4AlignedAddress(em_address)) {
Core_ExecException(em_address, em_address, ExecExceptionType::JUMP);
return;
}
if (GetSpaceLeft() < 0x10000 || blocks.IsFull()) {
ClearCache();
}
BeginWrite(JitBlockCache::MAX_BLOCK_INSTRUCTIONS * 16);
int block_num = blocks.AllocateBlock(em_address);
JitBlock *b = blocks.GetBlock(block_num);
b->DoIntegrityCheck(em_address, block_num, "BeforeJit");
DoJit(em_address, b);
b->DoIntegrityCheck(em_address, block_num, "BeforeFinalize");
blocks.FinalizeBlock(block_num, jo.enableBlocklink);
b->DoIntegrityCheck(em_address, block_num, "AfterFinalize");
EndWrite();
_dbg_assert_(js.nextExit <= 2);
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: %08x", GetCompilerPC() - 4);
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;
}
if (cleanSlate) {
// Our assumptions are all wrong so it's clean-slate time.
ClearCache();
Compile(em_address);
}
}
void ArmJit::RunLoopUntil(u64 globalticks) {
PROFILE_THIS_SCOPE("jit");
((void (*)())enterDispatcher)();
}
u32 ArmJit::GetCompilerPC() {
return js.compilerPC;
}
MIPSOpcode ArmJit::GetOffsetInstruction(int offset) {
const u32 addr = GetCompilerPC() + 4 * offset;
return Memory::IsValid4AlignedAddress(addr) ? ReadExecutedOp(addr) : Memory::Read_Instruction(addr);
}
void ArmJit::DoJit(u32 em_address, JitBlock *b) {
js.Begin(b);
// We add a downcount flag check before the block, used when entering from a linked block.
// The last block decremented downcounter, and the flag should still be available.
// Got three variants here of where we position the code, needs detailed benchmarking.
FixupBranch bail;
if (jo.useBackJump) {
// Moves the MOVI2R and B *before* checkedEntry, and just branch backwards there.
// Speedup seems to be zero unfortunately but I guess it may vary from device to device.
// Not intrusive so keeping it around here to experiment with, may help on ARMv6 due to
// large/slow construction of 32-bit immediates?
JumpTarget backJump = GetCodePtr();
gpr.SetRegImm(R0, js.blockStart);
B((const void *)outerLoopPCInR0);
b->checkedEntry = GetCodePtr();
SetCC(CC_LT);
B(backJump);
SetCC(CC_AL);
} else if (jo.useForwardJump) {
b->checkedEntry = GetCodePtr();
SetCC(CC_LT);
bail = B();
SetCC(CC_AL);
} else {
b->checkedEntry = GetCodePtr();
SetCC(CC_LT);
gpr.SetRegImm(R0, js.blockStart);
B((const void *)outerLoopPCInR0);
SetCC(CC_AL);
}
b->normalEntry = GetCodePtr();
gpr.Start();
fpr.Start();
js.numInstructions = 0;
while (js.compiling)
{
gpr.SetCompilerPC(GetCompilerPC()); // Let it know for log messages
// Jit breakpoints are quite fast, so let's do them in release too.
CheckJitBreakpoint(GetCompilerPC(), 0);
MIPSOpcode inst = ReadExecutedOp(GetCompilerPC());
//MIPSInfo info = MIPSGetInfo(inst);
//if (info & IS_VFPU) {
// logBlocks = 1;
//}
js.downcountAmount += MIPSGetInstructionCycleEstimate(inst);
MIPSCompileOp(inst, this);
js.compilerPC += 4;
js.numInstructions++;
if (jo.Disabled(JitDisable::REGALLOC_GPR)) {
gpr.FlushAll();
}
if (jo.Disabled(JitDisable::REGALLOC_FPR)) {
fpr.FlushAll();
FlushPrefixV();
}
// Safety check, in case we get a bunch of really large jit ops without a lot of branching.
if (GetSpaceLeft() < 0x800 || js.numInstructions >= JitBlockCache::MAX_BLOCK_INSTRUCTIONS)
{
FlushAll();
WriteExit(GetCompilerPC(), js.nextExit++);
js.compiling = false;
}
}
if (jo.useForwardJump) {
SetJumpTarget(bail);
gpr.SetRegImm(R0, js.blockStart);
B((const void *)outerLoopPCInR0);
}
FlushLitPool();
char temp[256];
if (logBlocks > 0 && dontLogBlocks == 0) {
INFO_LOG(Log::JIT, "=============== mips ===============");
for (u32 cpc = em_address; cpc != GetCompilerPC() + 4; cpc += 4) {
MIPSDisAsm(Memory::ReadUnchecked_Instruction(cpc), cpc, temp, sizeof(temp), true);
INFO_LOG(Log::JIT, "M: %08x %s", cpc, temp);
}
}
b->codeSize = GetCodePtr() - b->normalEntry;
if (logBlocks > 0 && dontLogBlocks == 0) {
INFO_LOG(Log::JIT, "=============== ARM ===============");
DisassembleArm(b->normalEntry, GetCodePtr() - b->normalEntry);
}
if (logBlocks > 0)
logBlocks--;
if (dontLogBlocks > 0)
dontLogBlocks--;
// Don't forget to zap the newly written instructions in the instruction cache!
FlushIcache();
b->originalSize = js.numInstructions;
}
bool ArmJit::DescribeCodePtr(const u8 *ptr, std::string &name)
{
// TODO: Not used by anything yet (except the modified VerySleepy on Windows)
return false;
}
void ArmJit::LinkBlock(u8 *exitPoint, const u8 *checkedEntry) {
if (PlatformIsWXExclusive()) {
ProtectMemoryPages(exitPoint, 32, MEM_PROT_READ | MEM_PROT_WRITE);
}
ARMXEmitter emit(exitPoint);
u32 op = *((const u32 *)emit.GetCodePointer());
bool prelinked = (op & 0xFF000000) == 0xEA000000;
// Jump directly to the block, yay.
emit.B(checkedEntry);
if (!prelinked) {
do {
op = *((const u32 *)emit.GetCodePointer());
// Overwrite whatever is here with a breakpoint.
emit.BKPT(1);
// Stop after overwriting the next unconditional branch or BKPT.
// It can be a BKPT if we unlinked, and are now linking a different one.
} while ((op & 0xFF000000) != 0xEA000000 && (op & 0xFFF000F0) != 0xE1200070);
}
emit.FlushIcache();
if (PlatformIsWXExclusive()) {
ProtectMemoryPages(exitPoint, 32, MEM_PROT_READ | MEM_PROT_EXEC);
}
}
void ArmJit::UnlinkBlock(u8 *checkedEntry, u32 originalAddress) {
if (PlatformIsWXExclusive()) {
ProtectMemoryPages(checkedEntry, 16, MEM_PROT_READ | MEM_PROT_WRITE);
}
// Send anyone who tries to run this block back to the dispatcher.
// Not entirely ideal, but .. pretty good.
// I hope there's enough space...
// checkedEntry is the only "linked" entrance so it's enough to overwrite that.
ARMXEmitter emit(checkedEntry);
emit.MOVI2R(R0, originalAddress);
emit.STR(R0, CTXREG, offsetof(MIPSState, pc));
emit.B(MIPSComp::jit->GetDispatcher());
emit.FlushIcache();
if (PlatformIsWXExclusive()) {
ProtectMemoryPages(checkedEntry, 16, MEM_PROT_READ | MEM_PROT_EXEC);
}
}
void ArmJit::Comp_ReplacementFunc(MIPSOpcode op)
{
// We get here if we execute the first instruction of a replaced function. This means
// that we do need to return to RA.
// Inlined function calls (caught in jal) are handled differently.
// 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) {
Comp_Generic(op);
return;
}
int index = op.encoding & MIPS_EMUHACK_VALUE_MASK;
const ReplacementTableEntry *entry = GetReplacementFunc(index);
if (!entry) {
ERROR_LOG_REPORT_ONCE(replFunc, Log::HLE, "Invalid replacement op %08x at %08x", op.encoding, js.compilerPC);
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->jitReplaceFunc) {
MIPSReplaceFunc repl = entry->jitReplaceFunc;
int cycles = (this->*repl)();
if (entry->flags & (REPFLAG_HOOKENTER | REPFLAG_HOOKEXIT)) {
// Compile the original instruction at this address. We ignore cycles for hooks.
MIPSCompileOp(Memory::Read_Instruction(GetCompilerPC()), this);
} else {
FlushAll();
// Flushed, so R1 is safe.
LDR(R1, CTXREG, MIPS_REG_RA * 4);
js.downcountAmount += cycles;
WriteExitDestInR(R1);
js.compiling = false;
}
} else if (entry->replaceFunc) {
FlushAll();
SaveDowncount();
RestoreRoundingMode();
gpr.SetRegImm(SCRATCHREG1, GetCompilerPC());
MovToPC(SCRATCHREG1);
// Standard function call, nothing fancy.
// The function returns the number of cycles it took in EAX.
if (BLInRange((const void *)(entry->replaceFunc))) {
BL((const void *)(entry->replaceFunc));
} else {
MOVI2R(R0, (uintptr_t)entry->replaceFunc);
BL(R0);
}
if (entry->flags & (REPFLAG_HOOKENTER | REPFLAG_HOOKEXIT)) {
// Compile the original instruction at this address. We ignore cycles for hooks.
ApplyRoundingMode();
RestoreDowncount();
MIPSCompileOp(Memory::Read_Instruction(GetCompilerPC()), this);
} else {
ApplyRoundingMode();
RestoreDowncount();
CMPI2R(R0, 0, SCRATCHREG2);
FixupBranch positive = B_CC(CC_GE);
RSB(R0, R0, Operand2(0));
MovFromPC(R1);
FixupBranch done = B();
SetJumpTarget(positive);
LDR(R1, CTXREG, MIPS_REG_RA * 4);
SetJumpTarget(done);
WriteDownCountR(R0);
WriteExitDestInR(R1);
js.compiling = false;
}
} else {
ERROR_LOG(Log::HLE, "Replacement function %s has neither jit nor regular impl", entry->name);
}
}
void ArmJit::Comp_Generic(MIPSOpcode op)
{
FlushAll();
MIPSInterpretFunc func = MIPSGetInterpretFunc(op);
if (func)
{
SaveDowncount();
// TODO: Perhaps keep the rounding mode for interp?
RestoreRoundingMode();
gpr.SetRegImm(SCRATCHREG1, GetCompilerPC());
MovToPC(SCRATCHREG1);
gpr.SetRegImm(R0, op.encoding);
QuickCallFunction(R1, (void *)&MIPSInterpretTrampoline);
ApplyRoundingMode();
RestoreDowncount();
}
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;
}
}
void ArmJit::MovFromPC(ARMReg r) {
LDR(r, CTXREG, offsetof(MIPSState, pc));
}
void ArmJit::MovToPC(ARMReg r) {
STR(r, CTXREG, offsetof(MIPSState, pc));
}
void ArmJit::SaveDowncount() {
if (jo.downcountInRegister)
STR(DOWNCOUNTREG, CTXREG, offsetof(MIPSState, downcount));
}
void ArmJit::RestoreDowncount() {
if (jo.downcountInRegister)
LDR(DOWNCOUNTREG, CTXREG, offsetof(MIPSState, downcount));
}
void ArmJit::WriteDownCount(int offset) {
if (jo.downcountInRegister) {
int theDowncount = js.downcountAmount + offset;
Operand2 op2;
if (TryMakeOperand2(theDowncount, op2)) {
SUBS(DOWNCOUNTREG, DOWNCOUNTREG, op2);
} else {
// Should be fine to use R2 here, flushed the regcache anyway.
// If js.downcountAmount can be expressed as an Imm8, we don't need this anyway.
gpr.SetRegImm(R2, theDowncount);
SUBS(DOWNCOUNTREG, DOWNCOUNTREG, R2);
}
} else {
int theDowncount = js.downcountAmount + offset;
LDR(SCRATCHREG2, CTXREG, offsetof(MIPSState, downcount));
Operand2 op2;
if (TryMakeOperand2(theDowncount, op2)) {
SUBS(SCRATCHREG2, SCRATCHREG2, op2);
} else {
// Should be fine to use R2 here, flushed the regcache anyway.
// If js.downcountAmount can be expressed as an Imm8, we don't need this anyway.
gpr.SetRegImm(R2, theDowncount);
SUBS(SCRATCHREG2, SCRATCHREG2, R2);
}
STR(SCRATCHREG2, CTXREG, offsetof(MIPSState, downcount));
}
}
// Abuses R2
void ArmJit::WriteDownCountR(ARMReg reg) {
if (jo.downcountInRegister) {
SUBS(DOWNCOUNTREG, DOWNCOUNTREG, reg);
} else {
LDR(R2, CTXREG, offsetof(MIPSState, downcount));
SUBS(R2, R2, reg);
STR(R2, CTXREG, offsetof(MIPSState, downcount));
}
}
// Destroys SCRATCHREG2. Does not destroy SCRATCHREG1.
void ArmJit::RestoreRoundingMode(bool force) {
// If the game has never set an interesting rounding mode, we can safely skip this.
if (force || js.hasSetRounding) {
QuickCallFunction(R1, restoreRoundingMode);
}
}
// Does not destroy R0 (SCRATCHREG1). Destroys R14 (SCRATCHREG2).
void ArmJit::ApplyRoundingMode(bool force) {
// If the game has never set an interesting rounding mode, we can safely skip this.
if (force || js.hasSetRounding) {
QuickCallFunction(R1, applyRoundingMode);
}
}
// Does (must!) not destroy R0 (SCRATCHREG1). Destroys R14 (SCRATCHREG2).
void ArmJit::UpdateRoundingMode(u32 fcr31) {
// We must set js.hasSetRounding at compile time, or this block will use the wrong rounding mode.
// The fcr31 parameter is -1 when not known at compile time, so we just assume it was changed.
if (fcr31 & 0x01000003) {
js.hasSetRounding = true;
}
}
// IDEA - could have a WriteDualExit that takes two destinations and two condition flags,
// and just have conditional that set PC "twice". This only works when we fall back to dispatcher
// though, as we need to have the SUBS flag set in the end. So with block linking in the mix,
// I don't think this gives us that much benefit.
void ArmJit::WriteExit(u32 destination, int exit_num)
{
// NOTE: Can't blindly check for bad destination addresses here, sometimes exits with bad destinations are written intentionally (like breaks).
_assert_msg_(exit_num < MAX_JIT_BLOCK_EXITS, "Expected a valid exit_num. dest=%08x", destination);
WriteDownCount();
//If nobody has taken care of this yet (this can be removed when all branches are done)
JitBlock *b = js.curBlock;
b->exitAddress[exit_num] = destination;
b->exitPtrs[exit_num] = GetWritableCodePtr();
// Link opportunity!
int block = blocks.GetBlockNumberFromStartAddress(destination);
if (block >= 0 && jo.enableBlocklink) {
// It exists! Joy of joy!
B(blocks.GetBlock(block)->checkedEntry);
b->linkStatus[exit_num] = true;
} else {
gpr.SetRegImm(R0, destination);
B((const void *)dispatcherPCInR0);
}
}
void ArmJit::WriteExitDestInR(ARMReg Reg)
{
// TODO: If not fast memory, check for invalid address in reg and trigger exception.
MovToPC(Reg);
WriteDownCount();
// TODO: shouldn't need an indirect branch here...
B((const void *)dispatcher);
}
void ArmJit::WriteSyscallExit()
{
WriteDownCount();
B((const void *)dispatcherCheckCoreState);
}
bool ArmJit::CheckJitBreakpoint(u32 addr, int downcountOffset) {
if (g_breakpoints.NeedsBreakCheckAt(addr)) {
MRS(R8);
FlushAll();
MOVI2R(SCRATCHREG1, GetCompilerPC());
MovToPC(SCRATCHREG1);
SaveDowncount();
RestoreRoundingMode();
MOVI2R(R0, addr);
QuickCallFunction(SCRATCHREG2, &JitBreakpoint);
// If 0, the conditional breakpoint wasn't taken.
CMPI2R(R0, 0, SCRATCHREG2);
FixupBranch skip = B_CC(CC_EQ);
WriteDownCount(downcountOffset);
ApplyRoundingMode();
RestoreDowncount();
B((const void *)dispatcherCheckCoreState);
SetJumpTarget(skip);
ApplyRoundingMode();
_MSR(true, false, R8);
return true;
}
return false;
}
bool ArmJit::CheckMemoryBreakpoint(int instructionOffset) {
if (g_breakpoints.HasMemChecks()) {
int off = instructionOffset + (js.inDelaySlot ? 1 : 0);
MRS(R8);
FlushAll();
SaveDowncount();
RestoreRoundingMode();
MOVI2R(R0, GetCompilerPC());
MovToPC(R0);
if (off != 0)
ADDI2R(R0, R0, off * 4, SCRATCHREG2);
QuickCallFunction(SCRATCHREG2, &JitMemCheck);
// If 0, the breakpoint wasn't tripped.
CMPI2R(R0, 0, SCRATCHREG2);
FixupBranch skip = B_CC(CC_EQ);
WriteDownCount(-1 - off);
ApplyRoundingMode();
RestoreDowncount();
B((const void *)dispatcherCheckCoreState);
SetJumpTarget(skip);
ApplyRoundingMode();
_MSR(true, false, R8);
return true;
}
return false;
}
void ArmJit::Comp_DoNothing(MIPSOpcode op) { }
} // namespace
#endif // PPSSPP_ARCH(ARM)