Files
ppsspp/Core/MIPS/x86/Asm.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

236 lines
7.4 KiB
C++

// Copyright (C) 2003 Dolphin 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 SVN repository and contact information can be found at
// http://code.google.com/p/dolphin-emu/
#include "ppsspp_config.h"
#if PPSSPP_ARCH(X86) || PPSSPP_ARCH(AMD64)
#include "Common/Math/math_util.h"
#include "ABI.h"
#include "x64Emitter.h"
#include "Core/Core.h"
#include "Core/MemMap.h"
#include "Core/System.h"
#include "Core/MIPS/MIPS.h"
#include "Core/CoreTiming.h"
#include "Common/MemoryUtil.h"
#include "Core/MIPS/JitCommon/JitCommon.h"
#include "Core/MIPS/x86/Jit.h"
using namespace Gen;
using namespace X64JitConstants;
extern volatile CoreState coreState;
namespace MIPSComp
{
//TODO - make an option
//#if _DEBUG
static bool enableDebug = false;
//#else
// bool enableDebug = false;
//#endif
//static bool enableStatistics = false; //unused?
//GLOBAL STATIC ALLOCATIONS x86
//EAX - ubiquitous scratch register - EVERYBODY scratches this
//EBP - Pointer to fpr/gpr regs
//GLOBAL STATIC ALLOCATIONS x64
//EAX - ubiquitous scratch register - EVERYBODY scratches this
//RBX - Base pointer of memory
//R14 - Pointer to fpr/gpr regs
//R15 - Pointer to array of block pointers
void ImHere() {
DEBUG_LOG(Log::CPU, "JIT Here: %08x", currentMIPS->pc);
}
void Jit::GenerateFixedCode(JitOptions &jo) {
BeginWrite(GetMemoryProtectPageSize());
AlignCodePage();
restoreRoundingMode = AlignCode16(); {
STMXCSR(MIPSSTATE_VAR(temp));
// Clear the rounding mode and flush-to-zero bits back to 0.
AND(32, MIPSSTATE_VAR(temp), Imm32(~(7 << 13)));
LDMXCSR(MIPSSTATE_VAR(temp));
RET();
}
applyRoundingMode = AlignCode16(); {
MOV(32, R(EAX), MIPSSTATE_VAR(fcr31));
AND(32, R(EAX), Imm32(0x01000003));
// If it's 0 (nearest + no flush0), we don't actually bother setting - we cleared the rounding
// mode out in restoreRoundingMode anyway. This is the most common.
FixupBranch skip = J_CC(CC_Z);
STMXCSR(MIPSSTATE_VAR(temp));
// The MIPS bits don't correspond exactly, so we have to adjust.
// 0 -> 0 (skip2), 1 -> 3, 2 -> 2 (skip2), 3 -> 1
TEST(8, R(AL), Imm8(1));
FixupBranch skip2 = J_CC(CC_Z);
XOR(32, R(EAX), Imm8(2));
SetJumpTarget(skip2);
// Adjustment complete, now reconstruct MXCSR
SHL(32, R(EAX), Imm8(13));
// Before setting new bits, we must clear the old ones.
AND(32, MIPSSTATE_VAR(temp), Imm32(~(7 << 13))); // Clearing bits 13-14 (rounding mode) and 15 (flush to zero)
OR(32, MIPSSTATE_VAR(temp), R(EAX));
TEST(32, MIPSSTATE_VAR(fcr31), Imm32(1 << 24));
FixupBranch skip3 = J_CC(CC_Z);
OR(32, MIPSSTATE_VAR(temp), Imm32(1 << 15));
SetJumpTarget(skip3);
LDMXCSR(MIPSSTATE_VAR(temp));
SetJumpTarget(skip);
RET();
}
enterDispatcher = AlignCode16();
ABI_PushAllCalleeSavedRegsAndAdjustStack();
#if PPSSPP_ARCH(AMD64)
// Two statically allocated registers.
MOV(64, R(MEMBASEREG), ImmPtr(Memory::base));
uintptr_t jitbase = (uintptr_t)GetBasePtr();
if (jitbase > 0x7FFFFFFFULL) {
MOV(64, R(JITBASEREG), ImmPtr(GetBasePtr()));
jo.reserveR15ForAsm = true;
}
#endif
// From the start of the FP reg, a single byte offset can reach all GPR + all FPR (but no VFPUR)
MOV(PTRBITS, R(CTXREG), ImmPtr(&mips_->f[0]));
outerLoop = GetCodePtr();
RestoreRoundingMode(true);
LEA(PTRBITS, ECX, MDisp(CTXREG, -(s32)offsetof(MIPSState, f[0]))); // Adjust to get the real pointer.
ABI_CallFunctionR(reinterpret_cast<void *>(&CoreTiming::Advance), ECX);
ApplyRoundingMode(true);
FixupBranch skipToCoreStateCheck = J(); //skip the downcount check
dispatcherCheckCoreState = GetCodePtr();
// The result of slice decrementation should be in flags if somebody jumped here
// IMPORTANT - We jump on negative, not carry!!!
FixupBranch bailCoreState = J_CC(CC_S, true);
SetJumpTarget(skipToCoreStateCheck);
if (RipAccessible((const void *)&coreState)) {
CMP(32, M(&coreState), Imm32(0)); // rip accessible
} else {
MOV(PTRBITS, R(RAX), ImmPtr((const void *)&coreState));
CMP(32, MatR(RAX), Imm32(0));
}
FixupBranch badCoreState = J_CC(CC_NZ, true);
FixupBranch skipToRealDispatch2 = J(); //skip the sync and compare first time
dispatcher = GetCodePtr();
// The result of slice decrementation should be in flags if somebody jumped here
// IMPORTANT - We jump on negative, not carry!!!
FixupBranch bail = J_CC(CC_S, true);
SetJumpTarget(skipToRealDispatch2);
dispatcherNoCheck = GetCodePtr();
MOV(32, R(EAX), MIPSSTATE_VAR(pc));
dispatcherInEAXNoCheck = GetCodePtr();
// Look the block up in the shadow. The address stays in EAX for MemFault if this faults.
AND(32, R(EAX), Imm32(Memory::BLOCK_SHADOW_ENTRY_MASK));
#if PPSSPP_ARCH(X86)
_assert_msg_(Memory::blockShadow != 0, "Block shadow bogus");
dispatcherFetch = GetCodePtr();
MOV(32, R(EAX), MDisp(EAX, (u32)Memory::blockShadow));
#elif PPSSPP_ARCH(AMD64)
MOV(64, R(RDX), ImmPtr(Memory::blockShadow));
dispatcherFetch = GetCodePtr();
MOV(32, R(EAX), MComplex(RDX, RAX, SCALE_1, 0));
#endif
TEST(32, R(EAX), R(EAX));
FixupBranch notfound = J_CC(CC_Z);
if (enableDebug) {
ADD(32, MIPSSTATE_VAR(debugCount), Imm8(1));
}
// The entry is the block's offset into the code space.
#if PPSSPP_ARCH(X86)
ADD(32, R(EAX), ImmPtr(GetBasePtr()));
#elif PPSSPP_ARCH(AMD64)
if (jo.reserveR15ForAsm) {
ADD(64, R(RAX), R(JITBASEREG));
} else {
// See above, reserveR15ForAsm is used when above 0x7FFFFFFF.
ADD(64, R(EAX), Imm32((u32)jitbase));
}
#endif
JMPptr(R(EAX));
SetJumpTarget(notfound);
//Ok, no block, let's jit
// A replacement hook's block shadow entry jumps here too (GetBlockShadowHookValue()). JitAt
// replaces it with the new block's entry, and destroying that block puts the hook value back.
blockShadowHook = GetCodePtr();
RestoreRoundingMode(true);
LEA(PTRBITS, ECX, MDisp(CTXREG, -(s32)offsetof(MIPSState, f[0]))); // Adjust to get the real pointer.
ABI_CallFunctionR(reinterpret_cast<void *>(&MIPSComp::JitAt), ECX);
ApplyRoundingMode(true);
JMP(dispatcherNoCheck, true); // Let's just dispatch again, we'll enter the block since we know it's there.
SetJumpTarget(bail);
SetJumpTarget(bailCoreState);
if (RipAccessible((const void *)&coreState)) {
CMP(32, M(&coreState), Imm32(0)); // rip accessible
} else {
MOV(PTRBITS, R(RAX), ImmPtr((const void *)&coreState));
CMP(32, MatR(RAX), Imm32(0));
}
J_CC(CC_Z, outerLoop, true);
const uint8_t *quitLoop = GetCodePtr();
SetJumpTarget(badCoreState);
RestoreRoundingMode(true);
ABI_PopAllCalleeSavedRegsAndAdjustStack();
RET();
crashHandler = GetCodePtr();
if (RipAccessible((const void *)&coreState)) {
MOV(32, M(&coreState), Imm32(CORE_RUNTIME_ERROR));
} else {
MOV(PTRBITS, R(RAX), ImmPtr((const void *)&coreState));
MOV(32, MatR(RAX), Imm32(CORE_RUNTIME_ERROR));
}
JMP(quitLoop, true);
// Let's spare the pre-generated code from unprotect-reprotect.
endOfPregeneratedCode = AlignCodePage();
EndWrite();
}
} // namespace
#endif // PPSSPP_ARCH(X86) || PPSSPP_ARCH(AMD64)