Merge pull request #12359 from mitaclaw/code-diff-dialog-refresh

BranchWatchDialog: A Total Replacement for CodeDiffDialog
This commit is contained in:
Admiral H. Curtiss 2024-03-02 14:13:54 +01:00 committed by GitHub
commit f2e04e0603
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 2938 additions and 839 deletions

View File

@ -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"

View File

@ -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;

View File

@ -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,

View File

@ -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;
}

View File

@ -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

View File

@ -0,0 +1,314 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/Debugger/BranchWatch.h"
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <fmt/format.h>
#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

View File

@ -0,0 +1,278 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstddef>
#include <cstdio>
#include <functional>
#include <unordered_map>
#include <vector>
#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<u64>(*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<Core::BranchWatchCollectionKey>
{
std::size_t operator()(const Core::BranchWatchCollectionKey& s) const noexcept
{
return std::hash<u64>{}(static_cast<const Core::FakeBranchWatchCollectionKey&>(s));
}
};
namespace Core
{
inline bool operator==(const BranchWatchCollectionKey& lhs,
const BranchWatchCollectionKey& rhs) noexcept
{
const std::hash<BranchWatchCollectionKey> 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<BranchWatchSelectionInspection>(Common::ToUnderlying(lhs) |
Common::ToUnderlying(rhs));
}
constexpr BranchWatchSelectionInspection operator&(BranchWatchSelectionInspection lhs,
BranchWatchSelectionInspection rhs)
{
return static_cast<BranchWatchSelectionInspection>(Common::ToUnderlying(lhs) &
Common::ToUnderlying(rhs));
}
constexpr BranchWatchSelectionInspection& operator|=(BranchWatchSelectionInspection& self,
BranchWatchSelectionInspection other)
{
return self = self | other;
}
using BranchWatchCollection =
std::unordered_map<BranchWatchCollectionKey, BranchWatchCollectionValue>;
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<BranchWatchSelectionValueType>;
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<FakeBranchWatchCollectionKey>(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<FakeBranchWatchCollectionKey>(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<FakeBranchWatchCollectionKey>(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<FakeBranchWatchCollectionKey>(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<FakeBranchWatchCollectionKey>(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<FakeBranchWatchCollectionKey>(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

View File

@ -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)
{
}

View File

@ -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;

View File

@ -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;

View File

@ -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<true>(op.address, op.branchTo, op.inst, RSCRATCH, RSCRATCH2,
CallerSavedRegistersInUse());
}
}
else
{
if ((opinfo->flags & FL_USE_FPU) && !js.firstFPInstructionFound)
{

View File

@ -98,6 +98,12 @@ public:
void WriteExternalExceptionExit();
void WriteRfiExitDestInRSCRATCH();
void WriteIdleExit(u32 destination);
template <bool condition>
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);

View File

@ -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 <bool condition>
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<true>(u32, u32, UGeckoInstruction, X64Reg, X64Reg, BitSet32);
template void Jit64::WriteBranchWatch<false>(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<true>(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<true>(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<true>(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<true>(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<false>(js.compilerPC, js.compilerPC + 4, inst, ABI_PARAM1, RSCRATCH, {});
}
WriteExit(js.compilerPC + 4);
}
else if (IsDebuggingEnabled())
{
WriteBranchWatch<false>(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<false>(js.compilerPC, js.compilerPC + 4, inst, ABI_PARAM1, RSCRATCH, {});
}
WriteExit(js.compilerPC + 4);
}
else if (IsDebuggingEnabled())
{
WriteBranchWatch<false>(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<true>(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<false>(js.compilerPC, js.compilerPC + 4, inst, ABI_PARAM1, RSCRATCH, {});
}
WriteExit(js.compilerPC + 4);
}
else if (IsDebuggingEnabled())
{
WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, RSCRATCH, RSCRATCH2,
CallerSavedRegistersInUse());
}
}

View File

@ -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<true>(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<true>(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<false>(nextPC, nextPC + 4, next, ABI_PARAM1, RSCRATCH, {});
}
WriteExit(nextPC + 4);
}
else if (IsDebuggingEnabled())
{
WriteBranchWatch<false>(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<false>(nextPC, nextPC + 4, next, ABI_PARAM1, RSCRATCH, {});
}
WriteExit(nextPC + 4);
}
else if (IsDebuggingEnabled())
{
WriteBranchWatch<false>(nextPC, nextPC + 4, next, RSCRATCH, RSCRATCH2,
CallerSavedRegistersInUse());
}
}
void Jit64::cmpXX(UGeckoInstruction inst)

View File

@ -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;

View File

@ -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<true>(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)
{

View File

@ -315,6 +315,16 @@ protected:
void MSRUpdated(u32 msr);
void MSRUpdated(Arm64Gen::ARM64Reg msr);
// Branch Watch
template <bool condition>
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,

View File

@ -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 <bool condition>
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<true>(u32, u32, UGeckoInstruction, ARM64Reg, ARM64Reg,
BitSet32, BitSet32);
template void JitArm64::WriteBranchWatch<false>(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<true>(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<true>(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<true>(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<true>(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<false>(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<false>(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<false>(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<false>(js.compilerPC, js.compilerPC + 4, inst, WA, WB, gpr_caller_save,
fpr.GetCallerSavedUsed());
}
gpr.Unlock(WA);
if (WB != ARM64Reg::INVALID_REG)

View File

@ -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);
}

View File

@ -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())

View File

@ -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);

View File

@ -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;

View File

@ -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)
{
}

View File

@ -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<SoundStream> 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

View File

@ -202,6 +202,7 @@
<ClInclude Include="Core\Core.h" />
<ClInclude Include="Core\CoreTiming.h" />
<ClInclude Include="Core\CPUThreadConfigCallback.h" />
<ClInclude Include="Core\Debugger\BranchWatch.h" />
<ClInclude Include="Core\Debugger\CodeTrace.h" />
<ClInclude Include="Core\Debugger\DebugInterface.h" />
<ClInclude Include="Core\Debugger\Debugger_SymbolMap.h" />
@ -868,6 +869,7 @@
<ClCompile Include="Core\Core.cpp" />
<ClCompile Include="Core\CoreTiming.cpp" />
<ClCompile Include="Core\CPUThreadConfigCallback.cpp" />
<ClCompile Include="Core\Debugger\BranchWatch.cpp" />
<ClCompile Include="Core\Debugger\CodeTrace.cpp" />
<ClCompile Include="Core\Debugger\Debugger_SymbolMap.cpp" />
<ClCompile Include="Core\Debugger\Dump.cpp" />

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,114 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <optional>
#include <string>
#include <QDialog>
#include <QModelIndexList>
#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<std::string> m_autosave_filepath;
};

View File

@ -0,0 +1,502 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Debugger/BranchWatchTableModel.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <QBrush>
#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<const char*, Column::NumberOfColumns> 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<int>(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<int> 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<int> 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<int> 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<int> 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<u32>(), false);
return;
case Column::DestinSymbol:
SetSymbolInspected(m_symbol_list[row].destin_addr.value<u32>(), false);
return;
}
}
void BranchWatchTableModel::SetOriginInspected(u32 origin_addr)
{
using Inspection = Core::BranchWatchSelectionInspection;
static const QList<int> 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<int>(i), Column::Origin);
emit dataChanged(index, index, roles);
}
}
void BranchWatchTableModel::SetDestinInspected(u32 destin_addr, bool nested)
{
using Inspection = Core::BranchWatchSelectionInspection;
static const QList<int> 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<int>(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<int> 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<u32>() == 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<u32>() == 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();
}

View File

@ -0,0 +1,119 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstdio>
#include <QAbstractTableModel>
#include <QFont>
#include <QList>
#include <QVariant>
#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<SymbolListValueType>;
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;
};

View File

@ -1,673 +0,0 @@
// Copyright 2022 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Debugger/CodeDiffDialog.h"
#include <algorithm>
#include <sstream>
#include <string>
#include <vector>
#include <QCheckBox>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QPushButton>
#include <QStyleHints>
#include <QTableWidget>
#include <QVBoxLayout>
#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<u32> 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<u32> 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<Diff>().swap(m_include);
std::vector<Diff>().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<Diff> CodeDiffDialog::CalculateSymbolsFromProfile() const
{
Profiler::ProfileStats prof_stats;
auto& blockstats = prof_stats.block_stats;
Core::System::GetInstance().GetJitInterface().GetProfileResults(&prof_stats);
std::vector<Diff> 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<u32>(iter.run_count),
.total_hits = static_cast<u32>(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<Diff>& 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<Diff>& 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);
}

View File

@ -1,86 +0,0 @@
// Copyright 2022 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QDialog>
#include <string>
#include <vector>
#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<Diff> CalculateSymbolsFromProfile() const;
void OnInclude();
void OnExclude();
void RemoveMissingSymbolsFromIncludes(const std::vector<Diff>& symbol_diff);
void RemoveMatchingSymbolsFromIncludes(const std::vector<Diff>& 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<Diff> m_exclude;
std::vector<Diff> m_include;
bool m_failed_requirements = false;
bool m_include_active = false;
};

View File

@ -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()

View File

@ -7,9 +7,9 @@
#include <QString>
#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;

View File

@ -137,9 +137,10 @@
<ClCompile Include="Debugger\AssembleInstructionDialog.cpp" />
<ClCompile Include="Debugger\AssemblerWidget.cpp" />
<ClCompile Include="Debugger\AssemblyEditor.cpp" />
<ClCompile Include="Debugger\BranchWatchDialog.cpp" />
<ClCompile Include="Debugger\BranchWatchTableModel.cpp" />
<ClCompile Include="Debugger\BreakpointDialog.cpp" />
<ClCompile Include="Debugger\BreakpointWidget.cpp" />
<ClCompile Include="Debugger\CodeDiffDialog.cpp" />
<ClCompile Include="Debugger\CodeViewWidget.cpp" />
<ClCompile Include="Debugger\CodeWidget.cpp" />
<ClCompile Include="Debugger\GekkoSyntaxHighlight.cpp" />
@ -349,9 +350,10 @@
<QtMoc Include="Debugger\AssembleInstructionDialog.h" />
<QtMoc Include="Debugger\AssemblerWidget.h" />
<QtMoc Include="Debugger\AssemblyEditor.h" />
<QtMoc Include="Debugger\BranchWatchDialog.h" />
<QtMoc Include="Debugger\BranchWatchTableModel.h" />
<QtMoc Include="Debugger\BreakpointDialog.h" />
<QtMoc Include="Debugger\BreakpointWidget.h" />
<QtMoc Include="Debugger\CodeDiffDialog.h" />
<QtMoc Include="Debugger\CodeViewWidget.h" />
<QtMoc Include="Debugger\CodeWidget.h" />
<QtMoc Include="Debugger\GekkoSyntaxHighlight.h" />

View File

@ -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);