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:
scawful
2025-10-06 21:49:12 -04:00
parent 14eba4a765
commit 7f71fd9e80
9 changed files with 1262 additions and 483 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View 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

View 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_

View File

@@ -4,16 +4,17 @@
#include <fstream>
#include <vector>
#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 {
@@ -96,11 +97,23 @@ void Emulator::Run(Rom* rom) {
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++;
}
// 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);
if (loaded) {
// Run frame
if (turbo_mode_) {
snes_.RunFrame();
@@ -116,35 +129,32 @@ void Emulator::Run(Rom* rom) {
fps_timer_ = 0.0;
}
// Generate and queue audio samples with better buffering
// 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 max_buffer_size = wanted_samples_ * 4 * 4; // Reduced from 6 to 4 frames
uint32_t target_buffer = wanted_samples_ * 4 * 2; // Target 2 frames buffered
uint32_t max_buffer = wanted_samples_ * 4 * 6; // Max 6 frames
// Only queue audio if buffer is not too full to prevent audio lag
if (queued <= max_buffer_size) {
if (queued < target_buffer) {
// Buffer is low, queue more audio
SDL_QueueAudio(audio_device_, audio_buffer_, wanted_samples_ * 4);
} else {
// Clear excess audio to prevent audio lag buildup
} 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 with better error handling
// Update PPU texture only on rendered frames
void* ppu_pixels_;
int ppu_pitch_;
if (SDL_LockTexture(ppu_texture_, NULL, &ppu_pixels_, &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;
}
snes_.SetPixels(static_cast<uint8_t*>(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;
}
}
}
}
@@ -170,13 +180,18 @@ void Emulator::RenderEmulatorInterface() {
// 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 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);
gui::EditorCard keyboard_card(ICON_MD_KEYBOARD " Keyboard Config",
ICON_MD_KEYBOARD);
// Configure default positions
static bool cards_configured = false;
@@ -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");
}
@@ -546,8 +560,7 @@ void Emulator::RenderBreakpointList() {
static bool write_mode = false;
static bool execute_mode = false;
if (ImGui::Combo("##TypeOfMemory", &current_memory_mode, "PRG\0RAM\0")) {
}
if (ImGui::Combo("##TypeOfMemory", &current_memory_mode, "PRG\0RAM\0")) {}
ImGui::Checkbox("Read", &read_mode);
SameLine();
@@ -682,13 +695,15 @@ void Emulator::RenderModernCpuDebugger() {
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);
@@ -696,48 +711,80 @@ void Emulator::RenderModernCpuDebugger() {
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));
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);
@@ -745,32 +792,59 @@ void Emulator::RenderModernCpuDebugger() {
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<uint32_t>(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 {
@@ -787,19 +861,22 @@ void Emulator::RenderPerformanceMonitor() {
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("##PerformanceMonitor", ImVec2(0, 0), true);
// Performance Metrics
if (ImGui::CollapsingHeader("Real-time Metrics", ImGuiTreeNodeFlags_DefaultOpen)) {
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);
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");
@@ -810,8 +887,9 @@ void Emulator::RenderPerformanceMonitor() {
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);
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();
@@ -820,12 +898,14 @@ void Emulator::RenderPerformanceMonitor() {
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);
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);
ImVec4 state_color = running_ ? ConvertColorToImVec4(theme.success)
: ConvertColorToImVec4(theme.warning);
ImGui::TextColored(state_color, "%s", running_ ? "Running" : "Paused");
ImGui::Columns(1);
@@ -856,11 +936,13 @@ void Emulator::RenderAIAgentPanel() {
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("##AIAgentPanel", ImVec2(0, 0), true);
// AI Agent Status
if (ImGui::CollapsingHeader("Agent Status", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::CollapsingHeader("Agent Status",
ImGuiTreeNodeFlags_DefaultOpen)) {
auto metrics = GetMetrics();
ImGui::Columns(2, "AgentColumns");
@@ -868,32 +950,39 @@ void Emulator::RenderAIAgentPanel() {
// Emulator Readiness
ImGui::Text("Emulator Ready:");
ImGui::SameLine();
ImVec4 ready_color = IsEmulatorReady() ? ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.error);
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::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);
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::TextColored(ConvertColorToImVec4(theme.info), "%llu",
metrics.cycles);
ImGui::Columns(1);
}
// AI Agent Controls
if (ImGui::CollapsingHeader("Agent Controls", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::CollapsingHeader("Agent Controls",
ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Columns(2, "ControlColumns");
// Single Step Control
@@ -926,7 +1015,8 @@ void Emulator::RenderAIAgentPanel() {
}
// Toggle Emulation
if (ImGui::Button(running_ ? "Pause Emulation" : "Resume Emulation", ImVec2(-1, 30))) {
if (ImGui::Button(running_ ? "Pause Emulation" : "Resume Emulation",
ImVec2(-1, 30))) {
running_ = !running_;
}
@@ -937,7 +1027,8 @@ void Emulator::RenderAIAgentPanel() {
if (ImGui::CollapsingHeader("Active Breakpoints")) {
auto breakpoints = GetBreakpoints();
if (breakpoints.empty()) {
ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), "No breakpoints set");
ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
"No breakpoints set");
} else {
ImGui::BeginChild("BreakpointsList", ImVec2(0, 150), true);
for (auto bp : breakpoints) {
@@ -958,7 +1049,8 @@ void Emulator::RenderAIAgentPanel() {
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(
"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");
@@ -987,8 +1079,7 @@ void Emulator::RenderCpuInstructionLog(
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())) {
if (ImGui::Selectable(absl::StrFormat("%06X:", entry.address).c_str())) {
// Logic to handle click (e.g., jump to address, set breakpoint)
}
@@ -1015,16 +1106,19 @@ void Emulator::RenderSaveStates() {
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()) {
@@ -1036,7 +1130,8 @@ void Emulator::RenderSaveStates() {
// 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()) {
@@ -1063,7 +1158,9 @@ void Emulator::RenderSaveStates() {
// 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);
@@ -1096,16 +1193,20 @@ void Emulator::RenderKeyboardConfig() {
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();
@@ -1115,7 +1216,8 @@ void Emulator::RenderKeyboardConfig() {
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());

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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();
}
rom_data_.resize(size_);
// Read file into rom_data_
// Validate size from stream
if (size_ < 32768 || size_ > 8 * 1024 * 1024) {
return absl::InvalidArgumentError(
absl::StrFormat("Invalid ROM size: %zu bytes", 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) {