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:
@@ -8,6 +8,8 @@ set(
|
|||||||
app/emu/cpu/internal/addressing.cc
|
app/emu/cpu/internal/addressing.cc
|
||||||
app/emu/cpu/internal/instructions.cc
|
app/emu/cpu/internal/instructions.cc
|
||||||
app/emu/debug/disassembly_viewer.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/cpu/cpu.cc
|
||||||
app/emu/video/ppu.cc
|
app/emu/video/ppu.cc
|
||||||
app/emu/memory/dma.cc
|
app/emu/memory/dma.cc
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ void Cpu::Reset(bool hard) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Cpu::RunOpcode() {
|
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_) {
|
if (reset_wanted_) {
|
||||||
reset_wanted_ = false;
|
reset_wanted_ = false;
|
||||||
// reset: brk/interrupt without writes
|
// 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,
|
void Cpu::LogInstructions(uint16_t PC, uint8_t opcode, uint16_t operand,
|
||||||
bool immediate, bool accumulator_mode) {
|
bool immediate, bool accumulator_mode) {
|
||||||
if (core::FeatureFlags::get().kLogInstructions) {
|
// Build full 24-bit address
|
||||||
// Build full 24-bit address
|
uint32_t full_address = (PB << 16) | PC;
|
||||||
uint32_t full_address = (PB << 16) | PC;
|
|
||||||
|
// Extract operand bytes based on instruction size
|
||||||
// Extract operand bytes based on instruction size
|
std::vector<uint8_t> operand_bytes;
|
||||||
std::vector<uint8_t> operand_bytes;
|
std::string operand_str;
|
||||||
std::string operand_str;
|
|
||||||
|
if (operand) {
|
||||||
if (operand) {
|
if (immediate) {
|
||||||
if (immediate) {
|
operand_str += "#";
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get mnemonic
|
if (accumulator_mode) {
|
||||||
const std::string& mnemonic = opcode_to_mnemonic.at(opcode);
|
// 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
|
// Record to new disassembly viewer
|
||||||
disassembly_viewer().RecordInstruction(full_address, opcode, operand_bytes,
|
disassembly_viewer().RecordInstruction(full_address, opcode, operand_bytes,
|
||||||
mnemonic, operand_str);
|
mnemonic, operand_str);
|
||||||
|
|||||||
@@ -57,6 +57,13 @@ class Cpu {
|
|||||||
// New disassembly viewer
|
// New disassembly viewer
|
||||||
debug::DisassemblyViewer& disassembly_viewer();
|
debug::DisassemblyViewer& disassembly_viewer();
|
||||||
const debug::DisassemblyViewer& disassembly_viewer() const;
|
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
|
// Public register access for debugging and UI
|
||||||
uint16_t A = 0; // Accumulator
|
uint16_t A = 0; // Accumulator
|
||||||
@@ -768,6 +775,10 @@ class Cpu {
|
|||||||
|
|
||||||
auto mutable_log_instructions() -> bool* { return &log_instructions_; }
|
auto mutable_log_instructions() -> bool* { return &log_instructions_; }
|
||||||
bool stopped() const { return stopped_; }
|
bool stopped() const { return stopped_; }
|
||||||
|
|
||||||
|
// Instruction logging control
|
||||||
|
void SetInstructionLogging(bool enabled) { log_instructions_ = enabled; }
|
||||||
|
bool IsInstructionLoggingEnabled() const { return log_instructions_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void compare(uint16_t register_value, uint16_t memory_value) {
|
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::vector<uint8_t>& operands,
|
||||||
const std::string& mnemonic,
|
const std::string& mnemonic,
|
||||||
const std::string& operand_str) {
|
const std::string& operand_str) {
|
||||||
|
// Skip if recording disabled (for performance)
|
||||||
|
if (!recording_enabled_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto it = instructions_.find(address);
|
auto it = instructions_.find(address);
|
||||||
if (it != instructions_.end()) {
|
if (it != instructions_.end()) {
|
||||||
// Instruction already recorded, just increment execution count
|
// Instruction already recorded, just increment execution count
|
||||||
it->second.execution_count++;
|
it->second.execution_count++;
|
||||||
} else {
|
} 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
|
// New instruction, add to map
|
||||||
DisassemblyEntry entry;
|
DisassemblyEntry entry;
|
||||||
entry.address = address;
|
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,
|
void DisassemblyViewer::Render(uint32_t current_pc,
|
||||||
const std::vector<uint32_t>& breakpoints) {
|
const std::vector<uint32_t>& breakpoints) {
|
||||||
// Update current PC and breakpoint flags
|
// Update current PC and breakpoint flags
|
||||||
|
|||||||
@@ -109,11 +109,31 @@ class DisassemblyViewer {
|
|||||||
* @brief Check if the disassembly viewer is available
|
* @brief Check if the disassembly viewer is available
|
||||||
*/
|
*/
|
||||||
bool IsAvailable() const { return !instructions_.empty(); }
|
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:
|
private:
|
||||||
// Sparse storage: only store executed instructions
|
// Sparse storage: only store executed instructions
|
||||||
std::map<uint32_t, DisassemblyEntry> 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
|
// UI state
|
||||||
char search_filter_[256] = "";
|
char search_filter_[256] = "";
|
||||||
uint32_t selected_address_ = 0;
|
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;
|
running_ = false;
|
||||||
snes_initialized_ = 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;
|
initialized_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +126,9 @@ void Emulator::Run(Rom* rom) {
|
|||||||
rom_data_ = rom->vector();
|
rom_data_ = rom->vector();
|
||||||
}
|
}
|
||||||
snes_.Init(rom_data_);
|
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
|
// Note: PPU pixel format set to 1 (XBGR) in Init() which matches ARGB8888 texture
|
||||||
|
|
||||||
@@ -768,6 +784,56 @@ void Emulator::RenderModernCpuDebugger() {
|
|||||||
try {
|
try {
|
||||||
auto& theme_manager = gui::ThemeManager::Get();
|
auto& theme_manager = gui::ThemeManager::Get();
|
||||||
const auto& theme = theme_manager.GetCurrentTheme();
|
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::TextColored(ConvertColorToImVec4(theme.accent), "CPU Status");
|
||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg,
|
ImGui::PushStyleColor(ImGuiCol_ChildBg,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "app/emu/snes.h"
|
#include "app/emu/snes.h"
|
||||||
|
#include "app/emu/debug/breakpoint_manager.h"
|
||||||
|
#include "app/emu/debug/disassembly_viewer.h"
|
||||||
#include "app/rom.h"
|
#include "app/rom.h"
|
||||||
|
|
||||||
namespace yaze {
|
namespace yaze {
|
||||||
@@ -54,6 +56,11 @@ class Emulator {
|
|||||||
auto wanted_samples() const -> int { return wanted_samples_; }
|
auto wanted_samples() const -> int { return wanted_samples_; }
|
||||||
void set_renderer(gfx::IRenderer* renderer) { renderer_ = renderer; }
|
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
|
// AI Agent Integration API
|
||||||
bool IsEmulatorReady() const { return snes_.running() && !rom_data_.empty(); }
|
bool IsEmulatorReady() const { return snes_.running() && !rom_data_.empty(); }
|
||||||
double GetCurrentFPS() const { return current_fps_; }
|
double GetCurrentFPS() const { return current_fps_; }
|
||||||
@@ -136,8 +143,13 @@ class Emulator {
|
|||||||
Snes snes_;
|
Snes snes_;
|
||||||
bool initialized_ = false;
|
bool initialized_ = false;
|
||||||
bool snes_initialized_ = false;
|
bool snes_initialized_ = false;
|
||||||
|
bool debugging_ = false;
|
||||||
gfx::IRenderer* renderer_ = nullptr;
|
gfx::IRenderer* renderer_ = nullptr;
|
||||||
void* ppu_texture_ = nullptr;
|
void* ppu_texture_ = nullptr;
|
||||||
|
|
||||||
|
// Debugger infrastructure
|
||||||
|
BreakpointManager breakpoint_manager_;
|
||||||
|
debug::DisassemblyViewer disassembly_viewer_;
|
||||||
|
|
||||||
std::vector<uint8_t> rom_data_;
|
std::vector<uint8_t> rom_data_;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user