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(state_color, input_config.ignore_imgui_text_input
272 " Game (Forced)"
273 : ICON_MD_SPORTS_ESPORTS " Game");
274 if (ImGui::IsItemHovered()) {
275 ImGui::SetTooltip(
276 input_config.ignore_imgui_text_input
277 ? "Game input forced on (ignores ImGui text capture)\nPress F1 "
278 "for controls"
279 : "Game input active\nPress F1 for controls");
280 }
281 }
282
283 ImGui::SameLine();
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;
288 emu->SetInputConfig(cfg);
289 }
290 if (ImGui::IsItemHovered()) {
291 ImGui::SetTooltip(
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.");
294 }
295
296 ImGui::SameLine();
297
298 // Option to disable ImGui keyboard navigation (prevents Tab from cycling UI)
299 static bool disable_nav = false;
300 if (ImGui::Checkbox("Disable Nav", &disable_nav)) {
301 ImGuiIO& imgui_io = ImGui::GetIO();
302 if (disable_nav) {
303 imgui_io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard;
304 } else {
305 imgui_io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
306 }
307 }
308 if (ImGui::IsItemHovered()) {
309 ImGui::SetTooltip(
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.");
313 }
314}
315
317 if (!emu)
318 return;
319
320 auto& theme_manager = ThemeManager::Get();
321 const auto& theme = theme_manager.GetCurrentTheme();
322
323 gui::StyledChild ppu_child(
324 "##SNES_PPU", ImVec2(0, 0),
325 {.bg = ConvertColorToImVec4(theme.editor_background)}, true,
326 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
327
328 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
329 ImVec2 snes_size = ImVec2(512, 480);
330
331 if (emu->is_snes_initialized() && emu->ppu_texture()) {
332 // Center the SNES display with aspect ratio preservation
333 float aspect = snes_size.x / snes_size.y;
334 float display_w = canvas_size.x;
335 float display_h = display_w / aspect;
336
337 if (display_h > canvas_size.y) {
338 display_h = canvas_size.y;
339 display_w = display_h * aspect;
340 }
341
342 float pos_x = (canvas_size.x - display_w) * 0.5f;
343 float pos_y = (canvas_size.y - display_h) * 0.5f;
344
345 ImGui::SetCursorPos(ImVec2(pos_x, pos_y));
346
347 // Render PPU texture with click detection for focus
348 ImGui::Image((ImTextureID)(intptr_t)emu->ppu_texture(),
349 ImVec2(display_w, display_h), ImVec2(0, 0), ImVec2(1, 1));
350
351 // Allow clicking on the display to ensure focus
352 // Modern emulators make the game area "sticky" for input
353 if (ImGui::IsItemHovered()) {
354 // ImGui::SetTooltip("Click to ensure game input focus");
355
356 // Visual feedback when hovered (subtle border)
357 ImDrawList* draw_list = ImGui::GetWindowDrawList();
358 ImVec2 screen_pos = ImGui::GetItemRectMin();
359 ImVec2 screen_size = ImGui::GetItemRectMax();
360 draw_list->AddRect(
361 screen_pos, screen_size,
362 ImGui::ColorConvertFloat4ToU32(ConvertColorToImVec4(theme.accent)),
363 0.0f, 2.0f);
364 }
365 } else {
366 // Not initialized - show helpful placeholder
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));
370 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
372 ImGui::SetCursorPosX((canvas_size.x - text_size.x) * 0.5f);
373 ImGui::TextColored(ConvertColorToImVec4(theme.text_primary),
374 "Load a ROM to start emulation");
375 ImGui::SetCursorPosX(
376 (canvas_size.x - ImGui::CalcTextSize("512x480 SNES output").x) * 0.5f);
377 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
378 "512x480 SNES output");
379 }
380}
381
383 if (!emu)
384 return;
385
386 auto& theme_manager = ThemeManager::Get();
387 const auto& theme = theme_manager.GetCurrentTheme();
388
389 gui::StyledChild perf_child("##Performance", ImVec2(0, 0),
390 {.bg = ConvertColorToImVec4(theme.child_bg)},
391 true);
392
393 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
394 ICON_MD_SPEED " Performance Monitor");
395 AddSectionSpacing();
396
397 auto metrics = emu->GetMetrics();
398
399 // FPS Graph
400 if (ImGui::CollapsingHeader(ICON_MD_SHOW_CHART " Frame Rate",
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);
405
406 const float target_ms =
407 emu->snes().memory().pal_timing() ? 1000.0f / 50.0f : 1000.0f / 60.0f;
408 auto frame_ms = emu->FrameTimeHistory();
409 auto fps_history = emu->FpsHistory();
410 if (!frame_ms.empty()) {
411 plotting::PlotStyleScope plot_style(theme);
412 plotting::PlotConfig config{.id = "Frame Times",
413 .y_label = "ms",
414 .flags = ImPlotFlags_NoLegend,
415 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
416 ImPlotAxisFlags_NoGridLines |
417 ImPlotAxisFlags_NoTickMarks,
418 .y_axis_flags = ImPlotAxisFlags_AutoFit};
419 plotting::PlotGuard plot(config);
420 if (plot) {
421 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, target_ms * 2.5f,
422 ImGuiCond_Always);
423 ImPlot::PlotLine("Frame ms", frame_ms.data(),
424 static_cast<int>(frame_ms.size()), 1.0, 0.0,
425 plot_style.item_spec());
426 ImPlot::PlotInfLines("Target", &target_ms, 1, plot_style.item_spec());
427 }
428 }
429
430 if (!fps_history.empty()) {
431 plotting::PlotStyleScope plot_style(theme);
432 plotting::PlotConfig fps_config{
433 .id = "FPS History",
434 .y_label = "fps",
435 .flags = ImPlotFlags_NoLegend,
436 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
437 ImPlotAxisFlags_NoGridLines |
438 ImPlotAxisFlags_NoTickMarks,
439 .y_axis_flags = ImPlotAxisFlags_AutoFit};
440 plotting::PlotGuard plot(fps_config);
441 if (plot) {
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,
445 plot_style.item_spec());
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());
449 }
450 }
451 }
452
453 if (ImGui::CollapsingHeader(ICON_MD_DATA_USAGE " DMA / VRAM Activity",
454 ImGuiTreeNodeFlags_DefaultOpen)) {
455 auto dma_hist = emu->DmaBytesHistory();
456 auto vram_hist = emu->VramBytesHistory();
457 if (!dma_hist.empty() || !vram_hist.empty()) {
458 plotting::PlotStyleScope plot_style(theme);
459 plotting::PlotConfig dma_config{
460 .id = "DMA/VRAM Bytes",
461 .y_label = "bytes/frame",
462 .flags = ImPlotFlags_None, // Legend tells DMA from VRAM
463 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
464 ImPlotAxisFlags_NoGridLines |
465 ImPlotAxisFlags_NoTickMarks,
466 .y_axis_flags = ImPlotAxisFlags_AutoFit};
467 plotting::PlotGuard plot(dma_config);
468 if (plot) {
469 // Calculate max_val before any plotting to avoid locking setup
470 float max_val = 512.0f;
471 if (!dma_hist.empty()) {
472 max_val = std::max(
473 max_val, *std::max_element(dma_hist.begin(), dma_hist.end()));
474 }
475 if (!vram_hist.empty()) {
476 max_val = std::max(
477 max_val, *std::max_element(vram_hist.begin(), vram_hist.end()));
478 }
479 // Setup must be called before any PlotX functions
480 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, max_val * 1.2f,
481 ImGuiCond_Always);
482 // Now do the plotting
483 if (!dma_hist.empty()) {
484 ImPlot::PlotLine("DMA", dma_hist.data(),
485 static_cast<int>(dma_hist.size()), 1.0, 0.0,
486 plot_style.item_spec());
487 }
488 if (!vram_hist.empty()) {
489 ImPlot::PlotLine("VRAM", vram_hist.data(),
490 static_cast<int>(vram_hist.size()), 1.0, 0.0,
491 plot_style.item_spec());
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 ImPlotSpec free_spec = plot_style.item_spec();
519 free_spec.FillColor = ConvertColorToImVec4(theme.plot_histogram);
520 ImPlot::PlotBars("Free", x.data(), y.data(),
521 static_cast<int>(free_bytes.size()), 0.67, free_spec);
522 }
523 } else {
524 ImGui::TextDisabled("Load a ROM to analyze free space.");
525 }
526 }
527
528 // CPU Stats
529 if (ImGui::CollapsingHeader(ICON_MD_MEMORY " CPU Status",
530 ImGuiTreeNodeFlags_DefaultOpen)) {
531 ImGui::Text("PC: $%02X:%04X", metrics.cpu_pb, metrics.cpu_pc);
532 ImGui::Text("Cycles: %llu", metrics.cycles);
533 }
534
535 // Audio Stats
536 if (ImGui::CollapsingHeader(ICON_MD_AUDIOTRACK " Audio Status",
537 ImGuiTreeNodeFlags_DefaultOpen)) {
538 if (emu->audio_backend()) {
539 auto audio_status = emu->audio_backend()->GetStatus();
540 ImGui::Text("Backend: %s",
541 emu->audio_backend()->GetBackendName().c_str());
542 ImGui::Text("Queued: %u frames", audio_status.queued_frames);
543 ImGui::Text("Playing: %s", audio_status.is_playing ? "YES" : "NO");
544
545 auto audio_history = emu->AudioQueueHistory();
546 if (!audio_history.empty()) {
547 plotting::PlotStyleScope plot_style(theme);
548 plotting::PlotConfig audio_config{
549 .id = "Audio Queue Depth",
550 .y_label = "frames",
551 .flags = ImPlotFlags_NoLegend,
552 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
553 ImPlotAxisFlags_NoGridLines |
554 ImPlotAxisFlags_NoTickMarks,
555 .y_axis_flags = ImPlotAxisFlags_AutoFit};
556 plotting::PlotGuard plot(audio_config);
557 if (plot) {
558 ImPlot::SetupAxisLimits(
559 ImAxis_Y1, 0.0f,
560 std::max(512.0f, *std::max_element(audio_history.begin(),
561 audio_history.end()) *
562 1.2f),
563 ImGuiCond_Always);
564 ImPlot::PlotLine("Queued", audio_history.data(),
565 static_cast<int>(audio_history.size()), 1.0, 0.0,
566 plot_style.item_spec());
567 }
568 }
569
570 auto audio_l = emu->AudioRmsLeftHistory();
571 auto audio_r = emu->AudioRmsRightHistory();
572 if (!audio_l.empty() || !audio_r.empty()) {
573 plotting::PlotStyleScope plot_style(theme);
574 plotting::PlotConfig audio_level_config{
575 .id = "Audio Levels (RMS)",
576 .y_label = "normalized",
577 .flags = ImPlotFlags_None, // Legend tells L from R
578 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
579 ImPlotAxisFlags_NoGridLines |
580 ImPlotAxisFlags_NoTickMarks,
581 .y_axis_flags = ImPlotAxisFlags_AutoFit};
582 plotting::PlotGuard plot(audio_level_config);
583 if (plot) {
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,
588 plot_style.item_spec());
589 }
590 if (!audio_r.empty()) {
591 ImPlot::PlotLine("R", audio_r.data(),
592 static_cast<int>(audio_r.size()), 1.0, 0.0,
593 plot_style.item_spec());
594 }
595 }
596 }
597 } else {
598 ImGui::TextColored(ConvertColorToImVec4(theme.error), "No audio backend");
599 }
600 }
601}
602
603void RenderKeyboardShortcuts(bool* show) {
604 if (!show || !*show)
605 return;
606
607 auto& theme_manager = ThemeManager::Get();
608 const auto& theme = theme_manager.GetCurrentTheme();
609
610 // Center the window
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);
614
615 gui::StyleColorGuard title_colors({
616 {ImGuiCol_TitleBg, ConvertColorToImVec4(theme.accent)},
617 {ImGuiCol_TitleBgActive, ConvertColorToImVec4(theme.accent)},
618 });
619
620 if (ImGui::Begin(ICON_MD_KEYBOARD " Keyboard Shortcuts", show,
621 ImGuiWindowFlags_NoCollapse)) {
622 // Emulator controls section
623 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
624 ICON_MD_VIDEOGAME_ASSET " Emulator Controls");
625 ImGui::Separator();
626 ImGui::Spacing();
627
628 if (ImGui::BeginTable("EmulatorControls", 2, ImGuiTableFlags_Borders)) {
629 ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthFixed, 120);
630 ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthStretch);
631 ImGui::TableHeadersRow();
632
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);
639 };
640
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");
646
647 ImGui::EndTable();
648 }
649
650 ImGui::Spacing();
651 ImGui::Spacing();
652
653 // Game controls section
654 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
655 ICON_MD_SPORTS_ESPORTS " SNES Controller");
656 ImGui::Separator();
657 ImGui::Spacing();
658
659 if (ImGui::BeginTable("GameControls", 2, ImGuiTableFlags_Borders)) {
660 ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthFixed, 120);
661 ImGui::TableSetupColumn("Button", ImGuiTableColumnFlags_WidthStretch);
662 ImGui::TableHeadersRow();
663
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);
670 };
671
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");
681
682 ImGui::EndTable();
683 }
684
685 ImGui::Spacing();
686 ImGui::Spacing();
687
688 // Tips section
689 ImGui::TextColored(ConvertColorToImVec4(theme.info), ICON_MD_INFO " Tips");
690 ImGui::Separator();
691 ImGui::Spacing();
692
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");
697
698 ImGui::Spacing();
699 ImGui::Spacing();
700
701 if (ImGui::Button("Close", ImVec2(-1, 30))) {
702 *show = false;
703 }
704 }
705 ImGui::End();
706}
707
709 if (!emu)
710 return;
711
712 auto& theme_manager = ThemeManager::Get();
713 const auto& theme = theme_manager.GetCurrentTheme();
714
715 // Main layout
716 gui::StyleColorGuard main_bg(ImGuiCol_ChildBg,
717 ConvertColorToImVec4(theme.window_bg));
718
719 RenderNavBar(emu);
720
721 ImGui::Separator();
722
723 // Main content area
724 RenderSnesPpu(emu);
725
726 // Keyboard shortcuts overlay (F1 to toggle)
727 static bool show_shortcuts = false;
728 if (ImGui::IsKeyPressed(ImGuiKey_F1)) {
729 show_shortcuts = !show_shortcuts;
730 }
731 RenderKeyboardShortcuts(&show_shortcuts);
732
733 // Tab key: Hold for turbo mode
734 // Use SDL directly to bypass ImGui's keyboard navigation capture
735 // Use SDL directly to bypass ImGui's keyboard navigation capture
736 platform::KeyboardState keyboard_state = SDL_GetKeyboardState(nullptr);
737 bool tab_pressed = platform::IsKeyPressed(keyboard_state, SDL_SCANCODE_TAB);
738 emu->set_turbo_mode(tab_pressed);
739}
740
742 if (!emu)
743 return;
744
745 auto& theme_manager = ThemeManager::Get();
746 const auto& theme = theme_manager.GetCurrentTheme();
747
748 gui::StyledChild controller_child(
749 "##VirtualController", ImVec2(0, 0),
750 {.bg = ConvertColorToImVec4(theme.child_bg)}, true);
751
752 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
753 ICON_MD_SPORTS_ESPORTS " Virtual Controller");
754 ImGui::SameLine();
755 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
756 "(Click to test input)");
757 ImGui::Separator();
758 ImGui::Spacing();
759
760 auto& input_mgr = emu->input_manager();
761
762 // Track which buttons are currently pressed via virtual controller
763 // Use a static to persist state across frames
764 static uint16_t virtual_buttons_pressed = 0;
765
766 // Helper lambda for controller buttons - press on mouse down, release on up
767 auto ControllerButton = [&](const char* label, input::SnesButton button,
768 ImVec2 size = ImVec2(50, 40)) {
769 ImGui::PushID(static_cast<int>(button));
770
771 uint16_t button_mask = 1 << static_cast<uint8_t>(button);
772 bool was_pressed = (virtual_buttons_pressed & button_mask) != 0;
773
774 // Style the button if it's currently pressed
775 std::optional<gui::StyleColorGuard> pressed_color;
776 if (was_pressed) {
777 pressed_color.emplace(ImGuiCol_Button,
778 ConvertColorToImVec4(theme.accent));
779 }
780
781 // Render the button
782 ImGui::Button(label, size);
783
784 // Check if mouse is held down on THIS button (after rendering)
785 bool is_active = ImGui::IsItemActive();
786
787 // Update virtual button state
788 if (is_active) {
789 virtual_buttons_pressed |= button_mask;
790 input_mgr.PressButton(button);
791 } else if (was_pressed) {
792 // Only release if we were the ones who pressed it
793 virtual_buttons_pressed &= ~button_mask;
794 input_mgr.ReleaseButton(button);
795 }
796
797 ImGui::PopID();
798 };
799
800 // D-Pad layout
801 ImGui::Text("D-Pad:");
802 ImGui::Indent();
803
804 // Up
805 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
807
808 // Left, (space), Right
810 ImGui::SameLine();
811 ImGui::Dummy(ImVec2(50, 40));
812 ImGui::SameLine();
814
815 // Down
816 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
818
819 ImGui::Unindent();
820 ImGui::Spacing();
821
822 // Face buttons (SNES layout: Y B on left, X A on right)
823 ImGui::Text("Face Buttons:");
824 ImGui::Indent();
825
826 // Top row: X
827 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
828 ControllerButton("X", input::SnesButton::X);
829
830 // Middle row: Y, A
831 ControllerButton("Y", input::SnesButton::Y);
832 ImGui::SameLine();
833 ImGui::Dummy(ImVec2(50, 40));
834 ImGui::SameLine();
835 ControllerButton("A", input::SnesButton::A);
836
837 // Bottom row: B
838 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
839 ControllerButton("B", input::SnesButton::B);
840
841 ImGui::Unindent();
842 ImGui::Spacing();
843
844 // Shoulder buttons
845 ImGui::Text("Shoulder:");
846 ControllerButton("L", input::SnesButton::L, ImVec2(70, 30));
847 ImGui::SameLine();
848 ControllerButton("R", input::SnesButton::R, ImVec2(70, 30));
849
850 ImGui::Spacing();
851
852 // Start/Select
853 ImGui::Text("Start/Select:");
854 ControllerButton("Select", input::SnesButton::SELECT, ImVec2(70, 30));
855 ImGui::SameLine();
856 ControllerButton("Start", input::SnesButton::START, ImVec2(70, 30));
857
858 ImGui::Spacing();
859 ImGui::Separator();
860
861 // Debug info - show current button state
862 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), "Debug:");
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");
867
868 // Show which buttons are detected
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");
895 }
896
897 ImGui::Spacing();
898
899 // Show what the game actually reads ($4218/$4219)
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");
904
905 // Show $4218/$4219 values (what game reads)
906 uint8_t reg_4218 = port_read & 0xFF; // Low byte
907 uint8_t reg_4219 = (port_read >> 8) & 0xFF; // High byte
908 ImGui::Text("$4218: 0x%02X $4219: 0x%02X", reg_4218, reg_4219);
909
910 // Decode $4218 (A, X, L, R in bits 7-4, unused 3-0)
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);
913
914 // Edge detection debug - track state changes
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;
919
920 bool a_now = (port_read & 0x0080) != 0;
921 bool a_before = (last_port_read & 0x0080) != 0;
922
923 if (a_now && !a_before) {
924 detected_edge = true;
925 frames_a_pressed = 1;
926 frames_since_release = 0;
927 } else if (a_now) {
928 frames_a_pressed++;
929 detected_edge = false;
930 } else {
931 frames_a_pressed = 0;
932 frames_since_release++;
933 detected_edge = false;
934 }
935
936 last_port_read = port_read;
937
938 ImGui::Spacing();
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);
942 if (detected_edge) {
943 ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), ">>> EDGE DETECTED <<<");
944 }
945}
946
947} // namespace ui
948} // namespace emu
949} // namespace yaze
A class for emulating and debugging SNES games.
Definition emulator.h:41
gfx::IRenderer * renderer()
Definition emulator.h:103
void Initialize(gfx::IRenderer *renderer, const std::vector< uint8_t > &rom_data)
Definition emulator.cc:114
bool is_debugging() const
Definition emulator.h:126
std::vector< float > FrameTimeHistory() const
Definition emulator.cc:851
void set_turbo_mode(bool turbo)
Definition emulator.h:108
void SetInputConfig(const input::InputConfig &config)
Definition emulator.cc:76
void * ppu_texture()
Definition emulator.h:104
void set_use_sdl_audio_stream(bool enabled)
Definition emulator.cc:81
double GetCurrentFPS() const
Definition emulator.h:133
std::vector< float > RomBankFreeBytes() const
Definition emulator.cc:886
void set_debugging(bool debugging)
Definition emulator.h:127
std::vector< float > AudioRmsRightHistory() const
Definition emulator.cc:881
std::vector< float > VramBytesHistory() const
Definition emulator.cc:871
std::vector< float > AudioQueueHistory() const
Definition emulator.cc:861
std::vector< float > DmaBytesHistory() const
Definition emulator.cc:866
bool use_sdl_audio_stream() const
Definition emulator.h:92
input::InputManager & input_manager()
Definition emulator.h:125
bool is_turbo_mode() const
Definition emulator.h:107
std::vector< float > FpsHistory() const
Definition emulator.cc:856
void set_running(bool running)
Definition emulator.h:62
bool is_snes_initialized() const
Definition emulator.h:129
std::vector< float > AudioRmsLeftHistory() const
Definition emulator.cc:876
audio::IAudioBackend * audio_backend()
Definition emulator.h:77
auto snes() -> Snes &
Definition emulator.h:60
auto running() const -> bool
Definition emulator.h:61
EmulatorMetrics GetMetrics()
Definition emulator.h:153
virtual std::string GetBackendName() const =0
virtual AudioStatus GetStatus() const =0
InputConfig GetConfig() const
RAII guard for ImGui style colors.
Definition style_guard.h:29
RAII guard for ImGui child windows with optional styling.
static ThemeManager & Get()
const ImPlotSpec & item_spec() const
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:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
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:140
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:59
FileDialogOptions MakeRomFileDialogOptions(bool include_all_files)
Definition file_util.cc:127
std::string GetFileExtension(const std::string &filename)
Gets the file extension from a filename.
Definition file_util.cc:55
SDL2/SDL3 compatibility layer.