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
270 ImGui::TextColored(state_color, input_config.ignore_imgui_text_input
274 if (ImGui::IsItemHovered()) {
276 input_config.ignore_imgui_text_input
277 ?
"Game input forced on (ignores ImGui text capture)\nPress F1 "
279 :
"Game input active\nPress F1 for controls");
284 bool force_game_input = input_config.ignore_imgui_text_input;
285 if (ImGui::Checkbox(
"Force Game Input", &force_game_input)) {
286 auto cfg = input_config;
287 cfg.ignore_imgui_text_input = force_game_input;
290 if (ImGui::IsItemHovered()) {
292 "When enabled, emulator input is not blocked by ImGui text widgets.\n"
293 "Use if the game controls stop working while typing in other panels.");
299 static bool disable_nav =
false;
300 if (ImGui::Checkbox(
"Disable Nav", &disable_nav)) {
301 ImGuiIO& imgui_io = ImGui::GetIO();
303 imgui_io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard;
305 imgui_io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
308 if (ImGui::IsItemHovered()) {
310 "Disable ImGui keyboard navigation.\n"
311 "Prevents Tab from cycling through UI elements.\n"
312 "Enable this if Tab isn't working for turbo mode.");
321 const auto& theme = theme_manager.GetCurrentTheme();
324 "##SNES_PPU", ImVec2(0, 0),
326 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
328 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
329 ImVec2 snes_size = ImVec2(512, 480);
333 float aspect = snes_size.x / snes_size.y;
334 float display_w = canvas_size.x;
335 float display_h = display_w / aspect;
337 if (display_h > canvas_size.y) {
338 display_h = canvas_size.y;
339 display_w = display_h * aspect;
342 float pos_x = (canvas_size.x - display_w) * 0.5f;
343 float pos_y = (canvas_size.y - display_h) * 0.5f;
345 ImGui::SetCursorPos(ImVec2(pos_x, pos_y));
348 ImGui::Image((ImTextureID)(intptr_t)emu->
ppu_texture(),
349 ImVec2(display_w, display_h), ImVec2(0, 0), ImVec2(1, 1));
353 if (ImGui::IsItemHovered()) {
357 ImDrawList* draw_list = ImGui::GetWindowDrawList();
358 ImVec2 screen_pos = ImGui::GetItemRectMin();
359 ImVec2 screen_size = ImGui::GetItemRectMax();
361 screen_pos, screen_size,
367 ImVec2 text_size = ImGui::CalcTextSize(
"Load a ROM to start emulation");
368 ImGui::SetCursorPos(ImVec2((canvas_size.x - text_size.x) * 0.5f,
369 (canvas_size.y - text_size.y) * 0.5f - 20));
372 ImGui::SetCursorPosX((canvas_size.x - text_size.x) * 0.5f);
374 "Load a ROM to start emulation");
375 ImGui::SetCursorPosX(
376 (canvas_size.x - ImGui::CalcTextSize(
"512x480 SNES output").x) * 0.5f);
378 "512x480 SNES output");
387 const auto& theme = theme_manager.GetCurrentTheme();
401 ImGuiTreeNodeFlags_DefaultOpen)) {
402 ImGui::Text(
"Current: %.2f FPS", metrics.fps);
403 ImGui::Text(
"Target: %.2f FPS",
404 emu->
snes().memory().pal_timing() ? 50.0 : 60.0);
406 const float target_ms =
407 emu->
snes().memory().pal_timing() ? 1000.0f / 50.0f : 1000.0f / 60.0f;
410 if (!frame_ms.empty()) {
414 .flags = ImPlotFlags_NoLegend,
415 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
416 ImPlotAxisFlags_NoGridLines |
417 ImPlotAxisFlags_NoTickMarks,
418 .y_axis_flags = ImPlotAxisFlags_AutoFit};
421 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, target_ms * 2.5f,
423 ImPlot::PlotLine(
"Frame ms", frame_ms.data(),
424 static_cast<int>(frame_ms.size()), 1.0, 0.0,
426 ImPlot::PlotInfLines(
"Target", &target_ms, 1, plot_style.
item_spec());
430 if (!fps_history.empty()) {
435 .flags = ImPlotFlags_NoLegend,
436 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
437 ImPlotAxisFlags_NoGridLines |
438 ImPlotAxisFlags_NoTickMarks,
439 .y_axis_flags = ImPlotAxisFlags_AutoFit};
442 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, 75.0f, ImGuiCond_Always);
443 ImPlot::PlotLine(
"FPS", fps_history.data(),
444 static_cast<int>(fps_history.size()), 1.0, 0.0,
446 const float target_fps =
447 emu->
snes().memory().pal_timing() ? 50.0f : 60.0f;
448 ImPlot::PlotInfLines(
"Target", &target_fps, 1, plot_style.
item_spec());
454 ImGuiTreeNodeFlags_DefaultOpen)) {
457 if (!dma_hist.empty() || !vram_hist.empty()) {
460 .
id =
"DMA/VRAM Bytes",
461 .y_label =
"bytes/frame",
462 .flags = ImPlotFlags_None,
463 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
464 ImPlotAxisFlags_NoGridLines |
465 ImPlotAxisFlags_NoTickMarks,
466 .y_axis_flags = ImPlotAxisFlags_AutoFit};
470 float max_val = 512.0f;
471 if (!dma_hist.empty()) {
473 max_val, *std::max_element(dma_hist.begin(), dma_hist.end()));
475 if (!vram_hist.empty()) {
477 max_val, *std::max_element(vram_hist.begin(), vram_hist.end()));
480 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, max_val * 1.2f,
483 if (!dma_hist.empty()) {
484 ImPlot::PlotLine(
"DMA", dma_hist.data(),
485 static_cast<int>(dma_hist.size()), 1.0, 0.0,
488 if (!vram_hist.empty()) {
489 ImPlot::PlotLine(
"VRAM", vram_hist.data(),
490 static_cast<int>(vram_hist.size()), 1.0, 0.0,
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 ImPlotSpec free_spec = plot_style.
item_spec();
520 ImPlot::PlotBars(
"Free", x.data(), y.data(),
521 static_cast<int>(free_bytes.size()), 0.67, free_spec);
524 ImGui::TextDisabled(
"Load a ROM to analyze free space.");
530 ImGuiTreeNodeFlags_DefaultOpen)) {
531 ImGui::Text(
"PC: $%02X:%04X", metrics.cpu_pb, metrics.cpu_pc);
532 ImGui::Text(
"Cycles: %llu", metrics.cycles);
537 ImGuiTreeNodeFlags_DefaultOpen)) {
540 ImGui::Text(
"Backend: %s",
542 ImGui::Text(
"Queued: %u frames", audio_status.queued_frames);
543 ImGui::Text(
"Playing: %s", audio_status.is_playing ?
"YES" :
"NO");
546 if (!audio_history.empty()) {
549 .
id =
"Audio Queue Depth",
551 .flags = ImPlotFlags_NoLegend,
552 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
553 ImPlotAxisFlags_NoGridLines |
554 ImPlotAxisFlags_NoTickMarks,
555 .y_axis_flags = ImPlotAxisFlags_AutoFit};
558 ImPlot::SetupAxisLimits(
560 std::max(512.0f, *std::max_element(audio_history.begin(),
561 audio_history.end()) *
564 ImPlot::PlotLine(
"Queued", audio_history.data(),
565 static_cast<int>(audio_history.size()), 1.0, 0.0,
572 if (!audio_l.empty() || !audio_r.empty()) {
575 .
id =
"Audio Levels (RMS)",
576 .y_label =
"normalized",
577 .flags = ImPlotFlags_None,
578 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
579 ImPlotAxisFlags_NoGridLines |
580 ImPlotAxisFlags_NoTickMarks,
581 .y_axis_flags = ImPlotAxisFlags_AutoFit};
584 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, 1.0f, ImGuiCond_Always);
585 if (!audio_l.empty()) {
586 ImPlot::PlotLine(
"L", audio_l.data(),
587 static_cast<int>(audio_l.size()), 1.0, 0.0,
590 if (!audio_r.empty()) {
591 ImPlot::PlotLine(
"R", audio_r.data(),
592 static_cast<int>(audio_r.size()), 1.0, 0.0,
608 const auto& theme = theme_manager.GetCurrentTheme();
611 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
612 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
613 ImGui::SetNextWindowSize(ImVec2(550, 600), ImGuiCond_Appearing);
621 ImGuiWindowFlags_NoCollapse)) {
628 if (ImGui::BeginTable(
"EmulatorControls", 2, ImGuiTableFlags_Borders)) {
629 ImGui::TableSetupColumn(
"Key", ImGuiTableColumnFlags_WidthFixed, 120);
630 ImGui::TableSetupColumn(
"Action", ImGuiTableColumnFlags_WidthStretch);
631 ImGui::TableHeadersRow();
633 auto AddRow = [](
const char* key,
const char* action) {
634 ImGui::TableNextRow();
635 ImGui::TableNextColumn();
636 ImGui::Text(
"%s", key);
637 ImGui::TableNextColumn();
638 ImGui::Text(
"%s", action);
641 AddRow(
"Space",
"Play/Pause emulation");
642 AddRow(
"F10",
"Step one frame");
643 AddRow(
"Ctrl+R",
"Reset SNES");
644 AddRow(
"Tab (hold)",
"Turbo mode (fast forward)");
645 AddRow(
"F1",
"Show/hide this help");
659 if (ImGui::BeginTable(
"GameControls", 2, ImGuiTableFlags_Borders)) {
660 ImGui::TableSetupColumn(
"Key", ImGuiTableColumnFlags_WidthFixed, 120);
661 ImGui::TableSetupColumn(
"Button", ImGuiTableColumnFlags_WidthStretch);
662 ImGui::TableHeadersRow();
664 auto AddRow = [](
const char* key,
const char* button) {
665 ImGui::TableNextRow();
666 ImGui::TableNextColumn();
667 ImGui::Text(
"%s", key);
668 ImGui::TableNextColumn();
669 ImGui::Text(
"%s", button);
672 AddRow(
"Arrow Keys",
"D-Pad (Up/Down/Left/Right)");
673 AddRow(
"X",
"A Button");
674 AddRow(
"Z",
"B Button");
675 AddRow(
"S",
"X Button");
676 AddRow(
"A",
"Y Button");
677 AddRow(
"D",
"L Shoulder");
678 AddRow(
"C",
"R Shoulder");
679 AddRow(
"Enter",
"Start");
680 AddRow(
"RShift",
"Select");
693 ImGui::BulletText(
"Input is disabled when typing in UI fields");
694 ImGui::BulletText(
"Check the status bar for input capture state");
695 ImGui::BulletText(
"Click the game screen to ensure focus");
696 ImGui::BulletText(
"The emulator continues running in background");
701 if (ImGui::Button(
"Close", ImVec2(-1, 30))) {
746 const auto& theme = theme_manager.GetCurrentTheme();
749 "##VirtualController", ImVec2(0, 0),
756 "(Click to test input)");
764 static uint16_t virtual_buttons_pressed = 0;
768 ImVec2 size = ImVec2(50, 40)) {
769 ImGui::PushID(
static_cast<int>(button));
771 uint16_t button_mask = 1 <<
static_cast<uint8_t
>(button);
772 bool was_pressed = (virtual_buttons_pressed & button_mask) != 0;
775 std::optional<gui::StyleColorGuard> pressed_color;
777 pressed_color.emplace(ImGuiCol_Button,
782 ImGui::Button(label, size);
785 bool is_active = ImGui::IsItemActive();
789 virtual_buttons_pressed |= button_mask;
790 input_mgr.PressButton(button);
791 }
else if (was_pressed) {
793 virtual_buttons_pressed &= ~button_mask;
794 input_mgr.ReleaseButton(button);
801 ImGui::Text(
"D-Pad:");
805 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
811 ImGui::Dummy(ImVec2(50, 40));
816 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
823 ImGui::Text(
"Face Buttons:");
827 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
833 ImGui::Dummy(ImVec2(50, 40));
838 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
845 ImGui::Text(
"Shoulder:");
853 ImGui::Text(
"Start/Select:");
863 uint16_t input_state = emu->
snes().GetInput1State();
864 ImGui::Text(
"current_state: 0x%04X", input_state);
865 ImGui::Text(
"virtual_pressed: 0x%04X", virtual_buttons_pressed);
866 ImGui::Text(
"Input registered: %s", input_state != 0 ?
"YES" :
"NO");
869 if (input_state != 0) {
870 ImGui::Text(
"Buttons:");
871 if (input_state & 0x0001)
872 ImGui::SameLine(), ImGui::Text(
"B");
873 if (input_state & 0x0002)
874 ImGui::SameLine(), ImGui::Text(
"Y");
875 if (input_state & 0x0004)
876 ImGui::SameLine(), ImGui::Text(
"Sel");
877 if (input_state & 0x0008)
878 ImGui::SameLine(), ImGui::Text(
"Sta");
879 if (input_state & 0x0010)
880 ImGui::SameLine(), ImGui::Text(
"Up");
881 if (input_state & 0x0020)
882 ImGui::SameLine(), ImGui::Text(
"Dn");
883 if (input_state & 0x0040)
884 ImGui::SameLine(), ImGui::Text(
"Lt");
885 if (input_state & 0x0080)
886 ImGui::SameLine(), ImGui::Text(
"Rt");
887 if (input_state & 0x0100)
888 ImGui::SameLine(), ImGui::Text(
"A");
889 if (input_state & 0x0200)
890 ImGui::SameLine(), ImGui::Text(
"X");
891 if (input_state & 0x0400)
892 ImGui::SameLine(), ImGui::Text(
"L");
893 if (input_state & 0x0800)
894 ImGui::SameLine(), ImGui::Text(
"R");
900 auto& snes = emu->
snes();
901 uint16_t port_read = snes.GetPortAutoRead(0);
902 ImGui::Text(
"port_auto_read[0]: 0x%04X", port_read);
903 ImGui::Text(
"auto_joy_read: %s", snes.IsAutoJoyReadEnabled() ?
"ON" :
"OFF");
906 uint8_t reg_4218 = port_read & 0xFF;
907 uint8_t reg_4219 = (port_read >> 8) & 0xFF;
908 ImGui::Text(
"$4218: 0x%02X $4219: 0x%02X", reg_4218, reg_4219);
911 ImGui::Text(
"$4218 bits: A=%d X=%d L=%d R=%d", (reg_4218 >> 7) & 1,
912 (reg_4218 >> 6) & 1, (reg_4218 >> 5) & 1, (reg_4218 >> 4) & 1);
915 static uint16_t last_port_read = 0;
916 static int frames_a_pressed = 0;
917 static int frames_since_release = 0;
918 static bool detected_edge =
false;
920 bool a_now = (port_read & 0x0080) != 0;
921 bool a_before = (last_port_read & 0x0080) != 0;
923 if (a_now && !a_before) {
924 detected_edge =
true;
925 frames_a_pressed = 1;
926 frames_since_release = 0;
929 detected_edge =
false;
931 frames_a_pressed = 0;
932 frames_since_release++;
933 detected_edge =
false;
936 last_port_read = port_read;
939 ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f),
"Edge Detection:");
940 ImGui::Text(
"A held for: %d frames", frames_a_pressed);
941 ImGui::Text(
"Released for: %d frames", frames_since_release);
943 ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f),
">>> EDGE DETECTED <<<");