diff --git a/src/app/emu/cpu/cpu.cc b/src/app/emu/cpu/cpu.cc index 1a871333..651b4597 100644 --- a/src/app/emu/cpu/cpu.cc +++ b/src/app/emu/cpu/cpu.cc @@ -123,33 +123,72 @@ void Cpu::RunOpcode() { } else { uint8_t opcode = ReadOpcode(); - // Debug: Log key instructions during boot + // AUDIO DEBUG: Enhanced logging for audio initialization tracking static int instruction_count = 0; instruction_count++; - - // Log first 50 fully, then every 100th until 3000, then stop - bool should_log = (instruction_count < 50) || - (instruction_count < 3000 && instruction_count % 100 == 0); - - // CRITICAL: Log LoadSongBank routine ($8888-$88FF) to trace data reads uint16_t cur_pc = PC - 1; - if (PB == 0x00 && cur_pc >= 0x8888 && cur_pc <= 0x88FF) { - // Detailed logging at critical handshake points - static int handshake_log_count = 0; - if (cur_pc == 0x88B3 || cur_pc == 0x88B6) { - if (handshake_log_count++ < 5 || handshake_log_count % 1000 == 0) { - // At $88B3: CMP.w APUIO0 - comparing A with F4 - // At $88B6: BNE .wait_for_sync_a - branch if not equal - uint8_t f4_val = callbacks_.read_byte(0x2140); // Read F4 directly - LOG_DEBUG("CPU", "Handshake wait: PC=$%04X A(counter)=$%02X F4(SPC)=$%02X X(remain)=$%04X", - cur_pc, A & 0xFF, f4_val, X); - } - } - should_log = (cur_pc >= 0x88CF && cur_pc <= 0x88E0); // Only log setup, not tight loop + + // Track entry into Bank $00 (where all audio code lives) + static bool entered_bank00 = false; + static bool logged_first_nmi = false; + + if (PB == 0x00 && !entered_bank00) { + LOG_INFO("CPU_AUDIO", "=== ENTERED BANK $00 at PC=$%04X (instruction #%d) ===", + cur_pc, instruction_count); + entered_bank00 = true; } + // Monitor NMI interrupts (audio init usually happens in NMI) + if (nmi_wanted_ && !logged_first_nmi) { + LOG_INFO("CPU_AUDIO", "=== FIRST NMI TRIGGERED at PC=$%02X:%04X ===", PB, cur_pc); + logged_first_nmi = true; + } + + // Track key audio routines in Bank $00 + if (PB == 0x00) { + static bool logged_routines[0x10000] = {false}; + + // NMI handler entry ($0080-$00FF region) + if (cur_pc >= 0x0080 && cur_pc <= 0x00FF) { + if (cur_pc == 0x0080 || cur_pc == 0x0090 || cur_pc == 0x00A0) { + if (!logged_routines[cur_pc]) { + LOG_INFO("CPU_AUDIO", "NMI code: PC=$00:%04X A=$%02X X=$%04X Y=$%04X", + cur_pc, A & 0xFF, X, Y); + logged_routines[cur_pc] = true; + } + } + } + + // LoadSongBank routine ($8888-$88FF) - This is where handshake happens! + if (cur_pc >= 0x8888 && cur_pc <= 0x88FF) { + // Log entry + if (cur_pc == 0x8888) { + LOG_INFO("CPU_AUDIO", ">>> LoadSongBank ENTRY at $8888! A=$%02X X=$%04X", + A & 0xFF, X); + } + + // Log handshake initiation ($88A0-$88B0 area writes $CC to F4) + if (cur_pc >= 0x88A0 && cur_pc <= 0x88B0 && !logged_routines[cur_pc]) { + LOG_INFO("CPU_AUDIO", "Handshake setup: PC=$%04X A=$%02X", cur_pc, A & 0xFF); + logged_routines[cur_pc] = true; + } + + // Log handshake wait loop + static int handshake_log_count = 0; + if (cur_pc == 0x88B3 || cur_pc == 0x88B6) { + if (handshake_log_count++ < 20 || handshake_log_count % 500 == 0) { + uint8_t f4_val = callbacks_.read_byte(0x2140); + LOG_INFO("CPU_AUDIO", "Handshake wait: PC=$%04X A=$%02X F4=$%02X X=$%04X [loop #%d]", + cur_pc, A & 0xFF, f4_val, X, handshake_log_count); + } + } + } + } + + // Log first 50 instructions for boot tracking + bool should_log = instruction_count < 50; if (should_log) { - LOG_DEBUG("CPU", "Exec #%d: $%02X:%04X opcode=$%02X", + LOG_DEBUG("CPU", "Boot #%d: $%02X:%04X opcode=$%02X", instruction_count, PB, PC - 1, opcode); } diff --git a/src/app/emu/emulator.cc b/src/app/emu/emulator.cc index 8a89d1d1..1cefb0bc 100644 --- a/src/app/emu/emulator.cc +++ b/src/app/emu/emulator.cc @@ -13,6 +13,7 @@ namespace yaze::core { #include "app/emu/cpu/internal/opcodes.h" #include "app/emu/debug/disassembly_viewer.h" +#include "app/emu/ui/input_handler.h" #include "app/gui/color.h" #include "app/gui/editor_layout.h" #include "app/gui/icons.h" @@ -146,6 +147,16 @@ void Emulator::Run(Rom* rom) { } } + // Initialize input manager if not already done + if (!input_manager_.IsInitialized()) { + if (!input_manager_.Initialize(input::InputBackendFactory::BackendType::SDL2)) { + LOG_ERROR("Emulator", "Failed to initialize input manager"); + } else { + LOG_INFO("Emulator", "Input manager initialized: %s", + input_manager_.backend()->GetBackendName().c_str()); + } + } + // Initialize SNES and create PPU texture on first run // This happens lazily when user opens the emulator window if (!snes_initialized_ && rom->is_loaded()) { @@ -209,7 +220,8 @@ void Emulator::Run(Rom* rom) { } if (running_) { - HandleEvents(); + // Poll input and update SNES controller state + input_manager_.Poll(&snes_, 1); // Player 1 uint64_t current_count = SDL_GetPerformanceCounter(); uint64_t delta = current_count - last_count; @@ -674,104 +686,9 @@ void Emulator::RenderNavBar() { } } -void Emulator::HandleEvents() { - // Handle user input events - if (ImGui::IsKeyPressed(keybindings_.a_button)) { - snes_.SetButtonState(1, 0, true); - } - - if (ImGui::IsKeyPressed(keybindings_.b_button)) { - snes_.SetButtonState(1, 1, true); - } - - if (ImGui::IsKeyPressed(keybindings_.select_button)) { - snes_.SetButtonState(1, 2, true); - } - - if (ImGui::IsKeyPressed(keybindings_.start_button)) { - snes_.SetButtonState(1, 3, true); - } - - if (ImGui::IsKeyPressed(keybindings_.up_button)) { - snes_.SetButtonState(1, 4, true); - } - - if (ImGui::IsKeyPressed(keybindings_.down_button)) { - snes_.SetButtonState(1, 5, true); - } - - if (ImGui::IsKeyPressed(keybindings_.left_button)) { - snes_.SetButtonState(1, 6, true); - } - - if (ImGui::IsKeyPressed(keybindings_.right_button)) { - snes_.SetButtonState(1, 7, true); - } - - if (ImGui::IsKeyPressed(keybindings_.x_button)) { - snes_.SetButtonState(1, 8, true); - } - - if (ImGui::IsKeyPressed(keybindings_.y_button)) { - snes_.SetButtonState(1, 9, true); - } - - if (ImGui::IsKeyPressed(keybindings_.l_button)) { - snes_.SetButtonState(1, 10, true); - } - - if (ImGui::IsKeyPressed(keybindings_.r_button)) { - snes_.SetButtonState(1, 11, true); - } - - if (ImGui::IsKeyReleased(keybindings_.a_button)) { - snes_.SetButtonState(1, 0, false); - } - - if (ImGui::IsKeyReleased(keybindings_.b_button)) { - snes_.SetButtonState(1, 1, false); - } - - if (ImGui::IsKeyReleased(keybindings_.select_button)) { - snes_.SetButtonState(1, 2, false); - } - - if (ImGui::IsKeyReleased(keybindings_.start_button)) { - snes_.SetButtonState(1, 3, false); - } - - if (ImGui::IsKeyReleased(keybindings_.up_button)) { - snes_.SetButtonState(1, 4, false); - } - - if (ImGui::IsKeyReleased(keybindings_.down_button)) { - snes_.SetButtonState(1, 5, false); - } - - if (ImGui::IsKeyReleased(keybindings_.left_button)) { - snes_.SetButtonState(1, 6, false); - } - - if (ImGui::IsKeyReleased(keybindings_.right_button)) { - snes_.SetButtonState(1, 7, false); - } - - if (ImGui::IsKeyReleased(keybindings_.x_button)) { - snes_.SetButtonState(1, 8, false); - } - - if (ImGui::IsKeyReleased(keybindings_.y_button)) { - snes_.SetButtonState(1, 9, false); - } - - if (ImGui::IsKeyReleased(keybindings_.l_button)) { - snes_.SetButtonState(1, 10, false); - } - - if (ImGui::IsKeyReleased(keybindings_.r_button)) { - snes_.SetButtonState(1, 11, false); - } -} +// REMOVED: HandleEvents() - replaced by ui::InputHandler::Poll() +// The old ImGui::IsKeyPressed/Released approach was event-based and didn't work properly +// for continuous game input. Now using SDL_GetKeyboardState() for proper polling. void Emulator::RenderBreakpointList() { if (ImGui::Button("Set SPC PC")) { @@ -1469,90 +1386,8 @@ void Emulator::RenderSaveStates() { } void Emulator::RenderKeyboardConfig() { - try { - auto& theme_manager = gui::ThemeManager::Get(); - const auto& theme = theme_manager.GetCurrentTheme(); - - ImGui::PushStyleColor(ImGuiCol_ChildBg, - ConvertColorToImVec4(theme.child_bg)); - ImGui::BeginChild("##KeyboardConfig", ImVec2(0, 0), true); - - // Keyboard Configuration - if (ImGui::CollapsingHeader("SNES Controller Mapping", - ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TextWrapped("Click on a button and press a key to remap it."); - ImGui::Separator(); - - if (ImGui::BeginTable("KeyboardTable", 2, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableSetupColumn("Button", ImGuiTableColumnFlags_WidthFixed, - 120); - ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableHeadersRow(); - - auto DrawKeyBinding = [&](const char* label, ImGuiKey& key) { - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::TextColored(ConvertColorToImVec4(theme.accent), "%s", label); - ImGui::TableNextColumn(); - - std::string button_label = - absl::StrFormat("%s##%s", ImGui::GetKeyName(key), label); - if (ImGui::Button(button_label.c_str(), ImVec2(-1, 0))) { - // TODO: Implement key remapping - ImGui::OpenPopup(absl::StrFormat("Remap%s", label).c_str()); - } - - if (ImGui::BeginPopup(absl::StrFormat("Remap%s", label).c_str())) { - ImGui::Text("Press any key..."); - // TODO: Detect key press and update binding - ImGui::EndPopup(); - } - }; - - DrawKeyBinding("A Button", keybindings_.a_button); - DrawKeyBinding("B Button", keybindings_.b_button); - DrawKeyBinding("X Button", keybindings_.x_button); - DrawKeyBinding("Y Button", keybindings_.y_button); - DrawKeyBinding("L Button", keybindings_.l_button); - DrawKeyBinding("R Button", keybindings_.r_button); - DrawKeyBinding("Start", keybindings_.start_button); - DrawKeyBinding("Select", keybindings_.select_button); - DrawKeyBinding("Up", keybindings_.up_button); - DrawKeyBinding("Down", keybindings_.down_button); - DrawKeyBinding("Left", keybindings_.left_button); - DrawKeyBinding("Right", keybindings_.right_button); - - ImGui::EndTable(); - } - } - - // Emulator Hotkeys - if (ImGui::CollapsingHeader("Emulator Hotkeys")) { - ImGui::TextWrapped("System-level keyboard shortcuts:"); - ImGui::Separator(); - - ImGui::BulletText("F1-F4: Quick save to slot 1-4"); - ImGui::BulletText("Shift+F1-F4: Quick load from slot 1-4"); - ImGui::BulletText("Backquote (`): Rewind gameplay"); - ImGui::BulletText("Tab: Fast forward (turbo mode)"); - ImGui::BulletText("Pause/Break: Pause/Resume emulation"); - ImGui::BulletText("F12: Take screenshot"); - } - - // Reset to defaults - if (ImGui::Button("Reset to Defaults", ImVec2(-1, 35))) { - keybindings_ = EmulatorKeybindings(); - } - - ImGui::EndChild(); - ImGui::PopStyleColor(); - } catch (const std::exception& e) { - try { - ImGui::PopStyleColor(); - } catch (...) {} - ImGui::Text("Keyboard Config Error: %s", e.what()); - } + // Delegate to the input manager UI + ui::RenderKeyboardConfig(&input_manager_); } void Emulator::RenderApuDebugger() { @@ -1677,24 +1512,80 @@ void Emulator::RenderApuDebugger() { } // Quick Actions - if (ImGui::CollapsingHeader("Quick Actions")) { - if (ImGui::Button("Force Handshake ($CC)", ImVec2(-1, 30))) { + if (ImGui::CollapsingHeader("Quick Actions", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::TextColored(ConvertColorToImVec4(theme.warning), + "⚠️ Manual Testing Tools"); + ImGui::Separator(); + + // Full handshake sequence test + if (ImGui::Button("🎯 Full Handshake Test", ImVec2(-1, 35))) { + LOG_INFO("APU_DEBUG", "=== MANUAL HANDSHAKE TEST SEQUENCE ==="); + + // Step 1: Write $CC to F4 (initiate handshake) snes_.Write(0x002140, 0xCC); - LOG_INFO("APU_DEBUG", "Manually forced handshake by writing $CC to F4"); + LOG_INFO("APU_DEBUG", "Step 1: Wrote $CC to F4 (port $2140)"); + + // Step 2: Write $01 to F5 (data port) + snes_.Write(0x002141, 0x01); + LOG_INFO("APU_DEBUG", "Step 2: Wrote $01 to F5 (port $2141)"); + + // Step 3: Write $00 to F6 (address low) + snes_.Write(0x002142, 0x00); + LOG_INFO("APU_DEBUG", "Step 3: Wrote $00 to F6 (port $2142)"); + + // Step 4: Write $02 to F7 (address high) + snes_.Write(0x002143, 0x02); + LOG_INFO("APU_DEBUG", "Step 4: Wrote $02 to F7 (port $2143)"); + + LOG_INFO("APU_DEBUG", "Handshake initiated - check Port Activity Log"); } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Manually trigger CPU handshake (for testing)"); + ImGui::SetTooltip("Manually execute full handshake sequence:\n" + "$CC → F4 (init), $01 → F5, $00 → F6, $02 → F7"); } + ImGui::Spacing(); + + // Individual port writes for debugging + if (ImGui::CollapsingHeader("Manual Port Writes")) { + static uint8_t port_values[4] = {0xCC, 0x01, 0x00, 0x02}; + + for (int i = 0; i < 4; ++i) { + ImGui::PushID(i); + ImGui::Text("F%d ($214%d):", i + 4, i); + ImGui::SameLine(); + ImGui::SetNextItemWidth(80); + ImGui::InputScalar("##val", ImGuiDataType_U8, &port_values[i], NULL, NULL, "%02X", + ImGuiInputTextFlags_CharsHexadecimal); + ImGui::SameLine(); + if (ImGui::Button("Write")) { + snes_.Write(0x002140 + i, port_values[i]); + LOG_INFO("APU_DEBUG", "Wrote $%02X to F%d (port $214%d)", + port_values[i], i + 4, i); + } + ImGui::PopID(); + } + } + + ImGui::Spacing(); + ImGui::Separator(); + + // System controls if (ImGui::Button("Reset APU", ImVec2(-1, 30))) { snes_.apu().Reset(); LOG_INFO("APU_DEBUG", "APU manually reset"); } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Full APU reset - clears all state"); + } if (ImGui::Button("Clear Port History", ImVec2(-1, 30))) { snes_.apu_handshake_tracker().Reset(); LOG_INFO("APU_DEBUG", "Port history cleared"); } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Clear the port activity log"); + } } ImGui::EndChild(); diff --git a/src/app/emu/emulator.h b/src/app/emu/emulator.h index 7bfeaab3..3991ea2e 100644 --- a/src/app/emu/emulator.h +++ b/src/app/emu/emulator.h @@ -8,6 +8,7 @@ #include "app/emu/audio/audio_backend.h" #include "app/emu/debug/breakpoint_manager.h" #include "app/emu/debug/disassembly_viewer.h" +#include "app/emu/input/input_manager.h" #include "app/rom.h" namespace yaze { @@ -21,20 +22,8 @@ class IRenderer; */ namespace emu { -struct EmulatorKeybindings { - ImGuiKey a_button = ImGuiKey_Z; - ImGuiKey b_button = ImGuiKey_A; - ImGuiKey x_button = ImGuiKey_S; - ImGuiKey y_button = ImGuiKey_X; - ImGuiKey l_button = ImGuiKey_Q; - ImGuiKey r_button = ImGuiKey_W; - ImGuiKey start_button = ImGuiKey_Enter; - ImGuiKey select_button = ImGuiKey_Backspace; - ImGuiKey up_button = ImGuiKey_UpArrow; - ImGuiKey down_button = ImGuiKey_DownArrow; - ImGuiKey left_button = ImGuiKey_LeftArrow; - ImGuiKey right_button = ImGuiKey_RightArrow; -}; +// REMOVED: EmulatorKeybindings (ImGuiKey-based) +// Now using ui::InputHandler with SDL_GetKeyboardState() for proper continuous polling /** * @class Emulator @@ -98,7 +87,6 @@ class Emulator { private: void RenderNavBar(); - void HandleEvents(); void RenderEmulatorInterface(); void RenderSnesPpu(); @@ -161,7 +149,8 @@ class Emulator { std::vector rom_data_; - EmulatorKeybindings keybindings_; + // Input handling (abstracted for SDL2/SDL3/custom backends) + input::InputManager input_manager_; }; } // namespace emu diff --git a/src/app/emu/input/input_backend.cc b/src/app/emu/input/input_backend.cc new file mode 100644 index 00000000..4258faf3 --- /dev/null +++ b/src/app/emu/input/input_backend.cc @@ -0,0 +1,182 @@ +#include "app/emu/input/input_backend.h" + +#include "SDL.h" +#include "util/log.h" + +namespace yaze { +namespace emu { +namespace input { + +/** + * @brief SDL2 input backend implementation + */ +class SDL2InputBackend : public IInputBackend { + public: + SDL2InputBackend() = default; + ~SDL2InputBackend() override { Shutdown(); } + + bool Initialize(const InputConfig& config) override { + if (initialized_) { + LOG_WARN("InputBackend", "Already initialized"); + return true; + } + + config_ = config; + + // Set default SDL2 keycodes if not configured + if (config_.key_a == 0) { + config_.key_a = SDLK_x; + config_.key_b = SDLK_z; + config_.key_x = SDLK_s; + config_.key_y = SDLK_a; + config_.key_l = SDLK_d; + config_.key_r = SDLK_c; + config_.key_start = SDLK_RETURN; + config_.key_select = SDLK_RSHIFT; + config_.key_up = SDLK_UP; + config_.key_down = SDLK_DOWN; + config_.key_left = SDLK_LEFT; + config_.key_right = SDLK_RIGHT; + } + + initialized_ = true; + LOG_INFO("InputBackend", "SDL2 Input Backend initialized"); + return true; + } + + void Shutdown() override { + if (initialized_) { + initialized_ = false; + LOG_INFO("InputBackend", "SDL2 Input Backend shut down"); + } + } + + 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); + + // 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)]); + state.SetButton(SnesButton::SELECT, keyboard_state[SDL_GetScancodeFromKey(config_.key_select)]); + state.SetButton(SnesButton::START, keyboard_state[SDL_GetScancodeFromKey(config_.key_start)]); + state.SetButton(SnesButton::UP, keyboard_state[SDL_GetScancodeFromKey(config_.key_up)]); + state.SetButton(SnesButton::DOWN, keyboard_state[SDL_GetScancodeFromKey(config_.key_down)]); + state.SetButton(SnesButton::LEFT, keyboard_state[SDL_GetScancodeFromKey(config_.key_left)]); + state.SetButton(SnesButton::RIGHT, keyboard_state[SDL_GetScancodeFromKey(config_.key_right)]); + state.SetButton(SnesButton::A, keyboard_state[SDL_GetScancodeFromKey(config_.key_a)]); + state.SetButton(SnesButton::X, keyboard_state[SDL_GetScancodeFromKey(config_.key_x)]); + state.SetButton(SnesButton::L, keyboard_state[SDL_GetScancodeFromKey(config_.key_l)]); + state.SetButton(SnesButton::R, keyboard_state[SDL_GetScancodeFromKey(config_.key_r)]); + } else { + // Event-based mode (use cached event state) + state = event_state_; + } + + // TODO: Add gamepad support + // if (config_.enable_gamepad) { ... } + + return state; + } + + void ProcessEvent(void* event) override { + if (!initialized_ || !event) return; + + SDL_Event* sdl_event = static_cast(event); + + // Cache keyboard events for event-based mode + if (sdl_event->type == SDL_KEYDOWN) { + UpdateEventState(sdl_event->key.keysym.sym, true); + } else if (sdl_event->type == SDL_KEYUP) { + UpdateEventState(sdl_event->key.keysym.sym, false); + } + + // TODO: Handle gamepad events + } + + InputConfig GetConfig() const override { return config_; } + + void SetConfig(const InputConfig& config) override { + config_ = config; + } + + std::string GetBackendName() const override { return "SDL2"; } + + bool IsInitialized() const override { return initialized_; } + + private: + void UpdateEventState(int keycode, bool pressed) { + // Map keycode to button and update event state + if (keycode == config_.key_a) event_state_.SetButton(SnesButton::A, pressed); + else if (keycode == config_.key_b) event_state_.SetButton(SnesButton::B, pressed); + else if (keycode == config_.key_x) event_state_.SetButton(SnesButton::X, pressed); + else if (keycode == config_.key_y) event_state_.SetButton(SnesButton::Y, pressed); + else if (keycode == config_.key_l) event_state_.SetButton(SnesButton::L, pressed); + else if (keycode == config_.key_r) event_state_.SetButton(SnesButton::R, pressed); + else if (keycode == config_.key_start) event_state_.SetButton(SnesButton::START, pressed); + else if (keycode == config_.key_select) event_state_.SetButton(SnesButton::SELECT, pressed); + else if (keycode == config_.key_up) event_state_.SetButton(SnesButton::UP, pressed); + else if (keycode == config_.key_down) event_state_.SetButton(SnesButton::DOWN, pressed); + else if (keycode == config_.key_left) event_state_.SetButton(SnesButton::LEFT, pressed); + else if (keycode == config_.key_right) event_state_.SetButton(SnesButton::RIGHT, pressed); + } + + InputConfig config_; + bool initialized_ = false; + ControllerState event_state_; // Cached state for event-based mode +}; + +/** + * @brief Null input backend for testing/replay + */ +class NullInputBackend : public IInputBackend { + public: + bool Initialize(const InputConfig& config) override { + config_ = config; + return true; + } + void Shutdown() override {} + ControllerState Poll(int player) override { return replay_state_; } + void ProcessEvent(void* event) override {} + InputConfig GetConfig() const override { return config_; } + void SetConfig(const InputConfig& config) override { config_ = config; } + std::string GetBackendName() const override { return "NULL"; } + bool IsInitialized() const override { return true; } + + // For replay/testing - set controller state directly + void SetReplayState(const ControllerState& state) { replay_state_ = state; } + + private: + InputConfig config_; + ControllerState replay_state_; +}; + +// Factory implementation +std::unique_ptr InputBackendFactory::Create(BackendType type) { + switch (type) { + case BackendType::SDL2: + return std::make_unique(); + + case BackendType::SDL3: + // TODO: Implement SDL3 backend when SDL3 is stable + LOG_WARN("InputBackend", "SDL3 backend not yet implemented, using SDL2"); + return std::make_unique(); + + case BackendType::NULL_BACKEND: + return std::make_unique(); + + default: + LOG_ERROR("InputBackend", "Unknown backend type, using SDL2"); + return std::make_unique(); + } +} + +} // namespace input +} // namespace emu +} // namespace yaze + diff --git a/src/app/emu/input/input_backend.h b/src/app/emu/input/input_backend.h new file mode 100644 index 00000000..d5254e77 --- /dev/null +++ b/src/app/emu/input/input_backend.h @@ -0,0 +1,151 @@ +#ifndef YAZE_APP_EMU_INPUT_INPUT_BACKEND_H_ +#define YAZE_APP_EMU_INPUT_INPUT_BACKEND_H_ + +#include +#include +#include + +namespace yaze { +namespace emu { +namespace input { + +/** + * @brief SNES controller button mapping (platform-agnostic) + */ +enum class SnesButton : uint8_t { + B = 0, // Bit 0 + Y = 1, // Bit 1 + SELECT = 2, // Bit 2 + START = 3, // Bit 3 + UP = 4, // Bit 4 + DOWN = 5, // Bit 5 + LEFT = 6, // Bit 6 + RIGHT = 7, // Bit 7 + A = 8, // Bit 8 + X = 9, // Bit 9 + L = 10, // Bit 10 + R = 11 // Bit 11 +}; + +/** + * @brief Controller state (16-bit SNES controller format) + */ +struct ControllerState { + uint16_t buttons = 0; // Bit field matching SNES hardware layout + + bool IsPressed(SnesButton button) const { + return (buttons & (1 << static_cast(button))) != 0; + } + + void SetButton(SnesButton button, bool pressed) { + if (pressed) { + buttons |= (1 << static_cast(button)); + } else { + buttons &= ~(1 << static_cast(button)); + } + } + + void Clear() { buttons = 0; } +}; + +/** + * @brief Input configuration (platform-agnostic key codes) + */ +struct InputConfig { + // Platform-agnostic key codes (mapped to platform-specific in backend) + // Using generic names that can be mapped to SDL2/SDL3/other + int key_a = 0; // Default: X key + int key_b = 0; // Default: Z key + int key_x = 0; // Default: S key + int key_y = 0; // Default: A key + int key_l = 0; // Default: D key + int key_r = 0; // Default: C key + int key_start = 0; // Default: Enter + int key_select = 0; // Default: RShift + int key_up = 0; // Default: Up arrow + int key_down = 0; // Default: Down arrow + int key_left = 0; // Default: Left arrow + int key_right = 0; // Default: Right arrow + + // Enable/disable continuous polling (vs event-based) + bool continuous_polling = true; + + // Enable gamepad support + bool enable_gamepad = true; + int gamepad_index = 0; // Which gamepad to use (0-3) +}; + +/** + * @brief Abstract input backend interface + * + * Allows swapping between SDL2, SDL3, or custom input implementations + * without changing emulator code. + */ +class IInputBackend { + public: + virtual ~IInputBackend() = default; + + /** + * @brief Initialize the input backend + */ + virtual bool Initialize(const InputConfig& config) = 0; + + /** + * @brief Shutdown the input backend + */ + virtual void Shutdown() = 0; + + /** + * @brief Poll current input state (call every frame) + * @param player Player number (1-4) + * @return Current controller state + */ + virtual ControllerState Poll(int player = 1) = 0; + + /** + * @brief Process platform-specific events (optional) + * @param event Platform-specific event data (e.g., SDL_Event*) + */ + virtual void ProcessEvent(void* event) = 0; + + /** + * @brief Get current configuration + */ + virtual InputConfig GetConfig() const = 0; + + /** + * @brief Update configuration (hot-reload) + */ + virtual void SetConfig(const InputConfig& config) = 0; + + /** + * @brief Get backend name for debugging + */ + virtual std::string GetBackendName() const = 0; + + /** + * @brief Check if backend is initialized + */ + virtual bool IsInitialized() const = 0; +}; + +/** + * @brief Factory for creating input backends + */ +class InputBackendFactory { + public: + enum class BackendType { + SDL2, + SDL3, // Future + NULL_BACKEND // For testing/replay + }; + + static std::unique_ptr Create(BackendType type); +}; + +} // namespace input +} // namespace emu +} // namespace yaze + +#endif // YAZE_APP_EMU_INPUT_INPUT_BACKEND_H_ + diff --git a/src/app/emu/input/input_manager.cc b/src/app/emu/input/input_manager.cc new file mode 100644 index 00000000..718c1b1d --- /dev/null +++ b/src/app/emu/input/input_manager.cc @@ -0,0 +1,83 @@ +#include "app/emu/input/input_manager.h" + +#include "app/emu/snes.h" +#include "util/log.h" + +namespace yaze { +namespace emu { +namespace input { + +bool InputManager::Initialize(InputBackendFactory::BackendType type) { + backend_ = InputBackendFactory::Create(type); + if (!backend_) { + LOG_ERROR("InputManager", "Failed to create input backend"); + return false; + } + + InputConfig config; + config.continuous_polling = true; // Always use continuous polling for games + config.enable_gamepad = false; // TODO: Enable when gamepad support added + + if (!backend_->Initialize(config)) { + LOG_ERROR("InputManager", "Failed to initialize input backend"); + return false; + } + + LOG_INFO("InputManager", "Initialized with backend: %s", + backend_->GetBackendName().c_str()); + return true; +} + +void InputManager::Initialize(std::unique_ptr backend) { + backend_ = std::move(backend); + + if (backend_) { + LOG_INFO("InputManager", "Initialized with custom backend: %s", + backend_->GetBackendName().c_str()); + } +} + +void InputManager::Shutdown() { + if (backend_) { + backend_->Shutdown(); + backend_.reset(); + } +} + +void InputManager::Poll(Snes* snes, int player) { + if (!snes || !backend_) return; + + // Poll backend for current controller state + ControllerState 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 + for (int i = 0; i < 12; i++) { + bool pressed = (state.buttons & (1 << i)) != 0; + snes->SetButtonState(player, i, pressed); + } +} + +void InputManager::ProcessEvent(void* event) { + if (backend_) { + backend_->ProcessEvent(event); + } +} + +InputConfig InputManager::GetConfig() const { + if (backend_) { + return backend_->GetConfig(); + } + return InputConfig{}; +} + +void InputManager::SetConfig(const InputConfig& config) { + if (backend_) { + backend_->SetConfig(config); + } +} + +} // namespace input +} // namespace emu +} // namespace yaze + diff --git a/src/app/emu/input/input_manager.h b/src/app/emu/input/input_manager.h new file mode 100644 index 00000000..34a4657d --- /dev/null +++ b/src/app/emu/input/input_manager.h @@ -0,0 +1,83 @@ +#ifndef YAZE_APP_EMU_INPUT_INPUT_MANAGER_H_ +#define YAZE_APP_EMU_INPUT_INPUT_MANAGER_H_ + +#include +#include "app/emu/input/input_backend.h" + +namespace yaze { +namespace emu { + +// Forward declaration +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); + + private: + std::unique_ptr backend_; +}; + +} // namespace input +} // namespace emu +} // namespace yaze + +#endif // YAZE_APP_EMU_INPUT_INPUT_MANAGER_H_ + diff --git a/src/app/emu/ui/debugger_ui.h b/src/app/emu/ui/debugger_ui.h new file mode 100644 index 00000000..7e4e9fc2 --- /dev/null +++ b/src/app/emu/ui/debugger_ui.h @@ -0,0 +1,50 @@ +#ifndef YAZE_APP_EMU_UI_DEBUGGER_UI_H_ +#define YAZE_APP_EMU_UI_DEBUGGER_UI_H_ + +#include +#include "imgui/imgui.h" + +namespace yaze { +namespace emu { + +// Forward declarations +class Emulator; + +namespace ui { + +/** + * @brief Modern CPU debugger with registers, flags, and controls + */ +void RenderModernCpuDebugger(Emulator* emu); + +/** + * @brief Breakpoint list and management + */ +void RenderBreakpointList(Emulator* emu); + +/** + * @brief Memory viewer/editor + */ +void RenderMemoryViewer(Emulator* emu); + +/** + * @brief CPU instruction log (legacy, prefer DisassemblyViewer) + */ +void RenderCpuInstructionLog(Emulator* emu, uint32_t log_size); + +/** + * @brief APU/Audio debugger with handshake tracker + */ +void RenderApuDebugger(Emulator* emu); + +/** + * @brief AI Agent panel for automated testing/gameplay + */ +void RenderAIAgentPanel(Emulator* emu); + +} // namespace ui +} // namespace emu +} // namespace yaze + +#endif // YAZE_APP_EMU_UI_DEBUGGER_UI_H_ + diff --git a/src/app/emu/ui/emulator_ui.h b/src/app/emu/ui/emulator_ui.h new file mode 100644 index 00000000..15c2f7ee --- /dev/null +++ b/src/app/emu/ui/emulator_ui.h @@ -0,0 +1,40 @@ +#ifndef YAZE_APP_EMU_UI_EMULATOR_UI_H_ +#define YAZE_APP_EMU_UI_EMULATOR_UI_H_ + +#include "imgui/imgui.h" + +namespace yaze { +namespace emu { + +// Forward declarations +class Emulator; +class Snes; + +namespace ui { + +/** + * @brief Main emulator UI interface - renders the emulator window + */ +void RenderEmulatorInterface(Emulator* emu); + +/** + * @brief Navigation bar with play/pause, step, reset controls + */ +void RenderNavBar(Emulator* emu); + +/** + * @brief SNES PPU output display + */ +void RenderSnesPpu(Emulator* emu); + +/** + * @brief Performance metrics (FPS, frame time, audio status) + */ +void RenderPerformanceMonitor(Emulator* emu); + +} // namespace ui +} // namespace emu +} // namespace yaze + +#endif // YAZE_APP_EMU_UI_EMULATOR_UI_H_ + diff --git a/src/app/emu/ui/input_handler.cc b/src/app/emu/ui/input_handler.cc new file mode 100644 index 00000000..46a74bcf --- /dev/null +++ b/src/app/emu/ui/input_handler.cc @@ -0,0 +1,125 @@ +#include "app/emu/ui/input_handler.h" + +#include "SDL.h" +#include "app/gui/icons.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace emu { +namespace ui { + +void RenderKeyboardConfig(input::InputManager* manager) { + if (!manager || !manager->backend()) return; + + auto config = manager->GetConfig(); + + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), + ICON_MD_INFO " Keyboard Configuration"); + ImGui::Separator(); + + ImGui::Text("Backend: %s", manager->backend()->GetBackendName().c_str()); + ImGui::Separator(); + + ImGui::TextWrapped("Configure keyboard bindings for SNES controller emulation. " + "Click a button and press a key to rebind."); + ImGui::Spacing(); + + auto RenderKeyBind = [&](const char* label, int* key) { + ImGui::Text("%s:", label); + ImGui::SameLine(150); + + // Show current key + const char* key_name = SDL_GetKeyName(*key); + ImGui::PushID(label); + if (ImGui::Button(key_name, ImVec2(120, 0))) { + ImGui::OpenPopup("Rebind"); + } + + if (ImGui::BeginPopup("Rebind")) { + ImGui::Text("Press any key..."); + ImGui::Separator(); + + // Poll for key press (SDL2-specific for now) + SDL_Event event; + if (SDL_PollEvent(&event) && event.type == SDL_KEYDOWN) { + if (event.key.keysym.sym != SDLK_UNKNOWN && event.key.keysym.sym != SDLK_ESCAPE) { + *key = event.key.keysym.sym; + ImGui::CloseCurrentPopup(); + } + } + + if (ImGui::Button("Cancel", ImVec2(-1, 0))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } + ImGui::PopID(); + }; + + // Face Buttons + if (ImGui::CollapsingHeader("Face Buttons", ImGuiTreeNodeFlags_DefaultOpen)) { + RenderKeyBind("A Button", &config.key_a); + RenderKeyBind("B Button", &config.key_b); + RenderKeyBind("X Button", &config.key_x); + RenderKeyBind("Y Button", &config.key_y); + } + + // D-Pad + if (ImGui::CollapsingHeader("D-Pad", ImGuiTreeNodeFlags_DefaultOpen)) { + RenderKeyBind("Up", &config.key_up); + RenderKeyBind("Down", &config.key_down); + RenderKeyBind("Left", &config.key_left); + RenderKeyBind("Right", &config.key_right); + } + + // Shoulder Buttons + if (ImGui::CollapsingHeader("Shoulder Buttons")) { + RenderKeyBind("L Button", &config.key_l); + RenderKeyBind("R Button", &config.key_r); + } + + // Start/Select + if (ImGui::CollapsingHeader("Start/Select")) { + RenderKeyBind("Start", &config.key_start); + RenderKeyBind("Select", &config.key_select); + } + + ImGui::Spacing(); + ImGui::Separator(); + + // Input mode + ImGui::Checkbox("Continuous Polling", &config.continuous_polling); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Recommended: ON for games (detects held buttons)\nOFF for event-based input"); + } + + ImGui::Spacing(); + + // Apply button + if (ImGui::Button("Apply Changes", ImVec2(-1, 30))) { + manager->SetConfig(config); + } + + ImGui::Spacing(); + + // Defaults + if (ImGui::Button("Reset to Defaults", ImVec2(-1, 30))) { + config = input::InputConfig(); // Reset to defaults + manager->SetConfig(config); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), + "Default Bindings:"); + ImGui::BulletText("A/B/X/Y: X/Z/S/A"); + ImGui::BulletText("L/R: D/C"); + ImGui::BulletText("D-Pad: Arrow Keys"); + ImGui::BulletText("Start/Select: Enter/RShift"); +} + +} // namespace ui +} // namespace emu +} // namespace yaze + diff --git a/src/app/emu/ui/input_handler.h b/src/app/emu/ui/input_handler.h new file mode 100644 index 00000000..113d7a9e --- /dev/null +++ b/src/app/emu/ui/input_handler.h @@ -0,0 +1,21 @@ +#ifndef YAZE_APP_EMU_UI_INPUT_HANDLER_H_ +#define YAZE_APP_EMU_UI_INPUT_HANDLER_H_ + +#include "app/emu/input/input_manager.h" + +namespace yaze { +namespace emu { +namespace ui { + +/** + * @brief Render keyboard configuration UI + * @param manager InputManager to configure + */ +void RenderKeyboardConfig(input::InputManager* manager); + +} // namespace ui +} // namespace emu +} // namespace yaze + +#endif // YAZE_APP_EMU_UI_INPUT_HANDLER_H_ +