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.
This commit is contained in:
@@ -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<uint8_t> 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<uint8_t> 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);
|
||||
|
||||
@@ -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<bool(uint32_t pc)> on_breakpoint_hit_;
|
||||
|
||||
// Instruction recording callback (for DisassemblyViewer)
|
||||
std::function<void(uint32_t address, uint8_t opcode, const std::vector<uint8_t>& 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) {
|
||||
|
||||
187
src/app/emu/debug/breakpoint_manager.cc
Normal file
187
src/app/emu/debug/breakpoint_manager.cc
Normal file
@@ -0,0 +1,187 @@
|
||||
#include "app/emu/debug/breakpoint_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#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<int>(type),
|
||||
static_cast<int>(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::Breakpoint> BreakpointManager::GetAllBreakpoints() const {
|
||||
std::vector<Breakpoint> 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::Breakpoint> BreakpointManager::GetBreakpoints(CpuType cpu) const {
|
||||
std::vector<Breakpoint> 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
|
||||
|
||||
143
src/app/emu/debug/breakpoint_manager.h
Normal file
143
src/app/emu/debug/breakpoint_manager.h
Normal file
@@ -0,0 +1,143 @@
|
||||
#ifndef YAZE_APP_EMU_DEBUG_BREAKPOINT_MANAGER_H
|
||||
#define YAZE_APP_EMU_DEBUG_BREAKPOINT_MANAGER_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
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<bool(uint32_t pc, uint32_t address, uint8_t value)> 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<Breakpoint> GetAllBreakpoints() const;
|
||||
|
||||
/**
|
||||
* @brief Get breakpoints for specific CPU
|
||||
*/
|
||||
std::vector<Breakpoint> 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<uint32_t, Breakpoint> 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
|
||||
|
||||
@@ -31,11 +31,22 @@ void DisassemblyViewer::RecordInstruction(uint32_t address, uint8_t opcode,
|
||||
const std::vector<uint8_t>& 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<std::pair<uint32_t, uint64_t>> 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<uint32_t>& breakpoints) {
|
||||
// Update current PC and breakpoint flags
|
||||
|
||||
@@ -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<uint32_t, DisassemblyEntry> 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;
|
||||
|
||||
165
src/app/emu/debug/watchpoint_manager.cc
Normal file
165
src/app/emu/debug/watchpoint_manager.cc
Normal file
@@ -0,0 +1,165 @@
|
||||
#include "app/emu/debug/watchpoint_manager.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#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::Watchpoint> WatchpointManager::GetAllWatchpoints() const {
|
||||
std::vector<Watchpoint> 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::AccessLog> WatchpointManager::GetHistory(
|
||||
uint32_t address, int max_entries) const {
|
||||
std::vector<AccessLog> 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<size_t>(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
|
||||
|
||||
138
src/app/emu/debug/watchpoint_manager.h
Normal file
138
src/app/emu/debug/watchpoint_manager.h
Normal file
@@ -0,0 +1,138 @@
|
||||
#ifndef YAZE_APP_EMU_DEBUG_WATCHPOINT_MANAGER_H
|
||||
#define YAZE_APP_EMU_DEBUG_WATCHPOINT_MANAGER_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <deque>
|
||||
|
||||
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<AccessLog> 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<Watchpoint> 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<AccessLog> 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<uint32_t, Watchpoint> 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
|
||||
|
||||
@@ -83,6 +83,19 @@ void Emulator::Initialize(gfx::IRenderer* renderer, const std::vector<uint8_t>&
|
||||
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<uint8_t>& 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,
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <vector>
|
||||
|
||||
#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<uint8_t> rom_data_;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user