refactor(emulator): enhance input handling and audio resampling features

- Renamed `turbo_mode()` to `is_turbo_mode()` for clarity in the Emulator class.
- Improved input handling in the Snes class by adding button state management and ensuring proper initialization of input controllers.
- Implemented multiple audio resampling methods (linear, cosine, cubic) in the Dsp class, allowing for enhanced audio quality during playback.
- Updated the user interface to include options for selecting audio interpolation methods and added keyboard shortcuts for emulator controls.

Benefits:
- Improved code readability and maintainability through clearer method naming and structured input management.
- Enhanced audio playback quality with new resampling techniques.
- Streamlined user experience with added UI features for audio settings and keyboard shortcuts.
This commit is contained in:
scawful
2025-10-11 13:56:49 -04:00
parent 0a4b17fdd0
commit 9ffb7803f5
11 changed files with 454 additions and 114 deletions

View File

@@ -1,5 +1,6 @@
#include "app/emu/audio/dsp.h" #include "app/emu/audio/dsp.h"
#include <cmath>
#include <cstring> #include <cstring>
namespace yaze { namespace yaze {
@@ -616,6 +617,37 @@ void Dsp::Write(uint8_t adr, uint8_t val) {
ram[adr] = val; ram[adr] = val;
} }
// Helper for 4-point cubic interpolation (Catmull-Rom)
// Provides higher quality resampling compared to linear interpolation.
inline int16_t InterpolateCubic(int16_t p0, int16_t p1, int16_t p2, int16_t p3,
double t) {
double t2 = t * t;
double t3 = t2 * t;
double c0 = p1;
double c1 = 0.5 * (p2 - p0);
double c2 = (p0 - 2.5 * p1 + 2.0 * p2 - 0.5 * p3);
double c3 = 0.5 * (-p0 + 3.0 * p1 - 3.0 * p2 + p3);
double result = c0 + c1 * t + c2 * t2 + c3 * t3;
// Clamp to 16-bit range
return result > 32767.0
? 32767
: (result < -32768.0 ? -32768 : static_cast<int16_t>(result));
}
// Helper for cosine interpolation
inline int16_t InterpolateCosine(int16_t s0, int16_t s1, double mu) {
const double mu2 = (1.0 - cos(mu * 3.14159265358979323846)) / 2.0;
return static_cast<int16_t>(s0 * (1.0 - mu2) + s1 * mu2);
}
// Helper for linear interpolation
inline int16_t InterpolateLinear(int16_t s0, int16_t s1, double frac) {
return static_cast<int16_t>(s0 + frac * (s1 - s0));
}
void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame, void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame,
bool pal_timing) { bool pal_timing) {
// Resample from native samples-per-frame (NTSC: ~534, PAL: ~641) // Resample from native samples-per-frame (NTSC: ~534, PAL: ~641)
@@ -625,8 +657,19 @@ void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame,
double location = static_cast<double>((lastFrameBoundary + 0x400) & 0x3ff); double location = static_cast<double>((lastFrameBoundary + 0x400) & 0x3ff);
location -= native_per_frame; location -= native_per_frame;
// Use linear interpolation for smoother resampling
for (int i = 0; i < samples_per_frame; i++) { for (int i = 0; i < samples_per_frame; i++) {
const int idx = static_cast<int>(location);
const double frac = location - idx;
switch (interpolation_type) {
case InterpolationType::Linear: {
// const int next_idx = (idx + 1) & 0x3ff;
// const int16_t s0_l = sampleBuffer[(idx * 2) + 0];
// const int16_t s1_l = sampleBuffer[(next_idx * 2) + 0];
// sample_data[(i * 2) + 0] = InterpolateLinear(s0_l, s1_l, frac);
// const int16_t s0_r = sampleBuffer[(idx * 2) + 1];
// const int16_t s1_r = sampleBuffer[(next_idx * 2) + 1];
// sample_data[(i * 2) + 1] = InterpolateLinear(s0_r, s1_r, frac);
const int idx = static_cast<int>(location) & 0x3ff; const int idx = static_cast<int>(location) & 0x3ff;
const int next_idx = (idx + 1) & 0x3ff; const int next_idx = (idx + 1) & 0x3ff;
@@ -645,7 +688,44 @@ void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame,
sample_data[(i * 2) + 1] = static_cast<int16_t>( sample_data[(i * 2) + 1] = static_cast<int16_t>(
s0_r + frac * (s1_r - s0_r)); s0_r + frac * (s1_r - s0_r));
// location += step;
break;
}
case InterpolationType::Cosine: {
const int next_idx = (idx + 1) & 0x3ff;
const int16_t s0_l = sampleBuffer[(idx * 2) + 0];
const int16_t s1_l = sampleBuffer[(next_idx * 2) + 0];
sample_data[(i * 2) + 0] = InterpolateCosine(s0_l, s1_l, frac);
const int16_t s0_r = sampleBuffer[(idx * 2) + 1];
const int16_t s1_r = sampleBuffer[(next_idx * 2) + 1];
sample_data[(i * 2) + 1] = InterpolateCosine(s0_r, s1_r, frac);
break;
}
case InterpolationType::Cubic: {
const int idx0 = (idx - 1 + 0x400) & 0x3ff;
const int idx1 = idx & 0x3ff;
const int idx2 = (idx + 1) & 0x3ff;
const int idx3 = (idx + 2) & 0x3ff;
// Left channel
const int16_t p0_l = sampleBuffer[(idx0 * 2) + 0];
const int16_t p1_l = sampleBuffer[(idx1 * 2) + 0];
const int16_t p2_l = sampleBuffer[(idx2 * 2) + 0];
const int16_t p3_l = sampleBuffer[(idx3 * 2) + 0];
sample_data[(i * 2) + 0] =
InterpolateCubic(p0_l, p1_l, p2_l, p3_l, frac);
// Right channel
const int16_t p0_r = sampleBuffer[(idx0 * 2) + 1];
const int16_t p1_r = sampleBuffer[(idx1 * 2) + 1];
const int16_t p2_r = sampleBuffer[(idx2 * 2) + 1];
const int16_t p3_r = sampleBuffer[(idx3 * 2) + 1];
sample_data[(i * 2) + 1] =
InterpolateCubic(p0_r, p1_r, p2_r, p3_r, frac);
break;
}
}
location += step; location += step;
} }
} }

