From 9afd09598cc81eabd3fcfe296c25ace2a71aca26 Mon Sep 17 00:00:00 2001 From: mitaclaw <140017135+mitaclaw@users.noreply.github.com> Date: Mon, 15 Apr 2024 21:52:40 -0700 Subject: [PATCH] DolphinQt: JIT Widget Refresh Fulfilling a certain six-year-old todo. --- Source/Android/jni/MainAndroid.cpp | 8 + Source/Core/Common/HostDisassembler.cpp | 198 ++--- Source/Core/Common/HostDisassembler.h | 28 +- Source/Core/Core/Host.h | 2 + .../CachedInterpreter/CachedInterpreter.cpp | 17 +- .../CachedInterpreter/CachedInterpreter.h | 3 + Source/Core/Core/PowerPC/Jit64/Jit.cpp | 44 +- Source/Core/Core/PowerPC/Jit64/Jit.h | 5 + Source/Core/Core/PowerPC/JitArm64/Jit.cpp | 21 +- Source/Core/Core/PowerPC/JitArm64/Jit.h | 7 + Source/Core/Core/PowerPC/JitCommon/JitBase.h | 4 + .../Core/Core/PowerPC/JitCommon/JitCache.cpp | 29 +- Source/Core/Core/PowerPC/JitCommon/JitCache.h | 9 +- Source/Core/Core/PowerPC/JitInterface.cpp | 62 +- Source/Core/Core/PowerPC/JitInterface.h | 23 +- Source/Core/DolphinNoGUI/MainNoGUI.cpp | 8 + Source/Core/DolphinQt/CMakeLists.txt | 3 + .../DolphinQt/Debugger/CodeViewWidget.cpp | 2 +- .../Core/DolphinQt/Debugger/CodeViewWidget.h | 2 +- Source/Core/DolphinQt/Debugger/CodeWidget.cpp | 5 + Source/Core/DolphinQt/Debugger/CodeWidget.h | 3 +- Source/Core/DolphinQt/Debugger/JITWidget.cpp | 765 ++++++++++++++---- Source/Core/DolphinQt/Debugger/JITWidget.h | 130 ++- .../DolphinQt/Debugger/JitBlockTableModel.cpp | 452 +++++++++++ .../DolphinQt/Debugger/JitBlockTableModel.h | 126 +++ Source/Core/DolphinQt/DolphinQt.vcxproj | 3 + Source/Core/DolphinQt/Host.cpp | 10 + Source/Core/DolphinQt/Host.h | 2 + Source/Core/DolphinQt/MainWindow.cpp | 6 +- Source/Core/DolphinQt/MenuBar.cpp | 8 + Source/Core/DolphinQt/MenuBar.h | 1 + .../DolphinQt/QtUtils/ClickableStatusBar.h | 22 + .../Core/DolphinTool/ToolHeadlessPlatform.cpp | 8 + Source/DSPTool/StubHost.cpp | 6 + Source/UnitTests/Core/PageFaultTest.cpp | 2 + Source/UnitTests/StubHost.cpp | 6 + 36 files changed, 1640 insertions(+), 390 deletions(-) create mode 100644 Source/Core/DolphinQt/Debugger/JitBlockTableModel.cpp create mode 100644 Source/Core/DolphinQt/Debugger/JitBlockTableModel.h create mode 100644 Source/Core/DolphinQt/QtUtils/ClickableStatusBar.h diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index f0a74986f6..1d26f1d895 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -146,6 +146,14 @@ void Host_UpdateDisasmDialog() { } +void Host_JitCacheCleared() +{ +} + +void Host_JitProfileDataWiped() +{ +} + void Host_UpdateMainFrame() { } diff --git a/Source/Core/Common/HostDisassembler.cpp b/Source/Core/Common/HostDisassembler.cpp index d21536ecc4..14dd3b5b46 100644 --- a/Source/Core/Common/HostDisassembler.cpp +++ b/Source/Core/Common/HostDisassembler.cpp @@ -3,52 +3,43 @@ #include "Common/HostDisassembler.h" -#include +#include + +#include +#include +#include #if defined(HAVE_LLVM) -#include #include #include #elif defined(_M_X86_64) #include // Bochs #endif -#include "Common/Assert.h" -#include "Common/VariantUtil.h" -#include "Core/PowerPC/JitInterface.h" -#include "Core/System.h" - #if defined(HAVE_LLVM) -class HostDisassemblerLLVM : public HostDisassembler +class HostDisassemblerLLVM final : public HostDisassembler { public: - HostDisassemblerLLVM(const std::string& host_disasm, int inst_size = -1, - const std::string& cpu = ""); - ~HostDisassemblerLLVM() - { - if (m_can_disasm) - LLVMDisasmDispose(m_llvm_context); - } + explicit HostDisassemblerLLVM(const char* host_disasm, const char* cpu = "", + std::size_t inst_size = 0); + ~HostDisassemblerLLVM(); private: - bool m_can_disasm; LLVMDisasmContextRef m_llvm_context; - int m_instruction_size; + std::size_t m_instruction_size; - std::string DisassembleHostBlock(const u8* code_start, const u32 code_size, - u32* host_instructions_count, u64 starting_pc) override; + std::size_t Disassemble(const u8* begin, const u8* end, std::ostream& stream) override; }; -HostDisassemblerLLVM::HostDisassemblerLLVM(const std::string& host_disasm, int inst_size, - const std::string& cpu) - : m_can_disasm(false), m_instruction_size(inst_size) +HostDisassemblerLLVM::HostDisassemblerLLVM(const char* host_disasm, const char* cpu, + std::size_t inst_size) + : m_instruction_size(inst_size) { LLVMInitializeAllTargetInfos(); LLVMInitializeAllTargetMCs(); LLVMInitializeAllDisassemblers(); - m_llvm_context = - LLVMCreateDisasmCPU(host_disasm.c_str(), cpu.c_str(), nullptr, 0, nullptr, nullptr); + m_llvm_context = LLVMCreateDisasmCPU(host_disasm, cpu, nullptr, 0, nullptr, nullptr); // Couldn't create llvm context if (!m_llvm_context) @@ -56,153 +47,112 @@ HostDisassemblerLLVM::HostDisassemblerLLVM(const std::string& host_disasm, int i LLVMSetDisasmOptions(m_llvm_context, LLVMDisassembler_Option_AsmPrinterVariant | LLVMDisassembler_Option_PrintLatency); - - m_can_disasm = true; } -std::string HostDisassemblerLLVM::DisassembleHostBlock(const u8* code_start, const u32 code_size, - u32* host_instructions_count, - u64 starting_pc) +HostDisassemblerLLVM::~HostDisassemblerLLVM() { - if (!m_can_disasm) - return "(No LLVM context)"; + if (m_llvm_context) + LLVMDisasmDispose(m_llvm_context); +} - u8* disasmPtr = (u8*)code_start; - const u8* end = code_start + code_size; +std::size_t HostDisassemblerLLVM::Disassemble(const u8* begin, const u8* end, std::ostream& stream) +{ + std::size_t instruction_count = 0; + if (!m_llvm_context) + return instruction_count; - std::ostringstream x86_disasm; - while ((u8*)disasmPtr < end) + while (begin < end) { char inst_disasm[256]; - size_t inst_size = LLVMDisasmInstruction(m_llvm_context, disasmPtr, (u64)(end - disasmPtr), - starting_pc, inst_disasm, 256); - x86_disasm << "0x" << std::hex << starting_pc << "\t"; - if (!inst_size) + const auto inst_size = + LLVMDisasmInstruction(m_llvm_context, const_cast(begin), static_cast(end - begin), + reinterpret_cast(begin), inst_disasm, sizeof(inst_disasm)); + if (inst_size == 0) { - x86_disasm << "Invalid inst:"; - - if (m_instruction_size != -1) + if (m_instruction_size != 0) { - // If we are on an architecture that has a fixed instruction size - // We can continue onward past this bad instruction. - std::string inst_str; - for (int i = 0; i < m_instruction_size; ++i) - inst_str += fmt::format("{:02x}", disasmPtr[i]); - - x86_disasm << inst_str << std::endl; - disasmPtr += m_instruction_size; + // If we are on an architecture that has a fixed instruction + // size, we can continue onward past this bad instruction. + fmt::println(stream, "{}\tInvalid inst: {:02x}", fmt::ptr(begin), + fmt::join(std::span{begin, m_instruction_size}, "")); + begin += m_instruction_size; } else { - // We can't continue if we are on an architecture that has flexible instruction sizes - // Dump the rest of the block instead - std::string code_block; - for (int i = 0; (disasmPtr + i) < end; ++i) - code_block += fmt::format("{:02x}", disasmPtr[i]); - - x86_disasm << code_block << std::endl; + // We can't continue if we are on an architecture that has flexible + // instruction sizes. Dump the rest of the block instead. + fmt::println(stream, "{}\tInvalid inst: {:02x}", fmt::ptr(begin), + fmt::join(std::span{begin, end}, "")); break; } } else { - x86_disasm << inst_disasm << std::endl; - disasmPtr += inst_size; - starting_pc += inst_size; + fmt::println(stream, "{}{}", fmt::ptr(begin), inst_disasm); + begin += inst_size; } - - (*host_instructions_count)++; + ++instruction_count; } - - return x86_disasm.str(); + return instruction_count; } #elif defined(_M_X86_64) -class HostDisassemblerX86 : public HostDisassembler +class HostDisassemblerBochs final : public HostDisassembler { public: - HostDisassemblerX86(); + explicit HostDisassemblerBochs(); + ~HostDisassemblerBochs() = default; private: disassembler m_disasm; - std::string DisassembleHostBlock(const u8* code_start, const u32 code_size, - u32* host_instructions_count, u64 starting_pc) override; + std::size_t Disassemble(const u8* begin, const u8* end, std::ostream& stream) override; }; -HostDisassemblerX86::HostDisassemblerX86() +HostDisassemblerBochs::HostDisassemblerBochs() { m_disasm.set_syntax_intel(); } -std::string HostDisassemblerX86::DisassembleHostBlock(const u8* code_start, const u32 code_size, - u32* host_instructions_count, u64 starting_pc) +std::size_t HostDisassemblerBochs::Disassemble(const u8* begin, const u8* end, std::ostream& stream) { - u64 disasmPtr = (u64)code_start; - const u8* end = code_start + code_size; - - std::ostringstream x86_disasm; - while ((u8*)disasmPtr < end) + std::size_t instruction_count = 0; + while (begin < end) { char inst_disasm[256]; - disasmPtr += m_disasm.disasm64(disasmPtr, disasmPtr, (u8*)disasmPtr, inst_disasm); - x86_disasm << inst_disasm << std::endl; - (*host_instructions_count)++; + + const auto inst_size = + m_disasm.disasm64(0, reinterpret_cast(begin), begin, inst_disasm); + fmt::println(stream, "{}\t{}", fmt::ptr(begin), inst_disasm); + begin += inst_size; + ++instruction_count; } - - return x86_disasm.str(); + return instruction_count; } #endif -std::unique_ptr GetNewDisassembler(const std::string& arch) +std::unique_ptr HostDisassembler::Factory(Platform arch) { + switch (arch) + { #if defined(HAVE_LLVM) - if (arch == "x86") + case Platform::x86_64: return std::make_unique("x86_64-none-unknown"); - if (arch == "aarch64") - return std::make_unique("aarch64-none-unknown", 4, "cortex-a57"); - if (arch == "armv7") - return std::make_unique("armv7-none-unknown", 4, "cortex-a15"); + case Platform::aarch64: + return std::make_unique("aarch64-none-unknown", "cortex-a57", 4); #elif defined(_M_X86_64) - if (arch == "x86") - return std::make_unique(); + case Platform::x86_64: + return std::make_unique(); +#else + case Platform{}: // warning C4065: "switch statement contains 'default' but no 'case' labels" #endif - return std::make_unique(); + default: + return std::make_unique(); + } } -DisassembleResult DisassembleBlock(HostDisassembler* disasm, u32 address) +std::size_t HostDisassembler::Disassemble(const u8* begin, const u8* end, std::ostream& stream) { - auto res = Core::System::GetInstance().GetJitInterface().GetHostCode(address); - - return std::visit(overloaded{[&](JitInterface::GetHostCodeError error) { - DisassembleResult result; - switch (error) - { - case JitInterface::GetHostCodeError::NoJitActive: - result.text = "(No JIT active)"; - break; - case JitInterface::GetHostCodeError::NoTranslation: - result.text = "(No translation)"; - break; - default: - ASSERT(false); - break; - } - result.entry_address = address; - result.instruction_count = 0; - result.code_size = 0; - return result; - }, - [&](JitInterface::GetHostCodeResult host_result) { - DisassembleResult new_result; - u32 instruction_count = 0; - new_result.text = disasm->DisassembleHostBlock( - host_result.code, host_result.code_size, &instruction_count, - (u64)host_result.code); - new_result.entry_address = host_result.entry_address; - new_result.code_size = host_result.code_size; - new_result.instruction_count = instruction_count; - return new_result; - }}, - res); + fmt::println(stream, "{}\t{:02x}", fmt::ptr(begin), fmt::join(std::span{begin, end}, "")); + return 0; } diff --git a/Source/Core/Common/HostDisassembler.h b/Source/Core/Common/HostDisassembler.h index 6c193c49c0..3761f52937 100644 --- a/Source/Core/Common/HostDisassembler.h +++ b/Source/Core/Common/HostDisassembler.h @@ -3,28 +3,24 @@ #pragma once +#include +#include #include -#include + #include "Common/CommonTypes.h" class HostDisassembler { public: - virtual ~HostDisassembler() {} - virtual std::string DisassembleHostBlock(const u8* code_start, const u32 code_size, - u32* host_instructions_count, u64 starting_pc) + enum class Platform { - return "(No disassembler)"; - } -}; + x86_64, + aarch64, + }; -struct DisassembleResult -{ - std::string text; - u32 entry_address = 0; - u32 instruction_count = 0; - u32 code_size = 0; -}; + virtual ~HostDisassembler() = default; -std::unique_ptr GetNewDisassembler(const std::string& arch); -DisassembleResult DisassembleBlock(HostDisassembler* disasm, u32 address); + static std::unique_ptr Factory(Platform arch); + + virtual std::size_t Disassemble(const u8* begin, const u8* end, std::ostream& stream); +}; diff --git a/Source/Core/Core/Host.h b/Source/Core/Core/Host.h index 66a5e2d78e..6d217f2650 100644 --- a/Source/Core/Core/Host.h +++ b/Source/Core/Core/Host.h @@ -60,6 +60,8 @@ void Host_PPCSymbolsChanged(); void Host_RefreshDSPDebuggerWindow(); void Host_RequestRenderWindowSize(int width, int height); void Host_UpdateDisasmDialog(); +void Host_JitCacheCleared(); +void Host_JitProfileDataWiped(); void Host_UpdateMainFrame(); void Host_UpdateTitle(const std::string& title); void Host_YieldToUI(); diff --git a/Source/Core/Core/PowerPC/CachedInterpreter/CachedInterpreter.cpp b/Source/Core/Core/PowerPC/CachedInterpreter/CachedInterpreter.cpp index 99b9e30163..a8a9f99ed3 100644 --- a/Source/Core/Core/PowerPC/CachedInterpreter/CachedInterpreter.cpp +++ b/Source/Core/Core/PowerPC/CachedInterpreter/CachedInterpreter.cpp @@ -10,6 +10,7 @@ #include "Core/CoreTiming.h" #include "Core/HLE/HLE.h" #include "Core/HW/CPU.h" +#include "Core/Host.h" #include "Core/PowerPC/Gekko.h" #include "Core/PowerPC/Interpreter/Interpreter.h" #include "Core/PowerPC/Jit64Common/Jit64Constants.h" @@ -290,13 +291,12 @@ void CachedInterpreter::Jit(u32 em_address, bool clear_cache_and_retry_on_failur b->far_begin = b->far_end = nullptr; b->codeSize = static_cast(b->near_end - b->normalEntry); - b->originalSize = code_block.m_num_instructions; // Mark the memory region that this code block uses in the RangeSizeSet. if (b->near_begin != b->near_end) m_free_ranges.erase(b->near_begin, b->near_end); - m_block_cache.FinalizeBlock(*b, jo.enableBlocklink, code_block.m_physical_addresses); + m_block_cache.FinalizeBlock(*b, jo.enableBlocklink, code_block, m_code_buffer); return; } @@ -413,6 +413,18 @@ std::vector CachedInterpreter::GetMemoryStats() const return {{"free", m_free_ranges.get_stats()}}; } +std::size_t CachedInterpreter::DisassembleNearCode(const JitBlock& block, + std::ostream& stream) const +{ + return Disassemble(block, stream); +} + +std::size_t CachedInterpreter::DisassembleFarCode(const JitBlock& block, std::ostream& stream) const +{ + stream << "N/A\n"; + return 0; +} + void CachedInterpreter::ClearCache() { m_block_cache.Clear(); @@ -420,4 +432,5 @@ void CachedInterpreter::ClearCache() ClearCodeSpace(); ResetFreeMemoryRanges(); RefreshConfig(); + Host_JitCacheCleared(); } diff --git a/Source/Core/Core/PowerPC/CachedInterpreter/CachedInterpreter.h b/Source/Core/Core/PowerPC/CachedInterpreter/CachedInterpreter.h index 9f84f5d4ec..4096c7b3a8 100644 --- a/Source/Core/Core/PowerPC/CachedInterpreter/CachedInterpreter.h +++ b/Source/Core/Core/PowerPC/CachedInterpreter/CachedInterpreter.h @@ -51,6 +51,9 @@ public: static std::size_t Disassemble(const JitBlock& block, std::ostream& stream); + std::size_t DisassembleNearCode(const JitBlock& block, std::ostream& stream) const override; + std::size_t DisassembleFarCode(const JitBlock& block, std::ostream& stream) const override; + JitBaseBlockCache* GetBlockCache() override { return &m_block_cache; } const char* GetName() const override { return "Cached Interpreter"; } const CommonAsmRoutinesBase* GetAsmRoutines() override { return nullptr; } diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.cpp b/Source/Core/Core/PowerPC/Jit64/Jit.cpp index 0f408f8bb7..7d26ab76af 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit.cpp @@ -9,6 +9,7 @@ #include #include +#include // for the PROFILER stuff #ifdef _WIN32 @@ -18,6 +19,7 @@ #include "Common/CommonTypes.h" #include "Common/EnumUtils.h" #include "Common/GekkoDisassembler.h" +#include "Common/HostDisassembler.h" #include "Common/IOFile.h" #include "Common/Logging/Log.h" #include "Common/StringUtil.h" @@ -30,6 +32,7 @@ #include "Core/HW/GPFifo.h" #include "Core/HW/Memmap.h" #include "Core/HW/ProcessorInterface.h" +#include "Core/Host.h" #include "Core/MachineContext.h" #include "Core/PatchEngine.h" #include "Core/PowerPC/Interpreter/Interpreter.h" @@ -116,7 +119,9 @@ using namespace PowerPC; and such, but it's currently limited to integer ops only. This can definitely be made better. */ -Jit64::Jit64(Core::System& system) : JitBase(system), QuantizedMemoryRoutines(*this) +Jit64::Jit64(Core::System& system) + : JitBase(system), QuantizedMemoryRoutines(*this), + m_disassembler(HostDisassembler::Factory(HostDisassembler::Platform::x86_64)) { } @@ -308,6 +313,7 @@ void Jit64::ClearCache() RefreshConfig(); asm_routines.Regenerate(); ResetFreeMemoryRanges(); + Host_JitCacheCleared(); } void Jit64::FreeRanges() @@ -826,7 +832,7 @@ void Jit64::Jit(u32 em_address, bool clear_cache_and_retry_on_failure) b->far_begin = far_start; b->far_end = far_end; - blocks.FinalizeBlock(*b, jo.enableBlocklink, code_block.m_physical_addresses); + blocks.FinalizeBlock(*b, jo.enableBlocklink, code_block, m_code_buffer); return; } } @@ -1193,7 +1199,6 @@ bool Jit64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) } b->codeSize = static_cast(GetCodePtr() - b->normalEntry); - b->originalSize = code_block.m_num_instructions; #ifdef JIT_LOG_GENERATED_CODE LogGeneratedX86(code_block.m_num_instructions, m_code_buffer, start, b); @@ -1213,6 +1218,39 @@ std::vector Jit64::GetMemoryStats() const return {{"near", m_free_ranges_near.get_stats()}, {"far", m_free_ranges_far.get_stats()}}; } +std::size_t Jit64::DisassembleNearCode(const JitBlock& block, std::ostream& stream) const +{ + // The last element of the JitBlock::linkData vector is not necessarily the furthest exit. + // See: Jit64::JustWriteExit + const auto iter = std::ranges::max_element(block.linkData, {}, &JitBlock::LinkData::exitPtrs); + + // Link data is not guaranteed, e.g. Jit64::WriteRfiExitDestInRSCRATCH + if (iter == block.linkData.end()) + return m_disassembler->Disassemble(block.normalEntry, block.near_end, stream); + + // A JitBlock's near_end only records where the XEmitter was after DoJit concludes. However, a + // JitBlock's exits will be modified by block linking. If Block A wants to link its final exit + // to the entry_point of Block B, and Block B follows Block A in memory, then the final exit's + // JMP will not have its destination modified but will instead be overwritten by a multibyte NOP. + // Trickily, Block A's near_end does not necessarily equal Block B's entry_point because Block B's + // entry_point is aligned to the next multiple of 4! This means the multibyte NOP may need to + // extend past Block A's near_end, complicating host code disassembly. If the opcode of a JMP + // instruction is found at the final exit, the block will be disassembled like normal. If one + // is not, the exit is assumed to be overwritten, and special action is taken. + const u8* const furthest_exit = iter->exitPtrs; + if (*furthest_exit == 0xE9) + return m_disassembler->Disassemble(block.normalEntry, block.near_end, stream); + + const auto inst_count = m_disassembler->Disassemble(block.normalEntry, furthest_exit, stream); + fmt::println(stream, "{}\tmultibyte nop", fmt::ptr(furthest_exit)); + return inst_count + 1; +} + +std::size_t Jit64::DisassembleFarCode(const JitBlock& block, std::ostream& stream) const +{ + return m_disassembler->Disassemble(block.far_begin, block.far_end, stream); +} + BitSet8 Jit64::ComputeStaticGQRs(const PPCAnalyst::CodeBlock& cb) const { return cb.m_gqr_used & ~cb.m_gqr_modified; diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.h b/Source/Core/Core/PowerPC/Jit64/Jit.h index 75fd8a118b..b1969704b3 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit.h +++ b/Source/Core/Core/PowerPC/Jit64/Jit.h @@ -34,6 +34,7 @@ #include "Core/PowerPC/JitCommon/JitBase.h" #include "Core/PowerPC/JitCommon/JitCache.h" +class HostDisassembler; namespace PPCAnalyst { struct CodeBlock; @@ -68,6 +69,9 @@ public: void EraseSingleBlock(const JitBlock& block) override; std::vector GetMemoryStats() const override; + std::size_t DisassembleNearCode(const JitBlock& block, std::ostream& stream) const override; + std::size_t DisassembleFarCode(const JitBlock& block, std::ostream& stream) const override; + // Finds a free memory region and sets the near and far code emitters to point at that region. // Returns false if no free memory region can be found for either of the two. bool SetEmitterStateToFreeCodeRegion(); @@ -288,6 +292,7 @@ private: const bool m_im_here_debug = false; const bool m_im_here_log = false; std::map m_been_here; + std::unique_ptr m_disassembler; }; void LogGeneratedX86(size_t size, const PPCAnalyst::CodeBuffer& code_buffer, const u8* normalEntry, diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp index ad129ad98f..d2dfbc605b 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp @@ -9,6 +9,7 @@ #include "Common/Arm64Emitter.h" #include "Common/CommonTypes.h" #include "Common/EnumUtils.h" +#include "Common/HostDisassembler.h" #include "Common/Logging/Log.h" #include "Common/MathUtil.h" #include "Common/MsgHandler.h" @@ -22,6 +23,7 @@ #include "Core/HW/GPFifo.h" #include "Core/HW/Memmap.h" #include "Core/HW/ProcessorInterface.h" +#include "Core/Host.h" #include "Core/PatchEngine.h" #include "Core/PowerPC/Interpreter/Interpreter.h" #include "Core/PowerPC/JitArm64/JitArm64_RegCache.h" @@ -39,7 +41,9 @@ constexpr size_t NEAR_CODE_SIZE = 1024 * 1024 * 64; constexpr size_t FAR_CODE_SIZE = 1024 * 1024 * 64; constexpr size_t TOTAL_CODE_SIZE = NEAR_CODE_SIZE * 2 + FAR_CODE_SIZE * 2; -JitArm64::JitArm64(Core::System& system) : JitBase(system), m_float_emit(this) +JitArm64::JitArm64(Core::System& system) + : JitBase(system), m_float_emit(this), + m_disassembler(HostDisassembler::Factory(HostDisassembler::Platform::aarch64)) { } @@ -186,6 +190,8 @@ void JitArm64::GenerateAsmAndResetFreeMemoryRanges() ResetFreeMemoryRanges(routines_near_end - routines_near_start, routines_far_end - routines_far_start); + + Host_JitCacheCleared(); } void JitArm64::FreeRanges() @@ -1014,7 +1020,7 @@ void JitArm64::Jit(u32 em_address, bool clear_cache_and_retry_on_failure) b->far_begin = far_start; b->far_end = far_end; - blocks.FinalizeBlock(*b, jo.enableBlocklink, code_block.m_physical_addresses); + blocks.FinalizeBlock(*b, jo.enableBlocklink, code_block, m_code_buffer); return; } } @@ -1048,6 +1054,16 @@ std::vector JitArm64::GetMemoryStats() const {"far_1", m_free_ranges_far_1.get_stats()}}; } +std::size_t JitArm64::DisassembleNearCode(const JitBlock& block, std::ostream& stream) const +{ + return m_disassembler->Disassemble(block.normalEntry, block.near_end, stream); +} + +std::size_t JitArm64::DisassembleFarCode(const JitBlock& block, std::ostream& stream) const +{ + return m_disassembler->Disassemble(block.far_begin, block.far_end, stream); +} + std::optional JitArm64::SetEmitterStateToFreeCodeRegion() { // Find some large free memory blocks and set code emitters to point at them. If we can't find @@ -1366,7 +1382,6 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) } b->codeSize = static_cast(GetCodePtr() - b->normalEntry); - b->originalSize = code_block.m_num_instructions; FlushIcache(); m_far_code.FlushIcache(); diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.h b/Source/Core/Core/PowerPC/JitArm64/Jit.h index f73c5f3252..e27392b3e5 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.h +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.h @@ -20,6 +20,8 @@ #include "Core/PowerPC/JitCommon/JitBase.h" #include "Core/PowerPC/PPCAnalyst.h" +class HostDisassembler; + class JitArm64 : public JitBase, public Arm64Gen::ARM64CodeBlock, public CommonAsmRoutinesBase { public: @@ -51,6 +53,9 @@ public: void EraseSingleBlock(const JitBlock& block) override; std::vector GetMemoryStats() const override; + std::size_t DisassembleNearCode(const JitBlock& block, std::ostream& stream) const override; + std::size_t DisassembleFarCode(const JitBlock& block, std::ostream& stream) const override; + const char* GetName() const override { return "JITARM64"; } // OPCODES @@ -413,4 +418,6 @@ protected: HyoutaUtilities::RangeSizeSet m_free_ranges_near_1; HyoutaUtilities::RangeSizeSet m_free_ranges_far_0; HyoutaUtilities::RangeSizeSet m_free_ranges_far_1; + + std::unique_ptr m_disassembler; }; diff --git a/Source/Core/Core/PowerPC/JitCommon/JitBase.h b/Source/Core/Core/PowerPC/JitCommon/JitBase.h index cad4e6d44e..fa2fdd167f 100644 --- a/Source/Core/Core/PowerPC/JitCommon/JitBase.h +++ b/Source/Core/Core/PowerPC/JitCommon/JitBase.h @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -202,6 +203,9 @@ public: using MemoryStats = std::pair>; virtual std::vector GetMemoryStats() const = 0; + virtual std::size_t DisassembleNearCode(const JitBlock& block, std::ostream& stream) const = 0; + virtual std::size_t DisassembleFarCode(const JitBlock& block, std::ostream& stream) const = 0; + virtual const CommonAsmRoutinesBase* GetAsmRoutines() = 0; virtual bool HandleFault(uintptr_t access_address, SContext* ctx) = 0; diff --git a/Source/Core/Core/PowerPC/JitCommon/JitCache.cpp b/Source/Core/Core/PowerPC/JitCommon/JitCache.cpp index 220c3783db..687d40417b 100644 --- a/Source/Core/Core/PowerPC/JitCommon/JitCache.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/JitCache.cpp @@ -8,13 +8,16 @@ #include #include #include +#include #include +#include #include #include "Common/CommonTypes.h" #include "Common/JitRegister.h" #include "Core/Config/MainSettings.h" #include "Core/Core.h" +#include "Core/Host.h" #include "Core/PowerPC/JitCommon/JitBase.h" #include "Core/PowerPC/MMU.h" #include "Core/PowerPC/PPCSymbolDB.h" @@ -124,6 +127,7 @@ void JitBaseBlockCache::WipeBlockProfilingData(const Core::CPUThreadGuard&) if (JitBlock::ProfileData* const profile_data = kv.second.profile_data.get()) *profile_data = {}; } + Host_JitProfileDataWiped(); } JitBlock* JitBaseBlockCache::AllocateBlock(u32 em_address) @@ -139,7 +143,8 @@ JitBlock* JitBaseBlockCache::AllocateBlock(u32 em_address) } void JitBaseBlockCache::FinalizeBlock(JitBlock& block, bool block_link, - const std::set& physical_addresses) + const PPCAnalyst::CodeBlock& code_block, + const PPCAnalyst::CodeBuffer& code_buffer) { size_t index = FastLookupIndexForAddress(block.effectiveAddress, block.feature_flags); if (m_entry_points_ptr) @@ -153,7 +158,18 @@ void JitBaseBlockCache::FinalizeBlock(JitBlock& block, bool block_link, } block.fast_block_map_index = index; - block.physical_addresses = physical_addresses; + block.physical_addresses = code_block.m_physical_addresses; + + block.originalSize = code_block.m_num_instructions; + if (m_jit.IsDebuggingEnabled()) + { + // TODO C++23: Can do this all in one statement with `std::vector::assign_range`. + const std::ranges::transform_view original_buffer_transform_view{ + std::span{code_buffer.data(), block.originalSize}, + [](const PPCAnalyst::CodeOp& op) { return std::make_pair(op.address, op.inst); }}; + block.original_buffer.assign(original_buffer_transform_view.begin(), + original_buffer_transform_view.end()); + } for (u32 addr : block.physical_addresses) { @@ -175,13 +191,14 @@ void JitBaseBlockCache::FinalizeBlock(JitBlock& block, bool block_link, if (Common::JitRegister::IsEnabled() && (symbol = m_jit.m_ppc_symbol_db.GetSymbolFromAddr(block.effectiveAddress)) != nullptr) { - Common::JitRegister::Register(block.normalEntry, block.codeSize, "JIT_PPC_{}_{:08x}", - symbol->function_name.c_str(), block.physicalAddress); + Common::JitRegister::Register(block.normalEntry, block.near_end - block.normalEntry, + "JIT_PPC_{}_{:08x}", symbol->function_name, + block.physicalAddress); } else { - Common::JitRegister::Register(block.normalEntry, block.codeSize, "JIT_PPC_{:08x}", - block.physicalAddress); + Common::JitRegister::Register(block.normalEntry, block.near_end - block.normalEntry, + "JIT_PPC_{:08x}", block.physicalAddress); } } diff --git a/Source/Core/Core/PowerPC/JitCommon/JitCache.h b/Source/Core/Core/PowerPC/JitCommon/JitCache.h index 9f4a127ffb..fb9b330097 100644 --- a/Source/Core/Core/PowerPC/JitCommon/JitCache.h +++ b/Source/Core/Core/PowerPC/JitCommon/JitCache.h @@ -19,6 +19,7 @@ #include "Common/CommonTypes.h" #include "Core/HW/Memmap.h" #include "Core/PowerPC/Gekko.h" +#include "Core/PowerPC/PPCAnalyst.h" class JitBase; @@ -105,6 +106,10 @@ struct JitBlock : public JitBlockData // This set stores all physical addresses of all occupied instructions. std::set physical_addresses; + // This is only available when debugging is enabled. It is a trimmed-down copy of the + // PPCAnalyst::CodeBuffer used to recompile this block, including repeat instructions. + std::vector> original_buffer; + std::unique_ptr profile_data; }; @@ -162,9 +167,11 @@ public: JitBlock** GetFastBlockMapFallback(); void RunOnBlocks(const Core::CPUThreadGuard& guard, std::function f) const; void WipeBlockProfilingData(const Core::CPUThreadGuard& guard); + std::size_t GetBlockCount() const { return block_map.size(); } JitBlock* AllocateBlock(u32 em_address); - void FinalizeBlock(JitBlock& block, bool block_link, const std::set& physical_addresses); + void FinalizeBlock(JitBlock& block, bool block_link, const PPCAnalyst::CodeBlock& code_block, + const PPCAnalyst::CodeBuffer& code_buffer); // Look for the block in the slow but accurate way. // This function shall be used if FastLookupIndexForAddress() failed. diff --git a/Source/Core/Core/PowerPC/JitInterface.cpp b/Source/Core/Core/PowerPC/JitInterface.cpp index f142fcee60..be0f21f2e9 100644 --- a/Source/Core/Core/PowerPC/JitInterface.cpp +++ b/Source/Core/Core/PowerPC/JitInterface.cpp @@ -188,46 +188,18 @@ void JitInterface::WipeBlockProfilingData(const Core::CPUThreadGuard& guard) m_jit->GetBlockCache()->WipeBlockProfilingData(guard); } -std::variant -JitInterface::GetHostCode(u32 address) const +void JitInterface::RunOnBlocks(const Core::CPUThreadGuard& guard, + std::function f) const { - if (!m_jit) - { - return GetHostCodeError::NoJitActive; - } + if (m_jit) + m_jit->GetBlockCache()->RunOnBlocks(guard, std::move(f)); +} - auto& ppc_state = m_system.GetPPCState(); - JitBlock* block = - m_jit->GetBlockCache()->GetBlockFromStartAddress(address, ppc_state.feature_flags); - if (!block) - { - for (int i = 0; i < 500; i++) - { - block = m_jit->GetBlockCache()->GetBlockFromStartAddress(address - 4 * i, - ppc_state.feature_flags); - if (block) - break; - } - - if (block) - { - if (!(block->effectiveAddress <= address && - block->originalSize + block->effectiveAddress >= address)) - block = nullptr; - } - - // Do not merge this "if" with the above - block changes inside it. - if (!block) - { - return GetHostCodeError::NoTranslation; - } - } - - GetHostCodeResult result; - result.code = block->normalEntry; - result.code_size = block->codeSize; - result.entry_address = block->effectiveAddress; - return result; +std::size_t JitInterface::GetBlockCount() const +{ + if (m_jit) + return m_jit->GetBlockCache()->GetBlockCount(); + return 0; } bool JitInterface::HandleFault(uintptr_t access_address, SContext* ctx) @@ -276,6 +248,20 @@ std::vector JitInterface::GetMemoryStats() const return {}; } +std::size_t JitInterface::DisassembleNearCode(const JitBlock& block, std::ostream& stream) const +{ + if (m_jit) + return m_jit->DisassembleNearCode(block, stream); + return 0; +} + +std::size_t JitInterface::DisassembleFarCode(const JitBlock& block, std::ostream& stream) const +{ + if (m_jit) + return m_jit->DisassembleFarCode(block, stream); + return 0; +} + void JitInterface::InvalidateICache(u32 address, u32 size, bool forced) { if (m_jit) diff --git a/Source/Core/Core/PowerPC/JitInterface.h b/Source/Core/Core/PowerPC/JitInterface.h index 09e3d49f8b..a382fb1ba4 100644 --- a/Source/Core/Core/PowerPC/JitInterface.h +++ b/Source/Core/Core/PowerPC/JitInterface.h @@ -6,11 +6,10 @@ #include #include #include +#include #include -#include #include #include -#include #include #include "Common/CommonTypes.h" @@ -46,23 +45,11 @@ public: CPUCoreBase* InitJitCore(PowerPC::CPUCore core); CPUCoreBase* GetCore() const; - // Debugging - enum class GetHostCodeError - { - NoJitActive, - NoTranslation, - }; - struct GetHostCodeResult - { - const u8* code; - u32 code_size; - u32 entry_address; - }; - void UpdateMembase(); void JitBlockLogDump(const Core::CPUThreadGuard& guard, std::FILE* file) const; void WipeBlockProfilingData(const Core::CPUThreadGuard& guard); - std::variant GetHostCode(u32 address) const; + void RunOnBlocks(const Core::CPUThreadGuard& guard, std::function f) const; + std::size_t GetBlockCount() const; // Memory Utilities bool HandleFault(uintptr_t access_address, SContext* ctx); @@ -85,6 +72,10 @@ public: using MemoryStats = std::pair>; std::vector GetMemoryStats() const; + // Disassemble the recompiled code from a JIT block. Returns the disassembled instruction count. + std::size_t DisassembleNearCode(const JitBlock& block, std::ostream& stream) const; + std::size_t DisassembleFarCode(const JitBlock& block, std::ostream& stream) const; + // If "forced" is true, a recompile is being requested on code that hasn't been modified. void InvalidateICache(u32 address, u32 size, bool forced); void InvalidateICacheLine(u32 address); diff --git a/Source/Core/DolphinNoGUI/MainNoGUI.cpp b/Source/Core/DolphinNoGUI/MainNoGUI.cpp index 40145e6e4c..f5aaae01ca 100644 --- a/Source/Core/DolphinNoGUI/MainNoGUI.cpp +++ b/Source/Core/DolphinNoGUI/MainNoGUI.cpp @@ -86,6 +86,14 @@ void Host_UpdateDisasmDialog() { } +void Host_JitCacheCleared() +{ +} + +void Host_JitProfileDataWiped() +{ +} + void Host_UpdateMainFrame() { s_update_main_frame_event.Set(); diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 79e5cce207..1daceff217 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -222,6 +222,8 @@ add_executable(dolphin-emu Debugger/CodeWidget.h Debugger/GekkoSyntaxHighlight.cpp Debugger/GekkoSyntaxHighlight.h + Debugger/JitBlockTableModel.cpp + Debugger/JitBlockTableModel.h Debugger/JITWidget.cpp Debugger/JITWidget.h Debugger/MemoryViewWidget.cpp @@ -297,6 +299,7 @@ add_executable(dolphin-emu QtUtils/BlockUserInputFilter.h QtUtils/ClearLayoutRecursively.cpp QtUtils/ClearLayoutRecursively.h + QtUtils/ClickableStatusBar.h QtUtils/DolphinFileDialog.cpp QtUtils/DolphinFileDialog.h QtUtils/DoubleClickEventFilter.cpp diff --git a/Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp b/Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp index 12cb8b30c5..792d78d9b0 100644 --- a/Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp +++ b/Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp @@ -881,7 +881,7 @@ void CodeViewWidget::OnPPCComparison() { const u32 addr = GetContextAddress(); - emit RequestPPCComparison(addr); + emit RequestPPCComparison(addr, m_system.GetPPCState().msr.IR); } void CodeViewWidget::OnAddFunction() diff --git a/Source/Core/DolphinQt/Debugger/CodeViewWidget.h b/Source/Core/DolphinQt/Debugger/CodeViewWidget.h index 4dec82b676..3fc78489ba 100644 --- a/Source/Core/DolphinQt/Debugger/CodeViewWidget.h +++ b/Source/Core/DolphinQt/Debugger/CodeViewWidget.h @@ -55,7 +55,7 @@ public: u32 AddressForRow(int row) const; signals: - void RequestPPCComparison(u32 addr); + void RequestPPCComparison(u32 address, bool translate_address); void ShowMemory(u32 address); void UpdateCodeWidget(); diff --git a/Source/Core/DolphinQt/Debugger/CodeWidget.cpp b/Source/Core/DolphinQt/Debugger/CodeWidget.cpp index 681f14286c..c36f8c31d7 100644 --- a/Source/Core/DolphinQt/Debugger/CodeWidget.cpp +++ b/Source/Core/DolphinQt/Debugger/CodeWidget.cpp @@ -210,6 +210,11 @@ void CodeWidget::OnBranchWatchDialog() m_branch_watch_dialog->activateWindow(); } +void CodeWidget::OnSetCodeAddress(u32 address) +{ + SetAddress(address, CodeViewWidget::SetAddressUpdate::WithDetailedUpdate); +} + void CodeWidget::OnPPCSymbolsChanged() { UpdateSymbols(); diff --git a/Source/Core/DolphinQt/Debugger/CodeWidget.h b/Source/Core/DolphinQt/Debugger/CodeWidget.h index e0eb0bb09f..bf51db9d59 100644 --- a/Source/Core/DolphinQt/Debugger/CodeWidget.h +++ b/Source/Core/DolphinQt/Debugger/CodeWidget.h @@ -43,6 +43,7 @@ public: void SetPC(); void OnBranchWatchDialog(); + void OnSetCodeAddress(u32 address); void ToggleBreakpoint(); void AddBreakpoint(); void SetAddress(u32 address, CodeViewWidget::SetAddressUpdate update); @@ -50,7 +51,7 @@ public: void Update(); void UpdateSymbols(); signals: - void RequestPPCComparison(u32 addr); + void RequestPPCComparison(u32 address, bool translate_address); void ShowMemory(u32 address); private: diff --git a/Source/Core/DolphinQt/Debugger/JITWidget.cpp b/Source/Core/DolphinQt/Debugger/JITWidget.cpp index ae9bf661b9..f83b3a6c9c 100644 --- a/Source/Core/DolphinQt/Debugger/JITWidget.cpp +++ b/Source/Core/DolphinQt/Debugger/JITWidget.cpp @@ -3,216 +3,494 @@ #include "DolphinQt/Debugger/JITWidget.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include #include +#include +#include #include -#include -#include +#include #include #include +#include +#include "Common/CommonFuncs.h" #include "Common/GekkoDisassembler.h" -#include "Common/HostDisassembler.h" #include "Core/Core.h" -#include "Core/PowerPC/PPCAnalyst.h" +#include "Core/PowerPC/JitCommon/JitCache.h" +#include "Core/PowerPC/JitInterface.h" +#include "Core/PowerPC/MMU.h" +#include "Core/PowerPC/PPCSymbolDB.h" #include "Core/System.h" +#include "DolphinQt/Debugger/JitBlockTableModel.h" #include "DolphinQt/Host.h" +#include "DolphinQt/QtUtils/ClickableStatusBar.h" +#include "DolphinQt/QtUtils/FromStdString.h" +#include "DolphinQt/QtUtils/ModalMessageBox.h" #include "DolphinQt/Settings.h" +#include "UICommon/UICommon.h" -JITWidget::JITWidget(QWidget* parent) : QDockWidget(parent) +class JitBlockProxyModel final : public QSortFilterProxyModel { - setWindowTitle(tr("JIT Blocks")); - setObjectName(QStringLiteral("jitwidget")); + friend JITWidget; - setHidden(!Settings::Instance().IsJITVisible() || !Settings::Instance().IsDebugModeEnabled()); +public: + explicit JitBlockProxyModel(QObject* parent = nullptr); + ~JitBlockProxyModel() override; - setAllowedAreas(Qt::AllDockWidgetAreas); + JitBlockProxyModel(const JitBlockProxyModel&) = delete; + JitBlockProxyModel(JitBlockProxyModel&&) = delete; + JitBlockProxyModel& operator=(const JitBlockProxyModel&) = delete; + JitBlockProxyModel& operator=(JitBlockProxyModel&&) = delete; - auto& settings = Settings::GetQSettings(); + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; + [[noreturn]] void setSourceModel(QAbstractItemModel* source_model) override; + void setSourceModel(JitBlockTableModel* source_model); + JitBlockTableModel* sourceModel() const; - CreateWidgets(); + const JitBlock& GetJitBlock(const QModelIndex& index); - restoreGeometry(settings.value(QStringLiteral("jitwidget/geometry")).toByteArray()); - // macOS: setHidden() needs to be evaluated before setFloating() for proper window presentation - // according to Settings - setFloating(settings.value(QStringLiteral("jitwidget/floating")).toBool()); + // Always connected slots (external signals) + void OnSymbolTextChanged(const QString& text); + template JitBlockProxyModel::*member> + void OnAddressTextChanged(const QString& text); - m_table_splitter->restoreState( - settings.value(QStringLiteral("jitwidget/tablesplitter")).toByteArray()); - m_asm_splitter->restoreState( - settings.value(QStringLiteral("jitwidget/asmsplitter")).toByteArray()); +private: + std::optional m_em_address_min, m_em_address_max, m_pm_address_covered; + QString m_symbol_name = {}; +}; - connect(&Settings::Instance(), &Settings::JITVisibilityChanged, this, - [this](bool visible) { setHidden(!visible); }); - - connect(&Settings::Instance(), &Settings::DebugModeToggled, this, - [this](bool enabled) { setHidden(!enabled || !Settings::Instance().IsJITVisible()); }); - - connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, &JITWidget::Update); - connect(Host::GetInstance(), &Host::UpdateDisasmDialog, this, &JITWidget::Update); - - ConnectWidgets(); - -#if defined(_M_X86_64) - m_disassembler = GetNewDisassembler("x86"); -#elif defined(_M_ARM_64) - m_disassembler = GetNewDisassembler("aarch64"); -#else - m_disassembler = GetNewDisassembler("UNK"); -#endif +const JitBlock& JitBlockProxyModel::GetJitBlock(const QModelIndex& index) +{ + return sourceModel()->GetJitBlock(mapToSource(index)); } -JITWidget::~JITWidget() +void JitBlockProxyModel::OnSymbolTextChanged(const QString& text) +{ + m_symbol_name = text; + invalidateRowsFilter(); +} + +template JitBlockProxyModel::*member> +void JitBlockProxyModel::OnAddressTextChanged(const QString& text) +{ + bool ok = false; + if (const u32 value = text.toUInt(&ok, 16); ok) + this->*member = value; + else + this->*member = std::nullopt; + invalidateRowsFilter(); +} + +bool JitBlockProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const +{ + if (source_parent.isValid()) [[unlikely]] + return false; + if (!m_symbol_name.isEmpty()) + { + if (const QVariant& symbol_name_v = *sourceModel()->GetSymbolList()[source_row]; + !symbol_name_v.isValid() || !static_cast(symbol_name_v.data()) + ->contains(m_symbol_name, Qt::CaseInsensitive)) + { + return false; + } + } + const JitBlock& block = sourceModel()->GetJitBlockRefs()[source_row]; + if (m_em_address_min.has_value()) + { + if (block.effectiveAddress < m_em_address_min.value()) + return false; + } + if (m_em_address_max.has_value()) + { + if (block.effectiveAddress > m_em_address_max.value()) + return false; + } + if (m_pm_address_covered.has_value()) + { + if (!block.physical_addresses.contains(m_pm_address_covered.value())) + return false; + } + return true; +} + +// Virtual setSourceModel is forbidden for type-safety reasons. +void JitBlockProxyModel::setSourceModel(QAbstractItemModel* source_model) +{ + Crash(); +} + +void JitBlockProxyModel::setSourceModel(JitBlockTableModel* source_model) +{ + QSortFilterProxyModel::setSourceModel(source_model); +} + +JitBlockTableModel* JitBlockProxyModel::sourceModel() const +{ + return static_cast(QSortFilterProxyModel::sourceModel()); +} + +JitBlockProxyModel::JitBlockProxyModel(QObject* parent) : QSortFilterProxyModel(parent) +{ +} + +JitBlockProxyModel::~JitBlockProxyModel() = default; + +void JITWidget::UpdateProfilingButton() +{ + const QSignalBlocker blocker(m_toggle_profiling_button); + const bool enabled = Config::Get(Config::MAIN_DEBUG_JIT_ENABLE_PROFILING); + m_toggle_profiling_button->setText(enabled ? tr("Stop Profiling") : tr("Start Profiling")); + m_toggle_profiling_button->setChecked(enabled); +} + +void JITWidget::UpdateOtherButtons(Core::State state) +{ + const bool jit_exists = m_system.GetJitInterface().GetCore() != nullptr; + m_clear_cache_button->setEnabled(jit_exists); + m_wipe_profiling_button->setEnabled(jit_exists); +} + +void JITWidget::UpdateDebugFont(const QFont& font) +{ + m_table_view->setFont(font); + m_ppc_asm_widget->setFont(font); + m_host_near_asm_widget->setFont(font); + m_host_far_asm_widget->setFont(font); +} + +void JITWidget::ClearDisassembly() +{ + m_ppc_asm_widget->clear(); + m_host_near_asm_widget->clear(); + m_host_far_asm_widget->clear(); + m_status_bar->clearMessage(); +} + +void JITWidget::ShowFreeMemoryStatus() +{ + const std::vector memory_stats = m_system.GetJitInterface().GetMemoryStats(); + QString message = tr("Free memory:"); + for (const auto& [name, stats] : memory_stats) + { + const auto& [free_size, fragmentation_ratio] = stats; + // i18n: Of each memory region, %1 is its remaining size displayed in an appropriate scale + // of bytes (e.g. MiB), %2 is its untranslated name, and %3 is its fragmentation percentage. + message.append(tr(" %1 %2 (%3% fragmented)") + .arg(QString::fromStdString(UICommon::FormatSize(free_size, 2))) + .arg(QtUtils::FromStdString(name)) + .arg(fragmentation_ratio * 100.0, 0, 'f', 2)); + } + m_status_bar->showMessage(message); +} + +void JITWidget::UpdateContent(Core::State state) +{ + ClearDisassembly(); + if (state == Core::State::Paused) + ShowFreeMemoryStatus(); +} + +static void DisassembleCodeBuffer(const JitBlock& block, PPCSymbolDB& ppc_symbol_db, + std::ostream& stream) +{ + // Instructions are 4 byte aligned, so next_address = 1 will never produce a false-negative. + for (u32 next_address = 1; const auto& [address, inst] : block.original_buffer) + { + if (address != next_address) + { + stream << ppc_symbol_db.GetDescription(address) << '\n'; + next_address = address; + } + fmt::print(stream, "0x{:08x}\t{}\n", address, + Common::GekkoDisassembler::Disassemble(inst.hex, address)); + next_address += sizeof(UGeckoInstruction); + } +} + +void JITWidget::CrossDisassemble(const JitBlock& block) +{ + // TODO C++20: std::ostringstream::view() + QtUtils::FromStdString + std::ostream::seekp(0) would + // save a lot of wasted allocation here, but compiler support for the first thing isn't here yet. + std::ostringstream stream; + DisassembleCodeBuffer(block, m_system.GetPPCSymbolDB(), stream); + m_ppc_asm_widget->setPlainText(QString::fromStdString(std::move(stream).str())); + + auto& jit_interface = m_system.GetJitInterface(); + + const auto host_near_instruction_count = jit_interface.DisassembleNearCode(block, stream); + m_host_near_asm_widget->setPlainText(QString::fromStdString(std::move(stream).str())); + + const auto host_far_instruction_count = jit_interface.DisassembleFarCode(block, stream); + m_host_far_asm_widget->setPlainText(QString::fromStdString(std::move(stream).str())); + + // i18n: "near" and "far" refer to the near code cache and far code cache of Dolphin's JITs. + // %1 and %2 are instruction counts from the near and far code caches, respectively. %3 is a + // percentage calculated from how inefficient (in other words, "blown-up") a given JIT block's + // recompilation was when considering the host instruction count vs the PPC instruction count. + m_status_bar->showMessage(tr("Host instruction count: %1 near %2 far (%3% blowup)") + .arg(host_near_instruction_count) + .arg(host_far_instruction_count) + .arg(static_cast(100 * (host_near_instruction_count + + host_far_instruction_count)) / + block.originalSize - + 100.0, + 0, 'f', 2)); +} + +void JITWidget::CrossDisassemble(const QModelIndex& index) +{ + if (index.isValid()) + { + CrossDisassemble(m_table_proxy->GetJitBlock(index)); + return; + } + UpdateContent(Core::GetState(m_system)); +} + +void JITWidget::CrossDisassemble() +{ + CrossDisassemble(m_table_view->currentIndex()); +} + +void JITWidget::TableEraseBlocks() +{ + auto* const selection_model = m_table_view->selectionModel(); + QModelIndexList index_list = selection_model->selectedRows(); + selection_model->clear(); // Side effect: currentChanged will be emitted (this is intended). + + std::ranges::transform(index_list, index_list.begin(), [this](const QModelIndex& index) { + return m_table_proxy->mapToSource(index); + }); + std::ranges::sort(index_list, std::less{}); // QModelIndex is incompatible with std::ranges::less + for (const QModelIndex& index : std::ranges::reverse_view{index_list}) + { + if (!index.isValid()) + continue; + m_table_model->removeRow(index.row()); + } +} + +void JITWidget::LoadQSettings() +{ + auto& settings = Settings::GetQSettings(); + + restoreGeometry(settings.value(QStringLiteral("jitwidget/geometry")).toByteArray()); + setHidden(!Settings::Instance().IsJITVisible() || !Settings::Instance().IsDebugModeEnabled()); + // macOS: setFloating() needs to be after setHidden() for proper window presentation + // according to Settings + setFloating(settings.value(QStringLiteral("jitwidget/floating")).toBool()); + m_table_view->horizontalHeader()->restoreState( + settings.value(QStringLiteral("jitwidget/tableheader/state")).toByteArray()); + m_table_splitter->restoreState( + settings.value(QStringLiteral("jitwidget/tablesplitter")).toByteArray()); + m_disasm_splitter->restoreState( + settings.value(QStringLiteral("jitwidget/disasmsplitter")).toByteArray()); +} + +void JITWidget::SaveQSettings() const { auto& settings = Settings::GetQSettings(); settings.setValue(QStringLiteral("jitwidget/geometry"), saveGeometry()); settings.setValue(QStringLiteral("jitwidget/floating"), isFloating()); + settings.setValue(QStringLiteral("jitwidget/tableheader/state"), + m_table_view->horizontalHeader()->saveState()); settings.setValue(QStringLiteral("jitwidget/tablesplitter"), m_table_splitter->saveState()); - settings.setValue(QStringLiteral("jitwidget/asmsplitter"), m_asm_splitter->saveState()); + settings.setValue(QStringLiteral("jitwidget/disasmsplitter"), m_disasm_splitter->saveState()); } -void JITWidget::CreateWidgets() +void JITWidget::ConnectSlots() { - m_table_widget = new QTableWidget; - - m_table_widget->setTabKeyNavigation(false); - m_table_widget->setColumnCount(7); - m_table_widget->setHorizontalHeaderLabels( - {tr("Address"), tr("PPC Size"), tr("Host Size"), - // i18n: The symbolic name of a code block - tr("Symbol"), - // i18n: These are the kinds of flags that a CPU uses (e.g. carry), - // not the kinds of flags that represent e.g. countries - tr("Flags"), - // i18n: The number of times a code block has been executed - tr("NumExec"), - // i18n: Performance cost, not monetary cost - tr("Cost")}); - - m_ppc_asm_widget = new QTextBrowser; - m_host_asm_widget = new QTextBrowser; - - m_table_splitter = new QSplitter(Qt::Vertical); - m_asm_splitter = new QSplitter(Qt::Horizontal); - - m_refresh_button = new QPushButton(tr("Refresh")); - - m_table_splitter->addWidget(m_table_widget); - m_table_splitter->addWidget(m_asm_splitter); - - m_asm_splitter->addWidget(m_ppc_asm_widget); - m_asm_splitter->addWidget(m_host_asm_widget); - - QWidget* widget = new QWidget; - auto* layout = new QVBoxLayout; - layout->setContentsMargins(2, 2, 2, 2); - widget->setLayout(layout); - - layout->addWidget(m_table_splitter); - layout->addWidget(m_refresh_button); - - setWidget(widget); + auto* const host = Host::GetInstance(); + connect(host, &Host::JitCacheCleared, this, &JITWidget::OnJitCacheCleared); + connect(host, &Host::UpdateDisasmDialog, this, &JITWidget::OnUpdateDisasmDialog); + connect(host, &Host::PPCSymbolsChanged, this, &JITWidget::OnPPCSymbolsUpdated); + connect(host, &Host::PPCBreakpointsChanged, this, &JITWidget::OnPPCBreakpointsChanged); + auto* const settings = &Settings::Instance(); + connect(settings, &Settings::ConfigChanged, this, &JITWidget::OnConfigChanged); + connect(settings, &Settings::DebugFontChanged, this, &JITWidget::OnDebugFontChanged); + connect(settings, &Settings::EmulationStateChanged, this, &JITWidget::OnEmulationStateChanged); } -void JITWidget::ConnectWidgets() +void JITWidget::DisconnectSlots() { - connect(m_refresh_button, &QPushButton::clicked, this, &JITWidget::Update); + auto* const host = Host::GetInstance(); + disconnect(host, &Host::JitCacheCleared, this, &JITWidget::OnJitCacheCleared); + disconnect(host, &Host::UpdateDisasmDialog, this, &JITWidget::OnUpdateDisasmDialog); + disconnect(host, &Host::PPCSymbolsChanged, this, &JITWidget::OnPPCSymbolsUpdated); + disconnect(host, &Host::PPCBreakpointsChanged, this, &JITWidget::OnPPCBreakpointsChanged); + auto* const settings = &Settings::Instance(); + disconnect(settings, &Settings::ConfigChanged, this, &JITWidget::OnConfigChanged); + disconnect(settings, &Settings::DebugFontChanged, this, &JITWidget::OnDebugFontChanged); + disconnect(settings, &Settings::EmulationStateChanged, this, &JITWidget::OnEmulationStateChanged); } -void JITWidget::Compare(u32 address) +void JITWidget::Show() { - m_address = address; + ConnectSlots(); + // Handle every slot that may have missed a signal while this widget was hidden. + // OnJitCacheCleared() can be skipped. + // OnUpdateDisasmDialog() can be skipped. + // OnPPCSymbolsUpdated() can be skipped. + // OnPPCBreakpointsChanged() can be skipped. + OnConfigChanged(); + OnDebugFontChanged(Settings::Instance().GetDebugFont()); + OnEmulationStateChanged(Core::GetState(m_system)); +} +void JITWidget::Hide() +{ + DisconnectSlots(); + ClearDisassembly(); +} + +void JITWidget::OnRequestPPCComparison(u32 address, bool translate_address) +{ Settings::Instance().SetJITVisible(true); raise(); - m_host_asm_widget->setFocus(); - Update(); + if (translate_address) + { + const std::optional pm_address = m_system.GetMMU().GetTranslatedAddress(address); + if (!pm_address.has_value()) + { + ModalMessageBox::warning( + this, tr("Error"), + tr("Effective address %1 has no physical address translation.").arg(address, 0, 16)); + return; + } + address = pm_address.value(); + } + m_pm_address_covered_line_edit->setText(QString::number(address, 16)); } -void JITWidget::Update() +void JITWidget::OnVisibilityToggled(bool visible) { - if (!isVisible()) - return; + setHidden(!visible); +} - if (!m_address || (Core::GetState(Core::System::GetInstance()) != Core::State::Paused)) +void JITWidget::OnDebugModeToggled(bool enabled) +{ + setHidden(!enabled || !Settings::Instance().IsJITVisible()); +} + +void JITWidget::OnToggleProfiling(bool enabled) +{ + Config::SetBaseOrCurrent(Config::MAIN_DEBUG_JIT_ENABLE_PROFILING, enabled); +} + +void JITWidget::OnClearCache() +{ + m_system.GetJitInterface().ClearCache(Core::CPUThreadGuard{m_system}); +} + +void JITWidget::OnWipeProfiling() +{ + m_system.GetJitInterface().WipeBlockProfilingData(Core::CPUThreadGuard{m_system}); +} + +void JITWidget::OnTableCurrentChanged(const QModelIndex& current, const QModelIndex& previous) +{ + CrossDisassemble(current); +} + +void JITWidget::OnTableDoubleClicked(const QModelIndex& index) +{ + emit SetCodeAddress(m_table_proxy->GetJitBlock(index).effectiveAddress); +} + +void JITWidget::OnTableContextMenu(const QPoint& pos) +{ + // There needs to be an option somewhere for a user to recover from hiding every column. + if (m_table_view->horizontalHeader()->hiddenSectionCount() == Column::NumberOfColumns) { - m_ppc_asm_widget->setHtml(QStringLiteral("%1").arg(tr("(ppc)"))); - m_host_asm_widget->setHtml(QStringLiteral("%1").arg(tr("(host)"))); + m_column_visibility_menu->exec(m_table_view->viewport()->mapToGlobal(pos)); return; } + m_table_context_menu->exec(m_table_view->viewport()->mapToGlobal(pos)); +} - // TODO: Actually do something with the table (Wx doesn't) +void JITWidget::OnTableHeaderContextMenu(const QPoint& pos) +{ + m_column_visibility_menu->exec(m_table_view->horizontalHeader()->mapToGlobal(pos)); +} - // Get host side code disassembly - auto host_instructions_disasm = DisassembleBlock(m_disassembler.get(), m_address); - m_address = host_instructions_disasm.entry_address; +void JITWidget::OnTableMenuViewCode() +{ + // TODO: CodeWidget doesn't support it yet, but eventually signal if the address should be + // translated with ((block.feature_flags & CPUEmuFeatureFlags::FEATURE_FLAG_MSR_IR) != 0). + if (const QModelIndex& index = m_table_view->currentIndex(); index.isValid()) + emit SetCodeAddress(m_table_proxy->GetJitBlock(index).effectiveAddress); +} - m_host_asm_widget->setHtml( - QStringLiteral("
%1
").arg(QString::fromStdString(host_instructions_disasm.text))); +void JITWidget::OnTableMenuEraseBlocks() +{ + TableEraseBlocks(); // Side effect: currentChanged will be emitted (this is intended). + // Because currentChanged has been emitted, OnTableCurrentChanged has already handled the rest. +} - // == Fill in ppc box - u32 ppc_addr = m_address; - PPCAnalyst::CodeBuffer code_buffer(32000); - PPCAnalyst::BlockStats st; - PPCAnalyst::BlockRegStats gpa; - PPCAnalyst::BlockRegStats fpa; - PPCAnalyst::CodeBlock code_block; - PPCAnalyst::PPCAnalyzer analyzer; - analyzer.SetDebuggingEnabled(Config::IsDebuggingEnabled()); - analyzer.SetBranchFollowingEnabled(Config::Get(Config::MAIN_JIT_FOLLOW_BRANCH)); - analyzer.SetFloatExceptionsEnabled(Config::Get(Config::MAIN_FLOAT_EXCEPTIONS)); - analyzer.SetDivByZeroExceptionsEnabled(Config::Get(Config::MAIN_DIVIDE_BY_ZERO_EXCEPTIONS)); - analyzer.SetOption(PPCAnalyst::PPCAnalyzer::OPTION_CONDITIONAL_CONTINUE); - analyzer.SetOption(PPCAnalyst::PPCAnalyzer::OPTION_BRANCH_FOLLOW); +void JITWidget::OnStatusBarPressed() +{ + if (Core::GetState(m_system) == Core::State::Paused) + ShowFreeMemoryStatus(); +} - code_block.m_stats = &st; - code_block.m_gpa = &gpa; - code_block.m_fpa = &fpa; +void JITWidget::OnJitCacheCleared() +{ + if (Core::GetState(m_system) != Core::State::Paused) + return; + ClearDisassembly(); + ShowFreeMemoryStatus(); +} - if (analyzer.Analyze(ppc_addr, &code_block, &code_buffer, code_buffer.size()) != 0xFFFFFFFF) - { - std::string ppc_disasm_str; - auto ppc_disasm = std::back_inserter(ppc_disasm_str); - for (u32 i = 0; i < code_block.m_num_instructions; i++) - { - const PPCAnalyst::CodeOp& op = code_buffer[i]; - const std::string opcode = Common::GekkoDisassembler::Disassemble(op.inst.hex, op.address); - fmt::format_to(ppc_disasm, "{:08x} {}\n", op.address, opcode); - } +void JITWidget::OnUpdateDisasmDialog() +{ + if (Core::GetState(m_system) != Core::State::Paused) + return; + CrossDisassemble(); +} - // Add stats to the end of the ppc box since it's generally the shortest. - fmt::format_to(ppc_disasm, "\n{} estimated cycles", st.numCycles); - fmt::format_to(ppc_disasm, "\nNum instr: PPC: {} Host: {}", code_block.m_num_instructions, - host_instructions_disasm.instruction_count); - if (code_block.m_num_instructions != 0 && host_instructions_disasm.instruction_count != 0) - { - fmt::format_to( - ppc_disasm, " (blowup: {}%)", - 100 * host_instructions_disasm.instruction_count / code_block.m_num_instructions - 100); - } +void JITWidget::OnPPCSymbolsUpdated() +{ + if (Core::GetState(m_system) != Core::State::Paused) + return; + CrossDisassemble(); +} - fmt::format_to(ppc_disasm, "\nNum bytes: PPC: {} Host: {}", code_block.m_num_instructions * 4, - host_instructions_disasm.code_size); - if (code_block.m_num_instructions != 0 && host_instructions_disasm.code_size != 0) - { - fmt::format_to( - ppc_disasm, " (blowup: {}%)", - 100 * host_instructions_disasm.code_size / (4 * code_block.m_num_instructions) - 100); - } +void JITWidget::OnPPCBreakpointsChanged() +{ + // Whatever row(s) might have been selected could no longer exist, because adding or removing + // breakpoints can invalidate JIT blocks. We must clear the selection to avoid stale indices. + auto* const selection_model = m_table_view->selectionModel(); + selection_model->clear(); // Side effect: currentChanged will be emitted (this is intended). + // Because currentChanged has been emitted, OnTableCurrentChanged has already handled the rest. +} - m_ppc_asm_widget->setHtml( - QStringLiteral("
%1
").arg(QString::fromStdString(ppc_disasm_str))); - } - else - { - m_host_asm_widget->setHtml( - QStringLiteral("
%1
") - .arg(QString::fromStdString(fmt::format("(non-code address: {:08x})", m_address)))); - m_ppc_asm_widget->setHtml(QStringLiteral("---")); - } +void JITWidget::OnConfigChanged() +{ + UpdateProfilingButton(); +} + +void JITWidget::OnDebugFontChanged(const QFont& font) +{ + UpdateDebugFont(font); +} + +void JITWidget::OnEmulationStateChanged(Core::State state) +{ + UpdateOtherButtons(state); + UpdateContent(state); } void JITWidget::closeEvent(QCloseEvent*) @@ -220,7 +498,186 @@ void JITWidget::closeEvent(QCloseEvent*) Settings::Instance().SetJITVisible(false); } -void JITWidget::showEvent(QShowEvent* event) +void JITWidget::showEvent(QShowEvent*) { - Update(); + emit ShowSignal(); + Show(); +} + +void JITWidget::hideEvent(QHideEvent*) +{ + emit HideSignal(); + Hide(); +} + +JITWidget::JITWidget(Core::System& system, QWidget* parent) : QDockWidget(parent), m_system(system) +{ + setWindowTitle(tr("JIT Blocks")); + setObjectName(QStringLiteral("jitwidget")); + setAllowedAreas(Qt::AllDockWidgetAreas); + + auto* const settings = &Settings::Instance(); + connect(settings, &Settings::JITVisibilityChanged, this, &JITWidget::OnVisibilityToggled); + connect(settings, &Settings::DebugModeToggled, this, &JITWidget::OnDebugModeToggled); + + m_table_view = new QTableView(nullptr); + m_table_proxy = new JitBlockProxyModel(m_table_view); + m_table_model = new JitBlockTableModel(m_system, m_system.GetJitInterface(), + m_system.GetPPCSymbolDB(), m_table_proxy); + + connect(this, &JITWidget::HideSignal, m_table_model, &JitBlockTableModel::OnHideSignal); + connect(this, &JITWidget::ShowSignal, m_table_model, &JitBlockTableModel::OnShowSignal); + + m_table_proxy->setSourceModel(m_table_model); + m_table_proxy->setSortRole(UserRole::SortRole); + m_table_proxy->setSortCaseSensitivity(Qt::CaseInsensitive); + + m_table_view->setModel(m_table_proxy); + m_table_view->setSortingEnabled(true); + m_table_view->sortByColumn(Column::EffectiveAddress, Qt::AscendingOrder); + m_table_view->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_table_view->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table_view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + m_table_view->setContextMenuPolicy(Qt::CustomContextMenu); + m_table_view->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_table_view->setCornerButtonEnabled(false); + m_table_view->verticalHeader()->hide(); + connect(m_table_view, &QTableView::doubleClicked, this, &JITWidget::OnTableDoubleClicked); + connect(m_table_view, &QTableView::customContextMenuRequested, this, + &JITWidget::OnTableContextMenu); + + auto* const horizontal_header = m_table_view->horizontalHeader(); + horizontal_header->setContextMenuPolicy(Qt::CustomContextMenu); + horizontal_header->setStretchLastSection(true); + horizontal_header->setSectionsMovable(true); + horizontal_header->setFirstSectionMovable(true); + connect(horizontal_header, &QHeaderView::sortIndicatorChanged, m_table_model, + &JitBlockTableModel::OnSortIndicatorChanged); + connect(horizontal_header, &QHeaderView::customContextMenuRequested, this, + &JITWidget::OnTableHeaderContextMenu); + + auto* const selection_model = m_table_view->selectionModel(); + connect(selection_model, &QItemSelectionModel::currentChanged, this, + &JITWidget::OnTableCurrentChanged); + + auto* const controls_layout = new QHBoxLayout(nullptr); + const auto address_filter_routine = [&](QLineEdit* line_edit, const QString& placeholder_text, + void (JitBlockProxyModel::*slot)(const QString&)) { + line_edit->setPlaceholderText(placeholder_text); + connect(line_edit, &QLineEdit::textChanged, m_table_proxy, slot); + controls_layout->addWidget(line_edit); + }; + address_filter_routine( + new QLineEdit(nullptr), tr("Min Effective Address"), + &JitBlockProxyModel::OnAddressTextChanged<&JitBlockProxyModel::m_em_address_min>); + address_filter_routine( + new QLineEdit(nullptr), tr("Max Effective Address"), + &JitBlockProxyModel::OnAddressTextChanged<&JitBlockProxyModel::m_em_address_max>); + address_filter_routine( + m_pm_address_covered_line_edit = new QLineEdit(nullptr), tr("Recompiles Physical Address"), + &JitBlockProxyModel::OnAddressTextChanged<&JitBlockProxyModel::m_pm_address_covered>); + + auto* const symbol_name_line_edit = new QLineEdit(nullptr); + symbol_name_line_edit->setPlaceholderText(tr("Symbol Name")); + connect(symbol_name_line_edit, &QLineEdit::textChanged, m_table_model, + &JitBlockTableModel::OnFilterSymbolTextChanged); + connect(symbol_name_line_edit, &QLineEdit::textChanged, m_table_proxy, + &JitBlockProxyModel::OnSymbolTextChanged); + controls_layout->addWidget(symbol_name_line_edit); + + m_toggle_profiling_button = new QPushButton(nullptr); + m_toggle_profiling_button->setToolTip( + tr("Toggle software JIT block profiling (will clear the JIT cache).")); + m_toggle_profiling_button->setCheckable(true); + connect(m_toggle_profiling_button, &QPushButton::toggled, this, &JITWidget::OnToggleProfiling); + controls_layout->addWidget(m_toggle_profiling_button); + + m_clear_cache_button = new QPushButton(tr("Clear Cache"), nullptr); + connect(m_clear_cache_button, &QPushButton::clicked, this, &JITWidget::OnClearCache); + controls_layout->addWidget(m_clear_cache_button); + + m_wipe_profiling_button = new QPushButton(tr("Wipe Profiling"), nullptr); + m_wipe_profiling_button->setToolTip(tr("Re-initialize software JIT block profiling data.")); + connect(m_wipe_profiling_button, &QPushButton::clicked, this, &JITWidget::OnWipeProfiling); + controls_layout->addWidget(m_wipe_profiling_button); + + m_disasm_splitter = new QSplitter(Qt::Horizontal, nullptr); + const auto text_box_routine = [&](QPlainTextEdit* text_edit, const QString& placeholder_text) { + text_edit->setWordWrapMode(QTextOption::NoWrap); + text_edit->setPlaceholderText(placeholder_text); + text_edit->setReadOnly(true); + m_disasm_splitter->addWidget(text_edit); + }; + text_box_routine(m_ppc_asm_widget = new QPlainTextEdit(nullptr), tr("PPC Instruction Coverage")); + text_box_routine(m_host_near_asm_widget = new QPlainTextEdit(nullptr), + tr("Host Near Code Cache")); + text_box_routine(m_host_far_asm_widget = new QPlainTextEdit(nullptr), tr("Host Far Code Cache")); + + m_table_splitter = new QSplitter(Qt::Vertical, nullptr); + m_table_splitter->addWidget(m_table_view); + m_table_splitter->addWidget(m_disasm_splitter); + + m_status_bar = new ClickableStatusBar(nullptr); + m_status_bar->setSizeGripEnabled(false); + connect(m_status_bar, &ClickableStatusBar::pressed, this, &JITWidget::OnStatusBarPressed); + + m_table_context_menu = new QMenu(this); + m_table_context_menu->addAction(tr("View &Code"), this, &JITWidget::OnTableMenuViewCode); + m_table_context_menu->addAction(tr("&Erase Block(s)"), this, &JITWidget::OnTableMenuEraseBlocks); + + LoadQSettings(); + + m_column_visibility_menu = new QMenu(this); + // These table header display names have abbreviated counterparts in JitBlockTableModel.cpp + static constexpr std::array headers = { + QT_TR_NOOP("PPC Feature Flags"), + // i18n: "Effective" means this memory address might be translated within the MMU. + QT_TR_NOOP("Effective Address"), + QT_TR_NOOP("Code Buffer Size"), + // i18n: This means to say it is a count of PPC instructions recompiled more than once. + QT_TR_NOOP("Repeat Instructions"), + // i18n: "Near Code" refers to the near code cache of Dolphin's JITs. + QT_TR_NOOP("Host Near Code Size"), + // i18n: "Far Code" refers to the far code cache of Dolphin's JITs. + QT_TR_NOOP("Host Far Code Size"), + QT_TR_NOOP("Run Count"), + // i18n: "Cycles" means instruction cycles. + QT_TR_NOOP("Cycles Spent"), + // i18n: "Cycles" means instruction cycles. + QT_TR_NOOP("Cycles Average"), + // i18n: "Cycles" means instruction cycles. + QT_TR_NOOP("Cycles Percent"), + // i18n: "ns" is an abbreviation of nanoseconds. + QT_TR_NOOP("Time Spent (ns)"), + // i18n: "ns" is an abbreviation of nanoseconds. + QT_TR_NOOP("Time Average (ns)"), + QT_TR_NOOP("Time Percent"), + // i18n: "Symbol" means debugging symbol (its name in particular). + QT_TR_NOOP("Symbol"), + }; + for (int column = 0; column < Column::NumberOfColumns; ++column) + { + auto* const action = + m_column_visibility_menu->addAction(tr(headers[column]), [this, column](bool enabled) { + m_table_view->setColumnHidden(column, !enabled); + }); + action->setChecked(!m_table_view->isColumnHidden(column)); + action->setCheckable(true); + } + + auto* const main_layout = new QVBoxLayout(nullptr); + main_layout->setContentsMargins(2, 2, 2, 2); + main_layout->setSpacing(0); + main_layout->addLayout(controls_layout); + main_layout->addWidget(m_table_splitter); + main_layout->addWidget(m_status_bar); + + auto* const main_widget = new QWidget(nullptr); + main_widget->setLayout(main_layout); + setWidget(main_widget); +} + +JITWidget::~JITWidget() +{ + SaveQSettings(); } diff --git a/Source/Core/DolphinQt/Debugger/JITWidget.h b/Source/Core/DolphinQt/Debugger/JITWidget.h index 3b2f80701d..d192a8ee0a 100644 --- a/Source/Core/DolphinQt/Debugger/JITWidget.h +++ b/Source/Core/DolphinQt/Debugger/JITWidget.h @@ -4,42 +4,128 @@ #pragma once #include -#include #include "Common/CommonTypes.h" +class BreakpointWidget; +class ClickableStatusBar; +class CodeWidget; +namespace Core +{ +enum class State; +class System; +} // namespace Core +struct JitBlock; +class JitBlockProxyModel; +class JitBlockTableModel; +namespace JitBlockTableModelColumn +{ +enum EnumType : int; +} +namespace JitBlockTableModelUserRole +{ +enum EnumType : int; +} +class MemoryWidget; class QCloseEvent; +class QFont; +class QLineEdit; +class QMenu; +class QPlainTextEdit; +class QPushButton; class QShowEvent; class QSplitter; -class QTextBrowser; -class QTableWidget; -class QPushButton; -class HostDisassembler; +class QTableView; -class JITWidget : public QDockWidget +class JITWidget final : public QDockWidget { Q_OBJECT -public: - explicit JITWidget(QWidget* parent = nullptr); - ~JITWidget(); - void Compare(u32 address); + using Column = JitBlockTableModelColumn::EnumType; + using UserRole = JitBlockTableModelUserRole::EnumType; + +signals: + void HideSignal(); + void ShowSignal(); + void SetCodeAddress(u32 address); + +public: + explicit JITWidget(Core::System& system, QWidget* parent = nullptr); + ~JITWidget() override; + + JITWidget(const JITWidget&) = delete; + JITWidget(JITWidget&&) = delete; + JITWidget& operator=(const JITWidget&) = delete; + JITWidget& operator=(JITWidget&&) = delete; + + // Always connected slots (external signals) + void OnRequestPPCComparison(u32 address, bool translate_address); private: - void Update(); - void CreateWidgets(); - void ConnectWidgets(); - - void closeEvent(QCloseEvent*) override; + void closeEvent(QCloseEvent* event) override; void showEvent(QShowEvent* event) override; + void hideEvent(QHideEvent* event) override; - QTableWidget* m_table_widget; - QTextBrowser* m_ppc_asm_widget; - QTextBrowser* m_host_asm_widget; + void UpdateProfilingButton(); + void UpdateOtherButtons(Core::State state); + void UpdateDebugFont(const QFont& font); + void ClearDisassembly(); + void ShowFreeMemoryStatus(); + void UpdateContent(Core::State state); + void CrossDisassemble(const JitBlock& block); + void CrossDisassemble(const QModelIndex& index); + void CrossDisassemble(); + void TableEraseBlocks(); + + // Setup and teardown + void LoadQSettings(); + void SaveQSettings() const; + void ConnectSlots(); + void DisconnectSlots(); + void Show(); + void Hide(); + + // Always connected slots (external signals) + void OnVisibilityToggled(bool visible); + void OnDebugModeToggled(bool visible); + + // Always connected slots (internal signals) + void OnToggleProfiling(bool enabled); + void OnClearCache(); + void OnWipeProfiling(); + void OnTableCurrentChanged(const QModelIndex& current, const QModelIndex& previous); + void OnTableDoubleClicked(const QModelIndex& index); + void OnTableContextMenu(const QPoint& pos); + void OnTableHeaderContextMenu(const QPoint& pos); + void OnTableMenuViewCode(); + void OnTableMenuEraseBlocks(); + void OnStatusBarPressed(); + + // Conditionally connected slots (external signals) + void OnJitCacheCleared(); + void OnUpdateDisasmDialog(); + void OnPPCSymbolsUpdated(); + void OnPPCBreakpointsChanged(); + void OnConfigChanged(); + void OnDebugFontChanged(const QFont& font); + void OnEmulationStateChanged(Core::State state); + + Core::System& m_system; + + QLineEdit* m_pm_address_covered_line_edit; + QPushButton* m_clear_cache_button; + QPushButton* m_toggle_profiling_button; + QPushButton* m_wipe_profiling_button; + QTableView* m_table_view; + JitBlockProxyModel* m_table_proxy; + JitBlockTableModel* m_table_model; + QPlainTextEdit* m_ppc_asm_widget; + QPlainTextEdit* m_host_near_asm_widget; + QPlainTextEdit* m_host_far_asm_widget; QSplitter* m_table_splitter; - QSplitter* m_asm_splitter; - QPushButton* m_refresh_button; + QSplitter* m_disasm_splitter; + ClickableStatusBar* m_status_bar; - std::unique_ptr m_disassembler; - u32 m_address = 0; + QMenu* m_table_context_menu; + QMenu* m_column_visibility_menu; }; diff --git a/Source/Core/DolphinQt/Debugger/JitBlockTableModel.cpp b/Source/Core/DolphinQt/Debugger/JitBlockTableModel.cpp new file mode 100644 index 0000000000..27bece81cd --- /dev/null +++ b/Source/Core/DolphinQt/Debugger/JitBlockTableModel.cpp @@ -0,0 +1,452 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/Debugger/JitBlockTableModel.h" + +#include +#include + +#include "Common/Assert.h" +#include "Common/Unreachable.h" +#include "Core/Core.h" +#include "Core/PowerPC/JitInterface.h" +#include "Core/PowerPC/PPCSymbolDB.h" +#include "DolphinQt/Host.h" +#include "DolphinQt/Settings.h" + +const JitBlock& JitBlockTableModel::GetJitBlock(const QModelIndex& index) const +{ + ASSERT(index.isValid()); + return m_jit_blocks[index.row()]; +} + +void JitBlockTableModel::SumOverallCosts() +{ + m_overall_cycles_spent = 0; + m_overall_time_spent = {}; + for (const JitBlock& block : m_jit_blocks) + { + if (block.profile_data == nullptr) + continue; + m_overall_cycles_spent += block.profile_data->cycles_spent; + m_overall_time_spent += block.profile_data->time_spent; + }; +} + +static QVariant GetSymbolNameQVariant(const Common::Symbol* symbol) +{ + return symbol ? QString::fromStdString(symbol->name) : QVariant{}; +} + +void JitBlockTableModel::PrefetchSymbols() +{ + m_symbol_list.clear(); + m_symbol_list.reserve(m_jit_blocks.size()); + // If the table viewing this model will be accessing every element, + // it would be a waste of effort to lazy-initialize the symbol list. + if (m_sorting_by_symbols || m_filtering_by_symbols) + { + for (const JitBlock& block : m_jit_blocks) + { + m_symbol_list.emplace_back( + GetSymbolNameQVariant(m_ppc_symbol_db.GetSymbolFromAddr(block.effectiveAddress))); + } + } + else + { + for (const JitBlock& block : m_jit_blocks) + { + m_symbol_list.emplace_back([this, &block]() { + return GetSymbolNameQVariant(m_ppc_symbol_db.GetSymbolFromAddr(block.effectiveAddress)); + }); + } + } +} + +void JitBlockTableModel::Clear() +{ + emit layoutAboutToBeChanged(); + m_jit_blocks.clear(); + m_symbol_list.clear(); + emit layoutChanged(); +} + +void JitBlockTableModel::Update(Core::State state) +{ + emit layoutAboutToBeChanged(); + m_jit_blocks.clear(); + if (state == Core::State::Paused) + { + m_jit_blocks.reserve(m_jit_interface.GetBlockCount()); + m_jit_interface.RunOnBlocks(Core::CPUThreadGuard{m_system}, [this](const JitBlock& block) { + m_jit_blocks.emplace_back(block); + }); + SumOverallCosts(); + } + PrefetchSymbols(); + emit layoutChanged(); +} + +void JitBlockTableModel::UpdateProfileData() +{ + const int row_count = rowCount(); + if (row_count <= 0) + return; + SumOverallCosts(); + static const QList roles = {Qt::DisplayRole}; + const int last = row_count - 1; + emit dataChanged(createIndex(0, Column::RunCount), createIndex(last, Column::TimePercent), roles); +} + +void JitBlockTableModel::UpdateSymbols() +{ + const int row_count = rowCount(); + if (row_count <= 0) + return; + PrefetchSymbols(); + static const QList roles = {Qt::DisplayRole}; + const int last = row_count - 1; + emit dataChanged(createIndex(0, Column::Symbol), createIndex(last, Column::Symbol), roles); +} + +void JitBlockTableModel::ConnectSlots() +{ + auto* const host = Host::GetInstance(); + connect(host, &Host::JitCacheCleared, this, &JitBlockTableModel::OnJitCacheCleared); + connect(host, &Host::JitProfileDataWiped, this, &JitBlockTableModel::OnJitProfileDataWiped); + connect(host, &Host::UpdateDisasmDialog, this, &JitBlockTableModel::OnUpdateDisasmDialog); + connect(host, &Host::PPCSymbolsChanged, this, &JitBlockTableModel::OnPPCSymbolsUpdated); + connect(host, &Host::PPCBreakpointsChanged, this, &JitBlockTableModel::OnPPCBreakpointsChanged); + auto* const settings = &Settings::Instance(); + connect(settings, &Settings::EmulationStateChanged, this, + &JitBlockTableModel::OnEmulationStateChanged); +} + +void JitBlockTableModel::DisconnectSlots() +{ + auto* const host = Host::GetInstance(); + disconnect(host, &Host::JitCacheCleared, this, &JitBlockTableModel::OnJitCacheCleared); + disconnect(host, &Host::JitProfileDataWiped, this, &JitBlockTableModel::OnJitProfileDataWiped); + disconnect(host, &Host::UpdateDisasmDialog, this, &JitBlockTableModel::OnUpdateDisasmDialog); + disconnect(host, &Host::PPCSymbolsChanged, this, &JitBlockTableModel::OnPPCSymbolsUpdated); + disconnect(host, &Host::PPCBreakpointsChanged, this, + &JitBlockTableModel::OnPPCBreakpointsChanged); + auto* const settings = &Settings::Instance(); + disconnect(settings, &Settings::EmulationStateChanged, this, + &JitBlockTableModel::OnEmulationStateChanged); +} + +void JitBlockTableModel::Show() +{ + ConnectSlots(); + // Every slot that may have missed a signal while this model was hidden can be handled by: + Update(Core::GetState(m_system)); +} + +void JitBlockTableModel::Hide() +{ + DisconnectSlots(); + Clear(); +} + +void JitBlockTableModel::OnShowSignal() +{ + Show(); +} + +void JitBlockTableModel::OnHideSignal() +{ + Hide(); +} + +void JitBlockTableModel::OnSortIndicatorChanged(int logicalIndex, Qt::SortOrder) +{ + m_sorting_by_symbols = logicalIndex == Column::Symbol; +} + +void JitBlockTableModel::OnFilterSymbolTextChanged(const QString& string) +{ + m_filtering_by_symbols = !string.isEmpty(); +} + +void JitBlockTableModel::OnJitCacheCleared() +{ + Update(Core::GetState(m_system)); +} + +void JitBlockTableModel::OnJitProfileDataWiped() +{ + UpdateProfileData(); +} + +void JitBlockTableModel::OnUpdateDisasmDialog() +{ + // This should hopefully catch all the little things that lead to stale JitBlock references. + Update(Core::GetState(m_system)); +} + +void JitBlockTableModel::OnPPCSymbolsUpdated() +{ + UpdateSymbols(); +} + +void JitBlockTableModel::OnPPCBreakpointsChanged() +{ + Update(Core::GetState(m_system)); +} + +void JitBlockTableModel::OnEmulationStateChanged(Core::State state) +{ + Update(state); +} + +static QString GetQStringDescription(const CPUEmuFeatureFlags flags) +{ + static const std::array descriptions = { + QStringLiteral(""), QStringLiteral("DR"), + QStringLiteral("IR"), QStringLiteral("DR|IR"), + QStringLiteral("PERFMON"), QStringLiteral("DR|PERFMON"), + QStringLiteral("IR|PERFMON"), QStringLiteral("DR|IR|PERFMON"), + }; + return descriptions[flags]; +} + +static QVariant GetValidSymbolStringVariant(const QVariant& symbol_name_v) +{ + if (symbol_name_v.isValid()) + return symbol_name_v; + return QStringLiteral(" --- "); +} + +QVariant JitBlockTableModel::DisplayRoleData(const QModelIndex& index) const +{ + const int column = index.column(); + if (column == Column::Symbol) + return GetValidSymbolStringVariant(*m_symbol_list[index.row()]); + + const JitBlock& jit_block = m_jit_blocks[index.row()]; + switch (column) + { + case Column::PPCFeatureFlags: + return GetQStringDescription(jit_block.feature_flags); + case Column::EffectiveAddress: + return QString::number(jit_block.effectiveAddress, 16); + case Column::CodeBufferSize: + return QString::number(jit_block.originalSize * sizeof(UGeckoInstruction)); + case Column::RepeatInstructions: + return QString::number(jit_block.originalSize - jit_block.physical_addresses.size()); + case Column::HostNearCodeSize: + return QString::number(jit_block.near_end - jit_block.near_begin); + case Column::HostFarCodeSize: + return QString::number(jit_block.far_end - jit_block.far_begin); + } + const JitBlock::ProfileData* const profile_data = jit_block.profile_data.get(); + if (profile_data == nullptr) + return QStringLiteral(" --- "); + switch (column) + { + case Column::RunCount: + return QString::number(profile_data->run_count); + case Column::CyclesSpent: + return QString::number(profile_data->cycles_spent); + case Column::CyclesAverage: + if (profile_data->run_count == 0) + return QStringLiteral(" --- "); + return QString::number( + static_cast(profile_data->cycles_spent) / profile_data->run_count, 'f', 6); + case Column::CyclesPercent: + if (m_overall_cycles_spent == 0) + return QStringLiteral(" --- "); + return QStringLiteral("%1%").arg(100.0 * profile_data->cycles_spent / m_overall_cycles_spent, + 10, 'f', 6); + case Column::TimeSpent: + { + const auto cast_time = + std::chrono::duration_cast(profile_data->time_spent); + return QString::number(cast_time.count()); + } + case Column::TimeAverage: + { + if (profile_data->run_count == 0) + return QStringLiteral(" --- "); + const auto cast_time = std::chrono::duration_cast>( + profile_data->time_spent); + return QString::number(cast_time.count() / profile_data->run_count, 'f', 6); + } + case Column::TimePercent: + { + if (m_overall_time_spent == JitBlock::ProfileData::Clock::duration{}) + return QStringLiteral(" --- "); + return QStringLiteral("%1%").arg( + 100.0 * profile_data->time_spent.count() / m_overall_time_spent.count(), 10, 'f', 6); + } + } + static_assert(Column::NumberOfColumns == 14); + Common::Unreachable(); +} + +QVariant JitBlockTableModel::TextAlignmentRoleData(const QModelIndex& index) const +{ + switch (index.column()) + { + case Column::PPCFeatureFlags: + case Column::EffectiveAddress: + return Qt::AlignCenter; + case Column::CodeBufferSize: + case Column::RepeatInstructions: + case Column::HostNearCodeSize: + case Column::HostFarCodeSize: + case Column::RunCount: + case Column::CyclesSpent: + case Column::CyclesAverage: + case Column::CyclesPercent: + case Column::TimeSpent: + case Column::TimeAverage: + case Column::TimePercent: + return QVariant::fromValue(Qt::AlignRight | Qt::AlignVCenter); + case Column::Symbol: + return QVariant::fromValue(Qt::AlignLeft | Qt::AlignVCenter); + } + static_assert(Column::NumberOfColumns == 14); + Common::Unreachable(); +} + +QVariant JitBlockTableModel::SortRoleData(const QModelIndex& index) const +{ + const int column = index.column(); + if (column == Column::Symbol) + return *m_symbol_list[index.row()]; + + const JitBlock& jit_block = m_jit_blocks[index.row()]; + switch (column) + { + case Column::PPCFeatureFlags: + return jit_block.feature_flags; + case Column::EffectiveAddress: + return jit_block.effectiveAddress; + case Column::CodeBufferSize: + return static_cast(jit_block.originalSize); + case Column::RepeatInstructions: + return static_cast(jit_block.originalSize - jit_block.physical_addresses.size()); + case Column::HostNearCodeSize: + return static_cast(jit_block.near_end - jit_block.near_begin); + case Column::HostFarCodeSize: + return static_cast(jit_block.far_end - jit_block.far_begin); + } + const JitBlock::ProfileData* const profile_data = jit_block.profile_data.get(); + if (profile_data == nullptr) + return QVariant(); + switch (column) + { + case Column::RunCount: + return static_cast(profile_data->run_count); + case Column::CyclesSpent: + case Column::CyclesPercent: + return static_cast(profile_data->cycles_spent); + case Column::CyclesAverage: + if (profile_data->run_count == 0) + return QVariant(); + return static_cast(profile_data->cycles_spent) / profile_data->run_count; + case Column::TimeSpent: + case Column::TimePercent: + return static_cast(profile_data->time_spent.count()); + case Column::TimeAverage: + if (profile_data->run_count == 0) + return QVariant(); + return static_cast(profile_data->time_spent.count()) / profile_data->run_count; + } + static_assert(Column::NumberOfColumns == 14); + Common::Unreachable(); +} + +QVariant JitBlockTableModel::data(const QModelIndex& index, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + return DisplayRoleData(index); + case Qt::TextAlignmentRole: + return TextAlignmentRoleData(index); + case UserRole::SortRole: + return SortRoleData(index); + } + return QVariant(); +} + +QVariant JitBlockTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation != Qt::Horizontal || role != Qt::DisplayRole) + return QVariant(); + + // These abbreviated table header display names have unabbreviated counterparts in JITWidget.cpp. + static constexpr std::array headers = { + // i18n: PPC Feature Flags + QT_TR_NOOP("PPC Feat. Flags"), + // i18n: Effective Address + QT_TR_NOOP("Eff. Address"), + // i18n: Code Buffer Size + QT_TR_NOOP("Code Buff. Size"), + // i18n: Repeat Instructions + QT_TR_NOOP("Repeat Instr."), + // i18n: Host Near Code Size + QT_TR_NOOP("Host N. Size"), + // i18n: Host Far Code Size + QT_TR_NOOP("Host F. Size"), + QT_TR_NOOP("Run Count"), + QT_TR_NOOP("Cycles Spent"), + // i18n: Cycles Average + QT_TR_NOOP("Cycles Avg."), + // i18n: Cycles Percent + QT_TR_NOOP("Cycles %"), + QT_TR_NOOP("Time Spent (ns)"), + // i18n: Time Average (ns) + QT_TR_NOOP("Time Avg. (ns)"), + // i18n: Time Percent + QT_TR_NOOP("Time %"), + QT_TR_NOOP("Symbol"), + }; + + return tr(headers[section]); +} + +int JitBlockTableModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) [[unlikely]] + return 0; + return m_jit_blocks.size(); +} + +int JitBlockTableModel::columnCount(const QModelIndex& parent) const +{ + if (parent.isValid()) [[unlikely]] + return 0; + return Column::NumberOfColumns; +} + +bool JitBlockTableModel::removeRows(int row, int count, const QModelIndex& parent) +{ + if (parent.isValid() || row < 0) [[unlikely]] + return false; + if (count <= 0) [[unlikely]] + return true; + + beginRemoveRows(parent, row, row + count - 1); // Last is inclusive in Qt! + for (const JitBlock& block : + std::span{m_jit_blocks.data() + row, static_cast(count)}) + { + m_jit_interface.EraseSingleBlock(block); + } + m_jit_blocks.remove(row, count); + m_symbol_list.remove(row, count); + endRemoveRows(); + return true; +} + +JitBlockTableModel::JitBlockTableModel(Core::System& system, JitInterface& jit_interface, + PPCSymbolDB& ppc_symbol_db, QObject* parent) + : QAbstractTableModel(parent), m_system(system), m_jit_interface(jit_interface), + m_ppc_symbol_db(ppc_symbol_db) +{ +} + +JitBlockTableModel::~JitBlockTableModel() = default; diff --git a/Source/Core/DolphinQt/Debugger/JitBlockTableModel.h b/Source/Core/DolphinQt/Debugger/JitBlockTableModel.h new file mode 100644 index 0000000000..eeaf2273d5 --- /dev/null +++ b/Source/Core/DolphinQt/Debugger/JitBlockTableModel.h @@ -0,0 +1,126 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/Lazy.h" +#include "Core/PowerPC/JitCommon/JitCache.h" + +namespace Core +{ +enum class State; +class System; +} // namespace Core +class JitInterface; +class PPCSymbolDB; +class QString; + +namespace JitBlockTableModelColumn +{ +enum EnumType : int +{ + PPCFeatureFlags = 0, + EffectiveAddress, + CodeBufferSize, + RepeatInstructions, + HostNearCodeSize, + HostFarCodeSize, + RunCount, + CyclesSpent, + CyclesAverage, + CyclesPercent, + TimeSpent, + TimeAverage, + TimePercent, + Symbol, + NumberOfColumns, +}; +} + +namespace JitBlockTableModelUserRole +{ +enum EnumType : int +{ + SortRole = Qt::UserRole, +}; +} + +class JitBlockTableModel final : public QAbstractTableModel +{ + Q_OBJECT + + using Column = JitBlockTableModelColumn::EnumType; + using UserRole = JitBlockTableModelUserRole::EnumType; + using JitBlockRefs = QList>; + using SymbolListValueType = Common::Lazy; + using SymbolList = QList; + +public: + explicit JitBlockTableModel(Core::System& system, JitInterface& jit_interface, + PPCSymbolDB& ppc_symbol_db, QObject* parent = nullptr); + ~JitBlockTableModel() override; + + JitBlockTableModel(const JitBlockTableModel&) = delete; + JitBlockTableModel(JitBlockTableModel&&) = delete; + JitBlockTableModel& operator=(const JitBlockTableModel&) = delete; + JitBlockTableModel& operator=(JitBlockTableModel&&) = delete; + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex& parent = QModelIndex{}) const override; + int columnCount(const QModelIndex& parent = QModelIndex{}) const override; + bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex{}) override; + + const JitBlock& GetJitBlock(const QModelIndex& index) const; + const JitBlockRefs& GetJitBlockRefs() const { return m_jit_blocks; } + const SymbolList& GetSymbolList() const { return m_symbol_list; } + + // Always connected slots (external signals) + void OnShowSignal(); + void OnHideSignal(); + void OnSortIndicatorChanged(int logicalIndex, Qt::SortOrder order); + void OnFilterSymbolTextChanged(const QString& string); + +private: + [[nodiscard]] QVariant DisplayRoleData(const QModelIndex& index) const; + [[nodiscard]] QVariant TextAlignmentRoleData(const QModelIndex& index) const; + [[nodiscard]] QVariant SortRoleData(const QModelIndex& index) const; + + void SumOverallCosts(); + void PrefetchSymbols(); + void Clear(); + void Update(Core::State state); + void UpdateProfileData(); + void UpdateSymbols(); + + // Setup and teardown + void ConnectSlots(); + void DisconnectSlots(); + void Show(); + void Hide(); + + // Conditionally connected slots (external signals) + void OnJitCacheCleared(); + void OnJitProfileDataWiped(); + void OnUpdateDisasmDialog(); + void OnPPCSymbolsUpdated(); + void OnPPCBreakpointsChanged(); + void OnEmulationStateChanged(Core::State state); + + Core::System& m_system; + JitInterface& m_jit_interface; + PPCSymbolDB& m_ppc_symbol_db; + + JitBlockRefs m_jit_blocks; + SymbolList m_symbol_list; + u64 m_overall_cycles_spent; + JitBlock::ProfileData::Clock::duration m_overall_time_spent; + bool m_sorting_by_symbols = false; + bool m_filtering_by_symbols = false; +}; diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 7ee8cac6a5..f275175de6 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -145,6 +145,7 @@ + @@ -249,6 +250,7 @@ + @@ -359,6 +361,7 @@ + diff --git a/Source/Core/DolphinQt/Host.cpp b/Source/Core/DolphinQt/Host.cpp index 80a4247a2b..3dc6f2a12b 100644 --- a/Source/Core/DolphinQt/Host.cpp +++ b/Source/Core/DolphinQt/Host.cpp @@ -256,6 +256,16 @@ void Host_UpdateDisasmDialog() QueueOnObject(QApplication::instance(), [] { emit Host::GetInstance()->UpdateDisasmDialog(); }); } +void Host_JitCacheCleared() +{ + QueueOnObject(QApplication::instance(), [] { emit Host::GetInstance()->JitCacheCleared(); }); +} + +void Host_JitProfileDataWiped() +{ + QueueOnObject(QApplication::instance(), [] { emit Host::GetInstance()->JitProfileDataWiped(); }); +} + void Host_PPCSymbolsChanged() { QueueOnObject(QApplication::instance(), [] { emit Host::GetInstance()->PPCSymbolsChanged(); }); diff --git a/Source/Core/DolphinQt/Host.h b/Source/Core/DolphinQt/Host.h index ce076d1a91..4f3be255e8 100644 --- a/Source/Core/DolphinQt/Host.h +++ b/Source/Core/DolphinQt/Host.h @@ -40,6 +40,8 @@ signals: void RequestStop(); void RequestRenderSize(int w, int h); void UpdateDisasmDialog(); + void JitCacheCleared(); + void JitProfileDataWiped(); void PPCSymbolsChanged(); void PPCBreakpointsChanged(); diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 5e733b1c71..8ccf384506 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -463,7 +463,7 @@ void MainWindow::CreateComponents() m_wii_tas_input_windows[i] = new WiiTASInputWindow(nullptr, i); } - m_jit_widget = new JITWidget(this); + m_jit_widget = new JITWidget(Core::System::GetInstance(), this); m_log_widget = new LogWidget(this); m_log_config_widget = new LogConfigWidget(this); m_memory_widget = new MemoryWidget(Core::System::GetInstance(), this); @@ -488,6 +488,7 @@ void MainWindow::CreateComponents() m_code_widget->SetAddress(addr, CodeViewWidget::SetAddressUpdate::WithDetailedUpdate); }; + connect(m_jit_widget, &JITWidget::SetCodeAddress, m_code_widget, &CodeWidget::OnSetCodeAddress); connect(m_watch_widget, &WatchWidget::RequestMemoryBreakpoint, request_memory_breakpoint); connect(m_watch_widget, &WatchWidget::ShowMemory, m_memory_widget, &MemoryWidget::SetAddress); connect(m_register_widget, &RegisterWidget::RequestMemoryBreakpoint, request_memory_breakpoint); @@ -500,7 +501,8 @@ void MainWindow::CreateComponents() connect(m_thread_widget, &ThreadWidget::RequestViewInMemory, request_view_in_memory); connect(m_thread_widget, &ThreadWidget::RequestViewInCode, request_view_in_code); - connect(m_code_widget, &CodeWidget::RequestPPCComparison, m_jit_widget, &JITWidget::Compare); + connect(m_code_widget, &CodeWidget::RequestPPCComparison, m_jit_widget, + &JITWidget::OnRequestPPCComparison); connect(m_code_widget, &CodeWidget::ShowMemory, m_memory_widget, &MemoryWidget::SetAddress); connect(m_memory_widget, &MemoryWidget::ShowCode, m_code_widget, [this](u32 address) { m_code_widget->SetAddress(address, CodeViewWidget::SetAddressUpdate::WithDetailedUpdate); diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp index 6170fa9ada..fef441c062 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -95,6 +96,7 @@ MenuBar::MenuBar(QWidget* parent) : QMenuBar(parent) connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, [=, this](Core::State state) { OnEmulationStateChanged(state); }); + connect(&Settings::Instance(), &Settings::ConfigChanged, this, &MenuBar::OnConfigChanged); connect(Host::GetInstance(), &Host::UpdateDisasmDialog, this, [this] { OnEmulationStateChanged(Core::GetState(Core::System::GetInstance())); }); @@ -167,6 +169,12 @@ void MenuBar::OnEmulationStateChanged(Core::State state) OnDebugModeToggled(Settings::Instance().IsDebugModeEnabled()); } +void MenuBar::OnConfigChanged() +{ + const QSignalBlocker blocker(m_jit_profile_blocks); + m_jit_profile_blocks->setChecked(Config::Get(Config::MAIN_DEBUG_JIT_ENABLE_PROFILING)); +} + void MenuBar::OnDebugModeToggled(bool enabled) { // Options diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h index 4ccc8df819..29457c15f7 100644 --- a/Source/Core/DolphinQt/MenuBar.h +++ b/Source/Core/DolphinQt/MenuBar.h @@ -127,6 +127,7 @@ signals: private: void OnEmulationStateChanged(Core::State state); + void OnConfigChanged(); void AddFileMenu(); diff --git a/Source/Core/DolphinQt/QtUtils/ClickableStatusBar.h b/Source/Core/DolphinQt/QtUtils/ClickableStatusBar.h new file mode 100644 index 0000000000..86ab9e6b1a --- /dev/null +++ b/Source/Core/DolphinQt/QtUtils/ClickableStatusBar.h @@ -0,0 +1,22 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +// I wanted a QStatusBar that emits a signal when clicked. Qt only provides event overrides. +class ClickableStatusBar final : public QStatusBar +{ + Q_OBJECT + +signals: + void pressed(); + +protected: + void mousePressEvent(QMouseEvent* event) override { emit pressed(); } + +public: + explicit ClickableStatusBar(QWidget* parent) : QStatusBar(parent) {} + ~ClickableStatusBar() override = default; +}; diff --git a/Source/Core/DolphinTool/ToolHeadlessPlatform.cpp b/Source/Core/DolphinTool/ToolHeadlessPlatform.cpp index 0b04cbfa19..4f445623b9 100644 --- a/Source/Core/DolphinTool/ToolHeadlessPlatform.cpp +++ b/Source/Core/DolphinTool/ToolHeadlessPlatform.cpp @@ -61,6 +61,14 @@ void Host_UpdateDisasmDialog() { } +void Host_JitCacheCleared() +{ +} + +void Host_JitProfileDataWiped() +{ +} + void Host_UpdateMainFrame() { } diff --git a/Source/DSPTool/StubHost.cpp b/Source/DSPTool/StubHost.cpp index e270acb23a..d54d005674 100644 --- a/Source/DSPTool/StubHost.cpp +++ b/Source/DSPTool/StubHost.cpp @@ -41,6 +41,12 @@ bool Host_UpdateDiscordPresenceRaw(const std::string& details, const std::string void Host_UpdateDisasmDialog() { } +void Host_JitCacheCleared() +{ +} +void Host_JitProfileDataWiped() +{ +} void Host_UpdateMainFrame() { } diff --git a/Source/UnitTests/Core/PageFaultTest.cpp b/Source/UnitTests/Core/PageFaultTest.cpp index 21ef289159..a4c5525da2 100644 --- a/Source/UnitTests/Core/PageFaultTest.cpp +++ b/Source/UnitTests/Core/PageFaultTest.cpp @@ -42,6 +42,8 @@ public: void Jit(u32 em_address) override {} void EraseSingleBlock(const JitBlock&) override {} std::vector GetMemoryStats() const override { return {}; } + std::size_t DisassembleNearCode(const JitBlock&, std::ostream&) const override { return 0; } + std::size_t DisassembleFarCode(const JitBlock&, std::ostream&) const override { return 0; } const CommonAsmRoutinesBase* GetAsmRoutines() override { return nullptr; } virtual bool HandleFault(uintptr_t access_address, SContext* ctx) override { diff --git a/Source/UnitTests/StubHost.cpp b/Source/UnitTests/StubHost.cpp index dc7dfe2277..96fe3e71d9 100644 --- a/Source/UnitTests/StubHost.cpp +++ b/Source/UnitTests/StubHost.cpp @@ -41,6 +41,12 @@ bool Host_UpdateDiscordPresenceRaw(const std::string& details, const std::string void Host_UpdateDisasmDialog() { } +void Host_JitCacheCleared() +{ +} +void Host_JitProfileDataWiped() +{ +} void Host_UpdateMainFrame() { }