#include "ppsspp_config.h"
#include <set>
#include <algorithm>
#include "ext/xxhash.h"
#include "Common/Profiler/Profiler.h"
#include "Common/Log.h"
#include "Common/Serialize/Serializer.h"
#include "Common/StringUtils.h"
#include "Core/Config.h"
#include "Core/Core.h"
#include "Core/CoreTiming.h"
#include "Core/HLE/sceKernelMemory.h"
#include "Core/MemMap.h"
#include "Core/MIPS/MIPS.h"
#include "Core/MIPS/MIPSCodeUtils.h"
#include "Core/MIPS/MIPSInt.h"
#include "Core/MIPS/MIPSTables.h"
#include "Core/MIPS/IR/IRRegCache.h"
#include "Core/MIPS/IR/IRInterpreter.h"
#include "Core/MIPS/IR/IRJit.h"
#include "Core/MIPS/IR/IRNativeCommon.h"
#include "Core/MIPS/JitCommon/JitCommon.h"
#include "Core/Reporting.h"
#include "Common/TimeUtil.h"
#include "Core/MIPS/MIPSTracer.h"
namespace MIPSComp {
IRJit::IRJit(MIPSState *mipsState, bool actualJit) : frontend_(mipsState->HasDefaultPrefix()), mips_(mipsState), blocks_(actualJit) {
InitIR();
compileToNative_ = actualJit;
jo.optimizeForInterpreter = !actualJit;
IROptions opts{};
opts.disableFlags = g_Config.uJitDisableFlags;
#if PPSSPP_ARCH(RISCV64)
opts.unalignedLoadStore = false;
opts.unalignedLoadStoreVec4 = true;
opts.preferVec4 = cpu_info.RiscV_V;
#elif PPSSPP_ARCH(ARM) || PPSSPP_ARCH(ARM64)
opts.unalignedLoadStore = (opts.disableFlags & (uint32_t)JitDisable::LSU_UNALIGNED) == 0;
opts.unalignedLoadStoreVec4 = true;
opts.preferVec4 = true;
#else
opts.unalignedLoadStore = (opts.disableFlags & (uint32_t)JitDisable::LSU_UNALIGNED) == 0;
opts.unalignedLoadStoreVec4 = false;
opts.preferVec4 = true;
#endif
opts.optimizeForInterpreter = jo.optimizeForInterpreter;
frontend_.SetOptions(opts);
}
IRJit::~IRJit() {
}
void IRJit::DoState(PointerWrap &p) {
frontend_.DoState(p);
}
void IRJit::UpdateFCR31() {
}
void IRJit::ClearCache() {
INFO_LOG(Log::JIT, "IRJit: Clearing the block cache!");
blocks_.Clear();
}
void IRJit::InvalidateCacheAt(u32 em_address, int length) {
std::vector<int> numbers = blocks_.FindInvalidatedBlockNumbers(em_address, length);
if (numbers.empty()) {
return;
}
DEBUG_LOG(Log::JIT, "Invalidating IR block cache at %08x (%d bytes): %d blocks", em_address, length, (int)numbers.size());
for (int block_num : numbers) {
auto block = blocks_.GetBlock(block_num);
int cookie = compileToNative_ ? block->GetNativeOffset() : block->GetIRArenaOffset();
blocks_.RemoveBlockFromPageLookup(block_num);
block->Destroy(cookie);
}
}
void IRJit::Compile(u32 em_address) {
_dbg_assert_(compilerEnabled_);
PROFILE_THIS_SCOPE("jitc");
std::vector<IRInst> instructions;
u32 mipsBytes;
if (!CompileBlock(em_address, instructions, mipsBytes)) {
ERROR_LOG(Log::JIT, "Ran out of block numbers, clearing cache");
ClearCache();
CompileBlock(em_address, instructions, mipsBytes);
}
if (frontend_.CheckRounding(em_address)) {
ClearCache();
CompileBlock(em_address, instructions, mipsBytes);
}
}
bool IRJit::CompileBlock(u32 em_address, std::vector<IRInst> &instructions, u32 &mipsBytes) {
_dbg_assert_(compilerEnabled_);
frontend_.DoJit(em_address, instructions, mipsBytes);
_dbg_assert_(!instructions.empty());
int block_num = blocks_.AllocateBlock(em_address, mipsBytes, instructions);
if ((block_num & ~MIPS_EMUHACK_VALUE_MASK) != 0) {
WARN_LOG(Log::JIT, "Failed to allocate block for %08x (%d instructions)", em_address, (int)instructions.size());
return false;
}
IRBlock *b = blocks_.GetBlock(block_num);
if (mipsTracer.tracing_enabled) {
b->UpdateHash();
}
if (!CompileNativeBlock(&blocks_, block_num))
return false;
if (mipsTracer.tracing_enabled) {
mipsTracer.prepare_block(b, blocks_);
}
blocks_.FinalizeBlock(block_num);
FinalizeNativeBlock(&blocks_, block_num);
return true;
}
void IRJit::RunLoopUntil(u64 globalticks) {
PROFILE_THIS_SCOPE("jit");
while (true) {
CoreTiming::Advance();
if (coreState != 0) {
break;
}
MIPSState *mips = mips_;
#ifdef _DEBUG
compilerEnabled_ = false;
#endif
while (mips->downcount >= 0) {
u32 inst = Memory::ReadUnchecked_U32(mips->pc);
u32 opcode = inst & 0xFF000000;
if (opcode == MIPS_EMUHACK_OPCODE) {
u32 offset = inst & 0x00FFFFFF;
const IRInst *instPtr = blocks_.GetArenaPtr() + offset;
if (instPtr->op == IROp::Downcount) {
mips->downcount -= instPtr->constant;
instPtr++;
}
#ifdef IR_PROFILING
IRBlock *block = blocks_.GetBlock(blocks_.GetBlockNumFromIRArenaOffset(offset));
Instant start = Instant::Now();
mips->pc = IRInterpret(mips, instPtr);
int64_t elapsedNanos = start.ElapsedNanos();
block->profileStats_.executions += 1;
block->profileStats_.totalNanos += elapsedNanos;
#else
mips->pc = IRInterpret(mips, instPtr);
#endif
if (!Memory::IsValid4AlignedAddress(mips->pc)) {
int blockNum = blocks_.GetBlockNumFromIRArenaOffset(offset);
IRBlock *block = blocks_.GetBlockUnchecked(blockNum);
Core_ExecException(mips->pc, block->GetOriginalStart(), ExecExceptionType::JUMP);
break;
}
} else {
#ifdef _DEBUG
compilerEnabled_ = true;
#endif
Compile(mips->pc);
#ifdef _DEBUG
compilerEnabled_ = false;
#endif
}
}
#ifdef _DEBUG
compilerEnabled_ = true;
#endif
}
}
bool IRJit::DescribeCodePtr(const u8 *ptr, std::string &name) {
return false;
}
void IRJit::LinkBlock(u8 *exitPoint, const u8 *checkedEntry) {
Crash();
}
void IRJit::UnlinkBlock(u8 *checkedEntry, u32 originalAddress) {
Crash();
}
void IRBlockCache::Clear() {
for (int i = 0; i < (int)blocks_.size(); ++i) {
int cookie = compileToNative_ ? blocks_[i].GetNativeOffset() : blocks_[i].GetIRArenaOffset();
blocks_[i].Destroy(cookie);
}
blocks_.clear();
byPage_.clear();
arena_.clear();
arena_.shrink_to_fit();
}
IRBlockCache::IRBlockCache(bool compileToNative) : compileToNative_(compileToNative) {}
int IRBlockCache::AllocateBlock(int emAddr, u32 origSize, const std::vector<IRInst> &insts) {
const u32 MAX_ARENA_SIZE = 0x1000000 - 1;
int offset = (int)arena_.size();
if (offset >= MAX_ARENA_SIZE) {
WARN_LOG(Log::JIT, "Filled JIT arena, restarting");
return -1;
}
for (int i = 0; i < insts.size(); i++) {
arena_.push_back(insts[i]);
}
int newBlockIndex = (int)blocks_.size();
blocks_.push_back(IRBlock(emAddr, origSize, offset, (u32)insts.size()));
return newBlockIndex;
}
int IRBlockCache::GetBlockNumFromIRArenaOffset(int offset) const {
int low = 0;
int high = (int)blocks_.size() - 1;
int found = -1;
while (low <= high) {
int mid = low + (high - low) / 2;
const int blockOffset = blocks_[mid].GetIRArenaOffset();
if (blockOffset == offset) {
found = mid;
break;
}
if (blockOffset < offset) {
low = mid + 1;
} else {
high = mid - 1;
}
}
#ifndef _DEBUG
return found;
#else
for (int i = 0; i < (int)blocks_.size(); i++) {
if (blocks_[i].GetIRArenaOffset() == offset) {
_dbg_assert_(i == found);
return i;
}
}
#endif
_dbg_assert_(found == -1);
return -1;
}
std::vector<int> IRBlockCache::FindInvalidatedBlockNumbers(u32 address, u32 lengthInBytes) {
u32 startPage = AddressToPage(address);
u32 endPage = AddressToPage(address + lengthInBytes);
std::vector<int> found;
for (u32 page = startPage; page <= endPage; ++page) {
const auto iter = byPage_.find(page);
if (iter == byPage_.end())
continue;
const std::vector<int> &blocksInPage = iter->second;
for (int i : blocksInPage) {
if (blocks_[i].OverlapsRange(address, lengthInBytes)) {
found.push_back(i);
}
}
}
return found;
}
void IRBlockCache::FinalizeBlock(int blockIndex) {
IRBlock &block = blocks_[blockIndex];
int cookie = compileToNative_ ? block.GetNativeOffset() : block.GetIRArenaOffset();
block.Finalize(cookie);
u32 startAddr, size;
block.GetRange(&startAddr, &size);
u32 startPage = AddressToPage(startAddr);
u32 endPage = AddressToPage(startAddr + size);
for (u32 page = startPage; page <= endPage; ++page) {
byPage_[page].push_back(blockIndex);
}
}
void IRBlockCache::RemoveBlockFromPageLookup(int blockIndex) {
IRBlock &block = blocks_[blockIndex];
u32 startAddr, size;
block.GetRange(&startAddr, &size);
u32 startPage = AddressToPage(startAddr);
u32 endPage = AddressToPage(startAddr + size);
for (u32 page = startPage; page <= endPage; ++page) {
auto iter = std::find(byPage_[page].begin(), byPage_[page].end(), blockIndex);
if (iter != byPage_[page].end()) {
byPage_[page].erase(iter);
} else if (block.IsValid()) {
WARN_LOG(Log::JIT, "RemoveBlock: Block at %08x was not found where expected in byPage table.", startAddr);
}
}
}
u32 IRBlockCache::AddressToPage(u32 addr) const {
return (addr & 0x3FFFFFFF) >> 10;
}
int IRBlockCache::FindPreloadBlock(u32 em_address) {
u32 page = AddressToPage(em_address);
auto iter = byPage_.find(page);
if (iter == byPage_.end())
return -1;
const std::vector<int> &blocksInPage = iter->second;
for (int i : blocksInPage) {
if (blocks_[i].GetOriginalStart() == em_address) {
if (blocks_[i].HashMatches()) {
return i;
}
}
}
return -1;
}
int IRBlockCache::FindByCookie(int cookie) {
if (blocks_.empty())
return -1;
if (!compileToNative_) {
return GetBlockNumFromIRArenaOffset(cookie);
}
for (int i = 0; i < GetNumBlocks(); ++i) {
int offset = blocks_[i].GetNativeOffset();
if (offset == cookie)
return i;
}
return -1;
}
std::vector<u32> IRBlockCache::SaveAndClearEmuHackOps() {
std::vector<u32> result;
result.resize(blocks_.size());
for (int number = 0; number < (int)blocks_.size(); ++number) {
IRBlock &b = blocks_[number];
int cookie = compileToNative_ ? b.GetNativeOffset() : b.GetIRArenaOffset();
if (b.IsValid() && b.RestoreOriginalFirstOp(cookie)) {
result[number] = number;
} else {
result[number] = 0;
}
}
return result;
}
void IRBlockCache::RestoreSavedEmuHackOps(const std::vector<u32> &saved) {
if ((int)blocks_.size() != (int)saved.size()) {
ERROR_LOG(Log::JIT, "RestoreSavedEmuHackOps: Wrong saved block size.");
return;
}
for (int number = 0; number < (int)blocks_.size(); ++number) {
IRBlock &b = blocks_[number];
if (b.IsValid() && saved[number] != 0 && b.HasOriginalFirstOp()) {
int cookie = compileToNative_ ? b.GetNativeOffset() : b.GetIRArenaOffset();
b.Finalize(cookie);
}
}
}
JitBlockDebugInfo IRBlockCache::GetBlockDebugInfo(int blockNum) const {
const IRBlock &ir = blocks_[blockNum];
JitBlockDebugInfo debugInfo{};
uint32_t start, size;
ir.GetRange(&start, &size);
debugInfo.originalAddress = start;
debugInfo.origDisasm.reserve(((start + size) - start) / 4);
for (u32 addr = start; addr < start + size; addr += 4) {
char temp[256];
MIPSDisAsm(Memory::Read_Instruction(addr), addr, temp, sizeof(temp), true);
std::string mipsDis = temp;
debugInfo.origDisasm.push_back(mipsDis);
}
debugInfo.irDisasm.reserve(ir.GetNumIRInstructions());
const IRInst *instructions = GetBlockInstructionPtr(ir);
for (int i = 0; i < ir.GetNumIRInstructions(); i++) {
IRInst inst = instructions[i];
char buffer[256];
DisassembleIR(buffer, sizeof(buffer), inst);
debugInfo.irDisasm.push_back(buffer);
}
return debugInfo;
}
void IRBlockCache::ComputeStats(BlockCacheStats &bcStats) const {
double totalBloat = 0.0;
double maxBloat = 0.0;
double minBloat = 1000000000.0;
for (const auto &b : blocks_) {
double codeSize = (double)b.GetNumIRInstructions() * 4;
if (codeSize == 0)
continue;
u32 origAddr, mipsBytes;
b.GetRange(&origAddr, &mipsBytes);
double origSize = (double)mipsBytes;
double bloat = codeSize / origSize;
if (bloat < minBloat) {
minBloat = bloat;
bcStats.minBloatBlock = origAddr;
}
if (bloat > maxBloat) {
maxBloat = bloat;
bcStats.maxBloatBlock = origAddr;
}
totalBloat += bloat;
}
bcStats.numBlocks = (int)blocks_.size();
bcStats.minBloat = minBloat;
bcStats.maxBloat = maxBloat;
bcStats.avgBloat = totalBloat / (double)blocks_.size();
}
int IRBlockCache::GetBlockNumberFromStartAddress(u32 em_address, bool realBlocksOnly) const {
u32 page = AddressToPage(em_address);
const auto iter = byPage_.find(page);
if (iter == byPage_.end())
return -1;
const std::vector<int> &blocksInPage = iter->second;
int best = -1;
for (int i : blocksInPage) {
if (blocks_[i].GetOriginalStart() == em_address) {
best = i;
if (blocks_[i].IsValid()) {
return i;
}
}
}
return best;
}
bool IRBlock::HasOriginalFirstOp() const {
return Memory::ReadUnchecked_U32(origAddr_) == origFirstOpcode_.encoding;
}
bool IRBlock::RestoreOriginalFirstOp(int cookie) {
const u32 emuhack = MIPS_EMUHACK_OPCODE | cookie;
if (Memory::ReadUnchecked_U32(origAddr_) == emuhack) {
Memory::Write_Opcode_JIT(origAddr_, origFirstOpcode_);
return true;
}
return false;
}
void IRBlock::Finalize(int cookie) {
if (origAddr_) {
origFirstOpcode_ = Memory::Read_Opcode_JIT(origAddr_);
MIPSOpcode opcode = MIPSOpcode(MIPS_EMUHACK_OPCODE | cookie);
Memory::Write_Opcode_JIT(origAddr_, opcode);
} else {
WARN_LOG(Log::JIT, "Finalizing invalid block (cookie: %d)", cookie);
}
}
void IRBlock::Destroy(int cookie) {
if (origAddr_) {
MIPSOpcode opcode = MIPSOpcode(MIPS_EMUHACK_OPCODE | cookie);
u32 memOp = Memory::ReadUnchecked_U32(origAddr_);
if (memOp == opcode.encoding) {
Memory::Write_Opcode_JIT(origAddr_, origFirstOpcode_);
} else {
DEBUG_LOG(Log::JIT, "IRBlock::Destroy: Note: Block at %08x was overwritten - checked for %08x, got %08x when restoring the MIPS op to %08x", origAddr_, opcode.encoding, memOp, origFirstOpcode_.encoding);
}
origAddr_ = 0;
}
}
u64 IRBlock::CalculateHash() const {
if (origAddr_) {
std::vector<u32> buffer;
buffer.resize(origSize_ / 4);
size_t pos = 0;
for (u32 off = 0; off < origSize_; off += 4) {
MIPSOpcode instr = Memory::ReadUnchecked_Instruction(origAddr_ + off, false);
buffer[pos++] = instr.encoding;
}
return XXH3_64bits(&buffer[0], origSize_);
}
return 0;
}
bool IRBlock::OverlapsRange(u32 addr, u32 size) const {
addr &= 0x3FFFFFFF;
u32 origAddr = origAddr_ & 0x3FFFFFFF;
return addr + size > origAddr && addr < origAddr + origSize_;
}
MIPSOpcode IRJit::GetOriginalOp(MIPSOpcode op) {
IRBlock *b = blocks_.GetBlock(blocks_.FindByCookie(op.encoding & 0xFFFFFF));
if (b) {
return b->GetOriginalFirstOp();
}
return op;
}
}