From 9ffb7803f54ecb687349537ae311f03495fc78c6 Mon Sep 17 00:00:00 2001 From: scawful Date: Sat, 11 Oct 2025 13:56:49 -0400 Subject: [PATCH] refactor(emulator): enhance input handling and audio resampling features - Renamed `turbo_mode()` to `is_turbo_mode()` for clarity in the Emulator class. - Improved input handling in the Snes class by adding button state management and ensuring proper initialization of input controllers. - Implemented multiple audio resampling methods (linear, cosine, cubic) in the Dsp class, allowing for enhanced audio quality during playback. - Updated the user interface to include options for selecting audio interpolation methods and added keyboard shortcuts for emulator controls. Benefits: - Improved code readability and maintainability through clearer method naming and structured input management. - Enhanced audio playback quality with new resampling techniques. - Streamlined user experience with added UI features for audio settings and keyboard shortcuts. --- src/app/emu/audio/dsp.cc | 118 ++++++++++-- src/app/emu/audio/dsp.h | 8 + src/app/emu/emulator.h | 2 +- src/app/emu/input/input_backend.cc | 25 ++- src/app/emu/input/input_manager.cc | 28 ++- src/app/emu/input/input_manager.h | 48 +---- src/app/emu/snes.cc | 32 +++- src/app/emu/snes.h | 4 + src/app/emu/ui/debugger_ui.cc | 11 ++ src/app/emu/ui/emulator_ui.cc | 287 +++++++++++++++++++++++++---- src/app/emu/ui/emulator_ui.h | 5 + 11 files changed, 454 insertions(+), 114 deletions(-) diff --git a/src/app/emu/audio/dsp.cc b/src/app/emu/audio/dsp.cc index 4ed32c79..e71b7041 100644 --- a/src/app/emu/audio/dsp.cc +++ b/src/app/emu/audio/dsp.cc @@ -1,5 +1,6 @@ #include "app/emu/audio/dsp.h" +#include #include namespace yaze { @@ -616,6 +617,37 @@ void Dsp::Write(uint8_t adr, uint8_t val) { ram[adr] = val; } +// Helper for 4-point cubic interpolation (Catmull-Rom) +// Provides higher quality resampling compared to linear interpolation. +inline int16_t InterpolateCubic(int16_t p0, int16_t p1, int16_t p2, int16_t p3, + double t) { + double t2 = t * t; + double t3 = t2 * t; + + double c0 = p1; + double c1 = 0.5 * (p2 - p0); + double c2 = (p0 - 2.5 * p1 + 2.0 * p2 - 0.5 * p3); + double c3 = 0.5 * (-p0 + 3.0 * p1 - 3.0 * p2 + p3); + + double result = c0 + c1 * t + c2 * t2 + c3 * t3; + + // Clamp to 16-bit range + return result > 32767.0 + ? 32767 + : (result < -32768.0 ? -32768 : static_cast(result)); +} + +// Helper for cosine interpolation +inline int16_t InterpolateCosine(int16_t s0, int16_t s1, double mu) { + const double mu2 = (1.0 - cos(mu * 3.14159265358979323846)) / 2.0; + return static_cast(s0 * (1.0 - mu2) + s1 * mu2); +} + +// Helper for linear interpolation +inline int16_t InterpolateLinear(int16_t s0, int16_t s1, double frac) { + return static_cast(s0 + frac * (s1 - s0)); +} + void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame, bool pal_timing) { // Resample from native samples-per-frame (NTSC: ~534, PAL: ~641) @@ -625,27 +657,75 @@ void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame, double location = static_cast((lastFrameBoundary + 0x400) & 0x3ff); location -= native_per_frame; - // Use linear interpolation for smoother resampling for (int i = 0; i < samples_per_frame; i++) { - const int idx = static_cast(location) & 0x3ff; - const int next_idx = (idx + 1) & 0x3ff; - - // Calculate interpolation factor (0.0 to 1.0) - const double frac = location - static_cast(location); - - // Linear interpolation for left channel - const int16_t s0_l = sampleBuffer[(idx * 2) + 0]; - const int16_t s1_l = sampleBuffer[(next_idx * 2) + 0]; - sample_data[(i * 2) + 0] = static_cast( - s0_l + frac * (s1_l - s0_l)); - - // Linear interpolation for right channel - const int16_t s0_r = sampleBuffer[(idx * 2) + 1]; - const int16_t s1_r = sampleBuffer[(next_idx * 2) + 1]; - sample_data[(i * 2) + 1] = static_cast( - s0_r + frac * (s1_r - s0_r)); - + const int idx = static_cast(location); + const double frac = location - idx; + + switch (interpolation_type) { + case InterpolationType::Linear: { + // const int next_idx = (idx + 1) & 0x3ff; + // const int16_t s0_l = sampleBuffer[(idx * 2) + 0]; + // const int16_t s1_l = sampleBuffer[(next_idx * 2) + 0]; + // sample_data[(i * 2) + 0] = InterpolateLinear(s0_l, s1_l, frac); + // const int16_t s0_r = sampleBuffer[(idx * 2) + 1]; + // const int16_t s1_r = sampleBuffer[(next_idx * 2) + 1]; + // sample_data[(i * 2) + 1] = InterpolateLinear(s0_r, s1_r, frac); + const int idx = static_cast(location) & 0x3ff; + const int next_idx = (idx + 1) & 0x3ff; + + // Calculate interpolation factor (0.0 to 1.0) + const double frac = location - static_cast(location); + + // Linear interpolation for left channel + const int16_t s0_l = sampleBuffer[(idx * 2) + 0]; + const int16_t s1_l = sampleBuffer[(next_idx * 2) + 0]; + sample_data[(i * 2) + 0] = static_cast( + s0_l + frac * (s1_l - s0_l)); + + // Linear interpolation for right channel + const int16_t s0_r = sampleBuffer[(idx * 2) + 1]; + const int16_t s1_r = sampleBuffer[(next_idx * 2) + 1]; + sample_data[(i * 2) + 1] = static_cast( + s0_r + frac * (s1_r - s0_r)); + + // location += step; + + break; + } + case InterpolationType::Cosine: { + const int next_idx = (idx + 1) & 0x3ff; + const int16_t s0_l = sampleBuffer[(idx * 2) + 0]; + const int16_t s1_l = sampleBuffer[(next_idx * 2) + 0]; + sample_data[(i * 2) + 0] = InterpolateCosine(s0_l, s1_l, frac); + const int16_t s0_r = sampleBuffer[(idx * 2) + 1]; + const int16_t s1_r = sampleBuffer[(next_idx * 2) + 1]; + sample_data[(i * 2) + 1] = InterpolateCosine(s0_r, s1_r, frac); + break; + } + case InterpolationType::Cubic: { + const int idx0 = (idx - 1 + 0x400) & 0x3ff; + const int idx1 = idx & 0x3ff; + const int idx2 = (idx + 1) & 0x3ff; + const int idx3 = (idx + 2) & 0x3ff; + // Left channel + const int16_t p0_l = sampleBuffer[(idx0 * 2) + 0]; + const int16_t p1_l = sampleBuffer[(idx1 * 2) + 0]; + const int16_t p2_l = sampleBuffer[(idx2 * 2) + 0]; + const int16_t p3_l = sampleBuffer[(idx3 * 2) + 0]; + sample_data[(i * 2) + 0] = + InterpolateCubic(p0_l, p1_l, p2_l, p3_l, frac); + // Right channel + const int16_t p0_r = sampleBuffer[(idx0 * 2) + 1]; + const int16_t p1_r = sampleBuffer[(idx1 * 2) + 1]; + const int16_t p2_r = sampleBuffer[(idx2 * 2) + 1]; + const int16_t p3_r = sampleBuffer[(idx3 * 2) + 1]; + sample_data[(i * 2) + 1] = + InterpolateCubic(p0_r, p1_r, p2_r, p3_r, frac); + break; + } + } location += step; + } } diff --git a/src/app/emu/audio/dsp.h b/src/app/emu/audio/dsp.h index b1699ae3..d83dc54f 100644 --- a/src/app/emu/audio/dsp.h +++ b/src/app/emu/audio/dsp.h @@ -7,6 +7,12 @@ namespace yaze { namespace emu { +enum class InterpolationType { + Linear, + Cosine, + Cubic, +}; + typedef struct DspChannel { // pitch uint16_t pitch; @@ -104,6 +110,8 @@ class Dsp { void GetSamples(int16_t* sample_data, int samples_per_frame, bool pal_timing); + InterpolationType interpolation_type = InterpolationType::Linear; + private: // sample ring buffer (1024 samples, *2 for stereo) int16_t sampleBuffer[0x400 * 2]; diff --git a/src/app/emu/emulator.h b/src/app/emu/emulator.h index 034e4d4b..fe3e4fd2 100644 --- a/src/app/emu/emulator.h +++ b/src/app/emu/emulator.h @@ -62,7 +62,7 @@ class Emulator { void* ppu_texture() { return ppu_texture_; } // Turbo mode - bool turbo_mode() const { return turbo_mode_; } + bool is_turbo_mode() const { return turbo_mode_; } void set_turbo_mode(bool turbo) { turbo_mode_ = turbo; } // Debugger access diff --git a/src/app/emu/input/input_backend.cc b/src/app/emu/input/input_backend.cc index 4258faf3..a85b1584 100644 --- a/src/app/emu/input/input_backend.cc +++ b/src/app/emu/input/input_backend.cc @@ -1,6 +1,7 @@ #include "app/emu/input/input_backend.h" #include "SDL.h" +#include "imgui/imgui.h" #include "util/log.h" namespace yaze { @@ -53,13 +54,29 @@ class SDL2InputBackend : public IInputBackend { ControllerState Poll(int player) override { if (!initialized_) return ControllerState{}; - + ControllerState state; - + if (config_.continuous_polling) { // Continuous polling mode (for games) const uint8_t* keyboard_state = SDL_GetKeyboardState(nullptr); + + // IMPORTANT: Only block input when actively typing in text fields + // Allow game input even when ImGui windows are open/focused + ImGuiIO& io = ImGui::GetIO(); + // Only block if user is actively typing in a text input field + // WantTextInput is true only when an InputText widget is active + if (io.WantTextInput) { + // User is typing in a text field + // Return empty state to prevent game from processing input + static int text_input_log_count = 0; + if (text_input_log_count++ < 5) { + LOG_DEBUG("InputBackend", "Blocking game input - WantTextInput=true"); + } + return ControllerState{}; + } + // Map keyboard to SNES buttons state.SetButton(SnesButton::B, keyboard_state[SDL_GetScancodeFromKey(config_.key_b)]); state.SetButton(SnesButton::Y, keyboard_state[SDL_GetScancodeFromKey(config_.key_y)]); @@ -77,10 +94,10 @@ class SDL2InputBackend : public IInputBackend { // Event-based mode (use cached event state) state = event_state_; } - + // TODO: Add gamepad support // if (config_.enable_gamepad) { ... } - + return state; } diff --git a/src/app/emu/input/input_manager.cc b/src/app/emu/input/input_manager.cc index 718c1b1d..96ba7539 100644 --- a/src/app/emu/input/input_manager.cc +++ b/src/app/emu/input/input_manager.cc @@ -15,8 +15,8 @@ bool InputManager::Initialize(InputBackendFactory::BackendType type) { } InputConfig config; - config.continuous_polling = true; // Always use continuous polling for games - config.enable_gamepad = false; // TODO: Enable when gamepad support added + config.continuous_polling = true; + config.enable_gamepad = false; if (!backend_->Initialize(config)) { LOG_ERROR("InputManager", "Failed to initialize input backend"); @@ -47,13 +47,16 @@ void InputManager::Shutdown() { void InputManager::Poll(Snes* snes, int player) { if (!snes || !backend_) return; - // Poll backend for current controller state - ControllerState state = backend_->Poll(player); + ControllerState physical_state = backend_->Poll(player); - // Update SNES controller state using the hardware button layout - // SNES controller bits: 0=B, 1=Y, 2=Select, 3=Start, 4-7=DPad, 8=A, 9=X, 10=L, 11=R + // Combine physical input with agent-controlled input (OR operation) + ControllerState final_state; + final_state.buttons = physical_state.buttons | agent_controller_state_.buttons; + + // Update ALL button states every frame to ensure proper press/release + // This is critical for games that check button state every frame for (int i = 0; i < 12; i++) { - bool pressed = (state.buttons & (1 << i)) != 0; + bool pressed = (final_state.buttons & (1 << i)) != 0; snes->SetButtonState(player, i, pressed); } } @@ -77,7 +80,14 @@ void InputManager::SetConfig(const InputConfig& config) { } } +void InputManager::PressButton(SnesButton button) { + agent_controller_state_.SetButton(button, true); +} + +void InputManager::ReleaseButton(SnesButton button) { + agent_controller_state_.SetButton(button, false); +} + } // namespace input } // namespace emu -} // namespace yaze - +} // namespace yaze \ No newline at end of file diff --git a/src/app/emu/input/input_manager.h b/src/app/emu/input/input_manager.h index 34a4657d..ca258ace 100644 --- a/src/app/emu/input/input_manager.h +++ b/src/app/emu/input/input_manager.h @@ -12,72 +12,36 @@ class Snes; namespace input { -/** - * @brief High-level input manager that bridges backend and SNES - * - * This class provides a simple interface for both GUI and headless modes: - * - Manages input backend lifecycle - * - Polls input and updates SNES controller state - * - Handles multiple players - * - Supports hot-swapping input configurations - */ class InputManager { public: InputManager() = default; ~InputManager() { Shutdown(); } - /** - * @brief Initialize with specific backend - */ bool Initialize(InputBackendFactory::BackendType type = InputBackendFactory::BackendType::SDL2); - - /** - * @brief Initialize with custom backend - */ void Initialize(std::unique_ptr backend); - - /** - * @brief Shutdown input system - */ void Shutdown(); - - /** - * @brief Poll input and update SNES controller state - * @param snes SNES instance to update - * @param player Player number (1-4) - */ void Poll(Snes* snes, int player = 1); - - /** - * @brief Process platform-specific event (optional) - * @param event Platform event (e.g., SDL_Event*) - */ void ProcessEvent(void* event); - /** - * @brief Get backend for configuration - */ IInputBackend* backend() { return backend_.get(); } const IInputBackend* backend() const { return backend_.get(); } - /** - * @brief Check if initialized - */ bool IsInitialized() const { return backend_ && backend_->IsInitialized(); } - /** - * @brief Get/set configuration - */ InputConfig GetConfig() const; void SetConfig(const InputConfig& config); + // --- Agent Control API --- + void PressButton(SnesButton button); + void ReleaseButton(SnesButton button); + private: std::unique_ptr backend_; + ControllerState agent_controller_state_; // State controlled by agent }; } // namespace input } // namespace emu } // namespace yaze -#endif // YAZE_APP_EMU_INPUT_INPUT_MANAGER_H_ - +#endif // YAZE_APP_EMU_INPUT_INPUT_MANAGER_H_ \ No newline at end of file diff --git a/src/app/emu/snes.cc b/src/app/emu/snes.cc index cdf4508e..e115ae4f 100644 --- a/src/app/emu/snes.cc +++ b/src/app/emu/snes.cc @@ -18,12 +18,15 @@ namespace emu { namespace { void input_latch(Input* input, bool value) { input->latch_line_ = value; - if (input->latch_line_) input->latched_state_ = input->current_state_; + if (input->latch_line_) { + input->latched_state_ = input->current_state_; + } } uint8_t input_read(Input* input) { if (input->latch_line_) input->latched_state_ = input->current_state_; uint8_t ret = input->latched_state_ & 1; + input->latched_state_ >>= 1; input->latched_state_ |= 0x8000; return ret; @@ -56,6 +59,8 @@ void Snes::Reset(bool hard) { ResetDma(&memory_); input1.latch_line_ = false; input2.latch_line_ = false; + input1.current_state_ = 0; // Clear current button states + input2.current_state_ = 0; // Clear current button states input1.latched_state_ = 0; input2.latched_state_ = 0; if (hard) memset(ram, 0, sizeof(ram)); @@ -392,7 +397,8 @@ uint8_t Snes::Rread(uint32_t adr) { return ReadBBus(adr & 0xff); // B-bus } if (adr == 0x4016) { - return input_read(&input1) | (memory_.open_bus() & 0xfc); + uint8_t result = input_read(&input1) | (memory_.open_bus() & 0xfc); + return result; } if (adr == 0x4017) { return input_read(&input2) | (memory_.open_bus() & 0xe0) | 0x1c; @@ -634,7 +640,27 @@ void Snes::SetSamples(int16_t* sample_data, int wanted_samples) { void Snes::SetPixels(uint8_t* pixel_data) { ppu_.PutPixels(pixel_data); } -void Snes::SetButtonState(int player, int button, bool pressed) {} +void Snes::SetButtonState(int player, int button, bool pressed) { + // Select the appropriate input based on player number + Input* input = (player == 1) ? &input1 : &input2; + + // SNES controller button mapping (standard layout) + // Bit 0: B, Bit 1: Y, Bit 2: Select, Bit 3: Start + // Bit 4: Up, Bit 5: Down, Bit 6: Left, Bit 7: Right + // Bit 8: A, Bit 9: X, Bit 10: L, Bit 11: R + + if (button < 0 || button > 11) return; // Validate button range + + uint16_t old_state = input->current_state_; + + if (pressed) { + // Set the button bit + input->current_state_ |= (1 << button); + } else { + // Clear the button bit + input->current_state_ &= ~(1 << button); + } +} void Snes::loadState(const std::string& path) { std::ifstream file(path, std::ios::binary); diff --git a/src/app/emu/snes.h b/src/app/emu/snes.h index c40b9288..e0432511 100644 --- a/src/app/emu/snes.h +++ b/src/app/emu/snes.h @@ -25,6 +25,10 @@ struct Input { class Snes { public: Snes() { + // Initialize input controllers to clean state + input1 = {}; + input2 = {}; + cpu_.callbacks().read_byte = [this](uint32_t adr) { return CpuRead(adr); }; cpu_.callbacks().write_byte = [this](uint32_t adr, uint8_t val) { CpuWrite(adr, val); }; cpu_.callbacks().idle = [this](bool waiting) { CpuIdle(waiting); }; diff --git a/src/app/emu/ui/debugger_ui.cc b/src/app/emu/ui/debugger_ui.cc index ddeb2a13..fe2c8c72 100644 --- a/src/app/emu/ui/debugger_ui.cc +++ b/src/app/emu/ui/debugger_ui.cc @@ -531,6 +531,17 @@ void RenderApuDebugger(Emulator* emu) { LOG_INFO("APU_DEBUG", "Port history cleared"); } } + + ImGui::Separator(); + ImGui::Text("Audio Resampling"); + + // Combo box for interpolation type + const char* items[] = {"Linear", "Cosine", "Cubic"}; + int current_item = static_cast(emu->snes().apu().dsp().interpolation_type); + if (ImGui::Combo("Interpolation", ¤t_item, items, IM_ARRAYSIZE(items))) { + emu->snes().apu().dsp().interpolation_type = + static_cast(current_item); + } ImGui::EndChild(); ImGui::PopStyleColor(); diff --git a/src/app/emu/ui/emulator_ui.cc b/src/app/emu/ui/emulator_ui.cc index 3e7ea8fa..cc39f45c 100644 --- a/src/app/emu/ui/emulator_ui.cc +++ b/src/app/emu/ui/emulator_ui.cc @@ -1,11 +1,14 @@ #include "app/emu/ui/emulator_ui.h" +#include + #include "absl/strings/str_format.h" #include "app/emu/emulator.h" #include "app/gui/color.h" #include "app/gui/icons.h" #include "app/gui/theme_manager.h" #include "imgui/imgui.h" +#include "util/file_util.h" #include "util/log.h" namespace yaze { @@ -41,6 +44,20 @@ void RenderNavBar(Emulator* emu) { auto& theme_manager = ThemeManager::Get(); const auto& theme = theme_manager.GetCurrentTheme(); + // Handle keyboard shortcuts for emulator control + // IMPORTANT: Use Shortcut() to avoid conflicts with game input + // Space - toggle play/pause (only when not typing in text fields) + if (ImGui::Shortcut(ImGuiKey_Space, ImGuiInputFlags_RouteGlobal)) { + emu->set_running(!emu->running()); + } + + // F10 - step one frame + if (ImGui::Shortcut(ImGuiKey_F10, ImGuiInputFlags_RouteGlobal)) { + if (!emu->running()) { + emu->snes().RunFrame(); + } + } + // Navbar with theme colors ImGui::PushStyleColor(ImGuiCol_Button, ConvertColorToImVec4(theme.button)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ConvertColorToImVec4(theme.button_hovered)); @@ -86,7 +103,51 @@ void RenderNavBar(Emulator* emu) { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Reset SNES (Ctrl+R)"); } - + + ImGui::SameLine(); + + // Load ROM button + if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load ROM", ImVec2(110, kButtonHeight))) { + std::string rom_path = util::FileDialogWrapper::ShowOpenFileDialog(); + if (!rom_path.empty()) { + // Check if it's a valid ROM file extension + std::string ext = util::GetFileExtension(rom_path); + if (ext == ".sfc" || ext == ".smc" || ext == ".SFC" || ext == ".SMC") { + try { + // Read ROM file into memory + std::ifstream rom_file(rom_path, std::ios::binary); + if (rom_file.good()) { + std::vector rom_data( + (std::istreambuf_iterator(rom_file)), + std::istreambuf_iterator() + ); + rom_file.close(); + + // Reinitialize emulator with new ROM + if (!rom_data.empty()) { + emu->Initialize(emu->renderer(), rom_data); + LOG_INFO("Emulator", "Loaded ROM: %s (%zu bytes)", + util::GetFileName(rom_path).c_str(), rom_data.size()); + } else { + LOG_ERROR("Emulator", "ROM file is empty: %s", rom_path.c_str()); + } + } else { + LOG_ERROR("Emulator", "Failed to open ROM file: %s", rom_path.c_str()); + } + } catch (const std::exception& e) { + LOG_ERROR("Emulator", "Error loading ROM: %s", e.what()); + } + } else { + LOG_WARN("Emulator", "Invalid ROM file extension: %s (expected .sfc or .smc)", + ext.c_str()); + } + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Load a different ROM file\n" + "Allows testing hacks with assembly patches applied"); + } + ImGui::SameLine(); ImGui::Separator(); ImGui::SameLine(); @@ -115,12 +176,12 @@ void RenderNavBar(Emulator* emu) { ImGui::SameLine(); // Turbo mode - bool turbo = false; // Need to expose this from Emulator + bool turbo = emu->is_turbo_mode(); if (ImGui::Checkbox(ICON_MD_FAST_FORWARD " Turbo", &turbo)) { - // emu->set_turbo_mode(turbo); + emu->set_turbo_mode(turbo); } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Fast forward (hold Tab)"); + ImGui::SetTooltip("Fast forward (shortcut: hold Tab)"); } ImGui::SameLine(); @@ -141,17 +202,17 @@ void RenderNavBar(Emulator* emu) { ImGui::TextColored(fps_color, ICON_MD_SPEED " %.1f FPS", fps); ImGui::SameLine(); - + // Audio backend status if (emu->audio_backend()) { auto audio_status = emu->audio_backend()->GetStatus(); - ImVec4 audio_color = audio_status.is_playing ? + ImVec4 audio_color = audio_status.is_playing ? ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.text_disabled); - + ImGui::TextColored(audio_color, ICON_MD_VOLUME_UP " %s | %u frames", emu->audio_backend()->GetBackendName().c_str(), audio_status.queued_frames); - + if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Audio Backend: %s\nQueued: %u frames\nPlaying: %s", emu->audio_backend()->GetBackendName().c_str(), @@ -159,55 +220,97 @@ void RenderNavBar(Emulator* emu) { audio_status.is_playing ? "YES" : "NO"); } } else { - ImGui::TextColored(ConvertColorToImVec4(theme.error), + ImGui::TextColored(ConvertColorToImVec4(theme.error), ICON_MD_VOLUME_OFF " No Backend"); } - + + ImGui::SameLine(); + ImGui::Separator(); + ImGui::SameLine(); + + // Input capture status indicator (like modern emulators) + ImGuiIO& io = ImGui::GetIO(); + if (io.WantCaptureKeyboard) { + // ImGui is capturing keyboard (typing in UI) + ImGui::TextColored(ConvertColorToImVec4(theme.warning), + ICON_MD_KEYBOARD " UI"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Keyboard captured by UI\nGame input disabled"); + } + } else { + // Emulator can receive input + ImGui::TextColored(ConvertColorToImVec4(theme.success), + ICON_MD_SPORTS_ESPORTS " Game"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Game input active\nPress F1 for controls"); + } + } + ImGui::PopStyleColor(3); } void RenderSnesPpu(Emulator* emu) { if (!emu) return; - + auto& theme_manager = ThemeManager::Get(); const auto& theme = theme_manager.GetCurrentTheme(); - + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.editor_background)); - ImGui::BeginChild("##SNES_PPU", ImVec2(0, 0), true, + ImGui::BeginChild("##SNES_PPU", ImVec2(0, 0), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); - + ImVec2 canvas_size = ImGui::GetContentRegionAvail(); ImVec2 snes_size = ImVec2(512, 480); - + if (emu->is_snes_initialized() && emu->ppu_texture()) { - // Center the SNES display + // Center the SNES display with aspect ratio preservation float aspect = snes_size.x / snes_size.y; float display_w = canvas_size.x; float display_h = display_w / aspect; - + if (display_h > canvas_size.y) { display_h = canvas_size.y; display_w = display_h * aspect; } - + float pos_x = (canvas_size.x - display_w) * 0.5f; float pos_y = (canvas_size.y - display_h) * 0.5f; - + ImGui::SetCursorPos(ImVec2(pos_x, pos_y)); - - // Render PPU texture - ImGui::Image((ImTextureID)(intptr_t)emu->ppu_texture(), - ImVec2(display_w, display_h), + + // Render PPU texture with click detection for focus + ImGui::Image((ImTextureID)(intptr_t)emu->ppu_texture(), + ImVec2(display_w, display_h), ImVec2(0, 0), ImVec2(1, 1)); + + // Allow clicking on the display to ensure focus + // Modern emulators make the game area "sticky" for input + if (ImGui::IsItemHovered()) { + // ImGui::SetTooltip("Click to ensure game input focus"); + + // Visual feedback when hovered (subtle border) + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImVec2 screen_pos = ImGui::GetItemRectMin(); + ImVec2 screen_size = ImGui::GetItemRectMax(); + draw_list->AddRect(screen_pos, screen_size, + ImGui::ColorConvertFloat4ToU32(ConvertColorToImVec4(theme.accent)), + 0.0f, 0, 2.0f); + } } else { - // Not initialized - show placeholder - ImVec2 text_size = ImGui::CalcTextSize("SNES PPU Output\n512x480"); + // Not initialized - show helpful placeholder + ImVec2 text_size = ImGui::CalcTextSize("Load a ROM to start emulation"); ImGui::SetCursorPos(ImVec2((canvas_size.x - text_size.x) * 0.5f, - (canvas_size.y - text_size.y) * 0.5f)); + (canvas_size.y - text_size.y) * 0.5f - 20)); ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), - ICON_MD_VIDEOGAME_ASSET "\nSNES PPU Output\n512x480"); + ICON_MD_VIDEOGAME_ASSET); + ImGui::SetCursorPosX((canvas_size.x - text_size.x) * 0.5f); + ImGui::TextColored(ConvertColorToImVec4(theme.text_primary), + "Load a ROM to start emulation"); + ImGui::SetCursorPosX((canvas_size.x - ImGui::CalcTextSize("512x480 SNES output").x) * 0.5f); + ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), + "512x480 SNES output"); } - + ImGui::EndChild(); ImGui::PopStyleColor(); } @@ -257,22 +360,134 @@ void RenderPerformanceMonitor(Emulator* emu) { ImGui::PopStyleColor(); } -void RenderEmulatorInterface(Emulator* emu) { - if (!emu) return; - +void RenderKeyboardShortcuts(bool* show) { + if (!show || !*show) return; + auto& theme_manager = ThemeManager::Get(); const auto& theme = theme_manager.GetCurrentTheme(); - + + // Center the window + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(550, 600), ImGuiCond_Appearing); + + ImGui::PushStyleColor(ImGuiCol_TitleBg, ConvertColorToImVec4(theme.accent)); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ConvertColorToImVec4(theme.accent)); + + if (ImGui::Begin(ICON_MD_KEYBOARD " Keyboard Shortcuts", show, + ImGuiWindowFlags_NoCollapse)) { + // Emulator controls section + ImGui::TextColored(ConvertColorToImVec4(theme.accent), + ICON_MD_VIDEOGAME_ASSET " Emulator Controls"); + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::BeginTable("EmulatorControls", 2, ImGuiTableFlags_Borders)) { + ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthFixed, 120); + ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + + auto AddRow = [](const char* key, const char* action) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%s", key); + ImGui::TableNextColumn(); + ImGui::Text("%s", action); + }; + + AddRow("Space", "Play/Pause emulation"); + AddRow("F10", "Step one frame"); + AddRow("Ctrl+R", "Reset SNES"); + AddRow("Tab (hold)", "Turbo mode (fast forward)"); + AddRow("F1", "Show/hide this help"); + + ImGui::EndTable(); + } + + ImGui::Spacing(); + ImGui::Spacing(); + + // Game controls section + ImGui::TextColored(ConvertColorToImVec4(theme.accent), + ICON_MD_SPORTS_ESPORTS " SNES Controller"); + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::BeginTable("GameControls", 2, ImGuiTableFlags_Borders)) { + ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthFixed, 120); + ImGui::TableSetupColumn("Button", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + + auto AddRow = [](const char* key, const char* button) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%s", key); + ImGui::TableNextColumn(); + ImGui::Text("%s", button); + }; + + AddRow("Arrow Keys", "D-Pad (Up/Down/Left/Right)"); + AddRow("X", "A Button"); + AddRow("Z", "B Button"); + AddRow("S", "X Button"); + AddRow("A", "Y Button"); + AddRow("D", "L Shoulder"); + AddRow("C", "R Shoulder"); + AddRow("Enter", "Start"); + AddRow("RShift", "Select"); + + ImGui::EndTable(); + } + + ImGui::Spacing(); + ImGui::Spacing(); + + // Tips section + ImGui::TextColored(ConvertColorToImVec4(theme.info), + ICON_MD_INFO " Tips"); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::BulletText("Input is disabled when typing in UI fields"); + ImGui::BulletText("Check the status bar for input capture state"); + ImGui::BulletText("Click the game screen to ensure focus"); + ImGui::BulletText("The emulator continues running in background"); + + ImGui::Spacing(); + ImGui::Spacing(); + + if (ImGui::Button("Close", ImVec2(-1, 30))) { + *show = false; + } + } + ImGui::End(); + + ImGui::PopStyleColor(2); +} + +void RenderEmulatorInterface(Emulator* emu) { + if (!emu) return; + + auto& theme_manager = ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + // Main layout ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.window_bg)); - + RenderNavBar(emu); - + ImGui::Separator(); - + // Main content area RenderSnesPpu(emu); - + + // Keyboard shortcuts overlay (F1 to toggle) + static bool show_shortcuts = false; + if (ImGui::IsKeyPressed(ImGuiKey_F1)) { + show_shortcuts = !show_shortcuts; + } + RenderKeyboardShortcuts(&show_shortcuts); + ImGui::PopStyleColor(); } diff --git a/src/app/emu/ui/emulator_ui.h b/src/app/emu/ui/emulator_ui.h index 15c2f7ee..20f923cb 100644 --- a/src/app/emu/ui/emulator_ui.h +++ b/src/app/emu/ui/emulator_ui.h @@ -32,6 +32,11 @@ void RenderSnesPpu(Emulator* emu); */ void RenderPerformanceMonitor(Emulator* emu); +/** + * @brief Keyboard shortcuts help overlay (F1 in modern emulators) + */ +void RenderKeyboardShortcuts(bool* show); + } // namespace ui } // namespace emu } // namespace yaze