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))) {
121 if (!rom_path.empty()) {
124 if (ext ==
".sfc" || ext ==
".smc" || ext ==
".SFC" || ext ==
".SMC") {
127 std::ifstream rom_file(rom_path, std::ios::binary);
128 if (rom_file.good()) {
129 std::vector<uint8_t> rom_data(
130 (std::istreambuf_iterator<char>(rom_file)),
131 std::istreambuf_iterator<char>());
135 if (!rom_data.empty()) {
137 LOG_INFO(
"Emulator",
"Loaded ROM: %s (%zu bytes)",
140 LOG_ERROR(
"Emulator",
"ROM file is empty: %s", rom_path.c_str());
143 LOG_ERROR(
"Emulator",
"Failed to open ROM file: %s",
146 }
catch (
const std::exception& e) {
147 LOG_ERROR(
"Emulator",
"Error loading ROM: %s", e.what());
151 "Invalid ROM file extension: %s (expected .sfc or .smc)",
156 if (ImGui::IsItemHovered()) {
158 "Load a different ROM file\n"
159 "Allows testing hacks with assembly patches applied");
171 if (ImGui::IsItemHovered()) {
172 ImGui::SetTooltip(
"Enable debugger features");
195 if (ImGui::IsItemHovered()) {
196 ImGui::SetTooltip(
"Fast forward (shortcut: hold Tab)");
208 }
else if (fps >= 45.0) {
214 ImGui::TextColored(fps_color,
ICON_MD_SPEED " %.1f FPS", fps);
227 audio_status.queued_frames);
229 if (ImGui::IsItemHovered()) {
230 ImGui::SetTooltip(
"Audio Backend: %s\nQueued: %u frames\nPlaying: %s",
232 audio_status.queued_frames,
233 audio_status.is_playing ?
"YES" :
"NO");
239 &use_sdl_audio_stream)) {
242 if (ImGui::IsItemHovered()) {
243 ImGui::SetTooltip(
"Use SDL audio stream for audio");
255 ImGuiIO& io = ImGui::GetIO();
257 if (io.WantCaptureKeyboard && !input_config.ignore_imgui_text_input) {
261 if (ImGui::IsItemHovered()) {
262 ImGui::SetTooltip(
"Keyboard captured by UI\nGame input disabled");
266 ImVec4 state_color = input_config.ignore_imgui_text_input
271 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.");
315 ImGui::PopStyleColor(3);
323 const auto& theme = theme_manager.GetCurrentTheme();
325 ImGui::PushStyleColor(ImGuiCol_ChildBg,
328 "##SNES_PPU", ImVec2(0, 0),
true,
329 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
331 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
332 ImVec2 snes_size = ImVec2(512, 480);
336 float aspect = snes_size.x / snes_size.y;
337 float display_w = canvas_size.x;
338 float display_h = display_w / aspect;
340 if (display_h > canvas_size.y) {
341 display_h = canvas_size.y;
342 display_w = display_h * aspect;
345 float pos_x = (canvas_size.x - display_w) * 0.5f;
346 float pos_y = (canvas_size.y - display_h) * 0.5f;
348 ImGui::SetCursorPos(ImVec2(pos_x, pos_y));
351 ImGui::Image((ImTextureID)(intptr_t)emu->
ppu_texture(),
352 ImVec2(display_w, display_h), ImVec2(0, 0), ImVec2(1, 1));
356 if (ImGui::IsItemHovered()) {
360 ImDrawList* draw_list = ImGui::GetWindowDrawList();
361 ImVec2 screen_pos = ImGui::GetItemRectMin();
362 ImVec2 screen_size = ImGui::GetItemRectMax();
364 screen_pos, screen_size,
370 ImVec2 text_size = ImGui::CalcTextSize(
"Load a ROM to start emulation");
371 ImGui::SetCursorPos(ImVec2((canvas_size.x - text_size.x) * 0.5f,
372 (canvas_size.y - text_size.y) * 0.5f - 20));
375 ImGui::SetCursorPosX((canvas_size.x - text_size.x) * 0.5f);
377 "Load a ROM to start emulation");
378 ImGui::SetCursorPosX(
379 (canvas_size.x - ImGui::CalcTextSize(
"512x480 SNES output").x) * 0.5f);
381 "512x480 SNES output");
385 ImGui::PopStyleColor();
393 const auto& theme = theme_manager.GetCurrentTheme();
396 ImGui::BeginChild(
"##Performance", ImVec2(0, 0),
true);
406 ImGuiTreeNodeFlags_DefaultOpen)) {
407 ImGui::Text(
"Current: %.2f FPS", metrics.fps);
408 ImGui::Text(
"Target: %.2f FPS",
409 emu->
snes().memory().pal_timing() ? 50.0 : 60.0);
411 const float target_ms =
412 emu->
snes().memory().pal_timing() ? 1000.0f / 50.0f : 1000.0f / 60.0f;
415 if (!frame_ms.empty()) {
420 .flags = ImPlotFlags_NoLegend,
421 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
422 ImPlotAxisFlags_NoGridLines |
423 ImPlotAxisFlags_NoTickMarks,
424 .y_axis_flags = ImPlotAxisFlags_AutoFit};
427 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, target_ms * 2.5f,
429 ImPlot::PlotLine(
"Frame ms", frame_ms.data(),
430 static_cast<int>(frame_ms.size()));
431 ImPlot::PlotInfLines(
"Target", &target_ms, 1);
435 if (!fps_history.empty()) {
440 .flags = ImPlotFlags_NoLegend,
441 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
442 ImPlotAxisFlags_NoGridLines |
443 ImPlotAxisFlags_NoTickMarks,
444 .y_axis_flags = ImPlotAxisFlags_AutoFit};
447 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, 75.0f, ImGuiCond_Always);
448 ImPlot::PlotLine(
"FPS", fps_history.data(),
449 static_cast<int>(fps_history.size()));
450 const float target_fps = emu->
snes().memory().pal_timing() ? 50.0f
452 ImPlot::PlotInfLines(
"Target", &target_fps, 1);
458 ImGuiTreeNodeFlags_DefaultOpen)) {
461 if (!dma_hist.empty() || !vram_hist.empty()) {
464 .
id =
"DMA/VRAM Bytes",
465 .y_label =
"bytes/frame",
466 .flags = ImPlotFlags_NoLegend,
467 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
468 ImPlotAxisFlags_NoGridLines |
469 ImPlotAxisFlags_NoTickMarks,
470 .y_axis_flags = ImPlotAxisFlags_AutoFit};
474 float max_val = 512.0f;
475 if (!dma_hist.empty()) {
476 max_val = std::max(max_val,
477 *std::max_element(dma_hist.begin(), dma_hist.end()));
479 if (!vram_hist.empty()) {
480 max_val = std::max(max_val, *std::max_element(vram_hist.begin(),
484 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, max_val * 1.2f,
487 if (!dma_hist.empty()) {
488 ImPlot::PlotLine(
"DMA", dma_hist.data(),
489 static_cast<int>(dma_hist.size()));
491 if (!vram_hist.empty()) {
492 ImPlot::PlotLine(
"VRAM", vram_hist.data(),
493 static_cast<int>(vram_hist.size()));
497 ImGui::TextDisabled(
"No DMA activity recorded yet");
502 ImGuiTreeNodeFlags_DefaultOpen)) {
504 if (!free_bytes.empty()) {
507 .
id =
"ROM Free Bytes",
508 .y_label =
"bytes (0xFF)",
509 .flags = ImPlotFlags_NoLegend | ImPlotFlags_NoBoxSelect,
510 .x_axis_flags = ImPlotAxisFlags_AutoFit,
511 .y_axis_flags = ImPlotAxisFlags_AutoFit};
514 std::vector<double> x(free_bytes.size());
515 std::vector<double> y(free_bytes.size());
516 for (
size_t i = 0; i < free_bytes.size(); ++i) {
517 x[i] =
static_cast<double>(i);
518 y[i] =
static_cast<double>(free_bytes[i]);
520 ImPlot::PlotBars(
"Free", x.data(), y.data(),
521 static_cast<int>(free_bytes.size()), 0.67, 0.0,
522 ImPlotBarsFlags_None);
525 ImGui::TextDisabled(
"Load a ROM to analyze free space.");
531 ImGuiTreeNodeFlags_DefaultOpen)) {
532 ImGui::Text(
"PC: $%02X:%04X", metrics.cpu_pb, metrics.cpu_pc);
533 ImGui::Text(
"Cycles: %llu", metrics.cycles);
538 ImGuiTreeNodeFlags_DefaultOpen)) {
541 ImGui::Text(
"Backend: %s",
543 ImGui::Text(
"Queued: %u frames", audio_status.queued_frames);
544 ImGui::Text(
"Playing: %s", audio_status.is_playing ?
"YES" :
"NO");
547 if (!audio_history.empty()) {
550 .
id =
"Audio Queue Depth",
552 .flags = ImPlotFlags_NoLegend,
553 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
554 ImPlotAxisFlags_NoGridLines |
555 ImPlotAxisFlags_NoTickMarks,
556 .y_axis_flags = ImPlotAxisFlags_AutoFit};
559 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f,
561 *std::max_element(audio_history.begin(),
562 audio_history.end()) *
565 ImPlot::PlotLine(
"Queued", audio_history.data(),
566 static_cast<int>(audio_history.size()));
572 if (!audio_l.empty() || !audio_r.empty()) {
575 .
id =
"Audio Levels (RMS)",
576 .y_label =
"normalized",
577 .flags = ImPlotFlags_NoLegend,
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()));
589 if (!audio_r.empty()) {
590 ImPlot::PlotLine(
"R", audio_r.data(),
591 static_cast<int>(audio_r.size()));
601 ImGui::PopStyleColor();
609 const auto& theme = theme_manager.GetCurrentTheme();
612 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
613 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
614 ImGui::SetNextWindowSize(ImVec2(550, 600), ImGuiCond_Appearing);
617 ImGui::PushStyleColor(ImGuiCol_TitleBgActive,
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))) {
707 ImGui::PopStyleColor(2);
750 const auto& theme = theme_manager.GetCurrentTheme();
753 ImGui::BeginChild(
"##VirtualController", ImVec2(0, 0),
true);
759 "(Click to test input)");
767 static uint16_t virtual_buttons_pressed = 0;
771 ImVec2 size = ImVec2(50, 40)) {
772 ImGui::PushID(
static_cast<int>(button));
774 uint16_t button_mask = 1 <<
static_cast<uint8_t
>(button);
775 bool was_pressed = (virtual_buttons_pressed & button_mask) != 0;
779 ImGui::PushStyleColor(ImGuiCol_Button,
784 ImGui::Button(label, size);
787 bool is_active = ImGui::IsItemActive();
791 virtual_buttons_pressed |= button_mask;
792 input_mgr.PressButton(button);
793 }
else if (was_pressed) {
795 virtual_buttons_pressed &= ~button_mask;
796 input_mgr.ReleaseButton(button);
800 ImGui::PopStyleColor();
807 ImGui::Text(
"D-Pad:");
811 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
817 ImGui::Dummy(ImVec2(50, 40));
822 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
829 ImGui::Text(
"Face Buttons:");
833 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
839 ImGui::Dummy(ImVec2(50, 40));
844 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
851 ImGui::Text(
"Shoulder:");
859 ImGui::Text(
"Start/Select:");
869 uint16_t input_state = emu->
snes().GetInput1State();
870 ImGui::Text(
"current_state: 0x%04X", input_state);
871 ImGui::Text(
"virtual_pressed: 0x%04X", virtual_buttons_pressed);
872 ImGui::Text(
"Input registered: %s", input_state != 0 ?
"YES" :
"NO");
875 if (input_state != 0) {
876 ImGui::Text(
"Buttons:");
877 if (input_state & 0x0001) ImGui::SameLine(), ImGui::Text(
"B");
878 if (input_state & 0x0002) ImGui::SameLine(), ImGui::Text(
"Y");
879 if (input_state & 0x0004) ImGui::SameLine(), ImGui::Text(
"Sel");
880 if (input_state & 0x0008) ImGui::SameLine(), ImGui::Text(
"Sta");
881 if (input_state & 0x0010) ImGui::SameLine(), ImGui::Text(
"Up");
882 if (input_state & 0x0020) ImGui::SameLine(), ImGui::Text(
"Dn");
883 if (input_state & 0x0040) ImGui::SameLine(), ImGui::Text(
"Lt");
884 if (input_state & 0x0080) ImGui::SameLine(), ImGui::Text(
"Rt");
885 if (input_state & 0x0100) ImGui::SameLine(), ImGui::Text(
"A");
886 if (input_state & 0x0200) ImGui::SameLine(), ImGui::Text(
"X");
887 if (input_state & 0x0400) ImGui::SameLine(), ImGui::Text(
"L");
888 if (input_state & 0x0800) ImGui::SameLine(), ImGui::Text(
"R");
894 auto& snes = emu->
snes();
895 uint16_t port_read = snes.GetPortAutoRead(0);
896 ImGui::Text(
"port_auto_read[0]: 0x%04X", port_read);
897 ImGui::Text(
"auto_joy_read: %s", snes.IsAutoJoyReadEnabled() ?
"ON" :
"OFF");
900 uint8_t reg_4218 = port_read & 0xFF;
901 uint8_t reg_4219 = (port_read >> 8) & 0xFF;
902 ImGui::Text(
"$4218: 0x%02X $4219: 0x%02X", reg_4218, reg_4219);
905 ImGui::Text(
"$4218 bits: A=%d X=%d L=%d R=%d",
906 (reg_4218 >> 7) & 1, (reg_4218 >> 6) & 1,
907 (reg_4218 >> 5) & 1, (reg_4218 >> 4) & 1);
910 static uint16_t last_port_read = 0;
911 static int frames_a_pressed = 0;
912 static int frames_since_release = 0;
913 static bool detected_edge =
false;
915 bool a_now = (port_read & 0x0080) != 0;
916 bool a_before = (last_port_read & 0x0080) != 0;
918 if (a_now && !a_before) {
919 detected_edge =
true;
920 frames_a_pressed = 1;
921 frames_since_release = 0;
924 detected_edge =
false;
926 frames_a_pressed = 0;
927 frames_since_release++;
928 detected_edge =
false;
931 last_port_read = port_read;
934 ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f),
"Edge Detection:");
935 ImGui::Text(
"A held for: %d frames", frames_a_pressed);
936 ImGui::Text(
"Released for: %d frames", frames_since_release);
938 ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f),
">>> EDGE DETECTED <<<");
942 ImGui::PopStyleColor();