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:
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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);
|
||||
|
||||
@@ -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); };
|
||||
|
||||
@@ -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", ¤t_item, items, IM_ARRAYSIZE(items))) {
|
||||
emu->snes().apu().dsp().interpolation_type =
|
||||
static_cast<InterpolationType>(current_item);
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user