From b5ec0cb637bb2f9ce7f98426239764d2cfef3626 Mon Sep 17 00:00:00 2001 From: scawful Date: Wed, 8 Oct 2025 23:38:20 -0400 Subject: [PATCH] feat: Enhance emulator UI and performance features - Added new UI components for the emulator, including dedicated panels for CPU and APU debugging, improving user interaction and debugging capabilities. - Implemented a caching mechanism for rendered objects in the DungeonCanvasViewer to optimize performance and reduce rendering time. - Updated the CMake configuration to include new UI source files, ensuring proper organization and build management. - Enhanced the theme manager with improved color definitions for better visibility and consistency across the UI. - Refactored the emulator interface to delegate rendering tasks to the UI layer, streamlining the codebase and improving maintainability. --- src/CMakeLists.txt | 4 + .../editor/dungeon/dungeon_canvas_viewer.cc | 40 +- src/app/editor/message/message_editor.cc | 2 +- src/app/emu/emulator.cc | 895 +----------------- src/app/emu/emulator.h | 13 + src/app/emu/ui/debugger_ui.cc | 594 ++++++++++++ src/app/emu/ui/emulator_ui.cc | 282 ++++++ src/app/emu/video/ppu.cc | 20 +- src/app/gui/theme_manager.cc | 73 +- tools/test_helpers/dungeon_test_harness.cc | 2 +- 10 files changed, 1025 insertions(+), 900 deletions(-) create mode 100644 src/app/emu/ui/debugger_ui.cc create mode 100644 src/app/emu/ui/emulator_ui.cc diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 420cfd55..45fbd766 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,8 @@ set( app/emu/input/input_backend.cc app/emu/input/input_manager.cc app/emu/ui/input_handler.cc + app/emu/ui/emulator_ui.cc + app/emu/ui/debugger_ui.cc app/emu/cpu/cpu.cc app/emu/video/ppu.cc app/emu/memory/dma.cc @@ -939,7 +941,9 @@ source_group("Application\\Emulator\\Input" FILES source_group("Application\\Emulator\\UI" FILES app/emu/ui/input_handler.cc app/emu/ui/input_handler.h + app/emu/ui/emulator_ui.cc app/emu/ui/emulator_ui.h + app/emu/ui/debugger_ui.cc app/emu/ui/debugger_ui.h ) diff --git a/src/app/editor/dungeon/dungeon_canvas_viewer.cc b/src/app/editor/dungeon/dungeon_canvas_viewer.cc index 624bf02f..d53bd6a2 100644 --- a/src/app/editor/dungeon/dungeon_canvas_viewer.cc +++ b/src/app/editor/dungeon/dungeon_canvas_viewer.cc @@ -179,6 +179,23 @@ void DungeonCanvasViewer::RenderObjectInCanvas(const zelda3::RoomObject &object, return; // Skip objects outside visible area } + // Calculate palette hash for caching + uint64_t palette_hash = 0; + for (size_t i = 0; i < palette.size() && i < 16; ++i) { + palette_hash ^= std::hash{}(palette[i].snes()) + 0x9e3779b9 + + (palette_hash << 6) + (palette_hash >> 2); + } + + // Check cache first + for (auto& cached : object_render_cache_) { + if (cached.object_id == object.id_ && cached.object_x == object.x_ && + cached.object_y == object.y_ && cached.object_size == object.size_ && + cached.palette_hash == palette_hash && cached.is_valid) { + canvas_.DrawBitmap(cached.rendered_bitmap, canvas_x, canvas_y, 1.0f, 255); + return; + } + } + // Create a mutable copy of the object to ensure tiles are loaded auto mutable_object = object; mutable_object.set_rom(rom_); @@ -192,10 +209,27 @@ void DungeonCanvasViewer::RenderObjectInCanvas(const zelda3::RoomObject &object, // Ensure the bitmap is valid and has content if (object_bitmap.width() > 0 && object_bitmap.height() > 0) { object_bitmap.SetPalette(palette); - // Queue texture creation for the object bitmap via Arena's deferred system + + // Add to cache + ObjectRenderCache cache_entry; + cache_entry.object_id = object.id_; + cache_entry.object_x = object.x_; + cache_entry.object_y = object.y_; + cache_entry.object_size = object.size_; + cache_entry.palette_hash = palette_hash; + cache_entry.rendered_bitmap = std::move(object_bitmap); // Move bitmap into cache + cache_entry.is_valid = true; + + if (object_render_cache_.size() >= 200) { // Limit cache size + object_render_cache_.erase(object_render_cache_.begin()); + } + object_render_cache_.push_back(std::move(cache_entry)); + + // Get pointer to cached bitmap and queue texture creation + gfx::Bitmap* cached_bitmap = &object_render_cache_.back().rendered_bitmap; gfx::Arena::Get().QueueTextureCommand( - gfx::Arena::TextureCommandType::CREATE, &object_bitmap); - canvas_.DrawBitmap(object_bitmap, canvas_x, canvas_y, 1.0f, 255); + gfx::Arena::TextureCommandType::CREATE, cached_bitmap); + canvas_.DrawBitmap(*cached_bitmap, canvas_x, canvas_y, 1.0f, 255); return; } } diff --git a/src/app/editor/message/message_editor.cc b/src/app/editor/message/message_editor.cc index 004e26cd..357a3589 100644 --- a/src/app/editor/message/message_editor.cc +++ b/src/app/editor/message/message_editor.cc @@ -336,7 +336,7 @@ void MessageEditor::DrawMessagePreview() { } else { // Create bitmap and queue texture creation current_font_gfx16_bitmap_.Create(kCurrentMessageWidth, kCurrentMessageHeight, - 172, message_preview_.current_preview_data_); + 64, message_preview_.current_preview_data_); current_font_gfx16_bitmap_.SetPalette(font_preview_colors_); gfx::Arena::Get().QueueTextureCommand( gfx::Arena::TextureCommandType::CREATE, ¤t_font_gfx16_bitmap_); diff --git a/src/app/emu/emulator.cc b/src/app/emu/emulator.cc index 1cefb0bc..5bb2bec6 100644 --- a/src/app/emu/emulator.cc +++ b/src/app/emu/emulator.cc @@ -11,51 +11,19 @@ namespace yaze::core { extern bool g_window_is_resizing; } -#include "app/emu/cpu/internal/opcodes.h" #include "app/emu/debug/disassembly_viewer.h" +#include "app/emu/ui/debugger_ui.h" +#include "app/emu/ui/emulator_ui.h" #include "app/emu/ui/input_handler.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 "imgui/imgui.h" -#include "imgui_memory_editor.h" -#include "util/file_util.h" namespace yaze { namespace emu { -namespace { -bool ShouldDisplay(const InstructionEntry& entry, const char* filter) { - if (filter[0] == '\0') { - return true; - } - - // Supported fields: address, opcode, operands - if (entry.operands.find(filter) != std::string::npos) { - return true; - } - - if (absl::StrFormat("%06X", entry.address).find(filter) != - std::string::npos) { - return true; - } - - if (opcode_to_mnemonic.at(entry.opcode).find(filter) != std::string::npos) { - return true; - } - - return false; -} - -} // namespace - -using ImGui::SameLine; -using ImGui::Separator; -using ImGui::TableNextColumn; -using ImGui::Text; - Emulator::~Emulator() { // Don't call Cleanup() in destructor - renderer is already destroyed // Just stop emulation @@ -395,20 +363,20 @@ void Emulator::RenderEmulatorInterface() { static bool show_apu_debugger_ = true; // 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", + static gui::EditorCard cpu_card(ICON_MD_MEMORY " CPU Debugger", ICON_MD_MEMORY); + static gui::EditorCard ppu_card(ICON_MD_VIDEOGAME_ASSET " PPU Display", ICON_MD_VIDEOGAME_ASSET); - gui::EditorCard memory_card(ICON_MD_DATA_ARRAY " Memory Viewer", + static gui::EditorCard memory_card(ICON_MD_DATA_ARRAY " Memory Viewer", ICON_MD_DATA_ARRAY); - gui::EditorCard breakpoints_card(ICON_MD_BUG_REPORT " Breakpoints", + static gui::EditorCard breakpoints_card(ICON_MD_BUG_REPORT " Breakpoints", ICON_MD_BUG_REPORT); - gui::EditorCard performance_card(ICON_MD_SPEED " Performance", + static 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", + static gui::EditorCard ai_card(ICON_MD_SMART_TOY " AI Agent", ICON_MD_SMART_TOY); + static gui::EditorCard save_states_card(ICON_MD_SAVE " Save States", ICON_MD_SAVE); + static gui::EditorCard keyboard_card(ICON_MD_KEYBOARD " Keyboard Config", ICON_MD_KEYBOARD); - gui::EditorCard apu_debug_card(ICON_MD_MUSIC_NOTE " APU Debugger", + static gui::EditorCard apu_debug_card(ICON_MD_MUSIC_NOTE " APU Debugger", ICON_MD_MUSIC_NOTE); // Configure default positions @@ -528,162 +496,13 @@ void Emulator::RenderEmulatorInterface() { } void Emulator::RenderSnesPpu() { - ImVec2 size = ImVec2(512, 480); - if (snes_.running()) { - ImGui::BeginChild("EmulatorOutput", ImVec2(0, 480), true, - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar); - ImGui::SetCursorPosX((ImGui::GetWindowSize().x - size.x) * 0.5f); - ImGui::SetCursorPosY((ImGui::GetWindowSize().y - size.y) * 0.5f); - ImGui::Image((ImTextureID)(intptr_t)ppu_texture_, size, ImVec2(0, 0), - ImVec2(1, 1)); - ImGui::EndChild(); - - } else { - ImGui::Text("Emulator output not available."); - ImGui::BeginChild("EmulatorOutput", ImVec2(0, 480), true, - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar); - ImGui::SetCursorPosX(((ImGui::GetWindowSize().x * 0.5f) - size.x) * 0.5f); - ImGui::SetCursorPosY(((ImGui::GetWindowSize().y * 0.5f) - size.y) * 0.5f); - ImGui::Dummy(size); - ImGui::EndChild(); - } - ImGui::Separator(); + // Delegate to UI layer + ui::RenderSnesPpu(this); } void Emulator::RenderNavBar() { - if (ImGui::Button(ICON_MD_PLAY_ARROW)) { - running_ = true; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Start Emulation"); - } - SameLine(); - - if (ImGui::Button(ICON_MD_PAUSE)) { - running_ = false; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Pause Emulation"); - } - SameLine(); - - if (ImGui::Button(ICON_MD_SKIP_NEXT)) { - // Step through Code logic - snes_.cpu().RunOpcode(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Step Through Code"); - } - SameLine(); - - if (ImGui::Button(ICON_MD_REFRESH)) { - // Reset Emulator logic - snes_.Reset(true); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Reset Emulator"); - } - SameLine(); - - if (ImGui::Button(ICON_MD_STOP)) { - // Stop Emulation logic - running_ = false; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Stop Emulation"); - } - SameLine(); - - if (ImGui::Button(ICON_MD_SAVE)) { - // Save State logic - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Save State"); - } - SameLine(); - - if (ImGui::Button(ICON_MD_SYSTEM_UPDATE_ALT)) {} - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Load State"); - } - - // Additional elements - SameLine(); - if (ImGui::Button(ICON_MD_SETTINGS)) { - // Settings logic - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Settings"); - } - - static bool open_file = false; - SameLine(); - if (ImGui::Button(ICON_MD_INFO)) { - open_file = true; - - // About Debugger logic - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("About Debugger"); - } - SameLine(); - // Recording control moved to DisassemblyViewer UI - bool recording = disassembly_viewer_.IsRecording(); - if (ImGui::Checkbox("Recording", &recording)) { - disassembly_viewer_.SetRecording(recording); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Toggle instruction recording to DisassemblyViewer\n(Always lightweight - uses sparse address map)"); - } - - SameLine(); - ImGui::Checkbox("Turbo", &turbo_mode_); - - // Display FPS and Audio Status - SameLine(); - ImGui::Text("|"); - SameLine(); - if (current_fps_ > 0) { - ImGui::Text("FPS: %.1f", current_fps_); - } else { - ImGui::Text("FPS: --"); - } - - SameLine(); - if (audio_backend_) { - auto audio_status = audio_backend_->GetStatus(); - ImGui::Text("| Audio: %u frames", audio_status.queued_frames); - } else { - ImGui::Text("| Audio: N/A"); - } - - static bool show_memory_viewer = false; - - SameLine(); - if (ImGui::Button(ICON_MD_MEMORY)) { - show_memory_viewer = !show_memory_viewer; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Memory Viewer"); - } - - if (show_memory_viewer) { - ImGui::Begin("Memory Viewer", &show_memory_viewer); - RenderMemoryViewer(); - ImGui::End(); - } - - if (open_file) { - auto file_name = util::FileDialogWrapper::ShowOpenFileDialog(); - if (!file_name.empty()) { - std::ifstream file(file_name, std::ios::binary); - // Load the data directly into rom_data - rom_data_.assign(std::istreambuf_iterator(file), - std::istreambuf_iterator()); - snes_.Init(rom_data_); - open_file = false; - } - } + // Delegate to UI layer + ui::RenderNavBar(this); } // REMOVED: HandleEvents() - replaced by ui::InputHandler::Poll() @@ -691,147 +510,13 @@ void Emulator::RenderNavBar() { // for continuous game input. Now using SDL_GetKeyboardState() for proper polling. void Emulator::RenderBreakpointList() { - if (ImGui::Button("Set SPC PC")) { - snes_.apu().spc700().PC = 0xFFEF; - } - Separator(); - Text("Breakpoints"); - Separator(); - static char breakpoint_input[10] = ""; - static int current_memory_mode = 0; - - static bool read_mode = false; - static bool write_mode = false; - static bool execute_mode = false; - - if (ImGui::Combo("##TypeOfMemory", ¤t_memory_mode, "PRG\0RAM\0")) {} - - ImGui::Checkbox("Read", &read_mode); - SameLine(); - ImGui::Checkbox("Write", &write_mode); - SameLine(); - ImGui::Checkbox("Execute", &execute_mode); - - // Breakpoint input fields and buttons - if (ImGui::InputText("##BreakpointInput", breakpoint_input, 10, - ImGuiInputTextFlags_EnterReturnsTrue)) { - int breakpoint = std::stoi(breakpoint_input, nullptr, 16); - snes_.cpu().SetBreakpoint(breakpoint); - memset(breakpoint_input, 0, sizeof(breakpoint_input)); - } - SameLine(); - if (ImGui::Button("Add")) { - int breakpoint = std::stoi(breakpoint_input, nullptr, 16); - snes_.cpu().SetBreakpoint(breakpoint); - memset(breakpoint_input, 0, sizeof(breakpoint_input)); - } - SameLine(); - if (ImGui::Button("Clear")) { - snes_.cpu().ClearBreakpoints(); - } - Separator(); - auto breakpoints = snes_.cpu().GetBreakpoints(); - if (!breakpoints.empty()) { - Text("Breakpoints:"); - ImGui::BeginChild("BreakpointsList", ImVec2(0, 100), true); - for (auto breakpoint : breakpoints) { - if (ImGui::Selectable(absl::StrFormat("0x%04X", breakpoint).c_str())) { - // Jump to breakpoint - // snes_.cpu().JumpToBreakpoint(breakpoint); - } - } - ImGui::EndChild(); - } - Separator(); - gui::InputHexByte("PB", &manual_pb_, 50.f); - gui::InputHexWord("PC", &manual_pc_, 75.f); - if (ImGui::Button("Set Current Address")) { - snes_.cpu().PC = manual_pc_; - snes_.cpu().PB = manual_pb_; - } + // Delegate to UI layer + ui::RenderBreakpointList(this); } void Emulator::RenderMemoryViewer() { - static MemoryEditor ram_edit; - static MemoryEditor aram_edit; - static MemoryEditor mem_edit; - - if (ImGui::BeginTable("MemoryViewerTable", 4, - ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) { - ImGui::TableSetupColumn("Bookmarks"); - ImGui::TableSetupColumn("RAM"); - ImGui::TableSetupColumn("ARAM"); - ImGui::TableSetupColumn("ROM"); - ImGui::TableHeadersRow(); - - TableNextColumn(); - if (ImGui::CollapsingHeader("Bookmarks", ImGuiTreeNodeFlags_DefaultOpen)) { - // Input for adding a new bookmark - static char nameBuf[256]; - static uint64_t uint64StringBuf; - ImGui::InputText("Name", nameBuf, IM_ARRAYSIZE(nameBuf)); - gui::InputHex("Address", &uint64StringBuf); - if (ImGui::Button("Add Bookmark")) { - bookmarks.push_back({nameBuf, uint64StringBuf}); - memset(nameBuf, 0, sizeof(nameBuf)); - uint64StringBuf = 0; - } - - // Tree view of bookmarks - for (const auto& bookmark : bookmarks) { - if (ImGui::TreeNode(bookmark.name.c_str(), ICON_MD_STAR)) { - auto bookmark_string = absl::StrFormat( - "%s: 0x%08X", bookmark.name.c_str(), bookmark.value); - if (ImGui::Selectable(bookmark_string.c_str())) { - mem_edit.GotoAddrAndHighlight(static_cast(bookmark.value), - 1); - } - SameLine(); - if (ImGui::Button("Delete")) { - // Logic to delete the bookmark - bookmarks.erase(std::remove_if(bookmarks.begin(), bookmarks.end(), - [&](const Bookmark& b) { - return b.name == bookmark.name && - b.value == bookmark.value; - }), - bookmarks.end()); - } - ImGui::TreePop(); - } - } - } - - TableNextColumn(); - if (ImGui::BeginChild("RAM", ImVec2(0, 0), true, - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse)) { - ram_edit.DrawContents((void*)snes_.get_ram(), 0x20000); - ImGui::EndChild(); - } - - TableNextColumn(); - if (ImGui::BeginChild("ARAM", ImVec2(0, 0), true, - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse)) { - aram_edit.DrawContents((void*)snes_.apu().ram.data(), - snes_.apu().ram.size()); - ImGui::EndChild(); - } - - TableNextColumn(); - if (ImGui::BeginChild("ROM", ImVec2(0, 0), true, - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoScrollWithMouse)) { - mem_edit.DrawContents((void*)snes_.memory().rom_.data(), - snes_.memory().rom_.size()); - ImGui::EndChild(); - } - - ImGui::EndTable(); - } + // Delegate to UI layer + ui::RenderMemoryViewer(this); } void Emulator::RenderModernCpuDebugger() { @@ -1051,338 +736,29 @@ void Emulator::RenderModernCpuDebugger() { } 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 - ImGui::Text("Audio Queue:"); - ImGui::SameLine(); - if (audio_backend_) { - auto audio_status = audio_backend_->GetStatus(); - ImVec4 audio_color = (audio_status.queued_frames >= 2 && audio_status.queued_frames <= 6) - ? ConvertColorToImVec4(theme.success) - : ConvertColorToImVec4(theme.warning); - ImGui::TextColored(audio_color, "%u frames", audio_status.queued_frames); - } else { - ImGui::TextColored(ConvertColorToImVec4(theme.error), "No backend"); - } - - 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()); - } + // Delegate to UI layer + ui::RenderPerformanceMonitor(this); } 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()); - } + // Delegate to UI layer + ui::RenderAIAgentPanel(this); } void Emulator::RenderCpuInstructionLog( const std::vector& instruction_log) { - // Filtering options - static char filter[256]; - ImGui::InputText("Filter", filter, IM_ARRAYSIZE(filter)); - - // Instruction list - ImGui::BeginChild("InstructionList", ImVec2(0, 0), ImGuiChildFlags_None); - for (const auto& entry : instruction_log) { - if (ShouldDisplay(entry, filter)) { - if (ImGui::Selectable(absl::StrFormat("%06X:", entry.address).c_str())) { - // Logic to handle click (e.g., jump to address, set breakpoint) - } - - ImGui::SameLine(); - - ImVec4 color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - ImGui::TextColored(color, "%s", - opcode_to_mnemonic.at(entry.opcode).c_str()); - ImVec4 operand_color = ImVec4(0.7f, 0.5f, 0.3f, 1.0f); - ImGui::SameLine(); - ImGui::TextColored(operand_color, "%s", entry.operands.c_str()); - } - } - // Jump to the bottom of the child scrollbar - if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { - ImGui::SetScrollHereY(1.0f); - } - - ImGui::EndChild(); + // Delegate to UI layer (legacy log deprecated) + ui::RenderCpuInstructionLog(this, instruction_log.size()); } 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()); - } + // TODO: Create ui::RenderSaveStates() when save state system is implemented + auto& theme_manager = gui::ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::TextColored(ConvertColorToImVec4(theme.warning), + ICON_MD_SAVE " Save States - Coming Soon"); + ImGui::TextWrapped("Save state functionality will be implemented here."); } void Emulator::RenderKeyboardConfig() { @@ -1391,211 +767,8 @@ void Emulator::RenderKeyboardConfig() { } void Emulator::RenderApuDebugger() { - try { - auto& theme_manager = gui::ThemeManager::Get(); - const auto& theme = theme_manager.GetCurrentTheme(); - - ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); - ImGui::BeginChild("##ApuDebugger", ImVec2(0, 0), true); - - // Handshake Status - if (ImGui::CollapsingHeader("APU Handshake Status", ImGuiTreeNodeFlags_DefaultOpen)) { - auto& tracker = snes_.apu_handshake_tracker(); - - // Phase indicator with color - ImGui::Text("Phase:"); - ImGui::SameLine(); - - auto phase_str = tracker.GetPhaseString(); - ImVec4 phase_color; - if (phase_str == "RUNNING") { - phase_color = ConvertColorToImVec4(theme.success); - } else if (phase_str == "TRANSFER_ACTIVE") { - phase_color = ConvertColorToImVec4(theme.info); - } else if (phase_str == "WAITING_BBAA" || phase_str == "IPL_BOOT") { - phase_color = ConvertColorToImVec4(theme.warning); - } else { - phase_color = ConvertColorToImVec4(theme.text_primary); - } - ImGui::TextColored(phase_color, "%s", phase_str.c_str()); - - // Handshake complete indicator - ImGui::Text("Handshake:"); - ImGui::SameLine(); - if (tracker.IsHandshakeComplete()) { - ImGui::TextColored(ConvertColorToImVec4(theme.success), "✓ Complete"); - } else { - ImGui::TextColored(ConvertColorToImVec4(theme.error), "✗ Waiting"); - } - - // Transfer progress - if (tracker.IsTransferActive() || tracker.GetBytesTransferred() > 0) { - ImGui::Text("Bytes Transferred: %d", tracker.GetBytesTransferred()); - ImGui::Text("Blocks: %d", tracker.GetBlockCount()); - - auto progress = tracker.GetTransferProgress(); - if (!progress.empty()) { - ImGui::TextColored(ConvertColorToImVec4(theme.info), "%s", progress.c_str()); - } - } - - // Status summary - ImGui::Separator(); - ImGui::TextWrapped("%s", tracker.GetStatusSummary().c_str()); - } - - // Port Activity Log - if (ImGui::CollapsingHeader("Port Activity Log")) { - ImGui::BeginChild("##PortLog", ImVec2(0, 200), true); - - auto& tracker = snes_.apu_handshake_tracker(); - const auto& history = tracker.GetPortHistory(); - - if (history.empty()) { - ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), - "No port activity yet"); - } else { - // Show last 50 entries (most recent at bottom) - int start_idx = std::max(0, static_cast(history.size()) - 50); - for (size_t i = start_idx; i < history.size(); ++i) { - const auto& entry = history[i]; - - ImVec4 color = entry.is_cpu ? ConvertColorToImVec4(theme.accent) - : ConvertColorToImVec4(theme.info); - - ImGui::TextColored(color, "[%04llu] %s F%d = $%02X @ PC=$%04X %s", - entry.timestamp, - entry.is_cpu ? "CPU→" : "SPC→", - entry.port + 4, - entry.value, - entry.pc, - entry.description.c_str()); - } - - // Auto-scroll to bottom - if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { - ImGui::SetScrollHereY(1.0f); - } - } - - ImGui::EndChild(); - } - - // Current Port Values - if (ImGui::CollapsingHeader("Current Port Values", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::BeginTable("APU_Ports", 4, ImGuiTableFlags_Borders)) { - ImGui::TableSetupColumn("Port"); - ImGui::TableSetupColumn("CPU → SPC"); - ImGui::TableSetupColumn("SPC → CPU"); - ImGui::TableSetupColumn("Addr"); - ImGui::TableHeadersRow(); - - for (int i = 0; i < 4; ++i) { - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text("F%d", i + 4); - - ImGui::TableNextColumn(); - ImGui::TextColored(ConvertColorToImVec4(theme.accent), "$%02X", - snes_.apu().in_ports_[i]); - - ImGui::TableNextColumn(); - ImGui::TextColored(ConvertColorToImVec4(theme.info), "$%02X", - snes_.apu().out_ports_[i]); - - ImGui::TableNextColumn(); - ImGui::TextDisabled("$214%d / $F%d", i, i + 4); - } - - ImGui::EndTable(); - } - } - - // Quick Actions - if (ImGui::CollapsingHeader("Quick Actions", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TextColored(ConvertColorToImVec4(theme.warning), - "⚠️ Manual Testing Tools"); - ImGui::Separator(); - - // Full handshake sequence test - if (ImGui::Button("🎯 Full Handshake Test", ImVec2(-1, 35))) { - LOG_INFO("APU_DEBUG", "=== MANUAL HANDSHAKE TEST SEQUENCE ==="); - - // Step 1: Write $CC to F4 (initiate handshake) - snes_.Write(0x002140, 0xCC); - LOG_INFO("APU_DEBUG", "Step 1: Wrote $CC to F4 (port $2140)"); - - // Step 2: Write $01 to F5 (data port) - snes_.Write(0x002141, 0x01); - LOG_INFO("APU_DEBUG", "Step 2: Wrote $01 to F5 (port $2141)"); - - // Step 3: Write $00 to F6 (address low) - snes_.Write(0x002142, 0x00); - LOG_INFO("APU_DEBUG", "Step 3: Wrote $00 to F6 (port $2142)"); - - // Step 4: Write $02 to F7 (address high) - snes_.Write(0x002143, 0x02); - LOG_INFO("APU_DEBUG", "Step 4: Wrote $02 to F7 (port $2143)"); - - LOG_INFO("APU_DEBUG", "Handshake initiated - check Port Activity Log"); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Manually execute full handshake sequence:\n" - "$CC → F4 (init), $01 → F5, $00 → F6, $02 → F7"); - } - - ImGui::Spacing(); - - // Individual port writes for debugging - if (ImGui::CollapsingHeader("Manual Port Writes")) { - static uint8_t port_values[4] = {0xCC, 0x01, 0x00, 0x02}; - - for (int i = 0; i < 4; ++i) { - ImGui::PushID(i); - ImGui::Text("F%d ($214%d):", i + 4, i); - ImGui::SameLine(); - ImGui::SetNextItemWidth(80); - ImGui::InputScalar("##val", ImGuiDataType_U8, &port_values[i], NULL, NULL, "%02X", - ImGuiInputTextFlags_CharsHexadecimal); - ImGui::SameLine(); - if (ImGui::Button("Write")) { - snes_.Write(0x002140 + i, port_values[i]); - LOG_INFO("APU_DEBUG", "Wrote $%02X to F%d (port $214%d)", - port_values[i], i + 4, i); - } - ImGui::PopID(); - } - } - - ImGui::Spacing(); - ImGui::Separator(); - - // System controls - if (ImGui::Button("Reset APU", ImVec2(-1, 30))) { - snes_.apu().Reset(); - LOG_INFO("APU_DEBUG", "APU manually reset"); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Full APU reset - clears all state"); - } - - if (ImGui::Button("Clear Port History", ImVec2(-1, 30))) { - snes_.apu_handshake_tracker().Reset(); - LOG_INFO("APU_DEBUG", "Port history cleared"); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Clear the port activity log"); - } - } - - ImGui::EndChild(); - ImGui::PopStyleColor(); - } catch (const std::exception& e) { - try { - ImGui::PopStyleColor(); - } catch (...) {} - ImGui::Text("APU Debugger Error: %s", e.what()); - } + // Delegate to UI layer + ui::RenderApuDebugger(this); } } // namespace emu diff --git a/src/app/emu/emulator.h b/src/app/emu/emulator.h index 3991ea2e..3c8e9923 100644 --- a/src/app/emu/emulator.h +++ b/src/app/emu/emulator.h @@ -39,6 +39,7 @@ class Emulator { auto snes() -> Snes& { return snes_; } auto running() const -> bool { return running_; } + void set_running(bool running) { running_ = running; } // Audio backend access audio::IAudioBackend* audio_backend() { return audio_backend_.get(); } @@ -49,10 +50,22 @@ class Emulator { auto wanted_samples() const -> int { return wanted_samples_; } void set_renderer(gfx::IRenderer* renderer) { renderer_ = renderer; } + // Render access + gfx::IRenderer* renderer() { return renderer_; } + void* ppu_texture() { return ppu_texture_; } + + // Turbo mode + bool turbo_mode() const { return turbo_mode_; } + void set_turbo_mode(bool turbo) { turbo_mode_ = turbo; } + // Debugger access BreakpointManager& breakpoint_manager() { return breakpoint_manager_; } + debug::DisassemblyViewer& disassembly_viewer() { return disassembly_viewer_; } + input::InputManager& input_manager() { return input_manager_; } bool is_debugging() const { return debugging_; } void set_debugging(bool debugging) { debugging_ = debugging; } + bool is_initialized() const { return initialized_; } + bool is_snes_initialized() const { return snes_initialized_; } // AI Agent Integration API bool IsEmulatorReady() const { return snes_.running() && !rom_data_.empty(); } diff --git a/src/app/emu/ui/debugger_ui.cc b/src/app/emu/ui/debugger_ui.cc new file mode 100644 index 00000000..ddeb2a13 --- /dev/null +++ b/src/app/emu/ui/debugger_ui.cc @@ -0,0 +1,594 @@ +#include "app/emu/ui/debugger_ui.h" + +#include "absl/strings/str_format.h" +#include "app/emu/emulator.h" +#include "app/emu/cpu/cpu.h" +#include "app/gui/color.h" +#include "app/gui/icons.h" +#include "app/gui/input.h" +#include "app/gui/theme_manager.h" +#include "imgui/imgui.h" +#include "imgui_memory_editor.h" +#include "util/log.h" + +namespace yaze { +namespace emu { +namespace ui { + +using namespace yaze::gui; + +namespace { +// UI Constants +constexpr float kStandardSpacing = 8.0f; +constexpr float kButtonHeight = 30.0f; +constexpr float kLargeButtonHeight = 35.0f; + +void AddSpacing() { ImGui::Spacing(); ImGui::Spacing(); } +void AddSectionSpacing() { ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); } + +} // namespace + +void RenderModernCpuDebugger(Emulator* emu) { + if (!emu) return; + + auto& theme_manager = ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##CPUDebugger", ImVec2(0, 0), true); + + // Title with icon + ImGui::TextColored(ConvertColorToImVec4(theme.accent), + ICON_MD_DEVELOPER_BOARD " 65816 CPU Debugger"); + AddSectionSpacing(); + + auto& cpu = emu->snes().cpu(); + + // Debugger Controls + if (ImGui::CollapsingHeader(ICON_MD_SETTINGS " Controls", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 6)); + + if (ImGui::Button(ICON_MD_SKIP_NEXT " Step", ImVec2(100, kButtonHeight))) { + cpu.RunOpcode(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Execute single instruction (F10)"); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_FAST_FORWARD " Run to BP", ImVec2(120, kButtonHeight))) { + // Run until breakpoint + emu->set_running(true); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Run until next breakpoint (F5)"); + } + + ImGui::PopStyleVar(); + } + + AddSpacing(); + + // CPU Registers + if (ImGui::CollapsingHeader(ICON_MD_MEMORY " Registers", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::BeginTable("CPU_Registers", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Reg", ImGuiTableColumnFlags_WidthFixed, 40.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 70.0f); + ImGui::TableSetupColumn("Reg", ImGuiTableColumnFlags_WidthFixed, 40.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 70.0f); + ImGui::TableHeadersRow(); + + // Row 1: A, X + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.text_secondary), "A:"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "$%04X", cpu.A); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.text_secondary), "X:"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "$%04X", cpu.X); + + // Row 2: Y, D + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.text_secondary), "Y:"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "$%04X", cpu.Y); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.text_secondary), "D:"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "$%04X", cpu.D); + + // Row 3: DB, PB + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.text_secondary), "DB:"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "$%02X", cpu.DB); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.text_secondary), "PB:"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "$%02X", cpu.PB); + + // Row 4: PC, SP + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.text_secondary), "PC:"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.warning), "$%04X", cpu.PC); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.text_secondary), "SP:"); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "$%04X", cpu.SP()); + + ImGui::EndTable(); + } + + AddSpacing(); + + // Status Flags (visual checkboxes) + ImGui::TextColored(ConvertColorToImVec4(theme.text_secondary), ICON_MD_FLAG " Flags:"); + ImGui::Indent(); + + auto RenderFlag = [&](const char* name, bool value) { + ImVec4 color = value ? ConvertColorToImVec4(theme.success) : + ConvertColorToImVec4(theme.text_disabled); + ImGui::TextColored(color, "%s %s", value ? ICON_MD_CHECK_BOX : ICON_MD_CHECK_BOX_OUTLINE_BLANK, name); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s: %s", name, value ? "Set" : "Clear"); + } + ImGui::SameLine(); + }; + + RenderFlag("N", cpu.GetNegativeFlag()); + RenderFlag("V", cpu.GetOverflowFlag()); + RenderFlag("D", cpu.GetDecimalFlag()); + RenderFlag("I", cpu.GetInterruptFlag()); + RenderFlag("Z", cpu.GetZeroFlag()); + RenderFlag("C", cpu.GetCarryFlag()); + ImGui::NewLine(); + + ImGui::Unindent(); + } + + AddSpacing(); + + // Breakpoint Management + if (ImGui::CollapsingHeader(ICON_MD_STOP_CIRCLE " Breakpoints")) { + static char bp_input[10] = ""; + + ImGui::SetNextItemWidth(150); + if (ImGui::InputTextWithHint("##BP", "Address (hex)", bp_input, sizeof(bp_input), + ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_EnterReturnsTrue)) { + if (strlen(bp_input) > 0) { + uint32_t addr = std::stoi(bp_input, nullptr, 16); + emu->SetBreakpoint(addr); + memset(bp_input, 0, sizeof(bp_input)); + } + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_ADD " Add", ImVec2(80, 0))) { + if (strlen(bp_input) > 0) { + uint32_t addr = std::stoi(bp_input, nullptr, 16); + emu->SetBreakpoint(addr); + memset(bp_input, 0, sizeof(bp_input)); + } + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_CLEAR_ALL " Clear All", ImVec2(100, 0))) { + emu->ClearAllBreakpoints(); + } + + AddSpacing(); + + // List breakpoints + auto breakpoints = emu->GetBreakpoints(); + if (!breakpoints.empty()) { + ImGui::BeginChild("##BPList", ImVec2(0, 150), true); + for (size_t i = 0; i < breakpoints.size(); ++i) { + uint32_t bp = breakpoints[i]; + ImGui::PushID(i); + + ImGui::TextColored(ConvertColorToImVec4(theme.accent), + ICON_MD_STOP " $%06X", bp); + + ImGui::SameLine(200); + if (ImGui::SmallButton(ICON_MD_DELETE " Remove")) { + cpu.ClearBreakpoint(bp); + } + + ImGui::PopID(); + } + ImGui::EndChild(); + } else { + ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), + "No breakpoints set"); + } + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); +} + +void RenderBreakpointList(Emulator* emu) { + if (!emu) return; + + auto& theme_manager = ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##BreakpointList", ImVec2(0, 0), true); + + ImGui::TextColored(ConvertColorToImVec4(theme.accent), + ICON_MD_STOP_CIRCLE " Breakpoint Manager"); + AddSectionSpacing(); + + // Same content as in RenderModernCpuDebugger but with more detail + auto breakpoints = emu->GetBreakpoints(); + + ImGui::Text("Active Breakpoints: %zu", breakpoints.size()); + AddSpacing(); + + if (!breakpoints.empty()) { + if (ImGui::BeginTable("BreakpointTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn(ICON_MD_TAG, ImGuiTableColumnFlags_WidthFixed, 40); + ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + + for (size_t i = 0; i < breakpoints.size(); ++i) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.error), ICON_MD_STOP); + + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "$%06X", breakpoints[i]); + + ImGui::TableNextColumn(); + ImGui::PushID(i); + if (ImGui::SmallButton(ICON_MD_DELETE " Remove")) { + emu->snes().cpu().ClearBreakpoint(breakpoints[i]); + } + ImGui::PopID(); + } + + ImGui::EndTable(); + } + } else { + ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), + ICON_MD_INFO " No breakpoints set"); + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); +} + +void RenderMemoryViewer(Emulator* emu) { + if (!emu) return; + + auto& theme_manager = ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + static MemoryEditor mem_edit; + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##MemoryViewer", ImVec2(0, 0), true); + + ImGui::TextColored(ConvertColorToImVec4(theme.accent), + ICON_MD_STORAGE " Memory Viewer"); + AddSectionSpacing(); + + // Memory region selector + static int region = 0; + const char* regions[] = {"RAM ($0000-$1FFF)", "ROM Bank 0", "WRAM ($7E0000-$7FFFFF)", "SRAM"}; + + ImGui::SetNextItemWidth(250); + if (ImGui::Combo(ICON_MD_MAP " Region", ®ion, regions, IM_ARRAYSIZE(regions))) { + // Region changed + } + + AddSpacing(); + + // Render memory editor + uint8_t* memory_base = emu->snes().get_ram(); + size_t memory_size = 0x20000; + + mem_edit.DrawContents(memory_base, memory_size, 0x0000); + + ImGui::EndChild(); + ImGui::PopStyleColor(); +} + +void RenderCpuInstructionLog(Emulator* emu, uint32_t log_size) { + if (!emu) return; + + auto& theme_manager = ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##InstructionLog", ImVec2(0, 0), true); + + ImGui::TextColored(ConvertColorToImVec4(theme.warning), + ICON_MD_WARNING " Legacy Instruction Log"); + ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), + "Deprecated - Use Disassembly Viewer instead"); + AddSectionSpacing(); + + // Show DisassemblyViewer stats instead + ImGui::Text(ICON_MD_INFO " DisassemblyViewer Active:"); + ImGui::BulletText("Unique addresses: %zu", emu->disassembly_viewer().GetInstructionCount()); + ImGui::BulletText("Recording: %s", emu->disassembly_viewer().IsRecording() ? "ON" : "OFF"); + ImGui::BulletText("Auto-scroll: Available in viewer"); + + AddSpacing(); + + if (ImGui::Button(ICON_MD_OPEN_IN_NEW " Open Disassembly Viewer", ImVec2(-1, kLargeButtonHeight))) { + // TODO: Open disassembly viewer window + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); +} + +void RenderApuDebugger(Emulator* emu) { + if (!emu) return; + + auto& theme_manager = ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##ApuDebugger", ImVec2(0, 0), true); + + // Title + ImGui::TextColored(ConvertColorToImVec4(theme.accent), + ICON_MD_MUSIC_NOTE " APU / SPC700 Debugger"); + AddSectionSpacing(); + + auto& tracker = emu->snes().apu_handshake_tracker(); + + // Handshake Status with enhanced visuals + if (ImGui::CollapsingHeader(ICON_MD_HANDSHAKE " Handshake Status", ImGuiTreeNodeFlags_DefaultOpen)) { + // Phase with icon and color + auto phase_str = tracker.GetPhaseString(); + ImVec4 phase_color; + const char* phase_icon; + + if (phase_str == "RUNNING") { + phase_color = ConvertColorToImVec4(theme.success); + phase_icon = ICON_MD_CHECK_CIRCLE; + } else if (phase_str == "TRANSFER_ACTIVE") { + phase_color = ConvertColorToImVec4(theme.info); + phase_icon = ICON_MD_SYNC; + } else if (phase_str == "WAITING_BBAA" || phase_str == "IPL_BOOT") { + phase_color = ConvertColorToImVec4(theme.warning); + phase_icon = ICON_MD_PENDING; + } else { + phase_color = ConvertColorToImVec4(theme.error); + phase_icon = ICON_MD_ERROR; + } + + ImGui::Text(ICON_MD_SETTINGS " Phase:"); + ImGui::SameLine(); + ImGui::TextColored(phase_color, "%s %s", phase_icon, phase_str.c_str()); + + // Handshake complete indicator + ImGui::Text(ICON_MD_LINK " Handshake:"); + ImGui::SameLine(); + if (tracker.IsHandshakeComplete()) { + ImGui::TextColored(ConvertColorToImVec4(theme.success), + ICON_MD_CHECK_CIRCLE " Complete"); + } else { + ImGui::TextColored(ConvertColorToImVec4(theme.warning), + ICON_MD_HOURGLASS_EMPTY " Waiting"); + } + + // Transfer progress + if (tracker.IsTransferActive() || tracker.GetBytesTransferred() > 0) { + AddSpacing(); + ImGui::Text(ICON_MD_CLOUD_UPLOAD " Transfer Progress:"); + ImGui::Indent(); + ImGui::BulletText("Bytes: %d", tracker.GetBytesTransferred()); + ImGui::BulletText("Blocks: %d", tracker.GetBlockCount()); + + auto progress = tracker.GetTransferProgress(); + if (!progress.empty()) { + ImGui::TextColored(ConvertColorToImVec4(theme.info), "%s", progress.c_str()); + } + ImGui::Unindent(); + } + + // Status summary + AddSectionSpacing(); + ImGui::TextWrapped("%s", tracker.GetStatusSummary().c_str()); + } + + // Port Activity Log + if (ImGui::CollapsingHeader(ICON_MD_LIST " Port Activity Log", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::BeginChild("##PortLog", ImVec2(0, 200), true); + + const auto& history = tracker.GetPortHistory(); + + if (history.empty()) { + ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), + ICON_MD_INFO " No port activity yet"); + } else { + // Show last 50 entries + int start_idx = std::max(0, static_cast(history.size()) - 50); + for (size_t i = start_idx; i < history.size(); ++i) { + const auto& entry = history[i]; + + ImVec4 color = entry.is_cpu ? ConvertColorToImVec4(theme.accent) + : ConvertColorToImVec4(theme.info); + const char* icon = entry.is_cpu ? ICON_MD_ARROW_FORWARD : ICON_MD_ARROW_BACK; + + ImGui::TextColored(color, "[%04llu] %s %s F%d = $%02X @ PC=$%04X %s", + entry.timestamp, + entry.is_cpu ? "CPU" : "SPC", + icon, + entry.port + 4, + entry.value, + entry.pc, + entry.description.c_str()); + } + + // Auto-scroll + if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { + ImGui::SetScrollHereY(1.0f); + } + } + + ImGui::EndChild(); + } + + // Current Port Values + if (ImGui::CollapsingHeader(ICON_MD_SETTINGS_INPUT_COMPONENT " Current Port Values", + ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::BeginTable("APU_Ports", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Port", ImGuiTableColumnFlags_WidthFixed, 50); + ImGui::TableSetupColumn("CPU → SPC", ImGuiTableColumnFlags_WidthFixed, 80); + ImGui::TableSetupColumn("SPC → CPU", ImGuiTableColumnFlags_WidthFixed, 80); + ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + + for (int i = 0; i < 4; ++i) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text(ICON_MD_SETTINGS " F%d", i + 4); + + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), + "$%02X", emu->snes().apu().in_ports_[i]); + + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.info), + "$%02X", emu->snes().apu().out_ports_[i]); + + ImGui::TableNextColumn(); + ImGui::TextDisabled("$214%d / $F%d", i, i + 4); + } + + ImGui::EndTable(); + } + } + + // Quick Actions + if (ImGui::CollapsingHeader(ICON_MD_BUILD " Quick Actions", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::TextColored(ConvertColorToImVec4(theme.warning), + ICON_MD_WARNING " Manual Testing Tools"); + AddSpacing(); + + // Full handshake test + if (ImGui::Button(ICON_MD_PLAY_CIRCLE " Full Handshake Test", ImVec2(-1, kLargeButtonHeight))) { + LOG_INFO("APU_DEBUG", "=== MANUAL HANDSHAKE TEST ==="); + emu->snes().Write(0x002140, 0xCC); + emu->snes().Write(0x002141, 0x01); + emu->snes().Write(0x002142, 0x00); + emu->snes().Write(0x002143, 0x02); + LOG_INFO("APU_DEBUG", "Handshake sequence executed"); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Execute full handshake sequence:\n" + "$CC → F4, $01 → F5, $00 → F6, $02 → F7"); + } + + AddSpacing(); + + // Manual port writes + if (ImGui::TreeNode(ICON_MD_EDIT " Manual Port Writes")) { + static uint8_t port_values[4] = {0xCC, 0x01, 0x00, 0x02}; + + for (int i = 0; i < 4; ++i) { + ImGui::PushID(i); + ImGui::Text("F%d ($214%d):", i + 4, i); + ImGui::SameLine(); + ImGui::SetNextItemWidth(80); + ImGui::InputScalar("##val", ImGuiDataType_U8, &port_values[i], NULL, NULL, "%02X", + ImGuiInputTextFlags_CharsHexadecimal); + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_SEND " Write", ImVec2(100, 0))) { + emu->snes().Write(0x002140 + i, port_values[i]); + LOG_INFO("APU_DEBUG", "Wrote $%02X to F%d", port_values[i], i + 4); + } + ImGui::PopID(); + } + + ImGui::TreePop(); + } + + AddSectionSpacing(); + + // System controls + if (ImGui::Button(ICON_MD_RESTART_ALT " Reset APU", ImVec2(-1, kButtonHeight))) { + emu->snes().apu().Reset(); + LOG_INFO("APU_DEBUG", "APU reset"); + } + + if (ImGui::Button(ICON_MD_CLEAR_ALL " Clear Port History", ImVec2(-1, kButtonHeight))) { + tracker.Reset(); + LOG_INFO("APU_DEBUG", "Port history cleared"); + } + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); +} + +void RenderAIAgentPanel(Emulator* emu) { + if (!emu) return; + + auto& theme_manager = ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##AIAgent", ImVec2(0, 0), true); + + ImGui::TextColored(ConvertColorToImVec4(theme.accent), + ICON_MD_SMART_TOY " AI Agent Integration"); + AddSectionSpacing(); + + // Agent status + bool agent_ready = emu->IsEmulatorReady(); + ImVec4 status_color = agent_ready ? ConvertColorToImVec4(theme.success) : + ConvertColorToImVec4(theme.error); + + ImGui::Text("Status:"); + ImGui::SameLine(); + ImGui::TextColored(status_color, "%s %s", + agent_ready ? ICON_MD_CHECK_CIRCLE : ICON_MD_ERROR, + agent_ready ? "Ready" : "Not Ready"); + + AddSpacing(); + + // Emulator metrics for agents + if (ImGui::CollapsingHeader(ICON_MD_DATA_OBJECT " Metrics", ImGuiTreeNodeFlags_DefaultOpen)) { + auto metrics = emu->GetMetrics(); + + ImGui::BulletText("FPS: %.2f", metrics.fps); + ImGui::BulletText("Cycles: %llu", metrics.cycles); + ImGui::BulletText("CPU PC: $%02X:%04X", metrics.cpu_pb, metrics.cpu_pc); + ImGui::BulletText("Audio Queued: %u frames", metrics.audio_frames_queued); + ImGui::BulletText("Running: %s", metrics.is_running ? "YES" : "NO"); + } + + // Agent controls + if (ImGui::CollapsingHeader(ICON_MD_PLAY_CIRCLE " Agent Controls")) { + if (ImGui::Button(ICON_MD_PLAY_ARROW " Start Agent Session", ImVec2(-1, kLargeButtonHeight))) { + // TODO: Start agent + } + + if (ImGui::Button(ICON_MD_STOP " Stop Agent", ImVec2(-1, kButtonHeight))) { + // TODO: Stop agent + } + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); +} + +} // namespace ui +} // namespace emu +} // namespace yaze + diff --git a/src/app/emu/ui/emulator_ui.cc b/src/app/emu/ui/emulator_ui.cc new file mode 100644 index 00000000..3e7ea8fa --- /dev/null +++ b/src/app/emu/ui/emulator_ui.cc @@ -0,0 +1,282 @@ +#include "app/emu/ui/emulator_ui.h" + +#include "absl/strings/str_format.h" +#include "app/emu/emulator.h" +#include "app/gui/color.h" +#include "app/gui/icons.h" +#include "app/gui/theme_manager.h" +#include "imgui/imgui.h" +#include "util/log.h" + +namespace yaze { +namespace emu { +namespace ui { + +using namespace yaze::gui; + +namespace { +// UI Constants for consistent spacing +constexpr float kStandardSpacing = 8.0f; +constexpr float kSectionSpacing = 16.0f; +constexpr float kButtonHeight = 30.0f; +constexpr float kIconSize = 24.0f; + +// Helper to add consistent spacing +void AddSpacing() { + ImGui::Spacing(); + ImGui::Spacing(); +} + +void AddSectionSpacing() { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); +} + +} // namespace + +void RenderNavBar(Emulator* emu) { + if (!emu) return; + + auto& theme_manager = ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + // Navbar with theme colors + ImGui::PushStyleColor(ImGuiCol_Button, ConvertColorToImVec4(theme.button)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ConvertColorToImVec4(theme.button_hovered)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ConvertColorToImVec4(theme.button_active)); + + // Play/Pause button with icon + bool is_running = emu->running(); + if (is_running) { + if (ImGui::Button(ICON_MD_PAUSE " Pause", ImVec2(100, kButtonHeight))) { + emu->set_running(false); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Pause emulation (Space)"); + } + } else { + if (ImGui::Button(ICON_MD_PLAY_ARROW " Play", ImVec2(100, kButtonHeight))) { + emu->set_running(true); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Start emulation (Space)"); + } + } + + ImGui::SameLine(); + + // Step button + if (ImGui::Button(ICON_MD_SKIP_NEXT " Step", ImVec2(80, kButtonHeight))) { + if (!is_running) { + emu->snes().RunFrame(); + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Step one frame (F10)"); + } + + ImGui::SameLine(); + + // Reset button + if (ImGui::Button(ICON_MD_RESTART_ALT " Reset", ImVec2(80, kButtonHeight))) { + emu->snes().Reset(); + LOG_INFO("Emulator", "System reset"); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Reset SNES (Ctrl+R)"); + } + + ImGui::SameLine(); + ImGui::Separator(); + ImGui::SameLine(); + + // Debugger toggle + bool is_debugging = emu->is_debugging(); + if (ImGui::Checkbox(ICON_MD_BUG_REPORT " Debug", &is_debugging)) { + emu->set_debugging(is_debugging); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Enable debugger features"); + } + + ImGui::SameLine(); + + // Recording toggle (for DisassemblyViewer) + // Access through emulator's disassembly viewer + // bool recording = emu->disassembly_viewer().IsRecording(); + // if (ImGui::Checkbox(ICON_MD_FIBER_MANUAL_RECORD " Rec", &recording)) { + // emu->disassembly_viewer().SetRecording(recording); + // } + // if (ImGui::IsItemHovered()) { + // ImGui::SetTooltip("Record instructions to Disassembly Viewer\n(Lightweight - uses sparse address map)"); + // } + + ImGui::SameLine(); + + // Turbo mode + bool turbo = false; // Need to expose this from Emulator + if (ImGui::Checkbox(ICON_MD_FAST_FORWARD " Turbo", &turbo)) { + // emu->set_turbo_mode(turbo); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Fast forward (hold Tab)"); + } + + ImGui::SameLine(); + ImGui::Separator(); + ImGui::SameLine(); + + // FPS Counter with color coding + double fps = emu->GetCurrentFPS(); + ImVec4 fps_color; + if (fps >= 58.0) { + fps_color = ConvertColorToImVec4(theme.success); // Green for good FPS + } else if (fps >= 45.0) { + fps_color = ConvertColorToImVec4(theme.warning); // Yellow for okay FPS + } else { + fps_color = ConvertColorToImVec4(theme.error); // Red for bad FPS + } + + ImGui::TextColored(fps_color, ICON_MD_SPEED " %.1f FPS", fps); + + ImGui::SameLine(); + + // Audio backend status + if (emu->audio_backend()) { + auto audio_status = emu->audio_backend()->GetStatus(); + ImVec4 audio_color = audio_status.is_playing ? + ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.text_disabled); + + ImGui::TextColored(audio_color, ICON_MD_VOLUME_UP " %s | %u frames", + emu->audio_backend()->GetBackendName().c_str(), + audio_status.queued_frames); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Audio Backend: %s\nQueued: %u frames\nPlaying: %s", + emu->audio_backend()->GetBackendName().c_str(), + audio_status.queued_frames, + audio_status.is_playing ? "YES" : "NO"); + } + } else { + ImGui::TextColored(ConvertColorToImVec4(theme.error), + ICON_MD_VOLUME_OFF " No Backend"); + } + + ImGui::PopStyleColor(3); +} + +void RenderSnesPpu(Emulator* emu) { + if (!emu) return; + + auto& theme_manager = ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.editor_background)); + ImGui::BeginChild("##SNES_PPU", ImVec2(0, 0), true, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + + ImVec2 canvas_size = ImGui::GetContentRegionAvail(); + ImVec2 snes_size = ImVec2(512, 480); + + if (emu->is_snes_initialized() && emu->ppu_texture()) { + // Center the SNES display + float aspect = snes_size.x / snes_size.y; + float display_w = canvas_size.x; + float display_h = display_w / aspect; + + if (display_h > canvas_size.y) { + display_h = canvas_size.y; + display_w = display_h * aspect; + } + + float pos_x = (canvas_size.x - display_w) * 0.5f; + float pos_y = (canvas_size.y - display_h) * 0.5f; + + ImGui::SetCursorPos(ImVec2(pos_x, pos_y)); + + // Render PPU texture + ImGui::Image((ImTextureID)(intptr_t)emu->ppu_texture(), + ImVec2(display_w, display_h), + ImVec2(0, 0), ImVec2(1, 1)); + } else { + // Not initialized - show placeholder + ImVec2 text_size = ImGui::CalcTextSize("SNES PPU Output\n512x480"); + ImGui::SetCursorPos(ImVec2((canvas_size.x - text_size.x) * 0.5f, + (canvas_size.y - text_size.y) * 0.5f)); + ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), + ICON_MD_VIDEOGAME_ASSET "\nSNES PPU Output\n512x480"); + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); +} + +void RenderPerformanceMonitor(Emulator* emu) { + if (!emu) return; + + auto& theme_manager = ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##Performance", ImVec2(0, 0), true); + + ImGui::TextColored(ConvertColorToImVec4(theme.accent), + ICON_MD_SPEED " Performance Monitor"); + AddSectionSpacing(); + + auto metrics = emu->GetMetrics(); + + // FPS Graph + if (ImGui::CollapsingHeader(ICON_MD_SHOW_CHART " Frame Rate", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("Current: %.2f FPS", metrics.fps); + ImGui::Text("Target: %.2f FPS", emu->snes().memory().pal_timing() ? 50.0 : 60.0); + + // TODO: Add FPS graph with ImPlot + } + + // CPU Stats + if (ImGui::CollapsingHeader(ICON_MD_MEMORY " CPU Status", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("PC: $%02X:%04X", metrics.cpu_pb, metrics.cpu_pc); + ImGui::Text("Cycles: %llu", metrics.cycles); + } + + // Audio Stats + if (ImGui::CollapsingHeader(ICON_MD_AUDIOTRACK " Audio Status", ImGuiTreeNodeFlags_DefaultOpen)) { + if (emu->audio_backend()) { + auto audio_status = emu->audio_backend()->GetStatus(); + ImGui::Text("Backend: %s", emu->audio_backend()->GetBackendName().c_str()); + ImGui::Text("Queued: %u frames", audio_status.queued_frames); + ImGui::Text("Playing: %s", audio_status.is_playing ? "YES" : "NO"); + } else { + ImGui::TextColored(ConvertColorToImVec4(theme.error), "No audio backend"); + } + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); +} + +void RenderEmulatorInterface(Emulator* emu) { + if (!emu) return; + + auto& theme_manager = ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + // Main layout + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.window_bg)); + + RenderNavBar(emu); + + ImGui::Separator(); + + // Main content area + RenderSnesPpu(emu); + + ImGui::PopStyleColor(); +} + +} // namespace ui +} // namespace emu +} // namespace yaze + diff --git a/src/app/emu/video/ppu.cc b/src/app/emu/video/ppu.cc index cb246981..a0ee7486 100644 --- a/src/app/emu/video/ppu.cc +++ b/src/app/emu/video/ppu.cc @@ -214,22 +214,30 @@ void Ppu::HandlePixel(int x, int y) { // Two pixels per X position for hi-res support: // pixel1 at x*8 + 0..3, pixel2 at x*8 + 4..7 + // Apply brightness + r = r * brightness / 15; + g = g * brightness / 15; + b = b * brightness / 15; + r2 = r2 * brightness / 15; + g2 = g2 * brightness / 15; + b2 = b2 * brightness / 15; + // First pixel (hi-res/main screen) pixelBuffer[row * 2048 + x * 8 + 0 + pixelOutputFormat] = - ((b2 << 3) | (b2 >> 2)) * brightness / 15; // Blue channel + ((b2 << 3) | (b2 >> 2)); // Blue channel pixelBuffer[row * 2048 + x * 8 + 1 + pixelOutputFormat] = - ((g2 << 3) | (g2 >> 2)) * brightness / 15; // Green channel + ((g2 << 3) | (g2 >> 2)); // Green channel pixelBuffer[row * 2048 + x * 8 + 2 + pixelOutputFormat] = - ((r2 << 3) | (r2 >> 2)) * brightness / 15; // Red channel + ((r2 << 3) | (r2 >> 2)); // 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; // Blue channel + ((b << 3) | (b >> 2)); // Blue channel pixelBuffer[row * 2048 + x * 8 + 5 + pixelOutputFormat] = - ((g << 3) | (g >> 2)) * brightness / 15; // Green channel + ((g << 3) | (g >> 2)); // Green channel pixelBuffer[row * 2048 + x * 8 + 6 + pixelOutputFormat] = - ((r << 3) | (r >> 2)) * brightness / 15; // Red channel + ((r << 3) | (r >> 2)); // Red channel pixelBuffer[row * 2048 + x * 8 + 7 + pixelOutputFormat] = 0xFF; // Alpha (opaque) } diff --git a/src/app/gui/theme_manager.cc b/src/app/gui/theme_manager.cc index c140908d..80d5c14d 100644 --- a/src/app/gui/theme_manager.cc +++ b/src/app/gui/theme_manager.cc @@ -203,17 +203,18 @@ void ThemeManager::CreateFallbackYazeClassic() { theme.resize_grip_hovered = RGBA(199, 209, 255, 153); // 0.78f, 0.82f, 1.00f, 0.60f theme.resize_grip_active = RGBA(199, 209, 255, 230); // 0.78f, 0.82f, 1.00f, 0.90f - // Complete ImGui colors with smart defaults using accent colors - theme.check_mark = RGBA(66, 150, 250, 255); // Solid blue checkmark (visible!) - theme.slider_grab = RGBA(66, 150, 250, 200); // Blue slider grab - theme.slider_grab_active = RGBA(92, 115, 92, 255); // Solid green when active - theme.input_text_cursor = theme.text_primary; // Use primary text color - theme.nav_cursor = theme.accent; // Use accent color for navigation - theme.nav_windowing_highlight = theme.accent; // Accent for window switching - theme.nav_windowing_dim_bg = RGBA(0, 0, 0, 128); // Semi-transparent overlay - theme.modal_window_dim_bg = RGBA(0, 0, 0, 89); // 0.35f alpha - theme.text_selected_bg = RGBA(89, 119, 89, 89); // Accent color with transparency - theme.drag_drop_target = theme.accent; // Use accent for drag targets + // ENHANCED: Complete ImGui colors with theme-aware smart defaults + // Use theme colors instead of hardcoded values for consistency + theme.check_mark = RGBA(125, 255, 125, 255); // Bright green checkmark (highly visible!) + theme.slider_grab = RGBA(92, 115, 92, 255); // Theme green (solid) + theme.slider_grab_active = RGBA(125, 146, 125, 255); // Lighter green when active + theme.input_text_cursor = RGBA(255, 255, 255, 255); // White cursor (always visible) + theme.nav_cursor = RGBA(125, 146, 125, 255); // Light green for navigation + theme.nav_windowing_highlight = RGBA(89, 119, 89, 200); // Accent with high visibility + theme.nav_windowing_dim_bg = RGBA(0, 0, 0, 150); // Darker overlay for better contrast + theme.modal_window_dim_bg = RGBA(0, 0, 0, 128); // 50% alpha for modals + theme.text_selected_bg = RGBA(92, 115, 92, 128); // Theme green with 50% alpha (visible selection!) + theme.drag_drop_target = RGBA(125, 146, 125, 200); // Bright green for drop zones // Table colors (from original) theme.table_header_bg = RGBA(46, 66, 46); // alttpDarkGreen @@ -893,23 +894,39 @@ void ThemeManager::ApplyClassicYazeTheme() { classic_theme.scrollbar_grab_hovered = RGBA(92, 115, 92, 102); // 0.36f, 0.45f, 0.36f, 0.40f classic_theme.scrollbar_grab_active = RGBA(92, 115, 92, 153); // 0.36f, 0.45f, 0.36f, 0.60f - // Add all the missing colors that CreateFallbackYazeClassic has - classic_theme.frame_bg = classic_theme.window_bg; - classic_theme.frame_bg_hovered = classic_theme.button_hovered; - classic_theme.frame_bg_active = classic_theme.button_active; - classic_theme.resize_grip = RGBA(255, 255, 255, 26); - classic_theme.resize_grip_hovered = RGBA(199, 209, 255, 153); - classic_theme.resize_grip_active = RGBA(199, 209, 255, 230); - classic_theme.check_mark = RGBA(230, 230, 230, 128); - classic_theme.slider_grab = RGBA(255, 255, 255, 77); - classic_theme.slider_grab_active = RGBA(92, 115, 92, 153); - classic_theme.input_text_cursor = classic_theme.text_primary; - classic_theme.nav_cursor = classic_theme.accent; - classic_theme.nav_windowing_highlight = classic_theme.accent; - classic_theme.nav_windowing_dim_bg = RGBA(0, 0, 0, 128); - classic_theme.modal_window_dim_bg = RGBA(0, 0, 0, 89); - classic_theme.text_selected_bg = RGBA(89, 119, 89, 89); - classic_theme.drag_drop_target = classic_theme.accent; + // ENHANCED: Frame colors for inputs/widgets + classic_theme.frame_bg = RGBA(46, 66, 46, 140); // Darker green with some transparency + classic_theme.frame_bg_hovered = RGBA(71, 92, 71, 170); // Mid green when hovered + classic_theme.frame_bg_active = RGBA(92, 115, 92, 200); // Light green when active + + // FIXED: Resize grips with better visibility + classic_theme.resize_grip = RGBA(92, 115, 92, 80); // Theme green, subtle + classic_theme.resize_grip_hovered = RGBA(125, 146, 125, 180); // Brighter when hovered + classic_theme.resize_grip_active = RGBA(125, 146, 125, 255); // Solid when active + + // FIXED: Checkmark - bright green for high visibility! + classic_theme.check_mark = RGBA(125, 255, 125, 255); // Bright green (clearly visible) + + // FIXED: Sliders with theme colors + classic_theme.slider_grab = RGBA(92, 115, 92, 255); // Theme green (solid) + classic_theme.slider_grab_active = RGBA(125, 146, 125, 255); // Lighter when grabbed + + // FIXED: Input cursor - white for maximum visibility + classic_theme.input_text_cursor = RGBA(255, 255, 255, 255); // White cursor (always visible) + + // FIXED: Navigation with theme colors + classic_theme.nav_cursor = RGBA(125, 146, 125, 255); // Light green navigation + classic_theme.nav_windowing_highlight = RGBA(92, 115, 92, 200); // Theme green highlight + classic_theme.nav_windowing_dim_bg = RGBA(0, 0, 0, 150); // Darker overlay + + // FIXED: Modals with better dimming + classic_theme.modal_window_dim_bg = RGBA(0, 0, 0, 128); // 50% alpha + + // FIXED: Text selection - visible and theme-appropriate! + classic_theme.text_selected_bg = RGBA(92, 115, 92, 128); // Theme green with 50% alpha (visible!) + + // FIXED: Drag/drop target with high visibility + classic_theme.drag_drop_target = RGBA(125, 146, 125, 200); // Bright green classic_theme.table_header_bg = RGBA(46, 66, 46); classic_theme.table_border_strong = RGBA(71, 92, 71); classic_theme.table_border_light = RGBA(66, 66, 71); diff --git a/tools/test_helpers/dungeon_test_harness.cc b/tools/test_helpers/dungeon_test_harness.cc index 940022c9..e896608a 100644 --- a/tools/test_helpers/dungeon_test_harness.cc +++ b/tools/test_helpers/dungeon_test_harness.cc @@ -37,7 +37,7 @@ class DungeonTestHarness { auto& ppu = snes.ppu(); // Run emulator until the main game loop is reached - int max_cycles = 5000000; // 5 million cycles should be plenty + int max_cycles = 15000000; // 15 million cycles should be plenty int cycles = 0; while (cycles < max_cycles) { snes.RunCycle();