diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 057d04d6..80e111fb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/app/emu/cpu/cpu.cc b/src/app/emu/cpu/cpu.cc index 192ac058..f3ca7e93 100644 --- a/src/app/emu/cpu/cpu.cc +++ b/src/app/emu/cpu/cpu.cc @@ -6,13 +6,29 @@ #include #include +#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(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 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(PB) << ":" << std::hex << PC << ": 0x" << std::setw(2) << std::setfill('0') << std::hex - << static_cast(opcode) << " " << opcode_to_mnemonic.at(opcode) - << " "; + << static_cast(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(operand); - } else { - oss_ops << std::hex << std::setw(4) << std::setfill('0') - << static_cast(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 { diff --git a/src/app/emu/cpu/cpu.h b/src/app/emu/cpu/cpu.h index e200accc..7d24857d 100644 --- a/src/app/emu/cpu/cpu.h +++ b/src/app/emu/cpu/cpu.h @@ -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 breakpoints_; - std::vector instruction_log_; + std::vector 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 diff --git a/src/app/emu/debug/disassembly_viewer.cc b/src/app/emu/debug/disassembly_viewer.cc new file mode 100644 index 00000000..3b8c6f3e --- /dev/null +++ b/src/app/emu/debug/disassembly_viewer.cc @@ -0,0 +1,438 @@ +#include "app/emu/debug/disassembly_viewer.h" + +#include +#include +#include +#include + +#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& 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& 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& 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(entry.opcode); + + // Operands + for (const auto& operand_byte : entry.operands) { + ss << " " << std::setw(2) << static_cast(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 DisassemblyViewer::GetSortedAddresses() const { + std::vector 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 + diff --git a/src/app/emu/debug/disassembly_viewer.h b/src/app/emu/debug/disassembly_viewer.h new file mode 100644 index 00000000..ee91e581 --- /dev/null +++ b/src/app/emu/debug/disassembly_viewer.h @@ -0,0 +1,146 @@ +#ifndef YAZE_APP_EMU_DEBUG_DISASSEMBLY_VIEWER_H_ +#define YAZE_APP_EMU_DEBUG_DISASSEMBLY_VIEWER_H_ + +#include +#include +#include +#include + +#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 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& 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& 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 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 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& 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_ + diff --git a/src/app/emu/emulator.cc b/src/app/emu/emulator.cc index 5c3b8777..64145f9d 100644 --- a/src/app/emu/emulator.cc +++ b/src/app/emu/emulator.cc @@ -4,16 +4,17 @@ #include #include -#include "util/file_util.h" #include "app/core/window.h" #include "app/emu/cpu/internal/opcodes.h" +#include "app/emu/debug/disassembly_viewer.h" +#include "app/gui/color.h" +#include "app/gui/editor_layout.h" #include "app/gui/icons.h" #include "app/gui/input.h" #include "app/gui/theme_manager.h" -#include "app/gui/color.h" -#include "app/gui/editor_layout.h" #include "imgui/imgui.h" #include "imgui_memory_editor.h" +#include "util/file_util.h" namespace yaze { namespace emu { @@ -60,14 +61,14 @@ void Emulator::Run(Rom* rom) { printf("Failed to create texture: %s\n", SDL_GetError()); return; } - + // Optimize texture for better performance SDL_SetTextureBlendMode(ppu_texture_, SDL_BLENDMODE_NONE); rom_data_ = rom->vector(); snes_.Init(rom_data_); - + // Note: PPU pixel format set to 1 (XBGR) in Init() which matches ARGB8888 texture - + wanted_frames_ = 1.0 / (snes_.memory().pal_timing() ? 50.0 : 60.0); wanted_samples_ = 48000 / (snes_.memory().pal_timing() ? 50 : 60); loaded = true; @@ -90,23 +91,35 @@ void Emulator::Run(Rom* rom) { last_count = current_count; double seconds = delta / (double)count_frequency; time_adder += seconds; - + // Cap time accumulation to prevent spiral of death and improve stability if (time_adder > wanted_frames_ * 3.0) { time_adder = wanted_frames_ * 3.0; } - - // allow 2 ms earlier, to prevent skipping due to being just below wanted + + // Track frames to skip for performance + int frames_to_process = 0; while (time_adder >= wanted_frames_ - 0.002) { time_adder -= wanted_frames_; + frames_to_process++; + } - if (loaded) { + // Limit maximum frames to process (prevent spiral of death) + if (frames_to_process > 4) { + frames_to_process = 4; + } + + if (loaded && frames_to_process > 0) { + // Process frames (skip rendering for all but last frame if falling behind) + for (int i = 0; i < frames_to_process; i++) { + bool should_render = (i == frames_to_process - 1); + // Run frame if (turbo_mode_) { snes_.RunFrame(); } snes_.RunFrame(); - + // Track FPS frame_count_++; fps_timer_ += wanted_frames_; @@ -116,35 +129,32 @@ void Emulator::Run(Rom* rom) { fps_timer_ = 0.0; } - // Generate and queue audio samples with better buffering - snes_.SetSamples(audio_buffer_, wanted_samples_); - uint32_t queued = SDL_GetQueuedAudioSize(audio_device_); - uint32_t max_buffer_size = wanted_samples_ * 4 * 4; // Reduced from 6 to 4 frames - - // Only queue audio if buffer is not too full to prevent audio lag - if (queued <= max_buffer_size) { - SDL_QueueAudio(audio_device_, audio_buffer_, wanted_samples_ * 4); - } else { - // Clear excess audio to prevent audio lag buildup - SDL_ClearQueuedAudio(audio_device_); - } + // Only render and handle audio on the last frame + if (should_render) { + // Generate and queue audio samples with improved buffering + snes_.SetSamples(audio_buffer_, wanted_samples_); + uint32_t queued = SDL_GetQueuedAudioSize(audio_device_); + uint32_t target_buffer = wanted_samples_ * 4 * 2; // Target 2 frames buffered + uint32_t max_buffer = wanted_samples_ * 4 * 6; // Max 6 frames - // Update PPU texture with better error handling - void* ppu_pixels_; - int ppu_pitch_; - if (SDL_LockTexture(ppu_texture_, NULL, &ppu_pixels_, &ppu_pitch_) != - 0) { - printf("Failed to lock texture: %s\n", SDL_GetError()); - // Don't return here, continue with next frame to prevent deadlock - continue; + if (queued < target_buffer) { + // Buffer is low, queue more audio + SDL_QueueAudio(audio_device_, audio_buffer_, wanted_samples_ * 4); + } else if (queued > max_buffer) { + // Buffer is too full, clear it to prevent lag + SDL_ClearQueuedAudio(audio_device_); + SDL_QueueAudio(audio_device_, audio_buffer_, wanted_samples_ * 4); + } + + // Update PPU texture only on rendered frames + void* ppu_pixels_; + int ppu_pitch_; + if (SDL_LockTexture(ppu_texture_, NULL, &ppu_pixels_, &ppu_pitch_) == + 0) { + snes_.SetPixels(static_cast(ppu_pixels_)); + SDL_UnlockTexture(ppu_texture_); + } } - snes_.SetPixels(static_cast(ppu_pixels_)); - SDL_UnlockTexture(ppu_texture_); - } - - // Don't run multiple frames if we're just slightly ahead - if (!turbo_mode_ && time_adder < wanted_frames_) { - break; } } } @@ -157,121 +167,126 @@ void Emulator::RenderEmulatorInterface() { try { auto& theme_manager = gui::ThemeManager::Get(); const auto& theme = theme_manager.GetCurrentTheme(); - - // Modern EditorCard-based layout - modular and flexible - static bool show_cpu_debugger_ = true; - static bool show_ppu_display_ = true; - static bool show_memory_viewer_ = false; - static bool show_breakpoints_ = false; - static bool show_performance_ = true; - static bool show_ai_agent_ = false; - static bool show_save_states_ = false; - static bool show_keyboard_config_ = false; - - // Create session-aware cards - gui::EditorCard cpu_card(ICON_MD_MEMORY " CPU Debugger", ICON_MD_MEMORY); - gui::EditorCard ppu_card(ICON_MD_VIDEOGAME_ASSET " PPU Display", ICON_MD_VIDEOGAME_ASSET); - gui::EditorCard memory_card(ICON_MD_DATA_ARRAY " Memory Viewer", ICON_MD_DATA_ARRAY); - gui::EditorCard breakpoints_card(ICON_MD_BUG_REPORT " Breakpoints", ICON_MD_BUG_REPORT); - gui::EditorCard performance_card(ICON_MD_SPEED " Performance", ICON_MD_SPEED); - gui::EditorCard ai_card(ICON_MD_SMART_TOY " AI Agent", ICON_MD_SMART_TOY); - gui::EditorCard save_states_card(ICON_MD_SAVE " Save States", ICON_MD_SAVE); - gui::EditorCard keyboard_card(ICON_MD_KEYBOARD " Keyboard Config", ICON_MD_KEYBOARD); - - // Configure default positions - static bool cards_configured = false; - if (!cards_configured) { - cpu_card.SetDefaultSize(400, 500); - cpu_card.SetPosition(gui::EditorCard::Position::Right); - - ppu_card.SetDefaultSize(550, 520); - ppu_card.SetPosition(gui::EditorCard::Position::Floating); - - memory_card.SetDefaultSize(800, 600); - memory_card.SetPosition(gui::EditorCard::Position::Floating); - - breakpoints_card.SetDefaultSize(400, 350); - breakpoints_card.SetPosition(gui::EditorCard::Position::Right); - - performance_card.SetDefaultSize(350, 300); - performance_card.SetPosition(gui::EditorCard::Position::Bottom); - - ai_card.SetDefaultSize(500, 450); - ai_card.SetPosition(gui::EditorCard::Position::Floating); - - save_states_card.SetDefaultSize(400, 300); - save_states_card.SetPosition(gui::EditorCard::Position::Floating); - - keyboard_card.SetDefaultSize(450, 400); - keyboard_card.SetPosition(gui::EditorCard::Position::Floating); - - cards_configured = true; - } - - // CPU Debugger Card - if (show_cpu_debugger_) { - if (cpu_card.Begin(&show_cpu_debugger_)) { - RenderModernCpuDebugger(); + + // Modern EditorCard-based layout - modular and flexible + static bool show_cpu_debugger_ = true; + static bool show_ppu_display_ = true; + static bool show_memory_viewer_ = false; + static bool show_breakpoints_ = false; + static bool show_performance_ = true; + static bool show_ai_agent_ = false; + static bool show_save_states_ = false; + static bool show_keyboard_config_ = false; + + // Create session-aware cards + gui::EditorCard cpu_card(ICON_MD_MEMORY " CPU Debugger", ICON_MD_MEMORY); + gui::EditorCard ppu_card(ICON_MD_VIDEOGAME_ASSET " PPU Display", + ICON_MD_VIDEOGAME_ASSET); + gui::EditorCard memory_card(ICON_MD_DATA_ARRAY " Memory Viewer", + ICON_MD_DATA_ARRAY); + gui::EditorCard breakpoints_card(ICON_MD_BUG_REPORT " Breakpoints", + ICON_MD_BUG_REPORT); + gui::EditorCard performance_card(ICON_MD_SPEED " Performance", + ICON_MD_SPEED); + gui::EditorCard ai_card(ICON_MD_SMART_TOY " AI Agent", ICON_MD_SMART_TOY); + gui::EditorCard save_states_card(ICON_MD_SAVE " Save States", ICON_MD_SAVE); + gui::EditorCard keyboard_card(ICON_MD_KEYBOARD " Keyboard Config", + ICON_MD_KEYBOARD); + + // Configure default positions + static bool cards_configured = false; + if (!cards_configured) { + cpu_card.SetDefaultSize(400, 500); + cpu_card.SetPosition(gui::EditorCard::Position::Right); + + ppu_card.SetDefaultSize(550, 520); + ppu_card.SetPosition(gui::EditorCard::Position::Floating); + + memory_card.SetDefaultSize(800, 600); + memory_card.SetPosition(gui::EditorCard::Position::Floating); + + breakpoints_card.SetDefaultSize(400, 350); + breakpoints_card.SetPosition(gui::EditorCard::Position::Right); + + performance_card.SetDefaultSize(350, 300); + performance_card.SetPosition(gui::EditorCard::Position::Bottom); + + ai_card.SetDefaultSize(500, 450); + ai_card.SetPosition(gui::EditorCard::Position::Floating); + + save_states_card.SetDefaultSize(400, 300); + save_states_card.SetPosition(gui::EditorCard::Position::Floating); + + keyboard_card.SetDefaultSize(450, 400); + keyboard_card.SetPosition(gui::EditorCard::Position::Floating); + + cards_configured = true; } - cpu_card.End(); - } - - // PPU Display Card - if (show_ppu_display_) { - if (ppu_card.Begin(&show_ppu_display_)) { - RenderSnesPpu(); + + // CPU Debugger Card + if (show_cpu_debugger_) { + if (cpu_card.Begin(&show_cpu_debugger_)) { + RenderModernCpuDebugger(); + } + cpu_card.End(); } - ppu_card.End(); - } - - // Memory Viewer Card - if (show_memory_viewer_) { - if (memory_card.Begin(&show_memory_viewer_)) { - RenderMemoryViewer(); + + // PPU Display Card + if (show_ppu_display_) { + if (ppu_card.Begin(&show_ppu_display_)) { + RenderSnesPpu(); + } + ppu_card.End(); } - memory_card.End(); - } - - // Breakpoints Card - if (show_breakpoints_) { - if (breakpoints_card.Begin(&show_breakpoints_)) { - RenderBreakpointList(); + + // Memory Viewer Card + if (show_memory_viewer_) { + if (memory_card.Begin(&show_memory_viewer_)) { + RenderMemoryViewer(); + } + memory_card.End(); } - breakpoints_card.End(); - } - - // Performance Monitor Card - if (show_performance_) { - if (performance_card.Begin(&show_performance_)) { - RenderPerformanceMonitor(); + + // Breakpoints Card + if (show_breakpoints_) { + if (breakpoints_card.Begin(&show_breakpoints_)) { + RenderBreakpointList(); + } + breakpoints_card.End(); } - performance_card.End(); - } - - // AI Agent Card - if (show_ai_agent_) { - if (ai_card.Begin(&show_ai_agent_)) { - RenderAIAgentPanel(); + + // Performance Monitor Card + if (show_performance_) { + if (performance_card.Begin(&show_performance_)) { + RenderPerformanceMonitor(); + } + performance_card.End(); } - ai_card.End(); - } - - // Save States Card - if (show_save_states_) { - if (save_states_card.Begin(&show_save_states_)) { - RenderSaveStates(); + + // AI Agent Card + if (show_ai_agent_) { + if (ai_card.Begin(&show_ai_agent_)) { + RenderAIAgentPanel(); + } + ai_card.End(); } - save_states_card.End(); - } - - // Keyboard Configuration Card - if (show_keyboard_config_) { - if (keyboard_card.Begin(&show_keyboard_config_)) { - RenderKeyboardConfig(); + + // Save States Card + if (show_save_states_) { + if (save_states_card.Begin(&show_save_states_)) { + RenderSaveStates(); + } + save_states_card.End(); } - keyboard_card.End(); - } - + + // Keyboard Configuration Card + if (show_keyboard_config_) { + if (keyboard_card.Begin(&show_keyboard_config_)) { + RenderKeyboardConfig(); + } + keyboard_card.End(); + } + } catch (const std::exception& e) { // Fallback to basic UI if theming fails ImGui::Text("Error loading emulator UI: %s", e.what()); @@ -358,8 +373,7 @@ void Emulator::RenderNavBar() { } SameLine(); - if (ImGui::Button(ICON_MD_SYSTEM_UPDATE_ALT)) { - } + if (ImGui::Button(ICON_MD_SYSTEM_UPDATE_ALT)) {} if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Load State"); } @@ -388,7 +402,7 @@ void Emulator::RenderNavBar() { SameLine(); ImGui::Checkbox("Turbo", &turbo_mode_); - + // Display FPS and Audio Status SameLine(); ImGui::Text("|"); @@ -398,7 +412,7 @@ void Emulator::RenderNavBar() { } else { ImGui::Text("FPS: --"); } - + SameLine(); uint32_t audio_queued = SDL_GetQueuedAudioSize(audio_device_); uint32_t audio_frames = audio_queued / (wanted_samples_ * 4); @@ -546,8 +560,7 @@ void Emulator::RenderBreakpointList() { static bool write_mode = false; static bool execute_mode = false; - if (ImGui::Combo("##TypeOfMemory", ¤t_memory_mode, "PRG\0RAM\0")) { - } + if (ImGui::Combo("##TypeOfMemory", ¤t_memory_mode, "PRG\0RAM\0")) {} ImGui::Checkbox("Read", &read_mode); SameLine(); @@ -681,96 +694,157 @@ void Emulator::RenderModernCpuDebugger() { try { auto& theme_manager = gui::ThemeManager::Get(); const auto& theme = theme_manager.GetCurrentTheme(); - - // CPU Status Panel - if (ImGui::CollapsingHeader("CPU Status", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "CPU Status"); + ImGui::PushStyleColor(ImGuiCol_ChildBg, + ConvertColorToImVec4(theme.child_bg)); ImGui::BeginChild("##CpuStatus", ImVec2(0, 120), true); - + // Compact register display in a table - if (ImGui::BeginTable("Registers", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { + if (ImGui::BeginTable( + "Registers", 4, + ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { ImGui::TableSetupColumn("Register", ImGuiTableColumnFlags_WidthFixed, 60); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 80); ImGui::TableSetupColumn("Register", ImGuiTableColumnFlags_WidthFixed, 60); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 80); ImGui::TableHeadersRow(); - + ImGui::TableNextRow(); - ImGui::TableNextColumn(); ImGui::Text("A"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X", snes_.cpu().A); - ImGui::TableNextColumn(); ImGui::Text("D"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X", snes_.cpu().D); - + ImGui::TableNextColumn(); + ImGui::Text("A"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X", + snes_.cpu().A); + ImGui::TableNextColumn(); + ImGui::Text("D"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X", + snes_.cpu().D); + ImGui::TableNextRow(); - ImGui::TableNextColumn(); ImGui::Text("X"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X", snes_.cpu().X); - ImGui::TableNextColumn(); ImGui::Text("DB"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", snes_.cpu().DB); - + ImGui::TableNextColumn(); + ImGui::Text("X"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X", + snes_.cpu().X); + ImGui::TableNextColumn(); + ImGui::Text("DB"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", + snes_.cpu().DB); + ImGui::TableNextRow(); - ImGui::TableNextColumn(); ImGui::Text("Y"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X", snes_.cpu().Y); - ImGui::TableNextColumn(); ImGui::Text("PB"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", snes_.cpu().PB); - + ImGui::TableNextColumn(); + ImGui::Text("Y"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X", + snes_.cpu().Y); + ImGui::TableNextColumn(); + ImGui::Text("PB"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", + snes_.cpu().PB); + ImGui::TableNextRow(); - ImGui::TableNextColumn(); ImGui::Text("PC"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.success), "0x%04X", snes_.cpu().PC); - ImGui::TableNextColumn(); ImGui::Text("SP"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", snes_.memory().mutable_sp()); - + ImGui::TableNextColumn(); + ImGui::Text("PC"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.success), "0x%04X", + snes_.cpu().PC); + ImGui::TableNextColumn(); + ImGui::Text("SP"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", + snes_.memory().mutable_sp()); + ImGui::TableNextRow(); - ImGui::TableNextColumn(); ImGui::Text("PS"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.warning), "0x%02X", snes_.cpu().status); - ImGui::TableNextColumn(); ImGui::Text("Cycle"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.info), "%llu", snes_.mutable_cycles()); - + ImGui::TableNextColumn(); + ImGui::Text("PS"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.warning), "0x%02X", + snes_.cpu().status); + ImGui::TableNextColumn(); + ImGui::Text("Cycle"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.info), "%llu", + snes_.mutable_cycles()); + ImGui::EndTable(); } - + ImGui::EndChild(); ImGui::PopStyleColor(); - } - - // SPC700 Status Panel - if (ImGui::CollapsingHeader("SPC700 Status", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + + // SPC700 Status Panel + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "SPC700 Status"); + ImGui::PushStyleColor(ImGuiCol_ChildBg, + ConvertColorToImVec4(theme.child_bg)); ImGui::BeginChild("##SpcStatus", ImVec2(0, 80), true); - - if (ImGui::BeginTable("SPCRegisters", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { + + if (ImGui::BeginTable( + "SPCRegisters", 4, + ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { ImGui::TableSetupColumn("Register", ImGuiTableColumnFlags_WidthFixed, 50); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 60); ImGui::TableSetupColumn("Register", ImGuiTableColumnFlags_WidthFixed, 50); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 60); ImGui::TableHeadersRow(); - + ImGui::TableNextRow(); - ImGui::TableNextColumn(); ImGui::Text("A"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", snes_.apu().spc700().A); - ImGui::TableNextColumn(); ImGui::Text("PC"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.success), "0x%04X", snes_.apu().spc700().PC); - + ImGui::TableNextColumn(); + ImGui::Text("A"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", + snes_.apu().spc700().A); + ImGui::TableNextColumn(); + ImGui::Text("PC"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.success), "0x%04X", + snes_.apu().spc700().PC); + ImGui::TableNextRow(); - ImGui::TableNextColumn(); ImGui::Text("X"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", snes_.apu().spc700().X); - ImGui::TableNextColumn(); ImGui::Text("SP"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", snes_.apu().spc700().SP); - + ImGui::TableNextColumn(); + ImGui::Text("X"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", + snes_.apu().spc700().X); + ImGui::TableNextColumn(); + ImGui::Text("SP"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", + snes_.apu().spc700().SP); + ImGui::TableNextRow(); - ImGui::TableNextColumn(); ImGui::Text("Y"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", snes_.apu().spc700().Y); - ImGui::TableNextColumn(); ImGui::Text("PSW"); - ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.warning), "0x%02X", snes_.apu().spc700().FlagsToByte(snes_.apu().spc700().PSW)); - + ImGui::TableNextColumn(); + ImGui::Text("Y"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", + snes_.apu().spc700().Y); + ImGui::TableNextColumn(); + ImGui::Text("PSW"); + ImGui::TableNextColumn(); + ImGui::TextColored( + ConvertColorToImVec4(theme.warning), "0x%02X", + snes_.apu().spc700().FlagsToByte(snes_.apu().spc700().PSW)); + ImGui::EndTable(); } - + ImGui::EndChild(); ImGui::PopStyleColor(); - } - - // CPU Instruction Log - RenderCpuInstructionLog(snes_.cpu().instruction_log_); + + // New Disassembly Viewer + if (ImGui::CollapsingHeader("Disassembly Viewer", + ImGuiTreeNodeFlags_DefaultOpen)) { + uint32_t current_pc = (static_cast(snes_.cpu().PB) << 16) | snes_.cpu().PC; + auto& disasm = snes_.cpu().disassembly_viewer(); + if (disasm.IsAvailable()) { + disasm.Render(current_pc, snes_.cpu().breakpoints_); + } else { + ImGui::TextColored(ConvertColorToImVec4(theme.error), "Disassembly viewer unavailable."); + } + } } catch (const std::exception& e) { // Ensure any pushed styles are popped on error try { @@ -786,60 +860,66 @@ void Emulator::RenderPerformanceMonitor() { try { auto& theme_manager = gui::ThemeManager::Get(); const auto& theme = theme_manager.GetCurrentTheme(); - - ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); - ImGui::BeginChild("##PerformanceMonitor", ImVec2(0, 0), true); - - // Performance Metrics - if (ImGui::CollapsingHeader("Real-time Metrics", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Columns(2, "PerfColumns"); - - // Frame Rate - ImGui::Text("Frame Rate:"); - ImGui::SameLine(); - if (current_fps_ > 0) { - ImVec4 fps_color = (current_fps_ >= 59.0 && current_fps_ <= 61.0) ? - ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.error); - ImGui::TextColored(fps_color, "%.1f FPS", current_fps_); - } else { - ImGui::TextColored(ConvertColorToImVec4(theme.warning), "-- FPS"); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, + ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##PerformanceMonitor", ImVec2(0, 0), true); + + // Performance Metrics + if (ImGui::CollapsingHeader("Real-time Metrics", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Columns(2, "PerfColumns"); + + // Frame Rate + ImGui::Text("Frame Rate:"); + ImGui::SameLine(); + if (current_fps_ > 0) { + ImVec4 fps_color = (current_fps_ >= 59.0 && current_fps_ <= 61.0) + ? ConvertColorToImVec4(theme.success) + : ConvertColorToImVec4(theme.error); + ImGui::TextColored(fps_color, "%.1f FPS", current_fps_); + } else { + ImGui::TextColored(ConvertColorToImVec4(theme.warning), "-- FPS"); + } + + // Audio Status + uint32_t audio_queued = SDL_GetQueuedAudioSize(audio_device_); + uint32_t audio_frames = audio_queued / (wanted_samples_ * 4); + ImGui::Text("Audio Queue:"); + ImGui::SameLine(); + ImVec4 audio_color = (audio_frames >= 2 && audio_frames <= 6) + ? ConvertColorToImVec4(theme.success) + : ConvertColorToImVec4(theme.warning); + ImGui::TextColored(audio_color, "%u frames", audio_frames); + + ImGui::NextColumn(); + + // Timing + double frame_time = (current_fps_ > 0) ? (1000.0 / current_fps_) : 0.0; + ImGui::Text("Frame Time:"); + ImGui::SameLine(); + ImGui::TextColored(ConvertColorToImVec4(theme.info), "%.2f ms", + frame_time); + + // Emulation State + ImGui::Text("State:"); + ImGui::SameLine(); + ImVec4 state_color = running_ ? ConvertColorToImVec4(theme.success) + : ConvertColorToImVec4(theme.warning); + ImGui::TextColored(state_color, "%s", running_ ? "Running" : "Paused"); + + ImGui::Columns(1); } - - // Audio Status - uint32_t audio_queued = SDL_GetQueuedAudioSize(audio_device_); - uint32_t audio_frames = audio_queued / (wanted_samples_ * 4); - ImGui::Text("Audio Queue:"); - ImGui::SameLine(); - ImVec4 audio_color = (audio_frames >= 2 && audio_frames <= 6) ? - ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.warning); - ImGui::TextColored(audio_color, "%u frames", audio_frames); - - ImGui::NextColumn(); - - // Timing - double frame_time = (current_fps_ > 0) ? (1000.0 / current_fps_) : 0.0; - ImGui::Text("Frame Time:"); - ImGui::SameLine(); - ImGui::TextColored(ConvertColorToImVec4(theme.info), "%.2f ms", frame_time); - - // Emulation State - ImGui::Text("State:"); - ImGui::SameLine(); - ImVec4 state_color = running_ ? ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.warning); - ImGui::TextColored(state_color, "%s", running_ ? "Running" : "Paused"); - - ImGui::Columns(1); - } - - // Memory Usage - if (ImGui::CollapsingHeader("Memory Usage")) { - ImGui::Text("ROM Size: %zu bytes", rom_data_.size()); - ImGui::Text("RAM Usage: %d KB", 128); // SNES RAM is 128KB - ImGui::Text("VRAM Usage: %d KB", 64); // SNES VRAM is 64KB - } - - ImGui::EndChild(); - ImGui::PopStyleColor(); + + // Memory Usage + if (ImGui::CollapsingHeader("Memory Usage")) { + ImGui::Text("ROM Size: %zu bytes", rom_data_.size()); + ImGui::Text("RAM Usage: %d KB", 128); // SNES RAM is 128KB + ImGui::Text("VRAM Usage: %d KB", 64); // SNES VRAM is 64KB + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); } catch (const std::exception& e) { // Ensure any pushed styles are popped on error try { @@ -855,117 +935,129 @@ void Emulator::RenderAIAgentPanel() { try { auto& theme_manager = gui::ThemeManager::Get(); const auto& theme = theme_manager.GetCurrentTheme(); - - ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); - ImGui::BeginChild("##AIAgentPanel", ImVec2(0, 0), true); - - // AI Agent Status - if (ImGui::CollapsingHeader("Agent Status", ImGuiTreeNodeFlags_DefaultOpen)) { - auto metrics = GetMetrics(); - - ImGui::Columns(2, "AgentColumns"); - - // Emulator Readiness - ImGui::Text("Emulator Ready:"); - ImGui::SameLine(); - ImVec4 ready_color = IsEmulatorReady() ? ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.error); - ImGui::TextColored(ready_color, "%s", IsEmulatorReady() ? "Yes" : "No"); - - // Current State - ImGui::Text("Current PC:"); - ImGui::SameLine(); - ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X:%02X", metrics.cpu_pc, metrics.cpu_pb); - - ImGui::NextColumn(); - - // Performance - ImGui::Text("FPS:"); - ImGui::SameLine(); - ImVec4 fps_color = (metrics.fps >= 59.0) ? ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.warning); - ImGui::TextColored(fps_color, "%.1f", metrics.fps); - - // Cycles - ImGui::Text("Cycles:"); - ImGui::SameLine(); - ImGui::TextColored(ConvertColorToImVec4(theme.info), "%llu", metrics.cycles); - - ImGui::Columns(1); - } - - // AI Agent Controls - if (ImGui::CollapsingHeader("Agent Controls", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Columns(2, "ControlColumns"); - - // Single Step Control - if (ImGui::Button("Step Instruction", ImVec2(-1, 30))) { - StepSingleInstruction(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, + ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##AIAgentPanel", ImVec2(0, 0), true); + + // AI Agent Status + if (ImGui::CollapsingHeader("Agent Status", + ImGuiTreeNodeFlags_DefaultOpen)) { + auto metrics = GetMetrics(); + + ImGui::Columns(2, "AgentColumns"); + + // Emulator Readiness + ImGui::Text("Emulator Ready:"); + ImGui::SameLine(); + ImVec4 ready_color = IsEmulatorReady() + ? ConvertColorToImVec4(theme.success) + : ConvertColorToImVec4(theme.error); + ImGui::TextColored(ready_color, "%s", IsEmulatorReady() ? "Yes" : "No"); + + // Current State + ImGui::Text("Current PC:"); + ImGui::SameLine(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X:%02X", + metrics.cpu_pc, metrics.cpu_pb); + + ImGui::NextColumn(); + + // Performance + ImGui::Text("FPS:"); + ImGui::SameLine(); + ImVec4 fps_color = (metrics.fps >= 59.0) + ? ConvertColorToImVec4(theme.success) + : ConvertColorToImVec4(theme.warning); + ImGui::TextColored(fps_color, "%.1f", metrics.fps); + + // Cycles + ImGui::Text("Cycles:"); + ImGui::SameLine(); + ImGui::TextColored(ConvertColorToImVec4(theme.info), "%llu", + metrics.cycles); + + ImGui::Columns(1); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Execute a single CPU instruction"); - } - - // Breakpoint Controls - static char bp_input[10] = ""; - ImGui::InputText("Breakpoint Address", bp_input, IM_ARRAYSIZE(bp_input)); - if (ImGui::Button("Add Breakpoint", ImVec2(-1, 25))) { - if (strlen(bp_input) > 0) { - uint32_t addr = std::stoi(bp_input, nullptr, 16); - SetBreakpoint(addr); - memset(bp_input, 0, sizeof(bp_input)); + + // AI Agent Controls + if (ImGui::CollapsingHeader("Agent Controls", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Columns(2, "ControlColumns"); + + // Single Step Control + if (ImGui::Button("Step Instruction", ImVec2(-1, 30))) { + StepSingleInstruction(); } - } - - ImGui::NextColumn(); - - // Clear All Breakpoints - if (ImGui::Button("Clear All Breakpoints", ImVec2(-1, 30))) { - ClearAllBreakpoints(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Remove all active breakpoints"); - } - - // Toggle Emulation - if (ImGui::Button(running_ ? "Pause Emulation" : "Resume Emulation", ImVec2(-1, 30))) { - running_ = !running_; - } - - ImGui::Columns(1); - } - - // Current Breakpoints - if (ImGui::CollapsingHeader("Active Breakpoints")) { - auto breakpoints = GetBreakpoints(); - if (breakpoints.empty()) { - ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), "No breakpoints set"); - } else { - ImGui::BeginChild("BreakpointsList", ImVec2(0, 150), true); - for (auto bp : breakpoints) { - if (ImGui::Selectable(absl::StrFormat("0x%04X", bp).c_str())) { - // Jump to breakpoint or remove it - } - ImGui::SameLine(); - if (ImGui::SmallButton(absl::StrFormat("Remove##%04X", bp).c_str())) { - // TODO: Implement individual breakpoint removal + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Execute a single CPU instruction"); + } + + // Breakpoint Controls + static char bp_input[10] = ""; + ImGui::InputText("Breakpoint Address", bp_input, IM_ARRAYSIZE(bp_input)); + if (ImGui::Button("Add Breakpoint", ImVec2(-1, 25))) { + if (strlen(bp_input) > 0) { + uint32_t addr = std::stoi(bp_input, nullptr, 16); + SetBreakpoint(addr); + memset(bp_input, 0, sizeof(bp_input)); } } - ImGui::EndChild(); + + ImGui::NextColumn(); + + // Clear All Breakpoints + if (ImGui::Button("Clear All Breakpoints", ImVec2(-1, 30))) { + ClearAllBreakpoints(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Remove all active breakpoints"); + } + + // Toggle Emulation + if (ImGui::Button(running_ ? "Pause Emulation" : "Resume Emulation", + ImVec2(-1, 30))) { + running_ = !running_; + } + + ImGui::Columns(1); } - } - - // AI Agent API Information - if (ImGui::CollapsingHeader("API Reference")) { - ImGui::TextWrapped("Available API functions for AI agents:"); - ImGui::BulletText("IsEmulatorReady() - Check if emulator is ready"); - ImGui::BulletText("GetMetrics() - Get current performance metrics"); - ImGui::BulletText("StepSingleInstruction() - Execute one CPU instruction"); - ImGui::BulletText("SetBreakpoint(address) - Set breakpoint at address"); - ImGui::BulletText("ClearAllBreakpoints() - Remove all breakpoints"); - ImGui::BulletText("GetBreakpoints() - Get list of active breakpoints"); - } - - ImGui::EndChild(); - ImGui::PopStyleColor(); + + // Current Breakpoints + if (ImGui::CollapsingHeader("Active Breakpoints")) { + auto breakpoints = GetBreakpoints(); + if (breakpoints.empty()) { + ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), + "No breakpoints set"); + } else { + ImGui::BeginChild("BreakpointsList", ImVec2(0, 150), true); + for (auto bp : breakpoints) { + if (ImGui::Selectable(absl::StrFormat("0x%04X", bp).c_str())) { + // Jump to breakpoint or remove it + } + ImGui::SameLine(); + if (ImGui::SmallButton(absl::StrFormat("Remove##%04X", bp).c_str())) { + // TODO: Implement individual breakpoint removal + } + } + ImGui::EndChild(); + } + } + + // AI Agent API Information + if (ImGui::CollapsingHeader("API Reference")) { + ImGui::TextWrapped("Available API functions for AI agents:"); + ImGui::BulletText("IsEmulatorReady() - Check if emulator is ready"); + ImGui::BulletText("GetMetrics() - Get current performance metrics"); + ImGui::BulletText( + "StepSingleInstruction() - Execute one CPU instruction"); + ImGui::BulletText("SetBreakpoint(address) - Set breakpoint at address"); + ImGui::BulletText("ClearAllBreakpoints() - Remove all breakpoints"); + ImGui::BulletText("GetBreakpoints() - Get list of active breakpoints"); + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); } catch (const std::exception& e) { // Ensure any pushed styles are popped on error try { @@ -979,95 +1071,100 @@ void Emulator::RenderAIAgentPanel() { void Emulator::RenderCpuInstructionLog( const std::vector& instruction_log) { - // Filtering options - static char filter[256]; - ImGui::InputText("Filter", filter, IM_ARRAYSIZE(filter)); + // Filtering options + static char filter[256]; + ImGui::InputText("Filter", filter, IM_ARRAYSIZE(filter)); - // Instruction list - ImGui::BeginChild("InstructionList", ImVec2(0, 0), ImGuiChildFlags_None); - for (const auto& entry : instruction_log) { - if (ShouldDisplay(entry, filter)) { - if (ImGui::Selectable( - absl::StrFormat("%06X:", entry.address).c_str())) { - // Logic to handle click (e.g., jump to address, set breakpoint) - } - - ImGui::SameLine(); - - ImVec4 color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - ImGui::TextColored(color, "%s", - opcode_to_mnemonic.at(entry.opcode).c_str()); - ImVec4 operand_color = ImVec4(0.7f, 0.5f, 0.3f, 1.0f); - ImGui::SameLine(); - ImGui::TextColored(operand_color, "%s", entry.operands.c_str()); + // Instruction list + ImGui::BeginChild("InstructionList", ImVec2(0, 0), ImGuiChildFlags_None); + for (const auto& entry : instruction_log) { + if (ShouldDisplay(entry, filter)) { + if (ImGui::Selectable(absl::StrFormat("%06X:", entry.address).c_str())) { + // Logic to handle click (e.g., jump to address, set breakpoint) } - } - // Jump to the bottom of the child scrollbar - if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { - ImGui::SetScrollHereY(1.0f); - } - ImGui::EndChild(); + ImGui::SameLine(); + + ImVec4 color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + ImGui::TextColored(color, "%s", + opcode_to_mnemonic.at(entry.opcode).c_str()); + ImVec4 operand_color = ImVec4(0.7f, 0.5f, 0.3f, 1.0f); + ImGui::SameLine(); + ImGui::TextColored(operand_color, "%s", entry.operands.c_str()); + } + } + // Jump to the bottom of the child scrollbar + if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { + ImGui::SetScrollHereY(1.0f); + } + + ImGui::EndChild(); } void Emulator::RenderSaveStates() { try { auto& theme_manager = gui::ThemeManager::Get(); const auto& theme = theme_manager.GetCurrentTheme(); - - ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, + ConvertColorToImVec4(theme.child_bg)); ImGui::BeginChild("##SaveStates", ImVec2(0, 0), true); - + // Save State Management - if (ImGui::CollapsingHeader("Quick Save/Load", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::CollapsingHeader("Quick Save/Load", + ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Columns(2, "SaveStateColumns"); - + // Save slots for (int i = 1; i <= 4; ++i) { - if (ImGui::Button(absl::StrFormat("Save Slot %d", i).c_str(), ImVec2(-1, 30))) { + if (ImGui::Button(absl::StrFormat("Save Slot %d", i).c_str(), + ImVec2(-1, 30))) { // TODO: Implement save state to slot } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Save current state to slot %d (F%d)", i, i); } } - + ImGui::NextColumn(); - + // Load slots for (int i = 1; i <= 4; ++i) { - if (ImGui::Button(absl::StrFormat("Load Slot %d", i).c_str(), ImVec2(-1, 30))) { + if (ImGui::Button(absl::StrFormat("Load Slot %d", i).c_str(), + ImVec2(-1, 30))) { // TODO: Implement load state from slot } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Load state from slot %d (Shift+F%d)", i, i); } } - + ImGui::Columns(1); } - + // File-based save states if (ImGui::CollapsingHeader("File-based Saves")) { static char save_name[256] = ""; ImGui::InputText("Save Name", save_name, IM_ARRAYSIZE(save_name)); - + if (ImGui::Button("Save to File", ImVec2(-1, 30))) { // TODO: Implement save to file } - + if (ImGui::Button("Load from File", ImVec2(-1, 30))) { // TODO: Implement load from file } } - + // Rewind functionality if (ImGui::CollapsingHeader("Rewind")) { - ImGui::TextWrapped("Rewind functionality allows you to step back through recent gameplay."); - + ImGui::TextWrapped( + "Rewind functionality allows you to step back through recent " + "gameplay."); + static bool rewind_enabled = false; ImGui::Checkbox("Enable Rewind (uses more memory)", &rewind_enabled); - + if (rewind_enabled) { if (ImGui::Button("Rewind 1 Second", ImVec2(-1, 30))) { // TODO: Implement rewind @@ -1076,11 +1173,11 @@ void Emulator::RenderSaveStates() { ImGui::SetTooltip("Rewind gameplay by 1 second (Backquote key)"); } } else { - ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), - "Enable rewind to use this feature"); + ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), + "Enable rewind to use this feature"); } } - + ImGui::EndChild(); ImGui::PopStyleColor(); } catch (const std::exception& e) { @@ -1095,39 +1192,44 @@ void Emulator::RenderKeyboardConfig() { try { auto& theme_manager = gui::ThemeManager::Get(); const auto& theme = theme_manager.GetCurrentTheme(); - - ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, + ConvertColorToImVec4(theme.child_bg)); ImGui::BeginChild("##KeyboardConfig", ImVec2(0, 0), true); - + // Keyboard Configuration - if (ImGui::CollapsingHeader("SNES Controller Mapping", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::CollapsingHeader("SNES Controller Mapping", + ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::TextWrapped("Click on a button and press a key to remap it."); ImGui::Separator(); - - if (ImGui::BeginTable("KeyboardTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableSetupColumn("Button", ImGuiTableColumnFlags_WidthFixed, 120); + + if (ImGui::BeginTable("KeyboardTable", 2, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Button", ImGuiTableColumnFlags_WidthFixed, + 120); ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthStretch); ImGui::TableHeadersRow(); - + auto DrawKeyBinding = [&](const char* label, ImGuiKey& key) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "%s", label); ImGui::TableNextColumn(); - - std::string button_label = absl::StrFormat("%s##%s", ImGui::GetKeyName(key), label); + + std::string button_label = + absl::StrFormat("%s##%s", ImGui::GetKeyName(key), label); if (ImGui::Button(button_label.c_str(), ImVec2(-1, 0))) { // TODO: Implement key remapping ImGui::OpenPopup(absl::StrFormat("Remap%s", label).c_str()); } - + if (ImGui::BeginPopup(absl::StrFormat("Remap%s", label).c_str())) { ImGui::Text("Press any key..."); // TODO: Detect key press and update binding ImGui::EndPopup(); } }; - + DrawKeyBinding("A Button", keybindings_.a_button); DrawKeyBinding("B Button", keybindings_.b_button); DrawKeyBinding("X Button", keybindings_.x_button); @@ -1140,16 +1242,16 @@ void Emulator::RenderKeyboardConfig() { DrawKeyBinding("Down", keybindings_.down_button); DrawKeyBinding("Left", keybindings_.left_button); DrawKeyBinding("Right", keybindings_.right_button); - + ImGui::EndTable(); } } - + // Emulator Hotkeys if (ImGui::CollapsingHeader("Emulator Hotkeys")) { ImGui::TextWrapped("System-level keyboard shortcuts:"); ImGui::Separator(); - + ImGui::BulletText("F1-F4: Quick save to slot 1-4"); ImGui::BulletText("Shift+F1-F4: Quick load from slot 1-4"); ImGui::BulletText("Backquote (`): Rewind gameplay"); @@ -1157,12 +1259,12 @@ void Emulator::RenderKeyboardConfig() { ImGui::BulletText("Pause/Break: Pause/Resume emulation"); ImGui::BulletText("F12: Take screenshot"); } - + // Reset to defaults if (ImGui::Button("Reset to Defaults", ImVec2(-1, 35))) { keybindings_ = EmulatorKeybindings(); } - + ImGui::EndChild(); ImGui::PopStyleColor(); } catch (const std::exception& e) { diff --git a/src/app/emu/video/ppu.cc b/src/app/emu/video/ppu.cc index 87433024..cb246981 100644 --- a/src/app/emu/video/ppu.cc +++ b/src/app/emu/video/ppu.cc @@ -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) { diff --git a/src/app/emu/video/ppu.h b/src/app/emu/video/ppu.h index fda6afc1..ee76ab76 100644 --- a/src/app/emu/video/ppu.h +++ b/src/app/emu/video/ppu.h @@ -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(); diff --git a/src/app/rom.cc b/src/app/rom.cc index 9ffb791f..f2a3fd5f 100644 --- a/src/app/rom.cc +++ b/src/app/rom.cc @@ -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(rom_data_.data()), size_); + + // Allocate and read ROM data + try { + rom_data_.resize(size_); + file.seekg(0, std::ios::beg); + file.read(reinterpret_cast(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) {