feat: Enhance ROM Loading with Validation and Disassembly Viewer Integration
- Added validation checks for ROM file existence and size constraints (minimum 32KB, maximum 8MB) during loading. - Integrated a new disassembly viewer to track and display executed instructions, enhancing debugging capabilities. - Updated CPU class to manage disassembly viewer instances, allowing for real-time instruction logging. - Improved emulator's CMake configuration to include new source files related to the disassembly viewer. - Enhanced pixel handling in the PPU to support BGRX format for better compatibility with SDL.
This commit is contained in:
@@ -7,6 +7,7 @@ set(
|
||||
app/emu/audio/internal/instructions.cc
|
||||
app/emu/cpu/internal/addressing.cc
|
||||
app/emu/cpu/internal/instructions.cc
|
||||
app/emu/debug/disassembly_viewer.cc
|
||||
app/emu/cpu/cpu.cc
|
||||
app/emu/video/ppu.cc
|
||||
app/emu/memory/dma.cc
|
||||
|
||||
@@ -6,13 +6,29 @@
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "app/core/features.h"
|
||||
#include "app/emu/cpu/internal/opcodes.h"
|
||||
#include "app/emu/debug/disassembly_viewer.h"
|
||||
#include "util/log.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
|
||||
debug::DisassemblyViewer& Cpu::disassembly_viewer() {
|
||||
if (disassembly_viewer_ == nullptr) {
|
||||
disassembly_viewer_ = new debug::DisassemblyViewer();
|
||||
}
|
||||
return *disassembly_viewer_;
|
||||
}
|
||||
|
||||
const debug::DisassemblyViewer& Cpu::disassembly_viewer() const {
|
||||
if (disassembly_viewer_ == nullptr) {
|
||||
const_cast<Cpu*>(this)->disassembly_viewer_ = new debug::DisassemblyViewer();
|
||||
}
|
||||
return *disassembly_viewer_;
|
||||
}
|
||||
|
||||
void Cpu::Reset(bool hard) {
|
||||
if (hard) {
|
||||
A = 0;
|
||||
@@ -1884,38 +1900,50 @@ void Cpu::ExecuteInstruction(uint8_t opcode) {
|
||||
}
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Get mnemonic
|
||||
const std::string& mnemonic = opcode_to_mnemonic.at(opcode);
|
||||
|
||||
// Record to new disassembly viewer
|
||||
disassembly_viewer().RecordInstruction(full_address, opcode, operand_bytes,
|
||||
mnemonic, operand_str);
|
||||
|
||||
// Also maintain legacy log for compatibility
|
||||
std::ostringstream oss;
|
||||
oss << "$" << std::uppercase << std::setw(2) << std::setfill('0')
|
||||
<< static_cast<int>(PB) << ":" << std::hex << PC << ": 0x"
|
||||
<< std::setw(2) << std::setfill('0') << std::hex
|
||||
<< static_cast<int>(opcode) << " " << opcode_to_mnemonic.at(opcode)
|
||||
<< " ";
|
||||
<< static_cast<int>(opcode) << " " << mnemonic << " " << operand_str;
|
||||
|
||||
// Log the operand.
|
||||
std::string ops;
|
||||
if (operand) {
|
||||
if (immediate) {
|
||||
ops += "#";
|
||||
}
|
||||
std::ostringstream oss_ops;
|
||||
oss_ops << "$";
|
||||
if (accumulator_mode) {
|
||||
oss_ops << std::hex << std::setw(2) << std::setfill('0')
|
||||
<< static_cast<int>(operand);
|
||||
} else {
|
||||
oss_ops << std::hex << std::setw(4) << std::setfill('0')
|
||||
<< static_cast<int>(operand);
|
||||
}
|
||||
ops = oss_ops.str();
|
||||
}
|
||||
|
||||
oss << ops << std::endl;
|
||||
|
||||
InstructionEntry entry(PC, opcode, ops, oss.str());
|
||||
InstructionEntry entry(PC, opcode, operand_str, oss.str());
|
||||
instruction_log_.push_back(entry);
|
||||
// Also emit to the central logger for user/agent-controlled sinks.
|
||||
|
||||
// Also emit to the central logger for user/agent-controlled sinks
|
||||
util::LogManager::instance().log(util::LogLevel::YAZE_DEBUG, "CPU",
|
||||
oss.str());
|
||||
} else {
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
|
||||
// Forward declarations
|
||||
namespace debug {
|
||||
class DisassemblyViewer;
|
||||
}
|
||||
|
||||
class InstructionEntry {
|
||||
public:
|
||||
// Constructor
|
||||
@@ -47,7 +52,26 @@ class Cpu {
|
||||
void Nmi() { nmi_wanted_ = true; }
|
||||
|
||||
std::vector<uint32_t> breakpoints_;
|
||||
std::vector<InstructionEntry> instruction_log_;
|
||||
std::vector<InstructionEntry> instruction_log_; // Legacy log for compatibility
|
||||
|
||||
// New disassembly viewer
|
||||
debug::DisassemblyViewer& disassembly_viewer();
|
||||
const debug::DisassemblyViewer& disassembly_viewer() const;
|
||||
|
||||
// Public register access for debugging and UI
|
||||
uint16_t A = 0; // Accumulator
|
||||
uint16_t X = 0; // X index register
|
||||
uint16_t Y = 0; // Y index register
|
||||
uint16_t D = 0; // Direct Page register
|
||||
uint8_t DB = 0; // Data Bank register
|
||||
uint8_t PB = 0; // Program Bank register
|
||||
uint16_t PC = 0; // Program Counter
|
||||
uint8_t status = 0b00110000; // Processor Status (P)
|
||||
|
||||
// Breakpoint management
|
||||
void set_int_delay(bool delay) { int_delay_ = delay; }
|
||||
|
||||
debug::DisassemblyViewer* disassembly_viewer_ = nullptr;
|
||||
|
||||
// ======================================================
|
||||
// Interrupt Vectors
|
||||
@@ -62,17 +86,9 @@ class Cpu {
|
||||
void DoInterrupt();
|
||||
|
||||
// ======================================================
|
||||
// Registers
|
||||
// Internal state
|
||||
|
||||
uint16_t A = 0; // Accumulator
|
||||
uint16_t X = 0; // X index register
|
||||
uint16_t Y = 0; // Y index register
|
||||
uint16_t D = 0; // Direct Page register
|
||||
uint8_t DB = 0; // Data Bank register
|
||||
uint8_t PB = 0; // Program Bank register
|
||||
uint16_t PC = 0; // Program Counter
|
||||
uint8_t E = 1; // Emulation mode flag
|
||||
uint8_t status = 0b00110000; // Processor Status (P)
|
||||
|
||||
// Mnemonic Value Binary Description
|
||||
// N #$80 10000000 Negative
|
||||
@@ -233,7 +249,6 @@ class Cpu {
|
||||
}
|
||||
}
|
||||
|
||||
void set_int_delay(bool delay) { int_delay_ = delay; }
|
||||
|
||||
// Addressing Modes
|
||||
|
||||
|
||||
438
src/app/emu/debug/disassembly_viewer.cc
Normal file
438
src/app/emu/debug/disassembly_viewer.cc
Normal file
@@ -0,0 +1,438 @@
|
||||
#include "app/emu/debug/disassembly_viewer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "app/gui/style.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace debug {
|
||||
|
||||
namespace {
|
||||
|
||||
// Color scheme for retro hacker aesthetic
|
||||
constexpr ImVec4 kColorAddress(0.4f, 0.8f, 1.0f, 1.0f); // Cyan for addresses
|
||||
constexpr ImVec4 kColorOpcode(0.8f, 0.8f, 0.8f, 1.0f); // Light gray for opcodes
|
||||
constexpr ImVec4 kColorMnemonic(1.0f, 0.8f, 0.2f, 1.0f); // Gold for mnemonics
|
||||
constexpr ImVec4 kColorOperand(0.6f, 1.0f, 0.6f, 1.0f); // Light green for operands
|
||||
constexpr ImVec4 kColorComment(0.5f, 0.5f, 0.5f, 1.0f); // Gray for comments
|
||||
constexpr ImVec4 kColorCurrentPC(1.0f, 0.3f, 0.3f, 1.0f); // Red for current PC
|
||||
constexpr ImVec4 kColorBreakpoint(1.0f, 0.0f, 0.0f, 1.0f); // Bright red for breakpoints
|
||||
constexpr ImVec4 kColorHotPath(1.0f, 0.6f, 0.0f, 1.0f); // Orange for hot paths
|
||||
|
||||
} // namespace
|
||||
|
||||
void DisassemblyViewer::RecordInstruction(uint32_t address, uint8_t opcode,
|
||||
const std::vector<uint8_t>& operands,
|
||||
const std::string& mnemonic,
|
||||
const std::string& operand_str) {
|
||||
auto it = instructions_.find(address);
|
||||
if (it != instructions_.end()) {
|
||||
// Instruction already recorded, just increment execution count
|
||||
it->second.execution_count++;
|
||||
} else {
|
||||
// New instruction, add to map
|
||||
DisassemblyEntry entry;
|
||||
entry.address = address;
|
||||
entry.opcode = opcode;
|
||||
entry.operands = operands;
|
||||
entry.mnemonic = mnemonic;
|
||||
entry.operand_str = operand_str;
|
||||
entry.size = 1 + operands.size();
|
||||
entry.execution_count = 1;
|
||||
entry.is_breakpoint = false;
|
||||
entry.is_current_pc = false;
|
||||
|
||||
instructions_[address] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
void DisassemblyViewer::Render(uint32_t current_pc,
|
||||
const std::vector<uint32_t>& breakpoints) {
|
||||
// Update current PC and breakpoint flags
|
||||
for (auto& [addr, entry] : instructions_) {
|
||||
entry.is_current_pc = (addr == current_pc);
|
||||
entry.is_breakpoint = std::find(breakpoints.begin(), breakpoints.end(), addr)
|
||||
!= breakpoints.end();
|
||||
}
|
||||
|
||||
RenderToolbar();
|
||||
RenderSearchBar();
|
||||
RenderDisassemblyTable(current_pc, breakpoints);
|
||||
}
|
||||
|
||||
void DisassemblyViewer::RenderToolbar() {
|
||||
if (ImGui::BeginTable("##DisasmToolbar", 6, ImGuiTableFlags_None)) {
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::Button(ICON_MD_CLEAR_ALL " Clear")) {
|
||||
Clear();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Clear all recorded instructions");
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::Button(ICON_MD_SAVE " Export")) {
|
||||
// TODO: Open file dialog and export
|
||||
ExportToFile("disassembly.asm");
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Export disassembly to file");
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::Checkbox("Auto-scroll", &auto_scroll_)) {
|
||||
// Toggle auto-scroll
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Auto-scroll to current PC");
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::Checkbox("Exec Count", &show_execution_counts_)) {
|
||||
// Toggle execution count display
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Show execution counts");
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::Checkbox("Hex Dump", &show_hex_dump_)) {
|
||||
// Toggle hex dump display
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Show hex dump of instruction bytes");
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text(ICON_MD_MEMORY " %zu instructions", instructions_.size());
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
void DisassemblyViewer::RenderSearchBar() {
|
||||
ImGui::PushItemWidth(-1.0f);
|
||||
if (ImGui::InputTextWithHint("##DisasmSearch", ICON_MD_SEARCH " Search (address, mnemonic, operand)...",
|
||||
search_filter_, IM_ARRAYSIZE(search_filter_))) {
|
||||
// Search filter updated
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
}
|
||||
|
||||
void DisassemblyViewer::RenderDisassemblyTable(uint32_t current_pc,
|
||||
const std::vector<uint32_t>& breakpoints) {
|
||||
// Table flags for professional disassembly view
|
||||
ImGuiTableFlags flags =
|
||||
ImGuiTableFlags_Borders |
|
||||
ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_ScrollY |
|
||||
ImGuiTableFlags_Resizable |
|
||||
ImGuiTableFlags_Sortable |
|
||||
ImGuiTableFlags_Reorderable |
|
||||
ImGuiTableFlags_Hideable;
|
||||
|
||||
// Calculate column count based on optional columns
|
||||
int column_count = 4; // BP, Address, Mnemonic, Operand (always shown)
|
||||
if (show_hex_dump_) column_count++;
|
||||
if (show_execution_counts_) column_count++;
|
||||
|
||||
if (!ImGui::BeginTable("##DisasmTable", column_count, flags, ImVec2(0.0f, 0.0f))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup columns
|
||||
ImGui::TableSetupColumn(ICON_MD_CIRCLE, ImGuiTableColumnFlags_WidthFixed, 25.0f); // Breakpoint indicator
|
||||
ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
||||
if (show_hex_dump_) {
|
||||
ImGui::TableSetupColumn("Hex", ImGuiTableColumnFlags_WidthFixed, 100.0f);
|
||||
}
|
||||
ImGui::TableSetupColumn("Mnemonic", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
||||
ImGui::TableSetupColumn("Operand", ImGuiTableColumnFlags_WidthStretch);
|
||||
if (show_execution_counts_) {
|
||||
ImGui::TableSetupColumn(ICON_MD_TRENDING_UP " Count", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
||||
}
|
||||
|
||||
ImGui::TableSetupScrollFreeze(0, 1); // Freeze header row
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
// Render instructions
|
||||
ImGuiListClipper clipper;
|
||||
auto sorted_addrs = GetSortedAddresses();
|
||||
clipper.Begin(sorted_addrs.size());
|
||||
|
||||
while (clipper.Step()) {
|
||||
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; row++) {
|
||||
uint32_t addr = sorted_addrs[row];
|
||||
const auto& entry = instructions_[addr];
|
||||
|
||||
// Skip if doesn't pass filter
|
||||
if (!PassesFilter(entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui::TableNextRow();
|
||||
|
||||
// Highlight current PC row
|
||||
if (entry.is_current_pc) {
|
||||
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
|
||||
ImGui::GetColorU32(ImVec4(0.3f, 0.0f, 0.0f, 0.5f)));
|
||||
}
|
||||
|
||||
// Column 0: Breakpoint indicator
|
||||
ImGui::TableNextColumn();
|
||||
if (entry.is_breakpoint) {
|
||||
ImGui::TextColored(kColorBreakpoint, ICON_MD_STOP);
|
||||
} else {
|
||||
ImGui::TextDisabled(" ");
|
||||
}
|
||||
|
||||
// Column 1: Address (clickable)
|
||||
ImGui::TableNextColumn();
|
||||
ImVec4 addr_color = GetAddressColor(entry, current_pc);
|
||||
|
||||
std::string addr_str = absl::StrFormat("$%02X:%04X",
|
||||
(addr >> 16) & 0xFF, addr & 0xFFFF);
|
||||
if (ImGui::Selectable(addr_str.c_str(), selected_address_ == addr,
|
||||
ImGuiSelectableFlags_SpanAllColumns)) {
|
||||
selected_address_ = addr;
|
||||
}
|
||||
|
||||
// Context menu on right-click
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
RenderContextMenu(addr);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// Column 2: Hex dump (optional)
|
||||
if (show_hex_dump_) {
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextColored(kColorOpcode, "%s", FormatHexDump(entry).c_str());
|
||||
}
|
||||
|
||||
// Column 3: Mnemonic (clickable for documentation)
|
||||
ImGui::TableNextColumn();
|
||||
ImVec4 mnemonic_color = GetMnemonicColor(entry);
|
||||
if (ImGui::Selectable(entry.mnemonic.c_str(), false)) {
|
||||
// TODO: Open documentation for this mnemonic
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Click for instruction documentation");
|
||||
}
|
||||
|
||||
// Column 4: Operand (clickable for jump-to-address)
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextColored(kColorOperand, "%s", entry.operand_str.c_str());
|
||||
|
||||
// Column 5: Execution count (optional)
|
||||
if (show_execution_counts_) {
|
||||
ImGui::TableNextColumn();
|
||||
|
||||
// Color-code by execution frequency (hot path highlighting)
|
||||
ImVec4 count_color = kColorComment;
|
||||
if (entry.execution_count > 10000) {
|
||||
count_color = kColorHotPath;
|
||||
} else if (entry.execution_count > 1000) {
|
||||
count_color = ImVec4(0.8f, 0.8f, 0.3f, 1.0f); // Yellow
|
||||
}
|
||||
|
||||
ImGui::TextColored(count_color, "%llu", entry.execution_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scroll to current PC
|
||||
if (auto_scroll_ && scroll_to_address_ != current_pc) {
|
||||
// Find row index of current PC
|
||||
auto it = std::find(sorted_addrs.begin(), sorted_addrs.end(), current_pc);
|
||||
if (it != sorted_addrs.end()) {
|
||||
int row_index = std::distance(sorted_addrs.begin(), it);
|
||||
ImGui::SetScrollY((row_index * ImGui::GetTextLineHeightWithSpacing()) -
|
||||
(ImGui::GetWindowHeight() * 0.5f));
|
||||
scroll_to_address_ = current_pc;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
void DisassemblyViewer::RenderContextMenu(uint32_t address) {
|
||||
auto& entry = instructions_[address];
|
||||
|
||||
if (ImGui::MenuItem(ICON_MD_FLAG " Toggle Breakpoint")) {
|
||||
// TODO: Implement breakpoint toggle callback
|
||||
}
|
||||
|
||||
if (ImGui::MenuItem(ICON_MD_MY_LOCATION " Jump to Address")) {
|
||||
JumpToAddress(address);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(ICON_MD_CONTENT_COPY " Copy Address")) {
|
||||
ImGui::SetClipboardText(absl::StrFormat("$%06X", address).c_str());
|
||||
}
|
||||
|
||||
if (ImGui::MenuItem(ICON_MD_CONTENT_COPY " Copy Instruction")) {
|
||||
std::string instr = absl::StrFormat("%s %s", entry.mnemonic.c_str(),
|
||||
entry.operand_str.c_str());
|
||||
ImGui::SetClipboardText(instr.c_str());
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem(ICON_MD_INFO " Show Info")) {
|
||||
// TODO: Show detailed instruction info
|
||||
}
|
||||
}
|
||||
|
||||
ImVec4 DisassemblyViewer::GetAddressColor(const DisassemblyEntry& entry,
|
||||
uint32_t current_pc) const {
|
||||
if (entry.is_current_pc) {
|
||||
return kColorCurrentPC;
|
||||
}
|
||||
if (entry.is_breakpoint) {
|
||||
return kColorBreakpoint;
|
||||
}
|
||||
return kColorAddress;
|
||||
}
|
||||
|
||||
ImVec4 DisassemblyViewer::GetMnemonicColor(const DisassemblyEntry& entry) const {
|
||||
// Color-code by instruction type
|
||||
const std::string& mnemonic = entry.mnemonic;
|
||||
|
||||
// Branches and jumps
|
||||
if (mnemonic.find('B') == 0 || mnemonic == "JMP" || mnemonic == "JSR" ||
|
||||
mnemonic == "RTL" || mnemonic == "RTS" || mnemonic == "RTI") {
|
||||
return ImVec4(0.8f, 0.4f, 1.0f, 1.0f); // Purple for control flow
|
||||
}
|
||||
|
||||
// Loads
|
||||
if (mnemonic.find("LD") == 0) {
|
||||
return ImVec4(0.4f, 1.0f, 0.4f, 1.0f); // Green for loads
|
||||
}
|
||||
|
||||
// Stores
|
||||
if (mnemonic.find("ST") == 0) {
|
||||
return ImVec4(1.0f, 0.6f, 0.4f, 1.0f); // Orange for stores
|
||||
}
|
||||
|
||||
return kColorMnemonic;
|
||||
}
|
||||
|
||||
std::string DisassemblyViewer::FormatHexDump(const DisassemblyEntry& entry) const {
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::uppercase << std::setfill('0');
|
||||
|
||||
// Opcode
|
||||
ss << std::setw(2) << static_cast<int>(entry.opcode);
|
||||
|
||||
// Operands
|
||||
for (const auto& operand_byte : entry.operands) {
|
||||
ss << " " << std::setw(2) << static_cast<int>(operand_byte);
|
||||
}
|
||||
|
||||
// Pad to consistent width (3 bytes max)
|
||||
for (size_t i = entry.operands.size(); i < 2; i++) {
|
||||
ss << " ";
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
bool DisassemblyViewer::PassesFilter(const DisassemblyEntry& entry) const {
|
||||
if (search_filter_[0] == '\0') {
|
||||
return true; // No filter active
|
||||
}
|
||||
|
||||
std::string filter_lower(search_filter_);
|
||||
std::transform(filter_lower.begin(), filter_lower.end(),
|
||||
filter_lower.begin(), ::tolower);
|
||||
|
||||
// Check address
|
||||
std::string addr_str = absl::StrFormat("%06x", entry.address);
|
||||
if (addr_str.find(filter_lower) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check mnemonic
|
||||
std::string mnemonic_lower = entry.mnemonic;
|
||||
std::transform(mnemonic_lower.begin(), mnemonic_lower.end(),
|
||||
mnemonic_lower.begin(), ::tolower);
|
||||
if (mnemonic_lower.find(filter_lower) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check operand
|
||||
std::string operand_lower = entry.operand_str;
|
||||
std::transform(operand_lower.begin(), operand_lower.end(),
|
||||
operand_lower.begin(), ::tolower);
|
||||
if (operand_lower.find(filter_lower) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void DisassemblyViewer::Clear() {
|
||||
instructions_.clear();
|
||||
selected_address_ = 0;
|
||||
scroll_to_address_ = 0;
|
||||
}
|
||||
|
||||
bool DisassemblyViewer::ExportToFile(const std::string& filepath) const {
|
||||
std::ofstream out(filepath);
|
||||
if (!out.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out << "; YAZE Disassembly Export\n";
|
||||
out << "; Total instructions: " << instructions_.size() << "\n";
|
||||
out << "; Generated: " << __DATE__ << " " << __TIME__ << "\n\n";
|
||||
|
||||
auto sorted_addrs = GetSortedAddresses();
|
||||
for (uint32_t addr : sorted_addrs) {
|
||||
const auto& entry = instructions_.at(addr);
|
||||
|
||||
out << absl::StrFormat("$%02X:%04X: %-8s %-6s %-20s ; exec=%llu\n",
|
||||
(addr >> 16) & 0xFF,
|
||||
addr & 0xFFFF,
|
||||
FormatHexDump(entry).c_str(),
|
||||
entry.mnemonic.c_str(),
|
||||
entry.operand_str.c_str(),
|
||||
entry.execution_count);
|
||||
}
|
||||
|
||||
out.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DisassemblyViewer::JumpToAddress(uint32_t address) {
|
||||
selected_address_ = address;
|
||||
scroll_to_address_ = 0; // Force scroll update
|
||||
auto_scroll_ = false; // Disable auto-scroll temporarily
|
||||
}
|
||||
|
||||
std::vector<uint32_t> DisassemblyViewer::GetSortedAddresses() const {
|
||||
std::vector<uint32_t> addrs;
|
||||
addrs.reserve(instructions_.size());
|
||||
|
||||
for (const auto& [addr, _] : instructions_) {
|
||||
addrs.push_back(addr);
|
||||
}
|
||||
|
||||
std::sort(addrs.begin(), addrs.end());
|
||||
return addrs;
|
||||
}
|
||||
|
||||
} // namespace debug
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
146
src/app/emu/debug/disassembly_viewer.h
Normal file
146
src/app/emu/debug/disassembly_viewer.h
Normal file
@@ -0,0 +1,146 @@
|
||||
#ifndef YAZE_APP_EMU_DEBUG_DISASSEMBLY_VIEWER_H_
|
||||
#define YAZE_APP_EMU_DEBUG_DISASSEMBLY_VIEWER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "app/emu/cpu/cpu.h"
|
||||
#include "app/gfx/bitmap.h"
|
||||
#include "app/gui/icons.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
namespace debug {
|
||||
|
||||
/**
|
||||
* @brief Represents a single disassembled instruction with metadata
|
||||
*/
|
||||
struct DisassemblyEntry {
|
||||
uint32_t address; // Full 24-bit address (bank:offset)
|
||||
uint8_t opcode; // The opcode byte
|
||||
std::vector<uint8_t> operands; // Operand bytes (0-2 bytes)
|
||||
std::string mnemonic; // Instruction mnemonic (e.g., "LDA", "STA")
|
||||
std::string operand_str; // Formatted operand string (e.g., "#$00", "($10),Y")
|
||||
uint8_t size; // Total instruction size in bytes
|
||||
uint64_t execution_count; // How many times this instruction was executed
|
||||
bool is_breakpoint; // Whether a breakpoint is set at this address
|
||||
bool is_current_pc; // Whether this is the current PC location
|
||||
|
||||
DisassemblyEntry()
|
||||
: address(0), opcode(0), size(1), execution_count(0),
|
||||
is_breakpoint(false), is_current_pc(false) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Advanced disassembly viewer with sparse storage and interactive features
|
||||
*
|
||||
* This viewer provides a professional disassembly interface similar to modern
|
||||
* debuggers and ROM hacking tools. Features include:
|
||||
* - Sparse address-based storage (only stores executed instructions)
|
||||
* - Optimized ImGui table rendering with virtual scrolling
|
||||
* - Clickable addresses, opcodes, and operands
|
||||
* - Context menus for setting breakpoints, jumping to addresses, etc.
|
||||
* - Highlighting of current PC, breakpoints, and hot paths
|
||||
* - Search and filter capabilities
|
||||
* - Export to assembly file
|
||||
*/
|
||||
class DisassemblyViewer {
|
||||
public:
|
||||
DisassemblyViewer() = default;
|
||||
~DisassemblyViewer() = default;
|
||||
|
||||
/**
|
||||
* @brief Record an instruction execution
|
||||
* @param address Full 24-bit address
|
||||
* @param opcode The opcode byte
|
||||
* @param operands Vector of operand bytes
|
||||
* @param mnemonic Instruction mnemonic
|
||||
* @param operand_str Formatted operand string
|
||||
*/
|
||||
void RecordInstruction(uint32_t address, uint8_t opcode,
|
||||
const std::vector<uint8_t>& operands,
|
||||
const std::string& mnemonic,
|
||||
const std::string& operand_str);
|
||||
|
||||
/**
|
||||
* @brief Render the disassembly viewer UI
|
||||
* @param current_pc Current program counter (24-bit)
|
||||
* @param breakpoints List of breakpoint addresses
|
||||
*/
|
||||
void Render(uint32_t current_pc, const std::vector<uint32_t>& breakpoints);
|
||||
|
||||
/**
|
||||
* @brief Clear all recorded instructions
|
||||
*/
|
||||
void Clear();
|
||||
|
||||
/**
|
||||
* @brief Get the number of unique instructions recorded
|
||||
*/
|
||||
size_t GetInstructionCount() const { return instructions_.size(); }
|
||||
|
||||
/**
|
||||
* @brief Export disassembly to file
|
||||
* @param filepath Path to output file
|
||||
* @return true if successful
|
||||
*/
|
||||
bool ExportToFile(const std::string& filepath) const;
|
||||
|
||||
/**
|
||||
* @brief Jump to a specific address in the viewer
|
||||
* @param address Address to jump to
|
||||
*/
|
||||
void JumpToAddress(uint32_t address);
|
||||
|
||||
/**
|
||||
* @brief Set whether to auto-scroll to current PC
|
||||
*/
|
||||
void SetAutoScroll(bool enabled) { auto_scroll_ = enabled; }
|
||||
|
||||
/**
|
||||
* @brief Get sorted list of addresses for rendering
|
||||
*/
|
||||
std::vector<uint32_t> GetSortedAddresses() const;
|
||||
|
||||
/**
|
||||
* @brief Check if the disassembly viewer is available
|
||||
*/
|
||||
bool IsAvailable() const { return !instructions_.empty(); }
|
||||
|
||||
private:
|
||||
// Sparse storage: only store executed instructions
|
||||
std::map<uint32_t, DisassemblyEntry> instructions_;
|
||||
|
||||
// UI state
|
||||
char search_filter_[256] = "";
|
||||
uint32_t selected_address_ = 0;
|
||||
uint32_t scroll_to_address_ = 0;
|
||||
bool auto_scroll_ = true;
|
||||
bool show_execution_counts_ = true;
|
||||
bool show_hex_dump_ = true;
|
||||
|
||||
// Rendering helpers
|
||||
void RenderToolbar();
|
||||
void RenderDisassemblyTable(uint32_t current_pc,
|
||||
const std::vector<uint32_t>& breakpoints);
|
||||
void RenderContextMenu(uint32_t address);
|
||||
void RenderSearchBar();
|
||||
|
||||
// Formatting helpers
|
||||
ImVec4 GetAddressColor(const DisassemblyEntry& entry, uint32_t current_pc) const;
|
||||
ImVec4 GetMnemonicColor(const DisassemblyEntry& entry) const;
|
||||
std::string FormatHexDump(const DisassemblyEntry& entry) const;
|
||||
|
||||
// Filter helper
|
||||
bool PassesFilter(const DisassemblyEntry& entry) const;
|
||||
};
|
||||
|
||||
} // namespace debug
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EMU_DEBUG_DISASSEMBLY_VIEWER_H_
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -207,18 +207,30 @@ void Ppu::HandlePixel(int x, int y) {
|
||||
}
|
||||
}
|
||||
int row = (y - 1) + (even_frame ? 0 : 239);
|
||||
|
||||
// SDL_PIXELFORMAT_ARGB8888 with pixelOutputFormat=0 (BGRX)
|
||||
// Memory layout: [B][G][R][A] at offsets 0,1,2,3 respectively
|
||||
// Convert 5-bit SNES color (0-31) to 8-bit (0-255) via (val << 3) | (val >> 2)
|
||||
// Two pixels per X position for hi-res support:
|
||||
// pixel1 at x*8 + 0..3, pixel2 at x*8 + 4..7
|
||||
|
||||
// First pixel (hi-res/main screen)
|
||||
pixelBuffer[row * 2048 + x * 8 + 0 + pixelOutputFormat] =
|
||||
((b2 << 3) | (b2 >> 2)) * brightness / 15;
|
||||
((b2 << 3) | (b2 >> 2)) * brightness / 15; // Blue channel
|
||||
pixelBuffer[row * 2048 + x * 8 + 1 + pixelOutputFormat] =
|
||||
((g2 << 3) | (g2 >> 2)) * brightness / 15;
|
||||
((g2 << 3) | (g2 >> 2)) * brightness / 15; // Green channel
|
||||
pixelBuffer[row * 2048 + x * 8 + 2 + pixelOutputFormat] =
|
||||
((r2 << 3) | (r2 >> 2)) * brightness / 15;
|
||||
((r2 << 3) | (r2 >> 2)) * brightness / 15; // Red channel
|
||||
pixelBuffer[row * 2048 + x * 8 + 3 + pixelOutputFormat] = 0xFF; // Alpha (opaque)
|
||||
|
||||
// Second pixel (lo-res/subscreen)
|
||||
pixelBuffer[row * 2048 + x * 8 + 4 + pixelOutputFormat] =
|
||||
((b << 3) | (b >> 2)) * brightness / 15;
|
||||
((b << 3) | (b >> 2)) * brightness / 15; // Blue channel
|
||||
pixelBuffer[row * 2048 + x * 8 + 5 + pixelOutputFormat] =
|
||||
((g << 3) | (g >> 2)) * brightness / 15;
|
||||
((g << 3) | (g >> 2)) * brightness / 15; // Green channel
|
||||
pixelBuffer[row * 2048 + x * 8 + 6 + pixelOutputFormat] =
|
||||
((r << 3) | (r >> 2)) * brightness / 15;
|
||||
((r << 3) | (r >> 2)) * brightness / 15; // Red channel
|
||||
pixelBuffer[row * 2048 + x * 8 + 7 + pixelOutputFormat] = 0xFF; // Alpha (opaque)
|
||||
}
|
||||
|
||||
int Ppu::GetPixel(int x, int y, bool subscreen, int* r, int* g, int* b) {
|
||||
|
||||
@@ -258,8 +258,9 @@ class Ppu {
|
||||
// Initialize the frame buffer
|
||||
void Init() {
|
||||
frame_buffer_.resize(256 * 240, 0);
|
||||
// Set to XBGR format (1) for compatibility with SDL_PIXELFORMAT_ARGB8888
|
||||
pixelOutputFormat = 1;
|
||||
// Set to BGRX format (0) for compatibility with SDL_PIXELFORMAT_ARGB8888
|
||||
// Format 0 = BGRX: [B][G][R][A] byte order in memory (little-endian)
|
||||
pixelOutputFormat = 0;
|
||||
}
|
||||
|
||||
void Reset();
|
||||
|
||||
@@ -279,6 +279,13 @@ absl::Status Rom::LoadFromFile(const std::string &filename,
|
||||
return absl::InvalidArgumentError(
|
||||
"Could not load ROM: parameter `filename` is empty.");
|
||||
}
|
||||
|
||||
// Validate file exists before proceeding
|
||||
if (!std::filesystem::exists(filename)) {
|
||||
return absl::NotFoundError(
|
||||
absl::StrCat("ROM file does not exist: ", filename));
|
||||
}
|
||||
|
||||
filename_ = std::filesystem::absolute(filename).string();
|
||||
short_name_ = filename_.substr(filename_.find_last_of("/\\") + 1);
|
||||
|
||||
@@ -288,9 +295,19 @@ absl::Status Rom::LoadFromFile(const std::string &filename,
|
||||
absl::StrCat("Could not open ROM file: ", filename_));
|
||||
}
|
||||
|
||||
// Get file size and resize rom_data_
|
||||
// Get file size and validate
|
||||
try {
|
||||
size_ = std::filesystem::file_size(filename_);
|
||||
|
||||
// Validate ROM size (minimum 32KB, maximum 8MB for expanded ROMs)
|
||||
if (size_ < 32768) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("ROM file too small (%zu bytes), minimum is 32KB", size_));
|
||||
}
|
||||
if (size_ > 8 * 1024 * 1024) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("ROM file too large (%zu bytes), maximum is 8MB", size_));
|
||||
}
|
||||
} catch (const std::filesystem::filesystem_error &e) {
|
||||
// Try to get the file size from the open file stream
|
||||
file.seekg(0, std::ios::end);
|
||||
@@ -299,11 +316,30 @@ absl::Status Rom::LoadFromFile(const std::string &filename,
|
||||
"Could not get file size: ", filename_, " - ", e.what()));
|
||||
}
|
||||
size_ = file.tellg();
|
||||
|
||||
// Validate size from stream
|
||||
if (size_ < 32768 || size_ > 8 * 1024 * 1024) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Invalid ROM size: %zu bytes", size_));
|
||||
}
|
||||
}
|
||||
rom_data_.resize(size_);
|
||||
|
||||
// Read file into rom_data_
|
||||
file.read(reinterpret_cast<char *>(rom_data_.data()), size_);
|
||||
|
||||
// Allocate and read ROM data
|
||||
try {
|
||||
rom_data_.resize(size_);
|
||||
file.seekg(0, std::ios::beg);
|
||||
file.read(reinterpret_cast<char *>(rom_data_.data()), size_);
|
||||
|
||||
if (!file) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("Failed to read ROM data, read %zu of %zu bytes",
|
||||
file.gcount(), size_));
|
||||
}
|
||||
} catch (const std::bad_alloc& e) {
|
||||
return absl::ResourceExhaustedError(
|
||||
absl::StrFormat("Failed to allocate memory for ROM (%zu bytes)", size_));
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
if (!options.load_zelda3_content) {
|
||||
|
||||
Reference in New Issue
Block a user