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:
@@ -123,33 +123,72 @@ void Cpu::RunOpcode() {
|
|||||||
} else {
|
} else {
|
||||||
uint8_t opcode = ReadOpcode();
|
uint8_t opcode = ReadOpcode();
|
||||||
|
|
||||||
// Debug: Log key instructions during boot
|
// AUDIO DEBUG: Enhanced logging for audio initialization tracking
|
||||||
static int instruction_count = 0;
|
static int instruction_count = 0;
|
||||||
instruction_count++;
|
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;
|
uint16_t cur_pc = PC - 1;
|
||||||
if (PB == 0x00 && cur_pc >= 0x8888 && cur_pc <= 0x88FF) {
|
|
||||||
// Detailed logging at critical handshake points
|
// Track entry into Bank $00 (where all audio code lives)
|
||||||
static int handshake_log_count = 0;
|
static bool entered_bank00 = false;
|
||||||
if (cur_pc == 0x88B3 || cur_pc == 0x88B6) {
|
static bool logged_first_nmi = false;
|
||||||
if (handshake_log_count++ < 5 || handshake_log_count % 1000 == 0) {
|
|
||||||
// At $88B3: CMP.w APUIO0 - comparing A with F4
|
if (PB == 0x00 && !entered_bank00) {
|
||||||
// At $88B6: BNE .wait_for_sync_a - branch if not equal
|
LOG_INFO("CPU_AUDIO", "=== ENTERED BANK $00 at PC=$%04X (instruction #%d) ===",
|
||||||
uint8_t f4_val = callbacks_.read_byte(0x2140); // Read F4 directly
|
cur_pc, instruction_count);
|
||||||
LOG_DEBUG("CPU", "Handshake wait: PC=$%04X A(counter)=$%02X F4(SPC)=$%02X X(remain)=$%04X",
|
entered_bank00 = true;
|
||||||
cur_pc, A & 0xFF, f4_val, X);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
should_log = (cur_pc >= 0x88CF && cur_pc <= 0x88E0); // Only log setup, not tight loop
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
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);
|
instruction_count, PB, PC - 1, opcode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace yaze::core {
|
|||||||
|
|
||||||
#include "app/emu/cpu/internal/opcodes.h"
|
#include "app/emu/cpu/internal/opcodes.h"
|
||||||
#include "app/emu/debug/disassembly_viewer.h"
|
#include "app/emu/debug/disassembly_viewer.h"
|
||||||
|
#include "app/emu/ui/input_handler.h"
|
||||||
#include "app/gui/color.h"
|
#include "app/gui/color.h"
|
||||||
#include "app/gui/editor_layout.h"
|
#include "app/gui/editor_layout.h"
|
||||||
#include "app/gui/icons.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
|
// Initialize SNES and create PPU texture on first run
|
||||||
// This happens lazily when user opens the emulator window
|
// This happens lazily when user opens the emulator window
|
||||||
if (!snes_initialized_ && rom->is_loaded()) {
|
if (!snes_initialized_ && rom->is_loaded()) {
|
||||||
@@ -209,7 +220,8 @@ void Emulator::Run(Rom* rom) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (running_) {
|
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 current_count = SDL_GetPerformanceCounter();
|
||||||
uint64_t delta = current_count - last_count;
|
uint64_t delta = current_count - last_count;
|
||||||
@@ -674,104 +686,9 @@ void Emulator::RenderNavBar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Emulator::HandleEvents() {
|
// REMOVED: HandleEvents() - replaced by ui::InputHandler::Poll()
|
||||||
// Handle user input events
|
// The old ImGui::IsKeyPressed/Released approach was event-based and didn't work properly
|
||||||
if (ImGui::IsKeyPressed(keybindings_.a_button)) {
|
// for continuous game input. Now using SDL_GetKeyboardState() for proper polling.
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Emulator::RenderBreakpointList() {
|
void Emulator::RenderBreakpointList() {
|
||||||
if (ImGui::Button("Set SPC PC")) {
|
if (ImGui::Button("Set SPC PC")) {
|
||||||
@@ -1469,90 +1386,8 @@ void Emulator::RenderSaveStates() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Emulator::RenderKeyboardConfig() {
|
void Emulator::RenderKeyboardConfig() {
|
||||||
try {
|
// Delegate to the input manager UI
|
||||||
auto& theme_manager = gui::ThemeManager::Get();
|
ui::RenderKeyboardConfig(&input_manager_);
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Emulator::RenderApuDebugger() {
|
void Emulator::RenderApuDebugger() {
|
||||||
@@ -1677,24 +1512,80 @@ void Emulator::RenderApuDebugger() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Quick Actions
|
// Quick Actions
|
||||||
if (ImGui::CollapsingHeader("Quick Actions")) {
|
if (ImGui::CollapsingHeader("Quick Actions", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
if (ImGui::Button("Force Handshake ($CC)", ImVec2(-1, 30))) {
|
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);
|
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()) {
|
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))) {
|
if (ImGui::Button("Reset APU", ImVec2(-1, 30))) {
|
||||||
snes_.apu().Reset();
|
snes_.apu().Reset();
|
||||||
LOG_INFO("APU_DEBUG", "APU manually 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))) {
|
if (ImGui::Button("Clear Port History", ImVec2(-1, 30))) {
|
||||||
snes_.apu_handshake_tracker().Reset();
|
snes_.apu_handshake_tracker().Reset();
|
||||||
LOG_INFO("APU_DEBUG", "Port history cleared");
|
LOG_INFO("APU_DEBUG", "Port history cleared");
|
||||||
}
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetTooltip("Clear the port activity log");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "app/emu/audio/audio_backend.h"
|
#include "app/emu/audio/audio_backend.h"
|
||||||
#include "app/emu/debug/breakpoint_manager.h"
|
#include "app/emu/debug/breakpoint_manager.h"
|
||||||
#include "app/emu/debug/disassembly_viewer.h"
|
#include "app/emu/debug/disassembly_viewer.h"
|
||||||
|
#include "app/emu/input/input_manager.h"
|
||||||
#include "app/rom.h"
|
#include "app/rom.h"
|
||||||
|
|
||||||
namespace yaze {
|
namespace yaze {
|
||||||
@@ -21,20 +22,8 @@ class IRenderer;
|
|||||||
*/
|
*/
|
||||||
namespace emu {
|
namespace emu {
|
||||||
|
|
||||||
struct EmulatorKeybindings {
|
// REMOVED: EmulatorKeybindings (ImGuiKey-based)
|
||||||
ImGuiKey a_button = ImGuiKey_Z;
|
// Now using ui::InputHandler with SDL_GetKeyboardState() for proper continuous polling
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class Emulator
|
* @class Emulator
|
||||||
@@ -98,7 +87,6 @@ class Emulator {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void RenderNavBar();
|
void RenderNavBar();
|
||||||
void HandleEvents();
|
|
||||||
void RenderEmulatorInterface();
|
void RenderEmulatorInterface();
|
||||||
|
|
||||||
void RenderSnesPpu();
|
void RenderSnesPpu();
|
||||||
@@ -161,7 +149,8 @@ class Emulator {
|
|||||||
|
|
||||||
std::vector<uint8_t> rom_data_;
|
std::vector<uint8_t> rom_data_;
|
||||||
|
|
||||||
EmulatorKeybindings keybindings_;
|
// Input handling (abstracted for SDL2/SDL3/custom backends)
|
||||||
|
input::InputManager input_manager_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace emu
|
} // namespace emu
|
||||||
|
|||||||
182
src/app/emu/input/input_backend.cc
Normal file
182
src/app/emu/input/input_backend.cc
Normal 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
|
||||||
|
|
||||||
151
src/app/emu/input/input_backend.h
Normal file
151
src/app/emu/input/input_backend.h
Normal 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_
|
||||||
|
|
||||||
83
src/app/emu/input/input_manager.cc
Normal file
83
src/app/emu/input/input_manager.cc
Normal 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
|
||||||
|
|
||||||
83
src/app/emu/input/input_manager.h
Normal file
83
src/app/emu/input/input_manager.h
Normal 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_
|
||||||
|
|
||||||
50
src/app/emu/ui/debugger_ui.h
Normal file
50
src/app/emu/ui/debugger_ui.h
Normal 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_
|
||||||
|
|
||||||
40
src/app/emu/ui/emulator_ui.h
Normal file
40
src/app/emu/ui/emulator_ui.h
Normal 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_
|
||||||
|
|
||||||
125
src/app/emu/ui/input_handler.cc
Normal file
125
src/app/emu/ui/input_handler.cc
Normal 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
|
||||||
|
|
||||||
21
src/app/emu/ui/input_handler.h
Normal file
21
src/app/emu/ui/input_handler.h
Normal 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_
|
||||||
|
|
||||||
Reference in New Issue
Block a user