diff --git a/Common/CodeBlock.h b/Common/CodeBlock.h index 853c15c149..2e3894dde2 100644 --- a/Common/CodeBlock.h +++ b/Common/CodeBlock.h @@ -40,6 +40,10 @@ public: return ptr - region; } + size_t GetRegionSize() const { + return region_size; + } + virtual const u8 *GetCodePtrFromWritablePtr(u8 *ptr) = 0; virtual u8 *GetWritablePtrFromCodePtr(const u8 *ptr) = 0; diff --git a/Core/ConfigValues.h b/Core/ConfigValues.h index fd5eb1eae7..e9abd43dcb 100644 --- a/Core/ConfigValues.h +++ b/Core/ConfigValues.h @@ -276,6 +276,7 @@ enum class DebugOverlay : int { GPU_PROFILE, GPU_ALLOCATOR, FRAMEBUFFER_LIST, + JIT_STATS, }; // Android-only for now diff --git a/Core/MIPS/IR/IRJit.cpp b/Core/MIPS/IR/IRJit.cpp index f0836213dd..93264f4881 100644 --- a/Core/MIPS/IR/IRJit.cpp +++ b/Core/MIPS/IR/IRJit.cpp @@ -190,9 +190,15 @@ void IRJit::Compile(u32 em_address) { std::vector instructions; u32 mipsBytes; - if (!CompileBlock(em_address, instructions, mipsBytes)) { - // Ran out of block numbers - need to reset. - ERROR_LOG(Log::JIT, "Ran out of block numbers, clearing cache"); + const CompileResult result = CompileBlock(em_address, instructions, mipsBytes); + if (result != CompileResult::OK) { + if (result == CompileResult::ARENA_FULL) { + ERROR_LOG(Log::JIT, "IR arena full (%d blocks), clearing cache", blocks_.GetNumBlocks()); + irArenaFullClears_++; + } else { + ERROR_LOG(Log::JIT, "Native code space full (%d blocks), clearing cache", blocks_.GetNumBlocks()); + codeFullClears_++; + } ClearCache(); CompileBlock(em_address, instructions, mipsBytes); } @@ -205,7 +211,7 @@ void IRJit::Compile(u32 em_address) { } // WARNING! This can be called from IRInterpret / the JIT, through the function preload stuff! -bool IRJit::CompileBlock(u32 em_address, std::vector &instructions, u32 &mipsBytes) { +IRJit::CompileResult IRJit::CompileBlock(u32 em_address, std::vector &instructions, u32 &mipsBytes) { _dbg_assert_(compilerEnabled_); frontend_.DoJit(em_address, instructions, mipsBytes); @@ -214,8 +220,8 @@ bool IRJit::CompileBlock(u32 em_address, std::vector &instructions, u32 int block_num = blocks_.AllocateBlock(em_address, mipsBytes, instructions); if (block_num < 0) { WARN_LOG(Log::JIT, "Failed to allocate block for %08x (%d instructions)", em_address, (int)instructions.size()); - // Out of block numbers. Caller will handle. - return false; + // Caller will handle. + return CompileResult::ARENA_FULL; } IRBlock *b = blocks_.GetBlock(block_num); @@ -226,7 +232,7 @@ bool IRJit::CompileBlock(u32 em_address, std::vector &instructions, u32 } if (!CompileNativeBlock(&blocks_, block_num)) - return false; + return CompileResult::CODE_FULL; if (mipsTracer.tracing_enabled) { mipsTracer.prepare_block(b, blocks_); @@ -235,7 +241,16 @@ bool IRJit::CompileBlock(u32 em_address, std::vector &instructions, u32 // Updates stats, also enters the block in the block shadow. blocks_.FinalizeBlock(block_num); FinalizeNativeBlock(&blocks_, block_num); - return true; + return CompileResult::OK; +} + +void IRJit::GetStats(JitStats *stats) const { + stats->numBlocks = blocks_.GetNumBlocks(); + stats->numValidBlocks = blocks_.CountValidBlocks(); + stats->irArenaUsed = blocks_.GetArenaSize(); + stats->irArenaSize = IRBlockCache::MAX_ARENA_SIZE; + stats->irArenaFullClears = irArenaFullClears_; + stats->codeFullClears = codeFullClears_; } void IRJit::RunLoopUntil(u64 globalticks) { @@ -331,8 +346,17 @@ void IRBlockCache::Clear() { IRBlockCache::IRBlockCache(bool compileToNative) : compileToNative_(compileToNative) {} +int IRBlockCache::CountValidBlocks() const { + int count = 0; + for (const IRBlock &block : blocks_) { + if (block.IsValid()) { + count++; + } + } + return count; +} + int IRBlockCache::AllocateBlock(int emAddr, u32 origSize, const std::vector &insts) { - const u32 MAX_ARENA_SIZE = 0x1000000 - 1; if (arena_.empty()) { // Offset 0 would mean "no block" in the block shadow, and 1 a replacement hook. arena_.push_back(IRInst{ IROp::Bad }); diff --git a/Core/MIPS/IR/IRJit.h b/Core/MIPS/IR/IRJit.h index 1e24f36f63..e828ec30f5 100644 --- a/Core/MIPS/IR/IRJit.h +++ b/Core/MIPS/IR/IRJit.h @@ -111,10 +111,15 @@ public: Clear(); } + // In IR instructions. + static constexpr u32 MAX_ARENA_SIZE = 0x1000000 - 1; + void Clear(); std::vector FindInvalidatedBlockNumbers(u32 address, u32 length); void FinalizeBlock(int blockNum); int GetNumBlocks() const override { return (int)blocks_.size(); } + int CountValidBlocks() const; + size_t GetArenaSize() const { return arena_.size(); } int AllocateBlock(int emAddr, u32 origSize, const std::vector &inst); IRBlock *GetBlock(int blockNum) { if (blockNum >= 0 && blockNum < (int)blocks_.size()) { @@ -204,6 +209,7 @@ public: void ClearCache() override; void InvalidateCacheAt(u32 em_address, int length = 4) override; void UpdateFCR31() override; + void GetStats(JitStats *stats) const override; bool CodeInRange(const u8 *ptr) const override { return false; @@ -222,7 +228,12 @@ protected: void RunBlocks(); virtual void WriteProfileReport(); - bool CompileBlock(u32 em_address, std::vector &instructions, u32 &mipsBytes); + enum class CompileResult { + OK, + ARENA_FULL, + CODE_FULL, + }; + CompileResult CompileBlock(u32 em_address, std::vector &instructions, u32 &mipsBytes); virtual bool CompileNativeBlock(IRBlockCache *irBlockCache, int block_num) { return true; } virtual void FinalizeNativeBlock(IRBlockCache *irBlockCache, int block_num) {} virtual void InvalidateNativeBlock(IRBlockCache *irBlockCache, int block_num) {} @@ -240,6 +251,9 @@ protected: // Set by --jit-profile. The IR interpreter times every block, the native JITs sample the host PC. bool profiling_ = false; + int irArenaFullClears_ = 0; + int codeFullClears_ = 0; + // where to write branch-likely trampolines. not used atm // u32 blTrampolines_; // int blTrampolineCount_; diff --git a/Core/MIPS/IR/IRNativeCommon.cpp b/Core/MIPS/IR/IRNativeCommon.cpp index c1e872f843..c3dd2bf091 100644 --- a/Core/MIPS/IR/IRNativeCommon.cpp +++ b/Core/MIPS/IR/IRNativeCommon.cpp @@ -906,6 +906,13 @@ const u8 *IRNativeJit::GetCodeBase() const { return backend_->CodeBlock().GetBasePtr(); } +void IRNativeJit::GetStats(JitStats *stats) const { + IRJit::GetStats(stats); + const CodeBlockCommon &code = backend_->CodeBlock(); + stats->codeUsed = code.GetOffset(code.GetCodePtr()); + stats->codeSize = code.GetRegionSize(); +} + u32 IRNativeJit::GetBlockShadowHookValue() const { return (u32)backend_->OffsetFromCodePtr(backend_->GetNativeHooks().blockShadowHook); } diff --git a/Core/MIPS/IR/IRNativeCommon.h b/Core/MIPS/IR/IRNativeCommon.h index efeb2cd6c8..b1329bcc06 100644 --- a/Core/MIPS/IR/IRNativeCommon.h +++ b/Core/MIPS/IR/IRNativeCommon.h @@ -227,6 +227,7 @@ public: JitBlockCacheDebugInterface *GetBlockCacheDebugInterface() override; const u8 *GetCodeBase() const override; + void GetStats(JitStats *stats) const override; protected: void Init(IRNativeBackend &backend); diff --git a/Core/MIPS/JitCommon/JitCommon.h b/Core/MIPS/JitCommon/JitCommon.h index a3e60f4f8d..5f51de8b4a 100644 --- a/Core/MIPS/JitCommon/JitCommon.h +++ b/Core/MIPS/JitCommon/JitCommon.h @@ -114,10 +114,27 @@ namespace MIPSComp { virtual int Replace_fabsf() = 0; }; + // For the JIT stats debug overlay. + struct JitStats { + int numBlocks = 0; + int numValidBlocks = 0; + // In IR instructions. Invalidated blocks keep their space until the cache is cleared. + size_t irArenaUsed = 0; + size_t irArenaSize = 0; + // In bytes. Zero when not compiling to native code. + size_t codeUsed = 0; + size_t codeSize = 0; + // How many times the cache was cleared because one of them filled up. + int irArenaFullClears = 0; + int codeFullClears = 0; + }; + class JitInterface { public: virtual ~JitInterface() {} + virtual void GetStats(JitStats *stats) const {} + virtual const u8 *GetCodeBase() const = 0; // Just for displaying in the debugger. virtual bool CodeInRange(const u8 *ptr) const = 0; virtual bool DescribeCodePtr(const u8 *ptr, std::string &name) = 0; diff --git a/UI/DebugOverlay.cpp b/UI/DebugOverlay.cpp index 0bf44c726c..4ad819e980 100644 --- a/UI/DebugOverlay.cpp +++ b/UI/DebugOverlay.cpp @@ -7,6 +7,7 @@ #include "Common/Data/Text/StringWriter.h" #include "Core/MIPS/MIPS.h" +#include "Core/MIPS/JitCommon/JitCommon.h" #include "Core/HW/Display.h" #include "Core/FrameTiming.h" #include "Core/HLE/sceSas.h" @@ -66,6 +67,40 @@ static void DrawDebugStats(UIContext *ctx, const Bounds &bounds) { ctx->RebindTexture(); } +static void DrawJitStats(UIContext *ctx, const Bounds &bounds) { + FontID ubuntu24("UBUNTU24"); + + char statbuf[1024]; + StringWriter w(statbuf, sizeof(statbuf)); + if (MIPSComp::jit) { + MIPSComp::JitStats stats; + MIPSComp::jit->GetStats(&stats); + const double irPercent = stats.irArenaSize ? 100.0 * stats.irArenaUsed / stats.irArenaSize : 0.0; + w.F("Blocks: %d (%d valid)\n", stats.numBlocks, stats.numValidBlocks); + if (stats.codeSize) { + // The native JITs compile from the IR, and keep it for the debugger. + w.F("Native code: %0.2f / %0.2f MB (%0.1f%%)\n", stats.codeUsed / (1024.0 * 1024.0), stats.codeSize / (1024.0 * 1024.0), + 100.0 * stats.codeUsed / stats.codeSize); + w.F("IR (debug info): %d / %d instructions (%0.1f%%)\n", (int)stats.irArenaUsed, (int)stats.irArenaSize, irPercent); + w.F("Cleared because full: native code %d, IR %d\n", stats.codeFullClears, stats.irArenaFullClears); + } else { + w.F("IR arena: %d / %d instructions (%0.1f%%)\n", (int)stats.irArenaUsed, (int)stats.irArenaSize, irPercent); + w.F("Cleared because full: %d\n", stats.irArenaFullClears); + } + } else { + w.W("No JIT (interpreter)\n"); + } + + ctx->Flush(); + ctx->BindFontTexture(); + ctx->Draw()->SetFontScale(.7f, .7f); + ctx->Draw()->DrawTextRect(ubuntu24, w.as_view(), bounds.x + 11, bounds.y + 31, bounds.w - 20, bounds.h - 30, 0xc0000000, FLAG_DYNAMIC_ASCII); + ctx->Draw()->DrawTextRect(ubuntu24, w.as_view(), bounds.x + 10, bounds.y + 30, bounds.w - 20, bounds.h - 30, 0xFFFFFFFF, FLAG_DYNAMIC_ASCII); + ctx->Draw()->SetFontScale(1.0f, 1.0f); + ctx->Flush(); + ctx->RebindTexture(); +} + static void DrawAudioDebugStats(UIContext *ctx, const Bounds &bounds) { FontID ubuntu24("UBUNTU24"); @@ -271,6 +306,10 @@ void DrawDebugOverlay(UIContext *ctx, const Bounds &bounds, DebugOverlay overlay if (inGame) DrawFramebufferList(ctx, gpu, layoutBounds); break; + case DebugOverlay::JIT_STATS: + if (inGame) + DrawJitStats(ctx, layoutBounds); + break; default: break; } diff --git a/UI/DevScreens.cpp b/UI/DevScreens.cpp index c652c919fa..8f1073904b 100644 --- a/UI/DevScreens.cpp +++ b/UI/DevScreens.cpp @@ -105,16 +105,18 @@ static const char *g_debugOverlayList[] = { "GPU Profile", "GPU Allocator Viewer", "Framebuffer list", + "JIT stats", }; void AddOverlayList(UI::ViewGroup *items, ScreenManager *screenManager) { using namespace UI; auto dev = GetI18NCategory(I18NCat::DEVELOPER); - int numOverlays = ARRAY_SIZE(g_debugOverlayList); + static_assert(ARRAY_SIZE(g_debugOverlayList) == (int)DebugOverlay::JIT_STATS + 1, "g_debugOverlayList must match DebugOverlay"); + PopupMultiChoice *choice = items->Add(new PopupMultiChoice((int *)&g_Config.iDebugOverlay, dev->T("Debug overlay"), g_debugOverlayList, 0, ARRAY_SIZE(g_debugOverlayList), I18NCat::DEVELOPER, screenManager)); if (!(g_Config.iGPUBackend == (int)GPUBackend::VULKAN || g_Config.iGPUBackend == (int)GPUBackend::OPENGL)) { - numOverlays -= 2; // skip the last 2. + choice->HideChoice((int)DebugOverlay::GPU_PROFILE); + choice->HideChoice((int)DebugOverlay::GPU_ALLOCATOR); } - items->Add(new PopupMultiChoice((int *)&g_Config.iDebugOverlay, dev->T("Debug overlay"), g_debugOverlayList, 0, numOverlays, I18NCat::DEVELOPER, screenManager)); } void SaveFrameDump() {