From 67f60bec7e59dbb24930b23246fa44aa089c9861 Mon Sep 17 00:00:00 2001 From: mitaclaw <140017135+mitaclaw@users.noreply.github.com> Date: Thu, 7 Dec 2023 09:18:38 -0800 Subject: [PATCH 1/5] PowerPC: Implement BranchWatch This new component can track code paths by watching branch hits. --- Source/Core/Core/CMakeLists.txt | 2 + Source/Core/Core/Debugger/BranchWatch.cpp | 314 ++++++++++++++++++ Source/Core/Core/Debugger/BranchWatch.h | 278 ++++++++++++++++ .../Core/PowerPC/Interpreter/Interpreter.cpp | 5 +- .../Core/PowerPC/Interpreter/Interpreter.h | 7 +- .../Core/Core/PowerPC/JitCommon/JitBase.cpp | 2 +- Source/Core/Core/PowerPC/JitCommon/JitBase.h | 4 +- Source/Core/Core/PowerPC/PowerPC.h | 4 + Source/Core/Core/System.cpp | 4 +- Source/Core/DolphinLib.props | 2 + 10 files changed, 614 insertions(+), 8 deletions(-) create mode 100644 Source/Core/Core/Debugger/BranchWatch.cpp create mode 100644 Source/Core/Core/Debugger/BranchWatch.h diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 1dcbc14ce7..290947e722 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -61,6 +61,8 @@ add_library(core CoreTiming.h CPUThreadConfigCallback.cpp CPUThreadConfigCallback.h + Debugger/BranchWatch.cpp + Debugger/BranchWatch.h Debugger/CodeTrace.cpp Debugger/CodeTrace.h Debugger/DebugInterface.h diff --git a/Source/Core/Core/Debugger/BranchWatch.cpp b/Source/Core/Core/Debugger/BranchWatch.cpp new file mode 100644 index 0000000000..cefc38ddb2 --- /dev/null +++ b/Source/Core/Core/Debugger/BranchWatch.cpp @@ -0,0 +1,314 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/Debugger/BranchWatch.h" + +#include +#include +#include + +#include + +#include "Common/Assert.h" +#include "Common/BitField.h" +#include "Common/CommonTypes.h" +#include "Core/Core.h" +#include "Core/PowerPC/Gekko.h" +#include "Core/PowerPC/MMU.h" + +namespace Core +{ +void BranchWatch::Clear(const CPUThreadGuard&) +{ + m_selection.clear(); + m_collection_vt.clear(); + m_collection_vf.clear(); + m_collection_pt.clear(); + m_collection_pf.clear(); + m_recording_phase = Phase::Blacklist; + m_blacklist_size = 0; +} + +// This is a bitfield aggregate of metadata required to reconstruct a BranchWatch's Collections and +// Selection from a text file (a snapshot). For maximum forward compatibility, should that ever be +// required, the StorageType is an unsigned long long instead of something more reasonable like an +// unsigned int or u8. This is because the snapshot text file format contains no version info. +union USnapshotMetadata +{ + using Inspection = BranchWatch::SelectionInspection; + using StorageType = unsigned long long; + + static_assert(Inspection::EndOfEnumeration == Inspection{(1u << 3) + 1}); + + StorageType hex; + + BitField<0, 1, bool, StorageType> is_virtual; + BitField<1, 1, bool, StorageType> condition; + BitField<2, 1, bool, StorageType> is_selected; + BitField<3, 4, Inspection, StorageType> inspection; + + USnapshotMetadata() : hex(0) {} + explicit USnapshotMetadata(bool is_virtual_, bool condition_, bool is_selected_, + Inspection inspection_) + : USnapshotMetadata() + { + is_virtual = is_virtual_; + condition = condition_; + is_selected = is_selected_; + inspection = inspection_; + } +}; + +void BranchWatch::Save(const CPUThreadGuard& guard, std::FILE* file) const +{ + if (!CanSave()) + { + ASSERT_MSG(CORE, false, "BranchWatch can not be saved."); + return; + } + if (file == nullptr) + return; + + const auto routine = [&](const Collection& collection, bool is_virtual, bool condition) { + for (const Collection::value_type& kv : collection) + { + const auto iter = std::find_if( + m_selection.begin(), m_selection.end(), + [&](const Selection::value_type& value) { return value.collection_ptr == &kv; }); + fmt::println(file, "{:08x} {:08x} {:08x} {} {} {:x}", kv.first.origin_addr, + kv.first.destin_addr, kv.first.original_inst.hex, kv.second.total_hits, + kv.second.hits_snapshot, + iter == m_selection.end() ? + USnapshotMetadata(is_virtual, condition, false, {}).hex : + USnapshotMetadata(is_virtual, condition, true, iter->inspection).hex); + } + }; + routine(m_collection_vt, true, true); + routine(m_collection_pt, false, true); + routine(m_collection_vf, true, false); + routine(m_collection_pf, false, false); +} + +void BranchWatch::Load(const CPUThreadGuard& guard, std::FILE* file) +{ + if (file == nullptr) + return; + + Clear(guard); + + u32 origin_addr, destin_addr, inst_hex; + std::size_t total_hits, hits_snapshot; + USnapshotMetadata snapshot_metadata = {}; + while (std::fscanf(file, "%x %x %x %zu %zu %llx", &origin_addr, &destin_addr, &inst_hex, + &total_hits, &hits_snapshot, &snapshot_metadata.hex) == 6) + { + const bool is_virtual = snapshot_metadata.is_virtual; + const bool condition = snapshot_metadata.condition; + + const auto [kv_iter, emplace_success] = + GetCollection(is_virtual, condition) + .try_emplace({{origin_addr, destin_addr}, inst_hex}, + BranchWatchCollectionValue{total_hits, hits_snapshot}); + + if (!emplace_success) + continue; + + if (snapshot_metadata.is_selected) + { + // TODO C++20: Parenthesized initialization of aggregates has bad compiler support. + m_selection.emplace_back(BranchWatchSelectionValueType{&*kv_iter, is_virtual, condition, + snapshot_metadata.inspection}); + } + else if (hits_snapshot != 0) + { + ++m_blacklist_size; // This will be very wrong when not in Blacklist mode. That's ok. + } + } + + if (!m_selection.empty()) + m_recording_phase = Phase::Reduction; +} + +void BranchWatch::IsolateHasExecuted(const CPUThreadGuard&) +{ + switch (m_recording_phase) + { + case Phase::Blacklist: + { + m_selection.reserve(GetCollectionSize() - m_blacklist_size); + const auto routine = [&](Collection& collection, bool is_virtual, bool condition) { + for (Collection::value_type& kv : collection) + { + if (kv.second.hits_snapshot == 0) + { + // TODO C++20: Parenthesized initialization of aggregates has bad compiler support. + m_selection.emplace_back( + BranchWatchSelectionValueType{&kv, is_virtual, condition, SelectionInspection{}}); + kv.second.hits_snapshot = kv.second.total_hits; + } + } + }; + routine(m_collection_vt, true, true); + routine(m_collection_vf, true, false); + routine(m_collection_pt, false, true); + routine(m_collection_pf, false, false); + m_recording_phase = Phase::Reduction; + return; + } + case Phase::Reduction: + std::erase_if(m_selection, [](const Selection::value_type& value) -> bool { + Collection::value_type* const kv = value.collection_ptr; + if (kv->second.total_hits == kv->second.hits_snapshot) + return true; + kv->second.hits_snapshot = kv->second.total_hits; + return false; + }); + return; + } +} + +void BranchWatch::IsolateNotExecuted(const CPUThreadGuard&) +{ + switch (m_recording_phase) + { + case Phase::Blacklist: + { + const auto routine = [&](Collection& collection) { + for (Collection::value_type& kv : collection) + kv.second.hits_snapshot = kv.second.total_hits; + }; + routine(m_collection_vt); + routine(m_collection_vf); + routine(m_collection_pt); + routine(m_collection_pf); + m_blacklist_size = GetCollectionSize(); + return; + } + case Phase::Reduction: + std::erase_if(m_selection, [](const Selection::value_type& value) -> bool { + Collection::value_type* const kv = value.collection_ptr; + if (kv->second.total_hits != kv->second.hits_snapshot) + return true; + kv->second.hits_snapshot = kv->second.total_hits; + return false; + }); + return; + } +} + +void BranchWatch::IsolateWasOverwritten(const CPUThreadGuard& guard) +{ + if (Core::GetState() == Core::State::Uninitialized) + { + ASSERT_MSG(CORE, false, "Core is uninitialized."); + return; + } + switch (m_recording_phase) + { + case Phase::Blacklist: + { + // This is a dirty hack of the assumptions that make the blacklist phase work. If the + // hits_snapshot is non-zero while in the blacklist phase, that means it has been marked + // for exclusion from the transition to the reduction phase. + const auto routine = [&](Collection& collection, PowerPC::RequestedAddressSpace address_space) { + for (Collection::value_type& kv : collection) + { + if (kv.second.hits_snapshot == 0) + { + const std::optional read_result = + PowerPC::MMU::HostTryReadInstruction(guard, kv.first.origin_addr, address_space); + if (!read_result.has_value()) + continue; + if (kv.first.original_inst.hex == read_result->value) + kv.second.hits_snapshot = ++m_blacklist_size; // Any non-zero number will work. + } + } + }; + routine(m_collection_vt, PowerPC::RequestedAddressSpace::Virtual); + routine(m_collection_vf, PowerPC::RequestedAddressSpace::Virtual); + routine(m_collection_pt, PowerPC::RequestedAddressSpace::Physical); + routine(m_collection_pf, PowerPC::RequestedAddressSpace::Physical); + return; + } + case Phase::Reduction: + std::erase_if(m_selection, [&guard](const Selection::value_type& value) -> bool { + const std::optional read_result = PowerPC::MMU::HostTryReadInstruction( + guard, value.collection_ptr->first.origin_addr, + value.is_virtual ? PowerPC::RequestedAddressSpace::Virtual : + PowerPC::RequestedAddressSpace::Physical); + if (!read_result.has_value()) + return false; + return value.collection_ptr->first.original_inst.hex == read_result->value; + }); + return; + } +} + +void BranchWatch::IsolateNotOverwritten(const CPUThreadGuard& guard) +{ + if (Core::GetState() == Core::State::Uninitialized) + { + ASSERT_MSG(CORE, false, "Core is uninitialized."); + return; + } + switch (m_recording_phase) + { + case Phase::Blacklist: + { + // Same dirty hack with != rather than ==, see above for details + const auto routine = [&](Collection& collection, PowerPC::RequestedAddressSpace address_space) { + for (Collection::value_type& kv : collection) + if (kv.second.hits_snapshot == 0) + { + const std::optional read_result = + PowerPC::MMU::HostTryReadInstruction(guard, kv.first.origin_addr, address_space); + if (!read_result.has_value()) + continue; + if (kv.first.original_inst.hex != read_result->value) + kv.second.hits_snapshot = ++m_blacklist_size; // Any non-zero number will work. + } + }; + routine(m_collection_vt, PowerPC::RequestedAddressSpace::Virtual); + routine(m_collection_vf, PowerPC::RequestedAddressSpace::Virtual); + routine(m_collection_pt, PowerPC::RequestedAddressSpace::Physical); + routine(m_collection_pf, PowerPC::RequestedAddressSpace::Physical); + return; + } + case Phase::Reduction: + std::erase_if(m_selection, [&guard](const Selection::value_type& value) -> bool { + const std::optional read_result = PowerPC::MMU::HostTryReadInstruction( + guard, value.collection_ptr->first.origin_addr, + value.is_virtual ? PowerPC::RequestedAddressSpace::Virtual : + PowerPC::RequestedAddressSpace::Physical); + if (!read_result.has_value()) + return false; + return value.collection_ptr->first.original_inst.hex != read_result->value; + }); + return; + } +} + +void BranchWatch::UpdateHitsSnapshot() +{ + switch (m_recording_phase) + { + case Phase::Reduction: + for (Selection::value_type& value : m_selection) + value.collection_ptr->second.hits_snapshot = value.collection_ptr->second.total_hits; + return; + case Phase::Blacklist: + return; + } +} + +void BranchWatch::ClearSelectionInspection() +{ + std::for_each(m_selection.begin(), m_selection.end(), + [](Selection::value_type& value) { value.inspection = {}; }); +} + +void BranchWatch::SetSelectedInspected(std::size_t idx, SelectionInspection inspection) +{ + m_selection[idx].inspection |= inspection; +} +} // namespace Core diff --git a/Source/Core/Core/Debugger/BranchWatch.h b/Source/Core/Core/Debugger/BranchWatch.h new file mode 100644 index 0000000000..be4972bc91 --- /dev/null +++ b/Source/Core/Core/Debugger/BranchWatch.h @@ -0,0 +1,278 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "Common/BitUtils.h" +#include "Common/CommonTypes.h" +#include "Common/EnumUtils.h" +#include "Core/PowerPC/Gekko.h" + +namespace Core +{ +class CPUThreadGuard; +} + +namespace Core +{ +struct FakeBranchWatchCollectionKey +{ + u32 origin_addr; + u32 destin_addr; + + // TODO C++20: constexpr w/ std::bit_cast + inline operator u64() const { return Common::BitCast(*this); } +}; +struct BranchWatchCollectionKey : FakeBranchWatchCollectionKey +{ + UGeckoInstruction original_inst; +}; +struct BranchWatchCollectionValue +{ + std::size_t total_hits = 0; + std::size_t hits_snapshot = 0; +}; +} // namespace Core + +template <> +struct std::hash +{ + std::size_t operator()(const Core::BranchWatchCollectionKey& s) const noexcept + { + return std::hash{}(static_cast(s)); + } +}; + +namespace Core +{ +inline bool operator==(const BranchWatchCollectionKey& lhs, + const BranchWatchCollectionKey& rhs) noexcept +{ + const std::hash hash; + return hash(lhs) == hash(rhs) && lhs.original_inst.hex == rhs.original_inst.hex; +} + +enum class BranchWatchSelectionInspection : u8 +{ + SetOriginNOP = 1u << 0, + SetDestinBLR = 1u << 1, + SetOriginSymbolBLR = 1u << 2, + SetDestinSymbolBLR = 1u << 3, + EndOfEnumeration, +}; + +constexpr BranchWatchSelectionInspection operator|(BranchWatchSelectionInspection lhs, + BranchWatchSelectionInspection rhs) +{ + return static_cast(Common::ToUnderlying(lhs) | + Common::ToUnderlying(rhs)); +} + +constexpr BranchWatchSelectionInspection operator&(BranchWatchSelectionInspection lhs, + BranchWatchSelectionInspection rhs) +{ + return static_cast(Common::ToUnderlying(lhs) & + Common::ToUnderlying(rhs)); +} + +constexpr BranchWatchSelectionInspection& operator|=(BranchWatchSelectionInspection& self, + BranchWatchSelectionInspection other) +{ + return self = self | other; +} + +using BranchWatchCollection = + std::unordered_map; + +struct BranchWatchSelectionValueType +{ + using Inspection = BranchWatchSelectionInspection; + + BranchWatchCollection::value_type* collection_ptr; + bool is_virtual; + bool condition; + // This is moreso a GUI thing, but it works best in the Core code for multiple reasons. + Inspection inspection; +}; + +using BranchWatchSelection = std::vector; + +enum class BranchWatchPhase : bool +{ + Blacklist, + Reduction, +}; + +class BranchWatch final // Class is final to enforce the safety of GetOffsetOfRecordingActive(). +{ +public: + using Collection = BranchWatchCollection; + using Selection = BranchWatchSelection; + using Phase = BranchWatchPhase; + using SelectionInspection = BranchWatchSelectionInspection; + + bool GetRecordingActive() const { return m_recording_active; } + void SetRecordingActive(bool active) { m_recording_active = active; } + void Start() { SetRecordingActive(true); } + void Pause() { SetRecordingActive(false); } + void Clear(const CPUThreadGuard& guard); + + void Save(const CPUThreadGuard& guard, std::FILE* file) const; + void Load(const CPUThreadGuard& guard, std::FILE* file); + + void IsolateHasExecuted(const CPUThreadGuard& guard); + void IsolateNotExecuted(const CPUThreadGuard& guard); + void IsolateWasOverwritten(const CPUThreadGuard& guard); + void IsolateNotOverwritten(const CPUThreadGuard& guard); + void UpdateHitsSnapshot(); + void ClearSelectionInspection(); + void SetSelectedInspected(std::size_t idx, SelectionInspection inspection); + + Selection& GetSelection() { return m_selection; } + const Selection& GetSelection() const { return m_selection; } + + std::size_t GetCollectionSize() const + { + return m_collection_vt.size() + m_collection_vf.size() + m_collection_pt.size() + + m_collection_pf.size(); + } + std::size_t GetBlacklistSize() const { return m_blacklist_size; } + Phase GetRecordingPhase() const { return m_recording_phase; }; + + // An empty selection in reduction mode can't be reconstructed when loading from a file. + bool CanSave() const { return !(m_recording_phase == Phase::Reduction && m_selection.empty()); } + + // All Hit member functions are for the CPUThread only. The static ones are static to remain + // compatible with the JITs' ABI_CallFunction function, which doesn't support non-static member + // functions. HitXX_fk are optimized for when origin and destination can be passed in one register + // easily as a Core::FakeBranchWatchCollectionKey (abbreviated as "fk"). HitXX_fk_n are the same, + // but also increment the total_hits by N (see dcbx JIT code). + static void HitVirtualTrue_fk(BranchWatch* branch_watch, u64 fake_key, u32 inst) + { + branch_watch->m_collection_vt[{Common::BitCast(fake_key), inst}] + .total_hits += 1; + } + + static void HitPhysicalTrue_fk(BranchWatch* branch_watch, u64 fake_key, u32 inst) + { + branch_watch->m_collection_pt[{Common::BitCast(fake_key), inst}] + .total_hits += 1; + } + + static void HitVirtualFalse_fk(BranchWatch* branch_watch, u64 fake_key, u32 inst) + { + branch_watch->m_collection_vf[{Common::BitCast(fake_key), inst}] + .total_hits += 1; + } + + static void HitPhysicalFalse_fk(BranchWatch* branch_watch, u64 fake_key, u32 inst) + { + branch_watch->m_collection_pf[{Common::BitCast(fake_key), inst}] + .total_hits += 1; + } + + static void HitVirtualTrue_fk_n(BranchWatch* branch_watch, u64 fake_key, u32 inst, u32 n) + { + branch_watch->m_collection_vt[{Common::BitCast(fake_key), inst}] + .total_hits += n; + } + + static void HitPhysicalTrue_fk_n(BranchWatch* branch_watch, u64 fake_key, u32 inst, u32 n) + { + branch_watch->m_collection_pt[{Common::BitCast(fake_key), inst}] + .total_hits += n; + } + + // HitVirtualFalse_fk_n and HitPhysicalFalse_fk_n are never used, so they are omitted here. + + static void HitVirtualTrue(BranchWatch* branch_watch, u32 origin, u32 destination, u32 inst) + { + HitVirtualTrue_fk(branch_watch, FakeBranchWatchCollectionKey{origin, destination}, inst); + } + + static void HitPhysicalTrue(BranchWatch* branch_watch, u32 origin, u32 destination, u32 inst) + { + HitPhysicalTrue_fk(branch_watch, FakeBranchWatchCollectionKey{origin, destination}, inst); + } + + static void HitVirtualFalse(BranchWatch* branch_watch, u32 origin, u32 destination, u32 inst) + { + HitVirtualFalse_fk(branch_watch, FakeBranchWatchCollectionKey{origin, destination}, inst); + } + + static void HitPhysicalFalse(BranchWatch* branch_watch, u32 origin, u32 destination, u32 inst) + { + HitPhysicalFalse_fk(branch_watch, FakeBranchWatchCollectionKey{origin, destination}, inst); + } + + void HitTrue(u32 origin, u32 destination, UGeckoInstruction inst, bool translate) + { + if (translate) + HitVirtualTrue(this, origin, destination, inst.hex); + else + HitPhysicalTrue(this, origin, destination, inst.hex); + } + + void HitFalse(u32 origin, u32 destination, UGeckoInstruction inst, bool translate) + { + if (translate) + HitVirtualFalse(this, origin, destination, inst.hex); + else + HitPhysicalFalse(this, origin, destination, inst.hex); + } + + // The JIT needs this value, but doesn't need to be a full-on friend. + static constexpr int GetOffsetOfRecordingActive() + { +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#endif + return offsetof(BranchWatch, m_recording_active); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + } + +private: + Collection& GetCollectionV(bool condition) + { + if (condition) + return m_collection_vt; + return m_collection_vf; + } + + Collection& GetCollectionP(bool condition) + { + if (condition) + return m_collection_pt; + return m_collection_pf; + } + + Collection& GetCollection(bool is_virtual, bool condition) + { + if (is_virtual) + return GetCollectionV(condition); + return GetCollectionP(condition); + } + + std::size_t m_blacklist_size = 0; + Phase m_recording_phase = Phase::Blacklist; + bool m_recording_active = false; + Collection m_collection_vt; // virtual address space | true path + Collection m_collection_vf; // virtual address space | false path + Collection m_collection_pt; // physical address space | true path + Collection m_collection_pf; // physical address space | false path + Selection m_selection; +}; + +#if _M_X86_64 +static_assert(BranchWatch::GetOffsetOfRecordingActive() < 0x80); // Makes JIT code smaller. +#endif +} // namespace Core diff --git a/Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp b/Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp index 71f01d13b7..fc5f1d2aba 100644 --- a/Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp +++ b/Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp @@ -64,8 +64,9 @@ void Interpreter::UpdatePC() m_ppc_state.pc = m_ppc_state.npc; } -Interpreter::Interpreter(Core::System& system, PowerPC::PowerPCState& ppc_state, PowerPC::MMU& mmu) - : m_system(system), m_ppc_state(ppc_state), m_mmu(mmu) +Interpreter::Interpreter(Core::System& system, PowerPC::PowerPCState& ppc_state, PowerPC::MMU& mmu, + Core::BranchWatch& branch_watch) + : m_system(system), m_ppc_state(ppc_state), m_mmu(mmu), m_branch_watch(branch_watch) { } diff --git a/Source/Core/Core/PowerPC/Interpreter/Interpreter.h b/Source/Core/Core/PowerPC/Interpreter/Interpreter.h index 90595de068..d51ad1a2dd 100644 --- a/Source/Core/Core/PowerPC/Interpreter/Interpreter.h +++ b/Source/Core/Core/PowerPC/Interpreter/Interpreter.h @@ -11,8 +11,9 @@ namespace Core { +class BranchWatch; class System; -} +} // namespace Core namespace PowerPC { class MMU; @@ -22,7 +23,8 @@ struct PowerPCState; class Interpreter : public CPUCoreBase { public: - Interpreter(Core::System& system, PowerPC::PowerPCState& ppc_state, PowerPC::MMU& mmu); + Interpreter(Core::System& system, PowerPC::PowerPCState& ppc_state, PowerPC::MMU& mmu, + Core::BranchWatch& branch_watch); Interpreter(const Interpreter&) = delete; Interpreter(Interpreter&&) = delete; Interpreter& operator=(const Interpreter&) = delete; @@ -314,6 +316,7 @@ private: Core::System& m_system; PowerPC::PowerPCState& m_ppc_state; PowerPC::MMU& m_mmu; + Core::BranchWatch& m_branch_watch; UGeckoInstruction m_prev_inst{}; u32 m_last_pc = 0; diff --git a/Source/Core/Core/PowerPC/JitCommon/JitBase.cpp b/Source/Core/Core/PowerPC/JitCommon/JitBase.cpp index c1cce80401..87f017f7f8 100644 --- a/Source/Core/Core/PowerPC/JitCommon/JitBase.cpp +++ b/Source/Core/Core/PowerPC/JitCommon/JitBase.cpp @@ -94,7 +94,7 @@ void JitTrampoline(JitBase& jit, u32 em_address) JitBase::JitBase(Core::System& system) : m_code_buffer(code_buffer_size), m_system(system), m_ppc_state(system.GetPPCState()), - m_mmu(system.GetMMU()) + m_mmu(system.GetMMU()), m_branch_watch(system.GetPowerPC().GetBranchWatch()) { m_registered_config_callback_id = CPUThreadConfigCallback::AddConfigChangedCallback([this] { if (DoesConfigNeedRefresh()) diff --git a/Source/Core/Core/PowerPC/JitCommon/JitBase.h b/Source/Core/Core/PowerPC/JitCommon/JitBase.h index 00fea6ef84..1d947da5ea 100644 --- a/Source/Core/Core/PowerPC/JitCommon/JitBase.h +++ b/Source/Core/Core/PowerPC/JitCommon/JitBase.h @@ -23,8 +23,9 @@ namespace Core { +class BranchWatch; class System; -} +} // namespace Core namespace PowerPC { class MMU; @@ -206,6 +207,7 @@ public: Core::System& m_system; PowerPC::PowerPCState& m_ppc_state; PowerPC::MMU& m_mmu; + Core::BranchWatch& m_branch_watch; }; void JitTrampoline(JitBase& jit, u32 em_address); diff --git a/Source/Core/Core/PowerPC/PowerPC.h b/Source/Core/Core/PowerPC/PowerPC.h index c0611ae916..750f0c6f69 100644 --- a/Source/Core/Core/PowerPC/PowerPC.h +++ b/Source/Core/Core/PowerPC/PowerPC.h @@ -14,6 +14,7 @@ #include "Common/CommonTypes.h" #include "Core/CPUThreadConfigCallback.h" +#include "Core/Debugger/BranchWatch.h" #include "Core/Debugger/PPCDebugInterface.h" #include "Core/PowerPC/BreakPoints.h" #include "Core/PowerPC/ConditionRegister.h" @@ -298,6 +299,8 @@ public: const MemChecks& GetMemChecks() const { return m_memchecks; } PPCDebugInterface& GetDebugInterface() { return m_debug_interface; } const PPCDebugInterface& GetDebugInterface() const { return m_debug_interface; } + Core::BranchWatch& GetBranchWatch() { return m_branch_watch; } + const Core::BranchWatch& GetBranchWatch() const { return m_branch_watch; } private: void InitializeCPUCore(CPUCore cpu_core); @@ -314,6 +317,7 @@ private: BreakPoints m_breakpoints; MemChecks m_memchecks; PPCDebugInterface m_debug_interface; + Core::BranchWatch m_branch_watch; CPUThreadConfigCallback::ConfigChangedCallbackID m_registered_config_callback_id; diff --git a/Source/Core/Core/System.cpp b/Source/Core/Core/System.cpp index 9c97e6febe..2da55975de 100644 --- a/Source/Core/Core/System.cpp +++ b/Source/Core/Core/System.cpp @@ -52,8 +52,8 @@ struct System::Impl m_memory(system), m_pixel_engine{system}, m_power_pc(system), m_mmu(system, m_memory, m_power_pc), m_processor_interface(system), m_serial_interface(system), m_system_timers(system), m_video_interface(system), - m_interpreter(system, m_power_pc.GetPPCState(), m_mmu), m_jit_interface(system), - m_fifo_player(system), m_fifo_recorder(system), m_movie(system) + m_interpreter(system, m_power_pc.GetPPCState(), m_mmu, m_power_pc.GetBranchWatch()), + m_jit_interface(system), m_fifo_player(system), m_fifo_recorder(system), m_movie(system) { } diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 42ae7ba5e6..a5039859f9 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -202,6 +202,7 @@ + @@ -868,6 +869,7 @@ + From 2aa250a68ab8fa35584d3ccb2d3fb2ccf367ef12 Mon Sep 17 00:00:00 2001 From: mitaclaw <140017135+mitaclaw@users.noreply.github.com> Date: Thu, 7 Dec 2023 09:32:58 -0800 Subject: [PATCH 2/5] Interpreter: Install BranchWatch --- .../Interpreter/Interpreter_Branch.cpp | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/Source/Core/Core/PowerPC/Interpreter/Interpreter_Branch.cpp b/Source/Core/Core/PowerPC/Interpreter/Interpreter_Branch.cpp index 8b5bac0509..1d57d971b4 100644 --- a/Source/Core/Core/PowerPC/Interpreter/Interpreter_Branch.cpp +++ b/Source/Core/Core/PowerPC/Interpreter/Interpreter_Branch.cpp @@ -7,6 +7,7 @@ #include "Common/CommonTypes.h" #include "Core/ConfigManager.h" #include "Core/Core.h" +#include "Core/Debugger/BranchWatch.h" #include "Core/HLE/HLE.h" #include "Core/PowerPC/Interpreter/ExceptionUtils.h" #include "Core/PowerPC/PowerPC.h" @@ -19,12 +20,13 @@ void Interpreter::bx(Interpreter& interpreter, UGeckoInstruction inst) if (inst.LK) LR(ppc_state) = ppc_state.pc + 4; - const auto address = u32(SignExt26(inst.LI << 2)); + u32 destination_addr = u32(SignExt26(inst.LI << 2)); + if (!inst.AA) + destination_addr += ppc_state.pc; + ppc_state.npc = destination_addr; - if (inst.AA) - ppc_state.npc = address; - else - ppc_state.npc = ppc_state.pc + address; + if (auto& branch_watch = interpreter.m_branch_watch; branch_watch.GetRecordingActive()) + branch_watch.HitTrue(ppc_state.pc, destination_addr, inst, ppc_state.msr.IR); interpreter.m_end_block = true; } @@ -33,6 +35,7 @@ void Interpreter::bx(Interpreter& interpreter, UGeckoInstruction inst) void Interpreter::bcx(Interpreter& interpreter, UGeckoInstruction inst) { auto& ppc_state = interpreter.m_ppc_state; + auto& branch_watch = interpreter.m_branch_watch; if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) CTR(ppc_state)--; @@ -49,12 +52,17 @@ void Interpreter::bcx(Interpreter& interpreter, UGeckoInstruction inst) if (inst.LK) LR(ppc_state) = ppc_state.pc + 4; - const auto address = u32(SignExt16(s16(inst.BD << 2))); + u32 destination_addr = u32(SignExt16(s16(inst.BD << 2))); + if (!inst.AA) + destination_addr += ppc_state.pc; + ppc_state.npc = destination_addr; - if (inst.AA) - ppc_state.npc = address; - else - ppc_state.npc = ppc_state.pc + address; + if (branch_watch.GetRecordingActive()) + branch_watch.HitTrue(ppc_state.pc, destination_addr, inst, ppc_state.msr.IR); + } + else if (branch_watch.GetRecordingActive()) + { + branch_watch.HitFalse(ppc_state.pc, ppc_state.pc + 4, inst, ppc_state.msr.IR); } interpreter.m_end_block = true; @@ -63,6 +71,7 @@ void Interpreter::bcx(Interpreter& interpreter, UGeckoInstruction inst) void Interpreter::bcctrx(Interpreter& interpreter, UGeckoInstruction inst) { auto& ppc_state = interpreter.m_ppc_state; + auto& branch_watch = interpreter.m_branch_watch; DEBUG_ASSERT_MSG(POWERPC, (inst.BO_2 & BO_DONT_DECREMENT_FLAG) != 0, "bcctrx with decrement and test CTR option is invalid!"); @@ -72,9 +81,17 @@ void Interpreter::bcctrx(Interpreter& interpreter, UGeckoInstruction inst) if (condition != 0) { - ppc_state.npc = CTR(ppc_state) & (~3); + const u32 destination_addr = CTR(ppc_state) & (~3); + ppc_state.npc = destination_addr; if (inst.LK_3) LR(ppc_state) = ppc_state.pc + 4; + + if (branch_watch.GetRecordingActive()) + branch_watch.HitTrue(ppc_state.pc, destination_addr, inst, ppc_state.msr.IR); + } + else if (branch_watch.GetRecordingActive()) + { + branch_watch.HitFalse(ppc_state.pc, ppc_state.pc + 4, inst, ppc_state.msr.IR); } interpreter.m_end_block = true; @@ -83,6 +100,7 @@ void Interpreter::bcctrx(Interpreter& interpreter, UGeckoInstruction inst) void Interpreter::bclrx(Interpreter& interpreter, UGeckoInstruction inst) { auto& ppc_state = interpreter.m_ppc_state; + auto& branch_watch = interpreter.m_branch_watch; if ((inst.BO_2 & BO_DONT_DECREMENT_FLAG) == 0) CTR(ppc_state)--; @@ -93,9 +111,17 @@ void Interpreter::bclrx(Interpreter& interpreter, UGeckoInstruction inst) if ((counter & condition) != 0) { - ppc_state.npc = LR(ppc_state) & (~3); + const u32 destination_addr = LR(ppc_state) & (~3); + ppc_state.npc = destination_addr; if (inst.LK_3) LR(ppc_state) = ppc_state.pc + 4; + + if (branch_watch.GetRecordingActive()) + branch_watch.HitTrue(ppc_state.pc, destination_addr, inst, ppc_state.msr.IR); + } + else if (branch_watch.GetRecordingActive()) + { + branch_watch.HitFalse(ppc_state.pc, ppc_state.pc + 4, inst, ppc_state.msr.IR); } interpreter.m_end_block = true; From 7cccedca1e95b755a923972ce08fbcd946e36bfb Mon Sep 17 00:00:00 2001 From: mitaclaw <140017135+mitaclaw@users.noreply.github.com> Date: Thu, 7 Dec 2023 09:35:14 -0800 Subject: [PATCH 3/5] Jit64: Install BranchWatch --- Source/Core/Core/PowerPC/Jit64/Jit.cpp | 13 +- Source/Core/Core/PowerPC/Jit64/Jit.h | 6 + Source/Core/Core/PowerPC/Jit64/Jit_Branch.cpp | 142 +++++++++++++++++- .../Core/Core/PowerPC/Jit64/Jit_Integer.cpp | 49 +++++- .../Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp | 35 +++++ 5 files changed, 234 insertions(+), 11 deletions(-) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.cpp b/Source/Core/Core/PowerPC/Jit64/Jit.cpp index 4e8fa16889..e83b9204aa 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit.cpp @@ -1041,7 +1041,18 @@ bool Jit64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) if (HandleFunctionHooking(op.address)) break; - if (!op.skip) + if (op.skip) + { + if (IsDebuggingEnabled()) + { + // The only thing that currently sets op.skip is the BLR following optimization. + // If any non-branch instruction starts setting that too, this will need to be changed. + ASSERT(op.inst.hex == 0x4e800020); + WriteBranchWatch(op.address, op.branchTo, op.inst, RSCRATCH, RSCRATCH2, + CallerSavedRegistersInUse()); + } + } + else { if ((opinfo->flags & FL_USE_FPU) && !js.firstFPInstructionFound) { diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.h b/Source/Core/Core/PowerPC/Jit64/Jit.h index 9fd3f9d7f7..0794dc34a3 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit.h +++ b/Source/Core/Core/PowerPC/Jit64/Jit.h @@ -98,6 +98,12 @@ public: void WriteExternalExceptionExit(); void WriteRfiExitDestInRSCRATCH(); void WriteIdleExit(u32 destination); + template + void WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst, Gen::X64Reg reg_a, + Gen::X64Reg reg_b, BitSet32 caller_save); + void WriteBranchWatchDestInRSCRATCH(u32 origin, UGeckoInstruction inst, Gen::X64Reg reg_a, + Gen::X64Reg reg_b, BitSet32 caller_save); + bool Cleanup(); void GenerateConstantOverflow(bool overflow); diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Branch.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Branch.cpp index 20de3a33a4..bd611e2c7b 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Branch.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Branch.cpp @@ -7,6 +7,7 @@ #include "Common/CommonTypes.h" #include "Common/x64Emitter.h" #include "Core/CoreTiming.h" +#include "Core/Debugger/BranchWatch.h" #include "Core/PowerPC/Gekko.h" #include "Core/PowerPC/Jit64/RegCache/JitRegCache.h" #include "Core/PowerPC/Jit64Common/Jit64PowerPCState.h" @@ -66,6 +67,68 @@ void Jit64::rfi(UGeckoInstruction inst) WriteRfiExitDestInRSCRATCH(); } +template +void Jit64::WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst, X64Reg reg_a, + X64Reg reg_b, BitSet32 caller_save) +{ + MOV(64, R(reg_a), ImmPtr(&m_branch_watch)); + MOVZX(32, 8, reg_b, MDisp(reg_a, Core::BranchWatch::GetOffsetOfRecordingActive())); + TEST(32, R(reg_b), R(reg_b)); + + FixupBranch branch_in = J_CC(CC_NZ, Jump::Near); + SwitchToFarCode(); + SetJumpTarget(branch_in); + + ABI_PushRegistersAndAdjustStack(caller_save, 0); + // Some call sites have an optimization to use ABI_PARAM1 as a scratch register. + if (reg_a != ABI_PARAM1) + MOV(64, R(ABI_PARAM1), R(reg_a)); + MOV(64, R(ABI_PARAM2), Imm64(Core::FakeBranchWatchCollectionKey{origin, destination})); + MOV(32, R(ABI_PARAM3), Imm32(inst.hex)); + ABI_CallFunction(m_ppc_state.msr.IR ? (condition ? &Core::BranchWatch::HitVirtualTrue_fk : + &Core::BranchWatch::HitVirtualFalse_fk) : + (condition ? &Core::BranchWatch::HitPhysicalTrue_fk : + &Core::BranchWatch::HitPhysicalFalse_fk)); + ABI_PopRegistersAndAdjustStack(caller_save, 0); + + FixupBranch branch_out = J(Jump::Near); + SwitchToNearCode(); + SetJumpTarget(branch_out); +} + +template void Jit64::WriteBranchWatch(u32, u32, UGeckoInstruction, X64Reg, X64Reg, BitSet32); +template void Jit64::WriteBranchWatch(u32, u32, UGeckoInstruction, X64Reg, X64Reg, BitSet32); + +void Jit64::WriteBranchWatchDestInRSCRATCH(u32 origin, UGeckoInstruction inst, X64Reg reg_a, + X64Reg reg_b, BitSet32 caller_save) +{ + MOV(64, R(reg_a), ImmPtr(&m_branch_watch)); + MOVZX(32, 8, reg_b, MDisp(reg_a, Core::BranchWatch::GetOffsetOfRecordingActive())); + TEST(32, R(reg_b), R(reg_b)); + + FixupBranch branch_in = J_CC(CC_NZ, Jump::Near); + SwitchToFarCode(); + SetJumpTarget(branch_in); + + // Assert RSCRATCH won't be clobbered before it is moved from. + static_assert(ABI_PARAM1 != RSCRATCH); + + ABI_PushRegistersAndAdjustStack(caller_save, 0); + // Some call sites have an optimization to use ABI_PARAM1 as a scratch register. + if (reg_a != ABI_PARAM1) + MOV(64, R(ABI_PARAM1), R(reg_a)); + MOV(32, R(ABI_PARAM3), R(RSCRATCH)); + MOV(32, R(ABI_PARAM2), Imm32(origin)); + MOV(32, R(ABI_PARAM4), Imm32(inst.hex)); + ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue : + &Core::BranchWatch::HitPhysicalTrue); + ABI_PopRegistersAndAdjustStack(caller_save, 0); + + FixupBranch branch_out = J(Jump::Near); + SwitchToNearCode(); + SetJumpTarget(branch_out); +} + void Jit64::bx(UGeckoInstruction inst) { INSTRUCTION_START @@ -81,6 +144,11 @@ void Jit64::bx(UGeckoInstruction inst) // Because PPCAnalyst::Flatten() merged the blocks. if (!js.isLastInstruction) { + if (IsDebuggingEnabled()) + { + WriteBranchWatch(js.compilerPC, js.op->branchTo, inst, RSCRATCH, RSCRATCH2, + CallerSavedRegistersInUse()); + } if (inst.LK && !js.op->skipLRStack) { // We have to fake the stack as the RET instruction was not @@ -94,6 +162,11 @@ void Jit64::bx(UGeckoInstruction inst) gpr.Flush(); fpr.Flush(); + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatch(js.compilerPC, js.op->branchTo, inst, ABI_PARAM1, RSCRATCH, {}); + } #ifdef ACID_TEST if (inst.LK) AND(32, PPCSTATE(cr), Imm32(~(0xFF000000))); @@ -144,6 +217,11 @@ void Jit64::bcx(UGeckoInstruction inst) if (!js.isLastInstruction && (inst.BO & BO_DONT_DECREMENT_FLAG) && (inst.BO & BO_DONT_CHECK_CONDITION)) { + if (IsDebuggingEnabled()) + { + WriteBranchWatch(js.compilerPC, js.op->branchTo, inst, RSCRATCH, RSCRATCH2, + CallerSavedRegistersInUse()); + } if (inst.LK && !js.op->skipLRStack) { // We have to fake the stack as the RET instruction was not @@ -160,6 +238,11 @@ void Jit64::bcx(UGeckoInstruction inst) gpr.Flush(); fpr.Flush(); + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatch(js.compilerPC, js.op->branchTo, inst, ABI_PARAM1, RSCRATCH, {}); + } if (js.op->branchIsIdleLoop) { WriteIdleExit(js.op->branchTo); @@ -179,8 +262,18 @@ void Jit64::bcx(UGeckoInstruction inst) { gpr.Flush(); fpr.Flush(); + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatch(js.compilerPC, js.compilerPC + 4, inst, ABI_PARAM1, RSCRATCH, {}); + } WriteExit(js.compilerPC + 4); } + else if (IsDebuggingEnabled()) + { + WriteBranchWatch(js.compilerPC, js.compilerPC + 4, inst, RSCRATCH, RSCRATCH2, + CallerSavedRegistersInUse()); + } } void Jit64::bcctrx(UGeckoInstruction inst) @@ -204,6 +297,12 @@ void Jit64::bcctrx(UGeckoInstruction inst) if (inst.LK_3) MOV(32, PPCSTATE_LR, Imm32(js.compilerPC + 4)); // LR = PC + 4; AND(32, R(RSCRATCH), Imm32(0xFFFFFFFC)); + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatchDestInRSCRATCH(js.compilerPC, inst, ABI_PARAM1, RSCRATCH2, + BitSet32{RSCRATCH}); + } WriteExitDestInRSCRATCH(inst.LK_3, js.compilerPC + 4); } else @@ -226,6 +325,12 @@ void Jit64::bcctrx(UGeckoInstruction inst) RCForkGuard fpr_guard = fpr.Fork(); gpr.Flush(); fpr.Flush(); + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatchDestInRSCRATCH(js.compilerPC, inst, ABI_PARAM1, RSCRATCH2, + BitSet32{RSCRATCH}); + } WriteExitDestInRSCRATCH(inst.LK_3, js.compilerPC + 4); // Would really like to continue the block here, but it ends. TODO. } @@ -235,8 +340,18 @@ void Jit64::bcctrx(UGeckoInstruction inst) { gpr.Flush(); fpr.Flush(); + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatch(js.compilerPC, js.compilerPC + 4, inst, ABI_PARAM1, RSCRATCH, {}); + } WriteExit(js.compilerPC + 4); } + else if (IsDebuggingEnabled()) + { + WriteBranchWatch(js.compilerPC, js.compilerPC + 4, inst, RSCRATCH, RSCRATCH2, + CallerSavedRegistersInUse()); + } } } @@ -270,10 +385,8 @@ void Jit64::bclrx(UGeckoInstruction inst) MOV(32, R(RSCRATCH), PPCSTATE_LR); // We don't have to do this because WriteBLRExit handles it for us. Specifically, since we only - // ever push - // divisible-by-four instruction addresses onto the stack, if the return address matches, we're - // already - // good. If it doesn't match, the mispredicted-BLR code handles the fixup. + // ever push divisible-by-four instruction addresses onto the stack, if the return address + // matches, we're already good. If it doesn't match, the mispredicted-BLR code handles the fixup. if (!m_enable_blr_optimization) AND(32, R(RSCRATCH), Imm32(0xFFFFFFFC)); if (inst.LK) @@ -287,10 +400,21 @@ void Jit64::bclrx(UGeckoInstruction inst) if (js.op->branchIsIdleLoop) { + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatch(js.compilerPC, js.op->branchTo, inst, ABI_PARAM1, RSCRATCH, {}); + } WriteIdleExit(js.op->branchTo); } else { + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatchDestInRSCRATCH(js.compilerPC, inst, ABI_PARAM1, RSCRATCH2, + BitSet32{RSCRATCH}); + } WriteBLRExit(); } } @@ -304,6 +428,16 @@ void Jit64::bclrx(UGeckoInstruction inst) { gpr.Flush(); fpr.Flush(); + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatch(js.compilerPC, js.compilerPC + 4, inst, ABI_PARAM1, RSCRATCH, {}); + } WriteExit(js.compilerPC + 4); } + else if (IsDebuggingEnabled()) + { + WriteBranchWatch(js.compilerPC, js.compilerPC + 4, inst, RSCRATCH, RSCRATCH2, + CallerSavedRegistersInUse()); + } } diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp index ec2d6ae778..afc1c9a920 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp @@ -394,18 +394,25 @@ void Jit64::DoMergedBranch() if (next.LK) MOV(32, PPCSTATE_SPR(SPR_LR), Imm32(nextPC + 4)); - WriteIdleExit(js.op[1].branchTo); + const u32 destination = js.op[1].branchTo; + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatch(nextPC, destination, next, ABI_PARAM1, RSCRATCH, {}); + } + WriteIdleExit(destination); } else if (next.OPCD == 16) // bcx { if (next.LK) MOV(32, PPCSTATE_SPR(SPR_LR), Imm32(nextPC + 4)); - u32 destination; - if (next.AA) - destination = SignExt16(next.BD << 2); - else - destination = nextPC + SignExt16(next.BD << 2); + const u32 destination = js.op[1].branchTo; + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatch(nextPC, destination, next, ABI_PARAM1, RSCRATCH, {}); + } WriteExit(destination, next.LK, nextPC + 4); } else if ((next.OPCD == 19) && (next.SUBOP10 == 528)) // bcctrx @@ -414,6 +421,11 @@ void Jit64::DoMergedBranch() MOV(32, PPCSTATE_SPR(SPR_LR), Imm32(nextPC + 4)); MOV(32, R(RSCRATCH), PPCSTATE_SPR(SPR_CTR)); AND(32, R(RSCRATCH), Imm32(0xFFFFFFFC)); + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatchDestInRSCRATCH(nextPC, next, ABI_PARAM1, RSCRATCH2, BitSet32{RSCRATCH}); + } WriteExitDestInRSCRATCH(next.LK, nextPC + 4); } else if ((next.OPCD == 19) && (next.SUBOP10 == 16)) // bclrx @@ -423,6 +435,11 @@ void Jit64::DoMergedBranch() AND(32, R(RSCRATCH), Imm32(0xFFFFFFFC)); if (next.LK) MOV(32, PPCSTATE_SPR(SPR_LR), Imm32(nextPC + 4)); + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatchDestInRSCRATCH(nextPC, next, ABI_PARAM1, RSCRATCH2, BitSet32{RSCRATCH}); + } WriteBLRExit(); } else @@ -480,8 +497,18 @@ void Jit64::DoMergedBranchCondition() { gpr.Flush(); fpr.Flush(); + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatch(nextPC, nextPC + 4, next, ABI_PARAM1, RSCRATCH, {}); + } WriteExit(nextPC + 4); } + else if (IsDebuggingEnabled()) + { + WriteBranchWatch(nextPC, nextPC + 4, next, RSCRATCH, RSCRATCH2, + CallerSavedRegistersInUse()); + } } void Jit64::DoMergedBranchImmediate(s64 val) @@ -515,8 +542,18 @@ void Jit64::DoMergedBranchImmediate(s64 val) { gpr.Flush(); fpr.Flush(); + if (IsDebuggingEnabled()) + { + // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function. + WriteBranchWatch(nextPC, nextPC + 4, next, ABI_PARAM1, RSCRATCH, {}); + } WriteExit(nextPC + 4); } + else if (IsDebuggingEnabled()) + { + WriteBranchWatch(nextPC, nextPC + 4, next, RSCRATCH, RSCRATCH2, + CallerSavedRegistersInUse()); + } } void Jit64::cmpXX(UGeckoInstruction inst) diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp index 6a1dc65141..05bd690694 100644 --- a/Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp +++ b/Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp @@ -15,6 +15,7 @@ #include "Core/ConfigManager.h" #include "Core/CoreTiming.h" +#include "Core/Debugger/BranchWatch.h" #include "Core/HW/CPU.h" #include "Core/HW/Memmap.h" #include "Core/PowerPC/Jit64/RegCache/JitRegCache.h" @@ -300,6 +301,40 @@ void Jit64::dcbx(UGeckoInstruction inst) // Load the loop_counter register with the amount of invalidations to execute. LEA(32, loop_counter, MDisp(RSCRATCH2, 1)); + + if (IsDebuggingEnabled()) + { + const X64Reg bw_reg_a = reg_cycle_count, bw_reg_b = reg_downcount; + const BitSet32 bw_caller_save = (CallerSavedRegistersInUse() | BitSet32{RSCRATCH2}) & + ~BitSet32{int(bw_reg_a), int(bw_reg_b)}; + + MOV(64, R(bw_reg_a), ImmPtr(&m_branch_watch)); + MOVZX(32, 8, bw_reg_b, MDisp(bw_reg_a, Core::BranchWatch::GetOffsetOfRecordingActive())); + TEST(32, R(bw_reg_b), R(bw_reg_b)); + + FixupBranch branch_in = J_CC(CC_NZ, Jump::Near); + SwitchToFarCode(); + SetJumpTarget(branch_in); + + // Assert RSCRATCH2 won't be clobbered before it is moved from. + static_assert(RSCRATCH2 != ABI_PARAM1); + + ABI_PushRegistersAndAdjustStack(bw_caller_save, 0); + MOV(64, R(ABI_PARAM1), R(bw_reg_a)); + // RSCRATCH2 holds the amount of faked branch watch hits. Move RSCRATCH2 first, because + // ABI_PARAM2 clobbers RSCRATCH2 on Windows and ABI_PARAM3 clobbers RSCRATCH2 on Linux! + MOV(32, R(ABI_PARAM4), R(RSCRATCH2)); + const PPCAnalyst::CodeOp& op = js.op[2]; + MOV(64, R(ABI_PARAM2), Imm64(Core::FakeBranchWatchCollectionKey{op.address, op.branchTo})); + MOV(32, R(ABI_PARAM3), Imm32(op.inst.hex)); + ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue_fk_n : + &Core::BranchWatch::HitPhysicalTrue_fk_n); + ABI_PopRegistersAndAdjustStack(bw_caller_save, 0); + + FixupBranch branch_out = J(Jump::Near); + SwitchToNearCode(); + SetJumpTarget(branch_out); + } } X64Reg addr = RSCRATCH; From fd8f2c782249e4724960a27f7e4560fc4946c214 Mon Sep 17 00:00:00 2001 From: mitaclaw <140017135+mitaclaw@users.noreply.github.com> Date: Wed, 20 Dec 2023 19:10:03 -0800 Subject: [PATCH 4/5] JitArm64: Install BranchWatch --- Source/Core/Core/PowerPC/JitArm64/Jit.cpp | 17 +- Source/Core/Core/PowerPC/JitArm64/Jit.h | 10 + .../Core/PowerPC/JitArm64/JitArm64_Branch.cpp | 189 ++++++++++++++++-- .../PowerPC/JitArm64/JitArm64_LoadStore.cpp | 60 ++++-- 4 files changed, 247 insertions(+), 29 deletions(-) diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp index 5d7e9b2831..49b4238cce 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp @@ -1181,7 +1181,22 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC) if (HandleFunctionHooking(op.address)) break; - if (!op.skip) + if (op.skip) + { + if (IsDebuggingEnabled()) + { + // The only thing that currently sets op.skip is the BLR following optimization. + // If any non-branch instruction starts setting that too, this will need to be changed. + ASSERT(op.inst.hex == 0x4e800020); + const ARM64Reg bw_reg_a = gpr.GetReg(), bw_reg_b = gpr.GetReg(); + const BitSet32 gpr_caller_save = + gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(bw_reg_a), DecodeReg(bw_reg_b)}; + WriteBranchWatch(op.address, op.branchTo, op.inst, bw_reg_a, bw_reg_b, + gpr_caller_save, fpr.GetCallerSavedUsed()); + gpr.Unlock(bw_reg_a, bw_reg_b); + } + } + else { if ((opinfo->flags & FL_USE_FPU) && !js.firstFPInstructionFound) { diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.h b/Source/Core/Core/PowerPC/JitArm64/Jit.h index 02fc3ca353..ad9c7e4672 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.h +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.h @@ -315,6 +315,16 @@ protected: void MSRUpdated(u32 msr); void MSRUpdated(Arm64Gen::ARM64Reg msr); + // Branch Watch + template + void WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst, + Arm64Gen::ARM64Reg reg_a, Arm64Gen::ARM64Reg reg_b, + BitSet32 gpr_caller_save, BitSet32 fpr_caller_save); + void WriteBranchWatchDestInRegister(u32 origin, Arm64Gen::ARM64Reg destination, + UGeckoInstruction inst, Arm64Gen::ARM64Reg reg_a, + Arm64Gen::ARM64Reg reg_b, BitSet32 gpr_caller_save, + BitSet32 fpr_caller_save); + // Exits void WriteExit(u32 destination, bool LK = false, u32 exit_address_after_return = 0, diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp index 9ea05ac06e..01cd813f2d 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp @@ -8,6 +8,7 @@ #include "Core/Core.h" #include "Core/CoreTiming.h" +#include "Core/Debugger/BranchWatch.h" #include "Core/PowerPC/JitArm64/JitArm64_RegCache.h" #include "Core/PowerPC/PPCTables.h" #include "Core/PowerPC/PowerPC.h" @@ -74,6 +75,70 @@ void JitArm64::rfi(UGeckoInstruction inst) gpr.Unlock(WA); } +template +void JitArm64::WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst, ARM64Reg reg_a, + ARM64Reg reg_b, BitSet32 gpr_caller_save, BitSet32 fpr_caller_save) +{ + const ARM64Reg branch_watch = EncodeRegTo64(reg_a); + MOVP2R(branch_watch, &m_branch_watch); + LDRB(IndexType::Unsigned, reg_b, branch_watch, Core::BranchWatch::GetOffsetOfRecordingActive()); + FixupBranch branch_over = CBZ(reg_b); + + FixupBranch branch_in = B(); + SwitchToFarCode(); + SetJumpTarget(branch_in); + + const ARM64Reg float_emit_tmp = EncodeRegTo64(reg_b); + ABI_PushRegisters(gpr_caller_save); + m_float_emit.ABI_PushRegisters(fpr_caller_save, float_emit_tmp); + ABI_CallFunction(m_ppc_state.msr.IR ? (condition ? &Core::BranchWatch::HitVirtualTrue_fk : + &Core::BranchWatch::HitVirtualFalse_fk) : + (condition ? &Core::BranchWatch::HitPhysicalTrue_fk : + &Core::BranchWatch::HitPhysicalFalse_fk), + branch_watch, Core::FakeBranchWatchCollectionKey{origin, destination}, inst.hex); + m_float_emit.ABI_PopRegisters(fpr_caller_save, float_emit_tmp); + ABI_PopRegisters(gpr_caller_save); + + FixupBranch branch_out = B(); + SwitchToNearCode(); + SetJumpTarget(branch_out); + SetJumpTarget(branch_over); +} + +template void JitArm64::WriteBranchWatch(u32, u32, UGeckoInstruction, ARM64Reg, ARM64Reg, + BitSet32, BitSet32); +template void JitArm64::WriteBranchWatch(u32, u32, UGeckoInstruction, ARM64Reg, ARM64Reg, + BitSet32, BitSet32); + +void JitArm64::WriteBranchWatchDestInRegister(u32 origin, ARM64Reg destination, + UGeckoInstruction inst, ARM64Reg reg_a, + ARM64Reg reg_b, BitSet32 gpr_caller_save, + BitSet32 fpr_caller_save) +{ + const ARM64Reg branch_watch = EncodeRegTo64(reg_a); + MOVP2R(branch_watch, &m_branch_watch); + LDRB(IndexType::Unsigned, reg_b, branch_watch, Core::BranchWatch::GetOffsetOfRecordingActive()); + FixupBranch branch_over = CBZ(reg_b); + + FixupBranch branch_in = B(); + SwitchToFarCode(); + SetJumpTarget(branch_in); + + const ARM64Reg float_emit_tmp = EncodeRegTo64(reg_b); + ABI_PushRegisters(gpr_caller_save); + m_float_emit.ABI_PushRegisters(fpr_caller_save, float_emit_tmp); + ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue : + &Core::BranchWatch::HitPhysicalTrue, + branch_watch, origin, destination, inst.hex); + m_float_emit.ABI_PopRegisters(fpr_caller_save, float_emit_tmp); + ABI_PopRegisters(gpr_caller_save); + + FixupBranch branch_out = B(); + SwitchToNearCode(); + SetJumpTarget(branch_out); + SetJumpTarget(branch_over); +} + void JitArm64::bx(UGeckoInstruction inst) { INSTRUCTION_START @@ -89,6 +154,16 @@ void JitArm64::bx(UGeckoInstruction inst) if (!js.isLastInstruction) { + if (IsDebuggingEnabled()) + { + const ARM64Reg WB = gpr.GetReg(), WC = gpr.GetReg(); + BitSet32 gpr_caller_save = gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WB), DecodeReg(WC)}; + if (WA != ARM64Reg::INVALID_REG && js.op->skipLRStack) + gpr_caller_save[DecodeReg(WA)] = false; + WriteBranchWatch(js.compilerPC, js.op->branchTo, inst, WB, WC, gpr_caller_save, + fpr.GetCallerSavedUsed()); + gpr.Unlock(WB, WC); + } if (inst.LK && !js.op->skipLRStack) { // We have to fake the stack as the RET instruction was not @@ -108,22 +183,37 @@ void JitArm64::bx(UGeckoInstruction inst) if (js.op->branchIsIdleLoop) { - if (WA != ARM64Reg::INVALID_REG) - gpr.Unlock(WA); + if (WA == ARM64Reg::INVALID_REG) + WA = gpr.GetReg(); + + if (IsDebuggingEnabled()) + { + const ARM64Reg WB = gpr.GetReg(); + WriteBranchWatch(js.compilerPC, js.op->branchTo, inst, WA, WB, {}, {}); + gpr.Unlock(WB); + } // make idle loops go faster - ARM64Reg WB = gpr.GetReg(); - ARM64Reg XB = EncodeRegTo64(WB); + ARM64Reg XA = EncodeRegTo64(WA); - MOVP2R(XB, &CoreTiming::GlobalIdle); - BLR(XB); - gpr.Unlock(WB); + MOVP2R(XA, &CoreTiming::GlobalIdle); + BLR(XA); + gpr.Unlock(WA); WriteExceptionExit(js.op->branchTo); return; } - WriteExit(js.op->branchTo, inst.LK, js.compilerPC + 4, inst.LK ? WA : ARM64Reg::INVALID_REG); + if (IsDebuggingEnabled()) + { + const ARM64Reg WB = gpr.GetReg(), WC = gpr.GetReg(); + const BitSet32 gpr_caller_save = + WA != ARM64Reg::INVALID_REG ? BitSet32{DecodeReg(WA)} & CALLER_SAVED_GPRS : BitSet32{}; + WriteBranchWatch(js.compilerPC, js.op->branchTo, inst, WB, WC, gpr_caller_save, {}); + gpr.Unlock(WB, WC); + } + WriteExit(js.op->branchTo, inst.LK, js.compilerPC + 4, WA); + if (WA != ARM64Reg::INVALID_REG) gpr.Unlock(WA); } @@ -134,7 +224,9 @@ void JitArm64::bcx(UGeckoInstruction inst) JITDISABLE(bJITBranchOff); ARM64Reg WA = gpr.GetReg(); - ARM64Reg WB = inst.LK ? gpr.GetReg() : WA; + ARM64Reg WB = inst.LK || IsDebuggingEnabled() ? gpr.GetReg() : WA; + ARM64Reg WC = IsDebuggingEnabled() && inst.LK && !js.op->branchIsIdleLoop ? gpr.GetReg() : + ARM64Reg::INVALID_REG; FixupBranch pCTRDontBranch; if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) // Decrement and test CTR @@ -166,6 +258,19 @@ void JitArm64::bcx(UGeckoInstruction inst) gpr.Flush(FlushMode::MaintainState, WB); fpr.Flush(FlushMode::MaintainState, ARM64Reg::INVALID_REG); + if (IsDebuggingEnabled()) + { + ARM64Reg bw_reg_a, bw_reg_b; + // WC is only allocated when WA is needed for WriteExit and cannot be clobbered. + if (WC == ARM64Reg::INVALID_REG) + bw_reg_a = WA, bw_reg_b = WB; + else + bw_reg_a = WB, bw_reg_b = WC; + const BitSet32 gpr_caller_save = + gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(bw_reg_a), DecodeReg(bw_reg_b)}; + WriteBranchWatch(js.compilerPC, js.op->branchTo, inst, bw_reg_a, bw_reg_b, + gpr_caller_save, fpr.GetCallerSavedUsed()); + } if (js.op->branchIsIdleLoop) { // make idle loops go faster @@ -178,7 +283,7 @@ void JitArm64::bcx(UGeckoInstruction inst) } else { - WriteExit(js.op->branchTo, inst.LK, js.compilerPC + 4, inst.LK ? WA : ARM64Reg::INVALID_REG); + WriteExit(js.op->branchTo, inst.LK, js.compilerPC + 4, WA); } if ((inst.BO & BO_DONT_CHECK_CONDITION) == 0) @@ -186,12 +291,26 @@ void JitArm64::bcx(UGeckoInstruction inst) if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) SetJumpTarget(pCTRDontBranch); + if (WC != ARM64Reg::INVALID_REG) + gpr.Unlock(WC); + if (!analyzer.HasOption(PPCAnalyst::PPCAnalyzer::OPTION_CONDITIONAL_CONTINUE)) { gpr.Flush(FlushMode::All, WA); fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + if (IsDebuggingEnabled()) + { + WriteBranchWatch(js.compilerPC, js.compilerPC + 4, inst, WA, WB, {}, {}); + } WriteExit(js.compilerPC + 4); } + else if (IsDebuggingEnabled()) + { + const BitSet32 gpr_caller_save = + gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WA), DecodeReg(WB)}; + WriteBranchWatch(js.compilerPC, js.compilerPC + 4, inst, WA, WB, gpr_caller_save, + fpr.GetCallerSavedUsed()); + } gpr.Unlock(WA); if (WB != WA) @@ -231,7 +350,17 @@ void JitArm64::bcctrx(UGeckoInstruction inst) LDR(IndexType::Unsigned, WA, PPC_REG, PPCSTATE_OFF_SPR(SPR_CTR)); AND(WA, WA, LogicalImm(~0x3, GPRSize::B32)); - WriteExit(WA, inst.LK_3, js.compilerPC + 4, inst.LK_3 ? WB : ARM64Reg::INVALID_REG); + if (IsDebuggingEnabled()) + { + const ARM64Reg WC = gpr.GetReg(), WD = gpr.GetReg(); + BitSet32 gpr_caller_save = BitSet32{DecodeReg(WA)}; + if (WB != ARM64Reg::INVALID_REG) + gpr_caller_save[DecodeReg(WB)] = true; + gpr_caller_save &= CALLER_SAVED_GPRS; + WriteBranchWatchDestInRegister(js.compilerPC, WA, inst, WC, WD, gpr_caller_save, {}); + gpr.Unlock(WC, WD); + } + WriteExit(WA, inst.LK_3, js.compilerPC + 4, WB); if (WB != ARM64Reg::INVALID_REG) gpr.Unlock(WB); @@ -247,7 +376,9 @@ void JitArm64::bclrx(UGeckoInstruction inst) (inst.BO & BO_DONT_DECREMENT_FLAG) == 0 || (inst.BO & BO_DONT_CHECK_CONDITION) == 0; ARM64Reg WA = gpr.GetReg(); - ARM64Reg WB = conditional || inst.LK ? gpr.GetReg() : ARM64Reg::INVALID_REG; + ARM64Reg WB = + conditional || inst.LK || IsDebuggingEnabled() ? gpr.GetReg() : ARM64Reg::INVALID_REG; + ARM64Reg WC = IsDebuggingEnabled() ? gpr.GetReg() : ARM64Reg::INVALID_REG; FixupBranch pCTRDontBranch; if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) // Decrement and test CTR @@ -281,6 +412,26 @@ void JitArm64::bclrx(UGeckoInstruction inst) gpr.Flush(conditional ? FlushMode::MaintainState : FlushMode::All, WB); fpr.Flush(conditional ? FlushMode::MaintainState : FlushMode::All, ARM64Reg::INVALID_REG); + if (IsDebuggingEnabled()) + { + BitSet32 gpr_caller_save; + BitSet32 fpr_caller_save; + if (conditional) + { + gpr_caller_save = gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WB), DecodeReg(WC)}; + if (js.op->branchIsIdleLoop) + gpr_caller_save[DecodeReg(WA)] = false; + fpr_caller_save = fpr.GetCallerSavedUsed(); + } + else + { + gpr_caller_save = + js.op->branchIsIdleLoop ? BitSet32{} : BitSet32{DecodeReg(WA)} & CALLER_SAVED_GPRS; + fpr_caller_save = {}; + } + WriteBranchWatchDestInRegister(js.compilerPC, WA, inst, WB, WC, gpr_caller_save, + fpr_caller_save); + } if (js.op->branchIsIdleLoop) { // make idle loops go faster @@ -301,12 +452,26 @@ void JitArm64::bclrx(UGeckoInstruction inst) if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) SetJumpTarget(pCTRDontBranch); + if (WC != ARM64Reg::INVALID_REG) + gpr.Unlock(WC); + if (!analyzer.HasOption(PPCAnalyst::PPCAnalyzer::OPTION_CONDITIONAL_CONTINUE)) { gpr.Flush(FlushMode::All, WA); fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG); + if (IsDebuggingEnabled()) + { + WriteBranchWatch(js.compilerPC, js.compilerPC + 4, inst, WA, WB, {}, {}); + } WriteExit(js.compilerPC + 4); } + else if (IsDebuggingEnabled()) + { + const BitSet32 gpr_caller_save = + gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WA), DecodeReg(WB)}; + WriteBranchWatch(js.compilerPC, js.compilerPC + 4, inst, WA, WB, gpr_caller_save, + fpr.GetCallerSavedUsed()); + } gpr.Unlock(WA); if (WB != ARM64Reg::INVALID_REG) diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp index 17e2171b8c..1eae3d923d 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp @@ -13,6 +13,7 @@ #include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/CoreTiming.h" +#include "Core/Debugger/BranchWatch.h" #include "Core/HW/DSP.h" #include "Core/HW/MMIO.h" #include "Core/HW/Memmap.h" @@ -769,18 +770,15 @@ void JitArm64::dcbx(UGeckoInstruction inst) js.op[1].inst.RA_6 == b && js.op[1].inst.RD_2 == b && js.op[2].inst.hex == 0x4200fff8; - gpr.Lock(ARM64Reg::W0, ARM64Reg::W1); - if (make_loop) - gpr.Lock(ARM64Reg::W2); + constexpr ARM64Reg WA = ARM64Reg::W0, WB = ARM64Reg::W1, loop_counter = ARM64Reg::W2; + // Be careful, loop_counter is only locked when make_loop == true. + gpr.Lock(WA, WB); - ARM64Reg WA = ARM64Reg::W0; - - if (make_loop) - gpr.BindToRegister(b, true); - - ARM64Reg loop_counter = ARM64Reg::INVALID_REG; if (make_loop) { + gpr.Lock(loop_counter); + gpr.BindToRegister(b, true); + // We'll execute somewhere between one single cacheline invalidation and however many are needed // to reduce the downcount to zero, never exceeding the amount requested by the game. // To stay consistent with the rest of the code we adjust the involved registers (CTR and Rb) @@ -788,10 +786,8 @@ void JitArm64::dcbx(UGeckoInstruction inst) // bdnz afterwards! So if we invalidate a single cache line, we don't adjust the registers at // all, if we invalidate 2 cachelines we adjust the registers by one step, and so on. - ARM64Reg reg_cycle_count = gpr.GetReg(); - ARM64Reg reg_downcount = gpr.GetReg(); - loop_counter = ARM64Reg::W2; - ARM64Reg WB = ARM64Reg::W1; + const ARM64Reg reg_cycle_count = gpr.GetReg(); + const ARM64Reg reg_downcount = gpr.GetReg(); // Figure out how many loops we want to do. const u8 cycle_count_per_loop = @@ -828,11 +824,43 @@ void JitArm64::dcbx(UGeckoInstruction inst) // Load the loop_counter register with the amount of invalidations to execute. ADD(loop_counter, WA, 1); + if (IsDebuggingEnabled()) + { + const ARM64Reg branch_watch = EncodeRegTo64(reg_cycle_count); + MOVP2R(branch_watch, &m_branch_watch); + LDRB(IndexType::Unsigned, WB, branch_watch, Core::BranchWatch::GetOffsetOfRecordingActive()); + FixupBranch branch_over = CBZ(WB); + + FixupBranch branch_in = B(); + SwitchToFarCode(); + SetJumpTarget(branch_in); + + const BitSet32 gpr_caller_save = + gpr.GetCallerSavedUsed() & + ~BitSet32{DecodeReg(WB), DecodeReg(reg_cycle_count), DecodeReg(reg_downcount)}; + ABI_PushRegisters(gpr_caller_save); + const ARM64Reg float_emit_tmp = EncodeRegTo64(WB); + const BitSet32 fpr_caller_save = fpr.GetCallerSavedUsed(); + m_float_emit.ABI_PushRegisters(fpr_caller_save, float_emit_tmp); + const PPCAnalyst::CodeOp& op = js.op[2]; + ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue_fk_n : + &Core::BranchWatch::HitPhysicalTrue_fk_n, + branch_watch, Core::FakeBranchWatchCollectionKey{op.address, op.branchTo}, + op.inst.hex, WA); + m_float_emit.ABI_PopRegisters(fpr_caller_save, float_emit_tmp); + ABI_PopRegisters(gpr_caller_save); + + FixupBranch branch_out = B(); + SwitchToNearCode(); + SetJumpTarget(branch_out); + SetJumpTarget(branch_over); + } + gpr.Unlock(reg_cycle_count, reg_downcount); } - ARM64Reg effective_addr = ARM64Reg::W1; - ARM64Reg physical_addr = gpr.GetReg(); + constexpr ARM64Reg effective_addr = WB; + const ARM64Reg physical_addr = gpr.GetReg(); if (a) ADD(effective_addr, gpr.R(a), gpr.R(b)); @@ -911,7 +939,7 @@ void JitArm64::dcbx(UGeckoInstruction inst) SwitchToNearCode(); SetJumpTarget(near_addr); - gpr.Unlock(effective_addr, physical_addr, WA); + gpr.Unlock(WA, WB, physical_addr); if (make_loop) gpr.Unlock(loop_counter); } From 8134c8a57211f54cf2109ae3c068c0c94370f786 Mon Sep 17 00:00:00 2001 From: mitaclaw <140017135+mitaclaw@users.noreply.github.com> Date: Thu, 7 Dec 2023 09:36:02 -0800 Subject: [PATCH 5/5] BranchWatchDialog: A Total Replacement for CodeDiffDialog With a purpose-built Branch Watch feature built into the emulated system: BranchWatchDialog, replacing CodeDiffDialog, is now better than ever! --- Source/Core/Common/CommonPaths.h | 2 + Source/Core/Common/FileUtil.cpp | 6 + Source/Core/Common/FileUtil.h | 2 + Source/Core/Core/Boot/Boot_BS2Emu.cpp | 8 + Source/Core/Core/System.h | 3 + Source/Core/DolphinQt/CMakeLists.txt | 6 +- .../DolphinQt/Debugger/BranchWatchDialog.cpp | 1014 +++++++++++++++++ .../DolphinQt/Debugger/BranchWatchDialog.h | 114 ++ .../Debugger/BranchWatchTableModel.cpp | 502 ++++++++ .../Debugger/BranchWatchTableModel.h | 119 ++ .../DolphinQt/Debugger/CodeDiffDialog.cpp | 673 ----------- .../Core/DolphinQt/Debugger/CodeDiffDialog.h | 86 -- Source/Core/DolphinQt/Debugger/CodeWidget.cpp | 31 +- Source/Core/DolphinQt/Debugger/CodeWidget.h | 8 +- Source/Core/DolphinQt/DolphinQt.vcxproj | 6 +- Source/Core/UICommon/UICommon.cpp | 4 + 16 files changed, 1805 insertions(+), 779 deletions(-) create mode 100644 Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp create mode 100644 Source/Core/DolphinQt/Debugger/BranchWatchDialog.h create mode 100644 Source/Core/DolphinQt/Debugger/BranchWatchTableModel.cpp create mode 100644 Source/Core/DolphinQt/Debugger/BranchWatchTableModel.h delete mode 100644 Source/Core/DolphinQt/Debugger/CodeDiffDialog.cpp delete mode 100644 Source/Core/DolphinQt/Debugger/CodeDiffDialog.h diff --git a/Source/Core/Common/CommonPaths.h b/Source/Core/Common/CommonPaths.h index bf30fb98a9..1dce5c47f5 100644 --- a/Source/Core/Common/CommonPaths.h +++ b/Source/Core/Common/CommonPaths.h @@ -75,6 +75,8 @@ #define DUMP_AUDIO_DIR "Audio" #define DUMP_DSP_DIR "DSP" #define DUMP_SSL_DIR "SSL" +#define DUMP_DEBUG_DIR "Debug" +#define DUMP_DEBUG_BRANCHWATCH_DIR "BranchWatch" #define LOGS_DIR "Logs" #define MAIL_LOGS_DIR "Mail" #define SHADERS_DIR "Shaders" diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index 562e66ff56..f7a8555875 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -856,6 +856,9 @@ static void RebuildUserDirectories(unsigned int dir_index) s_user_paths[D_DUMPTEXTURES_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_TEXTURES_DIR DIR_SEP; s_user_paths[D_DUMPDSP_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_DSP_DIR DIR_SEP; s_user_paths[D_DUMPSSL_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_SSL_DIR DIR_SEP; + s_user_paths[D_DUMPDEBUG_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_DEBUG_DIR DIR_SEP; + s_user_paths[D_DUMPDEBUG_BRANCHWATCH_IDX] = + s_user_paths[D_DUMPDEBUG_IDX] + DUMP_DEBUG_BRANCHWATCH_DIR DIR_SEP; s_user_paths[D_LOGS_IDX] = s_user_paths[D_USER_IDX] + LOGS_DIR DIR_SEP; s_user_paths[D_MAILLOGS_IDX] = s_user_paths[D_LOGS_IDX] + MAIL_LOGS_DIR DIR_SEP; s_user_paths[D_THEMES_IDX] = s_user_paths[D_USER_IDX] + THEMES_DIR DIR_SEP; @@ -932,6 +935,9 @@ static void RebuildUserDirectories(unsigned int dir_index) s_user_paths[D_DUMPTEXTURES_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_TEXTURES_DIR DIR_SEP; s_user_paths[D_DUMPDSP_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_DSP_DIR DIR_SEP; s_user_paths[D_DUMPSSL_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_SSL_DIR DIR_SEP; + s_user_paths[D_DUMPDEBUG_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_DEBUG_DIR DIR_SEP; + s_user_paths[D_DUMPDEBUG_BRANCHWATCH_IDX] = + s_user_paths[D_DUMP_IDX] + DUMP_DEBUG_BRANCHWATCH_DIR DIR_SEP; s_user_paths[F_MEM1DUMP_IDX] = s_user_paths[D_DUMP_IDX] + MEM1_DUMP; s_user_paths[F_MEM2DUMP_IDX] = s_user_paths[D_DUMP_IDX] + MEM2_DUMP; s_user_paths[F_ARAMDUMP_IDX] = s_user_paths[D_DUMP_IDX] + ARAM_DUMP; diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h index 94491cecfd..975ab55256 100644 --- a/Source/Core/Common/FileUtil.h +++ b/Source/Core/Common/FileUtil.h @@ -52,6 +52,8 @@ enum D_DUMPTEXTURES_IDX, D_DUMPDSP_IDX, D_DUMPSSL_IDX, + D_DUMPDEBUG_IDX, + D_DUMPDEBUG_BRANCHWATCH_IDX, D_LOAD_IDX, D_LOGS_IDX, D_MAILLOGS_IDX, diff --git a/Source/Core/Core/Boot/Boot_BS2Emu.cpp b/Source/Core/Core/Boot/Boot_BS2Emu.cpp index 09f9e5128a..d36b192bb5 100644 --- a/Source/Core/Core/Boot/Boot_BS2Emu.cpp +++ b/Source/Core/Core/Boot/Boot_BS2Emu.cpp @@ -19,6 +19,7 @@ #include "Core/Config/MainSettings.h" #include "Core/ConfigManager.h" #include "Core/Core.h" +#include "Core/Debugger/BranchWatch.h" #include "Core/HLE/HLE.h" #include "Core/HW/DVD/DVDInterface.h" #include "Core/HW/EXI/EXI_DeviceIPL.h" @@ -158,6 +159,11 @@ bool CBoot::RunApploader(Core::System& system, const Core::CPUThreadGuard& guard auto& ppc_state = system.GetPPCState(); auto& mmu = system.GetMMU(); + auto& branch_watch = system.GetPowerPC().GetBranchWatch(); + + const bool resume_branch_watch = branch_watch.GetRecordingActive(); + if (system.IsBranchWatchIgnoreApploader()) + branch_watch.Pause(); // Call iAppLoaderEntry. DEBUG_LOG_FMT(BOOT, "Call iAppLoaderEntry"); @@ -220,6 +226,8 @@ bool CBoot::RunApploader(Core::System& system, const Core::CPUThreadGuard& guard // return ppc_state.pc = ppc_state.gpr[3]; + branch_watch.SetRecordingActive(resume_branch_watch); + return true; } diff --git a/Source/Core/Core/System.h b/Source/Core/Core/System.h index fe60eabf12..acaf2daad7 100644 --- a/Source/Core/Core/System.h +++ b/Source/Core/Core/System.h @@ -141,9 +141,11 @@ public: bool IsPauseOnPanicMode() const { return m_pause_on_panic_enabled; } bool IsMIOS() const { return m_is_mios; } bool IsWii() const { return m_is_wii; } + bool IsBranchWatchIgnoreApploader() { return m_branch_watch_ignore_apploader; } void SetIsMIOS(bool is_mios) { m_is_mios = is_mios; } void SetIsWii(bool is_wii) { m_is_wii = is_wii; } + void SetIsBranchWatchIgnoreApploader(bool enable) { m_branch_watch_ignore_apploader = enable; } SoundStream* GetSoundStream() const; void SetSoundStream(std::unique_ptr sound_stream); @@ -202,5 +204,6 @@ private: bool m_pause_on_panic_enabled = false; bool m_is_mios = false; bool m_is_wii = false; + bool m_branch_watch_ignore_apploader = false; }; } // namespace Core diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 8285c17d4a..437323fc74 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -206,12 +206,14 @@ add_executable(dolphin-emu Debugger/AssemblerWidget.h Debugger/AssemblyEditor.cpp Debugger/AssemblyEditor.h + Debugger/BranchWatchDialog.cpp + Debugger/BranchWatchDialog.h + Debugger/BranchWatchTableModel.cpp + Debugger/BranchWatchTableModel.h Debugger/BreakpointDialog.cpp Debugger/BreakpointDialog.h Debugger/BreakpointWidget.cpp Debugger/BreakpointWidget.h - Debugger/CodeDiffDialog.cpp - Debugger/CodeDiffDialog.h Debugger/CodeViewWidget.cpp Debugger/CodeViewWidget.h Debugger/CodeWidget.cpp diff --git a/Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp b/Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp new file mode 100644 index 0000000000..f5e6a5acc3 --- /dev/null +++ b/Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp @@ -0,0 +1,1014 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/Debugger/BranchWatchDialog.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common/Assert.h" +#include "Common/CommonFuncs.h" +#include "Common/CommonTypes.h" +#include "Common/FileUtil.h" +#include "Common/IOFile.h" +#include "Core/ConfigManager.h" +#include "Core/Core.h" +#include "Core/Debugger/BranchWatch.h" +#include "Core/Debugger/PPCDebugInterface.h" +#include "Core/PowerPC/Gekko.h" +#include "Core/PowerPC/PowerPC.h" +#include "Core/System.h" +#include "DolphinQt/Debugger/BranchWatchTableModel.h" +#include "DolphinQt/Debugger/CodeWidget.h" +#include "DolphinQt/QtUtils/DolphinFileDialog.h" +#include "DolphinQt/QtUtils/ModalMessageBox.h" +#include "DolphinQt/QtUtils/SetWindowDecorations.h" +#include "DolphinQt/Settings.h" + +class BranchWatchProxyModel final : public QSortFilterProxyModel +{ + friend BranchWatchDialog; + +public: + explicit BranchWatchProxyModel(const Core::BranchWatch& branch_watch, QObject* parent = nullptr) + : QSortFilterProxyModel(parent), m_branch_watch(branch_watch) + { + } + + BranchWatchTableModel* sourceModel() const + { + return static_cast(QSortFilterProxyModel::sourceModel()); + } + void setSourceModel(BranchWatchTableModel* source_model) + { + QSortFilterProxyModel::setSourceModel(source_model); + } + + // Virtual setSourceModel is forbidden for type-safety reasons. See sourceModel(). + [[noreturn]] void setSourceModel(QAbstractItemModel* source_model) override { Crash(); } + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; + + template + void OnToggled(bool enabled) + { + this->*member = enabled; + invalidateRowsFilter(); + } + template + void OnSymbolTextChanged(const QString& text) + { + this->*member = text; + invalidateRowsFilter(); + } + template BranchWatchProxyModel::*member> + void 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(); + } + void OnDelete(QModelIndexList index_list); + + bool IsBranchTypeAllowed(UGeckoInstruction inst) const; + void SetInspected(const QModelIndex& index); + +private: + const Core::BranchWatch& m_branch_watch; + + QString m_origin_symbol_name = {}, m_destin_symbol_name = {}; + std::optional m_origin_min, m_origin_max, m_destin_min, m_destin_max; + bool m_b = {}, m_bl = {}, m_bc = {}, m_bcl = {}, m_blr = {}, m_blrl = {}, m_bclr = {}, + m_bclrl = {}, m_bctr = {}, m_bctrl = {}, m_bcctr = {}, m_bcctrl = {}; + bool m_cond_true = {}, m_cond_false = {}; +}; + +bool BranchWatchProxyModel::filterAcceptsRow(int source_row, const QModelIndex&) const +{ + const Core::BranchWatch::Selection::value_type& value = m_branch_watch.GetSelection()[source_row]; + if (value.condition) + { + if (!m_cond_true) + return false; + } + else if (!m_cond_false) + return false; + + const Core::BranchWatchCollectionKey& k = value.collection_ptr->first; + if (!IsBranchTypeAllowed(k.original_inst)) + return false; + + if (m_origin_min.has_value() && k.origin_addr < m_origin_min.value()) + return false; + if (m_origin_max.has_value() && k.origin_addr > m_origin_max.value()) + return false; + if (m_destin_min.has_value() && k.destin_addr < m_destin_min.value()) + return false; + if (m_destin_max.has_value() && k.destin_addr > m_destin_max.value()) + return false; + + if (!m_origin_symbol_name.isEmpty()) + { + if (const QVariant& symbol_name_v = sourceModel()->GetSymbolList()[source_row].origin_name; + !symbol_name_v.isValid() || + !symbol_name_v.value().contains(m_origin_symbol_name, Qt::CaseInsensitive)) + return false; + } + if (!m_destin_symbol_name.isEmpty()) + { + if (const QVariant& symbol_name_v = sourceModel()->GetSymbolList()[source_row].destin_name; + !symbol_name_v.isValid() || + !symbol_name_v.value().contains(m_destin_symbol_name, Qt::CaseInsensitive)) + return false; + } + return true; +} + +void BranchWatchProxyModel::OnDelete(QModelIndexList index_list) +{ + std::transform(index_list.begin(), index_list.end(), index_list.begin(), + [this](const QModelIndex& index) { return mapToSource(index); }); + sourceModel()->OnDelete(std::move(index_list)); +} + +static constexpr bool BranchSavesLR(UGeckoInstruction inst) +{ + DEBUG_ASSERT(inst.OPCD == 18 || inst.OPCD == 16 || + (inst.OPCD == 19 && (inst.SUBOP10 == 16 || inst.SUBOP10 == 528))); + // Every branch instruction uses the same LK field. + return inst.LK; +} + +bool BranchWatchProxyModel::IsBranchTypeAllowed(UGeckoInstruction inst) const +{ + const bool lr_saved = BranchSavesLR(inst); + switch (inst.OPCD) + { + case 18: + return lr_saved ? m_bl : m_b; + case 16: + return lr_saved ? m_bcl : m_bc; + case 19: + switch (inst.SUBOP10) + { + case 16: + if ((inst.BO & 0b10100) == 0b10100) // 1z1zz - Branch always + return lr_saved ? m_blrl : m_blr; + return lr_saved ? m_bclrl : m_bclr; + case 528: + if ((inst.BO & 0b10100) == 0b10100) // 1z1zz - Branch always + return lr_saved ? m_bctrl : m_bctr; + return lr_saved ? m_bcctrl : m_bcctr; + } + } + return false; +} + +void BranchWatchProxyModel::SetInspected(const QModelIndex& index) +{ + sourceModel()->SetInspected(mapToSource(index)); +} + +BranchWatchDialog::BranchWatchDialog(Core::System& system, Core::BranchWatch& branch_watch, + CodeWidget* code_widget, QWidget* parent) + : QDialog(parent), m_system(system), m_branch_watch(branch_watch), m_code_widget(code_widget) +{ + setWindowTitle(tr("Branch Watch Tool")); + setWindowFlags((windowFlags() | Qt::WindowMinMaxButtonsHint) & ~Qt::WindowContextHelpButtonHint); + SetQWidgetWindowDecorations(this); + setLayout([this]() { + auto* layout = new QVBoxLayout; + + // Controls Toolbar (widgets are added later) + layout->addWidget(m_control_toolbar = new QToolBar); + + // Branch Watch Table + layout->addWidget(m_table_view = [this]() { + const auto& ui_settings = Settings::Instance(); + + m_table_proxy = new BranchWatchProxyModel(m_branch_watch); + m_table_proxy->setSourceModel(m_table_model = + new BranchWatchTableModel(m_system, m_branch_watch)); + m_table_proxy->setSortRole(UserRole::SortRole); + + m_table_model->setFont(ui_settings.GetDebugFont()); + connect(&ui_settings, &Settings::DebugFontChanged, m_table_model, + &BranchWatchTableModel::setFont); + + auto* const table_view = new QTableView; + table_view->setModel(m_table_proxy); + table_view->setSortingEnabled(true); + table_view->sortByColumn(Column::Origin, Qt::AscendingOrder); + table_view->setSelectionMode(QAbstractItemView::ExtendedSelection); + table_view->setSelectionBehavior(QAbstractItemView::SelectRows); + table_view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + table_view->setContextMenuPolicy(Qt::CustomContextMenu); + table_view->setEditTriggers(QAbstractItemView::NoEditTriggers); + table_view->setCornerButtonEnabled(false); + table_view->verticalHeader()->hide(); + + QHeaderView* const horizontal_header = table_view->horizontalHeader(); + horizontal_header->restoreState( // Restore column visibility state. + Settings::GetQSettings() + .value(QStringLiteral("branchwatchdialog/tableheader/state")) + .toByteArray()); + horizontal_header->setContextMenuPolicy(Qt::CustomContextMenu); + horizontal_header->setStretchLastSection(true); + horizontal_header->setSectionsMovable(true); + horizontal_header->setFirstSectionMovable(true); + + connect(table_view, &QTableView::clicked, this, &BranchWatchDialog::OnTableClicked); + connect(table_view, &QTableView::customContextMenuRequested, this, + &BranchWatchDialog::OnTableContextMenu); + connect(horizontal_header, &QHeaderView::customContextMenuRequested, this, + &BranchWatchDialog::OnTableHeaderContextMenu); + connect(new QShortcut(QKeySequence(Qt::Key_Delete), this), &QShortcut::activated, this, + &BranchWatchDialog::OnTableDeleteKeypress); + + return table_view; + }()); + + m_mnu_column_visibility = [this]() { + static constexpr std::array headers = { + QT_TR_NOOP("Instruction"), QT_TR_NOOP("Condition"), QT_TR_NOOP("Origin"), + QT_TR_NOOP("Destination"), QT_TR_NOOP("Recent Hits"), QT_TR_NOOP("Total Hits"), + QT_TR_NOOP("Origin Symbol"), QT_TR_NOOP("Destination Symbol")}; + + auto* const menu = new QMenu(); + for (int column = 0; column < Column::NumberOfColumns; ++column) + { + QAction* action = 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); + } + return menu; + }(); + + // Menu Bar + layout->setMenuBar([this]() { + QMenuBar* const menu_bar = new QMenuBar; + menu_bar->setNativeMenuBar(false); + + QMenu* const menu_file = new QMenu(tr("&File"), menu_bar); + menu_file->addAction(tr("&Save Branch Watch"), this, &BranchWatchDialog::OnSave); + menu_file->addAction(tr("Save Branch Watch &As..."), this, &BranchWatchDialog::OnSaveAs); + menu_file->addAction(tr("&Load Branch Watch"), this, &BranchWatchDialog::OnLoad); + menu_file->addAction(tr("Load Branch Watch &From..."), this, &BranchWatchDialog::OnLoadFrom); + m_act_autosave = menu_file->addAction(tr("A&uto Save")); + m_act_autosave->setCheckable(true); + connect(m_act_autosave, &QAction::toggled, this, &BranchWatchDialog::OnToggleAutoSave); + menu_bar->addMenu(menu_file); + + QMenu* const menu_tool = new QMenu(tr("&Tool"), menu_bar); + menu_tool->setToolTipsVisible(true); + menu_tool->addAction(tr("Hide &Controls"), this, &BranchWatchDialog::OnHideShowControls) + ->setCheckable(true); + QAction* const act_ignore_apploader = + menu_tool->addAction(tr("Ignore &Apploader Branch Hits")); + act_ignore_apploader->setToolTip( + tr("This only applies to the initial boot of the emulated software.")); + act_ignore_apploader->setChecked(m_system.IsBranchWatchIgnoreApploader()); + act_ignore_apploader->setCheckable(true); + connect(act_ignore_apploader, &QAction::toggled, this, + &BranchWatchDialog::OnToggleIgnoreApploader); + + menu_tool->addMenu(m_mnu_column_visibility)->setText(tr("Column &Visibility")); + menu_tool->addAction(tr("Wipe &Inspection Data"), this, &BranchWatchDialog::OnWipeInspection); + menu_tool->addAction(tr("&Help"), this, &BranchWatchDialog::OnHelp); + + menu_bar->addMenu(menu_tool); + + return menu_bar; + }()); + + // Status Bar + layout->addWidget(m_status_bar = []() { + auto* const status_bar = new QStatusBar; + status_bar->setSizeGripEnabled(false); + return status_bar; + }()); + + // Tool Controls + m_control_toolbar->addWidget([this]() { + auto* const layout = new QGridLayout; + + layout->addWidget(m_btn_start_pause = new QPushButton(tr("Start Branch Watch")), 0, 0); + connect(m_btn_start_pause, &QPushButton::toggled, this, &BranchWatchDialog::OnStartPause); + m_btn_start_pause->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + m_btn_start_pause->setCheckable(true); + + layout->addWidget(m_btn_clear_watch = new QPushButton(tr("Clear Branch Watch")), 1, 0); + connect(m_btn_clear_watch, &QPushButton::pressed, this, + &BranchWatchDialog::OnClearBranchWatch); + m_btn_clear_watch->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + + layout->addWidget(m_btn_path_was_taken = new QPushButton(tr("Code Path Was Taken")), 0, 1); + connect(m_btn_path_was_taken, &QPushButton::pressed, this, + &BranchWatchDialog::OnCodePathWasTaken); + m_btn_path_was_taken->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + + layout->addWidget(m_btn_path_not_taken = new QPushButton(tr("Code Path Not Taken")), 1, 1); + connect(m_btn_path_not_taken, &QPushButton::pressed, this, + &BranchWatchDialog::OnCodePathNotTaken); + m_btn_path_not_taken->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + + auto* const group_box = new QGroupBox(tr("Tool Controls")); + group_box->setLayout(layout); + group_box->setAlignment(Qt::AlignHCenter); + + return group_box; + }()); + + // Spacer + m_control_toolbar->addWidget([]() { + auto* const widget = new QWidget; + widget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + return widget; + }()); + + // Branch Type Filter Options + m_control_toolbar->addWidget([this]() { + auto* const layout = new QGridLayout; + + const auto routine = [this, layout](const QString& text, const QString& tooltip, int row, + int column, void (BranchWatchProxyModel::*slot)(bool)) { + QCheckBox* const check_box = new QCheckBox(text); + check_box->setToolTip(tooltip); + layout->addWidget(check_box, row, column); + connect(check_box, &QCheckBox::toggled, [this, slot](bool checked) { + (m_table_proxy->*slot)(checked); + UpdateStatus(); + }); + check_box->setChecked(true); + }; + + // clang-format off + routine(QStringLiteral("b" ), tr("Branch" ), 0, 0, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_b >); + routine(QStringLiteral("bl" ), tr("Branch (LR saved)" ), 0, 1, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bl >); + routine(QStringLiteral("bc" ), tr("Branch Conditional" ), 0, 2, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bc >); + routine(QStringLiteral("bcl" ), tr("Branch Conditional (LR saved)" ), 0, 3, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bcl >); + routine(QStringLiteral("blr" ), tr("Branch to Link Register" ), 1, 0, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_blr >); + routine(QStringLiteral("blrl" ), tr("Branch to Link Register (LR saved)" ), 1, 1, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_blrl >); + routine(QStringLiteral("bclr" ), tr("Branch Conditional to Link Register" ), 1, 2, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bclr >); + routine(QStringLiteral("bclrl" ), tr("Branch Conditional to Link Register (LR saved)" ), 1, 3, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bclrl >); + routine(QStringLiteral("bctr" ), tr("Branch to Count Register" ), 2, 0, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bctr >); + routine(QStringLiteral("bctrl" ), tr("Branch to Count Register (LR saved)" ), 2, 1, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bctrl >); + routine(QStringLiteral("bcctr" ), tr("Branch Conditional to Count Register" ), 2, 2, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bcctr >); + routine(QStringLiteral("bcctrl"), tr("Branch Conditional to Count Register (LR saved)"), 2, 3, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bcctrl>); + // clang-format on + + auto* const group_box = new QGroupBox(tr("Branch Type")); + group_box->setLayout(layout); + group_box->setAlignment(Qt::AlignHCenter); + + return group_box; + }()); + + // Origin and Destination Filter Options + m_control_toolbar->addWidget([this]() { + auto* const layout = new QGridLayout; + + const auto routine = [this, layout](const QString& text, int row, int column, int width, + void (BranchWatchProxyModel::*slot)(const QString&)) { + QLineEdit* const line_edit = new QLineEdit; + layout->addWidget(line_edit, row, column, 1, width); + connect(line_edit, &QLineEdit::textChanged, [this, slot](const QString& text) { + (m_table_proxy->*slot)(text); + UpdateStatus(); + }); + line_edit->setPlaceholderText(text); + return line_edit; + }; + + // clang-format off + routine(tr("Origin Symbol" ), 0, 0, 1, &BranchWatchProxyModel::OnSymbolTextChanged<&BranchWatchProxyModel::m_origin_symbol_name>); + routine(tr("Origin Min" ), 1, 0, 1, &BranchWatchProxyModel::OnAddressTextChanged<&BranchWatchProxyModel::m_origin_min>)->setMaxLength(8); + routine(tr("Origin Max" ), 2, 0, 1, &BranchWatchProxyModel::OnAddressTextChanged<&BranchWatchProxyModel::m_origin_max>)->setMaxLength(8); + routine(tr("Destination Symbol"), 0, 1, 1, &BranchWatchProxyModel::OnSymbolTextChanged<&BranchWatchProxyModel::m_destin_symbol_name>); + routine(tr("Destination Min" ), 1, 1, 1, &BranchWatchProxyModel::OnAddressTextChanged<&BranchWatchProxyModel::m_destin_min>)->setMaxLength(8); + routine(tr("Destination Max" ), 2, 1, 1, &BranchWatchProxyModel::OnAddressTextChanged<&BranchWatchProxyModel::m_destin_max>)->setMaxLength(8); + // clang-format on + + auto* const group_box = new QGroupBox(tr("Origin and Destination")); + group_box->setLayout(layout); + group_box->setAlignment(Qt::AlignHCenter); + + return group_box; + }()); + + // Condition Filter Options + m_control_toolbar->addWidget([this]() { + auto* const layout = new QVBoxLayout; + layout->setAlignment(Qt::AlignHCenter); + + const auto routine = [this, layout](const QString& text, + void (BranchWatchProxyModel::*slot)(bool)) { + QCheckBox* const check_box = new QCheckBox(text); + layout->addWidget(check_box); + connect(check_box, &QCheckBox::toggled, [this, slot](bool checked) { + (m_table_proxy->*slot)(checked); + UpdateStatus(); + }); + check_box->setChecked(true); + return check_box; + }; + + routine(QStringLiteral("true"), + &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_cond_true>) + ->setToolTip(tr("This will also filter unconditional branches.\n" + "To filter for or against unconditional branches,\n" + "use the Branch Type filter options.")); + routine(QStringLiteral("false"), + &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_cond_false>); + + auto* const group_box = new QGroupBox(tr("Condition")); + group_box->setLayout(layout); + group_box->setAlignment(Qt::AlignHCenter); + + return group_box; + }()); + + // Misc. Controls + m_control_toolbar->addWidget([this]() { + auto* const layout = new QVBoxLayout; + + layout->addWidget(m_btn_was_overwritten = new QPushButton(tr("Branch Was Overwritten"))); + connect(m_btn_was_overwritten, &QPushButton::pressed, this, + &BranchWatchDialog::OnBranchWasOverwritten); + m_btn_was_overwritten->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + + layout->addWidget(m_btn_not_overwritten = new QPushButton(tr("Branch Not Overwritten"))); + connect(m_btn_not_overwritten, &QPushButton::pressed, this, + &BranchWatchDialog::OnBranchNotOverwritten); + m_btn_not_overwritten->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + + layout->addWidget(m_btn_wipe_recent_hits = new QPushButton(tr("Wipe Recent Hits"))); + connect(m_btn_wipe_recent_hits, &QPushButton::pressed, this, + &BranchWatchDialog::OnWipeRecentHits); + m_btn_wipe_recent_hits->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + m_btn_wipe_recent_hits->setEnabled(false); + + auto* const group_box = new QGroupBox(tr("Misc. Controls")); + group_box->setLayout(layout); + group_box->setAlignment(Qt::AlignHCenter); + + return group_box; + }()); + + connect(m_timer = new QTimer, &QTimer::timeout, this, &BranchWatchDialog::OnTimeout); + connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, + &BranchWatchDialog::OnEmulationStateChanged); + connect(m_table_proxy, &BranchWatchProxyModel::layoutChanged, this, + &BranchWatchDialog::UpdateStatus); + + return layout; + }()); + + // FIXME: On Linux, Qt6 has recently been resetting column widths to their defaults in many + // unexpected ways. This affects all kinds of QTables in Dolphin's GUI, so to avoid it in + // this QTableView, I have deferred this operation. Any earlier, and this would be undone. + // SetQWidgetWindowDecorations was moved to before these operations for the same reason. + m_table_view->setColumnWidth(Column::Instruction, 50); + m_table_view->setColumnWidth(Column::Condition, 50); + m_table_view->setColumnWidth(Column::OriginSymbol, 250); + m_table_view->setColumnWidth(Column::DestinSymbol, 250); + // The default column width (100 units) is fine for the rest. + + const auto& settings = Settings::GetQSettings(); + restoreGeometry(settings.value(QStringLiteral("branchwatchdialog/geometry")).toByteArray()); +} + +void BranchWatchDialog::done(int r) +{ + if (m_timer->isActive()) + m_timer->stop(); + auto& settings = Settings::GetQSettings(); + settings.setValue(QStringLiteral("branchwatchdialog/geometry"), saveGeometry()); + settings.setValue(QStringLiteral("branchwatchdialog/tableheader/state"), + m_table_view->horizontalHeader()->saveState()); + QDialog::done(r); +} + +static constexpr int BRANCH_WATCH_TOOL_TIMER_DELAY_MS = 100; +static constexpr int BRANCH_WATCH_TOOL_TIMER_PAUSE_ONESHOT_MS = 200; + +static bool TimerCondition(const Core::BranchWatch& branch_watch, Core::State state) +{ + return branch_watch.GetRecordingActive() && state > Core::State::Paused; +} + +int BranchWatchDialog::exec() +{ + if (TimerCondition(m_branch_watch, Core::GetState())) + m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS); + return QDialog::exec(); +} + +void BranchWatchDialog::open() +{ + if (TimerCondition(m_branch_watch, Core::GetState())) + m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS); + QDialog::open(); +} + +void BranchWatchDialog::OnStartPause(bool checked) +{ + if (checked) + { + m_branch_watch.Start(); + m_btn_start_pause->setText(tr("Pause Branch Watch")); + // Restart the timer if the situation calls for it, but always turn off single-shot. + m_timer->setSingleShot(false); + if (Core::GetState() > Core::State::Paused) + m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS); + } + else + { + m_branch_watch.Pause(); + m_btn_start_pause->setText(tr("Start Branch Watch")); + // Schedule one last update in the future in case Branch Watch is in the middle of a hit. + if (Core::GetState() > Core::State::Paused) + m_timer->setInterval(BRANCH_WATCH_TOOL_TIMER_PAUSE_ONESHOT_MS); + m_timer->setSingleShot(true); + } + Update(); +} + +void BranchWatchDialog::OnClearBranchWatch() +{ + { + const Core::CPUThreadGuard guard{m_system}; + m_table_model->OnClearBranchWatch(guard); + AutoSave(guard); + } + m_btn_wipe_recent_hits->setEnabled(false); + UpdateStatus(); +} + +static std::string GetSnapshotDefaultFilepath() +{ + return fmt::format("{}{}.txt", File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX), + SConfig::GetInstance().GetGameID()); +} + +void BranchWatchDialog::OnSave() +{ + if (!m_branch_watch.CanSave()) + { + ModalMessageBox::warning(this, tr("Error"), tr("There is nothing to save!")); + return; + } + + Save(Core::CPUThreadGuard{m_system}, GetSnapshotDefaultFilepath()); +} + +void BranchWatchDialog::OnSaveAs() +{ + if (!m_branch_watch.CanSave()) + { + ModalMessageBox::warning(this, tr("Error"), tr("There is nothing to save!")); + return; + } + + const QString filepath = DolphinFileDialog::getSaveFileName( + this, tr("Save Branch Watch snapshot"), + QString::fromStdString(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)), + tr("Text file (*.txt);;All Files (*)")); + if (filepath.isEmpty()) + return; + + Save(Core::CPUThreadGuard{m_system}, filepath.toStdString()); +} + +void BranchWatchDialog::OnLoad() +{ + Load(Core::CPUThreadGuard{m_system}, GetSnapshotDefaultFilepath()); +} + +void BranchWatchDialog::OnLoadFrom() +{ + const QString filepath = DolphinFileDialog::getOpenFileName( + this, tr("Load Branch Watch snapshot"), + QString::fromStdString(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)), + tr("Text file (*.txt);;All Files (*)"), nullptr, QFileDialog::Option::ReadOnly); + if (filepath.isEmpty()) + return; + + Load(Core::CPUThreadGuard{m_system}, filepath.toStdString()); +} + +void BranchWatchDialog::OnCodePathWasTaken() +{ + { + const Core::CPUThreadGuard guard{m_system}; + m_table_model->OnCodePathWasTaken(guard); + AutoSave(guard); + } + m_btn_wipe_recent_hits->setEnabled(true); + UpdateStatus(); +} + +void BranchWatchDialog::OnCodePathNotTaken() +{ + { + const Core::CPUThreadGuard guard{m_system}; + m_table_model->OnCodePathNotTaken(guard); + AutoSave(guard); + } + UpdateStatus(); +} + +void BranchWatchDialog::OnBranchWasOverwritten() +{ + if (Core::GetState() == Core::State::Uninitialized) + { + ModalMessageBox::warning(this, tr("Error"), tr("Core is uninitialized.")); + return; + } + { + const Core::CPUThreadGuard guard{m_system}; + m_table_model->OnBranchWasOverwritten(guard); + AutoSave(guard); + } + UpdateStatus(); +} + +void BranchWatchDialog::OnBranchNotOverwritten() +{ + if (Core::GetState() == Core::State::Uninitialized) + { + ModalMessageBox::warning(this, tr("Error"), tr("Core is uninitialized.")); + return; + } + { + const Core::CPUThreadGuard guard{m_system}; + m_table_model->OnBranchNotOverwritten(guard); + AutoSave(guard); + } + UpdateStatus(); +} + +void BranchWatchDialog::OnWipeRecentHits() +{ + m_table_model->OnWipeRecentHits(); +} + +void BranchWatchDialog::OnWipeInspection() +{ + m_table_model->OnWipeInspection(); +} + +void BranchWatchDialog::OnTimeout() +{ + Update(); +} + +void BranchWatchDialog::OnEmulationStateChanged(Core::State new_state) +{ + if (!isVisible()) + return; + + if (TimerCondition(m_branch_watch, new_state)) + m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS); + else if (m_timer->isActive()) + m_timer->stop(); + Update(); +} + +void BranchWatchDialog::OnHelp() +{ + ModalMessageBox::information( + this, tr("Branch Watch Tool Help (1/4)"), + tr("Branch Watch is a code-searching tool that can isolate branches tracked by the emulated " + "CPU by testing candidate branches with simple criteria. If you are familiar with Cheat " + "Engine's Ultimap, Branch Watch is similar to that.\n\n" + "Press the \"Start Branch Watch\" button to activate Branch Watch. Branch Watch persists " + "across emulation sessions, and a snapshot of your progress can be saved to and loaded " + "from the User Directory to persist after Dolphin Emulator is closed. \"Save As...\" and " + "\"Load From...\" actions are also available, and auto-saving can be enabled to save a " + "snapshot at every step of a search. The \"Pause Branch Watch\" button will halt Branch " + "Watch from tracking further branch hits until it is told to resume. Press the \"Clear " + "Branch Watch\" button to clear all candidates and return to the blacklist phase.")); + ModalMessageBox::information( + this, tr("Branch Watch Tool Help (2/4)"), + tr("Branch Watch starts in the blacklist phase, meaning no candidates have been chosen yet, " + "but candidates found so far can be excluded from the candidacy by pressing the \"Code " + "Path Not Taken\", \"Branch Was Overwritten\", and \"Branch Not Overwritten\" buttons. " + "Once the \"Code Path Was Taken\" button is pressed for the first time, Branch Watch will " + "switch to the reduction phase, and the table will populate with all eligible " + "candidates.")); + ModalMessageBox::information( + this, tr("Branch Watch Tool Help (3/4)"), + tr("Once in the reduction phase, it is time to start narrowing down the candidates shown in " + "the table. Further reduce the candidates by checking whether a code path was or was not " + "taken since the last time it was checked. It is also possible to reduce the candidates " + "by determining whether a branch instruction has or has not been overwritten since it was " + "first hit. Filter the candidates by branch kind, branch condition, origin or destination " + "address, and origin or destination symbol name.\n\n" + "After enough passes and experimentation, you may be able to find function calls and " + "conditional code paths that are only taken when an action is performed in the emulated " + "software.")); + ModalMessageBox::information( + this, tr("Branch Watch Tool Help (4/4)"), + tr("Rows in the table can be left-clicked on the origin, destination, and symbol columns to " + "view the associated address in Code View. Right-clicking the selected row(s) will bring " + "up a context menu.\n\n" + "If the origin column of a row selection is right-clicked, an action to replace the " + "branch instruction at the origin(s) with a NOP instruction (No Operation), and an action " + "to copy the address(es) to the clipboard will be available.\n\n" + "If the destination column of a row selection is right-clicked, an action to replace the " + "instruction at the destination(s) with a BLR instruction (Branch to Link Register) will " + "be available, but only if the branch instruction at every origin saves the link " + "register, and an action to copy the address(es) to the clipboard will be available.\n\n" + "If the origin / destination symbol column of a row selection is right-clicked, an action " + "to replace the instruction(s) at the start of the symbol with a BLR instruction will be " + "available, but only if every origin / destination symbol is found.\n\n" + "All context menus have the action to delete the selected row(s) from the candidates.")); +} + +void BranchWatchDialog::OnToggleAutoSave(bool checked) +{ + if (!checked) + return; + + const QString filepath = DolphinFileDialog::getSaveFileName( + this, tr("Select Branch Watch snapshot auto-save file (for user folder location, cancel)"), + QString::fromStdString(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)), + tr("Text file (*.txt);;All Files (*)")); + if (filepath.isEmpty()) + m_autosave_filepath = std::nullopt; + else + m_autosave_filepath = filepath.toStdString(); +} + +void BranchWatchDialog::OnHideShowControls(bool checked) +{ + if (checked) + m_control_toolbar->hide(); + else + m_control_toolbar->show(); +} + +void BranchWatchDialog::OnToggleIgnoreApploader(bool checked) +{ + m_system.SetIsBranchWatchIgnoreApploader(checked); +} + +void BranchWatchDialog::OnTableClicked(const QModelIndex& index) +{ + const QVariant v = m_table_proxy->data(index, UserRole::ClickRole); + switch (index.column()) + { + case Column::OriginSymbol: + case Column::DestinSymbol: + if (!v.isValid()) + return; + [[fallthrough]]; + case Column::Origin: + case Column::Destination: + m_code_widget->SetAddress(v.value(), CodeViewWidget::SetAddressUpdate::WithDetailedUpdate); + return; + } +} + +void BranchWatchDialog::OnTableContextMenu(const QPoint& pos) +{ + const QModelIndex index = m_table_view->indexAt(pos); + if (!index.isValid()) + return; + QModelIndexList index_list = m_table_view->selectionModel()->selectedRows(index.column()); + + QMenu* const menu = new QMenu; + menu->addAction(tr("&Delete"), [this, index_list]() { OnTableDelete(std::move(index_list)); }); + switch (index.column()) + { + case Column::Origin: + { + QAction* const action = menu->addAction(tr("Insert &NOP")); + if (Core::GetState() != Core::State::Uninitialized) + connect(action, &QAction::triggered, + [this, index_list]() { OnTableSetNOP(std::move(index_list)); }); + else + action->setEnabled(false); + menu->addAction(tr("&Copy Address"), [this, index_list = std::move(index_list)]() { + OnTableCopyAddress(std::move(index_list)); + }); + break; + } + case Column::Destination: + { + QAction* const action = menu->addAction(tr("Insert &BLR")); + const bool enable_action = + Core::GetState() != Core::State::Uninitialized && + std::all_of(index_list.begin(), index_list.end(), [this](const QModelIndex& index) { + const QModelIndex sibling = index.siblingAtColumn(Column::Instruction); + return BranchSavesLR(m_table_proxy->data(sibling, UserRole::ClickRole).value()); + }); + if (enable_action) + connect(action, &QAction::triggered, + [this, index_list]() { OnTableSetBLR(std::move(index_list)); }); + else + action->setEnabled(false); + menu->addAction(tr("&Copy Address"), [this, index_list = std::move(index_list)]() { + OnTableCopyAddress(std::move(index_list)); + }); + break; + } + case Column::OriginSymbol: + case Column::DestinSymbol: + { + QAction* const action = menu->addAction(tr("Insert &BLR at start")); + const bool enable_action = + Core::GetState() != Core::State::Uninitialized && + std::all_of(index_list.begin(), index_list.end(), [this](const QModelIndex& index) { + return m_table_proxy->data(index, UserRole::ClickRole).isValid(); + }); + if (enable_action) + connect(action, &QAction::triggered, [this, index_list = std::move(index_list)]() { + OnTableSetBLR(std::move(index_list)); + }); + else + action->setEnabled(false); + break; + } + } + menu->exec(m_table_view->viewport()->mapToGlobal(pos)); +} + +void BranchWatchDialog::OnTableHeaderContextMenu(const QPoint& pos) +{ + m_mnu_column_visibility->exec(m_table_view->horizontalHeader()->mapToGlobal(pos)); +} + +void BranchWatchDialog::OnTableDelete(QModelIndexList index_list) +{ + m_table_proxy->OnDelete(std::move(index_list)); + UpdateStatus(); +} + +void BranchWatchDialog::OnTableDeleteKeypress() +{ + OnTableDelete(m_table_view->selectionModel()->selectedRows()); +} + +void BranchWatchDialog::OnTableSetBLR(QModelIndexList index_list) +{ + for (const QModelIndex& index : index_list) + { + m_system.GetPowerPC().GetDebugInterface().SetPatch( + Core::CPUThreadGuard{m_system}, + m_table_proxy->data(index, UserRole::ClickRole).value(), 0x4e800020); + m_table_proxy->SetInspected(index); + } + // TODO: This is not ideal. What I need is a signal for when memory has been changed by the GUI, + // but I cannot find one. UpdateDisasmDialog comes close, but does too much in one signal. For + // example, CodeViewWidget will scroll to the current PC when UpdateDisasmDialog is signaled. This + // seems like a pervasive issue. For example, modifying an instruction in the CodeViewWidget will + // not reflect in the MemoryViewWidget, and vice versa. Neither of these widgets changing memory + // will reflect in the JITWidget, either. At the very least, we can make sure the CodeWidget + // is updated in an acceptable way. + m_code_widget->Update(); +} + +void BranchWatchDialog::OnTableSetNOP(QModelIndexList index_list) +{ + for (const QModelIndex& index : index_list) + { + m_system.GetPowerPC().GetDebugInterface().SetPatch( + Core::CPUThreadGuard{m_system}, + m_table_proxy->data(index, UserRole::ClickRole).value(), 0x60000000); + m_table_proxy->SetInspected(index); + } + // Same issue as OnSetBLR. + m_code_widget->Update(); +} + +void BranchWatchDialog::OnTableCopyAddress(QModelIndexList index_list) +{ + auto iter = index_list.begin(); + if (iter == index_list.end()) + return; + + QString text; + text.reserve(index_list.size() * 9 - 1); + while (true) + { + text.append(QString::number(m_table_proxy->data(*iter, UserRole::ClickRole).value(), 16)); + if (++iter == index_list.end()) + break; + text.append(QChar::fromLatin1('\n')); + } + QApplication::clipboard()->setText(text); +} + +void BranchWatchDialog::Update() +{ + if (m_branch_watch.GetRecordingPhase() == Core::BranchWatch::Phase::Blacklist) + UpdateStatus(); + m_table_model->UpdateHits(); +} + +void BranchWatchDialog::UpdateSymbols() +{ + m_table_model->UpdateSymbols(); +} + +void BranchWatchDialog::UpdateStatus() +{ + switch (m_branch_watch.GetRecordingPhase()) + { + case Core::BranchWatch::Phase::Blacklist: + { + const std::size_t candidate_size = m_branch_watch.GetCollectionSize(); + const std::size_t blacklist_size = m_branch_watch.GetBlacklistSize(); + if (blacklist_size == 0) + { + m_status_bar->showMessage(tr("Candidates: %1").arg(candidate_size)); + return; + } + m_status_bar->showMessage(tr("Candidates: %1 | Excluded: %2 | Remaining: %3") + .arg(candidate_size) + .arg(blacklist_size) + .arg(candidate_size - blacklist_size)); + return; + } + case Core::BranchWatch::Phase::Reduction: + { + const std::size_t candidate_size = m_branch_watch.GetSelection().size(); + if (candidate_size == 0) + { + m_status_bar->showMessage(tr("Zero candidates remaining.")); + return; + } + const std::size_t remaining_size = m_table_proxy->rowCount(); + m_status_bar->showMessage(tr("Candidates: %1 | Filtered: %2 | Remaining: %3") + .arg(candidate_size) + .arg(candidate_size - remaining_size) + .arg(remaining_size)); + return; + } + } +} + +void BranchWatchDialog::Save(const Core::CPUThreadGuard& guard, const std::string& filepath) +{ + File::IOFile file(filepath, "w"); + if (!file.IsOpen()) + { + ModalMessageBox::warning( + this, tr("Error"), + tr("Failed to save Branch Watch snapshot \"%1\"").arg(QString::fromStdString(filepath))); + return; + } + + m_table_model->Save(guard, file.GetHandle()); +} + +void BranchWatchDialog::Load(const Core::CPUThreadGuard& guard, const std::string& filepath) +{ + File::IOFile file(filepath, "r"); + if (!file.IsOpen()) + { + ModalMessageBox::warning( + this, tr("Error"), + tr("Failed to open Branch Watch snapshot \"%1\"").arg(QString::fromStdString(filepath))); + return; + } + + m_table_model->Load(guard, file.GetHandle()); + m_btn_wipe_recent_hits->setEnabled(m_branch_watch.GetRecordingPhase() == + Core::BranchWatch::Phase::Reduction); +} + +void BranchWatchDialog::AutoSave(const Core::CPUThreadGuard& guard) +{ + if (!m_act_autosave->isChecked() || !m_branch_watch.CanSave()) + return; + Save(guard, m_autosave_filepath ? m_autosave_filepath.value() : GetSnapshotDefaultFilepath()); +} diff --git a/Source/Core/DolphinQt/Debugger/BranchWatchDialog.h b/Source/Core/DolphinQt/Debugger/BranchWatchDialog.h new file mode 100644 index 0000000000..b167ca106d --- /dev/null +++ b/Source/Core/DolphinQt/Debugger/BranchWatchDialog.h @@ -0,0 +1,114 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include +#include + +#include "Core/Core.h" + +namespace Core +{ +class BranchWatch; +class CPUThreadGuard; +class System; +} // namespace Core +class BranchWatchProxyModel; +class BranchWatchTableModel; +class CodeWidget; +class QAction; +class QMenu; +class QPoint; +class QPushButton; +class QStatusBar; +class QTableView; +class QTimer; +class QToolBar; +class QWidget; + +namespace BranchWatchTableModelColumn +{ +enum EnumType : int; +} +namespace BranchWatchTableModelUserRole +{ +enum EnumType : int; +} + +class BranchWatchDialog : public QDialog +{ + Q_OBJECT + + using Column = BranchWatchTableModelColumn::EnumType; + using UserRole = BranchWatchTableModelUserRole::EnumType; + +public: + explicit BranchWatchDialog(Core::System& system, Core::BranchWatch& branch_watch, + CodeWidget* code_widget, QWidget* parent = nullptr); + void done(int r) override; + int exec() override; + void open() override; + +private: + void OnStartPause(bool checked); + void OnClearBranchWatch(); + void OnSave(); + void OnSaveAs(); + void OnLoad(); + void OnLoadFrom(); + void OnCodePathWasTaken(); + void OnCodePathNotTaken(); + void OnBranchWasOverwritten(); + void OnBranchNotOverwritten(); + void OnWipeRecentHits(); + void OnWipeInspection(); + void OnTimeout(); + void OnEmulationStateChanged(Core::State new_state); + void OnHelp(); + void OnToggleAutoSave(bool checked); + void OnHideShowControls(bool checked); + void OnToggleIgnoreApploader(bool checked); + + void OnTableClicked(const QModelIndex& index); + void OnTableContextMenu(const QPoint& pos); + void OnTableHeaderContextMenu(const QPoint& pos); + void OnTableDelete(QModelIndexList index_list); + void OnTableDeleteKeypress(); + void OnTableSetBLR(QModelIndexList index_list); + void OnTableSetNOP(QModelIndexList index_list); + void OnTableCopyAddress(QModelIndexList index_list); + +public: + // TODO: Step doesn't cause EmulationStateChanged to be emitted, so it has to call this manually. + void Update(); + // TODO: There seems to be a lack of a ubiquitous signal for when symbols change. + void UpdateSymbols(); + +private: + void UpdateStatus(); + void Save(const Core::CPUThreadGuard& guard, const std::string& filepath); + void Load(const Core::CPUThreadGuard& guard, const std::string& filepath); + void AutoSave(const Core::CPUThreadGuard& guard); + + Core::System& m_system; + Core::BranchWatch& m_branch_watch; + CodeWidget* m_code_widget; + + QPushButton *m_btn_start_pause, *m_btn_clear_watch, *m_btn_path_was_taken, *m_btn_path_not_taken, + *m_btn_was_overwritten, *m_btn_not_overwritten, *m_btn_wipe_recent_hits; + QAction* m_act_autosave; + QMenu* m_mnu_column_visibility; + + QToolBar* m_control_toolbar; + QTableView* m_table_view; + BranchWatchProxyModel* m_table_proxy; + BranchWatchTableModel* m_table_model; + QStatusBar* m_status_bar; + QTimer* m_timer; + + std::optional m_autosave_filepath; +}; diff --git a/Source/Core/DolphinQt/Debugger/BranchWatchTableModel.cpp b/Source/Core/DolphinQt/Debugger/BranchWatchTableModel.cpp new file mode 100644 index 0000000000..800973a6dd --- /dev/null +++ b/Source/Core/DolphinQt/Debugger/BranchWatchTableModel.cpp @@ -0,0 +1,502 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/Debugger/BranchWatchTableModel.h" + +#include +#include +#include + +#include + +#include "Common/Assert.h" +#include "Common/GekkoDisassembler.h" +#include "Core/Debugger/BranchWatch.h" +#include "Core/PowerPC/PPCSymbolDB.h" + +QVariant BranchWatchTableModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + return DisplayRoleData(index); + case Qt::FontRole: + return FontRoleData(index); + case Qt::TextAlignmentRole: + return TextAlignmentRoleData(index); + case Qt::ForegroundRole: + return ForegroundRoleData(index); + case UserRole::ClickRole: + return ClickRoleData(index); + case UserRole::SortRole: + return SortRoleData(index); + } + return QVariant(); +} + +QVariant BranchWatchTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical || role != Qt::DisplayRole) + return QVariant(); + + static constexpr std::array headers = { + QT_TR_NOOP("Instr."), QT_TR_NOOP("Cond."), + QT_TR_NOOP("Origin"), QT_TR_NOOP("Destination"), + QT_TR_NOOP("Recent Hits"), QT_TR_NOOP("Total Hits"), + QT_TR_NOOP("Origin Symbol"), QT_TR_NOOP("Destination Symbol")}; + return tr(headers[section]); +} + +int BranchWatchTableModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return static_cast(m_branch_watch.GetSelection().size()); +} + +int BranchWatchTableModel::columnCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return Column::NumberOfColumns; +} + +bool BranchWatchTableModel::removeRows(int row, int count, const QModelIndex& parent) +{ + if (parent.isValid() || row < 0) + return false; + if (count <= 0) + return true; + + auto& selection = m_branch_watch.GetSelection(); + beginRemoveRows(parent, row, row + count - 1); // Last is inclusive in Qt! + selection.erase(selection.begin() + row, selection.begin() + row + count); + m_symbol_list.remove(row, count); + endRemoveRows(); + return true; +} + +void BranchWatchTableModel::OnClearBranchWatch(const Core::CPUThreadGuard& guard) +{ + emit layoutAboutToBeChanged(); + m_branch_watch.Clear(guard); + m_symbol_list.clear(); + emit layoutChanged(); +} + +void BranchWatchTableModel::OnCodePathWasTaken(const Core::CPUThreadGuard& guard) +{ + emit layoutAboutToBeChanged(); + m_branch_watch.IsolateHasExecuted(guard); + PrefetchSymbols(); + emit layoutChanged(); +} + +void BranchWatchTableModel::OnCodePathNotTaken(const Core::CPUThreadGuard& guard) +{ + emit layoutAboutToBeChanged(); + m_branch_watch.IsolateNotExecuted(guard); + PrefetchSymbols(); + emit layoutChanged(); +} + +void BranchWatchTableModel::OnBranchWasOverwritten(const Core::CPUThreadGuard& guard) +{ + emit layoutAboutToBeChanged(); + m_branch_watch.IsolateWasOverwritten(guard); + PrefetchSymbols(); + emit layoutChanged(); +} + +void BranchWatchTableModel::OnBranchNotOverwritten(const Core::CPUThreadGuard& guard) +{ + emit layoutAboutToBeChanged(); + m_branch_watch.IsolateNotOverwritten(guard); + PrefetchSymbols(); + emit layoutChanged(); +} + +void BranchWatchTableModel::OnWipeRecentHits() +{ + const int row_count = rowCount(); + if (row_count <= 0) + return; + static const QList roles = {Qt::DisplayRole}; + m_branch_watch.UpdateHitsSnapshot(); + const int last = row_count - 1; + emit dataChanged(createIndex(0, Column::RecentHits), createIndex(last, Column::RecentHits), + roles); +} + +void BranchWatchTableModel::OnWipeInspection() +{ + const int row_count = rowCount(); + if (row_count <= 0) + return; + static const QList roles = {Qt::FontRole, Qt::ForegroundRole}; + m_branch_watch.ClearSelectionInspection(); + const int last = row_count - 1; + emit dataChanged(createIndex(0, Column::Origin), createIndex(last, Column::Destination), roles); + emit dataChanged(createIndex(0, Column::OriginSymbol), createIndex(last, Column::DestinSymbol), + roles); +} + +void BranchWatchTableModel::OnDelete(QModelIndexList index_list) +{ + std::sort(index_list.begin(), index_list.end()); + // TODO C++20: std::ranges::reverse_view + for (auto iter = index_list.rbegin(); iter != index_list.rend(); ++iter) + { + if (!iter->isValid()) + continue; + removeRow(iter->row()); + } +} + +void BranchWatchTableModel::Save(const Core::CPUThreadGuard& guard, std::FILE* file) const +{ + m_branch_watch.Save(guard, file); +} + +void BranchWatchTableModel::Load(const Core::CPUThreadGuard& guard, std::FILE* file) +{ + emit layoutAboutToBeChanged(); + m_branch_watch.Load(guard, file); + PrefetchSymbols(); + emit layoutChanged(); +} + +void BranchWatchTableModel::UpdateSymbols() +{ + const int row_count = rowCount(); + if (row_count <= 0) + return; + static const QList roles = {Qt::DisplayRole}; + PrefetchSymbols(); + const int last = row_count - 1; + emit dataChanged(createIndex(0, Column::OriginSymbol), createIndex(last, Column::DestinSymbol), + roles); +} + +void BranchWatchTableModel::UpdateHits() +{ + const int row_count = rowCount(); + if (row_count <= 0) + return; + static const QList roles = {Qt::DisplayRole}; + const int last = row_count - 1; + emit dataChanged(createIndex(0, Column::RecentHits), createIndex(last, Column::TotalHits), roles); +} + +void BranchWatchTableModel::SetInspected(const QModelIndex& index) +{ + const int row = index.row(); + switch (index.column()) + { + case Column::Origin: + SetOriginInspected(m_branch_watch.GetSelection()[row].collection_ptr->first.origin_addr); + return; + case Column::Destination: + SetDestinInspected(m_branch_watch.GetSelection()[row].collection_ptr->first.destin_addr, false); + return; + case Column::OriginSymbol: + SetSymbolInspected(m_symbol_list[row].origin_addr.value(), false); + return; + case Column::DestinSymbol: + SetSymbolInspected(m_symbol_list[row].destin_addr.value(), false); + return; + } +} + +void BranchWatchTableModel::SetOriginInspected(u32 origin_addr) +{ + using Inspection = Core::BranchWatchSelectionInspection; + static const QList roles = {Qt::FontRole, Qt::ForegroundRole}; + + const Core::BranchWatch::Selection& selection = m_branch_watch.GetSelection(); + for (std::size_t i = 0; i < selection.size(); ++i) + { + if (selection[i].collection_ptr->first.origin_addr != origin_addr) + continue; + m_branch_watch.SetSelectedInspected(i, Inspection::SetOriginNOP); + const QModelIndex index = createIndex(static_cast(i), Column::Origin); + emit dataChanged(index, index, roles); + } +} + +void BranchWatchTableModel::SetDestinInspected(u32 destin_addr, bool nested) +{ + using Inspection = Core::BranchWatchSelectionInspection; + static const QList roles = {Qt::FontRole, Qt::ForegroundRole}; + + const Core::BranchWatch::Selection& selection = m_branch_watch.GetSelection(); + for (std::size_t i = 0; i < selection.size(); ++i) + { + if (selection[i].collection_ptr->first.destin_addr != destin_addr) + continue; + m_branch_watch.SetSelectedInspected(i, Inspection::SetDestinBLR); + const QModelIndex index = createIndex(static_cast(i), Column::Destination); + emit dataChanged(index, index, roles); + } + + if (nested) + return; + SetSymbolInspected(destin_addr, true); +} + +void BranchWatchTableModel::SetSymbolInspected(u32 symbol_addr, bool nested) +{ + using Inspection = Core::BranchWatchSelectionInspection; + static const QList roles = {Qt::FontRole, Qt::ForegroundRole}; + + for (qsizetype i = 0; i < m_symbol_list.size(); ++i) + { + const SymbolListValueType& value = m_symbol_list[i]; + if (value.origin_addr.isValid() && value.origin_addr.value() == symbol_addr) + { + m_branch_watch.SetSelectedInspected(i, Inspection::SetOriginSymbolBLR); + const QModelIndex index = createIndex(i, Column::OriginSymbol); + emit dataChanged(index, index, roles); + } + if (value.destin_addr.isValid() && value.destin_addr.value() == symbol_addr) + { + m_branch_watch.SetSelectedInspected(i, Inspection::SetDestinSymbolBLR); + const QModelIndex index = createIndex(i, Column::DestinSymbol); + emit dataChanged(index, index, roles); + } + } + + if (nested) + return; + SetDestinInspected(symbol_addr, true); +} + +void BranchWatchTableModel::PrefetchSymbols() +{ + if (m_branch_watch.GetRecordingPhase() != Core::BranchWatch::Phase::Reduction) + return; + + const Core::BranchWatch::Selection& selection = m_branch_watch.GetSelection(); + m_symbol_list.clear(); + m_symbol_list.reserve(selection.size()); + for (const Core::BranchWatch::Selection::value_type& value : selection) + { + const Core::BranchWatch::Collection::value_type* const kv = value.collection_ptr; + m_symbol_list.emplace_back(g_symbolDB.GetSymbolFromAddr(kv->first.origin_addr), + g_symbolDB.GetSymbolFromAddr(kv->first.destin_addr)); + } +} + +static QVariant GetValidSymbolStringVariant(const QVariant& symbol_name_v) +{ + if (symbol_name_v.isValid()) + return symbol_name_v; + return QStringLiteral(" --- "); +} + +static QString GetInstructionMnemonic(u32 hex) +{ + const std::string disas = Common::GekkoDisassembler::Disassemble(hex, 0); + const std::string::size_type split = disas.find('\t'); + // I wish I could disassemble just the mnemonic! + if (split == std::string::npos) + return QString::fromStdString(disas); + return QString::fromLatin1(disas.data(), split); +} + +static bool BranchIsUnconditional(UGeckoInstruction inst) +{ + if (inst.OPCD == 18) // bx + return true; + // If BranchWatch is doing its job, the input will be only bcx, bclrx, and bcctrx instructions. + DEBUG_ASSERT(inst.OPCD == 16 || (inst.OPCD == 19 && (inst.SUBOP10 == 16 || inst.SUBOP10 == 528))); + if ((inst.BO & 0b10100) == 0b10100) // 1z1zz - Branch always + return true; + return false; +} + +static QString GetConditionString(const Core::BranchWatch::Selection::value_type& value, + const Core::BranchWatch::Collection::value_type* kv) +{ + if (value.condition == false) + return BranchWatchTableModel::tr("false"); + if (BranchIsUnconditional(kv->first.original_inst)) + return QStringLiteral(""); + return BranchWatchTableModel::tr("true"); +} + +QVariant BranchWatchTableModel::DisplayRoleData(const QModelIndex& index) const +{ + switch (index.column()) + { + case Column::OriginSymbol: + return GetValidSymbolStringVariant(m_symbol_list[index.row()].origin_name); + case Column::DestinSymbol: + return GetValidSymbolStringVariant(m_symbol_list[index.row()].destin_name); + } + const Core::BranchWatch::Selection::value_type& value = + m_branch_watch.GetSelection()[index.row()]; + const Core::BranchWatch::Collection::value_type* kv = value.collection_ptr; + switch (index.column()) + { + case Column::Instruction: + return GetInstructionMnemonic(kv->first.original_inst.hex); + case Column::Condition: + return GetConditionString(value, kv); + case Column::Origin: + return QString::number(kv->first.origin_addr, 16); + case Column::Destination: + return QString::number(kv->first.destin_addr, 16); + case Column::RecentHits: + return QString::number(kv->second.total_hits - kv->second.hits_snapshot); + case Column::TotalHits: + return QString::number(kv->second.total_hits); + } + return QVariant(); +} + +QVariant BranchWatchTableModel::FontRoleData(const QModelIndex& index) const +{ + m_font.setBold([&]() -> bool { + switch (index.column()) + { + using Inspection = Core::BranchWatchSelectionInspection; + case Column::Origin: + return (m_branch_watch.GetSelection()[index.row()].inspection & Inspection::SetOriginNOP) != + Inspection{}; + case Column::Destination: + return (m_branch_watch.GetSelection()[index.row()].inspection & Inspection::SetDestinBLR) != + Inspection{}; + case Column::OriginSymbol: + return (m_branch_watch.GetSelection()[index.row()].inspection & + Inspection::SetOriginSymbolBLR) != Inspection{}; + case Column::DestinSymbol: + return (m_branch_watch.GetSelection()[index.row()].inspection & + Inspection::SetDestinSymbolBLR) != Inspection{}; + } + // Importantly, this code path avoids subscripting the selection to get an inspection value. + return false; + }()); + return m_font; +} + +QVariant BranchWatchTableModel::TextAlignmentRoleData(const QModelIndex& index) const +{ + // Qt enums become QFlags when operators are used. QVariant's constructors don't support QFlags. + switch (index.column()) + { + case Column::Condition: + case Column::Origin: + case Column::Destination: + return Qt::AlignCenter; + case Column::RecentHits: + case Column::TotalHits: + return QVariant::fromValue(Qt::AlignRight | Qt::AlignVCenter); + case Column::Instruction: + case Column::OriginSymbol: + case Column::DestinSymbol: + return QVariant::fromValue(Qt::AlignLeft | Qt::AlignVCenter); + } + return QVariant(); +} + +QVariant BranchWatchTableModel::ForegroundRoleData(const QModelIndex& index) const +{ + switch (index.column()) + { + using Inspection = Core::BranchWatchSelectionInspection; + case Column::Origin: + { + const Inspection inspection = m_branch_watch.GetSelection()[index.row()].inspection; + return (inspection & Inspection::SetOriginNOP) != Inspection{} ? QBrush(Qt::red) : QVariant(); + } + case Column::Destination: + { + const Inspection inspection = m_branch_watch.GetSelection()[index.row()].inspection; + return (inspection & Inspection::SetDestinBLR) != Inspection{} ? QBrush(Qt::red) : QVariant(); + } + case Column::OriginSymbol: + { + const Inspection inspection = m_branch_watch.GetSelection()[index.row()].inspection; + return (inspection & Inspection::SetOriginSymbolBLR) != Inspection{} ? QBrush(Qt::red) : + QVariant(); + } + case Column::DestinSymbol: + { + const Inspection inspection = m_branch_watch.GetSelection()[index.row()].inspection; + return (inspection & Inspection::SetDestinSymbolBLR) != Inspection{} ? QBrush(Qt::red) : + QVariant(); + } + } + // Importantly, this code path avoids subscripting the selection to get an inspection value. + return QVariant(); +} + +QVariant BranchWatchTableModel::ClickRoleData(const QModelIndex& index) const +{ + switch (index.column()) + { + case Column::OriginSymbol: + return m_symbol_list[index.row()].origin_addr; + case Column::DestinSymbol: + return m_symbol_list[index.row()].destin_addr; + } + const Core::BranchWatch::Collection::value_type* kv = + m_branch_watch.GetSelection()[index.row()].collection_ptr; + switch (index.column()) + { + case Column::Instruction: + return kv->first.original_inst.hex; + case Column::Origin: + return kv->first.origin_addr; + case Column::Destination: + return kv->first.destin_addr; + } + return QVariant(); +} + +// 0 == false, 1 == true, 2 == unconditional +static int GetConditionInteger(const Core::BranchWatch::Selection::value_type& value, + const Core::BranchWatch::Collection::value_type* kv) +{ + if (value.condition == false) + return 0; + if (BranchIsUnconditional(kv->first.original_inst)) + return 2; + return 1; +} + +QVariant BranchWatchTableModel::SortRoleData(const QModelIndex& index) const +{ + switch (index.column()) + { + case Column::OriginSymbol: + return m_symbol_list[index.row()].origin_name; + case Column::DestinSymbol: + return m_symbol_list[index.row()].destin_name; + } + const Core::BranchWatch::Selection::value_type& selection_value = + m_branch_watch.GetSelection()[index.row()]; + const Core::BranchWatch::Collection::value_type* kv = selection_value.collection_ptr; + switch (index.column()) + { + // QVariant's ctor only supports (unsigned) int and (unsigned) long long for some stupid reason. + // std::size_t is unsigned long on some platforms, which results in an ambiguous conversion. + case Column::Instruction: + return GetInstructionMnemonic(kv->first.original_inst.hex); + case Column::Condition: + return GetConditionInteger(selection_value, kv); + case Column::Origin: + return kv->first.origin_addr; + case Column::Destination: + return kv->first.destin_addr; + case Column::RecentHits: + return qulonglong{kv->second.total_hits - kv->second.hits_snapshot}; + case Column::TotalHits: + return qulonglong{kv->second.total_hits}; + } + return QVariant(); +} diff --git a/Source/Core/DolphinQt/Debugger/BranchWatchTableModel.h b/Source/Core/DolphinQt/Debugger/BranchWatchTableModel.h new file mode 100644 index 0000000000..7b3cf42bb1 --- /dev/null +++ b/Source/Core/DolphinQt/Debugger/BranchWatchTableModel.h @@ -0,0 +1,119 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include + +#include "Common/SymbolDB.h" + +namespace Core +{ +class BranchWatch; +class CPUThreadGuard; +class System; +} // namespace Core + +namespace BranchWatchTableModelColumn +{ +enum EnumType : int +{ + Instruction = 0, + Condition, + Origin, + Destination, + RecentHits, + TotalHits, + OriginSymbol, + DestinSymbol, + NumberOfColumns, +}; +} + +namespace BranchWatchTableModelUserRole +{ +enum EnumType : int +{ + ClickRole = Qt::UserRole, + SortRole, +}; +} + +struct BranchWatchTableModelSymbolListValueType +{ + explicit BranchWatchTableModelSymbolListValueType(const Common::Symbol* const origin_symbol, + const Common::Symbol* const destin_symbol) + : origin_name(origin_symbol ? QString::fromStdString(origin_symbol->name) : QVariant{}), + origin_addr(origin_symbol ? origin_symbol->address : QVariant{}), + destin_name(destin_symbol ? QString::fromStdString(destin_symbol->name) : QVariant{}), + destin_addr(destin_symbol ? destin_symbol->address : QVariant{}) + { + } + QVariant origin_name, origin_addr; + QVariant destin_name, destin_addr; +}; + +class BranchWatchTableModel final : public QAbstractTableModel +{ + Q_OBJECT + +public: + using Column = BranchWatchTableModelColumn::EnumType; + using UserRole = BranchWatchTableModelUserRole::EnumType; + using SymbolListValueType = BranchWatchTableModelSymbolListValueType; + using SymbolList = QList; + + explicit BranchWatchTableModel(Core::System& system, Core::BranchWatch& branch_watch, + QObject* parent = nullptr) + : QAbstractTableModel(parent), m_system(system), m_branch_watch(branch_watch) + { + } + 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; + void setFont(const QFont& font) { m_font = font; } + + void OnClearBranchWatch(const Core::CPUThreadGuard& guard); + void OnCodePathWasTaken(const Core::CPUThreadGuard& guard); + void OnCodePathNotTaken(const Core::CPUThreadGuard& guard); + void OnBranchWasOverwritten(const Core::CPUThreadGuard& guard); + void OnBranchNotOverwritten(const Core::CPUThreadGuard& guard); + void OnWipeRecentHits(); + void OnWipeInspection(); + void OnDelete(QModelIndexList index_list); + + void Save(const Core::CPUThreadGuard& guard, std::FILE* file) const; + void Load(const Core::CPUThreadGuard& guard, std::FILE* file); + void UpdateSymbols(); + void UpdateHits(); + void SetInspected(const QModelIndex& index); + + const SymbolList& GetSymbolList() const { return m_symbol_list; } + +private: + void SetOriginInspected(u32 origin_addr); + void SetDestinInspected(u32 destin_addr, bool nested); + void SetSymbolInspected(u32 symbol_addr, bool nested); + void PrefetchSymbols(); + + [[nodiscard]] QVariant DisplayRoleData(const QModelIndex& index) const; + [[nodiscard]] QVariant FontRoleData(const QModelIndex& index) const; + [[nodiscard]] QVariant TextAlignmentRoleData(const QModelIndex& index) const; + [[nodiscard]] QVariant ForegroundRoleData(const QModelIndex& index) const; + [[nodiscard]] QVariant ClickRoleData(const QModelIndex& index) const; + [[nodiscard]] QVariant SortRoleData(const QModelIndex& index) const; + + Core::System& m_system; + Core::BranchWatch& m_branch_watch; + + SymbolList m_symbol_list; + mutable QFont m_font; +}; diff --git a/Source/Core/DolphinQt/Debugger/CodeDiffDialog.cpp b/Source/Core/DolphinQt/Debugger/CodeDiffDialog.cpp deleted file mode 100644 index 423036742d..0000000000 --- a/Source/Core/DolphinQt/Debugger/CodeDiffDialog.cpp +++ /dev/null @@ -1,673 +0,0 @@ -// Copyright 2022 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "DolphinQt/Debugger/CodeDiffDialog.h" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Common/FileUtil.h" -#include "Common/IOFile.h" -#include "Common/MsgHandler.h" -#include "Common/StringUtil.h" -#include "Core/ConfigManager.h" -#include "Core/Core.h" -#include "Core/Debugger/PPCDebugInterface.h" -#include "Core/HW/CPU.h" -#include "Core/PowerPC/JitInterface.h" -#include "Core/PowerPC/MMU.h" -#include "Core/PowerPC/PPCSymbolDB.h" -#include "Core/PowerPC/PowerPC.h" -#include "Core/PowerPC/Profiler.h" -#include "Core/System.h" - -#include "DolphinQt/Debugger/CodeWidget.h" -#include "DolphinQt/Host.h" -#include "DolphinQt/QtUtils/ModalMessageBox.h" -#include "DolphinQt/Settings.h" - -static const QString RECORD_BUTTON_STYLESHEET = QStringLiteral( - "QPushButton:checked { background-color: rgb(150, 0, 0); border-style: solid;" - "padding: 0px; border-width: 3px; border-color: rgb(150,0,0); color: rgb(255, 255, 255);}"); - -CodeDiffDialog::CodeDiffDialog(CodeWidget* parent) : QDialog(parent), m_code_widget(parent) -{ - setWindowTitle(tr("Code Diff Tool")); - CreateWidgets(); - auto& settings = Settings::GetQSettings(); - restoreGeometry(settings.value(QStringLiteral("diffdialog/geometry")).toByteArray()); - ConnectWidgets(); -} - -void CodeDiffDialog::reject() -{ - ClearData(); - auto& settings = Settings::GetQSettings(); - settings.setValue(QStringLiteral("diffdialog/geometry"), saveGeometry()); - QDialog::reject(); -} - -void CodeDiffDialog::CreateWidgets() -{ - bool running = Core::GetState() != Core::State::Uninitialized; - - auto* btns_layout = new QGridLayout; - m_exclude_btn = new QPushButton(tr("Code did not get executed")); - m_include_btn = new QPushButton(tr("Code has been executed")); - m_record_btn = new QPushButton(tr("Start Recording")); - m_record_btn->setCheckable(true); - m_record_btn->setStyleSheet(RECORD_BUTTON_STYLESHEET); - m_record_btn->setEnabled(running); - m_exclude_btn->setEnabled(false); - m_include_btn->setEnabled(false); - - btns_layout->addWidget(m_exclude_btn, 0, 0); - btns_layout->addWidget(m_include_btn, 0, 1); - btns_layout->addWidget(m_record_btn, 0, 2); - - auto* labels_layout = new QHBoxLayout; - m_exclude_size_label = new QLabel(tr("Excluded: 0")); - m_include_size_label = new QLabel(tr("Included: 0")); - - btns_layout->addWidget(m_exclude_size_label, 1, 0); - btns_layout->addWidget(m_include_size_label, 1, 1); - - m_matching_results_table = new QTableWidget(); - m_matching_results_table->setColumnCount(5); - m_matching_results_table->setHorizontalHeaderLabels( - {tr("Address"), tr("Total Hits"), tr("Hits"), tr("Symbol"), tr("Inspected")}); - m_matching_results_table->setSelectionMode(QAbstractItemView::SingleSelection); - m_matching_results_table->setSelectionBehavior(QAbstractItemView::SelectRows); - m_matching_results_table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - m_matching_results_table->setContextMenuPolicy(Qt::CustomContextMenu); - m_matching_results_table->setColumnWidth(0, 60); - m_matching_results_table->setColumnWidth(1, 60); - m_matching_results_table->setColumnWidth(2, 4); - m_matching_results_table->setColumnWidth(3, 210); - m_matching_results_table->setColumnWidth(4, 65); - m_matching_results_table->setCornerButtonEnabled(false); - m_autosave_check = new QCheckBox(tr("Auto Save")); - m_save_btn = new QPushButton(tr("Save")); - m_save_btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - m_save_btn->setEnabled(running); - m_load_btn = new QPushButton(tr("Load")); - m_load_btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - m_load_btn->setEnabled(running); - m_reset_btn = new QPushButton(tr("Reset All")); - m_reset_btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - m_help_btn = new QPushButton(tr("Help")); - m_help_btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - auto* bottom_controls_layout = new QHBoxLayout; - bottom_controls_layout->addWidget(m_reset_btn, 0, Qt::AlignLeft); - bottom_controls_layout->addStretch(); - bottom_controls_layout->addWidget(m_autosave_check, 0, Qt::AlignRight); - bottom_controls_layout->addWidget(m_save_btn, 0, Qt::AlignRight); - bottom_controls_layout->addWidget(m_load_btn, 0, Qt::AlignRight); - bottom_controls_layout->addWidget(m_help_btn, 0, Qt::AlignRight); - - auto* layout = new QVBoxLayout(); - layout->addLayout(btns_layout); - layout->addLayout(labels_layout); - layout->addWidget(m_matching_results_table); - layout->addLayout(bottom_controls_layout); - - setLayout(layout); - resize(515, 400); -} - -void CodeDiffDialog::ConnectWidgets() -{ -#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) - connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, - [this](Qt::ColorScheme colorScheme) { - m_record_btn->setStyleSheet(RECORD_BUTTON_STYLESHEET); - }); -#endif - connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, - [this](Core::State state) { UpdateButtons(state != Core::State::Uninitialized); }); - connect(m_record_btn, &QPushButton::toggled, this, &CodeDiffDialog::OnRecord); - connect(m_include_btn, &QPushButton::pressed, [this]() { Update(UpdateType::Include); }); - connect(m_exclude_btn, &QPushButton::pressed, [this]() { Update(UpdateType::Exclude); }); - connect(m_matching_results_table, &QTableWidget::itemClicked, [this]() { OnClickItem(); }); - connect(m_save_btn, &QPushButton::pressed, this, &CodeDiffDialog::SaveDataBackup); - connect(m_load_btn, &QPushButton::pressed, this, &CodeDiffDialog::LoadDataBackup); - connect(m_reset_btn, &QPushButton::pressed, this, &CodeDiffDialog::ClearData); - connect(m_help_btn, &QPushButton::pressed, this, &CodeDiffDialog::InfoDisp); - connect(m_matching_results_table, &CodeDiffDialog::customContextMenuRequested, this, - &CodeDiffDialog::OnContextMenu); -} - -void CodeDiffDialog::OnClickItem() -{ - UpdateItem(); - auto address = m_matching_results_table->currentItem()->data(Qt::UserRole).toUInt(); - m_code_widget->SetAddress(address, CodeViewWidget::SetAddressUpdate::WithDetailedUpdate); -} - -void CodeDiffDialog::SaveDataBackup() -{ - if (Core::GetState() == Core::State::Uninitialized) - { - ModalMessageBox::information(this, tr("Code Diff Tool"), - tr("Emulation must be started before saving a file.")); - return; - } - - if (m_include.empty()) - return; - - std::string filename = - File::GetUserPath(D_LOGS_IDX) + SConfig::GetInstance().GetGameID() + "_CodeDiff.txt"; - File::IOFile f(filename, "w"); - if (!f) - { - ModalMessageBox::information( - this, tr("Code Diff Tool"), - tr("Failed to save file to: %1").arg(QString::fromStdString(filename))); - return; - } - - // Copy list of BLR tested functions: - std::set address_blr; - for (int i = 0; i < m_matching_results_table->rowCount(); i++) - { - if (m_matching_results_table->item(i, 4)->text() == QStringLiteral("X")) - address_blr.insert(m_matching_results_table->item(i, 4)->data(Qt::UserRole).toUInt()); - } - - for (const auto& line : m_include) - { - bool blr = address_blr.contains(line.addr); - f.WriteString( - fmt::format("{} {} {} {:d} {}\n", line.addr, line.hits, line.total_hits, blr, line.symbol)); - } -} - -void CodeDiffDialog::LoadDataBackup() -{ - if (Core::GetState() == Core::State::Uninitialized) - { - ModalMessageBox::information(this, tr("Code Diff Tool"), - tr("Emulation must be started before loading a file.")); - return; - } - - if (g_symbolDB.IsEmpty()) - { - ModalMessageBox::warning( - this, tr("Code Diff Tool"), - tr("Symbol map not found.\n\nIf one does not exist, you can generate one from " - "the Menu bar:\nSymbols -> Generate Symbols From ->\n\tAddress | Signature " - "Database | RSO Modules")); - return; - } - - std::string filename = - File::GetUserPath(D_LOGS_IDX) + SConfig::GetInstance().GetGameID() + "_CodeDiff.txt"; - File::IOFile f(filename, "r"); - if (!f) - { - ModalMessageBox::information( - this, tr("Code Diff Tool"), - tr("Failed to find or open file: %1").arg(QString::fromStdString(filename))); - return; - }; - - ClearData(); - - std::set blr_addresses; - char line[512]; - while (fgets(line, 512, f.GetHandle())) - { - bool blr = false; - Diff temp; - std::istringstream iss(line); - iss.imbue(std::locale::classic()); - iss >> temp.addr >> temp.hits >> temp.total_hits >> blr >> std::ws; - std::getline(iss, temp.symbol); - - if (blr) - blr_addresses.insert(temp.addr); - - m_include.push_back(std::move(temp)); - } - - Update(UpdateType::Backup); - - for (int i = 0; i < m_matching_results_table->rowCount(); i++) - { - if (blr_addresses.contains(m_matching_results_table->item(i, 4)->data(Qt::UserRole).toUInt())) - MarkRowBLR(i); - } -} - -void CodeDiffDialog::ClearData() -{ - if (m_record_btn->isChecked()) - m_record_btn->toggle(); - ClearBlockCache(); - m_matching_results_table->clear(); - m_matching_results_table->setRowCount(0); - m_matching_results_table->setHorizontalHeaderLabels( - {tr("Address"), tr("Total Hits"), tr("Hits"), tr("Symbol"), tr("Inspected")}); - m_matching_results_table->setEditTriggers(QAbstractItemView::EditTrigger::NoEditTriggers); - m_exclude_size_label->setText(tr("Excluded: %1").arg(0)); - m_include_size_label->setText(tr("Included: %1").arg(0)); - m_exclude_btn->setEnabled(false); - m_include_btn->setEnabled(false); - m_include_active = false; - // Swap is used instead of clear for efficiency in the case of huge m_include/m_exclude - std::vector().swap(m_include); - std::vector().swap(m_exclude); - Core::System::GetInstance().GetJitInterface().SetProfilingState( - JitInterface::ProfilingState::Disabled); -} - -void CodeDiffDialog::ClearBlockCache() -{ - Core::State old_state = Core::GetState(); - - if (old_state == Core::State::Running) - Core::SetState(Core::State::Paused, false); - - Core::System::GetInstance().GetJitInterface().ClearCache(); - - if (old_state == Core::State::Running) - Core::SetState(Core::State::Running); -} - -void CodeDiffDialog::OnRecord(bool enabled) -{ - if (m_failed_requirements) - { - m_failed_requirements = false; - return; - } - - if (Core::GetState() == Core::State::Uninitialized) - { - ModalMessageBox::information(this, tr("Code Diff Tool"), - tr("Emulation must be started to record.")); - m_failed_requirements = true; - m_record_btn->setChecked(false); - return; - } - - if (g_symbolDB.IsEmpty()) - { - ModalMessageBox::warning( - this, tr("Code Diff Tool"), - tr("Symbol map not found.\n\nIf one does not exist, you can generate one from " - "the Menu bar:\nSymbols -> Generate Symbols From ->\n\tAddress | Signature " - "Database | RSO Modules")); - m_failed_requirements = true; - m_record_btn->setChecked(false); - return; - } - - JitInterface::ProfilingState state; - - if (enabled) - { - ClearBlockCache(); - m_record_btn->setText(tr("Stop Recording")); - state = JitInterface::ProfilingState::Enabled; - m_exclude_btn->setEnabled(true); - m_include_btn->setEnabled(true); - } - else - { - ClearBlockCache(); - m_record_btn->setText(tr("Start Recording")); - state = JitInterface::ProfilingState::Disabled; - m_exclude_btn->setEnabled(false); - m_include_btn->setEnabled(false); - } - - m_record_btn->update(); - Core::System::GetInstance().GetJitInterface().SetProfilingState(state); -} - -void CodeDiffDialog::OnInclude() -{ - const auto recorded_symbols = CalculateSymbolsFromProfile(); - - if (recorded_symbols.empty()) - return; - - if (m_include.empty() && m_exclude.empty()) - { - m_include = recorded_symbols; - m_include_active = true; - } - else if (m_include.empty()) - { - // If include becomes empty after having items on it, don't refill it until after a reset. - if (m_include_active) - return; - - // If we are building include for the first time and we have an exlcude list, then include = - // recorded - excluded. - m_include = recorded_symbols; - RemoveMatchingSymbolsFromIncludes(m_exclude); - m_include_active = true; - } - else - { - // If include already exists, keep items that are in both include and recorded. Exclude list - // becomes irrelevant. - RemoveMissingSymbolsFromIncludes(recorded_symbols); - } -} - -void CodeDiffDialog::OnExclude() -{ - const auto recorded_symbols = CalculateSymbolsFromProfile(); - if (m_include.empty() && m_exclude.empty()) - { - m_exclude = recorded_symbols; - } - else if (m_include.empty()) - { - // If there is only an exclude list, update it. - for (auto& iter : recorded_symbols) - { - auto pos = std::lower_bound(m_exclude.begin(), m_exclude.end(), iter.symbol); - - if (pos == m_exclude.end() || pos->symbol != iter.symbol) - m_exclude.insert(pos, iter); - } - } - else - { - // If include already exists, the exclude list will have been used to trim it, so the exclude - // list is now irrelevant, as anythng not on the include list is effectively excluded. - // Exclude/subtract recorded items from the include list. - RemoveMatchingSymbolsFromIncludes(recorded_symbols); - } -} - -std::vector CodeDiffDialog::CalculateSymbolsFromProfile() const -{ - Profiler::ProfileStats prof_stats; - auto& blockstats = prof_stats.block_stats; - Core::System::GetInstance().GetJitInterface().GetProfileResults(&prof_stats); - std::vector current; - current.reserve(20000); - - // Convert blockstats to smaller struct Diff. Exclude repeat functions via symbols. - for (const auto& iter : blockstats) - { - std::string symbol = g_symbolDB.GetDescription(iter.addr); - if (!std::any_of(current.begin(), current.end(), - [&symbol](const Diff& v) { return v.symbol == symbol; })) - { - current.push_back(Diff{ - .addr = iter.addr, - .symbol = std::move(symbol), - .hits = static_cast(iter.run_count), - .total_hits = static_cast(iter.run_count), - }); - } - } - - std::sort(current.begin(), current.end(), - [](const Diff& v1, const Diff& v2) { return (v1.symbol < v2.symbol); }); - - return current; -} - -void CodeDiffDialog::RemoveMissingSymbolsFromIncludes(const std::vector& symbol_diff) -{ - m_include.erase(std::remove_if(m_include.begin(), m_include.end(), - [&](const Diff& v) { - auto arg = std::none_of( - symbol_diff.begin(), symbol_diff.end(), [&](const Diff& p) { - return p.symbol == v.symbol || p.addr == v.addr; - }); - return arg; - }), - m_include.end()); - for (auto& original_includes : m_include) - { - auto pos = std::lower_bound(symbol_diff.begin(), symbol_diff.end(), original_includes.symbol); - if (pos != symbol_diff.end() && - (pos->symbol == original_includes.symbol || pos->addr == original_includes.addr)) - { - original_includes.total_hits += pos->hits; - original_includes.hits = pos->hits; - } - } -} - -void CodeDiffDialog::RemoveMatchingSymbolsFromIncludes(const std::vector& symbol_list) -{ - m_include.erase(std::remove_if(m_include.begin(), m_include.end(), - [&](const Diff& i) { - return std::any_of( - symbol_list.begin(), symbol_list.end(), [&](const Diff& s) { - return i.symbol == s.symbol || i.addr == s.addr; - }); - }), - m_include.end()); -} - -void CodeDiffDialog::Update(UpdateType type) -{ - // Wrap everything in a pause - Core::State old_state = Core::GetState(); - if (old_state == Core::State::Running) - Core::SetState(Core::State::Paused, false); - - // Main process - if (type == UpdateType::Include) - { - OnInclude(); - } - else if (type == UpdateType::Exclude) - { - OnExclude(); - } - - if (type != UpdateType::Backup && m_autosave_check->isChecked() && !m_include.empty()) - SaveDataBackup(); - - const auto create_item = [](const QString& string = {}, const u32 address = 0x00000000) { - QTableWidgetItem* item = new QTableWidgetItem(string); - item->setData(Qt::UserRole, address); - item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - return item; - }; - - int i = 0; - m_matching_results_table->clear(); - m_matching_results_table->setRowCount(i); - m_matching_results_table->setHorizontalHeaderLabels( - {tr("Address"), tr("Total Hits"), tr("Hits"), tr("Symbol"), tr("Inspected")}); - - for (auto& iter : m_include) - { - m_matching_results_table->setRowCount(i + 1); - - QString fix_sym = QString::fromStdString(iter.symbol); - fix_sym.replace(QStringLiteral("\t"), QStringLiteral(" ")); - - m_matching_results_table->setItem( - i, 0, create_item(QStringLiteral("%1").arg(iter.addr, 1, 16), iter.addr)); - m_matching_results_table->setItem( - i, 1, create_item(QStringLiteral("%1").arg(iter.total_hits), iter.addr)); - m_matching_results_table->setItem(i, 2, - create_item(QStringLiteral("%1").arg(iter.hits), iter.addr)); - m_matching_results_table->setItem(i, 3, - create_item(QStringLiteral("%1").arg(fix_sym), iter.addr)); - m_matching_results_table->setItem(i, 4, create_item(QStringLiteral(""), iter.addr)); - i++; - } - - // If we have ruled out all functions from being included. - if (m_include_active && m_include.empty()) - { - m_matching_results_table->setRowCount(1); - m_matching_results_table->setItem(0, 3, create_item(tr("No possible functions left. Reset."))); - } - - m_exclude_size_label->setText(tr("Excluded: %1").arg(m_exclude.size())); - m_include_size_label->setText(tr("Included: %1").arg(m_include.size())); - - Core::System::GetInstance().GetJitInterface().ClearCache(); - if (old_state == Core::State::Running) - Core::SetState(Core::State::Running); -} - -void CodeDiffDialog::InfoDisp() -{ - ModalMessageBox::information( - this, tr("Code Diff Tool Help"), - tr("Used to find functions based on when they should be running.\nSimilar to Cheat Engine " - "Ultimap.\n" - "A symbol map must be loaded prior to use.\n" - "Include/Exclude lists will persist on ending/restarting emulation.\nThese lists " - "will not persist on Dolphin close." - "\n\n'Start Recording': " - "keeps track of what functions run.\n'Stop Recording': erases current " - "recording without any change to the lists.\n'Code did not get executed': click while " - "recording, will add recorded functions to an exclude " - "list, then reset the recording list.\n'Code has been executed': click while recording, " - "will add recorded function to an include list, then reset the recording list.\n\nAfter " - "you use " - "both exclude and include once, the exclude list will be subtracted from the include " - "list " - "and any includes left over will be displayed.\nYou can continue to use " - "'Code did not get executed'/'Code has been executed' to narrow down the " - "results.\n\n" - "Saving will store the current list in Dolphin's Log folder (File -> Open User " - "Folder)")); - ModalMessageBox::information( - this, tr("Code Diff Tool Help"), - tr("Example:\n" - "You want to find a function that runs when HP is modified.\n1. Start recording and " - "play the game without letting HP be modified, then press 'Code did not get " - "executed'.\n2. Immediately gain/lose HP and press 'Code has been executed'.\n3. Repeat " - "1 or 2 to " - "narrow down the results.\nIncludes (Code has been executed) should " - "have short recordings focusing on what you want.\n\nPressing 'Code has been " - "executed' twice will only keep functions that ran for both recordings. Hits will update " - "to reflect the last recording's " - "number of Hits. Total Hits will reflect the total number of " - "times a function has been executed until the lists are cleared with Reset.\n\nRight " - "click -> 'Set blr' will place a " - "blr at the top of the symbol.\n")); -} - -void CodeDiffDialog::OnContextMenu() -{ - if (m_matching_results_table->currentItem() == nullptr) - return; - UpdateItem(); - QMenu* menu = new QMenu(this); - menu->addAction(tr("&Go to start of function"), this, &CodeDiffDialog::OnGoTop); - menu->addAction(tr("Set &blr"), this, &CodeDiffDialog::OnSetBLR); - menu->addAction(tr("&Delete"), this, &CodeDiffDialog::OnDelete); - menu->exec(QCursor::pos()); -} - -void CodeDiffDialog::OnGoTop() -{ - auto item = m_matching_results_table->currentItem(); - if (!item) - return; - Common::Symbol* symbol = g_symbolDB.GetSymbolFromAddr(item->data(Qt::UserRole).toUInt()); - if (!symbol) - return; - m_code_widget->SetAddress(symbol->address, CodeViewWidget::SetAddressUpdate::WithDetailedUpdate); -} - -void CodeDiffDialog::OnDelete() -{ - // Delete from include list and qtable widget - auto item = m_matching_results_table->currentItem(); - if (!item) - return; - int row = m_matching_results_table->row(item); - if (row == -1) - return; - // TODO: If/when sorting is ever added, .erase needs to find item position instead; leaving as is - // for performance - if (!m_include.empty()) - { - m_include.erase(m_include.begin() + row); - } - m_matching_results_table->removeRow(row); -} - -void CodeDiffDialog::OnSetBLR() -{ - auto item = m_matching_results_table->currentItem(); - if (!item) - return; - - Common::Symbol* symbol = g_symbolDB.GetSymbolFromAddr(item->data(Qt::UserRole).toUInt()); - if (!symbol) - return; - - MarkRowBLR(item->row()); - if (m_autosave_check->isChecked()) - SaveDataBackup(); - - { - auto& system = Core::System::GetInstance(); - Core::CPUThreadGuard guard(system); - system.GetPowerPC().GetDebugInterface().SetPatch(guard, symbol->address, 0x4E800020); - } - - m_code_widget->Update(); -} - -void CodeDiffDialog::MarkRowBLR(int row) -{ - m_matching_results_table->item(row, 0)->setForeground(QBrush(Qt::red)); - m_matching_results_table->item(row, 1)->setForeground(QBrush(Qt::red)); - m_matching_results_table->item(row, 2)->setForeground(QBrush(Qt::red)); - m_matching_results_table->item(row, 3)->setForeground(QBrush(Qt::red)); - m_matching_results_table->item(row, 4)->setForeground(QBrush(Qt::red)); - m_matching_results_table->item(row, 4)->setText(QStringLiteral("X")); -} - -void CodeDiffDialog::UpdateItem() -{ - QTableWidgetItem* item = m_matching_results_table->currentItem(); - if (!item) - return; - - int row = m_matching_results_table->row(item); - if (row == -1) - return; - uint address = item->data(Qt::UserRole).toUInt(); - - auto symbolName = g_symbolDB.GetDescription(address); - if (symbolName == " --- ") - return; - - QString newName = - QString::fromStdString(symbolName).replace(QStringLiteral("\t"), QStringLiteral(" ")); - m_matching_results_table->item(row, 3)->setText(newName); -} - -void CodeDiffDialog::UpdateButtons(bool running) -{ - m_save_btn->setEnabled(running); - m_load_btn->setEnabled(running); - m_record_btn->setEnabled(running); -} diff --git a/Source/Core/DolphinQt/Debugger/CodeDiffDialog.h b/Source/Core/DolphinQt/Debugger/CodeDiffDialog.h deleted file mode 100644 index c22c63e1ad..0000000000 --- a/Source/Core/DolphinQt/Debugger/CodeDiffDialog.h +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2022 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include -#include "Common/CommonTypes.h" - -class CodeWidget; -class QLabel; -class QPushButton; -class QCheckBox; -class QTableWidget; - -struct Diff -{ - u32 addr = 0; - std::string symbol; - u32 hits = 0; - u32 total_hits = 0; - - bool operator<(const std::string& val) const { return symbol < val; } -}; - -class CodeDiffDialog : public QDialog -{ - Q_OBJECT - -public: - explicit CodeDiffDialog(CodeWidget* parent); - void reject() override; - -private: - enum class UpdateType - { - Include, - Exclude, - Backup - }; - - void CreateWidgets(); - void ConnectWidgets(); - void SaveDataBackup(); - void LoadDataBackup(); - void ClearData(); - void ClearBlockCache(); - void OnClickItem(); - void OnRecord(bool enabled); - std::vector CalculateSymbolsFromProfile() const; - void OnInclude(); - void OnExclude(); - void RemoveMissingSymbolsFromIncludes(const std::vector& symbol_diff); - void RemoveMatchingSymbolsFromIncludes(const std::vector& symbol_list); - void Update(UpdateType type); - void InfoDisp(); - - void OnContextMenu(); - - void OnGoTop(); - void OnDelete(); - void OnSetBLR(); - - void MarkRowBLR(int row); - void UpdateItem(); - void UpdateButtons(bool running); - - QTableWidget* m_matching_results_table; - QCheckBox* m_autosave_check; - QLabel* m_exclude_size_label; - QLabel* m_include_size_label; - QPushButton* m_exclude_btn; - QPushButton* m_include_btn; - QPushButton* m_record_btn; - QPushButton* m_reset_btn; - QPushButton* m_save_btn; - QPushButton* m_load_btn; - QPushButton* m_help_btn; - CodeWidget* m_code_widget; - - std::vector m_exclude; - std::vector m_include; - bool m_failed_requirements = false; - bool m_include_active = false; -}; diff --git a/Source/Core/DolphinQt/Debugger/CodeWidget.cpp b/Source/Core/DolphinQt/Debugger/CodeWidget.cpp index 0bf2937dbf..1b9989e401 100644 --- a/Source/Core/DolphinQt/Debugger/CodeWidget.cpp +++ b/Source/Core/DolphinQt/Debugger/CodeWidget.cpp @@ -27,6 +27,7 @@ #include "Core/PowerPC/PPCSymbolDB.h" #include "Core/PowerPC/PowerPC.h" #include "Core/System.h" +#include "DolphinQt/Debugger/BranchWatchDialog.h" #include "DolphinQt/Host.h" #include "DolphinQt/QtUtils/SetWindowDecorations.h" #include "DolphinQt/Settings.h" @@ -35,7 +36,10 @@ static const QString BOX_SPLITTER_STYLESHEET = QStringLiteral( "QSplitter::handle { border-top: 1px dashed black; width: 1px; margin-left: 10px; " "margin-right: 10px; }"); -CodeWidget::CodeWidget(QWidget* parent) : QDockWidget(parent), m_system(Core::System::GetInstance()) +CodeWidget::CodeWidget(QWidget* parent) + : QDockWidget(parent), m_system(Core::System::GetInstance()), + m_branch_watch_dialog( + new BranchWatchDialog(m_system, m_system.GetPowerPC().GetBranchWatch(), this)) { setWindowTitle(tr("Code")); setObjectName(QStringLiteral("code")); @@ -105,7 +109,7 @@ void CodeWidget::CreateWidgets() layout->setSpacing(0); m_search_address = new QLineEdit; - m_code_diff = new QPushButton(tr("Diff")); + m_branch_watch = new QPushButton(tr("Branch Watch")); m_code_view = new CodeViewWidget; m_search_address->setPlaceholderText(tr("Search Address")); @@ -149,7 +153,7 @@ void CodeWidget::CreateWidgets() m_code_splitter->addWidget(m_code_view); layout->addWidget(m_search_address, 0, 0); - layout->addWidget(m_code_diff, 0, 2); + layout->addWidget(m_branch_watch, 0, 2); layout->addWidget(m_code_splitter, 1, 0, -1, -1); QWidget* widget = new QWidget(this); @@ -181,7 +185,7 @@ void CodeWidget::ConnectWidgets() }); connect(m_search_callstack, &QLineEdit::textChanged, this, &CodeWidget::UpdateCallstack); - connect(m_code_diff, &QPushButton::pressed, this, &CodeWidget::OnDiff); + connect(m_branch_watch, &QPushButton::pressed, this, &CodeWidget::OnBranchWatchDialog); connect(m_symbols_list, &QListWidget::itemPressed, this, &CodeWidget::OnSelectSymbol); connect(m_callstack_list, &QListWidget::itemPressed, this, &CodeWidget::OnSelectCallstack); @@ -209,15 +213,11 @@ void CodeWidget::ConnectWidgets() connect(m_code_view, &CodeViewWidget::ShowMemory, this, &CodeWidget::ShowMemory); } -void CodeWidget::OnDiff() +void CodeWidget::OnBranchWatchDialog() { - if (!m_diff_dialog) - m_diff_dialog = new CodeDiffDialog(this); - m_diff_dialog->setWindowFlag(Qt::WindowMinimizeButtonHint); - SetQWidgetWindowDecorations(m_diff_dialog); - m_diff_dialog->show(); - m_diff_dialog->raise(); - m_diff_dialog->activateWindow(); + m_branch_watch_dialog->open(); + m_branch_watch_dialog->raise(); + m_branch_watch_dialog->activateWindow(); } void CodeWidget::OnSearchAddress() @@ -394,6 +394,10 @@ void CodeWidget::UpdateSymbols() } m_symbols_list->sortItems(); + + // TODO: There seems to be a lack of a ubiquitous signal for when symbols change. + // This is the best location to catch the signals from MenuBar and CodeViewWidget. + m_branch_watch_dialog->UpdateSymbols(); } void CodeWidget::UpdateFunctionCalls(const Common::Symbol* symbol) @@ -464,6 +468,9 @@ void CodeWidget::Step() power_pc.SetMode(old_mode); Core::DisplayMessage(tr("Step successful!").toStdString(), 2000); // Will get a UpdateDisasmDialog(), don't update the GUI here. + + // TODO: Step doesn't cause EmulationStateChanged to be emitted, so it has to call this manually. + m_branch_watch_dialog->Update(); } void CodeWidget::StepOver() diff --git a/Source/Core/DolphinQt/Debugger/CodeWidget.h b/Source/Core/DolphinQt/Debugger/CodeWidget.h index 1e933a8c70..e0a5679bfb 100644 --- a/Source/Core/DolphinQt/Debugger/CodeWidget.h +++ b/Source/Core/DolphinQt/Debugger/CodeWidget.h @@ -7,9 +7,9 @@ #include #include "Common/CommonTypes.h" -#include "DolphinQt/Debugger/CodeDiffDialog.h" #include "DolphinQt/Debugger/CodeViewWidget.h" +class BranchWatchDialog; class QCloseEvent; class QLineEdit; class QShowEvent; @@ -41,7 +41,7 @@ public: void ShowPC(); void SetPC(); - void OnDiff(); + void OnBranchWatchDialog(); void ToggleBreakpoint(); void AddBreakpoint(); void SetAddress(u32 address, CodeViewWidget::SetAddressUpdate update); @@ -72,9 +72,9 @@ private: Core::System& m_system; - CodeDiffDialog* m_diff_dialog = nullptr; + BranchWatchDialog* m_branch_watch_dialog; QLineEdit* m_search_address; - QPushButton* m_code_diff; + QPushButton* m_branch_watch; QLineEdit* m_search_callstack; QListWidget* m_callstack_list; diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 063a048880..4cdb14fb33 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -137,9 +137,10 @@ + + - @@ -349,9 +350,10 @@ + + - diff --git a/Source/Core/UICommon/UICommon.cpp b/Source/Core/UICommon/UICommon.cpp index eff685e5c7..5ff9364dd0 100644 --- a/Source/Core/UICommon/UICommon.cpp +++ b/Source/Core/UICommon/UICommon.cpp @@ -74,6 +74,8 @@ static void CreateDumpPath(std::string path) File::CreateFullPath(File::GetUserPath(D_DUMPFRAMES_IDX)); File::CreateFullPath(File::GetUserPath(D_DUMPOBJECTS_IDX)); File::CreateFullPath(File::GetUserPath(D_DUMPTEXTURES_IDX)); + File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_IDX)); + File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)); } static void CreateLoadPath(std::string path) @@ -253,6 +255,8 @@ void CreateDirectories() File::CreateFullPath(File::GetUserPath(D_DUMPDSP_IDX)); File::CreateFullPath(File::GetUserPath(D_DUMPSSL_IDX)); File::CreateFullPath(File::GetUserPath(D_DUMPTEXTURES_IDX)); + File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_IDX)); + File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)); File::CreateFullPath(File::GetUserPath(D_GAMESETTINGS_IDX)); File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX)); File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX) + USA_DIR DIR_SEP);