From 9bc31bc8fc0de6a8853dc3127fd432df32d1d2aa Mon Sep 17 00:00:00 2001 From: scawful Date: Wed, 8 Oct 2025 17:20:07 -0400 Subject: [PATCH] feat: Add breakpoint and watchpoint management for enhanced debugging - Introduced BreakpointManager and WatchpointManager classes to manage CPU breakpoints and memory watchpoints, respectively. - Implemented functionality for adding, removing, enabling, and disabling breakpoints and watchpoints. - Added support for conditional breakpoints and logging memory access history for watchpoints. - Enhanced the Emulator class to integrate breakpoint and instruction logging callbacks for improved debugging capabilities. - Updated DisassemblyViewer to record executed instructions and manage instruction limits for performance optimization. --- src/CMakeLists.txt | 2 + src/app/emu/cpu/cpu.cc | 67 +++++---- src/app/emu/cpu/cpu.h | 11 ++ src/app/emu/debug/breakpoint_manager.cc | 187 ++++++++++++++++++++++++ src/app/emu/debug/breakpoint_manager.h | 143 ++++++++++++++++++ src/app/emu/debug/disassembly_viewer.cc | 34 +++++ src/app/emu/debug/disassembly_viewer.h | 20 +++ src/app/emu/debug/watchpoint_manager.cc | 165 +++++++++++++++++++++ src/app/emu/debug/watchpoint_manager.h | 138 +++++++++++++++++ src/app/emu/emulator.cc | 66 +++++++++ src/app/emu/emulator.h | 12 ++ 11 files changed, 819 insertions(+), 26 deletions(-) create mode 100644 src/app/emu/debug/breakpoint_manager.cc create mode 100644 src/app/emu/debug/breakpoint_manager.h create mode 100644 src/app/emu/debug/watchpoint_manager.cc create mode 100644 src/app/emu/debug/watchpoint_manager.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 93900392..acd174a7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,8 @@ set( app/emu/cpu/internal/addressing.cc app/emu/cpu/internal/instructions.cc app/emu/debug/disassembly_viewer.cc + app/emu/debug/breakpoint_manager.cc + app/emu/debug/watchpoint_manager.cc app/emu/cpu/cpu.cc app/emu/video/ppu.cc app/emu/memory/dma.cc diff --git a/src/app/emu/cpu/cpu.cc b/src/app/emu/cpu/cpu.cc index f3ca7e93..bd51cd49 100644 --- a/src/app/emu/cpu/cpu.cc +++ b/src/app/emu/cpu/cpu.cc @@ -52,6 +52,15 @@ void Cpu::Reset(bool hard) { } void Cpu::RunOpcode() { + // Check for execute breakpoint BEFORE running instruction + if (on_breakpoint_hit_) { + uint32_t current_pc = (PB << 16) | PC; + if (on_breakpoint_hit_(current_pc)) { + // Breakpoint hit - pause execution + return; // Don't run this opcode yet + } + } + if (reset_wanted_) { reset_wanted_ = false; // reset: brk/interrupt without writes @@ -1901,34 +1910,40 @@ void Cpu::ExecuteInstruction(uint8_t opcode) { void Cpu::LogInstructions(uint16_t PC, uint8_t opcode, uint16_t operand, bool immediate, bool accumulator_mode) { - if (core::FeatureFlags::get().kLogInstructions) { - // Build full 24-bit address - uint32_t full_address = (PB << 16) | PC; - - // Extract operand bytes based on instruction size - std::vector operand_bytes; - std::string operand_str; - - if (operand) { - if (immediate) { - operand_str += "#"; - } - - if (accumulator_mode) { - // 8-bit operand - operand_bytes.push_back(operand & 0xFF); - operand_str += absl::StrFormat("$%02X", operand & 0xFF); - } else { - // 16-bit operand (little-endian) - operand_bytes.push_back(operand & 0xFF); - operand_bytes.push_back((operand >> 8) & 0xFF); - operand_str += absl::StrFormat("$%04X", operand); - } + // Build full 24-bit address + uint32_t full_address = (PB << 16) | PC; + + // Extract operand bytes based on instruction size + std::vector operand_bytes; + std::string operand_str; + + if (operand) { + if (immediate) { + operand_str += "#"; } - // Get mnemonic - const std::string& mnemonic = opcode_to_mnemonic.at(opcode); - + if (accumulator_mode) { + // 8-bit operand + operand_bytes.push_back(operand & 0xFF); + operand_str += absl::StrFormat("$%02X", operand & 0xFF); + } else { + // 16-bit operand (little-endian) + operand_bytes.push_back(operand & 0xFF); + operand_bytes.push_back((operand >> 8) & 0xFF); + operand_str += absl::StrFormat("$%04X", operand); + } + } + + // Get mnemonic + const std::string& mnemonic = opcode_to_mnemonic.at(opcode); + + // NEW: Call recording callback if set (for DisassemblyViewer) + if (on_instruction_executed_) { + on_instruction_executed_(full_address, opcode, operand_bytes, mnemonic, operand_str); + } + + // Legacy: Record to old disassembly viewer if feature flag enabled + if (core::FeatureFlags::get().kLogInstructions) { // Record to new disassembly viewer disassembly_viewer().RecordInstruction(full_address, opcode, operand_bytes, mnemonic, operand_str); diff --git a/src/app/emu/cpu/cpu.h b/src/app/emu/cpu/cpu.h index 7dfd9e2b..e7e3a03c 100644 --- a/src/app/emu/cpu/cpu.h +++ b/src/app/emu/cpu/cpu.h @@ -57,6 +57,13 @@ class Cpu { // New disassembly viewer debug::DisassemblyViewer& disassembly_viewer(); const debug::DisassemblyViewer& disassembly_viewer() const; + + // Breakpoint callback (set by Emulator) + std::function on_breakpoint_hit_; + + // Instruction recording callback (for DisassemblyViewer) + std::function& operands, + const std::string& mnemonic, const std::string& operand_str)> on_instruction_executed_; // Public register access for debugging and UI uint16_t A = 0; // Accumulator @@ -768,6 +775,10 @@ class Cpu { auto mutable_log_instructions() -> bool* { return &log_instructions_; } bool stopped() const { return stopped_; } + + // Instruction logging control + void SetInstructionLogging(bool enabled) { log_instructions_ = enabled; } + bool IsInstructionLoggingEnabled() const { return log_instructions_; } private: void compare(uint16_t register_value, uint16_t memory_value) { diff --git a/src/app/emu/debug/breakpoint_manager.cc b/src/app/emu/debug/breakpoint_manager.cc new file mode 100644 index 00000000..9a3157ea --- /dev/null +++ b/src/app/emu/debug/breakpoint_manager.cc @@ -0,0 +1,187 @@ +#include "app/emu/debug/breakpoint_manager.h" + +#include +#include "util/log.h" + +namespace yaze { +namespace emu { + +uint32_t BreakpointManager::AddBreakpoint(uint32_t address, Type type, CpuType cpu, + const std::string& condition, + const std::string& description) { + Breakpoint bp; + bp.id = next_id_++; + bp.address = address; + bp.type = type; + bp.cpu = cpu; + bp.enabled = true; + bp.condition = condition; + bp.hit_count = 0; + bp.description = description.empty() + ? (cpu == CpuType::CPU_65816 ? "CPU Breakpoint" : "SPC700 Breakpoint") + : description; + + breakpoints_[bp.id] = bp; + + LOG_INFO("Breakpoint", "Added breakpoint #%d: %s at $%06X (type=%d, cpu=%d)", + bp.id, bp.description.c_str(), address, static_cast(type), + static_cast(cpu)); + + return bp.id; +} + +void BreakpointManager::RemoveBreakpoint(uint32_t id) { + auto it = breakpoints_.find(id); + if (it != breakpoints_.end()) { + LOG_INFO("Breakpoint", "Removed breakpoint #%d", id); + breakpoints_.erase(it); + } +} + +void BreakpointManager::SetEnabled(uint32_t id, bool enabled) { + auto it = breakpoints_.find(id); + if (it != breakpoints_.end()) { + it->second.enabled = enabled; + LOG_INFO("Breakpoint", "Breakpoint #%d %s", id, enabled ? "enabled" : "disabled"); + } +} + +bool BreakpointManager::ShouldBreakOnExecute(uint32_t pc, CpuType cpu) { + for (auto& [id, bp] : breakpoints_) { + if (!bp.enabled || bp.cpu != cpu || bp.type != Type::EXECUTE) { + continue; + } + + if (bp.address == pc) { + bp.hit_count++; + last_hit_ = &bp; + + // Check condition if present + if (!bp.condition.empty()) { + if (!EvaluateCondition(bp.condition, pc, pc, 0)) { + continue; // Condition not met + } + } + + LOG_INFO("Breakpoint", "Hit breakpoint #%d at PC=$%06X (hits=%d)", + id, pc, bp.hit_count); + return true; + } + } + return false; +} + +bool BreakpointManager::ShouldBreakOnMemoryAccess(uint32_t address, bool is_write, + uint8_t value, uint32_t pc) { + for (auto& [id, bp] : breakpoints_) { + if (!bp.enabled || bp.address != address) { + continue; + } + + // Check if this breakpoint applies to this access type + bool applies = false; + switch (bp.type) { + case Type::READ: + applies = !is_write; + break; + case Type::WRITE: + applies = is_write; + break; + case Type::ACCESS: + applies = true; + break; + default: + continue; // Not a memory breakpoint + } + + if (applies) { + bp.hit_count++; + last_hit_ = &bp; + + // Check condition if present + if (!bp.condition.empty()) { + if (!EvaluateCondition(bp.condition, pc, address, value)) { + continue; + } + } + + LOG_INFO("Breakpoint", "Hit %s breakpoint #%d at $%06X (value=$%02X, PC=$%06X, hits=%d)", + is_write ? "WRITE" : "READ", id, address, value, pc, bp.hit_count); + return true; + } + } + return false; +} + +std::vector BreakpointManager::GetAllBreakpoints() const { + std::vector result; + result.reserve(breakpoints_.size()); + for (const auto& [id, bp] : breakpoints_) { + result.push_back(bp); + } + // Sort by ID for consistent ordering + std::sort(result.begin(), result.end(), + [](const Breakpoint& a, const Breakpoint& b) { return a.id < b.id; }); + return result; +} + +std::vector BreakpointManager::GetBreakpoints(CpuType cpu) const { + std::vector result; + for (const auto& [id, bp] : breakpoints_) { + if (bp.cpu == cpu) { + result.push_back(bp); + } + } + std::sort(result.begin(), result.end(), + [](const Breakpoint& a, const Breakpoint& b) { return a.id < b.id; }); + return result; +} + +void BreakpointManager::ClearAll() { + LOG_INFO("Breakpoint", "Cleared all breakpoints (%zu total)", breakpoints_.size()); + breakpoints_.clear(); + last_hit_ = nullptr; +} + +void BreakpointManager::ClearAll(CpuType cpu) { + auto it = breakpoints_.begin(); + int cleared = 0; + while (it != breakpoints_.end()) { + if (it->second.cpu == cpu) { + it = breakpoints_.erase(it); + cleared++; + } else { + ++it; + } + } + LOG_INFO("Breakpoint", "Cleared %d breakpoints for %s", cleared, + cpu == CpuType::CPU_65816 ? "CPU" : "SPC700"); +} + +void BreakpointManager::ResetHitCounts() { + for (auto& [id, bp] : breakpoints_) { + bp.hit_count = 0; + } +} + +bool BreakpointManager::EvaluateCondition(const std::string& condition, + uint32_t pc, uint32_t address, + uint8_t value) { + // Simple condition evaluation for now + // Future: Could integrate Lua or expression parser + + if (condition.empty()) { + return true; // No condition = always true + } + + // Support simple comparisons: "value > 10", "value == 0xFF", etc. + // Format: "value OPERATOR number" + + // For now, just return true (conditions not implemented yet) + // TODO: Implement proper expression evaluation + return true; +} + +} // namespace emu +} // namespace yaze + diff --git a/src/app/emu/debug/breakpoint_manager.h b/src/app/emu/debug/breakpoint_manager.h new file mode 100644 index 00000000..1d8b8826 --- /dev/null +++ b/src/app/emu/debug/breakpoint_manager.h @@ -0,0 +1,143 @@ +#ifndef YAZE_APP_EMU_DEBUG_BREAKPOINT_MANAGER_H +#define YAZE_APP_EMU_DEBUG_BREAKPOINT_MANAGER_H + +#include +#include +#include +#include +#include + +namespace yaze { +namespace emu { + +/** + * @class BreakpointManager + * @brief Manages CPU and SPC700 breakpoints for debugging + * + * Provides comprehensive breakpoint support including: + * - Execute breakpoints (break when PC reaches address) + * - Read breakpoints (break when memory address is read) + * - Write breakpoints (break when memory address is written) + * - Access breakpoints (break on read OR write) + * - Conditional breakpoints (break when expression is true) + * + * Inspired by Mesen2's debugging capabilities. + */ +class BreakpointManager { + public: + enum class Type { + EXECUTE, // Break when PC reaches this address + READ, // Break when this address is read + WRITE, // Break when this address is written + ACCESS, // Break when this address is read OR written + CONDITIONAL // Break when condition evaluates to true + }; + + enum class CpuType { + CPU_65816, // Main CPU + SPC700 // Audio CPU + }; + + struct Breakpoint { + uint32_t id; + uint32_t address; + Type type; + CpuType cpu; + bool enabled; + std::string condition; // For conditional breakpoints (e.g., "A > 0x10") + uint32_t hit_count; + std::string description; // User-friendly label + + // Optional callback for advanced logic + std::function callback; + }; + + BreakpointManager() = default; + ~BreakpointManager() = default; + + /** + * @brief Add a new breakpoint + * @param address Memory address or PC value + * @param type Breakpoint type + * @param cpu Which CPU to break on + * @param condition Optional condition string + * @param description Optional user-friendly description + * @return Unique breakpoint ID + */ + uint32_t AddBreakpoint(uint32_t address, Type type, CpuType cpu, + const std::string& condition = "", + const std::string& description = ""); + + /** + * @brief Remove a breakpoint by ID + */ + void RemoveBreakpoint(uint32_t id); + + /** + * @brief Enable or disable a breakpoint + */ + void SetEnabled(uint32_t id, bool enabled); + + /** + * @brief Check if execution should break at this address + * @param pc Current program counter + * @param cpu Which CPU is executing + * @return true if breakpoint hit + */ + bool ShouldBreakOnExecute(uint32_t pc, CpuType cpu); + + /** + * @brief Check if execution should break on memory access + * @param address Memory address being accessed + * @param is_write True if write, false if read + * @param value Value being read/written + * @param pc Current program counter (for logging) + * @return true if breakpoint hit + */ + bool ShouldBreakOnMemoryAccess(uint32_t address, bool is_write, + uint8_t value, uint32_t pc); + + /** + * @brief Get all breakpoints + */ + std::vector GetAllBreakpoints() const; + + /** + * @brief Get breakpoints for specific CPU + */ + std::vector GetBreakpoints(CpuType cpu) const; + + /** + * @brief Clear all breakpoints + */ + void ClearAll(); + + /** + * @brief Clear all breakpoints for specific CPU + */ + void ClearAll(CpuType cpu); + + /** + * @brief Get the last breakpoint that was hit + */ + const Breakpoint* GetLastHit() const { return last_hit_; } + + /** + * @brief Reset hit counts for all breakpoints + */ + void ResetHitCounts(); + + private: + std::unordered_map breakpoints_; + uint32_t next_id_ = 1; + const Breakpoint* last_hit_ = nullptr; + + bool EvaluateCondition(const std::string& condition, uint32_t pc, + uint32_t address, uint8_t value); +}; + +} // namespace emu +} // namespace yaze + +#endif // YAZE_APP_EMU_DEBUG_BREAKPOINT_MANAGER_H + diff --git a/src/app/emu/debug/disassembly_viewer.cc b/src/app/emu/debug/disassembly_viewer.cc index 3b8c6f3e..fea92444 100644 --- a/src/app/emu/debug/disassembly_viewer.cc +++ b/src/app/emu/debug/disassembly_viewer.cc @@ -31,11 +31,22 @@ void DisassemblyViewer::RecordInstruction(uint32_t address, uint8_t opcode, const std::vector& operands, const std::string& mnemonic, const std::string& operand_str) { + // Skip if recording disabled (for performance) + if (!recording_enabled_) { + return; + } + auto it = instructions_.find(address); if (it != instructions_.end()) { // Instruction already recorded, just increment execution count it->second.execution_count++; } else { + // Check if we're at the limit + if (instructions_.size() >= max_instructions_) { + // Trim to 80% of max to avoid constant trimming + TrimToSize(max_instructions_ * 0.8); + } + // New instruction, add to map DisassemblyEntry entry; entry.address = address; @@ -52,6 +63,29 @@ void DisassemblyViewer::RecordInstruction(uint32_t address, uint8_t opcode, } } +void DisassemblyViewer::TrimToSize(size_t target_size) { + if (instructions_.size() <= target_size) { + return; + } + + // Keep most-executed instructions + // Remove least-executed ones + std::vector> addr_counts; + for (const auto& [addr, entry] : instructions_) { + addr_counts.push_back({addr, entry.execution_count}); + } + + // Sort by execution count (ascending) + std::sort(addr_counts.begin(), addr_counts.end(), + [](const auto& a, const auto& b) { return a.second < b.second; }); + + // Remove least-executed instructions + size_t to_remove = instructions_.size() - target_size; + for (size_t i = 0; i < to_remove && i < addr_counts.size(); i++) { + instructions_.erase(addr_counts[i].first); + } +} + void DisassemblyViewer::Render(uint32_t current_pc, const std::vector& breakpoints) { // Update current PC and breakpoint flags diff --git a/src/app/emu/debug/disassembly_viewer.h b/src/app/emu/debug/disassembly_viewer.h index ee91e581..b567442a 100644 --- a/src/app/emu/debug/disassembly_viewer.h +++ b/src/app/emu/debug/disassembly_viewer.h @@ -109,11 +109,31 @@ class DisassemblyViewer { * @brief Check if the disassembly viewer is available */ bool IsAvailable() const { return !instructions_.empty(); } + + /** + * @brief Enable/disable recording (for performance) + */ + void SetRecording(bool enabled) { recording_enabled_ = enabled; } + bool IsRecording() const { return recording_enabled_; } + + /** + * @brief Set maximum number of instructions to keep + */ + void SetMaxInstructions(size_t max) { max_instructions_ = max; } + + /** + * @brief Clear old instructions to save memory + */ + void TrimToSize(size_t target_size); private: // Sparse storage: only store executed instructions std::map instructions_; + // Performance limits + bool recording_enabled_ = true; + size_t max_instructions_ = 10000; // Limit to prevent memory bloat + // UI state char search_filter_[256] = ""; uint32_t selected_address_ = 0; diff --git a/src/app/emu/debug/watchpoint_manager.cc b/src/app/emu/debug/watchpoint_manager.cc new file mode 100644 index 00000000..6a0768e9 --- /dev/null +++ b/src/app/emu/debug/watchpoint_manager.cc @@ -0,0 +1,165 @@ +#include "app/emu/debug/watchpoint_manager.h" + +#include +#include +#include "absl/strings/str_format.h" +#include "util/log.h" + +namespace yaze { +namespace emu { + +uint32_t WatchpointManager::AddWatchpoint(uint32_t start_address, uint32_t end_address, + bool track_reads, bool track_writes, + bool break_on_access, + const std::string& description) { + Watchpoint wp; + wp.id = next_id_++; + wp.start_address = start_address; + wp.end_address = end_address; + wp.track_reads = track_reads; + wp.track_writes = track_writes; + wp.break_on_access = break_on_access; + wp.enabled = true; + wp.description = description.empty() + ? absl::StrFormat("Watch $%06X-$%06X", start_address, end_address) + : description; + + watchpoints_[wp.id] = wp; + + LOG_INFO("Watchpoint", "Added watchpoint #%d: %s (R=%d, W=%d, Break=%d)", + wp.id, wp.description.c_str(), track_reads, track_writes, break_on_access); + + return wp.id; +} + +void WatchpointManager::RemoveWatchpoint(uint32_t id) { + auto it = watchpoints_.find(id); + if (it != watchpoints_.end()) { + LOG_INFO("Watchpoint", "Removed watchpoint #%d", id); + watchpoints_.erase(it); + } +} + +void WatchpointManager::SetEnabled(uint32_t id, bool enabled) { + auto it = watchpoints_.find(id); + if (it != watchpoints_.end()) { + it->second.enabled = enabled; + LOG_INFO("Watchpoint", "Watchpoint #%d %s", id, enabled ? "enabled" : "disabled"); + } +} + +bool WatchpointManager::OnMemoryAccess(uint32_t pc, uint32_t address, bool is_write, + uint8_t old_value, uint8_t new_value, + uint64_t cycle_count) { + bool should_break = false; + + for (auto& [id, wp] : watchpoints_) { + if (!wp.enabled || !IsInRange(wp, address)) { + continue; + } + + // Check if this access type is tracked + bool should_log = (is_write && wp.track_writes) || (!is_write && wp.track_reads); + if (!should_log) { + continue; + } + + // Log the access + AccessLog log; + log.pc = pc; + log.address = address; + log.old_value = old_value; + log.new_value = new_value; + log.is_write = is_write; + log.cycle_count = cycle_count; + log.description = absl::StrFormat("%s at $%06X: $%02X -> $%02X (PC=$%06X)", + is_write ? "WRITE" : "READ", + address, old_value, new_value, pc); + + wp.history.push_back(log); + + // Limit history size + if (wp.history.size() > Watchpoint::kMaxHistorySize) { + wp.history.pop_front(); + } + + // Check if should break + if (wp.break_on_access) { + should_break = true; + LOG_INFO("Watchpoint", "Hit watchpoint #%d: %s", id, log.description.c_str()); + } + } + + return should_break; +} + +std::vector WatchpointManager::GetAllWatchpoints() const { + std::vector result; + result.reserve(watchpoints_.size()); + for (const auto& [id, wp] : watchpoints_) { + result.push_back(wp); + } + std::sort(result.begin(), result.end(), + [](const Watchpoint& a, const Watchpoint& b) { return a.id < b.id; }); + return result; +} + +std::vector WatchpointManager::GetHistory( + uint32_t address, int max_entries) const { + std::vector result; + + for (const auto& [id, wp] : watchpoints_) { + if (IsInRange(wp, address)) { + for (const auto& log : wp.history) { + if (log.address == address) { + result.push_back(log); + if (result.size() >= static_cast(max_entries)) { + break; + } + } + } + } + } + + return result; +} + +void WatchpointManager::ClearAll() { + LOG_INFO("Watchpoint", "Cleared all watchpoints (%zu total)", watchpoints_.size()); + watchpoints_.clear(); +} + +void WatchpointManager::ClearHistory() { + for (auto& [id, wp] : watchpoints_) { + wp.history.clear(); + } + LOG_INFO("Watchpoint", "Cleared all watchpoint history"); +} + +bool WatchpointManager::ExportHistoryToCSV(const std::string& filepath) const { + std::ofstream out(filepath); + if (!out.is_open()) { + return false; + } + + // CSV Header + out << "Watchpoint,PC,Address,Type,OldValue,NewValue,Cycle,Description\n"; + + for (const auto& [id, wp] : watchpoints_) { + for (const auto& log : wp.history) { + out << absl::StrFormat("%d,$%06X,$%06X,%s,$%02X,$%02X,%llu,\"%s\"\n", + id, log.pc, log.address, + log.is_write ? "WRITE" : "READ", + log.old_value, log.new_value, log.cycle_count, + log.description); + } + } + + out.close(); + LOG_INFO("Watchpoint", "Exported watchpoint history to %s", filepath.c_str()); + return true; +} + +} // namespace emu +} // namespace yaze + diff --git a/src/app/emu/debug/watchpoint_manager.h b/src/app/emu/debug/watchpoint_manager.h new file mode 100644 index 00000000..361652a8 --- /dev/null +++ b/src/app/emu/debug/watchpoint_manager.h @@ -0,0 +1,138 @@ +#ifndef YAZE_APP_EMU_DEBUG_WATCHPOINT_MANAGER_H +#define YAZE_APP_EMU_DEBUG_WATCHPOINT_MANAGER_H + +#include +#include +#include +#include +#include + +namespace yaze { +namespace emu { + +/** + * @class WatchpointManager + * @brief Manages memory watchpoints for debugging + * + * Watchpoints track memory accesses (reads/writes) and can break execution + * when specific memory locations are accessed. This is crucial for: + * - Finding where variables are modified + * - Detecting buffer overflows + * - Tracking down corruption bugs + * - Understanding data flow + * + * Inspired by Mesen2's memory debugging capabilities. + */ +class WatchpointManager { + public: + struct AccessLog { + uint32_t pc; // Where the access happened (program counter) + uint32_t address; // What address was accessed + uint8_t old_value; // Value before write (0 for reads) + uint8_t new_value; // Value after write / value read + bool is_write; // True for write, false for read + uint64_t cycle_count; // When it happened (CPU cycle) + std::string description; // Optional description + }; + + struct Watchpoint { + uint32_t id; + uint32_t start_address; + uint32_t end_address; // For range watchpoints + bool track_reads; + bool track_writes; + bool break_on_access; // If true, pause emulation on access + bool enabled; + std::string description; + + // Access history for this watchpoint + std::deque history; + static constexpr size_t kMaxHistorySize = 1000; + }; + + WatchpointManager() = default; + ~WatchpointManager() = default; + + /** + * @brief Add a memory watchpoint + * @param start_address Starting address of range to watch + * @param end_address Ending address (inclusive), or same as start for single byte + * @param track_reads Track read accesses + * @param track_writes Track write accesses + * @param break_on_access Pause emulation when accessed + * @param description User-friendly description + * @return Unique watchpoint ID + */ + uint32_t AddWatchpoint(uint32_t start_address, uint32_t end_address, + bool track_reads, bool track_writes, + bool break_on_access = false, + const std::string& description = ""); + + /** + * @brief Remove a watchpoint + */ + void RemoveWatchpoint(uint32_t id); + + /** + * @brief Enable or disable a watchpoint + */ + void SetEnabled(uint32_t id, bool enabled); + + /** + * @brief Check if memory access should break/log + * @param pc Current program counter + * @param address Memory address being accessed + * @param is_write True for write, false for read + * @param old_value Previous value at address + * @param new_value New value (for writes) or value read + * @param cycle_count Current CPU cycle + * @return true if should break execution + */ + bool OnMemoryAccess(uint32_t pc, uint32_t address, bool is_write, + uint8_t old_value, uint8_t new_value, uint64_t cycle_count); + + /** + * @brief Get all watchpoints + */ + std::vector GetAllWatchpoints() const; + + /** + * @brief Get access history for a specific address + * @param address Address to query + * @param max_entries Maximum number of entries to return + * @return Vector of access logs + */ + std::vector GetHistory(uint32_t address, int max_entries = 100) const; + + /** + * @brief Clear all watchpoints + */ + void ClearAll(); + + /** + * @brief Clear history for all watchpoints + */ + void ClearHistory(); + + /** + * @brief Export access history to CSV + * @param filepath Output file path + * @return true if successful + */ + bool ExportHistoryToCSV(const std::string& filepath) const; + + private: + std::unordered_map watchpoints_; + uint32_t next_id_ = 1; + + // Check if address is within watchpoint range + bool IsInRange(const Watchpoint& wp, uint32_t address) const { + return address >= wp.start_address && address <= wp.end_address; + } +}; + +} // namespace emu +} // namespace yaze + +#endif // YAZE_APP_EMU_DEBUG_WATCHPOINT_MANAGER_H + diff --git a/src/app/emu/emulator.cc b/src/app/emu/emulator.cc index 0f68a6a2..5a7586ae 100644 --- a/src/app/emu/emulator.cc +++ b/src/app/emu/emulator.cc @@ -83,6 +83,19 @@ void Emulator::Initialize(gfx::IRenderer* renderer, const std::vector& running_ = false; snes_initialized_ = false; + // Set up CPU breakpoint callback + snes_.cpu().on_breakpoint_hit_ = [this](uint32_t pc) -> bool { + return breakpoint_manager_.ShouldBreakOnExecute(pc, BreakpointManager::CpuType::CPU_65816); + }; + + // Set up instruction recording callback for DisassemblyViewer + snes_.cpu().on_instruction_executed_ = [this](uint32_t address, uint8_t opcode, + const std::vector& operands, + const std::string& mnemonic, + const std::string& operand_str) { + disassembly_viewer_.RecordInstruction(address, opcode, operands, mnemonic, operand_str); + }; + initialized_ = true; } @@ -113,6 +126,9 @@ void Emulator::Run(Rom* rom) { rom_data_ = rom->vector(); } snes_.Init(rom_data_); + + // Enable instruction logging for disassembly viewer + snes_.cpu().SetInstructionLogging(true); // Note: PPU pixel format set to 1 (XBGR) in Init() which matches ARGB8888 texture @@ -768,6 +784,56 @@ void Emulator::RenderModernCpuDebugger() { try { auto& theme_manager = gui::ThemeManager::Get(); const auto& theme = theme_manager.GetCurrentTheme(); + + // Debugger controls toolbar + if (ImGui::Button(ICON_MD_PLAY_ARROW)) { running_ = true; } + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_PAUSE)) { running_ = false; } + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_SKIP_NEXT " Step")) { + if (!running_) snes_.cpu().RunOpcode(); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_REFRESH)) { snes_.Reset(true); } + + ImGui::Separator(); + + // Breakpoint controls + static char bp_addr[16] = "00FFD9"; + ImGui::Text(ICON_MD_BUG_REPORT " Breakpoints:"); + ImGui::PushItemWidth(100); + ImGui::InputText("##BPAddr", bp_addr, IM_ARRAYSIZE(bp_addr), + ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase); + ImGui::PopItemWidth(); + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_ADD " Add")) { + uint32_t addr = std::strtoul(bp_addr, nullptr, 16); + breakpoint_manager_.AddBreakpoint(addr, BreakpointManager::Type::EXECUTE, + BreakpointManager::CpuType::CPU_65816, + "", absl::StrFormat("BP at $%06X", addr)); + } + + // List breakpoints + ImGui::BeginChild("##BPList", ImVec2(0, 100), true); + for (const auto& bp : breakpoint_manager_.GetAllBreakpoints()) { + if (bp.cpu == BreakpointManager::CpuType::CPU_65816) { + bool enabled = bp.enabled; + if (ImGui::Checkbox(absl::StrFormat("##en%d", bp.id).c_str(), &enabled)) { + breakpoint_manager_.SetEnabled(bp.id, enabled); + } + ImGui::SameLine(); + ImGui::Text("$%06X", bp.address); + ImGui::SameLine(); + ImGui::TextDisabled("(hits: %d)", bp.hit_count); + ImGui::SameLine(); + if (ImGui::SmallButton(absl::StrFormat(ICON_MD_DELETE "##%d", bp.id).c_str())) { + breakpoint_manager_.RemoveBreakpoint(bp.id); + } + } + } + ImGui::EndChild(); + + ImGui::Separator(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "CPU Status"); ImGui::PushStyleColor(ImGuiCol_ChildBg, diff --git a/src/app/emu/emulator.h b/src/app/emu/emulator.h index dde133a1..8227d96b 100644 --- a/src/app/emu/emulator.h +++ b/src/app/emu/emulator.h @@ -5,6 +5,8 @@ #include #include "app/emu/snes.h" +#include "app/emu/debug/breakpoint_manager.h" +#include "app/emu/debug/disassembly_viewer.h" #include "app/rom.h" namespace yaze { @@ -54,6 +56,11 @@ class Emulator { auto wanted_samples() const -> int { return wanted_samples_; } void set_renderer(gfx::IRenderer* renderer) { renderer_ = renderer; } + // Debugger access + BreakpointManager& breakpoint_manager() { return breakpoint_manager_; } + bool is_debugging() const { return debugging_; } + void set_debugging(bool debugging) { debugging_ = debugging; } + // AI Agent Integration API bool IsEmulatorReady() const { return snes_.running() && !rom_data_.empty(); } double GetCurrentFPS() const { return current_fps_; } @@ -136,8 +143,13 @@ class Emulator { Snes snes_; bool initialized_ = false; bool snes_initialized_ = false; + bool debugging_ = false; gfx::IRenderer* renderer_ = nullptr; void* ppu_texture_ = nullptr; + + // Debugger infrastructure + BreakpointManager breakpoint_manager_; + debug::DisassemblyViewer disassembly_viewer_; std::vector rom_data_;