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(
121 if (!rom_path.empty()) {
122 // Check if it's a valid ROM file extension
123 std::string ext = util::GetFileExtension(rom_path);
124 if (ext == ".sfc" || ext == ".smc" || ext == ".SFC" || ext == ".SMC") {
125 try {
126 // Read ROM file into memory
127 std::ifstream rom_file(rom_path, std::ios::binary);
128 if (rom_file.good()) {
129 std::vector<uint8_t> rom_data(
130 (std::istreambuf_iterator<char>(rom_file)),
131 std::istreambuf_iterator<char>());
132 rom_file.close();
133
134 // Reinitialize emulator with new ROM
135 if (!rom_data.empty()) {
136 emu->Initialize(emu->renderer(), rom_data);
137 LOG_INFO("Emulator", "Loaded ROM: %s (%zu bytes)",
138 util::GetFileName(rom_path).c_str(), rom_data.size());
139 } else {
140 LOG_ERROR("Emulator", "ROM file is empty: %s", rom_path.c_str());
141 }
142 } else {
143 LOG_ERROR("Emulator", "Failed to open ROM file: %s",
144 rom_path.c_str());
145 }
146 } catch (const std::exception& e) {
147 LOG_ERROR("Emulator", "Error loading ROM: %s", e.what());
148 }
149 } else {
150 LOG_WARN("Emulator",
151 "Invalid ROM file extension: %s (expected .sfc or .smc)",
152 ext.c_str());
153 }
154 }
155 }
156 if (ImGui::IsItemHovered()) {
157 ImGui::SetTooltip(
158 "Load a different ROM file\n"
159 "Allows testing hacks with assembly patches applied");
160 }
161
162 ImGui::SameLine();
163 ImGui::Separator();
164 ImGui::SameLine();
165
166 // Debugger toggle
167 bool is_debugging = emu->is_debugging();
168 if (ImGui::Checkbox(ICON_MD_BUG_REPORT " Debug", &is_debugging)) {
169 emu->set_debugging(is_debugging);
170 }
171 if (ImGui::IsItemHovered()) {
172 ImGui::SetTooltip("Enable debugger features");
173 }
174
175 ImGui::SameLine();
176
177 // Recording toggle (for DisassemblyViewer)
178 // Access through emulator's disassembly viewer
179 // bool recording = emu->disassembly_viewer().IsRecording();
180 // if (ImGui::Checkbox(ICON_MD_FIBER_MANUAL_RECORD " Rec", &recording)) {
181 // emu->disassembly_viewer().SetRecording(recording);
182 // }
183 // if (ImGui::IsItemHovered()) {
184 // ImGui::SetTooltip("Record instructions to Disassembly
185 // Viewer\n(Lightweight - uses sparse address map)");
186 // }
187
188 ImGui::SameLine();
189
190 // Turbo mode
191 bool turbo = emu->is_turbo_mode();
192 if (ImGui::Checkbox(ICON_MD_FAST_FORWARD " Turbo", &turbo)) {
193 emu->set_turbo_mode(turbo);
194 }
195 if (ImGui::IsItemHovered()) {
196 ImGui::SetTooltip("Fast forward (shortcut: hold Tab)");
197 }
198
199 ImGui::SameLine();
200 ImGui::Separator();
201 ImGui::SameLine();
202
203 // FPS Counter with color coding
204 double fps = emu->GetCurrentFPS();
205 ImVec4 fps_color;
206 if (fps >= 58.0) {
207 fps_color = ConvertColorToImVec4(theme.success); // Green for good FPS
208 } else if (fps >= 45.0) {
209 fps_color = ConvertColorToImVec4(theme.warning); // Yellow for okay FPS
210 } else {
211 fps_color = ConvertColorToImVec4(theme.error); // Red for bad FPS
212 }
213
214 ImGui::TextColored(fps_color, ICON_MD_SPEED " %.1f FPS", fps);
215
216 ImGui::SameLine();
217
218 // Audio backend status
219 if (emu->audio_backend()) {
220 auto audio_status = emu->audio_backend()->GetStatus();
221 ImVec4 audio_color = audio_status.is_playing
222 ? ConvertColorToImVec4(theme.success)
223 : ConvertColorToImVec4(theme.text_disabled);
224
225 ImGui::TextColored(audio_color, ICON_MD_VOLUME_UP " %s | %u frames",
226 emu->audio_backend()->GetBackendName().c_str(),
227 audio_status.queued_frames);
228
229 if (ImGui::IsItemHovered()) {
230 ImGui::SetTooltip("Audio Backend: %s\nQueued: %u frames\nPlaying: %s",
231 emu->audio_backend()->GetBackendName().c_str(),
232 audio_status.queued_frames,
233 audio_status.is_playing ? "YES" : "NO");
234 }
235
236 ImGui::SameLine();
237 static bool use_sdl_audio_stream = emu->use_sdl_audio_stream();
238 if (ImGui::Checkbox(ICON_MD_SETTINGS " SDL Audio Stream",
239 &use_sdl_audio_stream)) {
240 emu->set_use_sdl_audio_stream(use_sdl_audio_stream);
241 }
242 if (ImGui::IsItemHovered()) {
243 ImGui::SetTooltip("Use SDL audio stream for audio");
244 }
245 } else {
246 ImGui::TextColored(ConvertColorToImVec4(theme.error),
247 ICON_MD_VOLUME_OFF " No Backend");
248 }
249
250 ImGui::SameLine();
251 ImGui::Separator();
252 ImGui::SameLine();
253
254 // Input capture status indicator (like modern emulators)
255 ImGuiIO& io = ImGui::GetIO();
256 const auto input_config = emu->input_manager().GetConfig();
257 if (io.WantCaptureKeyboard && !input_config.ignore_imgui_text_input) {
258 // ImGui is capturing keyboard (typing in UI)
259 ImGui::TextColored(ConvertColorToImVec4(theme.warning),
260 ICON_MD_KEYBOARD " UI");
261 if (ImGui::IsItemHovered()) {
262 ImGui::SetTooltip("Keyboard captured by UI\nGame input disabled");
263 }
264 } else {
265 // Emulator can receive input
266 ImVec4 state_color = input_config.ignore_imgui_text_input
267 ? ConvertColorToImVec4(theme.accent)
268 : ConvertColorToImVec4(theme.success);
269 ImGui::TextColored(
270 state_color,
271 input_config.ignore_imgui_text_input
272 ? ICON_MD_SPORTS_ESPORTS " 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 ImGui::PopStyleColor(3);
316}
317
319 if (!emu)
320 return;
321
322 auto& theme_manager = ThemeManager::Get();
323 const auto& theme = theme_manager.GetCurrentTheme();
324
325 ImGui::PushStyleColor(ImGuiCol_ChildBg,
326 ConvertColorToImVec4(theme.editor_background));
327 ImGui::BeginChild(
328 "##SNES_PPU", ImVec2(0, 0), true,
329 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
330
331 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
332 ImVec2 snes_size = ImVec2(512, 480);
333
334 if (emu->is_snes_initialized() && emu->ppu_texture()) {
335 // Center the SNES display with aspect ratio preservation
336 float aspect = snes_size.x / snes_size.y;
337 float display_w = canvas_size.x;
338 float display_h = display_w / aspect;
339
340 if (display_h > canvas_size.y) {
341 display_h = canvas_size.y;
342 display_w = display_h * aspect;
343 }
344
345 float pos_x = (canvas_size.x - display_w) * 0.5f;
346 float pos_y = (canvas_size.y - display_h) * 0.5f;
347
348 ImGui::SetCursorPos(ImVec2(pos_x, pos_y));
349
350 // Render PPU texture with click detection for focus
351 ImGui::Image((ImTextureID)(intptr_t)emu->ppu_texture(),
352 ImVec2(display_w, display_h), ImVec2(0, 0), ImVec2(1, 1));
353
354 // Allow clicking on the display to ensure focus
355 // Modern emulators make the game area "sticky" for input
356 if (ImGui::IsItemHovered()) {
357 // ImGui::SetTooltip("Click to ensure game input focus");
358
359 // Visual feedback when hovered (subtle border)
360 ImDrawList* draw_list = ImGui::GetWindowDrawList();
361 ImVec2 screen_pos = ImGui::GetItemRectMin();
362 ImVec2 screen_size = ImGui::GetItemRectMax();
363 draw_list->AddRect(
364 screen_pos, screen_size,
365 ImGui::ColorConvertFloat4ToU32(ConvertColorToImVec4(theme.accent)),
366 0.0f, 0, 2.0f);
367 }
368 } else {
369 // Not initialized - show helpful placeholder
370 ImVec2 text_size = ImGui::CalcTextSize("Load a ROM to start emulation");
371 ImGui::SetCursorPos(ImVec2((canvas_size.x - text_size.x) * 0.5f,
372 (canvas_size.y - text_size.y) * 0.5f - 20));
373 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
375 ImGui::SetCursorPosX((canvas_size.x - text_size.x) * 0.5f);
376 ImGui::TextColored(ConvertColorToImVec4(theme.text_primary),
377 "Load a ROM to start emulation");
378 ImGui::SetCursorPosX(
379 (canvas_size.x - ImGui::CalcTextSize("512x480 SNES output").x) * 0.5f);
380 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
381 "512x480 SNES output");
382 }
383
384 ImGui::EndChild();
385 ImGui::PopStyleColor();
386}
387
389 if (!emu)
390 return;
391
392 auto& theme_manager = ThemeManager::Get();
393 const auto& theme = theme_manager.GetCurrentTheme();
394
395 ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg));
396 ImGui::BeginChild("##Performance", ImVec2(0, 0), true);
397
398 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
399 ICON_MD_SPEED " Performance Monitor");
400 AddSectionSpacing();
401
402 auto metrics = emu->GetMetrics();
403
404 // FPS Graph
405 if (ImGui::CollapsingHeader(ICON_MD_SHOW_CHART " Frame Rate",
406 ImGuiTreeNodeFlags_DefaultOpen)) {
407 ImGui::Text("Current: %.2f FPS", metrics.fps);
408 ImGui::Text("Target: %.2f FPS",
409 emu->snes().memory().pal_timing() ? 50.0 : 60.0);
410
411 const float target_ms =
412 emu->snes().memory().pal_timing() ? 1000.0f / 50.0f : 1000.0f / 60.0f;
413 auto frame_ms = emu->FrameTimeHistory();
414 auto fps_history = emu->FpsHistory();
415 if (!frame_ms.empty()) {
416 plotting::PlotStyleScope plot_style(theme);
418 .id = "Frame Times",
419 .y_label = "ms",
420 .flags = ImPlotFlags_NoLegend,
421 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
422 ImPlotAxisFlags_NoGridLines |
423 ImPlotAxisFlags_NoTickMarks,
424 .y_axis_flags = ImPlotAxisFlags_AutoFit};
425 plotting::PlotGuard plot(config);
426 if (plot) {
427 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, target_ms * 2.5f,
428 ImGuiCond_Always);
429 ImPlot::PlotLine("Frame ms", frame_ms.data(),
430 static_cast<int>(frame_ms.size()));
431 ImPlot::PlotInfLines("Target", &target_ms, 1);
432 }
433 }
434
435 if (!fps_history.empty()) {
436 plotting::PlotStyleScope plot_style(theme);
437 plotting::PlotConfig fps_config{
438 .id = "FPS History",
439 .y_label = "fps",
440 .flags = ImPlotFlags_NoLegend,
441 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
442 ImPlotAxisFlags_NoGridLines |
443 ImPlotAxisFlags_NoTickMarks,
444 .y_axis_flags = ImPlotAxisFlags_AutoFit};
445 plotting::PlotGuard plot(fps_config);
446 if (plot) {
447 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, 75.0f, ImGuiCond_Always);
448 ImPlot::PlotLine("FPS", fps_history.data(),
449 static_cast<int>(fps_history.size()));
450 const float target_fps = emu->snes().memory().pal_timing() ? 50.0f
451 : 60.0f;
452 ImPlot::PlotInfLines("Target", &target_fps, 1);
453 }
454 }
455 }
456
457 if (ImGui::CollapsingHeader(ICON_MD_DATA_USAGE " DMA / VRAM Activity",
458 ImGuiTreeNodeFlags_DefaultOpen)) {
459 auto dma_hist = emu->DmaBytesHistory();
460 auto vram_hist = emu->VramBytesHistory();
461 if (!dma_hist.empty() || !vram_hist.empty()) {
462 plotting::PlotStyleScope plot_style(theme);
463 plotting::PlotConfig dma_config{
464 .id = "DMA/VRAM Bytes",
465 .y_label = "bytes/frame",
466 .flags = ImPlotFlags_NoLegend,
467 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
468 ImPlotAxisFlags_NoGridLines |
469 ImPlotAxisFlags_NoTickMarks,
470 .y_axis_flags = ImPlotAxisFlags_AutoFit};
471 plotting::PlotGuard plot(dma_config);
472 if (plot) {
473 // Calculate max_val before any plotting to avoid locking setup
474 float max_val = 512.0f;
475 if (!dma_hist.empty()) {
476 max_val = std::max(max_val,
477 *std::max_element(dma_hist.begin(), dma_hist.end()));
478 }
479 if (!vram_hist.empty()) {
480 max_val = std::max(max_val, *std::max_element(vram_hist.begin(),
481 vram_hist.end()));
482 }
483 // Setup must be called before any PlotX functions
484 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f, max_val * 1.2f,
485 ImGuiCond_Always);
486 // Now do the plotting
487 if (!dma_hist.empty()) {
488 ImPlot::PlotLine("DMA", dma_hist.data(),
489 static_cast<int>(dma_hist.size()));
490 }
491 if (!vram_hist.empty()) {
492 ImPlot::PlotLine("VRAM", vram_hist.data(),
493 static_cast<int>(vram_hist.size()));
494 }
495 }
496 } else {
497 ImGui::TextDisabled("No DMA activity recorded yet");
498 }
499 }
500
501 if (ImGui::CollapsingHeader(ICON_MD_STORAGE " ROM Free Space",
502 ImGuiTreeNodeFlags_DefaultOpen)) {
503 auto free_bytes = emu->RomBankFreeBytes();
504 if (!free_bytes.empty()) {
505 plotting::PlotStyleScope plot_style(theme);
506 plotting::PlotConfig free_config{
507 .id = "ROM Free Bytes",
508 .y_label = "bytes (0xFF)",
509 .flags = ImPlotFlags_NoLegend | ImPlotFlags_NoBoxSelect,
510 .x_axis_flags = ImPlotAxisFlags_AutoFit,
511 .y_axis_flags = ImPlotAxisFlags_AutoFit};
512 plotting::PlotGuard plot(free_config);
513 if (plot) {
514 std::vector<double> x(free_bytes.size());
515 std::vector<double> y(free_bytes.size());
516 for (size_t i = 0; i < free_bytes.size(); ++i) {
517 x[i] = static_cast<double>(i);
518 y[i] = static_cast<double>(free_bytes[i]);
519 }
520 ImPlot::PlotBars("Free", x.data(), y.data(),
521 static_cast<int>(free_bytes.size()), 0.67, 0.0,
522 ImPlotBarsFlags_None);
523 }
524 } else {
525 ImGui::TextDisabled("Load a ROM to analyze free space.");
526 }
527 }
528
529 // CPU Stats
530 if (ImGui::CollapsingHeader(ICON_MD_MEMORY " CPU Status",
531 ImGuiTreeNodeFlags_DefaultOpen)) {
532 ImGui::Text("PC: $%02X:%04X", metrics.cpu_pb, metrics.cpu_pc);
533 ImGui::Text("Cycles: %llu", metrics.cycles);
534 }
535
536 // Audio Stats
537 if (ImGui::CollapsingHeader(ICON_MD_AUDIOTRACK " Audio Status",
538 ImGuiTreeNodeFlags_DefaultOpen)) {
539 if (emu->audio_backend()) {
540 auto audio_status = emu->audio_backend()->GetStatus();
541 ImGui::Text("Backend: %s",
542 emu->audio_backend()->GetBackendName().c_str());
543 ImGui::Text("Queued: %u frames", audio_status.queued_frames);
544 ImGui::Text("Playing: %s", audio_status.is_playing ? "YES" : "NO");
545
546 auto audio_history = emu->AudioQueueHistory();
547 if (!audio_history.empty()) {
548 plotting::PlotStyleScope plot_style(theme);
549 plotting::PlotConfig audio_config{
550 .id = "Audio Queue Depth",
551 .y_label = "frames",
552 .flags = ImPlotFlags_NoLegend,
553 .x_axis_flags = ImPlotAxisFlags_NoTickLabels |
554 ImPlotAxisFlags_NoGridLines |
555 ImPlotAxisFlags_NoTickMarks,
556 .y_axis_flags = ImPlotAxisFlags_AutoFit};
557 plotting::PlotGuard plot(audio_config);
558 if (plot) {
559 ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0f,
560 std::max(512.0f,
561 *std::max_element(audio_history.begin(),
562 audio_history.end()) *
563 1.2f),
564 ImGuiCond_Always);
565 ImPlot::PlotLine("Queued", audio_history.data(),
566 static_cast<int>(audio_history.size()));
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_NoLegend,
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()));
588 }
589 if (!audio_r.empty()) {
590 ImPlot::PlotLine("R", audio_r.data(),
591 static_cast<int>(audio_r.size()));
592 }
593 }
594 }
595 } else {
596 ImGui::TextColored(ConvertColorToImVec4(theme.error), "No audio backend");
597 }
598 }
599
600 ImGui::EndChild();
601 ImGui::PopStyleColor();
602}
603
604void RenderKeyboardShortcuts(bool* show) {
605 if (!show || !*show)
606 return;
607
608 auto& theme_manager = ThemeManager::Get();
609 const auto& theme = theme_manager.GetCurrentTheme();
610
611 // Center the window
612 ImVec2 center = ImGui::GetMainViewport()->GetCenter();
613 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
614 ImGui::SetNextWindowSize(ImVec2(550, 600), ImGuiCond_Appearing);
615
616 ImGui::PushStyleColor(ImGuiCol_TitleBg, ConvertColorToImVec4(theme.accent));
617 ImGui::PushStyleColor(ImGuiCol_TitleBgActive,
618 ConvertColorToImVec4(theme.accent));
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 ImGui::PopStyleColor(2);
708}
709
711 if (!emu)
712 return;
713
714 auto& theme_manager = ThemeManager::Get();
715 const auto& theme = theme_manager.GetCurrentTheme();
716
717 // Main layout
718 ImGui::PushStyleColor(ImGuiCol_ChildBg,
719 ConvertColorToImVec4(theme.window_bg));
720
721 RenderNavBar(emu);
722
723 ImGui::Separator();
724
725 // Main content area
726 RenderSnesPpu(emu);
727
728 // Keyboard shortcuts overlay (F1 to toggle)
729 static bool show_shortcuts = false;
730 if (ImGui::IsKeyPressed(ImGuiKey_F1)) {
731 show_shortcuts = !show_shortcuts;
732 }
733 RenderKeyboardShortcuts(&show_shortcuts);
734
735 // Tab key: Hold for turbo mode
736 // Use SDL directly to bypass ImGui's keyboard navigation capture
737 // Use SDL directly to bypass ImGui's keyboard navigation capture
738 platform::KeyboardState keyboard_state = SDL_GetKeyboardState(nullptr);
739 bool tab_pressed = platform::IsKeyPressed(keyboard_state, SDL_SCANCODE_TAB);
740 emu->set_turbo_mode(tab_pressed);
741
742 ImGui::PopStyleColor();
743}
744
746 if (!emu)
747 return;
748
749 auto& theme_manager = ThemeManager::Get();
750 const auto& theme = theme_manager.GetCurrentTheme();
751
752 ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg));
753 ImGui::BeginChild("##VirtualController", ImVec2(0, 0), true);
754
755 ImGui::TextColored(ConvertColorToImVec4(theme.accent),
756 ICON_MD_SPORTS_ESPORTS " Virtual Controller");
757 ImGui::SameLine();
758 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
759 "(Click to test input)");
760 ImGui::Separator();
761 ImGui::Spacing();
762
763 auto& input_mgr = emu->input_manager();
764
765 // Track which buttons are currently pressed via virtual controller
766 // Use a static to persist state across frames
767 static uint16_t virtual_buttons_pressed = 0;
768
769 // Helper lambda for controller buttons - press on mouse down, release on up
770 auto ControllerButton = [&](const char* label, input::SnesButton button,
771 ImVec2 size = ImVec2(50, 40)) {
772 ImGui::PushID(static_cast<int>(button));
773
774 uint16_t button_mask = 1 << static_cast<uint8_t>(button);
775 bool was_pressed = (virtual_buttons_pressed & button_mask) != 0;
776
777 // Style the button if it's currently pressed
778 if (was_pressed) {
779 ImGui::PushStyleColor(ImGuiCol_Button,
780 ConvertColorToImVec4(theme.accent));
781 }
782
783 // Render the button
784 ImGui::Button(label, size);
785
786 // Check if mouse is held down on THIS button (after rendering)
787 bool is_active = ImGui::IsItemActive();
788
789 // Update virtual button state
790 if (is_active) {
791 virtual_buttons_pressed |= button_mask;
792 input_mgr.PressButton(button);
793 } else if (was_pressed) {
794 // Only release if we were the ones who pressed it
795 virtual_buttons_pressed &= ~button_mask;
796 input_mgr.ReleaseButton(button);
797 }
798
799 if (was_pressed) {
800 ImGui::PopStyleColor();
801 }
802
803 ImGui::PopID();
804 };
805
806 // D-Pad layout
807 ImGui::Text("D-Pad:");
808 ImGui::Indent();
809
810 // Up
811 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
813
814 // Left, (space), Right
816 ImGui::SameLine();
817 ImGui::Dummy(ImVec2(50, 40));
818 ImGui::SameLine();
820
821 // Down
822 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
824
825 ImGui::Unindent();
826 ImGui::Spacing();
827
828 // Face buttons (SNES layout: Y B on left, X A on right)
829 ImGui::Text("Face Buttons:");
830 ImGui::Indent();
831
832 // Top row: X
833 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
834 ControllerButton("X", input::SnesButton::X);
835
836 // Middle row: Y, A
837 ControllerButton("Y", input::SnesButton::Y);
838 ImGui::SameLine();
839 ImGui::Dummy(ImVec2(50, 40));
840 ImGui::SameLine();
841 ControllerButton("A", input::SnesButton::A);
842
843 // Bottom row: B
844 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 55);
845 ControllerButton("B", input::SnesButton::B);
846
847 ImGui::Unindent();
848 ImGui::Spacing();
849
850 // Shoulder buttons
851 ImGui::Text("Shoulder:");
852 ControllerButton("L", input::SnesButton::L, ImVec2(70, 30));
853 ImGui::SameLine();
854 ControllerButton("R", input::SnesButton::R, ImVec2(70, 30));
855
856 ImGui::Spacing();
857
858 // Start/Select
859 ImGui::Text("Start/Select:");
860 ControllerButton("Select", input::SnesButton::SELECT, ImVec2(70, 30));
861 ImGui::SameLine();
862 ControllerButton("Start", input::SnesButton::START, ImVec2(70, 30));
863
864 ImGui::Spacing();
865 ImGui::Separator();
866
867 // Debug info - show current button state
868 ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), "Debug:");
869 uint16_t input_state = emu->snes().GetInput1State();
870 ImGui::Text("current_state: 0x%04X", input_state);
871 ImGui::Text("virtual_pressed: 0x%04X", virtual_buttons_pressed);
872 ImGui::Text("Input registered: %s", input_state != 0 ? "YES" : "NO");
873
874 // Show which buttons are detected
875 if (input_state != 0) {
876 ImGui::Text("Buttons:");
877 if (input_state & 0x0001) ImGui::SameLine(), ImGui::Text("B");
878 if (input_state & 0x0002) ImGui::SameLine(), ImGui::Text("Y");
879 if (input_state & 0x0004) ImGui::SameLine(), ImGui::Text("Sel");
880 if (input_state & 0x0008) ImGui::SameLine(), ImGui::Text("Sta");
881 if (input_state & 0x0010) ImGui::SameLine(), ImGui::Text("Up");
882 if (input_state & 0x0020) ImGui::SameLine(), ImGui::Text("Dn");
883 if (input_state & 0x0040) ImGui::SameLine(), ImGui::Text("Lt");
884 if (input_state & 0x0080) ImGui::SameLine(), ImGui::Text("Rt");
885 if (input_state & 0x0100) ImGui::SameLine(), ImGui::Text("A");
886 if (input_state & 0x0200) ImGui::SameLine(), ImGui::Text("X");
887 if (input_state & 0x0400) ImGui::SameLine(), ImGui::Text("L");
888 if (input_state & 0x0800) ImGui::SameLine(), ImGui::Text("R");
889 }
890
891 ImGui::Spacing();
892
893 // Show what the game actually reads ($4218/$4219)
894 auto& snes = emu->snes();
895 uint16_t port_read = snes.GetPortAutoRead(0);
896 ImGui::Text("port_auto_read[0]: 0x%04X", port_read);
897 ImGui::Text("auto_joy_read: %s", snes.IsAutoJoyReadEnabled() ? "ON" : "OFF");
898
899 // Show $4218/$4219 values (what game reads)
900 uint8_t reg_4218 = port_read & 0xFF; // Low byte
901 uint8_t reg_4219 = (port_read >> 8) & 0xFF; // High byte
902 ImGui::Text("$4218: 0x%02X $4219: 0x%02X", reg_4218, reg_4219);
903
904 // Decode $4218 (A, X, L, R in bits 7-4, unused 3-0)
905 ImGui::Text("$4218 bits: A=%d X=%d L=%d R=%d",
906 (reg_4218 >> 7) & 1, (reg_4218 >> 6) & 1,
907 (reg_4218 >> 5) & 1, (reg_4218 >> 4) & 1);
908
909 // Edge detection debug - track state changes
910 static uint16_t last_port_read = 0;
911 static int frames_a_pressed = 0;
912 static int frames_since_release = 0;
913 static bool detected_edge = false;
914
915 bool a_now = (port_read & 0x0080) != 0;
916 bool a_before = (last_port_read & 0x0080) != 0;
917
918 if (a_now && !a_before) {
919 detected_edge = true;
920 frames_a_pressed = 1;
921 frames_since_release = 0;
922 } else if (a_now) {
923 frames_a_pressed++;
924 detected_edge = false;
925 } else {
926 frames_a_pressed = 0;
927 frames_since_release++;
928 detected_edge = false;
929 }
930
931 last_port_read = port_read;
932
933 ImGui::Spacing();
934 ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Edge Detection:");
935 ImGui::Text("A held for: %d frames", frames_a_pressed);
936 ImGui::Text("Released for: %d frames", frames_since_release);
937 if (detected_edge) {
938 ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), ">>> EDGE DETECTED <<<");
939 }
940
941 ImGui::EndChild();
942 ImGui::PopStyleColor();
943}
944
945} // namespace ui
946} // namespace emu
947} // 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:113
bool is_debugging() const
Definition emulator.h:119
std::vector< float > FrameTimeHistory() const
Definition emulator.cc:802
void set_turbo_mode(bool turbo)
Definition emulator.h:105
void SetInputConfig(const input::InputConfig &config)
Definition emulator.cc:75
void * ppu_texture()
Definition emulator.h:101
void set_use_sdl_audio_stream(bool enabled)
Definition emulator.cc:80
double GetCurrentFPS() const
Definition emulator.h:126
std::vector< float > RomBankFreeBytes() const
Definition emulator.cc:837
void set_debugging(bool debugging)
Definition emulator.h:120
std::vector< float > AudioRmsRightHistory() const
Definition emulator.cc:832
std::vector< float > VramBytesHistory() const
Definition emulator.cc:822
std::vector< float > AudioQueueHistory() const
Definition emulator.cc:812
std::vector< float > DmaBytesHistory() const
Definition emulator.cc:817
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:807
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:827
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
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.