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"
14#include "imgui/imgui.h"
15#include "util/file_util.h"
16#include "util/log.h"
17
18namespace yaze {
19namespace emu {
20namespace ui {
21
22using namespace yaze::gui;
23
24namespace {
25// UI Constants for consistent spacing
26constexpr float kStandardSpacing = 8.0f;
27constexpr float kSectionSpacing = 16.0f;
28constexpr float kButtonHeight = 30.0f;
29constexpr float kIconSize = 24.0f;
30
31// Helper to add consistent spacing
32void AddSpacing() {
33 ImGui::Spacing();
34 ImGui::Spacing();
35}
36
37void AddSectionSpacing() {
38 ImGui::Spacing();
39 ImGui::Separator();
40 ImGui::Spacing();
41}
42
43} // namespace
44
46 if (!emu)
47 return;
48
49 auto& theme_manager = ThemeManager::Get();
50 const auto& theme = theme_manager.GetCurrentTheme();
51
52 // Handle keyboard shortcuts for emulator control
53 // IMPORTANT: Use Shortcut() to avoid conflicts with game input
54 // Space - toggle play/pause (only when not typing in text fields)
55 if (ImGui::Shortcut(ImGuiKey_Space, ImGuiInputFlags_RouteGlobal)) {
56 emu->set_running(!emu->running());
57 }
58
59 // F10 - step one frame
60 if (ImGui::Shortcut(ImGuiKey_F10, ImGuiInputFlags_RouteGlobal)) {
61 if (!emu->running()) {
62 emu->snes().RunFrame();
63 }
64 }
65
66 // Navbar with theme colors
67 ImGui::PushStyleColor(ImGuiCol_Button, ConvertColorToImVec4(theme.button));
68 ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
69 ConvertColorToImVec4(theme.button_hovered));
70 ImGui::PushStyleColor(ImGuiCol_ButtonActive,
71 ConvertColorToImVec4(theme.button_active));
72
73 // Play/Pause button with icon
74 bool is_running = emu->running();
75 if (is_running) {
76 if (ImGui::Button(ICON_MD_PAUSE, ImVec2(50, kButtonHeight))) {
77 emu->set_running(false);
78 }
79 if (ImGui::IsItemHovered()) {
80 ImGui::SetTooltip("Pause emulation (Space)");
81 }
82 } else {
83 if (ImGui::Button(ICON_MD_PLAY_ARROW, ImVec2(50, kButtonHeight))) {
84 emu->set_running(true);
85 }
86 if (ImGui::IsItemHovered()) {
87 ImGui::SetTooltip("Start emulation (Space)");
88 }
89 }
90
91 ImGui::SameLine();
92
93 // Step button
94 if (ImGui::Button(ICON_MD_SKIP_NEXT, ImVec2(50, kButtonHeight))) {
95 if (!is_running) {
96 emu->snes().RunFrame();
97 }
98 }
99 if (ImGui::IsItemHovered()) {
100 ImGui::SetTooltip("Step one frame (F10)");
101 }
102
103 ImGui::SameLine();
104
105 // Reset button
106 if (ImGui::Button(ICON_MD_RESTART_ALT, ImVec2(50, kButtonHeight))) {
107 emu->snes().Reset();
108 LOG_INFO("Emulator", "System reset");
109 }
110 if (ImGui::IsItemHovered()) {
111 ImGui::SetTooltip("Reset SNES (Ctrl+R)");
112 }
113
114 ImGui::SameLine();
115
116 // Load ROM button
117 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load ROM",
118 ImVec2(110, kButtonHeight))) {
119 std::string rom_path = util::FileDialogWrapper::ShowOpenFileDialog();
120 if (!rom_path.empty()) {
121 // Check if it's a valid ROM file extension
122 std::string ext = util::GetFileExtension(rom_path);
123 if (ext == ".sfc" || ext == ".smc" || ext == ".SFC" || ext == ".SMC") {
124 try {
125 // Read ROM file into memory
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>());
131 rom_file.close();
132
133 // Reinitialize emulator with new ROM
134 if (!rom_data.empty()) {
135 emu->Initialize(emu->renderer(), rom_data);
136 LOG_INFO("Emulator", "Loaded ROM: %s (%zu bytes)",
137 util::GetFileName(rom_path).c_str(), rom_data.size());
138 } else {
139 LOG_ERROR("Emulator", "ROM file is empty: %s", rom_path.c_str());
140 }
141 } else {
142 LOG_ERROR("Emulator", "Failed to open ROM file: %s",
143 rom_path.c_str());
144 }
145 } catch (const std::exception& e) {
146 LOG_ERROR("Emulator", "Error loading ROM: %s", e.what());
147 }
148 } else {
149 LOG_WARN("Emulator",
150 "Invalid ROM file extension: %s (expected .sfc or .smc)",
151 ext.c_str());
152 }
153 }
154 }
155 if (ImGui::IsItemHovered()) {
156 ImGui::SetTooltip(
157 "Load a different ROM file\n"
158 "Allows testing hacks with assembly patches applied");
159 }
160
161 ImGui::SameLine();
162 ImGui::Separator();
163 ImGui::SameLine();
164
165 // Debugger toggle
166 bool is_debugging = emu->is_debugging();
167 if (ImGui::Checkbox(ICON_MD_BUG_REPORT " Debug", &is_debugging)) {
168 emu->set_debugging(is_debugging);
169 }
170 if (ImGui::IsItemHovered()) {
171 ImGui::SetTooltip("Enable debugger features");
172 }
173
174 ImGui::SameLine();
175
176 // Recording toggle (for DisassemblyViewer)
177 // Access through emulator's disassembly viewer
178 // bool recording = emu->disassembly_viewer().IsRecording();
179 // if (ImGui::Checkbox(ICON_MD_FIBER_MANUAL_RECORD " Rec", &recording)) {
180 // emu->disassembly_viewer().SetRecording(recording);
181 // }
182 // if (ImGui::IsItemHovered()) {
183 // ImGui::SetTooltip("Record instructions to Disassembly
184 // Viewer\n(Lightweight - uses sparse address map)");
185 // }
186
187 ImGui::SameLine();
188
189 // Turbo mode
190 bool turbo = emu->is_turbo_mode();
191 if (ImGui::Checkbox(ICON_MD_FAST_FORWARD " Turbo", &turbo)) {
192 emu->set_turbo_mode(turbo);
193 }
194 if (ImGui::IsItemHovered()) {
195 ImGui::SetTooltip("Fast forward (shortcut: hold Tab)");
196 }
197
198 ImGui::SameLine();
199 ImGui::Separator();
200 ImGui::SameLine();
201
202 // FPS Counter with color coding
203 double fps = emu->GetCurrentFPS();
204 ImVec4 fps_color;
205 if (fps >= 58.0) {
206 fps_color = ConvertColorToImVec4(theme.success); // Green for good FPS
207 } else if (fps >= 45.0) {
208 fps_color = ConvertColorToImVec4(theme.warning); // Yellow for okay FPS
209 } else {
210 fps_color = ConvertColorToImVec4(theme.error); // Red for bad FPS
211 }
212
213 ImGui::TextColored(fps_color, ICON_MD_SPEED " %.1f FPS", fps);
214
215 ImGui::SameLine();
216
217 // Audio backend status
218 if (emu->audio_backend()) {
219 auto audio_status = emu->audio_backend()->GetStatus();
220 ImVec4 audio_color = audio_status.is_playing
221 ? ConvertColorToImVec4(theme.success)
222 : ConvertColorToImVec4(theme.text_disabled);
223
224 ImGui::TextColored(audio_color, ICON_MD_VOLUME_UP " %s | %u frames",
225 emu->audio_backend()->GetBackendName().c_str(),
226 audio_status.queued_frames);
227
228 if (ImGui::IsItemHovered()) {
229 ImGui::SetTooltip("Audio Backend: %s\nQueued: %u frames\nPlaying: %s",
230 emu->audio_backend()->GetBackendName().c_str(),
231 audio_status.queued_frames,
232 audio_status.is_playing ? "YES" : "NO");
233 }
234
235 ImGui::SameLine();
236 static bool use_sdl_audio_stream = emu->use_sdl_audio_stream();
237 if (ImGui::Checkbox(ICON_MD_SETTINGS " SDL Audio Stream",
238 &use_sdl_audio_stream)) {
239 emu->set_use_sdl_audio_stream(use_sdl_audio_stream);
240 }
241 if (ImGui::IsItemHovered()) {
242 ImGui::SetTooltip("Use SDL audio stream for audio");
243 }
244 } else {
245 ImGui::TextColored(ConvertColorToImVec4(theme.error),
246 ICON_MD_VOLUME_OFF " No Backend");
247 }
248
249 ImGui::SameLine();
250 ImGui::Separator();
251 ImGui::SameLine();
252
253 // Input capture status indicator (like modern emulators)
254 ImGuiIO& io = ImGui::GetIO();
255 const auto input_config = emu->input_manager().GetConfig();
256 if (io.WantCaptureKeyboard && !input_config.ignore_imgui_text_input) {
257 // ImGui is capturing keyboard (typing in UI)
258 ImGui::TextColored(ConvertColorToImVec4(theme.warning),
259 ICON_MD_KEYBOARD " UI");
260 if (ImGui::IsItemHovered()) {
261 ImGui::SetTooltip("Keyboard captured by UI\nGame input disabled");
262 }
263 } else {
264 // Emulator can receive input
265 ImVec4 state_color = input_config.ignore_imgui_text_input
266 ? ConvertColorToImVec4(theme.accent)
267 : ConvertColorToImVec4(theme.success);
268 ImGui::TextColored(
269 state_color,
270 input_config.ignore_imgui_text_input
271 ? ICON_MD_SPORTS_ESPORTS " Game (Forced)"
272 : ICON_MD_SPORTS_ESPORTS " Game");
273 if (ImGui::IsItemHovered()) {
274 ImGui::SetTooltip(
275 input_config.ignore_imgui_text_input
276 ? "Game input forced on (ignores ImGui text capture)\nPress F1 "
277 "for controls"
278 : "Game input active\nPress F1 for controls");
279 }
280 }
281
282 ImGui::SameLine();
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;
287 emu->SetInputConfig(cfg);
288 }
289 if (ImGui::IsItemHovered()) {
290 ImGui::SetTooltip(
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.");
293 }
294
295 ImGui::SameLine();
296
297 // Option to disable ImGui keyboard navigation (prevents Tab from cycling UI)
298 static bool disable_nav = false;
299 if (ImGui::Checkbox("Disable Nav", &disable_nav)) {
300 ImGuiIO& imgui_io = ImGui::GetIO();
301 if (disable_nav) {
302 imgui_io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard;
303 } else {
304 imgui_io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
305 }
306 }
307 if (ImGui::IsItemHovered()) {
308 ImGui::SetTooltip(
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.");
312 }
313
314 ImGui::PopStyleColor(3);
315}
316
318 if (!emu)
319 return;
320
321 auto& theme_manager = ThemeManager::Get();
322 const auto& theme = theme_manager.GetCurrentTheme();
323
324 ImGui::PushStyleColor(ImGuiCol_ChildBg,
325 ConvertColorToImVec4(theme.editor_background));
326 ImGui::BeginChild(
327 "##SNES_PPU", ImVec2(0, 0), 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 ImGui::EndChild();
384 ImGui::PopStyleColor();
385}
386
388 if (!emu)
389 return;
390
391 auto& theme_manager = ThemeManager::Get();
392 const auto& theme = theme_manager.GetCurrentTheme();
393
394 ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg));
395 ImGui::BeginChild("##Performance", ImVec2(0, 0), true);
396
397 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
398 ICON_MD_SPEED " Performance Monitor");
399 AddSectionSpacing();
400
401 auto metrics = emu->GetMetrics();
402
403 // FPS Graph
404 if (ImGui::CollapsingHeader(ICON_MD_SHOW_CHART " Frame Rate",
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);
409
410 const float target_ms =
411 emu->snes().memory().pal_timing() ? 1000.0f / 50.0f : 1000.0f / 60.0f;
412 auto frame_ms = emu->FrameTimeHistory();
413 auto fps_history = emu->FpsHistory();
414 if (!frame_ms.empty()) {
415 plotting::PlotStyleScope plot_style(theme);
417 .id = "Frame Times",
418 .y_label = "ms",
419 .flags = ImPlotFlags_NoLegend,
420 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
421 ImPlotAxisFlags_NoGridLines |
422 ImPlotAxisFlags_NoTickMarks,
423 .y_axis_flags = ImPlotAxisFlags_AutoFit};
424 plotting::PlotGuard plot(config);
425 if (plot) {
426 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, target_ms * 2.5f,
427 ImGuiCond_Always);
428 ImPlot::PlotLine("Frame ms", frame_ms.data(),
429 static_cast<int>(frame_ms.size()));
430 ImPlot::PlotInfLines("Target", &target_ms, 1);
431 }
432 }
433
434 if (!fps_history.empty()) {
435 plotting::PlotStyleScope plot_style(theme);
436 plotting::PlotConfig fps_config{
437 .id = "FPS History",
438 .y_label = "fps",
439 .flags = ImPlotFlags_NoLegend,
440 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
441 ImPlotAxisFlags_NoGridLines |
442 ImPlotAxisFlags_NoTickMarks,
443 .y_axis_flags = ImPlotAxisFlags_AutoFit};
444 plotting::PlotGuard plot(fps_config);
445 if (plot) {
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
450 : 60.0f;
451 ImPlot::PlotInfLines("Target", &target_fps, 1);
452 }
453 }
454 }
455
456 if (ImGui::CollapsingHeader(ICON_MD_DATA_USAGE " DMA / VRAM Activity",
457 ImGuiTreeNodeFlags_DefaultOpen)) {
458 auto dma_hist = emu->DmaBytesHistory();
459 auto vram_hist = emu->VramBytesHistory();
460 if (!dma_hist.empty() || !vram_hist.empty()) {
461 plotting::PlotStyleScope plot_style(theme);
462 plotting::PlotConfig dma_config{
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};
470 plotting::PlotGuard plot(dma_config);
471 if (plot) {
472 // Calculate max_val before any plotting to avoid locking setup
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()));
477 }
478 if (!vram_hist.empty()) {
479 max_val = std::max(max_val, *std::max_element(vram_hist.begin(),
480 vram_hist.end()));
481 }
482 // Setup must be called before any PlotX functions
483 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, max_val * 1.2f,
484 ImGuiCond_Always);
485 // Now do the plotting
486 if (!dma_hist.empty()) {
487 ImPlot::PlotLine("DMA", dma_hist.data(),
488 static_cast<int>(dma_hist.size()));
489 }
490 if (!vram_hist.empty()) {
491 ImPlot::PlotLine("VRAM", vram_hist.data(),
492 static_cast<int>(vram_hist.size()));
493 }
494 }
495 } else {
496 ImGui::TextDisabled("No DMA activity recorded yet");
497 }
498 }
499
500 if (ImGui::CollapsingHeader(ICON_MD_STORAGE " ROM Free Space",
501 ImGuiTreeNodeFlags_DefaultOpen)) {
502 auto free_bytes = emu->RomBankFreeBytes();
503 if (!free_bytes.empty()) {
504 plotting::PlotStyleScope plot_style(theme);
505 plotting::PlotConfig free_config{
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};
511 plotting::PlotGuard plot(free_config);
512 if (plot) {
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]);
518 }
519 ImPlot::PlotBars("Free", x.data(), y.data(),
520 static_cast<int>(free_bytes.size()), 0.67, 0.0,
521 ImPlotBarsFlags_None);
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(ImAxis_Y1, 0.0f,
559 std::max(512.0f,
560 *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()));
566 }
567 }
568
569 auto audio_l = emu->AudioRmsLeftHistory();
570 auto audio_r = emu->AudioRmsRightHistory();
571 if (!audio_l.empty() || !audio_r.empty()) {
572 plotting::PlotStyleScope plot_style(theme);
573 plotting::PlotConfig audio_level_config{
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};
581 plotting::PlotGuard plot(audio_level_config);
582 if (plot) {
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()));
587 }
588 if (!audio_r.empty()) {
589 ImPlot::PlotLine("R", audio_r.data(),
590 static_cast<int>(audio_r.size()));
591 }
592 }
593 }
594 } else {
595 ImGui::TextColored(ConvertColorToImVec4(theme.error), "No audio backend");
596 }
597 }
598
599 ImGui::EndChild();
600 ImGui::PopStyleColor();
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 ImGui::PushStyleColor(ImGuiCol_TitleBg, ConvertColorToImVec4(theme.accent));
616 ImGui::PushStyleColor(ImGuiCol_TitleBgActive,
617 ConvertColorToImVec4(theme.accent));
618
619 if (ImGui::Begin(ICON_MD_KEYBOARD " Keyboard Shortcuts", show,
620 ImGuiWindowFlags_NoCollapse)) {
621 // Emulator controls section
622 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
623 ICON_MD_VIDEOGAME_ASSET " Emulator Controls");
624 ImGui::Separator();
625 ImGui::Spacing();
626
627 if (ImGui::BeginTable("EmulatorControls", 2, ImGuiTableFlags_Borders)) {
628 ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthFixed, 120);
629 ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthStretch);
630 ImGui::TableHeadersRow();
631
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);
638 };
639
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");
645
646 ImGui::EndTable();
647 }
648
649 ImGui::Spacing();
650 ImGui::Spacing();
651
652 // Game controls section
653 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
654 ICON_MD_SPORTS_ESPORTS " SNES Controller");
655 ImGui::Separator();
656 ImGui::Spacing();
657
658 if (ImGui::BeginTable("GameControls", 2, ImGuiTableFlags_Borders)) {
659 ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthFixed, 120);
660 ImGui::TableSetupColumn("Button", ImGuiTableColumnFlags_WidthStretch);
661 ImGui::TableHeadersRow();
662
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);
669 };
670
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");
680
681 ImGui::EndTable();
682 }
683
684 ImGui::Spacing();
685 ImGui::Spacing();
686
687 // Tips section
688 ImGui::TextColored(ConvertColorToImVec4(theme.info), ICON_MD_INFO " Tips");
689 ImGui::Separator();
690 ImGui::Spacing();
691
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");
696
697 ImGui::Spacing();
698 ImGui::Spacing();
699
700 if (ImGui::Button("Close", ImVec2(-1, 30))) {
701 *show = false;
702 }
703 }
704 ImGui::End();
705
706 ImGui::PopStyleColor(2);
707}
708
710 if (!emu)
711 return;
712
713 auto& theme_manager = ThemeManager::Get();
714 const auto& theme = theme_manager.GetCurrentTheme();
715
716 // Main layout
717 ImGui::PushStyleColor(ImGuiCol_ChildBg,
718 ConvertColorToImVec4(theme.window_bg));
719
720 RenderNavBar(emu);
721
722 ImGui::Separator();
723
724 // Main content area
725 RenderSnesPpu(emu);
726
727 // Keyboard shortcuts overlay (F1 to toggle)
728 static bool show_shortcuts = false;
729 if (ImGui::IsKeyPressed(ImGuiKey_F1)) {
730 show_shortcuts = !show_shortcuts;
731 }
732 RenderKeyboardShortcuts(&show_shortcuts);
733
734 // Tab key: Hold for turbo mode
735 // Use SDL directly to bypass ImGui's keyboard navigation capture
736 // Use SDL directly to bypass ImGui's keyboard navigation capture
737 platform::KeyboardState keyboard_state = SDL_GetKeyboardState(nullptr);
738 bool tab_pressed = platform::IsKeyPressed(keyboard_state, SDL_SCANCODE_TAB);
739 emu->set_turbo_mode(tab_pressed);
740
741 ImGui::PopStyleColor();
742}
743
745 if (!emu)
746 return;
747
748 auto& theme_manager = ThemeManager::Get();
749 const auto& theme = theme_manager.GetCurrentTheme();
750
751 ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg));
752 ImGui::BeginChild("##VirtualController", ImVec2(0, 0), true);
753
754 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
755 ICON_MD_SPORTS_ESPORTS " Virtual Controller");
756 ImGui::SameLine();
757 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
758 "(Click to test input)");
759 ImGui::Separator();
760 ImGui::Spacing();
761
762 auto& input_mgr = emu->input_manager();
763
764 // Track which buttons are currently pressed via virtual controller
765 // Use a static to persist state across frames
766 static uint16_t virtual_buttons_pressed = 0;
767
768 // Helper lambda for controller buttons - press on mouse down, release on up
769 auto ControllerButton = [&](const char* label, input::SnesButton button,
770 ImVec2 size = ImVec2(50, 40)) {
771 ImGui::PushID(static_cast<int>(button));
772
773 uint16_t button_mask = 1 << static_cast<uint8_t>(button);
774 bool was_pressed = (virtual_buttons_pressed & button_mask) != 0;
775
776 // Style the button if it's currently pressed
777 if (was_pressed) {
778 ImGui::PushStyleColor(ImGuiCol_Button,
779 ConvertColorToImVec4(theme.accent));
780 }
781
782 // Render the button
783 ImGui::Button(label, size);
784
785 // Check if mouse is held down on THIS button (after rendering)
786 bool is_active = ImGui::IsItemActive();
787
788 // Update virtual button state
789 if (is_active) {
790 virtual_buttons_pressed |= button_mask;
791 input_mgr.PressButton(button);
792 } else if (was_pressed) {
793 // Only release if we were the ones who pressed it
794 virtual_buttons_pressed &= ~button_mask;
795 input_mgr.ReleaseButton(button);
796 }
797
798 if (was_pressed) {
799 ImGui::PopStyleColor();
800 }
801
802 ImGui::PopID();
803 };
804
805 // D-Pad layout
806 ImGui::Text("D-Pad:");
807 ImGui::Indent();
808
809 // Up
810 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
812
813 // Left, (space), Right
815 ImGui::SameLine();
816 ImGui::Dummy(ImVec2(50, 40));
817 ImGui::SameLine();
819
820 // Down
821 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
823
824 ImGui::Unindent();
825 ImGui::Spacing();
826
827 // Face buttons (SNES layout: Y B on left, X A on right)
828 ImGui::Text("Face Buttons:");
829 ImGui::Indent();
830
831 // Top row: X
832 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
833 ControllerButton("X", input::SnesButton::X);
834
835 // Middle row: Y, A
836 ControllerButton("Y", input::SnesButton::Y);
837 ImGui::SameLine();
838 ImGui::Dummy(ImVec2(50, 40));
839 ImGui::SameLine();
840 ControllerButton("A", input::SnesButton::A);
841
842 // Bottom row: B
843 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
844 ControllerButton("B", input::SnesButton::B);
845
846 ImGui::Unindent();
847 ImGui::Spacing();
848
849 // Shoulder buttons
850 ImGui::Text("Shoulder:");
851 ControllerButton("L", input::SnesButton::L, ImVec2(70, 30));
852 ImGui::SameLine();
853 ControllerButton("R", input::SnesButton::R, ImVec2(70, 30));
854
855 ImGui::Spacing();
856
857 // Start/Select
858 ImGui::Text("Start/Select:");
859 ControllerButton("Select", input::SnesButton::SELECT, ImVec2(70, 30));
860 ImGui::SameLine();
861 ControllerButton("Start", input::SnesButton::START, ImVec2(70, 30));
862
863 ImGui::Spacing();
864 ImGui::Separator();
865
866 // Debug info - show current button state
867 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), "Debug:");
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");
872
873 // Show which buttons are detected
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");
888 }
889
890 ImGui::Spacing();
891
892 // Show what the game actually reads ($4218/$4219)
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");
897
898 // Show $4218/$4219 values (what game reads)
899 uint8_t reg_4218 = port_read & 0xFF; // Low byte
900 uint8_t reg_4219 = (port_read >> 8) & 0xFF; // High byte
901 ImGui::Text("$4218: 0x%02X $4219: 0x%02X", reg_4218, reg_4219);
902
903 // Decode $4218 (A, X, L, R in bits 7-4, unused 3-0)
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);
907
908 // Edge detection debug - track state changes
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;
913
914 bool a_now = (port_read & 0x0080) != 0;
915 bool a_before = (last_port_read & 0x0080) != 0;
916
917 if (a_now && !a_before) {
918 detected_edge = true;
919 frames_a_pressed = 1;
920 frames_since_release = 0;
921 } else if (a_now) {
922 frames_a_pressed++;
923 detected_edge = false;
924 } else {
925 frames_a_pressed = 0;
926 frames_since_release++;
927 detected_edge = false;
928 }
929
930 last_port_read = port_read;
931
932 ImGui::Spacing();
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);
936 if (detected_edge) {
937 ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), ">>> EDGE DETECTED <<<");
938 }
939
940 ImGui::EndChild();
941 ImGui::PopStyleColor();
942}
943
944} // namespace ui
945} // namespace emu
946} // namespace yaze
A class for emulating and debugging SNES games.
Definition emulator.h:39
gfx::IRenderer * renderer()
Definition emulator.h:100
void Initialize(gfx::IRenderer *renderer, const std::vector< uint8_t > &rom_data)
Definition emulator.cc:108
bool is_debugging() const
Definition emulator.h:119
std::vector< float > FrameTimeHistory() const
Definition emulator.cc:782
void set_turbo_mode(bool turbo)
Definition emulator.h:105
void SetInputConfig(const input::InputConfig &config)
Definition emulator.cc:74
void * ppu_texture()
Definition emulator.h:101
void set_use_sdl_audio_stream(bool enabled)
Definition emulator.cc:79
double GetCurrentFPS() const
Definition emulator.h:126
std::vector< float > RomBankFreeBytes() const
Definition emulator.cc:817
void set_debugging(bool debugging)
Definition emulator.h:120
std::vector< float > AudioRmsRightHistory() const
Definition emulator.cc:812
std::vector< float > VramBytesHistory() const
Definition emulator.cc:802
std::vector< float > AudioQueueHistory() const
Definition emulator.cc:792
std::vector< float > DmaBytesHistory() const
Definition emulator.cc:797
bool use_sdl_audio_stream() const
Definition emulator.h:89
input::InputManager & input_manager()
Definition emulator.h:118
bool is_turbo_mode() const
Definition emulator.h:104
std::vector< float > FpsHistory() const
Definition emulator.cc:787
void set_running(bool running)
Definition emulator.h:60
bool is_snes_initialized() const
Definition emulator.h:122
std::vector< float > AudioRmsLeftHistory() const
Definition emulator.cc:807
audio::IAudioBackend * audio_backend()
Definition emulator.h:74
auto snes() -> Snes &
Definition emulator.h:58
auto running() const -> bool
Definition emulator.h:59
EmulatorMetrics GetMetrics()
Definition emulator.h:146
virtual std::string GetBackendName() const =0
virtual AudioStatus GetStatus() const =0
InputConfig GetConfig() const
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:23
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
std::string GetFileExtension(const std::string &filename)
Gets the file extension from a filename.
Definition file_util.cc:15
SDL2/SDL3 compatibility layer.