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 <cmath>
#include <cstring>
namespace yaze {
@@ -616,6 +617,37 @@ void Dsp::Write(uint8_t adr, uint8_t 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,
bool pal_timing) {
// Resample from native samples-per-frame (NTSC: ~534, PAL: ~641)
@@ -625,27 +657,75 @@ void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame,
double location = static_cast<double>((lastFrameBoundary + 0x400) & 0x3ff);
location -= native_per_frame;
// Use linear interpolation for smoother resampling
for (int i = 0; i < samples_per_frame; i++) {
const int idx = static_cast<int>(location) & 0x3ff;
const int next_idx = (idx + 1) & 0x3ff;
// Calculate interpolation factor (0.0 to 1.0)
const double frac = location - static_cast<int>(location);
// Linear interpolation for left channel
const int16_t s0_l = sampleBuffer[(idx * 2) + 0];
const int16_t s1_l = sampleBuffer[(next_idx * 2) + 0];
sample_data[(i * 2) + 0] = static_cast<int16_t>(
s0_l + frac * (s1_l - s0_l));
// Linear interpolation for right channel
const int16_t s0_r = sampleBuffer[(idx * 2) + 1];
const int16_t s1_r = sampleBuffer[(next_idx * 2) + 1];
sample_data[(i * 2) + 1] = static_cast<int16_t>(
s0_r + frac * (s1_r - s0_r));
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 next_idx = (idx + 1) & 0x3ff;
// Calculate interpolation factor (0.0 to 1.0)
const double frac = location - static_cast<int>(location);
// Linear interpolation for left channel
const int16_t s0_l = sampleBuffer[(idx * 2) + 0];
const int16_t s1_l = sampleBuffer[(next_idx * 2) + 0];
sample_data[(i * 2) + 0] = static_cast<int16_t>(
s0_l + frac * (s1_l - s0_l));
// Linear interpolation for right channel
const int16_t s0_r = sampleBuffer[(idx * 2) + 1];
const int16_t s1_r = sampleBuffer[(next_idx * 2) + 1];
sample_data[(i * 2) + 1] = static_cast<int16_t>(
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;
}
}

View File

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

View File

@@ -62,7 +62,7 @@ class Emulator {
void* ppu_texture() { return ppu_texture_; }
// 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; }
// Debugger access

View File

@@ -1,6 +1,7 @@
#include "app/emu/input/input_backend.h"
#include "SDL.h"
#include "imgui/imgui.h"
#include "util/log.h"
namespace yaze {
@@ -53,13 +54,29 @@ class SDL2InputBackend : public IInputBackend {
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);
// 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
state.SetButton(SnesButton::B, keyboard_state[SDL_GetScancodeFromKey(config_.key_b)]);
state.SetButton(SnesButton::Y, keyboard_state[SDL_GetScancodeFromKey(config_.key_y)]);
@@ -77,10 +94,10 @@ class SDL2InputBackend : public IInputBackend {
// Event-based mode (use cached event state)
state = event_state_;
}
// TODO: Add gamepad support
// if (config_.enable_gamepad) { ... }
return state;
}

View File

@@ -15,8 +15,8 @@ bool InputManager::Initialize(InputBackendFactory::BackendType type) {
}
InputConfig config;
config.continuous_polling = true; // Always use continuous polling for games
config.enable_gamepad = false; // TODO: Enable when gamepad support added
config.continuous_polling = true;
config.enable_gamepad = false;
if (!backend_->Initialize(config)) {
LOG_ERROR("InputManager", "Failed to initialize input backend");
@@ -47,13 +47,16 @@ void InputManager::Shutdown() {
void InputManager::Poll(Snes* snes, int player) {
if (!snes || !backend_) return;
// Poll backend for current controller state
ControllerState state = backend_->Poll(player);
ControllerState physical_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
// Combine physical input with agent-controlled input (OR operation)
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++) {
bool pressed = (state.buttons & (1 << i)) != 0;
bool pressed = (final_state.buttons & (1 << i)) != 0;
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 emu
} // namespace yaze
} // namespace yaze

View File

@@ -12,72 +12,36 @@ 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);
// --- Agent Control API ---
void PressButton(SnesButton button);
void ReleaseButton(SnesButton button);
private:
std::unique_ptr<IInputBackend> backend_;
ControllerState agent_controller_state_; // State controlled by agent
};
} // namespace input
} // namespace emu
} // 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 {
void input_latch(Input* input, bool 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) {
if (input->latch_line_) input->latched_state_ = input->current_state_;
uint8_t ret = input->latched_state_ & 1;
input->latched_state_ >>= 1;
input->latched_state_ |= 0x8000;
return ret;
@@ -56,6 +59,8 @@ void Snes::Reset(bool hard) {
ResetDma(&memory_);
input1.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;
input2.latched_state_ = 0;
if (hard) memset(ram, 0, sizeof(ram));
@@ -392,7 +397,8 @@ uint8_t Snes::Rread(uint32_t adr) {
return ReadBBus(adr & 0xff); // B-bus
}
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) {
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::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) {
std::ifstream file(path, std::ios::binary);

View File

@@ -25,6 +25,10 @@ struct Input {
class Snes {
public:
Snes() {
// Initialize input controllers to clean state
input1 = {};
input2 = {};
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().idle = [this](bool waiting) { CpuIdle(waiting); };

View File

@@ -531,6 +531,17 @@ void RenderApuDebugger(Emulator* emu) {
LOG_INFO("APU_DEBUG", "Port history cleared");
}
}
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::PopStyleColor();

View File

@@ -1,11 +1,14 @@
#include "app/emu/ui/emulator_ui.h"
#include <fstream>
#include "absl/strings/str_format.h"
#include "app/emu/emulator.h"
#include "app/gui/color.h"
#include "app/gui/icons.h"
#include "app/gui/theme_manager.h"
#include "imgui/imgui.h"
#include "util/file_util.h"
#include "util/log.h"
namespace yaze {
@@ -41,6 +44,20 @@ void RenderNavBar(Emulator* emu) {
auto& theme_manager = ThemeManager::Get();
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
ImGui::PushStyleColor(ImGuiCol_Button, ConvertColorToImVec4(theme.button));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ConvertColorToImVec4(theme.button_hovered));
@@ -86,7 +103,51 @@ void RenderNavBar(Emulator* emu) {
if (ImGui::IsItemHovered()) {
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::Separator();
ImGui::SameLine();
@@ -115,12 +176,12 @@ void RenderNavBar(Emulator* emu) {
ImGui::SameLine();
// 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)) {
// emu->set_turbo_mode(turbo);
emu->set_turbo_mode(turbo);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Fast forward (hold Tab)");
ImGui::SetTooltip("Fast forward (shortcut: hold Tab)");
}
ImGui::SameLine();
@@ -141,17 +202,17 @@ void RenderNavBar(Emulator* emu) {
ImGui::TextColored(fps_color, ICON_MD_SPEED " %.1f FPS", fps);
ImGui::SameLine();
// Audio backend status
if (emu->audio_backend()) {
auto audio_status = emu->audio_backend()->GetStatus();
ImVec4 audio_color = audio_status.is_playing ?
ImVec4 audio_color = audio_status.is_playing ?
ConvertColorToImVec4(theme.success) : ConvertColorToImVec4(theme.text_disabled);
ImGui::TextColored(audio_color, ICON_MD_VOLUME_UP " %s | %u frames",
emu->audio_backend()->GetBackendName().c_str(),
audio_status.queued_frames);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Audio Backend: %s\nQueued: %u frames\nPlaying: %s",
emu->audio_backend()->GetBackendName().c_str(),
@@ -159,55 +220,97 @@ void RenderNavBar(Emulator* emu) {
audio_status.is_playing ? "YES" : "NO");
}
} else {
ImGui::TextColored(ConvertColorToImVec4(theme.error),
ImGui::TextColored(ConvertColorToImVec4(theme.error),
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);
}
void RenderSnesPpu(Emulator* emu) {
if (!emu) return;
auto& theme_manager = ThemeManager::Get();
const auto& theme = theme_manager.GetCurrentTheme();
ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.editor_background));
ImGui::BeginChild("##SNES_PPU", ImVec2(0, 0), true,
ImGui::BeginChild("##SNES_PPU", ImVec2(0, 0), true,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImVec2 canvas_size = ImGui::GetContentRegionAvail();
ImVec2 snes_size = ImVec2(512, 480);
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 display_w = canvas_size.x;
float display_h = display_w / aspect;
if (display_h > canvas_size.y) {
display_h = canvas_size.y;
display_w = display_h * aspect;
}
float pos_x = (canvas_size.x - display_w) * 0.5f;
float pos_y = (canvas_size.y - display_h) * 0.5f;
ImGui::SetCursorPos(ImVec2(pos_x, pos_y));
// Render PPU texture
ImGui::Image((ImTextureID)(intptr_t)emu->ppu_texture(),
ImVec2(display_w, display_h),
// Render PPU texture with click detection for focus
ImGui::Image((ImTextureID)(intptr_t)emu->ppu_texture(),
ImVec2(display_w, display_h),
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 {
// Not initialized - show placeholder
ImVec2 text_size = ImGui::CalcTextSize("SNES PPU Output\n512x480");
// Not initialized - show helpful placeholder
ImVec2 text_size = ImGui::CalcTextSize("Load a ROM to start emulation");
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),
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::PopStyleColor();
}
@@ -257,22 +360,134 @@ void RenderPerformanceMonitor(Emulator* emu) {
ImGui::PopStyleColor();
}
void RenderEmulatorInterface(Emulator* emu) {
if (!emu) return;
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) {
if (!emu) return;
auto& theme_manager = ThemeManager::Get();
const auto& theme = theme_manager.GetCurrentTheme();
// Main layout
ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.window_bg));
RenderNavBar(emu);
ImGui::Separator();
// Main content area
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();
}

View File

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