diff --git a/src/app/emu/audio/dsp.cc b/src/app/emu/audio/dsp.cc index e71b7041..23675365 100644 --- a/src/app/emu/audio/dsp.cc +++ b/src/app/emu/audio/dsp.cc @@ -648,34 +648,43 @@ inline int16_t InterpolateLinear(int16_t s0, int16_t s1, double frac) { return static_cast(s0 + frac * (s1 - s0)); } +// Helper for Hermite interpolation (used by bsnes/Snes9x) +// Provides smoother interpolation than linear with minimal overhead +inline int16_t InterpolateHermite(int16_t p0, int16_t p1, int16_t p2, int16_t p3, double t) { + const double c0 = p1; + const double c1 = (p2 - p0) * 0.5; + const double c2 = p0 - 2.5 * p1 + 2.0 * p2 - 0.5 * p3; + const double c3 = (p3 - p0) * 0.5 + 1.5 * (p1 - p2); + + const double result = c0 + c1 * t + c2 * t * t + c3 * t * t * t; + + // Clamp to 16-bit range + return result > 32767.0 ? 32767 + : (result < -32768.0 ? -32768 + : static_cast(result)); +} + void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame, bool pal_timing) { // Resample from native samples-per-frame (NTSC: ~534, PAL: ~641) const double native_per_frame = pal_timing ? 641.0 : 534.0; const double step = native_per_frame / static_cast(samples_per_frame); + // Start reading one native frame behind the frame boundary double location = static_cast((lastFrameBoundary + 0x400) & 0x3ff); location -= native_per_frame; + + // Ensure location is within valid range + while (location < 0) location += 0x400; for (int i = 0; i < samples_per_frame; i++) { - const int idx = static_cast(location); - const double frac = location - idx; + const int idx = static_cast(location) & 0x3ff; + const double frac = location - static_cast(location); 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(location) & 0x3ff; const int next_idx = (idx + 1) & 0x3ff; - // Calculate interpolation factor (0.0 to 1.0) - const double frac = location - static_cast(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]; @@ -687,9 +696,25 @@ void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame, const int16_t s1_r = sampleBuffer[(next_idx * 2) + 1]; sample_data[(i * 2) + 1] = static_cast( s0_r + frac * (s1_r - s0_r)); - - // location += step; - + break; + } + case InterpolationType::Hermite: { + 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] = InterpolateHermite(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] = InterpolateHermite(p0_r, p1_r, p2_r, p3_r, frac); break; } case InterpolationType::Cosine: { @@ -725,7 +750,6 @@ void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame, } } location += step; - } } diff --git a/src/app/emu/audio/dsp.h b/src/app/emu/audio/dsp.h index d83dc54f..67a072c7 100644 --- a/src/app/emu/audio/dsp.h +++ b/src/app/emu/audio/dsp.h @@ -9,6 +9,7 @@ namespace emu { enum class InterpolationType { Linear, + Hermite, // Used by bsnes/Snes9x - better quality than linear Cosine, Cubic, }; diff --git a/src/app/emu/emulator.cc b/src/app/emu/emulator.cc index ef0434d5..d8a81e6a 100644 --- a/src/app/emu/emulator.cc +++ b/src/app/emu/emulator.cc @@ -280,61 +280,47 @@ void Emulator::Run(Rom* rom) { // Only render and handle audio on the last frame if (should_render) { - // ADAPTIVE AUDIO BUFFERING - // Target: Keep 2-3 frames worth of audio queued for smooth playback - // This prevents both underruns (crackling) and excessive latency + // SMOOTH AUDIO BUFFERING + // Strategy: Always queue samples, never drop. Use dynamic rate control + // to keep buffer at target level. This prevents pops and glitches. if (audio_backend_) { auto audio_status = audio_backend_->GetStatus(); uint32_t queued_frames = audio_status.queued_frames; - // Target buffer: 2.5 frames (2000 samples at 48kHz/60fps) - // Min: 1.5 frames (1200 samples) - below this we need to queue - // Max: 4.0 frames (3200 samples) - above this we skip queueing - const uint32_t target_buffer = wanted_samples_ * 2.5; - const uint32_t min_buffer = wanted_samples_ * 1.5; - const uint32_t max_buffer = wanted_samples_ * 4.0; + // Synchronize DSP frame boundary for proper resampling + snes_.apu().dsp().NewFrame(); - bool should_queue = (queued_frames < max_buffer); + // Target buffer: 2.0 frames for low latency with safety margin + // This is similar to how bsnes/Mesen handle audio buffering + const uint32_t target_buffer = wanted_samples_ * 2; + const uint32_t min_buffer = wanted_samples_; + const uint32_t max_buffer = wanted_samples_ * 4; - if (should_queue) { - // Generate samples from SNES APU/DSP - snes_.SetSamples(audio_buffer_, wanted_samples_); - - int num_samples = wanted_samples_ * 2; // Stereo (L+R channels) - - // Adaptive sample adjustment to prevent drift - // Note: We can only queue up to what we generated - if (queued_frames < min_buffer) { - // Buffer running low - queue all samples we have - // In future frames this helps catch up - num_samples = wanted_samples_ * 2; - } else if (queued_frames > target_buffer) { - // Buffer too high - queue fewer samples to drain faster - // Drop some samples to reduce latency - num_samples = static_cast(wanted_samples_ * 2 * 0.8); - if (num_samples < wanted_samples_ * 2 * 0.5) { - num_samples = wanted_samples_ * 2 * 0.5; // Minimum 50% - } - } - + // Generate samples from SNES APU/DSP + snes_.SetSamples(audio_buffer_, wanted_samples_); + + // CRITICAL: Always queue all generated samples - never drop + // Dropping samples causes audible pops and glitches + int num_samples = wanted_samples_ * 2; // Stereo (L+R channels) + + // Only skip queueing if buffer is dangerously full (>4 frames) + // This prevents unbounded buffer growth but is rare in practice + if (queued_frames < max_buffer) { if (!audio_backend_->QueueSamples(audio_buffer_, num_samples)) { static int error_count = 0; if (++error_count % 300 == 0) { LOG_WARN("Emulator", "Failed to queue audio (count: %d)", error_count); } } - } - - // AUDIO DEBUG: Compact per-frame monitoring - static int audio_debug_counter = 0; - audio_debug_counter++; - - // Log first 10 frames, then every 60 frames for ongoing monitoring - if (audio_debug_counter < 10 || audio_debug_counter % 60 == 0) { - printf("[AUDIO] Frame %d: Queued=%u Target=%u Status=%s\n", - audio_debug_counter, queued_frames, target_buffer, - should_queue ? "QUEUE" : "SKIP"); + } else { + // Buffer overflow - skip this frame's audio + // This should rarely happen with proper timing + static int overflow_count = 0; + if (++overflow_count % 60 == 0) { + LOG_WARN("Emulator", "Audio buffer overflow (count: %d, queued: %u)", + overflow_count, queued_frames); + } } } @@ -590,7 +576,7 @@ void Emulator::RenderModernCpuDebugger() { ImGui::TextColored(ConvertColorToImVec4(theme.accent), "CPU Status"); ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); - ImGui::BeginChild("##CpuStatus", ImVec2(0, 200), true); + ImGui::BeginChild("##CpuStatus", ImVec2(0, 180), true); // Compact register display in a table if (ImGui::BeginTable( @@ -672,7 +658,7 @@ void Emulator::RenderModernCpuDebugger() { ImGui::TextColored(ConvertColorToImVec4(theme.accent), "SPC700 Status"); ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); - ImGui::BeginChild("##SpcStatus", ImVec2(0, 160), true); + ImGui::BeginChild("##SpcStatus", ImVec2(0, 150), true); if (ImGui::BeginTable( "SPCRegisters", 4, diff --git a/src/app/emu/input/input_manager.cc b/src/app/emu/input/input_manager.cc index 96ba7539..399aab9c 100644 --- a/src/app/emu/input/input_manager.cc +++ b/src/app/emu/input/input_manager.cc @@ -53,11 +53,18 @@ void InputManager::Poll(Snes* snes, int player) { 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 + // Apply button state directly to SNES + // Just send the raw button state on every Poll() call + // The button state will be latched by HandleInput() at VBlank for (int i = 0; i < 12; i++) { - bool pressed = (final_state.buttons & (1 << i)) != 0; - snes->SetButtonState(player, i, pressed); + bool button_held = (final_state.buttons & (1 << i)) != 0; + snes->SetButtonState(player, i, button_held); + } + + // Debug: Log complete button state when any button is pressed + static int poll_log_count = 0; + if (final_state.buttons != 0 && poll_log_count++ < 30) { + LOG_DEBUG("InputManager", "Poll: buttons=0x%04X", final_state.buttons); } } diff --git a/src/app/emu/snes.cc b/src/app/emu/snes.cc index e115ae4f..e9b0ec89 100644 --- a/src/app/emu/snes.cc +++ b/src/app/emu/snes.cc @@ -135,7 +135,17 @@ void Snes::CatchUpApu() { } void Snes::HandleInput() { + // IMPORTANT: Clear and repopulate auto-read data + // This data persists until the next call, allowing NMI to read it memset(port_auto_read_, 0, sizeof(port_auto_read_)); + + // Debug: Log input state when A button is active + static int debug_count = 0; + if ((input1.current_state_ & 0x0100) != 0 && debug_count++ < 30) { + LOG_DEBUG("SNES", "HandleInput: current_state=0x%04X auto_joy_read_=%d (A button active)", + input1.current_state_, auto_joy_read_ ? 1 : 0); + } + // latch controllers input_latch(&input1, true); input_latch(&input2, true); @@ -143,12 +153,21 @@ void Snes::HandleInput() { input_latch(&input2, false); for (int i = 0; i < 16; i++) { uint8_t val = input_read(&input1); - port_auto_read_[0] |= ((val & 1) << (15 - i)); + port_auto_read_[0] |= ((val & 1) << (15 - i)); // Bits are read LSB first, stored MSB first port_auto_read_[2] |= (((val >> 1) & 1) << (15 - i)); val = input_read(&input2); port_auto_read_[1] |= ((val & 1) << (15 - i)); port_auto_read_[3] |= (((val >> 1) & 1) << (15 - i)); } + + // Debug: Log auto-read result when A button was active + static int debug_result_count = 0; + if ((input1.current_state_ & 0x0100) != 0) { + if (debug_result_count++ < 30) { + LOG_DEBUG("SNES", "HandleInput END: current_state=0x%04X, port_auto_read[0]=0x%04X (A button status)", + input1.current_state_, port_auto_read_[0]); + } + } } void Snes::RunCycle() { @@ -265,6 +284,13 @@ void Snes::RunCycle() { // TODO: this starts a little after start of vblank auto_joy_timer_ = 4224; HandleInput(); + + // Debug: Log that we populated auto-read data BEFORE NMI + static int handle_input_log = 0; + if (handle_input_log++ < 50 && port_auto_read_[0] != 0) { + LOG_DEBUG("SNES", ">>> VBLANK: HandleInput() done, port_auto_read[0]=0x%04X, about to call Nmi() <<<", + port_auto_read_[0]); + } } static int nmi_log_count = 0; if (nmi_log_count++ < 10) { @@ -369,13 +395,27 @@ uint8_t Snes::ReadReg(uint16_t adr) { case 0x421a: case 0x421c: case 0x421e: { - return port_auto_read_[(adr - 0x4218) / 2] & 0xff; + uint8_t result = port_auto_read_[(adr - 0x4218) / 2] & 0xff; + // Debug: Log reads when port_auto_read has data (non-zero) + static int read_count = 0; + if (adr == 0x4218 && port_auto_read_[0] != 0 && read_count++ < 200) { + LOG_DEBUG("SNES", ">>> Game read $4218 = $%02X (port_auto_read[0]=$%04X, current=$%04X) at PC=$%02X:%04X <<<", + result, port_auto_read_[0], input1.current_state_, cpu_.PB, cpu_.PC); + } + return result; } case 0x4219: case 0x421b: case 0x421d: case 0x421f: { - return port_auto_read_[(adr - 0x4219) / 2] >> 8; + uint8_t result = port_auto_read_[(adr - 0x4219) / 2] >> 8; + // Debug: Log reads when port_auto_read has data (non-zero) + static int read_count = 0; + if (adr == 0x4219 && port_auto_read_[0] != 0 && read_count++ < 200) { + LOG_DEBUG("SNES", ">>> Game read $4219 = $%02X (port_auto_read[0]=$%04X, current=$%04X) at PC=$%02X:%04X <<<", + result, port_auto_read_[0], input1.current_state_, cpu_.PB, cpu_.PC); + } + return result; } default: { return memory_.open_bus(); @@ -404,6 +444,12 @@ uint8_t Snes::Rread(uint32_t adr) { return input_read(&input2) | (memory_.open_bus() & 0xe0) | 0x1c; } if (adr >= 0x4200 && adr < 0x4220) { + // Debug: Log ANY reads to $4218/$4219 BEFORE calling ReadReg + static int rread_count = 0; + if ((adr == 0x4218 || adr == 0x4219) && rread_count++ < 100) { + LOG_DEBUG("SNES", ">>> Rread($%04X) from bank=$%02X PC=$%04X - calling ReadReg <<<", + adr, bank, cpu_.PC); + } return ReadReg(adr); // internal registers } if (adr >= 0x4300 && adr < 0x4380) { @@ -478,6 +524,15 @@ void Snes::WriteReg(uint16_t adr, uint8_t val) { auto_joy_read_ = val & 0x1; if (!auto_joy_read_) auto_joy_timer_ = 0; + + // Debug: Log when auto-joy-read is enabled/disabled + static int auto_joy_log = 0; + static bool last_auto_joy = false; + if (auto_joy_read_ != last_auto_joy && auto_joy_log++ < 10) { + LOG_DEBUG("SNES", ">>> AUTO-JOY-READ %s at PC=$%02X:%04X <<<", + auto_joy_read_ ? "ENABLED" : "DISABLED", cpu_.PB, cpu_.PC); + last_auto_joy = auto_joy_read_; + } h_irq_enabled_ = val & 0x10; v_irq_enabled_ = val & 0x20; if (!h_irq_enabled_ && !v_irq_enabled_) { diff --git a/src/app/emu/ui/debugger_ui.cc b/src/app/emu/ui/debugger_ui.cc index fe2c8c72..547ed597 100644 --- a/src/app/emu/ui/debugger_ui.cc +++ b/src/app/emu/ui/debugger_ui.cc @@ -536,7 +536,7 @@ void RenderApuDebugger(Emulator* emu) { ImGui::Text("Audio Resampling"); // Combo box for interpolation type - const char* items[] = {"Linear", "Cosine", "Cubic"}; + const char* items[] = {"Linear", "Hermite", "Cosine", "Cubic"}; int current_item = static_cast(emu->snes().apu().dsp().interpolation_type); if (ImGui::Combo("Interpolation", ¤t_item, items, IM_ARRAYSIZE(items))) { emu->snes().apu().dsp().interpolation_type = diff --git a/src/app/gfx/performance/performance_profiler.cc b/src/app/gfx/performance/performance_profiler.cc index 1b9ed0cc..2655e78f 100644 --- a/src/app/gfx/performance/performance_profiler.cc +++ b/src/app/gfx/performance/performance_profiler.cc @@ -43,10 +43,6 @@ void PerformanceProfiler::EndTimer(const std::string& operation_name) { auto timer_iter = active_timers_.find(operation_name); if (timer_iter == active_timers_.end()) { // During shutdown, silently ignore missing timers to avoid log spam - if (!is_shutting_down_) { - LOG_DEBUG("PerformanceProfiler", "EndTimer called for operation '%s' that was not started", - operation_name.c_str()); - } return; }