yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
emulator_ui.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <fstream>
5
6#include "absl/strings/str_format.h"
7#include "app/emu/emulator.h"
10#include "app/gui/core/icons.h"
15#include "imgui/imgui.h"
16#include "util/file_util.h"
17#include "util/log.h"
18
19namespace yaze {
20namespace emu {
21namespace ui {
22
23using namespace yaze::gui;
24
25namespace {
26// UI Constants for consistent spacing
27constexpr float kStandardSpacing = 8.0f;
28constexpr float kSectionSpacing = 16.0f;
29constexpr float kButtonHeight = 30.0f;
30constexpr float kIconSize = 24.0f;
31
32// Helper to add consistent spacing
33void AddSpacing() {
34 ImGui::Spacing();
35 ImGui::Spacing();
36}
37
38void AddSectionSpacing() {
39 ImGui::Spacing();
40 ImGui::Separator();
41 ImGui::Spacing();
42}
43
44} // namespace
45
47 if (!emu)
48 return;
49
50 auto& theme_manager = ThemeManager::Get();
51 const auto& theme = theme_manager.GetCurrentTheme();
52
53 // Handle keyboard shortcuts for emulator control
54 // IMPORTANT: Use Shortcut() to avoid conflicts with game input
55 // Space - toggle play/pause (only when not typing in text fields)
56 if (ImGui::Shortcut(ImGuiKey_Space, ImGuiInputFlags_RouteGlobal)) {
57 emu->set_running(!emu->running());
58 }
59
60 // F10 - step one frame
61 if (ImGui::Shortcut(ImGuiKey_F10, ImGuiInputFlags_RouteGlobal)) {
62 if (!emu->running()) {
63 emu->snes().RunFrame();
64 }
65 }
66
67 // Navbar with theme colors
68 gui::StyleColorGuard navbar_colors({
69 {ImGuiCol_Button, ConvertColorToImVec4(theme.button)},
70 {ImGuiCol_ButtonHovered, ConvertColorToImVec4(theme.button_hovered)},
71 {ImGuiCol_ButtonActive, ConvertColorToImVec4(theme.button_active)},
72 });
73
74 // Play/Pause button with icon
75 bool is_running = emu->running();
76 if (is_running) {
77 if (ImGui::Button(ICON_MD_PAUSE, ImVec2(50, kButtonHeight))) {
78 emu->set_running(false);
79 }
80 if (ImGui::IsItemHovered()) {
81 ImGui::SetTooltip("Pause emulation (Space)");
82 }
83 } else {
84 if (ImGui::Button(ICON_MD_PLAY_ARROW, ImVec2(50, kButtonHeight))) {
85 emu->set_running(true);
86 }
87 if (ImGui::IsItemHovered()) {
88 ImGui::SetTooltip("Start emulation (Space)");
89 }
90 }
91
92 ImGui::SameLine();
93
94 // Step button
95 if (ImGui::Button(ICON_MD_SKIP_NEXT, ImVec2(50, kButtonHeight))) {
96 if (!is_running) {
97 emu->snes().RunFrame();
98 }
99 }
100 if (ImGui::IsItemHovered()) {
101 ImGui::SetTooltip("Step one frame (F10)");
102 }
103
104 ImGui::SameLine();
105
106 // Reset button
107 if (ImGui::Button(ICON_MD_RESTART_ALT, ImVec2(50, kButtonHeight))) {
108 emu->snes().Reset();
109 LOG_INFO("Emulator", "System reset");
110 }
111 if (ImGui::IsItemHovered()) {
112 ImGui::SetTooltip("Reset SNES (Ctrl+R)");
113 }
114
115 ImGui::SameLine();
116
117 // Load ROM button
118 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load ROM",
119 ImVec2(110, kButtonHeight))) {
120 std::string rom_path = util::FileDialogWrapper::ShowOpenFileDialog(
122 if (!rom_path.empty()) {
123 // Check if it's a valid ROM file extension
124 std::string ext = util::GetFileExtension(rom_path);
125 if (ext == ".sfc" || ext == ".smc" || ext == ".SFC" || ext == ".SMC") {
126 try {
127 // Read ROM file into memory
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>());
133 rom_file.close();
134
135 // Reinitialize emulator with new ROM
136 if (!rom_data.empty()) {
137 emu->Initialize(emu->renderer(), rom_data);
138 LOG_INFO("Emulator", "Loaded ROM: %s (%zu bytes)",
139 util::GetFileName(rom_path).c_str(), rom_data.size());
140 } else {
141 LOG_ERROR("Emulator", "ROM file is empty: %s", rom_path.c_str());
142 }
143 } else {
144 LOG_ERROR("Emulator", "Failed to open ROM file: %s",
145 rom_path.c_str());
146 }
147 } catch (const std::exception& e) {
148 LOG_ERROR("Emulator", "Error loading ROM: %s", e.what());
149 }
150 } else {
151 LOG_WARN("Emulator",
152 "Invalid ROM file extension: %s (expected .sfc or .smc)",
153 ext.c_str());
154 }
155 }
156 }
157 if (ImGui::IsItemHovered()) {
158 ImGui::SetTooltip(
159 "Load a different ROM file\n"
160 "Allows testing hacks with assembly patches applied");
161 }
162
163 ImGui::SameLine();
164 ImGui::Separator();
165 ImGui::SameLine();
166
167 // Debugger toggle
168 bool is_debugging = emu->is_debugging();
169 if (ImGui::Checkbox(ICON_MD_BUG_REPORT " Debug", &is_debugging)) {
170 emu->set_debugging(is_debugging);
171 }
172 if (ImGui::IsItemHovered()) {
173 ImGui::SetTooltip("Enable debugger features");
174 }
175
176 ImGui::SameLine();
177
178 // Recording toggle (for DisassemblyViewer)
179 // Access through emulator's disassembly viewer
180 // bool recording = emu->disassembly_viewer().IsRecording();
181 // if (ImGui::Checkbox(ICON_MD_FIBER_MANUAL_RECORD " Rec", &recording)) {
182 // emu->disassembly_viewer().SetRecording(recording);
183 // }
184 // if (ImGui::IsItemHovered()) {
185 // ImGui::SetTooltip("Record instructions to Disassembly
186 // Viewer\n(Lightweight - uses sparse address map)");
187 // }
188
189 ImGui::SameLine();
190
191 // Turbo mode
192 bool turbo = emu->is_turbo_mode();
193 if (ImGui::Checkbox(ICON_MD_FAST_FORWARD " Turbo", &turbo)) {
194 emu->set_turbo_mode(turbo);
195 }
196 if (ImGui::IsItemHovered()) {
197 ImGui::SetTooltip("Fast forward (shortcut: hold Tab)");
198 }
199
200 ImGui::SameLine();
201 ImGui::Separator();
202 ImGui::SameLine();
203
204 // FPS Counter with color coding
205 double fps = emu->GetCurrentFPS();
206 ImVec4 fps_color;
207 if (fps >= 58.0) {
208 fps_color = ConvertColorToImVec4(theme.success); // Green for good FPS
209 } else if (fps >= 45.0) {
210 fps_color = ConvertColorToImVec4(theme.warning); // Yellow for okay FPS
211 } else {
212 fps_color = ConvertColorToImVec4(theme.error); // Red for bad FPS
213 }
214
215 ImGui::TextColored(fps_color, ICON_MD_SPEED " %.1f FPS", fps);
216
217 ImGui::SameLine();
218
219 // Audio backend status
220 if (emu->audio_backend()) {
221 auto audio_status = emu->audio_backend()->GetStatus();
222 ImVec4 audio_color = audio_status.is_playing
223 ? ConvertColorToImVec4(theme.success)
224 : ConvertColorToImVec4(theme.text_disabled);
225
226 ImGui::TextColored(audio_color, ICON_MD_VOLUME_UP " %s | %u frames",
227 emu->audio_backend()->GetBackendName().c_str(),
228 audio_status.queued_frames);
229
230 if (ImGui::IsItemHovered()) {
231 ImGui::SetTooltip("Audio Backend: %s\nQueued: %u frames\nPlaying: %s",
232 emu->audio_backend()->GetBackendName().c_str(),
233 audio_status.queued_frames,
234 audio_status.is_playing ? "YES" : "NO");
235 }
236
237 ImGui::SameLine();
238 static bool use_sdl_audio_stream = emu->use_sdl_audio_stream();
239 if (ImGui::Checkbox(ICON_MD_SETTINGS " SDL Audio Stream",
240 &use_sdl_audio_stream)) {
241 emu->set_use_sdl_audio_stream(use_sdl_audio_stream);
242 }
243 if (ImGui::IsItemHovered()) {
244 ImGui::SetTooltip("Use SDL audio stream for audio");
245 }
246 } else {
247 ImGui::TextColored(ConvertColorToImVec4(theme.error),
248 ICON_MD_VOLUME_OFF " No Backend");
249 }
250
251 ImGui::SameLine();
252 ImGui::Separator();
253 ImGui::SameLine();
254
255 // Input capture status indicator (like modern emulators)
256 ImGuiIO& io = ImGui::GetIO();
257 const auto input_config = emu->input_manager().GetConfig();
258 if (io.WantCaptureKeyboard && !input_config.ignore_imgui_text_input) {
259 // ImGui is capturing keyboard (typing in UI)
260 ImGui::TextColored(ConvertColorToImVec4(theme.warning),
261 ICON_MD_KEYBOARD " UI");
262 if (ImGui::IsItemHovered()) {
263 ImGui::SetTooltip("Keyboard captured by UI\nGame input disabled");
264 }
265 } else {
266 // Emulator can receive input
267 ImVec4 state_color = input_config.ignore_imgui_text_input
268 ? ConvertColorToImVec4(theme.accent)
269 : ConvertColorToImVec4(theme.success);
270 ImGui::TextColored(
271 state_color,
272 input_config.ignore_imgui_text_input
273 ? ICON_MD_SPORTS_ESPORTS " Game (Forced)"
274 : ICON_MD_SPORTS_ESPORTS " Game");
275 if (ImGui::IsItemHovered()) {
276 ImGui::SetTooltip(
277 input_config.ignore_imgui_text_input
278 ? "Game input forced on (ignores ImGui text capture)\nPress F1 "
279 "for controls"
280 : "Game input active\nPress F1 for controls");
281 }
282 }
283
284 ImGui::SameLine();
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;
289 emu->SetInputConfig(cfg);
290 }
291 if (ImGui::IsItemHovered()) {
292 ImGui::SetTooltip(
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.");
295 }
296
297 ImGui::SameLine();
298
299 // Option to disable ImGui keyboard navigation (prevents Tab from cycling UI)
300 static bool disable_nav = false;
301 if (ImGui::Checkbox("Disable Nav", &disable_nav)) {
302 ImGuiIO& imgui_io = ImGui::GetIO();
303 if (disable_nav) {
304 imgui_io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard;
305 } else {
306 imgui_io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
307 }
308 }
309 if (ImGui::IsItemHovered()) {
310 ImGui::SetTooltip(
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.");
314 }
315
316}
317
319 if (!emu)
320 return;
321
322 auto& theme_manager = ThemeManager::Get();
323 const auto& theme = theme_manager.GetCurrentTheme();
324
325 gui::StyledChild ppu_child(
326 "##SNES_PPU", ImVec2(0, 0),
327 {.bg = ConvertColorToImVec4(theme.editor_background)}, true,
328 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
329
330 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
331 ImVec2 snes_size = ImVec2(512, 480);
332
333 if (emu->is_snes_initialized() && emu->ppu_texture()) {
334 // Center the SNES display with aspect ratio preservation
335 float aspect = snes_size.x / snes_size.y;
336 float display_w = canvas_size.x;
337 float display_h = display_w / aspect;
338
339 if (display_h > canvas_size.y) {
340 display_h = canvas_size.y;
341 display_w = display_h * aspect;
342 }
343
344 float pos_x = (canvas_size.x - display_w) * 0.5f;
345 float pos_y = (canvas_size.y - display_h) * 0.5f;
346
347 ImGui::SetCursorPos(ImVec2(pos_x, pos_y));
348
349 // Render PPU texture with click detection for focus
350 ImGui::Image((ImTextureID)(intptr_t)emu->ppu_texture(),
351 ImVec2(display_w, display_h), ImVec2(0, 0), ImVec2(1, 1));
352
353 // Allow clicking on the display to ensure focus
354 // Modern emulators make the game area "sticky" for input
355 if (ImGui::IsItemHovered()) {
356 // ImGui::SetTooltip("Click to ensure game input focus");
357
358 // Visual feedback when hovered (subtle border)
359 ImDrawList* draw_list = ImGui::GetWindowDrawList();
360 ImVec2 screen_pos = ImGui::GetItemRectMin();
361 ImVec2 screen_size = ImGui::GetItemRectMax();
362 draw_list->AddRect(
363 screen_pos, screen_size,
364 ImGui::ColorConvertFloat4ToU32(ConvertColorToImVec4(theme.accent)),
365 0.0f, 0, 2.0f);
366 }
367 } else {
368 // Not initialized - show helpful placeholder
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));
372 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
374 ImGui::SetCursorPosX((canvas_size.x - text_size.x) * 0.5f);
375 ImGui::TextColored(ConvertColorToImVec4(theme.text_primary),
376 "Load a ROM to start emulation");
377 ImGui::SetCursorPosX(
378 (canvas_size.x - ImGui::CalcTextSize("512x480 SNES output").x) * 0.5f);
379 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
380 "512x480 SNES output");
381 }
382
383}
384
386 if (!emu)
387 return;
388
389 auto& theme_manager = ThemeManager::Get();
390 const auto& theme = theme_manager.GetCurrentTheme();
391
392 gui::StyledChild perf_child("##Performance", ImVec2(0, 0),
393 {.bg = ConvertColorToImVec4(theme.child_bg)},
394 true);
395
396 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
397 ICON_MD_SPEED " Performance Monitor");
398 AddSectionSpacing();
399
400 auto metrics = emu->GetMetrics();
401
402 // FPS Graph
403 if (ImGui::CollapsingHeader(ICON_MD_SHOW_CHART " Frame Rate",
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);
408
409 const float target_ms =
410 emu->snes().memory().pal_timing() ? 1000.0f / 50.0f : 1000.0f / 60.0f;
411 auto frame_ms = emu->FrameTimeHistory();
412 auto fps_history = emu->FpsHistory();
413 if (!frame_ms.empty()) {
414 plotting::PlotStyleScope plot_style(theme);
416 .id = "Frame Times",
417 .y_label = "ms",
418 .flags = ImPlotFlags_NoLegend,
419 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
420 ImPlotAxisFlags_NoGridLines |
421 ImPlotAxisFlags_NoTickMarks,
422 .y_axis_flags = ImPlotAxisFlags_AutoFit};
423 plotting::PlotGuard plot(config);
424 if (plot) {
425 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, target_ms * 2.5f,
426 ImGuiCond_Always);
427 ImPlot::PlotLine("Frame ms", frame_ms.data(),
428 static_cast<int>(frame_ms.size()));
429 ImPlot::PlotInfLines("Target", &target_ms, 1);
430 }
431 }
432
433 if (!fps_history.empty()) {
434 plotting::PlotStyleScope plot_style(theme);
435 plotting::PlotConfig fps_config{
436 .id = "FPS History",
437 .y_label = "fps",
438 .flags = ImPlotFlags_NoLegend,
439 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
440 ImPlotAxisFlags_NoGridLines |
441 ImPlotAxisFlags_NoTickMarks,
442 .y_axis_flags = ImPlotAxisFlags_AutoFit};
443 plotting::PlotGuard plot(fps_config);
444 if (plot) {
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
449 : 60.0f;
450 ImPlot::PlotInfLines("Target", &target_fps, 1);
451 }
452 }
453 }
454
455 if (ImGui::CollapsingHeader(ICON_MD_DATA_USAGE " DMA / VRAM Activity",
456 ImGuiTreeNodeFlags_DefaultOpen)) {
457 auto dma_hist = emu->DmaBytesHistory();
458 auto vram_hist = emu->VramBytesHistory();
459 if (!dma_hist.empty() || !vram_hist.empty()) {
460 plotting::PlotStyleScope plot_style(theme);
461 plotting::PlotConfig dma_config{
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};
469 plotting::PlotGuard plot(dma_config);
470 if (plot) {
471 // Calculate max_val before any plotting to avoid locking setup
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()));
476 }
477 if (!vram_hist.empty()) {
478 max_val = std::max(max_val, *std::max_element(vram_hist.begin(),
479 vram_hist.end()));
480 }
481 // Setup must be called before any PlotX functions
482 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, max_val * 1.2f,
483 ImGuiCond_Always);
484 // Now do the plotting
485 if (!dma_hist.empty()) {
486 ImPlot::PlotLine("DMA", dma_hist.data(),
487 static_cast<int>(dma_hist.size()));
488 }
489 if (!vram_hist.empty()) {
490 ImPlot::PlotLine("VRAM", vram_hist.data(),
491 static_cast<int>(vram_hist.size()));
492 }
493 }
494 } else {
495 ImGui::TextDisabled("No DMA activity recorded yet");
496 }
497 }
498
499 if (ImGui::CollapsingHeader(ICON_MD_STORAGE " ROM Free Space",
500 ImGuiTreeNodeFlags_DefaultOpen)) {
501 auto free_bytes = emu->RomBankFreeBytes();
502 if (!free_bytes.empty()) {
503 plotting::PlotStyleScope plot_style(theme);
504 plotting::PlotConfig free_config{
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};
510 plotting::PlotGuard plot(free_config);
511 if (plot) {
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]);
517 }
518 ImPlot::PlotBars("Free", x.data(), y.data(),
519 static_cast<int>(free_bytes.size()), 0.67, 0.0,
520 ImPlotBarsFlags_None);
521 }
522 } else {
523 ImGui::TextDisabled("Load a ROM to analyze free space.");
524 }
525 }
526
527 // CPU Stats
528 if (ImGui::CollapsingHeader(ICON_MD_MEMORY " CPU Status",
529 ImGuiTreeNodeFlags_DefaultOpen)) {
530 ImGui::Text("PC: $%02X:%04X", metrics.cpu_pb, metrics.cpu_pc);
531 ImGui::Text("Cycles: %llu", metrics.cycles);
532 }
533
534 // Audio Stats
535 if (ImGui::CollapsingHeader(ICON_MD_AUDIOTRACK " Audio Status",
536 ImGuiTreeNodeFlags_DefaultOpen)) {
537 if (emu->audio_backend()) {
538 auto audio_status = emu->audio_backend()->GetStatus();
539 ImGui::Text("Backend: %s",
540 emu->audio_backend()->GetBackendName().c_str());
541 ImGui::Text("Queued: %u frames", audio_status.queued_frames);
542 ImGui::Text("Playing: %s", audio_status.is_playing ? "YES" : "NO");
543
544 auto audio_history = emu->AudioQueueHistory();
545 if (!audio_history.empty()) {
546 plotting::PlotStyleScope plot_style(theme);
547 plotting::PlotConfig audio_config{
548 .id = "Audio Queue Depth",
549 .y_label = "frames",
550 .flags = ImPlotFlags_NoLegend,
551 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
552 ImPlotAxisFlags_NoGridLines |
553 ImPlotAxisFlags_NoTickMarks,
554 .y_axis_flags = ImPlotAxisFlags_AutoFit};
555 plotting::PlotGuard plot(audio_config);
556 if (plot) {
557 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f,
558 std::max(512.0f,
559 *std::max_element(audio_history.begin(),
560 audio_history.end()) *
561 1.2f),
562 ImGuiCond_Always);
563 ImPlot::PlotLine("Queued", audio_history.data(),
564 static_cast<int>(audio_history.size()));
565 }
566 }
567
568 auto audio_l = emu->AudioRmsLeftHistory();
569 auto audio_r = emu->AudioRmsRightHistory();
570 if (!audio_l.empty() || !audio_r.empty()) {
571 plotting::PlotStyleScope plot_style(theme);
572 plotting::PlotConfig audio_level_config{
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};
580 plotting::PlotGuard plot(audio_level_config);
581 if (plot) {
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()));
586 }
587 if (!audio_r.empty()) {
588 ImPlot::PlotLine("R", audio_r.data(),
589 static_cast<int>(audio_r.size()));
590 }
591 }
592 }
593 } else {
594 ImGui::TextColored(ConvertColorToImVec4(theme.error), "No audio backend");
595 }
596 }
597
598}
599
600void RenderKeyboardShortcuts(bool* show) {
601 if (!show || !*show)
602 return;
603
604 auto& theme_manager = ThemeManager::Get();
605 const auto& theme = theme_manager.GetCurrentTheme();
606
607 // Center the window
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);
611
612 gui::StyleColorGuard title_colors({
613 {ImGuiCol_TitleBg, ConvertColorToImVec4(theme.accent)},
614 {ImGuiCol_TitleBgActive, ConvertColorToImVec4(theme.accent)},
615 });
616
617 if (ImGui::Begin(ICON_MD_KEYBOARD " Keyboard Shortcuts", show,
618 ImGuiWindowFlags_NoCollapse)) {
619 // Emulator controls section
620 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
621 ICON_MD_VIDEOGAME_ASSET " Emulator Controls");
622 ImGui::Separator();
623 ImGui::Spacing();
624
625 if (ImGui::BeginTable("EmulatorControls", 2, ImGuiTableFlags_Borders)) {
626 ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthFixed, 120);
627 ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthStretch);
628 ImGui::TableHeadersRow();
629
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);
636 };
637
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");
643
644 ImGui::EndTable();
645 }
646
647 ImGui::Spacing();
648 ImGui::Spacing();
649
650 // Game controls section
651 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
652 ICON_MD_SPORTS_ESPORTS " SNES Controller");
653 ImGui::Separator();
654 ImGui::Spacing();
655
656 if (ImGui::BeginTable("GameControls", 2, ImGuiTableFlags_Borders)) {
657 ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthFixed, 120);
658 ImGui::TableSetupColumn("Button", ImGuiTableColumnFlags_WidthStretch);
659 ImGui::TableHeadersRow();
660
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);
667 };
668
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");
678
679 ImGui::EndTable();
680 }
681
682 ImGui::Spacing();
683 ImGui::Spacing();
684
685 // Tips section
686 ImGui::TextColored(ConvertColorToImVec4(theme.info), ICON_MD_INFO " Tips");
687 ImGui::Separator();
688 ImGui::Spacing();
689
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");
694
695 ImGui::Spacing();
696 ImGui::Spacing();
697
698 if (ImGui::Button("Close", ImVec2(-1, 30))) {
699 *show = false;
700 }
701 }
702 ImGui::End();
703}
704
706 if (!emu)
707 return;
708
709 auto& theme_manager = ThemeManager::Get();
710 const auto& theme = theme_manager.GetCurrentTheme();
711
712 // Main layout
713 gui::StyleColorGuard main_bg(ImGuiCol_ChildBg,
714 ConvertColorToImVec4(theme.window_bg));
715
716 RenderNavBar(emu);
717
718 ImGui::Separator();
719
720 // Main content area
721 RenderSnesPpu(emu);
722
723 // Keyboard shortcuts overlay (F1 to toggle)
724 static bool show_shortcuts = false;
725 if (ImGui::IsKeyPressed(ImGuiKey_F1)) {
726 show_shortcuts = !show_shortcuts;
727 }
728 RenderKeyboardShortcuts(&show_shortcuts);
729
730 // Tab key: Hold for turbo mode
731 // Use SDL directly to bypass ImGui's keyboard navigation capture
732 // Use SDL directly to bypass ImGui's keyboard navigation capture
733 platform::KeyboardState keyboard_state = SDL_GetKeyboardState(nullptr);
734 bool tab_pressed = platform::IsKeyPressed(keyboard_state, SDL_SCANCODE_TAB);
735 emu->set_turbo_mode(tab_pressed);
736}
737
739 if (!emu)
740 return;
741
742 auto& theme_manager = ThemeManager::Get();
743 const auto& theme = theme_manager.GetCurrentTheme();
744
745 gui::StyledChild controller_child(
746 "##VirtualController", ImVec2(0, 0),
747 {.bg = ConvertColorToImVec4(theme.child_bg)}, true);
748
749 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
750 ICON_MD_SPORTS_ESPORTS " Virtual Controller");
751 ImGui::SameLine();
752 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
753 "(Click to test input)");
754 ImGui::Separator();
755 ImGui::Spacing();
756
757 auto& input_mgr = emu->input_manager();
758
759 // Track which buttons are currently pressed via virtual controller
760 // Use a static to persist state across frames
761 static uint16_t virtual_buttons_pressed = 0;
762
763 // Helper lambda for controller buttons - press on mouse down, release on up
764 auto ControllerButton = [&](const char* label, input::SnesButton button,
765 ImVec2 size = ImVec2(50, 40)) {
766 ImGui::PushID(static_cast<int>(button));
767
768 uint16_t button_mask = 1 << static_cast<uint8_t>(button);
769 bool was_pressed = (virtual_buttons_pressed & button_mask) != 0;
770
771 // Style the button if it's currently pressed
772 std::optional<gui::StyleColorGuard> pressed_color;
773 if (was_pressed) {
774 pressed_color.emplace(ImGuiCol_Button,
775 ConvertColorToImVec4(theme.accent));
776 }
777
778 // Render the button
779 ImGui::Button(label, size);
780
781 // Check if mouse is held down on THIS button (after rendering)
782 bool is_active = ImGui::IsItemActive();
783
784 // Update virtual button state
785 if (is_active) {
786 virtual_buttons_pressed |= button_mask;
787 input_mgr.PressButton(button);
788 } else if (was_pressed) {
789 // Only release if we were the ones who pressed it
790 virtual_buttons_pressed &= ~button_mask;
791 input_mgr.ReleaseButton(button);
792 }
793
794 ImGui::PopID();
795 };
796
797 // D-Pad layout
798 ImGui::Text("D-Pad:");
799 ImGui::Indent();
800
801 // Up
802 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
804
805 // Left, (space), Right
807 ImGui::SameLine();
808 ImGui::Dummy(ImVec2(50, 40));
809 ImGui::SameLine();
811
812 // Down
813 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
815
816 ImGui::Unindent();
817 ImGui::Spacing();
818
819 // Face buttons (SNES layout: Y B on left, X A on right)
820 ImGui::Text("Face Buttons:");
821 ImGui::Indent();
822
823 // Top row: X
824 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
825 ControllerButton("X", input::SnesButton::X);
826
827 // Middle row: Y, A
828 ControllerButton("Y", input::SnesButton::Y);
829 ImGui::SameLine();
830 ImGui::Dummy(ImVec2(50, 40));
831 ImGui::SameLine();
832 ControllerButton("A", input::SnesButton::A);
833
834 // Bottom row: B
835 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
836 ControllerButton("B", input::SnesButton::B);
837
838 ImGui::Unindent();
839 ImGui::Spacing();
840
841 // Shoulder buttons
842 ImGui::Text("Shoulder:");
843 ControllerButton("L", input::SnesButton::L, ImVec2(70, 30));
844 ImGui::SameLine();
845 ControllerButton("R", input::SnesButton::R, ImVec2(70, 30));
846
847 ImGui::Spacing();
848
849 // Start/Select
850 ImGui::Text("Start/Select:");
851 ControllerButton("Select", input::SnesButton::SELECT, ImVec2(70, 30));
852 ImGui::SameLine();
853 ControllerButton("Start", input::SnesButton::START, ImVec2(70, 30));
854
855 ImGui::Spacing();
856 ImGui::Separator();
857
858 // Debug info - show current button state
859 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), "Debug:");
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");
864
865 // Show which buttons are detected
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");
880 }
881
882 ImGui::Spacing();
883
884 // Show what the game actually reads ($4218/$4219)
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");
889
890 // Show $4218/$4219 values (what game reads)
891 uint8_t reg_4218 = port_read & 0xFF; // Low byte
892 uint8_t reg_4219 = (port_read >> 8) & 0xFF; // High byte
893 ImGui::Text("$4218: 0x%02X $4219: 0x%02X", reg_4218, reg_4219);
894
895 // Decode $4218 (A, X, L, R in bits 7-4, unused 3-0)
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);
899
900 // Edge detection debug - track state changes
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;
905
906 bool a_now = (port_read & 0x0080) != 0;
907 bool a_before = (last_port_read & 0x0080) != 0;
908
909 if (a_now && !a_before) {
910 detected_edge = true;
911 frames_a_pressed = 1;
912 frames_since_release = 0;
913 } else if (a_now) {
914 frames_a_pressed++;
915 detected_edge = false;
916 } else {
917 frames_a_pressed = 0;
918 frames_since_release++;
919 detected_edge = false;
920 }
921
922 last_port_read = port_read;
923
924 ImGui::Spacing();
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);
928 if (detected_edge) {
929 ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), ">>> EDGE DETECTED <<<");
930 }
931
932}
933
934} // namespace ui
935} // namespace emu
936} // namespace yaze
A class for emulating and debugging SNES games.
Definition emulator.h:40
gfx::IRenderer * renderer()
Definition emulator.h:101
void Initialize(gfx::IRenderer *renderer, const std::vector< uint8_t > &rom_data)
Definition emulator.cc:114
bool is_debugging() const
Definition emulator.h:124
std::vector< float > FrameTimeHistory() const
Definition emulator.cc:813
void set_turbo_mode(bool turbo)
Definition emulator.h:106
void SetInputConfig(const input::InputConfig &config)
Definition emulator.cc:76
void * ppu_texture()
Definition emulator.h:102
void set_use_sdl_audio_stream(bool enabled)
Definition emulator.cc:81
double GetCurrentFPS() const
Definition emulator.h:131
std::vector< float > RomBankFreeBytes() const
Definition emulator.cc:848
void set_debugging(bool debugging)
Definition emulator.h:125
std::vector< float > AudioRmsRightHistory() const
Definition emulator.cc:843
std::vector< float > VramBytesHistory() const
Definition emulator.cc:833
std::vector< float > AudioQueueHistory() const
Definition emulator.cc:823
std::vector< float > DmaBytesHistory() const
Definition emulator.cc:828
bool use_sdl_audio_stream() const
Definition emulator.h:90
input::InputManager & input_manager()
Definition emulator.h:123
bool is_turbo_mode() const
Definition emulator.h:105
std::vector< float > FpsHistory() const
Definition emulator.cc:818
void set_running(bool running)
Definition emulator.h:61
bool is_snes_initialized() const
Definition emulator.h:127
std::vector< float > AudioRmsLeftHistory() const
Definition emulator.cc:838
audio::IAudioBackend * audio_backend()
Definition emulator.h:75
auto snes() -> Snes &
Definition emulator.h:59
auto running() const -> bool
Definition emulator.h:60
EmulatorMetrics GetMetrics()
Definition emulator.h:151
virtual std::string GetBackendName() const =0
virtual AudioStatus GetStatus() const =0
InputConfig GetConfig() const
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui child windows with optional styling.
static ThemeManager & Get()
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_PAUSE
Definition icons.h:1389
#define ICON_MD_SETTINGS
Definition icons.h:1699
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_MEMORY
Definition icons.h:1195
#define ICON_MD_STORAGE
Definition icons.h:1865
#define ICON_MD_VOLUME_UP
Definition icons.h:2111
#define ICON_MD_DATA_USAGE
Definition icons.h:525
#define ICON_MD_FAST_FORWARD
Definition icons.h:724
#define ICON_MD_ARROW_FORWARD
Definition icons.h:184
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_VIDEOGAME_ASSET
Definition icons.h:2076
#define ICON_MD_ARROW_DOWNWARD
Definition icons.h:180
#define ICON_MD_BUG_REPORT
Definition icons.h:327
#define ICON_MD_SHOW_CHART
Definition icons.h:1736
#define ICON_MD_SPEED
Definition icons.h:1817
#define ICON_MD_AUDIOTRACK
Definition icons.h:213
#define ICON_MD_KEYBOARD
Definition icons.h:1028
#define ICON_MD_ARROW_UPWARD
Definition icons.h:189
#define ICON_MD_SKIP_NEXT
Definition icons.h:1773
#define ICON_MD_ARROW_BACK
Definition icons.h:173
#define ICON_MD_SPORTS_ESPORTS
Definition icons.h:1826
#define ICON_MD_VOLUME_OFF
Definition icons.h:2110
#define ICON_MD_RESTART_ALT
Definition icons.h:1602
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
SnesButton
SNES controller button mapping (platform-agnostic)
void RenderPerformanceMonitor(Emulator *emu)
Performance metrics (FPS, frame time, audio status)
void RenderKeyboardShortcuts(bool *show)
Keyboard shortcuts help overlay (F1 in modern emulators)
void RenderSnesPpu(Emulator *emu)
SNES PPU output display.
void RenderNavBar(Emulator *emu)
Navigation bar with play/pause, step, reset controls.
void RenderEmulatorInterface(Emulator *emu)
Main emulator UI interface - renders the emulator window.
void RenderVirtualController(Emulator *emu)
Virtual SNES controller for testing input without keyboard Useful for debugging input issues - bypass...
Graphical User Interface (GUI) components for the application.
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:134
bool IsKeyPressed(KeyboardState state, SDL_Scancode scancode)
Check if a key is pressed using the keyboard state.
Definition sdl_compat.h:142
const Uint8 * KeyboardState
Definition sdl_compat.h:32
std::string GetFileName(const std::string &filename)
Gets the filename from a full path.
Definition file_util.cc:19
FileDialogOptions MakeRomFileDialogOptions(bool include_all_files)
Definition file_util.cc:87
std::string GetFileExtension(const std::string &filename)
Gets the file extension from a filename.
Definition file_util.cc:15
SDL2/SDL3 compatibility layer.