Files
ppsspp/Core/MIPS/IR/IRNativeCommon.h
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

216 lines
7.2 KiB
C++

// Copyright (c) 2023- 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/.
#pragma once
#include <unordered_map>
#include "Core/MIPS/IR/IRJit.h"
#include "Core/MIPS/JitCommon/JitBlockCache.h"
namespace MIPSComp {
typedef void (*IRNativeFuncNoArg)();
enum class IRProfilerStatus : int32_t {
NOT_RUNNING,
IN_JIT,
TIMER_ADVANCE,
COMPILING,
MATH_HELPER,
REPLACEMENT,
SYSCALL,
INTERPRET,
IR_INTERPRET,
};
struct IRNativeHooks {
IRNativeFuncNoArg enterDispatcher = nullptr;
const uint8_t *dispatcher = nullptr;
const uint8_t *dispatchFetch = nullptr;
const uint8_t *crashHandler = nullptr;
// The dispatcher's compile path, where a replacement hook's block shadow entry goes.
const uint8_t *blockShadowHook = nullptr;
uint32_t *profilerPC = nullptr;
IRProfilerStatus *profilerStatus = nullptr;
};
struct IRNativeBlockExit {
int offset;
int len;
uint32_t dest;
};
struct IRNativeBlock {
int checkedOffset = 0;
std::vector<IRNativeBlockExit> exits;
};
class IRNativeBackend {
public:
IRNativeBackend(IRBlockCache &blocks);
virtual ~IRNativeBackend();
void CompileIRInst(IRInst inst);
virtual bool DescribeCodePtr(const u8 *ptr, std::string &name) const;
bool CodeInRange(const u8 *ptr) const;
int OffsetFromCodePtr(const u8 *ptr);
virtual void GenerateFixedCode(MIPSState *mipsState) = 0;
virtual bool CompileBlock(IRBlockCache *irBlockCache, int block_num) = 0;
virtual void ClearAllBlocks() = 0;
virtual void InvalidateBlock(IRBlockCache *irBlockCache, int block_num) = 0;
void FinalizeBlock(IRBlockCache *irBlockCache, int block_num, const JitOptions &jo);
virtual void UpdateFCR31(MIPSState *mipsState) {}
const IRNativeHooks &GetNativeHooks() const {
return hooks_;
}
const IRNativeBlock *GetNativeBlock(int block_num) const;
void SetBlockCheckedOffset(int block_num, int offset);
virtual const CodeBlockCommon &CodeBlock() const = 0;
protected:
virtual void CompIR_Arith(IRInst inst) = 0;
virtual void CompIR_Assign(IRInst inst) = 0;
virtual void CompIR_Basic(IRInst inst) = 0;
virtual void CompIR_Bits(IRInst inst) = 0;
virtual void CompIR_Breakpoint(IRInst inst) = 0;
virtual void CompIR_Compare(IRInst inst) = 0;
virtual void CompIR_CondAssign(IRInst inst) = 0;
virtual void CompIR_CondStore(IRInst inst) = 0;
virtual void CompIR_Div(IRInst inst) = 0;
virtual void CompIR_Exit(IRInst inst) = 0;
virtual void CompIR_ExitIf(IRInst inst) = 0;
virtual void CompIR_FArith(IRInst inst) = 0;
virtual void CompIR_FAssign(IRInst inst) = 0;
virtual void CompIR_FCompare(IRInst inst) = 0;
virtual void CompIR_FCondAssign(IRInst inst) = 0;
virtual void CompIR_FCvt(IRInst inst) = 0;
virtual void CompIR_FLoad(IRInst inst) = 0;
virtual void CompIR_FRound(IRInst inst) = 0;
virtual void CompIR_FSat(IRInst inst) = 0;
virtual void CompIR_FSpecial(IRInst inst) = 0;
virtual void CompIR_FStore(IRInst inst) = 0;
virtual void CompIR_Generic(IRInst inst) = 0;
virtual void CompIR_HiLo(IRInst inst) = 0;
virtual void CompIR_Interpret(IRInst inst) = 0;
virtual void CompIR_Load(IRInst inst) = 0;
virtual void CompIR_LoadShift(IRInst inst) = 0;
virtual void CompIR_Logic(IRInst inst) = 0;
virtual void CompIR_Mult(IRInst inst) = 0;
virtual void CompIR_RoundingMode(IRInst inst) = 0;
virtual void CompIR_Shift(IRInst inst) = 0;
virtual void CompIR_Store(IRInst inst) = 0;
virtual void CompIR_StoreShift(IRInst inst) = 0;
virtual void CompIR_System(IRInst inst) = 0;
virtual void CompIR_Transfer(IRInst inst) = 0;
virtual void CompIR_VecArith(IRInst inst) = 0;
virtual void CompIR_VecAssign(IRInst inst) = 0;
virtual void CompIR_VecHoriz(IRInst inst) = 0;
virtual void CompIR_VecLoad(IRInst inst) = 0;
virtual void CompIR_VecPack(IRInst inst) = 0;
virtual void CompIR_VecStore(IRInst inst) = 0;
virtual void CompIR_ValidateAddress(IRInst inst) = 0;
virtual void OverwriteExit(int srcOffset, int len, int block_num) = 0;
// Returns true when debugging statistics should be compiled in.
bool DebugStatsEnabled() const;
bool DebugProfilerEnabled() const;
// Callback (compile when DebugStatsEnabled()) to log a base interpreter hit.
// Call the func returned by MIPSGetInterpretFunc(op) directly for interpret.
static void NotifyMIPSInterpret(const char *name);
// Callback to log AND perform a base interpreter op. Alternative to NotifyMIPSInterpret().
static void DoMIPSInst(uint32_t op);
// Callback to log AND perform an IR interpreter inst. Returns 0 or a PC to jump to.
static uint32_t DoIRInst(uint64_t inst);
static int ReportBadAddress(uint32_t addr, uint32_t alignment, uint32_t isWrite);
void AddLinkableExit(int block_num, uint32_t pc, int exitStartOffset, int exitLen);
void EraseAllLinks(int block_num);
IRNativeHooks hooks_;
IRBlockCache &blocks_;
std::vector<IRNativeBlock> nativeBlocks_;
std::unordered_multimap<uint32_t, int> linksTo_;
};
class IRNativeBlockCacheDebugInterface : public JitBlockCacheDebugInterface {
public:
IRNativeBlockCacheDebugInterface(const MIPSComp::IRBlockCache &irBlocks);
void Init(const IRNativeBackend *backend);
int GetNumBlocks() const override;
int GetBlockNumberFromStartAddress(u32 em_address) const override;
JitBlockDebugInfo GetBlockDebugInfo(int blockNum) const override;
JitBlockMeta GetBlockMeta(int blockNum) const override;
JitBlockProfileStats GetBlockProfileStats(int blockNum) const override;
void ComputeStats(BlockCacheStats &bcStats) const override;
bool IsValidBlock(int blockNum) const override;
private:
void GetBlockCodeRange(int blockNum, int *startOffset, int *size) const;
const MIPSComp::IRBlockCache &irBlocks_;
const CodeBlockCommon *codeBlock_ = nullptr;
const IRNativeBackend *backend_ = nullptr;
};
class IRNativeJit : public IRJit {
public:
IRNativeJit(MIPSState *mipsState);
void RunLoopUntil(u64 globalticks) override;
void ClearCache() override;
bool DescribeCodePtr(const u8 *ptr, std::string &name) override;
bool CodeInRange(const u8 *ptr) const override;
bool IsAtDispatchFetch(const u8 *ptr) const override;
u32 GetBlockShadowHookValue() const override;
const u8 *GetDispatcher() const override;
const u8 *GetCrashHandler() const override;
void UpdateFCR31() override;
JitBlockCacheDebugInterface *GetBlockCacheDebugInterface() override;
const u8 *GetCodeBase() const override;
protected:
void Init(IRNativeBackend &backend);
bool CompileNativeBlock(IRBlockCache *irBlockCache, int block_num) override;
void FinalizeNativeBlock(IRBlockCache *irBlockCache, int block_num) override;
void InvalidateNativeBlock(IRBlockCache *irBlockCache, int block_num) override;
IRNativeBackend *backend_ = nullptr;
IRNativeHooks hooks_;
IRNativeBlockCacheDebugInterface debugInterface_;
};
} // namespace MIPSComp