// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #include "no-jit-improvements.h" #include "Config.h" #include "R5900OpcodeTables.h" #include "vtlb.h" #include #include namespace NoJITImprovements { namespace { // Short fixed blocks avoid runtime allocation on the EE execution path. // Branch delay slots remain on the original interpreter path because the // branch handlers execute them directly. static constexpr u32 BLOCK_INSTRUCTION_CAPACITY = 16; static constexpr u32 BLOCK_CACHE_SIZE = 4096; static constexpr u32 INVALID_PC = std::numeric_limits::max(); struct alignas(64) EEBlock { u32 start_pc = INVALID_PC; u8 instruction_count = 0; std::array code{}; std::array opcodes{}; }; static std::array s_ee_blocks; static __fi u32 GetCacheIndex(u32 pc) { const u32 instruction_address = pc >> 2; return (instruction_address ^ (instruction_address >> 11) ^ (instruction_address >> 19)) & (BLOCK_CACHE_SIZE - 1); } static bool IsBlockCurrent(const EEBlock& block) { if (block.instruction_count == 0) return false; const u32 byte_count = static_cast(block.instruction_count) * sizeof(u32); return vtlb_memSafeCmpBytes(block.start_pc, block.code.data(), byte_count) == 0; } static bool BuildBlock(EEBlock& block, u32 pc) { block.start_pc = INVALID_PC; block.instruction_count = 0; u32 instruction_pc = pc; for (u32 i = 0; i < BLOCK_INSTRUCTION_CAPACITY; i++, instruction_pc += sizeof(u32)) { bool valid = false; const u32 code = vtlb_ramRead(instruction_pc, &valid); if (!valid) break; const R5900::OPCODE& opcode = R5900::GetInstruction(code); block.code[i] = code; block.opcodes[i] = &opcode; block.instruction_count++; // A store can modify a later instruction in the current block. // COP0 operations can alter address translation. End immediately // after either so the next block validates the new memory view. const bool changes_memory_view = (opcode.flags & (IS_BRANCH | IS_STORE)) != 0 || (code >> 26) == 0x10; if (changes_memory_view || instruction_pc > (INVALID_PC - sizeof(u32))) break; } if (block.instruction_count == 0) return false; // Publish the key last so a partially rebuilt slot is never treated // as a valid hit if execution is interrupted by a VM exit. block.start_pc = pc; return true; } static bool IsCacheEnabled() { #if defined(PCSX2_DEVBUILD) || defined(PCSX2_RECOMPILER_TESTS) // Debug stepping and the interpreter oracle require the original // one-instruction fetch path. return false; #else // EE cache emulation can intentionally expose instruction bytes // which differ from RAM, so side-effect-free RAM validation cannot // be used while that compatibility option is active. return !CHECK_EEREC && !CHECK_CACHE; #endif } } bool ExecuteEEBlock(u32 pc, ExecuteInstruction execute_instruction) { if (!execute_instruction || !IsCacheEnabled()) return false; EEBlock& block = s_ee_blocks[GetCacheIndex(pc)]; if (block.start_pc != pc || !IsBlockCurrent(block)) { if (!BuildBlock(block, pc)) return false; } for (u32 i = 0; i < block.instruction_count; i++) { if (!execute_instruction(pc + (i * sizeof(u32)), block.code[i], *block.opcodes[i])) break; } return true; } void ResetEEBlockCache() { for (EEBlock& block : s_ee_blocks) { block.start_pc = INVALID_PC; block.instruction_count = 0; } } void InvalidateEEBlockCache(u32 address, u32 size_in_words) { if (size_in_words == 0) return; // Large clears are cheaper to handle as a complete cache reset. if (size_in_words >= BLOCK_CACHE_SIZE) { ResetEEBlockCache(); return; } const u64 clear_start = address; const u64 clear_end = clear_start + (static_cast(size_in_words) * sizeof(u32)); if (clear_end > (static_cast(std::numeric_limits::max()) + 1)) { ResetEEBlockCache(); return; } // A cached block can begin up to fifteen instructions before the // cleared range. Probe only those possible direct-mapped slots instead // of scanning the complete cache. const u64 lookbehind = (BLOCK_INSTRUCTION_CAPACITY - 1) * sizeof(u32); const u64 candidate_start = (clear_start > lookbehind) ? (clear_start - lookbehind) : 0; for (u64 candidate_pc = candidate_start & ~static_cast(3); candidate_pc < clear_end; candidate_pc += sizeof(u32)) { EEBlock& block = s_ee_blocks[GetCacheIndex(static_cast(candidate_pc))]; if (block.instruction_count == 0) continue; const u64 block_start = block.start_pc; const u64 block_end = block_start + (static_cast(block.instruction_count) * sizeof(u32)); if (clear_start < block_end && block_start < clear_end) { block.start_pc = INVALID_PC; block.instruction_count = 0; } } } }