View File

@@ -7,6 +7,12 @@
namespace yaze { namespace yaze {
namespace emu { namespace emu {
enum class InterpolationType {
Linear,
Cosine,
Cubic,
};
typedef struct DspChannel { typedef struct DspChannel {
// pitch // pitch
uint16_t pitch; uint16_t pitch;
@@ -104,6 +110,8 @@ class Dsp {
void GetSamples(int16_t* sample_data, int samples_per_frame, bool pal_timing); void GetSamples(int16_t* sample_data, int samples_per_frame, bool pal_timing);
InterpolationType interpolation_type = InterpolationType::Linear;
private: private:
// sample ring buffer (1024 samples, *2 for stereo) // sample ring buffer (1024 samples, *2 for stereo)
int16_t sampleBuffer[0x400 * 2]; int16_t sampleBuffer[0x400 * 2];

View File

@@ -62,7 +62,7 @@ class Emulator {
void* ppu_texture() { return ppu_texture_; } void* ppu_texture() { return ppu_texture_; }
// Turbo mode // Turbo mode
bool turbo_mode() const { return turbo_mode_; } bool is_turbo_mode() const { return turbo_mode_; }
void set_turbo_mode(bool turbo) { turbo_mode_ = turbo; } void set_turbo_mode(bool turbo) { turbo_mode_ = turbo; }
// Debugger access // Debugger access

View File

@@ -1,6 +1,7 @@
#include "app/emu/input/input_backend.h" #include "app/emu/input/input_backend.h"
#include "SDL.h" #include "SDL.h"
#include "imgui/imgui.h"
#include "util/log.h" #include "util/log.h"
namespace yaze { namespace yaze {
@@ -60,6 +61,22 @@ class SDL2InputBackend : public IInputBackend {
// Continuous polling mode (for games) // Continuous polling mode (for games)
const uint8_t* keyboard_state = SDL_GetKeyboardState(nullptr); const uint8_t* keyboard_state = SDL_GetKeyboardState(nullptr);
// IMPORTANT: Only block input when actively typing in text fields
// Allow game input even when ImGui windows are open/focused
ImGuiIO& io = ImGui::GetIO();
// Only block if user is actively typing in a text input field
// WantTextInput is true only when an InputText widget is active
if (io.WantTextInput) {
// User is typing in a text field
// Return empty state to prevent game from processing input
static int text_input_log_count = 0;
if (text_input_log_count++ < 5) {
LOG_DEBUG("InputBackend", "Blocking game input - WantTextInput=true");
}
return ControllerState{};
}
// Map keyboard to SNES buttons // Map keyboard to SNES buttons
state.SetButton(SnesButton::B, keyboard_state[SDL_GetScancodeFromKey(config_.key_b)]); 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::Y, keyboard_state[SDL_GetScancodeFromKey(config_.key_y)]);

View File

@@ -15,8 +15,8 @@ bool InputManager::Initialize(InputBackendFactory::BackendType type) {
} }
InputConfig config; InputConfig config;
config.continuous_polling = true; // Always use continuous polling for games config.continuous_polling = true;
config.enable_gamepad = false; // TODO: Enable when gamepad support added config.enable_gamepad = false;
if (!backend_->Initialize(config)) { if (!backend_->Initialize(config)) {
LOG_ERROR("InputManager", "Failed to initialize input backend"); LOG_ERROR("InputManager", "Failed to initialize input backend");
@@ -47,13 +47,16 @@ void InputManager::Shutdown() {
void InputManager::Poll(Snes* snes, int player) { void InputManager::Poll(Snes* snes, int player) {
if (!snes || !backend_) return; if (!snes || !backend_) return;
// Poll backend for current controller state ControllerState physical_state = backend_->Poll(player);
ControllerState state = backend_->Poll(player);
// Update SNES controller state using the hardware button layout // Combine physical input with agent-controlled input (OR operation)
// SNES controller bits: 0=B, 1=Y, 2=Select, 3=Start, 4-7=DPad, 8=A, 9=X, 10=L, 11=R ControllerState final_state;
final_state.buttons = physical_state.buttons | agent_controller_state_.buttons;
// Update ALL button states every frame to ensure proper press/release
// This is critical for games that check button state every frame
for (int i = 0; i < 12; i++) { for (int i = 0; i < 12; i++) {
bool pressed = (state.buttons & (1 << i)) != 0; bool pressed = (final_state.buttons & (1 << i)) != 0;
snes->SetButtonState(player, i, pressed); snes->SetButtonState(player, i, pressed);
} }
} }
@@ -77,7 +80,14 @@ void InputManager::SetConfig(const InputConfig& config) {
} }
} }
void InputManager::PressButton(SnesButton button) {
agent_controller_state_.SetButton(button, true);
}
void InputManager::ReleaseButton(SnesButton button) {
agent_controller_state_.SetButton(button, false);
}
} // namespace input } // namespace input
} // namespace emu } // namespace emu
} // namespace yaze } // namespace yaze

View File

@@ -12,67 +12,32 @@ class Snes;
namespace input { 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 { class InputManager {
public: public:
InputManager() = default; InputManager() = default;
~InputManager() { Shutdown(); } ~InputManager() { Shutdown(); }
/**
* @brief Initialize with specific backend
*/
bool Initialize(InputBackendFactory::BackendType type = InputBackendFactory::BackendType::SDL2); bool Initialize(InputBackendFactory::BackendType type = InputBackendFactory::BackendType::SDL2);
/**
* @brief Initialize with custom backend
*/
void Initialize(std::unique_ptr<IInputBackend> backend); void Initialize(std::unique_ptr<IInputBackend> backend);
/**
* @brief Shutdown input system
*/
void Shutdown(); 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); 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); void ProcessEvent(void* event);
/**
* @brief Get backend for configuration
*/
IInputBackend* backend() { return backend_.get(); } IInputBackend* backend() { return backend_.get(); }
const IInputBackend* backend() const { return backend_.get(); } const IInputBackend* backend() const { return backend_.get(); }
/**
* @brief Check if initialized
*/
bool IsInitialized() const { return backend_ && backend_->IsInitialized(); } bool IsInitialized() const { return backend_ && backend_->IsInitialized(); }
/**
* @brief Get/set configuration
*/
InputConfig GetConfig() const; InputConfig GetConfig() const;
void SetConfig(const InputConfig& config); void SetConfig(const InputConfig& config);
// --- Agent Control API ---
void PressButton(SnesButton button);
void ReleaseButton(SnesButton button);
private: private:
std::unique_ptr<IInputBackend> backend_; std::unique_ptr<IInputBackend> backend_;
ControllerState agent_controller_state_; // State controlled by agent
}; };
} // namespace input } // namespace input
@@ -80,4 +45,3 @@ class InputManager {
} // namespace yaze } // namespace yaze
#endif // YAZE_APP_EMU_INPUT_INPUT_MANAGER_H_ #endif // YAZE_APP_EMU_INPUT_INPUT_MANAGER_H_

View File

@@ -18,12 +18,15 @@ namespace emu {
namespace { namespace {
void input_latch(Input* input, bool value) { void input_latch(Input* input, bool value) {
input->latch_line_ = value; input->latch_line_ = value;
if (input->latch_line_) input->latched_state_ = input->current_state_; if (input->latch_line_) {
input->latched_state_ = input->current_state_;
}
} }
uint8_t input_read(Input* input) { uint8_t input_read(Input* input) {
if (input->latch_line_) input->latched_state_ = input->current_state_; if (input->latch_line_) input->latched_state_ = input->current_state_;
uint8_t ret = input->latched_state_ & 1; uint8_t ret = input->latched_state_ & 1;
input->latched_state_ >>= 1; input->latched_state_ >>= 1;
input->latched_state_ |= 0x8000; input->latched_state_ |= 0x8000;
return ret; return ret;
@@ -56,6 +59,8 @@ void Snes::Reset(bool hard) {
ResetDma(&memory_); ResetDma(&memory_);
input1.latch_line_ = false; input1.latch_line_ = false;
input2.latch_line_ = false; input2.latch_line_ = false;
input1.current_state_ = 0; // Clear current button states
input2.current_state_ = 0; // Clear current button states
input1.latched_state_ = 0; input1.latched_state_ = 0;
input2.latched_state_ = 0; input2.latched_state_ = 0;
if (hard) memset(ram, 0, sizeof(ram)); if (hard) memset(ram, 0, sizeof(ram));
@@ -392,7 +397,8 @@ uint8_t Snes::Rread(uint32_t adr) {
return ReadBBus(adr & 0xff); // B-bus return ReadBBus(adr & 0xff); // B-bus
} }
if (adr == 0x4016) { if (adr == 0x4016) {
return input_read(&input1) | (memory_.open_bus() & 0xfc); uint8_t result = input_read(&input1) | (memory_.open_bus() & 0xfc);
return result;
} }
if (adr == 0x4017) { if (adr == 0x4017) {
return input_read(&input2) | (memory_.open_bus() & 0xe0) | 0x1c; return input_read(&input2) | (memory_.open_bus() & 0xe0) | 0x1c;
@@ -634,7 +640,27 @@ void Snes::SetSamples(int16_t* sample_data, int wanted_samples) {
void Snes::SetPixels(uint8_t* pixel_data) { ppu_.PutPixels(pixel_data); } void Snes::SetPixels(uint8_t* pixel_data) { ppu_.PutPixels(pixel_data); }
void Snes::SetButtonState(int player, int button, bool pressed) {} void Snes::SetButtonState(int player, int button, bool pressed) {
// Select the appropriate input based on player number
Input* input = (player == 1) ? &input1 : &input2;
// SNES controller button mapping (standard layout)
// Bit 0: B, Bit 1: Y, Bit 2: Select, Bit 3: Start
// Bit 4: Up, Bit 5: Down, Bit 6: Left, Bit 7: Right
// Bit 8: A, Bit 9: X, Bit 10: L, Bit 11: R
if (button < 0 || button > 11) return; // Validate button range
uint16_t old_state = input->current_state_;
if (pressed) {
// Set the button bit
input->current_state_ |= (1 << button);
} else {
// Clear the button bit
input->current_state_ &= ~(1 << button);
}
}
void Snes::loadState(const std::string& path) { void Snes::loadState(const std::string& path) {
std::ifstream file(path, std::ios::binary); std::ifstream file(path, std::ios::binary);

View File

@@ -25,6 +25,10 @@ struct Input {
class Snes { class Snes {
public: public:
Snes() { Snes() {
// Initialize input controllers to clean state
input1 = {};
input2 = {};
cpu_.callbacks().read_byte = [this](uint32_t adr) { return CpuRead(adr); }; cpu_.callbacks().read_byte = [this](uint32_t adr) { return CpuRead(adr); };
cpu_.callbacks().write_byte = [this](uint32_t adr, uint8_t val) { CpuWrite(adr, val); }; cpu_.callbacks().write_byte = [this](uint32_t adr, uint8_t val) { CpuWrite(adr, val); };
cpu_.callbacks().idle = [this](bool waiting) { CpuIdle(waiting); }; cpu_.callbacks().idle = [this](bool waiting) { CpuIdle(waiting); };

View File

@@ -532,6 +532,17 @@ void RenderApuDebugger(Emulator* emu) {
} }
} }
ImGui::Separator();
ImGui::Text("Audio Resampling");
// Combo box for interpolation type
const char* items[] = {"Linear", "Cosine", "Cubic"};
int current_item = static_cast<int>(emu->snes().apu().dsp().interpolation_type);
if (ImGui::Combo("Interpolation", &current_item, items, IM_ARRAYSIZE(items))) {
emu->snes().apu().dsp().interpolation_type =
static_cast<InterpolationType>(current_item);
}
ImGui::EndChild(); ImGui::EndChild();
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }

View File

@@ -1,11 +1,14 @@
#include "app/emu/ui/emulator_ui.h" #include "app/emu/ui/emulator_ui.h"
#include <fstream>
#include "absl/strings/str_format.h" #include "absl/strings/str_format.h"
#include "app/emu/emulator.h" #include "app/emu/emulator.h"
#include "app/gui/color.h" #include "app/gui/color.h"
#include "app/gui/icons.h" #include "app/gui/icons.h"
#include "app/gui/theme_manager.h" #include "app/gui/theme_manager.h"
#include "imgui/imgui.h" #include "imgui/imgui.h"
#include "util/file_util.h"
#include "util/log.h" #include "util/log.h"
namespace yaze { namespace yaze {
@@ -41,6 +44,20 @@ void RenderNavBar(Emulator* emu) {
auto& theme_manager = ThemeManager::Get(); auto& theme_manager = ThemeManager::Get();
const auto& theme = theme_manager.GetCurrentTheme(); const auto& theme = theme_manager.GetCurrentTheme();
// Handle keyboard shortcuts for emulator control
// IMPORTANT: Use Shortcut() to avoid conflicts with game input
// Space - toggle play/pause (only when not typing in text fields)
if (ImGui::Shortcut(ImGuiKey_Space, ImGuiInputFlags_RouteGlobal)) {
emu->set_running(!emu->running());
}
// F10 - step one frame
if (ImGui::Shortcut(ImGuiKey_F10, ImGuiInputFlags_RouteGlobal)) {
if (!emu->running()) {
emu->snes().RunFrame();
}
}
// Navbar with theme colors // Navbar with theme colors
ImGui::PushStyleColor(ImGuiCol_Button, ConvertColorToImVec4(theme.button)); ImGui::PushStyleColor(ImGuiCol_Button, ConvertColorToImVec4(theme.button));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ConvertColorToImVec4(theme.button_hovered)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ConvertColorToImVec4(theme.button_hovered));
@@ -87,6 +104,50 @@ void RenderNavBar(Emulator* emu) {
ImGui::SetTooltip("Reset SNES (Ctrl+R)"); ImGui::SetTooltip("Reset SNES (Ctrl+R)");
} }
ImGui::SameLine();
// Load ROM button
if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load ROM", ImVec2(110, kButtonHeight))) {
std::string rom_path = util::FileDialogWrapper::ShowOpenFileDialog();
if (!rom_path.empty()) {
// Check if it's a valid ROM file extension
std::string ext = util::GetFileExtension(rom_path);
if (ext == ".sfc" || ext == ".smc" || ext == ".SFC" || ext == ".SMC") {
try {
// Read ROM file into memory
std::ifstream rom_file(rom_path, std::ios::binary);
if (rom_file.good()) {
std::vector<uint8_t> rom_data(
(std::istreambuf_iterator<char>(rom_file)),
std::istreambuf_iterator<char>()
);
rom_file.close();
// Reinitialize emulator with new ROM
if (!rom_data.empty()) {
emu->Initialize(emu->renderer(), rom_data);
LOG_INFO("Emulator", "Loaded ROM: %s (%zu bytes)",
util::GetFileName(rom_path).c_str(), rom_data.size());
} else {
LOG_ERROR("Emulator", "ROM file is empty: %s", rom_path.c_str());
}
} else {
LOG_ERROR("Emulator", "Failed to open ROM file: %s", rom_path.c_str());
}
} catch (const std::exception& e) {
LOG_ERROR("Emulator", "Error loading ROM: %s", e.what());
}
} else {
LOG_WARN("Emulator", "Invalid ROM file extension: %s (expected .sfc or .smc)",
ext.c_str());
}
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Load a different ROM file\n"
"Allows testing hacks with assembly patches applied");
}
ImGui::SameLine(); ImGui::SameLine();
ImGui::Separator(); ImGui::Separator();
ImGui::SameLine(); ImGui::SameLine();
@@ -115,12 +176,12 @@ void RenderNavBar(Emulator* emu) {
ImGui::SameLine(); ImGui::SameLine();
// Turbo mode // Turbo mode
bool turbo = false; // Need to expose this from Emulator bool turbo = emu->is_turbo_mode();
if (ImGui::Checkbox(ICON_MD_FAST_FORWARD " Turbo", &turbo)) { if (ImGui::Checkbox(ICON_MD_FAST_FORWARD " Turbo", &turbo)) {
// emu->set_turbo_mode(turbo); emu->set_turbo_mode(turbo);
} }
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Fast forward (hold Tab)"); ImGui::SetTooltip("Fast forward (shortcut: hold Tab)");
} }
ImGui::SameLine(); ImGui::SameLine();
@@ -163,6 +224,28 @@ void RenderNavBar(Emulator* emu) {
ICON_MD_VOLUME_OFF " No Backend"); ICON_MD_VOLUME_OFF " No Backend");
} }
ImGui::SameLine();
ImGui::Separator();
ImGui::SameLine();
// Input capture status indicator (like modern emulators)
ImGuiIO& io = ImGui::GetIO();
if (io.WantCaptureKeyboard) {
// ImGui is capturing keyboard (typing in UI)
ImGui::TextColored(ConvertColorToImVec4(theme.warning),
ICON_MD_KEYBOARD " UI");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Keyboard captured by UI\nGame input disabled");
}
} else {
// Emulator can receive input
ImGui::TextColored(ConvertColorToImVec4(theme.success),
ICON_MD_SPORTS_ESPORTS " Game");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Game input active\nPress F1 for controls");
}
}
ImGui::PopStyleColor(3); ImGui::PopStyleColor(3);
} }
@@ -180,7 +263,7 @@ void RenderSnesPpu(Emulator* emu) {
ImVec2 snes_size = ImVec2(512, 480); ImVec2 snes_size = ImVec2(512, 480);
if (emu->is_snes_initialized() && emu->ppu_texture()) { if (emu->is_snes_initialized() && emu->ppu_texture()) {
// Center the SNES display // Center the SNES display with aspect ratio preservation
float aspect = snes_size.x / snes_size.y; float aspect = snes_size.x / snes_size.y;
float display_w = canvas_size.x; float display_w = canvas_size.x;
float display_h = display_w / aspect; float display_h = display_w / aspect;
@@ -195,17 +278,37 @@ void RenderSnesPpu(Emulator* emu) {
ImGui::SetCursorPos(ImVec2(pos_x, pos_y)); ImGui::SetCursorPos(ImVec2(pos_x, pos_y));
// Render PPU texture // Render PPU texture with click detection for focus
ImGui::Image((ImTextureID)(intptr_t)emu->ppu_texture(), ImGui::Image((ImTextureID)(intptr_t)emu->ppu_texture(),
ImVec2(display_w, display_h), ImVec2(display_w, display_h),
ImVec2(0, 0), ImVec2(1, 1)); ImVec2(0, 0), ImVec2(1, 1));
// Allow clicking on the display to ensure focus
// Modern emulators make the game area "sticky" for input
if (ImGui::IsItemHovered()) {
// ImGui::SetTooltip("Click to ensure game input focus");
// Visual feedback when hovered (subtle border)
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 screen_pos = ImGui::GetItemRectMin();
ImVec2 screen_size = ImGui::GetItemRectMax();
draw_list->AddRect(screen_pos, screen_size,
ImGui::ColorConvertFloat4ToU32(ConvertColorToImVec4(theme.accent)),
0.0f, 0, 2.0f);
}
} else { } else {
// Not initialized - show placeholder // Not initialized - show helpful placeholder
ImVec2 text_size = ImGui::CalcTextSize("SNES PPU Output\n512x480"); ImVec2 text_size = ImGui::CalcTextSize("Load a ROM to start emulation");
ImGui::SetCursorPos(ImVec2((canvas_size.x - text_size.x) * 0.5f, ImGui::SetCursorPos(ImVec2((canvas_size.x - text_size.x) * 0.5f,
(canvas_size.y - text_size.y) * 0.5f)); (canvas_size.y - text_size.y) * 0.5f - 20));
ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
ICON_MD_VIDEOGAME_ASSET "\nSNES PPU Output\n512x480"); ICON_MD_VIDEOGAME_ASSET);
ImGui::SetCursorPosX((canvas_size.x - text_size.x) * 0.5f);
ImGui::TextColored(ConvertColorToImVec4(theme.text_primary),
"Load a ROM to start emulation");
ImGui::SetCursorPosX((canvas_size.x - ImGui::CalcTextSize("512x480 SNES output").x) * 0.5f);
ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled),
"512x480 SNES output");
} }
ImGui::EndChild(); ImGui::EndChild();
@@ -257,6 +360,111 @@ void RenderPerformanceMonitor(Emulator* emu) {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
void RenderKeyboardShortcuts(bool* show) {
if (!show || !*show) return;
auto& theme_manager = ThemeManager::Get();
const auto& theme = theme_manager.GetCurrentTheme();
// Center the window
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(550, 600), ImGuiCond_Appearing);
ImGui::PushStyleColor(ImGuiCol_TitleBg, ConvertColorToImVec4(theme.accent));
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ConvertColorToImVec4(theme.accent));
if (ImGui::Begin(ICON_MD_KEYBOARD " Keyboard Shortcuts", show,
ImGuiWindowFlags_NoCollapse)) {
// Emulator controls section
ImGui::TextColored(ConvertColorToImVec4(theme.accent),
ICON_MD_VIDEOGAME_ASSET " Emulator Controls");
ImGui::Separator();
ImGui::Spacing();
if (ImGui::BeginTable("EmulatorControls", 2, ImGuiTableFlags_Borders)) {
ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthFixed, 120);
ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
auto AddRow = [](const char* key, const char* action) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%s", key);
ImGui::TableNextColumn();
ImGui::Text("%s", action);
};
AddRow("Space", "Play/Pause emulation");
AddRow("F10", "Step one frame");
AddRow("Ctrl+R", "Reset SNES");
AddRow("Tab (hold)", "Turbo mode (fast forward)");
AddRow("F1", "Show/hide this help");
ImGui::EndTable();
}
ImGui::Spacing();
ImGui::Spacing();
// Game controls section
ImGui::TextColored(ConvertColorToImVec4(theme.accent),
ICON_MD_SPORTS_ESPORTS " SNES Controller");
ImGui::Separator();
ImGui::Spacing();
if (ImGui::BeginTable("GameControls", 2, ImGuiTableFlags_Borders)) {
ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthFixed, 120);
ImGui::TableSetupColumn("Button", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
auto AddRow = [](const char* key, const char* button) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%s", key);
ImGui::TableNextColumn();
ImGui::Text("%s", button);
};
AddRow("Arrow Keys", "D-Pad (Up/Down/Left/Right)");
AddRow("X", "A Button");
AddRow("Z", "B Button");
AddRow("S", "X Button");
AddRow("A", "Y Button");
AddRow("D", "L Shoulder");
AddRow("C", "R Shoulder");
AddRow("Enter", "Start");
AddRow("RShift", "Select");
ImGui::EndTable();
}
ImGui::Spacing();
ImGui::Spacing();
// Tips section
ImGui::TextColored(ConvertColorToImVec4(theme.info),
ICON_MD_INFO " Tips");
ImGui::Separator();
ImGui::Spacing();
ImGui::BulletText("Input is disabled when typing in UI fields");
ImGui::BulletText("Check the status bar for input capture state");
ImGui::BulletText("Click the game screen to ensure focus");
ImGui::BulletText("The emulator continues running in background");
ImGui::Spacing();
ImGui::Spacing();
if (ImGui::Button("Close", ImVec2(-1, 30))) {
*show = false;
}
}
ImGui::End();
ImGui::PopStyleColor(2);
}
void RenderEmulatorInterface(Emulator* emu) { void RenderEmulatorInterface(Emulator* emu) {
if (!emu) return; if (!emu) return;
@@ -273,6 +481,13 @@ void RenderEmulatorInterface(Emulator* emu) {
// Main content area // Main content area
RenderSnesPpu(emu); RenderSnesPpu(emu);
// Keyboard shortcuts overlay (F1 to toggle)
static bool show_shortcuts = false;
if (ImGui::IsKeyPressed(ImGuiKey_F1)) {
show_shortcuts = !show_shortcuts;
}
RenderKeyboardShortcuts(&show_shortcuts);
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }

View File

@@ -32,6 +32,11 @@ void RenderSnesPpu(Emulator* emu);
*/ */
void RenderPerformanceMonitor(Emulator* emu); void RenderPerformanceMonitor(Emulator* emu);
/**
* @brief Keyboard shortcuts help overlay (F1 in modern emulators)
*/
void RenderKeyboardShortcuts(bool* show);
} // namespace ui } // namespace ui
} // namespace emu } // namespace emu
} // namespace yaze } // namespace yaze