50 const auto& theme = theme_manager.GetCurrentTheme();
55 if (ImGui::Shortcut(ImGuiKey_Space, ImGuiInputFlags_RouteGlobal)) {
60 if (ImGui::Shortcut(ImGuiKey_F10, ImGuiInputFlags_RouteGlobal)) {
62 emu->
snes().RunFrame();
68 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
70 ImGui::PushStyleColor(ImGuiCol_ButtonActive,
74 bool is_running = emu->
running();
76 if (ImGui::Button(
ICON_MD_PAUSE, ImVec2(50, kButtonHeight))) {
79 if (ImGui::IsItemHovered()) {
80 ImGui::SetTooltip(
"Pause emulation (Space)");
86 if (ImGui::IsItemHovered()) {
87 ImGui::SetTooltip(
"Start emulation (Space)");
96 emu->
snes().RunFrame();
99 if (ImGui::IsItemHovered()) {
100 ImGui::SetTooltip(
"Step one frame (F10)");
108 LOG_INFO(
"Emulator",
"System reset");
110 if (ImGui::IsItemHovered()) {
111 ImGui::SetTooltip(
"Reset SNES (Ctrl+R)");
118 ImVec2(110, kButtonHeight))) {
120 if (!rom_path.empty()) {
123 if (ext ==
".sfc" || ext ==
".smc" || ext ==
".SFC" || ext ==
".SMC") {
126 std::ifstream rom_file(rom_path, std::ios::binary);
127 if (rom_file.good()) {
128 std::vector<uint8_t> rom_data(
129 (std::istreambuf_iterator<char>(rom_file)),
130 std::istreambuf_iterator<char>());
134 if (!rom_data.empty()) {
136 LOG_INFO(
"Emulator",
"Loaded ROM: %s (%zu bytes)",
139 LOG_ERROR(
"Emulator",
"ROM file is empty: %s", rom_path.c_str());
142 LOG_ERROR(
"Emulator",
"Failed to open ROM file: %s",
145 }
catch (
const std::exception& e) {
146 LOG_ERROR(
"Emulator",
"Error loading ROM: %s", e.what());
150 "Invalid ROM file extension: %s (expected .sfc or .smc)",
155 if (ImGui::IsItemHovered()) {
157 "Load a different ROM file\n"
158 "Allows testing hacks with assembly patches applied");
170 if (ImGui::IsItemHovered()) {
171 ImGui::SetTooltip(
"Enable debugger features");
194 if (ImGui::IsItemHovered()) {
195 ImGui::SetTooltip(
"Fast forward (shortcut: hold Tab)");
207 }
else if (fps >= 45.0) {
213 ImGui::TextColored(fps_color,
ICON_MD_SPEED " %.1f FPS", fps);
226 audio_status.queued_frames);
228 if (ImGui::IsItemHovered()) {
229 ImGui::SetTooltip(
"Audio Backend: %s\nQueued: %u frames\nPlaying: %s",
231 audio_status.queued_frames,
232 audio_status.is_playing ?
"YES" :
"NO");
238 &use_sdl_audio_stream)) {
241 if (ImGui::IsItemHovered()) {
242 ImGui::SetTooltip(
"Use SDL audio stream for audio");
254 ImGuiIO& io = ImGui::GetIO();
256 if (io.WantCaptureKeyboard && !input_config.ignore_imgui_text_input) {
260 if (ImGui::IsItemHovered()) {
261 ImGui::SetTooltip(
"Keyboard captured by UI\nGame input disabled");
265 ImVec4 state_color = input_config.ignore_imgui_text_input
270 input_config.ignore_imgui_text_input
273 if (ImGui::IsItemHovered()) {
275 input_config.ignore_imgui_text_input
276 ?
"Game input forced on (ignores ImGui text capture)\nPress F1 "
278 :
"Game input active\nPress F1 for controls");
283 bool force_game_input = input_config.ignore_imgui_text_input;
284 if (ImGui::Checkbox(
"Force Game Input", &force_game_input)) {
285 auto cfg = input_config;
286 cfg.ignore_imgui_text_input = force_game_input;
289 if (ImGui::IsItemHovered()) {
291 "When enabled, emulator input is not blocked by ImGui text widgets.\n"
292 "Use if the game controls stop working while typing in other panels.");
298 static bool disable_nav =
false;
299 if (ImGui::Checkbox(
"Disable Nav", &disable_nav)) {
300 ImGuiIO& imgui_io = ImGui::GetIO();
302 imgui_io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard;
304 imgui_io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
307 if (ImGui::IsItemHovered()) {
309 "Disable ImGui keyboard navigation.\n"
310 "Prevents Tab from cycling through UI elements.\n"
311 "Enable this if Tab isn't working for turbo mode.");
314 ImGui::PopStyleColor(3);
322 const auto& theme = theme_manager.GetCurrentTheme();
324 ImGui::PushStyleColor(ImGuiCol_ChildBg,
327 "##SNES_PPU", ImVec2(0, 0),
true,
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");
384 ImGui::PopStyleColor();
392 const auto& theme = theme_manager.GetCurrentTheme();
395 ImGui::BeginChild(
"##Performance", ImVec2(0, 0),
true);
405 ImGuiTreeNodeFlags_DefaultOpen)) {
406 ImGui::Text(
"Current: %.2f FPS", metrics.fps);
407 ImGui::Text(
"Target: %.2f FPS",
408 emu->
snes().memory().pal_timing() ? 50.0 : 60.0);
410 const float target_ms =
411 emu->
snes().memory().pal_timing() ? 1000.0f / 50.0f : 1000.0f / 60.0f;
414 if (!frame_ms.empty()) {
419 .flags = ImPlotFlags_NoLegend,
420 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
421 ImPlotAxisFlags_NoGridLines |
422 ImPlotAxisFlags_NoTickMarks,
423 .y_axis_flags = ImPlotAxisFlags_AutoFit};
426 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, target_ms * 2.5f,
428 ImPlot::PlotLine(
"Frame ms", frame_ms.data(),
429 static_cast<int>(frame_ms.size()));
430 ImPlot::PlotInfLines(
"Target", &target_ms, 1);
434 if (!fps_history.empty()) {
439 .flags = ImPlotFlags_NoLegend,
440 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
441 ImPlotAxisFlags_NoGridLines |
442 ImPlotAxisFlags_NoTickMarks,
443 .y_axis_flags = ImPlotAxisFlags_AutoFit};
446 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, 75.0f, ImGuiCond_Always);
447 ImPlot::PlotLine(
"FPS", fps_history.data(),
448 static_cast<int>(fps_history.size()));
449 const float target_fps = emu->
snes().memory().pal_timing() ? 50.0f
451 ImPlot::PlotInfLines(
"Target", &target_fps, 1);
457 ImGuiTreeNodeFlags_DefaultOpen)) {
460 if (!dma_hist.empty() || !vram_hist.empty()) {
463 .
id =
"DMA/VRAM Bytes",
464 .y_label =
"bytes/frame",
465 .flags = ImPlotFlags_NoLegend,
466 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
467 ImPlotAxisFlags_NoGridLines |
468 ImPlotAxisFlags_NoTickMarks,
469 .y_axis_flags = ImPlotAxisFlags_AutoFit};
473 float max_val = 512.0f;
474 if (!dma_hist.empty()) {
475 max_val = std::max(max_val,
476 *std::max_element(dma_hist.begin(), dma_hist.end()));
478 if (!vram_hist.empty()) {
479 max_val = std::max(max_val, *std::max_element(vram_hist.begin(),
483 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, max_val * 1.2f,
486 if (!dma_hist.empty()) {
487 ImPlot::PlotLine(
"DMA", dma_hist.data(),
488 static_cast<int>(dma_hist.size()));
490 if (!vram_hist.empty()) {
491 ImPlot::PlotLine(
"VRAM", vram_hist.data(),
492 static_cast<int>(vram_hist.size()));
496 ImGui::TextDisabled(
"No DMA activity recorded yet");
501 ImGuiTreeNodeFlags_DefaultOpen)) {
503 if (!free_bytes.empty()) {
506 .
id =
"ROM Free Bytes",
507 .y_label =
"bytes (0xFF)",
508 .flags = ImPlotFlags_NoLegend | ImPlotFlags_NoBoxSelect,
509 .x_axis_flags = ImPlotAxisFlags_AutoFit,
510 .y_axis_flags = ImPlotAxisFlags_AutoFit};
513 std::vector<double> x(free_bytes.size());
514 std::vector<double> y(free_bytes.size());
515 for (
size_t i = 0; i < free_bytes.size(); ++i) {
516 x[i] =
static_cast<double>(i);
517 y[i] =
static_cast<double>(free_bytes[i]);
519 ImPlot::PlotBars(
"Free", x.data(), y.data(),
520 static_cast<int>(free_bytes.size()), 0.67, 0.0,
521 ImPlotBarsFlags_None);
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(ImAxis_Y1, 0.0f,
560 *std::max_element(audio_history.begin(),
561 audio_history.end()) *
564 ImPlot::PlotLine(
"Queued", audio_history.data(),
565 static_cast<int>(audio_history.size()));
571 if (!audio_l.empty() || !audio_r.empty()) {
574 .
id =
"Audio Levels (RMS)",
575 .y_label =
"normalized",
576 .flags = ImPlotFlags_NoLegend,
577 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
578 ImPlotAxisFlags_NoGridLines |
579 ImPlotAxisFlags_NoTickMarks,
580 .y_axis_flags = ImPlotAxisFlags_AutoFit};
583 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, 1.0f, ImGuiCond_Always);
584 if (!audio_l.empty()) {
585 ImPlot::PlotLine(
"L", audio_l.data(),
586 static_cast<int>(audio_l.size()));
588 if (!audio_r.empty()) {
589 ImPlot::PlotLine(
"R", audio_r.data(),
590 static_cast<int>(audio_r.size()));
600 ImGui::PopStyleColor();
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);
616 ImGui::PushStyleColor(ImGuiCol_TitleBgActive,
620 ImGuiWindowFlags_NoCollapse)) {
627 if (ImGui::BeginTable(
"EmulatorControls", 2, ImGuiTableFlags_Borders)) {
628 ImGui::TableSetupColumn(
"Key", ImGuiTableColumnFlags_WidthFixed, 120);
629 ImGui::TableSetupColumn(
"Action", ImGuiTableColumnFlags_WidthStretch);
630 ImGui::TableHeadersRow();
632 auto AddRow = [](
const char* key,
const char* action) {
633 ImGui::TableNextRow();
634 ImGui::TableNextColumn();
635 ImGui::Text(
"%s", key);
636 ImGui::TableNextColumn();
637 ImGui::Text(
"%s", action);
640 AddRow(
"Space",
"Play/Pause emulation");
641 AddRow(
"F10",
"Step one frame");
642 AddRow(
"Ctrl+R",
"Reset SNES");
643 AddRow(
"Tab (hold)",
"Turbo mode (fast forward)");
644 AddRow(
"F1",
"Show/hide this help");
658 if (ImGui::BeginTable(
"GameControls", 2, ImGuiTableFlags_Borders)) {
659 ImGui::TableSetupColumn(
"Key", ImGuiTableColumnFlags_WidthFixed, 120);
660 ImGui::TableSetupColumn(
"Button", ImGuiTableColumnFlags_WidthStretch);
661 ImGui::TableHeadersRow();
663 auto AddRow = [](
const char* key,
const char* button) {
664 ImGui::TableNextRow();
665 ImGui::TableNextColumn();
666 ImGui::Text(
"%s", key);
667 ImGui::TableNextColumn();
668 ImGui::Text(
"%s", button);
671 AddRow(
"Arrow Keys",
"D-Pad (Up/Down/Left/Right)");
672 AddRow(
"X",
"A Button");
673 AddRow(
"Z",
"B Button");
674 AddRow(
"S",
"X Button");
675 AddRow(
"A",
"Y Button");
676 AddRow(
"D",
"L Shoulder");
677 AddRow(
"C",
"R Shoulder");
678 AddRow(
"Enter",
"Start");
679 AddRow(
"RShift",
"Select");
692 ImGui::BulletText(
"Input is disabled when typing in UI fields");
693 ImGui::BulletText(
"Check the status bar for input capture state");
694 ImGui::BulletText(
"Click the game screen to ensure focus");
695 ImGui::BulletText(
"The emulator continues running in background");
700 if (ImGui::Button(
"Close", ImVec2(-1, 30))) {
706 ImGui::PopStyleColor(2);
749 const auto& theme = theme_manager.GetCurrentTheme();
752 ImGui::BeginChild(
"##VirtualController", ImVec2(0, 0),
true);
758 "(Click to test input)");
766 static uint16_t virtual_buttons_pressed = 0;
770 ImVec2 size = ImVec2(50, 40)) {
771 ImGui::PushID(
static_cast<int>(button));
773 uint16_t button_mask = 1 <<
static_cast<uint8_t
>(button);
774 bool was_pressed = (virtual_buttons_pressed & button_mask) != 0;
778 ImGui::PushStyleColor(ImGuiCol_Button,
783 ImGui::Button(label, size);
786 bool is_active = ImGui::IsItemActive();
790 virtual_buttons_pressed |= button_mask;
791 input_mgr.PressButton(button);
792 }
else if (was_pressed) {
794 virtual_buttons_pressed &= ~button_mask;
795 input_mgr.ReleaseButton(button);
799 ImGui::PopStyleColor();
806 ImGui::Text(
"D-Pad:");
810 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
816 ImGui::Dummy(ImVec2(50, 40));
821 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
828 ImGui::Text(
"Face Buttons:");
832 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
838 ImGui::Dummy(ImVec2(50, 40));
843 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
850 ImGui::Text(
"Shoulder:");
858 ImGui::Text(
"Start/Select:");
868 uint16_t input_state = emu->
snes().GetInput1State();
869 ImGui::Text(
"current_state: 0x%04X", input_state);
870 ImGui::Text(
"virtual_pressed: 0x%04X", virtual_buttons_pressed);
871 ImGui::Text(
"Input registered: %s", input_state != 0 ?
"YES" :
"NO");
874 if (input_state != 0) {
875 ImGui::Text(
"Buttons:");
876 if (input_state & 0x0001) ImGui::SameLine(), ImGui::Text(
"B");
877 if (input_state & 0x0002) ImGui::SameLine(), ImGui::Text(
"Y");
878 if (input_state & 0x0004) ImGui::SameLine(), ImGui::Text(
"Sel");
879 if (input_state & 0x0008) ImGui::SameLine(), ImGui::Text(
"Sta");
880 if (input_state & 0x0010) ImGui::SameLine(), ImGui::Text(
"Up");
881 if (input_state & 0x0020) ImGui::SameLine(), ImGui::Text(
"Dn");
882 if (input_state & 0x0040) ImGui::SameLine(), ImGui::Text(
"Lt");
883 if (input_state & 0x0080) ImGui::SameLine(), ImGui::Text(
"Rt");
884 if (input_state & 0x0100) ImGui::SameLine(), ImGui::Text(
"A");
885 if (input_state & 0x0200) ImGui::SameLine(), ImGui::Text(
"X");
886 if (input_state & 0x0400) ImGui::SameLine(), ImGui::Text(
"L");
887 if (input_state & 0x0800) ImGui::SameLine(), ImGui::Text(
"R");
893 auto& snes = emu->
snes();
894 uint16_t port_read = snes.GetPortAutoRead(0);
895 ImGui::Text(
"port_auto_read[0]: 0x%04X", port_read);
896 ImGui::Text(
"auto_joy_read: %s", snes.IsAutoJoyReadEnabled() ?
"ON" :
"OFF");
899 uint8_t reg_4218 = port_read & 0xFF;
900 uint8_t reg_4219 = (port_read >> 8) & 0xFF;
901 ImGui::Text(
"$4218: 0x%02X $4219: 0x%02X", reg_4218, reg_4219);
904 ImGui::Text(
"$4218 bits: A=%d X=%d L=%d R=%d",
905 (reg_4218 >> 7) & 1, (reg_4218 >> 6) & 1,
906 (reg_4218 >> 5) & 1, (reg_4218 >> 4) & 1);
909 static uint16_t last_port_read = 0;
910 static int frames_a_pressed = 0;
911 static int frames_since_release = 0;
912 static bool detected_edge =
false;
914 bool a_now = (port_read & 0x0080) != 0;
915 bool a_before = (last_port_read & 0x0080) != 0;
917 if (a_now && !a_before) {
918 detected_edge =
true;
919 frames_a_pressed = 1;
920 frames_since_release = 0;
923 detected_edge =
false;
925 frames_a_pressed = 0;
926 frames_since_release++;
927 detected_edge =
false;
930 last_port_read = port_read;
933 ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f),
"Edge Detection:");
934 ImGui::Text(
"A held for: %d frames", frames_a_pressed);
935 ImGui::Text(
"Released for: %d frames", frames_since_release);
937 ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f),
">>> EDGE DETECTED <<<");
941 ImGui::PopStyleColor();