feat: Implement input management system with SDL2 support

- Introduced a new input management system utilizing SDL2 for continuous polling of SNES controller states, enhancing responsiveness and gameplay experience.
- Replaced the previous ImGui-based event handling with a more robust input manager that supports multiple backends and configurations.
- Added a dedicated input backend for SDL2, allowing for flexible key mapping and improved input handling.
- Updated the Emulator class to integrate the new input manager, ensuring seamless interaction with the emulator's UI and game logic.
- Refactored keyboard configuration UI to facilitate easy remapping of SNES controller buttons, improving user accessibility.
This commit is contained in:
scawful
2025-10-08 21:40:21 -04:00
parent ba70176ee2
commit 0579fc2c65
11 changed files with 878 additions and 224 deletions

View File

@@ -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();