// Copyright 2024 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "Core/Debugger/BranchWatch.h" #include #include #include #include #include "Common/Assert.h" #include "Common/BitField.h" #include "Common/CommonTypes.h" #include "Core/Core.h" #include "Core/PowerPC/Gekko.h" #include "Core/PowerPC/MMU.h" namespace Core { void BranchWatch::Clear(const CPUThreadGuard&) { m_selection.clear(); m_collection_vt.clear(); m_collection_vf.clear(); m_collection_pt.clear(); m_collection_pf.clear(); m_recording_phase = Phase::Blacklist; m_blacklist_size = 0; } // This is a bitfield aggregate of metadata required to reconstruct a BranchWatch's Collections and // Selection from a text file (a snapshot). For maximum forward compatibility, should that ever be // required, the StorageType is an unsigned long long instead of something more reasonable like an // unsigned int or u8. This is because the snapshot text file format contains no version info. union USnapshotMetadata { using Inspection = BranchWatch::SelectionInspection; using StorageType = unsigned long long; static_assert(Inspection::EndOfEnumeration == Inspection{(1u << 5) + 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, 6, 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 bool is_reduction_phase = GetRecordingPhase() == Phase::Reduction; const auto routine = [&](const Collection& collection, bool is_virtual, bool condition) { for (const Collection::value_type& kv : collection) { const auto iter = std::ranges::find_if(m_selection, [&](const Selection::value_type& value) { return value.collection_ptr == &kv; }); const bool selected = iter != m_selection.end(); if (is_reduction_phase && !selected) continue; // Unselected hits are irrelevant to the reduction phase. const auto inspection = selected ? iter->inspection : SelectionInspection{}; 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, USnapshotMetadata(is_virtual, condition, selected, 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(guard.GetSystem()) == 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(guard.GetSystem()) == 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::ranges::for_each(m_selection, [](Selection::value_type& value) { value.inspection = {}; }); } void BranchWatch::SetSelectedInspected(std::size_t idx, SelectionInspection inspection) { m_selection[idx].inspection |= inspection; } } // namespace Core