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