mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-15 15:30:29 +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>
209 lines
6.4 KiB
C++
209 lines
6.4 KiB
C++
// Copyright (c) 2012- PPSSPP Project / 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 git repository and contact information can be found at
|
|
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <map>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
#include <string>
|
|
|
|
#include "ppsspp_config.h"
|
|
#include "Common/CommonTypes.h"
|
|
#include "Common/CodeBlock.h"
|
|
#include "Core/MIPS/MIPS.h"
|
|
|
|
// Can probably reduce to 2 now that block continuation is gone.
|
|
const int MAX_JIT_BLOCK_EXITS = 4;
|
|
|
|
constexpr bool JIT_USE_COMPILEDHASH = true;
|
|
|
|
struct BlockCacheStats {
|
|
int numBlocks;
|
|
float avgBloat; // In code bytes, not instructions!
|
|
float minBloat;
|
|
u32 minBloatBlock;
|
|
float maxBloat;
|
|
u32 maxBloatBlock;
|
|
};
|
|
|
|
enum class DestroyType {
|
|
DESTROY,
|
|
INVALIDATE,
|
|
// Skips jit unlink, since it'll be poisoned anyway.
|
|
CLEAR,
|
|
};
|
|
|
|
// We should be careful not to access these block structures during runtime as they are large.
|
|
// Fine to mess with them at block compile time though.
|
|
struct JitBlock {
|
|
bool ContainsAddress(u32 address) const;
|
|
void DoIntegrityCheck(u32 emAddress, int blockNum, const char *reason) const;
|
|
|
|
const u8 *checkedEntry; // const, we have to translate to writable.
|
|
const u8 *normalEntry;
|
|
|
|
u8 *exitPtrs[MAX_JIT_BLOCK_EXITS]; // to be able to rewrite the exit jump
|
|
u32 exitAddress[MAX_JIT_BLOCK_EXITS]; // 0xFFFFFFFF == unknown
|
|
u32 sentinel;
|
|
|
|
u32 originalAddress;
|
|
MIPSOpcode originalFirstOpcode; //to be able to restore
|
|
|
|
uint64_t compiledHash;
|
|
|
|
u32 codeSize; // in bytes, so it doesn't fit in a u16 for a long block
|
|
u16 originalSize; // in instructions, bounded by MAX_BLOCK_INSTRUCTIONS
|
|
u16 blockNum;
|
|
|
|
bool invalid;
|
|
bool linkStatus[MAX_JIT_BLOCK_EXITS];
|
|
};
|
|
|
|
typedef void (*CompiledCode)();
|
|
|
|
struct JitBlockDebugInfo {
|
|
uint32_t originalAddress;
|
|
std::vector<std::string> origDisasm;
|
|
std::vector<std::string> irDisasm; // if any
|
|
std::vector<std::string> targetDisasm;
|
|
};
|
|
|
|
struct JitBlockProfileStats {
|
|
int64_t executions;
|
|
int64_t totalNanos;
|
|
};
|
|
|
|
// Only used for debugging, so keeping it tight is not that important.
|
|
struct JitBlockMeta {
|
|
bool valid;
|
|
uint32_t addr;
|
|
uint32_t sizeInBytes;
|
|
};
|
|
|
|
class JitBlockCacheDebugInterface {
|
|
public:
|
|
virtual int GetNumBlocks() const = 0;
|
|
virtual int GetBlockNumberFromStartAddress(u32 em_address) const = 0;
|
|
virtual JitBlockDebugInfo GetBlockDebugInfo(int blockNum) const = 0; // Expensive
|
|
virtual JitBlockMeta GetBlockMeta(int blockNum) const = 0;
|
|
virtual JitBlockProfileStats GetBlockProfileStats(int blockNum) const = 0;
|
|
virtual void ComputeStats(BlockCacheStats &bcStats) const = 0;
|
|
virtual bool IsValidBlock(int blockNum) const = 0;
|
|
virtual bool SupportsProfiling() const { return false; }
|
|
|
|
virtual ~JitBlockCacheDebugInterface() {}
|
|
};
|
|
|
|
class JitBlockCache : public JitBlockCacheDebugInterface {
|
|
public:
|
|
JitBlockCache(MIPSState *mipsState, CodeBlockCommon *codeBlock);
|
|
~JitBlockCache();
|
|
|
|
int AllocateBlock(u32 emAddress);
|
|
void FinalizeBlock(int block_num, bool block_link);
|
|
|
|
void Clear();
|
|
void Init();
|
|
void Shutdown();
|
|
void Reset();
|
|
|
|
bool IsFull() const;
|
|
void ComputeStats(BlockCacheStats &bcStats) const override;
|
|
|
|
// Code Cache
|
|
JitBlock *GetBlock(int no) { return &blocks_[no]; }
|
|
const JitBlock *GetBlock(int no) const { return &blocks_[no]; }
|
|
|
|
// Fast way to get a block. Only works on the first source-cpu instruction of a block.
|
|
int GetBlockNumberFromStartAddress(u32 em_address) const override;
|
|
|
|
// slower, but can get numbers from within blocks, not just the first instruction.
|
|
// WARNING! WILL NOT WORK WITH JIT INLINING ENABLED (not yet a feature but will be soon)
|
|
// Returns a list of block numbers - only one block can start at a particular address, but they CAN overlap.
|
|
// This one is slow so should only be used for one-shots from the debugger UI, not for anything during runtime.
|
|
void GetBlockNumbersFromAddress(u32 em_address, std::vector<int> *block_numbers) const;
|
|
// Similar to above, but only the first matching address.
|
|
int GetBlockNumberFromAddress(u32 em_address) const;
|
|
int GetBlockNumberFromShadowValue(u32 value) const;
|
|
|
|
u32 GetAddressFromBlockPtr(const u8 *ptr) const;
|
|
|
|
MIPSOpcode GetOriginalFirstOp(int block_num) const;
|
|
|
|
bool RangeMayHaveBlocks(u32 start, u32 end) const;
|
|
|
|
// DOES NOT WORK CORRECTLY WITH JIT INLINING
|
|
void InvalidateICache(u32 address, const u32 length);
|
|
void InvalidateChangedBlocks();
|
|
void DestroyBlock(int block_num, DestroyType type);
|
|
|
|
int GetNumBlocks() const override { return num_blocks_; }
|
|
bool IsValidBlock(int blockNum) const override { return blockNum >= 0 && blockNum < num_blocks_ && !blocks_[blockNum].invalid; }
|
|
JitBlockMeta GetBlockMeta(int blockNum) const override {
|
|
JitBlockMeta meta{};
|
|
if (IsValidBlock(blockNum)) {
|
|
meta.valid = true;
|
|
meta.addr = blocks_[blockNum].originalAddress;
|
|
meta.sizeInBytes = blocks_[blockNum].originalSize;
|
|
}
|
|
return meta;
|
|
}
|
|
JitBlockProfileStats GetBlockProfileStats(int blockNum) const override {
|
|
return JitBlockProfileStats{};
|
|
}
|
|
|
|
static int GetBlockExitSize();
|
|
|
|
JitBlockDebugInfo GetBlockDebugInfo(int blockNum) const override;
|
|
|
|
enum {
|
|
MAX_BLOCK_INSTRUCTIONS = 0x4000,
|
|
};
|
|
|
|
private:
|
|
void LinkBlockExits(int i);
|
|
void LinkBlock(int i);
|
|
void UnlinkBlock(int i);
|
|
|
|
void AddBlockMap(int block_num);
|
|
void RemoveBlockMap(int block_num);
|
|
|
|
// What the block shadow holds for a block: its normal entry's offset into the code space.
|
|
u32 GetShadowValueForBlock(int block_num) const;
|
|
|
|
int num_blocks_ = 0;
|
|
CodeBlockCommon *codeBlock_;
|
|
JitBlock *blocks_ = nullptr;
|
|
std::unordered_multimap<u32, int> links_to_;
|
|
std::map<std::pair<u32,u32>, u32> block_map_; // (end_addr, start_addr) -> number
|
|
|
|
enum {
|
|
JITBLOCK_RANGE_SCRATCH = 0,
|
|
JITBLOCK_RANGE_RAMBOTTOM = 1,
|
|
JITBLOCK_RANGE_RAMTOP = 2,
|
|
JITBLOCK_RANGE_COUNT = 3,
|
|
};
|
|
std::pair<u32, u32> blockMemRanges_[JITBLOCK_RANGE_COUNT];
|
|
|
|
enum {
|
|
// Where does this number come from?
|
|
MAX_NUM_BLOCKS = 65536 * 4
|
|
};
|
|
};
|