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

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

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

View File

@@ -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<uint8_t> rom_data_;
EmulatorKeybindings keybindings_;
// Input handling (abstracted for SDL2/SDL3/custom backends)
input::InputManager input_manager_;
};
} // namespace emu

View File

@@ -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<SDL_Event*>(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<IInputBackend> InputBackendFactory::Create(BackendType type) {
switch (type) {
case BackendType::SDL2:
return std::make_unique<SDL2InputBackend>();
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<SDL2InputBackend>();
case BackendType::NULL_BACKEND:
return std::make_unique<NullInputBackend>();
default:
LOG_ERROR("InputBackend", "Unknown backend type, using SDL2");
return std::make_unique<SDL2InputBackend>();
}
}
} // namespace input
} // namespace emu
} // namespace yaze

View File

@@ -0,0 +1,151 @@
#ifndef YAZE_APP_EMU_INPUT_INPUT_BACKEND_H_
#define YAZE_APP_EMU_INPUT_INPUT_BACKEND_H_
#include <cstdint>
#include <memory>
#include <string>
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<uint8_t>(button))) != 0;
}
void SetButton(SnesButton button, bool pressed) {
if (pressed) {
buttons |= (1 << static_cast<uint8_t>(button));
} else {
buttons &= ~(1 << static_cast<uint8_t>(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<IInputBackend> Create(BackendType type);
};
} // namespace input
} // namespace emu
} // namespace yaze
#endif // YAZE_APP_EMU_INPUT_INPUT_BACKEND_H_

View File

@@ -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<IInputBackend> 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

View File

@@ -0,0 +1,83 @@
#ifndef YAZE_APP_EMU_INPUT_INPUT_MANAGER_H_
#define YAZE_APP_EMU_INPUT_INPUT_MANAGER_H_
#include <memory>
#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<IInputBackend> 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<IInputBackend> backend_;
};
} // namespace input
} // namespace emu
} // namespace yaze
#endif // YAZE_APP_EMU_INPUT_INPUT_MANAGER_H_

View File

@@ -0,0 +1,50 @@
#ifndef YAZE_APP_EMU_UI_DEBUGGER_UI_H_
#define YAZE_APP_EMU_UI_DEBUGGER_UI_H_
#include <cstdint>
#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_

View File

@@ -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_

View File

@@ -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

View File

@@ -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_