51 const auto& theme = theme_manager.GetCurrentTheme();
56 if (ImGui::Shortcut(ImGuiKey_Space, ImGuiInputFlags_RouteGlobal)) {
61 if (ImGui::Shortcut(ImGuiKey_F10, ImGuiInputFlags_RouteGlobal)) {
63 emu->
snes().RunFrame();
75 bool is_running = emu->
running();
77 if (ImGui::Button(
ICON_MD_PAUSE, ImVec2(50, kButtonHeight))) {
80 if (ImGui::IsItemHovered()) {
81 ImGui::SetTooltip(
"Pause emulation (Space)");
87 if (ImGui::IsItemHovered()) {
88 ImGui::SetTooltip(
"Start emulation (Space)");
97 emu->
snes().RunFrame();
100 if (ImGui::IsItemHovered()) {
101 ImGui::SetTooltip(
"Step one frame (F10)");
109 LOG_INFO(
"Emulator",
"System reset");
111 if (ImGui::IsItemHovered()) {
112 ImGui::SetTooltip(
"Reset SNES (Ctrl+R)");
119 ImVec2(110, kButtonHeight))) {
122 if (!rom_path.empty()) {
125 if (ext ==
".sfc" || ext ==
".smc" || ext ==
".SFC" || ext ==
".SMC") {
128 std::ifstream rom_file(rom_path, std::ios::binary);
129 if (rom_file.good()) {
130 std::vector<uint8_t> rom_data(
131 (std::istreambuf_iterator<char>(rom_file)),
132 std::istreambuf_iterator<char>());
136 if (!rom_data.empty()) {
138 LOG_INFO(
"Emulator",
"Loaded ROM: %s (%zu bytes)",
141 LOG_ERROR(
"Emulator",
"ROM file is empty: %s", rom_path.c_str());
144 LOG_ERROR(
"Emulator",
"Failed to open ROM file: %s",
147 }
catch (
const std::exception& e) {
148 LOG_ERROR(
"Emulator",
"Error loading ROM: %s", e.what());
152 "Invalid ROM file extension: %s (expected .sfc or .smc)",
157 if (ImGui::IsItemHovered()) {
159 "Load a different ROM file\n"
160 "Allows testing hacks with assembly patches applied");
172 if (ImGui::IsItemHovered()) {
173 ImGui::SetTooltip(
"Enable debugger features");
196 if (ImGui::IsItemHovered()) {
197 ImGui::SetTooltip(
"Fast forward (shortcut: hold Tab)");
209 }
else if (fps >= 45.0) {
215 ImGui::TextColored(fps_color,
ICON_MD_SPEED " %.1f FPS", fps);
228 audio_status.queued_frames);
230 if (ImGui::IsItemHovered()) {
231 ImGui::SetTooltip(
"Audio Backend: %s\nQueued: %u frames\nPlaying: %s",
233 audio_status.queued_frames,
234 audio_status.is_playing ?
"YES" :
"NO");
240 &use_sdl_audio_stream)) {
243 if (ImGui::IsItemHovered()) {
244 ImGui::SetTooltip(
"Use SDL audio stream for audio");
256 ImGuiIO& io = ImGui::GetIO();
258 if (io.WantCaptureKeyboard && !input_config.ignore_imgui_text_input) {
262 if (ImGui::IsItemHovered()) {
263 ImGui::SetTooltip(
"Keyboard captured by UI\nGame input disabled");
267 ImVec4 state_color = input_config.ignore_imgui_text_input
272 input_config.ignore_imgui_text_input
275 if (ImGui::IsItemHovered()) {
277 input_config.ignore_imgui_text_input
278 ?
"Game input forced on (ignores ImGui text capture)\nPress F1 "
280 :
"Game input active\nPress F1 for controls");
285 bool force_game_input = input_config.ignore_imgui_text_input;
286 if (ImGui::Checkbox(
"Force Game Input", &force_game_input)) {
287 auto cfg = input_config;
288 cfg.ignore_imgui_text_input = force_game_input;
291 if (ImGui::IsItemHovered()) {
293 "When enabled, emulator input is not blocked by ImGui text widgets.\n"
294 "Use if the game controls stop working while typing in other panels.");
300 static bool disable_nav =
false;
301 if (ImGui::Checkbox(
"Disable Nav", &disable_nav)) {
302 ImGuiIO& imgui_io = ImGui::GetIO();
304 imgui_io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard;
306 imgui_io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
309 if (ImGui::IsItemHovered()) {
311 "Disable ImGui keyboard navigation.\n"
312 "Prevents Tab from cycling through UI elements.\n"
313 "Enable this if Tab isn't working for turbo mode.");
323 const auto& theme = theme_manager.GetCurrentTheme();
326 "##SNES_PPU", ImVec2(0, 0),
328 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
330 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
331 ImVec2 snes_size = ImVec2(512, 480);
335 float aspect = snes_size.x / snes_size.y;
336 float display_w = canvas_size.x;
337 float display_h = display_w / aspect;
339 if (display_h > canvas_size.y) {
340 display_h = canvas_size.y;
341 display_w = display_h * aspect;
344 float pos_x = (canvas_size.x - display_w) * 0.5f;
345 float pos_y = (canvas_size.y - display_h) * 0.5f;
347 ImGui::SetCursorPos(ImVec2(pos_x, pos_y));
350 ImGui::Image((ImTextureID)(intptr_t)emu->
ppu_texture(),
351 ImVec2(display_w, display_h), ImVec2(0, 0), ImVec2(1, 1));
355 if (ImGui::IsItemHovered()) {
359 ImDrawList* draw_list = ImGui::GetWindowDrawList();
360 ImVec2 screen_pos = ImGui::GetItemRectMin();
361 ImVec2 screen_size = ImGui::GetItemRectMax();
363 screen_pos, screen_size,
369 ImVec2 text_size = ImGui::CalcTextSize(
"Load a ROM to start emulation");
370 ImGui::SetCursorPos(ImVec2((canvas_size.x - text_size.x) * 0.5f,
371 (canvas_size.y - text_size.y) * 0.5f - 20));
374 ImGui::SetCursorPosX((canvas_size.x - text_size.x) * 0.5f);
376 "Load a ROM to start emulation");
377 ImGui::SetCursorPosX(
378 (canvas_size.x - ImGui::CalcTextSize(
"512x480 SNES output").x) * 0.5f);
380 "512x480 SNES output");
390 const auto& theme = theme_manager.GetCurrentTheme();
404 ImGuiTreeNodeFlags_DefaultOpen)) {
405 ImGui::Text(
"Current: %.2f FPS", metrics.fps);
406 ImGui::Text(
"Target: %.2f FPS",
407 emu->
snes().memory().pal_timing() ? 50.0 : 60.0);
409 const float target_ms =
410 emu->
snes().memory().pal_timing() ? 1000.0f / 50.0f : 1000.0f / 60.0f;
413 if (!frame_ms.empty()) {
418 .flags = ImPlotFlags_NoLegend,
419 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
420 ImPlotAxisFlags_NoGridLines |
421 ImPlotAxisFlags_NoTickMarks,
422 .y_axis_flags = ImPlotAxisFlags_AutoFit};
425 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, target_ms * 2.5f,
427 ImPlot::PlotLine(
"Frame ms", frame_ms.data(),
428 static_cast<int>(frame_ms.size()));
429 ImPlot::PlotInfLines(
"Target", &target_ms, 1);
433 if (!fps_history.empty()) {
438 .flags = ImPlotFlags_NoLegend,
439 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
440 ImPlotAxisFlags_NoGridLines |
441 ImPlotAxisFlags_NoTickMarks,
442 .y_axis_flags = ImPlotAxisFlags_AutoFit};
445 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, 75.0f, ImGuiCond_Always);
446 ImPlot::PlotLine(
"FPS", fps_history.data(),
447 static_cast<int>(fps_history.size()));
448 const float target_fps = emu->
snes().memory().pal_timing() ? 50.0f
450 ImPlot::PlotInfLines(
"Target", &target_fps, 1);
456 ImGuiTreeNodeFlags_DefaultOpen)) {
459 if (!dma_hist.empty() || !vram_hist.empty()) {
462 .
id =
"DMA/VRAM Bytes",
463 .y_label =
"bytes/frame",
464 .flags = ImPlotFlags_NoLegend,
465 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
466 ImPlotAxisFlags_NoGridLines |
467 ImPlotAxisFlags_NoTickMarks,
468 .y_axis_flags = ImPlotAxisFlags_AutoFit};
472 float max_val = 512.0f;
473 if (!dma_hist.empty()) {
474 max_val = std::max(max_val,
475 *std::max_element(dma_hist.begin(), dma_hist.end()));
477 if (!vram_hist.empty()) {
478 max_val = std::max(max_val, *std::max_element(vram_hist.begin(),
482 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, max_val * 1.2f,
485 if (!dma_hist.empty()) {
486 ImPlot::PlotLine(
"DMA", dma_hist.data(),
487 static_cast<int>(dma_hist.size()));
489 if (!vram_hist.empty()) {
490 ImPlot::PlotLine(
"VRAM", vram_hist.data(),
491 static_cast<int>(vram_hist.size()));
495 ImGui::TextDisabled(
"No DMA activity recorded yet");
500 ImGuiTreeNodeFlags_DefaultOpen)) {
502 if (!free_bytes.empty()) {
505 .
id =
"ROM Free Bytes",
506 .y_label =
"bytes (0xFF)",
507 .flags = ImPlotFlags_NoLegend | ImPlotFlags_NoBoxSelect,
508 .x_axis_flags = ImPlotAxisFlags_AutoFit,
509 .y_axis_flags = ImPlotAxisFlags_AutoFit};
512 std::vector<double> x(free_bytes.size());
513 std::vector<double> y(free_bytes.size());
514 for (
size_t i = 0; i < free_bytes.size(); ++i) {
515 x[i] =
static_cast<double>(i);
516 y[i] =
static_cast<double>(free_bytes[i]);
518 ImPlot::PlotBars(
"Free", x.data(), y.data(),
519 static_cast<int>(free_bytes.size()), 0.67, 0.0,
520 ImPlotBarsFlags_None);
523 ImGui::TextDisabled(
"Load a ROM to analyze free space.");
529 ImGuiTreeNodeFlags_DefaultOpen)) {
530 ImGui::Text(
"PC: $%02X:%04X", metrics.cpu_pb, metrics.cpu_pc);
531 ImGui::Text(
"Cycles: %llu", metrics.cycles);
536 ImGuiTreeNodeFlags_DefaultOpen)) {
539 ImGui::Text(
"Backend: %s",
541 ImGui::Text(
"Queued: %u frames", audio_status.queued_frames);
542 ImGui::Text(
"Playing: %s", audio_status.is_playing ?
"YES" :
"NO");
545 if (!audio_history.empty()) {
548 .
id =
"Audio Queue Depth",
550 .flags = ImPlotFlags_NoLegend,
551 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
552 ImPlotAxisFlags_NoGridLines |
553 ImPlotAxisFlags_NoTickMarks,
554 .y_axis_flags = ImPlotAxisFlags_AutoFit};
557 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f,
559 *std::max_element(audio_history.begin(),
560 audio_history.end()) *
563 ImPlot::PlotLine(
"Queued", audio_history.data(),
564 static_cast<int>(audio_history.size()));
570 if (!audio_l.empty() || !audio_r.empty()) {
573 .
id =
"Audio Levels (RMS)",
574 .y_label =
"normalized",
575 .flags = ImPlotFlags_NoLegend,
576 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
577 ImPlotAxisFlags_NoGridLines |
578 ImPlotAxisFlags_NoTickMarks,
579 .y_axis_flags = ImPlotAxisFlags_AutoFit};
582 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, 1.0f, ImGuiCond_Always);
583 if (!audio_l.empty()) {
584 ImPlot::PlotLine(
"L", audio_l.data(),
585 static_cast<int>(audio_l.size()));
587 if (!audio_r.empty()) {
588 ImPlot::PlotLine(
"R", audio_r.data(),
589 static_cast<int>(audio_r.size()));
605 const auto& theme = theme_manager.GetCurrentTheme();
608 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
609 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
610 ImGui::SetNextWindowSize(ImVec2(550, 600), ImGuiCond_Appearing);
618 ImGuiWindowFlags_NoCollapse)) {
625 if (ImGui::BeginTable(
"EmulatorControls", 2, ImGuiTableFlags_Borders)) {
626 ImGui::TableSetupColumn(
"Key", ImGuiTableColumnFlags_WidthFixed, 120);
627 ImGui::TableSetupColumn(
"Action", ImGuiTableColumnFlags_WidthStretch);
628 ImGui::TableHeadersRow();
630 auto AddRow = [](
const char* key,
const char* action) {
631 ImGui::TableNextRow();
632 ImGui::TableNextColumn();
633 ImGui::Text(
"%s", key);
634 ImGui::TableNextColumn();
635 ImGui::Text(
"%s", action);
638 AddRow(
"Space",
"Play/Pause emulation");
639 AddRow(
"F10",
"Step one frame");
640 AddRow(
"Ctrl+R",
"Reset SNES");
641 AddRow(
"Tab (hold)",
"Turbo mode (fast forward)");
642 AddRow(
"F1",
"Show/hide this help");
656 if (ImGui::BeginTable(
"GameControls", 2, ImGuiTableFlags_Borders)) {
657 ImGui::TableSetupColumn(
"Key", ImGuiTableColumnFlags_WidthFixed, 120);
658 ImGui::TableSetupColumn(
"Button", ImGuiTableColumnFlags_WidthStretch);
659 ImGui::TableHeadersRow();
661 auto AddRow = [](
const char* key,
const char* button) {
662 ImGui::TableNextRow();
663 ImGui::TableNextColumn();
664 ImGui::Text(
"%s", key);
665 ImGui::TableNextColumn();
666 ImGui::Text(
"%s", button);
669 AddRow(
"Arrow Keys",
"D-Pad (Up/Down/Left/Right)");
670 AddRow(
"X",
"A Button");
671 AddRow(
"Z",
"B Button");
672 AddRow(
"S",
"X Button");
673 AddRow(
"A",
"Y Button");
674 AddRow(
"D",
"L Shoulder");
675 AddRow(
"C",
"R Shoulder");
676 AddRow(
"Enter",
"Start");
677 AddRow(
"RShift",
"Select");
690 ImGui::BulletText(
"Input is disabled when typing in UI fields");
691 ImGui::BulletText(
"Check the status bar for input capture state");
692 ImGui::BulletText(
"Click the game screen to ensure focus");
693 ImGui::BulletText(
"The emulator continues running in background");
698 if (ImGui::Button(
"Close", ImVec2(-1, 30))) {
743 const auto& theme = theme_manager.GetCurrentTheme();
746 "##VirtualController", ImVec2(0, 0),
753 "(Click to test input)");
761 static uint16_t virtual_buttons_pressed = 0;
765 ImVec2 size = ImVec2(50, 40)) {
766 ImGui::PushID(
static_cast<int>(button));
768 uint16_t button_mask = 1 <<
static_cast<uint8_t
>(button);
769 bool was_pressed = (virtual_buttons_pressed & button_mask) != 0;
772 std::optional<gui::StyleColorGuard> pressed_color;
774 pressed_color.emplace(ImGuiCol_Button,
779 ImGui::Button(label, size);
782 bool is_active = ImGui::IsItemActive();
786 virtual_buttons_pressed |= button_mask;
787 input_mgr.PressButton(button);
788 }
else if (was_pressed) {
790 virtual_buttons_pressed &= ~button_mask;
791 input_mgr.ReleaseButton(button);
798 ImGui::Text(
"D-Pad:");
802 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
808 ImGui::Dummy(ImVec2(50, 40));
813 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
820 ImGui::Text(
"Face Buttons:");
824 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
830 ImGui::Dummy(ImVec2(50, 40));
835 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
842 ImGui::Text(
"Shoulder:");
850 ImGui::Text(
"Start/Select:");
860 uint16_t input_state = emu->
snes().GetInput1State();
861 ImGui::Text(
"current_state: 0x%04X", input_state);
862 ImGui::Text(
"virtual_pressed: 0x%04X", virtual_buttons_pressed);
863 ImGui::Text(
"Input registered: %s", input_state != 0 ?
"YES" :
"NO");
866 if (input_state != 0) {
867 ImGui::Text(
"Buttons:");
868 if (input_state & 0x0001) ImGui::SameLine(), ImGui::Text(
"B");
869 if (input_state & 0x0002) ImGui::SameLine(), ImGui::Text(
"Y");
870 if (input_state & 0x0004) ImGui::SameLine(), ImGui::Text(
"Sel");
871 if (input_state & 0x0008) ImGui::SameLine(), ImGui::Text(
"Sta");
872 if (input_state & 0x0010) ImGui::SameLine(), ImGui::Text(
"Up");
873 if (input_state & 0x0020) ImGui::SameLine(), ImGui::Text(
"Dn");
874 if (input_state & 0x0040) ImGui::SameLine(), ImGui::Text(
"Lt");
875 if (input_state & 0x0080) ImGui::SameLine(), ImGui::Text(
"Rt");
876 if (input_state & 0x0100) ImGui::SameLine(), ImGui::Text(
"A");
877 if (input_state & 0x0200) ImGui::SameLine(), ImGui::Text(
"X");
878 if (input_state & 0x0400) ImGui::SameLine(), ImGui::Text(
"L");
879 if (input_state & 0x0800) ImGui::SameLine(), ImGui::Text(
"R");
885 auto& snes = emu->
snes();
886 uint16_t port_read = snes.GetPortAutoRead(0);
887 ImGui::Text(
"port_auto_read[0]: 0x%04X", port_read);
888 ImGui::Text(
"auto_joy_read: %s", snes.IsAutoJoyReadEnabled() ?
"ON" :
"OFF");
891 uint8_t reg_4218 = port_read & 0xFF;
892 uint8_t reg_4219 = (port_read >> 8) & 0xFF;
893 ImGui::Text(
"$4218: 0x%02X $4219: 0x%02X", reg_4218, reg_4219);
896 ImGui::Text(
"$4218 bits: A=%d X=%d L=%d R=%d",
897 (reg_4218 >> 7) & 1, (reg_4218 >> 6) & 1,
898 (reg_4218 >> 5) & 1, (reg_4218 >> 4) & 1);
901 static uint16_t last_port_read = 0;
902 static int frames_a_pressed = 0;
903 static int frames_since_release = 0;
904 static bool detected_edge =
false;
906 bool a_now = (port_read & 0x0080) != 0;
907 bool a_before = (last_port_read & 0x0080) != 0;
909 if (a_now && !a_before) {
910 detected_edge =
true;
911 frames_a_pressed = 1;
912 frames_since_release = 0;
915 detected_edge =
false;
917 frames_a_pressed = 0;
918 frames_since_release++;
919 detected_edge =
false;
922 last_port_read = port_read;
925 ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f),
"Edge Detection:");
926 ImGui::Text(
"A held for: %d frames", frames_a_pressed);
927 ImGui::Text(
"Released for: %d frames", frames_since_release);
929 ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f),
">>> EDGE DETECTED <<<");