From 14eba4a765dcf9ef5522beda809a99f95b8f81d0 Mon Sep 17 00:00:00 2001 From: scawful Date: Mon, 6 Oct 2025 21:18:56 -0400 Subject: [PATCH] feat: Add Emulator Editor Type and UI Enhancements - Introduced a new editor type for the emulator, allowing users to access emulator functionalities directly from the editor manager. - Updated the editor selection dialog to include the emulator option with appropriate descriptions and shortcuts. - Enhanced the emulator interface with modern theming and improved layout for better user experience. - Implemented various UI components for performance monitoring, AI agent integration, and save state management, enriching the emulator's capabilities. --- src/app/editor/editor.h | 7 +- src/app/editor/editor_manager.cc | 3 + src/app/editor/ui/editor_selection_dialog.cc | 8 +- src/app/emu/emulator.cc | 696 ++++++++++++++++--- src/app/emu/emulator.h | 36 + src/app/emu/video/ppu.cc | 2 +- 6 files changed, 656 insertions(+), 96 deletions(-) diff --git a/src/app/editor/editor.h b/src/app/editor/editor.h index 9ccaf593..c220df60 100644 --- a/src/app/editor/editor.h +++ b/src/app/editor/editor.h @@ -53,6 +53,7 @@ struct EditorContext { enum class EditorType { kAssembly, kDungeon, + kEmulator, kGraphics, kMusic, kOverworld, @@ -65,9 +66,9 @@ enum class EditorType { kSettings, }; -constexpr std::array kEditorNames = { - "Assembly", "Dungeon", "Graphics", "Music", "Overworld", - "Palette", "Screen", "Sprite", "Message", "Hex", "Agent", "Settings", +constexpr std::array kEditorNames = { + "Assembly", "Dungeon", "Emulator", "Graphics", "Music", "Overworld", + "Palette", "Screen", "Sprite", "Message", "Hex", "Agent", "Settings", }; /** diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index a4308941..8c0d716e 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -468,6 +468,9 @@ void EditorManager::Initialize(const std::string& filename) { case EditorType::kAssembly: show_asm_editor_ = true; break; + case EditorType::kEmulator: + show_emulator_ = true; + break; case EditorType::kSettings: current_editor_set_->settings_editor_.set_active(true); break; diff --git a/src/app/editor/ui/editor_selection_dialog.cc b/src/app/editor/ui/editor_selection_dialog.cc index 60bc09ec..1c014aa8 100644 --- a/src/app/editor/ui/editor_selection_dialog.cc +++ b/src/app/editor/ui/editor_selection_dialog.cc @@ -56,6 +56,10 @@ EditorSelectionDialog::EditorSelectionDialog() { "Direct ROM memory editing and comparison", "Ctrl+0", false, true, ImVec4(0.2f, 0.8f, 0.4f, 1.0f)}, // Matrix green + {EditorType::kEmulator, "Emulator", ICON_MD_VIDEOGAME_ASSET, + "Test and debug your ROM in real-time with live debugging", "Ctrl+Shift+E", false, true, + ImVec4(0.2f, 0.6f, 1.0f, 1.0f)}, // Emulator blue + {EditorType::kAgent, "AI Agent", ICON_MD_SMART_TOY, "Configure AI agent, collaboration, and automation", "Ctrl+Shift+A", false, false, ImVec4(0.8f, 0.4f, 1.0f, 1.0f)}, // Purple/magenta @@ -279,9 +283,7 @@ void EditorSelectionDialog::DrawEditorCard(const EditorInfo& info, int index) { // Draw shortcut hint if available if (info.shortcut && info.shortcut[0]) { ImGui::SetCursorScreenPos(ImVec2(cursor_pos.x + 10, cursor_pos.y + button_size.y - 20)); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); - ImGui::Text("%s", info.shortcut); - ImGui::PopStyleColor(); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "%s", info.shortcut); } // Hover glow effect diff --git a/src/app/emu/emulator.cc b/src/app/emu/emulator.cc index 46699bbb..5c3b8777 100644 --- a/src/app/emu/emulator.cc +++ b/src/app/emu/emulator.cc @@ -9,6 +9,9 @@ #include "app/emu/cpu/internal/opcodes.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" @@ -40,7 +43,6 @@ bool ShouldDisplay(const InstructionEntry& entry, const char* filter) { } // namespace -using ImGui::NextColumn; using ImGui::SameLine; using ImGui::Separator; using ImGui::TableNextColumn; @@ -50,6 +52,7 @@ void Emulator::Run(Rom* rom) { static bool loaded = false; if (!snes_.running() && rom->is_loaded()) { // Use ARGB8888 format to match PPU output (XBGR layout with format=1) + // Add better error handling and texture optimization ppu_texture_ = SDL_CreateTexture(core::Renderer::Get().renderer(), SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 512, 480); @@ -57,6 +60,9 @@ void Emulator::Run(Rom* rom) { printf("Failed to create texture: %s\n", SDL_GetError()); return; } + + // Optimize texture for better performance + SDL_SetTextureBlendMode(ppu_texture_, SDL_BLENDMODE_NONE); rom_data_ = rom->vector(); snes_.Init(rom_data_); @@ -85,9 +91,9 @@ void Emulator::Run(Rom* rom) { double seconds = delta / (double)count_frequency; time_adder += seconds; - // Cap time accumulation to prevent spiral of death - if (time_adder > wanted_frames_ * 5.0) { - time_adder = wanted_frames_ * 5.0; + // Cap time accumulation to prevent spiral of death and improve stability + if (time_adder > wanted_frames_ * 3.0) { + time_adder = wanted_frames_ * 3.0; } // allow 2 ms earlier, to prevent skipping due to being just below wanted @@ -110,21 +116,27 @@ void Emulator::Run(Rom* rom) { fps_timer_ = 0.0; } - // Generate and queue audio samples + // Generate and queue audio samples with better buffering snes_.SetSamples(audio_buffer_, wanted_samples_); uint32_t queued = SDL_GetQueuedAudioSize(audio_device_); - // Keep audio buffer filled but not overflowing (max 6 frames worth) - if (queued <= wanted_samples_ * 4 * 6) { + uint32_t max_buffer_size = wanted_samples_ * 4 * 4; // Reduced from 6 to 4 frames + + // Only queue audio if buffer is not too full to prevent audio lag + if (queued <= max_buffer_size) { SDL_QueueAudio(audio_device_, audio_buffer_, wanted_samples_ * 4); + } else { + // Clear excess audio to prevent audio lag buildup + SDL_ClearQueuedAudio(audio_device_); } - // Update PPU texture + // Update PPU texture with better error handling void* ppu_pixels_; int ppu_pitch_; if (SDL_LockTexture(ppu_texture_, NULL, &ppu_pixels_, &ppu_pitch_) != 0) { printf("Failed to lock texture: %s\n", SDL_GetError()); - return; + // Don't return here, continue with next frame to prevent deadlock + continue; } snes_.SetPixels(static_cast(ppu_pixels_)); SDL_UnlockTexture(ppu_texture_); @@ -141,71 +153,133 @@ void Emulator::Run(Rom* rom) { } void Emulator::RenderEmulatorInterface() { - if (ImGui::BeginTable("Emulator", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) { - ImGui::TableSetupColumn("CPU"); - ImGui::TableSetupColumn("PPU"); - ImGui::TableHeadersRow(); - - // CPU Column - ImGui::TableNextColumn(); + // Apply modern theming with safety checks + try { + auto& theme_manager = gui::ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + // Modern EditorCard-based layout - modular and flexible + static bool show_cpu_debugger_ = true; + static bool show_ppu_display_ = true; + static bool show_memory_viewer_ = false; + static bool show_breakpoints_ = false; + static bool show_performance_ = true; + static bool show_ai_agent_ = false; + static bool show_save_states_ = false; + static bool show_keyboard_config_ = false; + + // Create session-aware cards + gui::EditorCard cpu_card(ICON_MD_MEMORY " CPU Debugger", ICON_MD_MEMORY); + gui::EditorCard ppu_card(ICON_MD_VIDEOGAME_ASSET " PPU Display", ICON_MD_VIDEOGAME_ASSET); + gui::EditorCard memory_card(ICON_MD_DATA_ARRAY " Memory Viewer", ICON_MD_DATA_ARRAY); + gui::EditorCard breakpoints_card(ICON_MD_BUG_REPORT " Breakpoints", ICON_MD_BUG_REPORT); + gui::EditorCard performance_card(ICON_MD_SPEED " Performance", ICON_MD_SPEED); + gui::EditorCard ai_card(ICON_MD_SMART_TOY " AI Agent", ICON_MD_SMART_TOY); + gui::EditorCard save_states_card(ICON_MD_SAVE " Save States", ICON_MD_SAVE); + gui::EditorCard keyboard_card(ICON_MD_KEYBOARD " Keyboard Config", ICON_MD_KEYBOARD); + + // Configure default positions + static bool cards_configured = false; + if (!cards_configured) { + cpu_card.SetDefaultSize(400, 500); + cpu_card.SetPosition(gui::EditorCard::Position::Right); - // CPU Register Values - if (ImGui::CollapsingHeader("Register Values", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::BeginChild("##CpuState", ImVec2(0, 100), ImGuiChildFlags_None, - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar); - ImGui::Columns(2, "registersColumns"); - - ImGui::Text("A: 0x%04X", snes_.cpu().A); - ImGui::Text("X: 0x%04X", snes_.cpu().X); - ImGui::Text("Y: 0x%04X", snes_.cpu().Y); - ImGui::Text("PC: 0x%04X", snes_.cpu().PC); - ImGui::Text("SP: 0x%02X", snes_.memory().mutable_sp()); - - ImGui::NextColumn(); - - ImGui::Text("D: 0x%04X", snes_.cpu().D); - ImGui::Text("DB: 0x%02X", snes_.cpu().DB); - ImGui::Text("PB: 0x%02X", snes_.cpu().PB); - ImGui::Text("PS: 0x%02X", snes_.cpu().status); - ImGui::Text("Cycle: %llu", snes_.mutable_cycles()); - - ImGui::Columns(1); - ImGui::EndChild(); + ppu_card.SetDefaultSize(550, 520); + ppu_card.SetPosition(gui::EditorCard::Position::Floating); + + memory_card.SetDefaultSize(800, 600); + memory_card.SetPosition(gui::EditorCard::Position::Floating); + + breakpoints_card.SetDefaultSize(400, 350); + breakpoints_card.SetPosition(gui::EditorCard::Position::Right); + + performance_card.SetDefaultSize(350, 300); + performance_card.SetPosition(gui::EditorCard::Position::Bottom); + + ai_card.SetDefaultSize(500, 450); + ai_card.SetPosition(gui::EditorCard::Position::Floating); + + save_states_card.SetDefaultSize(400, 300); + save_states_card.SetPosition(gui::EditorCard::Position::Floating); + + keyboard_card.SetDefaultSize(450, 400); + keyboard_card.SetPosition(gui::EditorCard::Position::Floating); + + cards_configured = true; + } + + // CPU Debugger Card + if (show_cpu_debugger_) { + if (cpu_card.Begin(&show_cpu_debugger_)) { + RenderModernCpuDebugger(); } - - // SPC Registers - if (ImGui::CollapsingHeader("SPC Registers", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::BeginChild("##SpcState", ImVec2(0, 100), ImGuiChildFlags_None, - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar); - ImGui::Columns(2, "spcRegistersColumns"); - - ImGui::Text("A: 0x%02X", snes_.apu().spc700().A); - ImGui::Text("X: 0x%02X", snes_.apu().spc700().X); - ImGui::Text("Y: 0x%02X", snes_.apu().spc700().Y); - - ImGui::NextColumn(); - - ImGui::Text("PC: 0x%04X", snes_.apu().spc700().PC); - ImGui::Text("SP: 0x%02X", snes_.apu().spc700().SP); - ImGui::Text("PSW: 0x%02X", snes_.apu().spc700().FlagsToByte(snes_.apu().spc700().PSW)); - - ImGui::Columns(1); - ImGui::EndChild(); + cpu_card.End(); + } + + // PPU Display Card + if (show_ppu_display_) { + if (ppu_card.Begin(&show_ppu_display_)) { + RenderSnesPpu(); + } + ppu_card.End(); + } + + // Memory Viewer Card + if (show_memory_viewer_) { + if (memory_card.Begin(&show_memory_viewer_)) { + RenderMemoryViewer(); + } + memory_card.End(); + } + + // Breakpoints Card + if (show_breakpoints_) { + if (breakpoints_card.Begin(&show_breakpoints_)) { + RenderBreakpointList(); + } + breakpoints_card.End(); + } + + // Performance Monitor Card + if (show_performance_) { + if (performance_card.Begin(&show_performance_)) { + RenderPerformanceMonitor(); + } + performance_card.End(); + } + + // AI Agent Card + if (show_ai_agent_) { + if (ai_card.Begin(&show_ai_agent_)) { + RenderAIAgentPanel(); + } + ai_card.End(); + } + + // Save States Card + if (show_save_states_) { + if (save_states_card.Begin(&show_save_states_)) { + RenderSaveStates(); + } + save_states_card.End(); + } + + // Keyboard Configuration Card + if (show_keyboard_config_) { + if (keyboard_card.Begin(&show_keyboard_config_)) { + RenderKeyboardConfig(); + } + keyboard_card.End(); + } + + } catch (const std::exception& e) { + // Fallback to basic UI if theming fails + ImGui::Text("Error loading emulator UI: %s", e.what()); + if (ImGui::Button("Retry")) { + // Force theme manager reinitialization + auto& theme_manager = gui::ThemeManager::Get(); + theme_manager.InitializeBuiltInThemes(); } - - // CPU Instruction Log - RenderCpuInstructionLog(snes_.cpu().instruction_log_); - - // PPU Column - ImGui::TableNextColumn(); - - // SNES PPU - RenderSnesPpu(); - - // Breakpoint List - RenderBreakpointList(); - - ImGui::EndTable(); } } @@ -233,22 +307,6 @@ void Emulator::RenderSnesPpu() { } void Emulator::RenderNavBar() { - if (ImGui::BeginMenuBar()) { - if (ImGui::BeginMenu("Options")) { - if (ImGui::MenuItem("Input")) { - // Input options logic - } - if (ImGui::MenuItem("Audio")) { - // Audio options logic - } - if (ImGui::MenuItem("Video")) { - // Video options logic - } - ImGui::EndMenu(); - } - ImGui::EndMenuBar(); - } - if (ImGui::Button(ICON_MD_PLAY_ARROW)) { running_ = true; } @@ -619,10 +677,308 @@ void Emulator::RenderMemoryViewer() { } } +void Emulator::RenderModernCpuDebugger() { + try { + auto& theme_manager = gui::ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + // CPU Status Panel + if (ImGui::CollapsingHeader("CPU Status", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##CpuStatus", ImVec2(0, 120), true); + + // Compact register display in a table + if (ImGui::BeginTable("Registers", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("Register", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 80); + ImGui::TableSetupColumn("Register", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 80); + ImGui::TableHeadersRow(); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); ImGui::Text("A"); + ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X", snes_.cpu().A); + ImGui::TableNextColumn(); ImGui::Text("D"); + ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X", snes_.cpu().D); + + ImGui::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::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::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::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::EndTable(); + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); + } + + // SPC700 Status Panel + if (ImGui::CollapsingHeader("SPC700 Status", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##SpcStatus", ImVec2(0, 80), true); + + if (ImGui::BeginTable("SPCRegisters", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("Register", ImGuiTableColumnFlags_WidthFixed, 50); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableSetupColumn("Register", ImGuiTableColumnFlags_WidthFixed, 50); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableHeadersRow(); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); ImGui::Text("A"); + ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%02X", snes_.apu().spc700().A); + ImGui::TableNextColumn(); ImGui::Text("PC"); + ImGui::TableNextColumn(); ImGui::TextColored(ConvertColorToImVec4(theme.success), "0x%04X", snes_.apu().spc700().PC); + + ImGui::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::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::EndTable(); + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); + } + + // CPU Instruction Log + RenderCpuInstructionLog(snes_.cpu().instruction_log_); + } catch (const std::exception& e) { + // Ensure any pushed styles are popped on error + try { + ImGui::PopStyleColor(); + } catch (...) { + // Ignore PopStyleColor errors + } + ImGui::Text("CPU Debugger Error: %s", e.what()); + } +} + +void Emulator::RenderPerformanceMonitor() { + try { + auto& theme_manager = gui::ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##PerformanceMonitor", ImVec2(0, 0), true); + + // Performance Metrics + if (ImGui::CollapsingHeader("Real-time Metrics", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Columns(2, "PerfColumns"); + + // Frame Rate + ImGui::Text("Frame Rate:"); + ImGui::SameLine(); + if (current_fps_ > 0) { + ImVec4 fps_color = (current_fps_ >= 59.0 && current_fps_ <= 61.0) ? + ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.error); + ImGui::TextColored(fps_color, "%.1f FPS", current_fps_); + } else { + ImGui::TextColored(ConvertColorToImVec4(theme.warning), "-- FPS"); + } + + // Audio Status + uint32_t audio_queued = SDL_GetQueuedAudioSize(audio_device_); + uint32_t audio_frames = audio_queued / (wanted_samples_ * 4); + ImGui::Text("Audio Queue:"); + ImGui::SameLine(); + ImVec4 audio_color = (audio_frames >= 2 && audio_frames <= 6) ? + ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.warning); + ImGui::TextColored(audio_color, "%u frames", audio_frames); + + ImGui::NextColumn(); + + // Timing + double frame_time = (current_fps_ > 0) ? (1000.0 / current_fps_) : 0.0; + ImGui::Text("Frame Time:"); + ImGui::SameLine(); + ImGui::TextColored(ConvertColorToImVec4(theme.info), "%.2f ms", frame_time); + + // Emulation State + ImGui::Text("State:"); + ImGui::SameLine(); + ImVec4 state_color = running_ ? ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.warning); + ImGui::TextColored(state_color, "%s", running_ ? "Running" : "Paused"); + + ImGui::Columns(1); + } + + // Memory Usage + if (ImGui::CollapsingHeader("Memory Usage")) { + ImGui::Text("ROM Size: %zu bytes", rom_data_.size()); + ImGui::Text("RAM Usage: %d KB", 128); // SNES RAM is 128KB + ImGui::Text("VRAM Usage: %d KB", 64); // SNES VRAM is 64KB + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); + } catch (const std::exception& e) { + // Ensure any pushed styles are popped on error + try { + ImGui::PopStyleColor(); + } catch (...) { + // Ignore PopStyleColor errors + } + ImGui::Text("Performance Monitor Error: %s", e.what()); + } +} + +void Emulator::RenderAIAgentPanel() { + try { + auto& theme_manager = gui::ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##AIAgentPanel", ImVec2(0, 0), true); + + // AI Agent Status + if (ImGui::CollapsingHeader("Agent Status", ImGuiTreeNodeFlags_DefaultOpen)) { + auto metrics = GetMetrics(); + + ImGui::Columns(2, "AgentColumns"); + + // Emulator Readiness + ImGui::Text("Emulator Ready:"); + ImGui::SameLine(); + ImVec4 ready_color = IsEmulatorReady() ? ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.error); + ImGui::TextColored(ready_color, "%s", IsEmulatorReady() ? "Yes" : "No"); + + // Current State + ImGui::Text("Current PC:"); + ImGui::SameLine(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "0x%04X:%02X", metrics.cpu_pc, metrics.cpu_pb); + + ImGui::NextColumn(); + + // Performance + ImGui::Text("FPS:"); + ImGui::SameLine(); + ImVec4 fps_color = (metrics.fps >= 59.0) ? ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.warning); + ImGui::TextColored(fps_color, "%.1f", metrics.fps); + + // Cycles + ImGui::Text("Cycles:"); + ImGui::SameLine(); + ImGui::TextColored(ConvertColorToImVec4(theme.info), "%llu", metrics.cycles); + + ImGui::Columns(1); + } + + // AI Agent Controls + if (ImGui::CollapsingHeader("Agent Controls", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Columns(2, "ControlColumns"); + + // Single Step Control + if (ImGui::Button("Step Instruction", ImVec2(-1, 30))) { + StepSingleInstruction(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Execute a single CPU instruction"); + } + + // Breakpoint Controls + static char bp_input[10] = ""; + ImGui::InputText("Breakpoint Address", bp_input, IM_ARRAYSIZE(bp_input)); + if (ImGui::Button("Add Breakpoint", ImVec2(-1, 25))) { + if (strlen(bp_input) > 0) { + uint32_t addr = std::stoi(bp_input, nullptr, 16); + SetBreakpoint(addr); + memset(bp_input, 0, sizeof(bp_input)); + } + } + + ImGui::NextColumn(); + + // Clear All Breakpoints + if (ImGui::Button("Clear All Breakpoints", ImVec2(-1, 30))) { + ClearAllBreakpoints(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Remove all active breakpoints"); + } + + // Toggle Emulation + if (ImGui::Button(running_ ? "Pause Emulation" : "Resume Emulation", ImVec2(-1, 30))) { + running_ = !running_; + } + + ImGui::Columns(1); + } + + // Current Breakpoints + if (ImGui::CollapsingHeader("Active Breakpoints")) { + auto breakpoints = GetBreakpoints(); + if (breakpoints.empty()) { + ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), "No breakpoints set"); + } else { + ImGui::BeginChild("BreakpointsList", ImVec2(0, 150), true); + for (auto bp : breakpoints) { + if (ImGui::Selectable(absl::StrFormat("0x%04X", bp).c_str())) { + // Jump to breakpoint or remove it + } + ImGui::SameLine(); + if (ImGui::SmallButton(absl::StrFormat("Remove##%04X", bp).c_str())) { + // TODO: Implement individual breakpoint removal + } + } + ImGui::EndChild(); + } + } + + // AI Agent API Information + if (ImGui::CollapsingHeader("API Reference")) { + ImGui::TextWrapped("Available API functions for AI agents:"); + ImGui::BulletText("IsEmulatorReady() - Check if emulator is ready"); + ImGui::BulletText("GetMetrics() - Get current performance metrics"); + ImGui::BulletText("StepSingleInstruction() - Execute one CPU instruction"); + ImGui::BulletText("SetBreakpoint(address) - Set breakpoint at address"); + ImGui::BulletText("ClearAllBreakpoints() - Remove all breakpoints"); + ImGui::BulletText("GetBreakpoints() - Get list of active breakpoints"); + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); + } catch (const std::exception& e) { + // Ensure any pushed styles are popped on error + try { + ImGui::PopStyleColor(); + } catch (...) { + // Ignore PopStyleColor errors + } + ImGui::Text("AI Agent Panel Error: %s", e.what()); + } +} + void Emulator::RenderCpuInstructionLog( const std::vector& instruction_log) { - if (ImGui::CollapsingHeader("Instruction Log", - ImGuiTreeNodeFlags_DefaultOpen)) { // Filtering options static char filter[256]; ImGui::InputText("Filter", filter, IM_ARRAYSIZE(filter)); @@ -652,6 +1008,168 @@ void Emulator::RenderCpuInstructionLog( } ImGui::EndChild(); +} + +void Emulator::RenderSaveStates() { + try { + auto& theme_manager = gui::ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##SaveStates", ImVec2(0, 0), true); + + // Save State Management + 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))) { + // TODO: Implement save state to slot + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Save current state to slot %d (F%d)", i, i); + } + } + + ImGui::NextColumn(); + + // Load slots + for (int i = 1; i <= 4; ++i) { + if (ImGui::Button(absl::StrFormat("Load Slot %d", i).c_str(), ImVec2(-1, 30))) { + // TODO: Implement load state from slot + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Load state from slot %d (Shift+F%d)", i, i); + } + } + + ImGui::Columns(1); + } + + // File-based save states + if (ImGui::CollapsingHeader("File-based Saves")) { + static char save_name[256] = ""; + ImGui::InputText("Save Name", save_name, IM_ARRAYSIZE(save_name)); + + if (ImGui::Button("Save to File", ImVec2(-1, 30))) { + // TODO: Implement save to file + } + + if (ImGui::Button("Load from File", ImVec2(-1, 30))) { + // TODO: Implement load from file + } + } + + // Rewind functionality + if (ImGui::CollapsingHeader("Rewind")) { + ImGui::TextWrapped("Rewind functionality allows you to step back through recent gameplay."); + + static bool rewind_enabled = false; + ImGui::Checkbox("Enable Rewind (uses more memory)", &rewind_enabled); + + if (rewind_enabled) { + if (ImGui::Button("Rewind 1 Second", ImVec2(-1, 30))) { + // TODO: Implement rewind + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Rewind gameplay by 1 second (Backquote key)"); + } + } else { + ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), + "Enable rewind to use this feature"); + } + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); + } catch (const std::exception& e) { + try { + ImGui::PopStyleColor(); + } catch (...) {} + ImGui::Text("Save States Error: %s", e.what()); + } +} + +void Emulator::RenderKeyboardConfig() { + try { + auto& theme_manager = gui::ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##KeyboardConfig", ImVec2(0, 0), true); + + // Keyboard Configuration + 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); + ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + + auto DrawKeyBinding = [&](const char* label, ImGuiKey& key) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "%s", label); + ImGui::TableNextColumn(); + + std::string button_label = absl::StrFormat("%s##%s", ImGui::GetKeyName(key), label); + if (ImGui::Button(button_label.c_str(), ImVec2(-1, 0))) { + // TODO: Implement key remapping + ImGui::OpenPopup(absl::StrFormat("Remap%s", label).c_str()); + } + + if (ImGui::BeginPopup(absl::StrFormat("Remap%s", label).c_str())) { + ImGui::Text("Press any key..."); + // TODO: Detect key press and update binding + ImGui::EndPopup(); + } + }; + + DrawKeyBinding("A Button", keybindings_.a_button); + DrawKeyBinding("B Button", keybindings_.b_button); + DrawKeyBinding("X Button", keybindings_.x_button); + DrawKeyBinding("Y Button", keybindings_.y_button); + DrawKeyBinding("L Button", keybindings_.l_button); + DrawKeyBinding("R Button", keybindings_.r_button); + DrawKeyBinding("Start", keybindings_.start_button); + DrawKeyBinding("Select", keybindings_.select_button); + DrawKeyBinding("Up", keybindings_.up_button); + DrawKeyBinding("Down", keybindings_.down_button); + DrawKeyBinding("Left", keybindings_.left_button); + DrawKeyBinding("Right", keybindings_.right_button); + + ImGui::EndTable(); + } + } + + // Emulator Hotkeys + if (ImGui::CollapsingHeader("Emulator Hotkeys")) { + ImGui::TextWrapped("System-level keyboard shortcuts:"); + ImGui::Separator(); + + ImGui::BulletText("F1-F4: Quick save to slot 1-4"); + ImGui::BulletText("Shift+F1-F4: Quick load from slot 1-4"); + ImGui::BulletText("Backquote (`): Rewind gameplay"); + ImGui::BulletText("Tab: Fast forward (turbo mode)"); + ImGui::BulletText("Pause/Break: Pause/Resume emulation"); + ImGui::BulletText("F12: Take screenshot"); + } + + // Reset to defaults + if (ImGui::Button("Reset to Defaults", ImVec2(-1, 35))) { + keybindings_ = EmulatorKeybindings(); + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); + } catch (const std::exception& e) { + try { + ImGui::PopStyleColor(); + } catch (...) {} + ImGui::Text("Keyboard Config Error: %s", e.what()); } } diff --git a/src/app/emu/emulator.h b/src/app/emu/emulator.h index d59a57e3..bd2a0f0a 100644 --- a/src/app/emu/emulator.h +++ b/src/app/emu/emulator.h @@ -48,6 +48,37 @@ class Emulator { audio_device_ = audio_device; } auto wanted_samples() const -> int { return wanted_samples_; } + + // AI Agent Integration API + bool IsEmulatorReady() const { return snes_.running() && !rom_data_.empty(); } + double GetCurrentFPS() const { return current_fps_; } + uint64_t GetCurrentCycle() { return snes_.mutable_cycles(); } + uint16_t GetCPUPC() { return snes_.cpu().PC; } + uint8_t GetCPUB() { return snes_.cpu().DB; } + void StepSingleInstruction() { snes_.cpu().RunOpcode(); } + void SetBreakpoint(uint32_t address) { snes_.cpu().SetBreakpoint(address); } + void ClearAllBreakpoints() { snes_.cpu().ClearBreakpoints(); } + std::vector GetBreakpoints() { return snes_.cpu().GetBreakpoints(); } + + // Performance monitoring for AI agents + struct EmulatorMetrics { + double fps; + uint64_t cycles; + uint32_t audio_frames_queued; + bool is_running; + uint16_t cpu_pc; + uint8_t cpu_pb; + }; + EmulatorMetrics GetMetrics() { + return { + .fps = current_fps_, + .cycles = snes_.mutable_cycles(), + .audio_frames_queued = SDL_GetQueuedAudioSize(audio_device_) / (wanted_samples_ * 4), + .is_running = running_, + .cpu_pc = snes_.cpu().PC, + .cpu_pb = snes_.cpu().PB + }; + } private: void RenderNavBar(); @@ -57,6 +88,11 @@ class Emulator { void RenderSnesPpu(); void RenderBreakpointList(); void RenderMemoryViewer(); + void RenderModernCpuDebugger(); + void RenderPerformanceMonitor(); + void RenderAIAgentPanel(); + void RenderSaveStates(); + void RenderKeyboardConfig(); struct Bookmark { std::string name; diff --git a/src/app/emu/video/ppu.cc b/src/app/emu/video/ppu.cc index 6370447a..87433024 100644 --- a/src/app/emu/video/ppu.cc +++ b/src/app/emu/video/ppu.cc @@ -114,7 +114,7 @@ void Ppu::Reset() { fixed_color_g_ = 0; fixed_color_b_ = 0; forced_blank_ = true; - brightness = 0; + brightness = 15; // SNES hardware default is maximum brightness mode = 0; bg3priority = false; even_frame = false;