feat(emu): implement SDL audio stream support in emulator

- Added functionality to enable SDL audio streaming in the Emulator class, allowing for improved audio handling.
- Introduced environment variable checks to configure audio streaming on initialization.
- Updated audio backend to support native audio frame queuing and resampling, enhancing audio performance and flexibility.

Benefits:
- Enhances audio playback quality and responsiveness in the emulator.
- Provides users with the option to utilize SDL audio streaming for better audio management.
This commit is contained in:
scawful
2025-10-13 14:26:33 -04:00
parent cc427d037e
commit a582210fa8
8 changed files with 393 additions and 122 deletions

View File

@@ -4,6 +4,7 @@
#include <SDL.h>
#include <algorithm>
#include <vector>
#include "util/log.h"
namespace yaze {
@@ -42,6 +43,10 @@ bool SDL2AudioBackend::Initialize(const AudioConfig& config) {
return false;
}
device_format_ = have.format;
device_channels_ = have.channels;
device_freq_ = have.freq;
// Verify we got what we asked for
if (have.freq != want.freq || have.channels != want.channels) {
LOG_WARN("AudioBackend",
@@ -56,6 +61,13 @@ bool SDL2AudioBackend::Initialize(const AudioConfig& config) {
have.freq, have.channels, have.samples);
initialized_ = true;
audio_stream_enabled_ = false;
stream_native_rate_ = 0;
if (audio_stream_) {
SDL_FreeAudioStream(audio_stream_);
audio_stream_ = nullptr;
}
stream_buffer_.clear();
// Start playback immediately (unpause)
SDL_PauseAudioDevice(device_id_, 0);
@@ -66,6 +78,14 @@ bool SDL2AudioBackend::Initialize(const AudioConfig& config) {
void SDL2AudioBackend::Shutdown() {
if (!initialized_) return;
if (audio_stream_) {
SDL_FreeAudioStream(audio_stream_);
audio_stream_ = nullptr;
}
audio_stream_enabled_ = false;
stream_native_rate_ = 0;
stream_buffer_.clear();
if (device_id_ != 0) {
SDL_PauseAudioDevice(device_id_, 1);
SDL_CloseAudioDevice(device_id_);
@@ -95,6 +115,9 @@ void SDL2AudioBackend::Stop() {
void SDL2AudioBackend::Clear() {
if (!initialized_) return;
SDL_ClearQueuedAudio(device_id_);
if (audio_stream_) {
SDL_AudioStreamClear(audio_stream_);
}
}
bool SDL2AudioBackend::QueueSamples(const int16_t* samples, int num_samples) {
@@ -152,6 +175,56 @@ bool SDL2AudioBackend::QueueSamples(const float* samples, int num_samples) {
return QueueSamples(int_samples.data(), num_samples);
}
bool SDL2AudioBackend::QueueSamplesNative(const int16_t* samples,
int frames_per_channel, int channels,
int native_rate) {
if (!initialized_ || samples == nullptr) {
return false;
}
if (!audio_stream_enabled_ || audio_stream_ == nullptr) {
return false;
}
if (native_rate != stream_native_rate_ ||
channels != config_.channels) {
SetAudioStreamResampling(true, native_rate, channels);
if (audio_stream_ == nullptr) {
return false;
}
}
const int bytes_in =
frames_per_channel * channels * static_cast<int>(sizeof(int16_t));
if (SDL_AudioStreamPut(audio_stream_, samples, bytes_in) < 0) {
LOG_ERROR("AudioBackend", "SDL_AudioStreamPut failed: %s", SDL_GetError());
return false;
}
const int available_bytes = SDL_AudioStreamAvailable(audio_stream_);
if (available_bytes < 0) {
LOG_ERROR("AudioBackend", "SDL_AudioStreamAvailable failed: %s", SDL_GetError());
return false;
}
if (available_bytes == 0) {
return true;
}
const int available_samples = available_bytes / static_cast<int>(sizeof(int16_t));
if (static_cast<int>(stream_buffer_.size()) < available_samples) {
stream_buffer_.resize(available_samples);
}
if (SDL_AudioStreamGet(audio_stream_, stream_buffer_.data(), available_bytes) < 0) {
LOG_ERROR("AudioBackend", "SDL_AudioStreamGet failed: %s", SDL_GetError());
return false;
}
return QueueSamples(stream_buffer_.data(), available_samples);
}
AudioStatus SDL2AudioBackend::GetStatus() const {
AudioStatus status;
@@ -181,6 +254,51 @@ AudioConfig SDL2AudioBackend::GetConfig() const {
return config_;
}
void SDL2AudioBackend::SetAudioStreamResampling(bool enable, int native_rate,
int channels) {
if (!initialized_) return;
if (!enable) {
if (audio_stream_) {
SDL_FreeAudioStream(audio_stream_);
audio_stream_ = nullptr;
}
audio_stream_enabled_ = false;
stream_native_rate_ = 0;
stream_buffer_.clear();
return;
}
const bool needs_recreate =
(audio_stream_ == nullptr) || (stream_native_rate_ != native_rate) ||
(channels != config_.channels);
if (!needs_recreate) {
audio_stream_enabled_ = true;
return;
}
if (audio_stream_) {
SDL_FreeAudioStream(audio_stream_);
audio_stream_ = nullptr;
}
audio_stream_ = SDL_NewAudioStream(AUDIO_S16, channels, native_rate,
device_format_, device_channels_,
device_freq_);
if (!audio_stream_) {
LOG_ERROR("AudioBackend", "SDL_NewAudioStream failed: %s", SDL_GetError());
audio_stream_enabled_ = false;
stream_native_rate_ = 0;
return;
}
SDL_AudioStreamClear(audio_stream_);
audio_stream_enabled_ = true;
stream_native_rate_ = native_rate;
stream_buffer_.clear();
}
void SDL2AudioBackend::SetVolume(float volume) {
volume_ = std::clamp(volume, 0.0f, 1.0f);
}
@@ -212,4 +330,3 @@ std::unique_ptr<IAudioBackend> AudioBackendFactory::Create(BackendType type) {
} // namespace audio
} // namespace emu
} // namespace yaze

View File

@@ -4,9 +4,12 @@
#ifndef YAZE_APP_EMU_AUDIO_AUDIO_BACKEND_H
#define YAZE_APP_EMU_AUDIO_AUDIO_BACKEND_H
#include <SDL.h>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace yaze {
namespace emu {
@@ -57,6 +60,10 @@ class IAudioBackend {
// Audio data
virtual bool QueueSamples(const int16_t* samples, int num_samples) = 0;
virtual bool QueueSamples(const float* samples, int num_samples) = 0;
virtual bool QueueSamplesNative(const int16_t* samples, int frames_per_channel,
int channels, int native_rate) {
return false;
}
// Status queries
virtual AudioStatus GetStatus() const = 0;
@@ -67,6 +74,11 @@ class IAudioBackend {
virtual void SetVolume(float volume) = 0;
virtual float GetVolume() const = 0;
// Optional: enable/disable SDL_AudioStream-based resampling
virtual void SetAudioStreamResampling(bool enable, int native_rate,
int channels) {}
virtual bool SupportsAudioStream() const { return false; }
// Get backend name for debugging
virtual std::string GetBackendName() const = 0;
};
@@ -89,6 +101,8 @@ class SDL2AudioBackend : public IAudioBackend {
bool QueueSamples(const int16_t* samples, int num_samples) override;
bool QueueSamples(const float* samples, int num_samples) override;
bool QueueSamplesNative(const int16_t* samples, int frames_per_channel,
int channels, int native_rate) override;
AudioStatus GetStatus() const override;
bool IsInitialized() const override;
@@ -97,6 +111,10 @@ class SDL2AudioBackend : public IAudioBackend {
void SetVolume(float volume) override;
float GetVolume() const override;
void SetAudioStreamResampling(bool enable, int native_rate,
int channels) override;
bool SupportsAudioStream() const override { return true; }
std::string GetBackendName() const override { return "SDL2"; }
private:
@@ -104,6 +122,13 @@ class SDL2AudioBackend : public IAudioBackend {
AudioConfig config_;
bool initialized_ = false;
float volume_ = 1.0f;
SDL_AudioFormat device_format_ = AUDIO_S16;
int device_channels_ = 2;
int device_freq_ = 48000;
bool audio_stream_enabled_ = false;
int stream_native_rate_ = 0;
SDL_AudioStream* audio_stream_ = nullptr;
std::vector<int16_t> stream_buffer_;
};
/**
@@ -125,4 +150,3 @@ class AudioBackendFactory {
} // namespace yaze
#endif // YAZE_APP_EMU_AUDIO_AUDIO_BACKEND_H

View File

@@ -753,5 +753,25 @@ void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame,
}
}
int Dsp::CopyNativeFrame(int16_t* sample_data, bool pal_timing) {
if (sample_data == nullptr) {
return 0;
}
const int native_per_frame = pal_timing ? 641 : 534;
const int total_samples = native_per_frame * 2;
int start_index = static_cast<int>(
(lastFrameBoundary + 0x400 - native_per_frame) & 0x3ff);
for (int i = 0; i < native_per_frame; ++i) {
const int idx = (start_index + i) & 0x3ff;
sample_data[(i * 2) + 0] = sampleBuffer[(idx * 2) + 0];
sample_data[(i * 2) + 1] = sampleBuffer[(idx * 2) + 1];
}
return total_samples / 2; // return frames per channel
}
} // namespace emu
} // namespace yaze

View File

@@ -110,6 +110,7 @@ class Dsp {
int16_t GetSample(int ch);
void GetSamples(int16_t* sample_data, int samples_per_frame, bool pal_timing);
int CopyNativeFrame(int16_t* sample_data, bool pal_timing);
InterpolationType interpolation_type = InterpolationType::Linear;

View File

@@ -1,5 +1,6 @@
#include "app/emu/emulator.h"
#include <cstdlib>
#include <cstdint>
#include <fstream>
#include <vector>
@@ -24,6 +25,10 @@ namespace yaze::core {
namespace yaze {
namespace emu {
namespace {
constexpr int kNativeSampleRate = 32000;
}
Emulator::~Emulator() {
// Don't call Cleanup() in destructor - renderer is already destroyed
// Just stop emulation
@@ -42,12 +47,28 @@ void Emulator::Cleanup() {
// Reset state
snes_initialized_ = false;
audio_stream_active_ = false;
}
void Emulator::set_use_sdl_audio_stream(bool enabled) {
if (use_sdl_audio_stream_ != enabled) {
use_sdl_audio_stream_ = enabled;
audio_stream_config_dirty_ = true;
}
}
void Emulator::Initialize(gfx::IRenderer* renderer, const std::vector<uint8_t>& rom_data) {
// This method is now optional - emulator can be initialized lazily in Run()
renderer_ = renderer;
rom_data_ = rom_data;
if (!audio_stream_env_checked_) {
const char* env_value = std::getenv("YAZE_USE_SDL_AUDIO_STREAM");
if (env_value && std::atoi(env_value) != 0) {
set_use_sdl_audio_stream(true);
}
audio_stream_env_checked_ = true;
}
// Cards are registered in EditorManager::Initialize() to avoid duplication
@@ -73,6 +94,7 @@ void Emulator::Initialize(gfx::IRenderer* renderer, const std::vector<uint8_t>&
} else {
LOG_INFO("Emulator", "Audio backend initialized: %s",
audio_backend_->GetBackendName().c_str());
audio_stream_config_dirty_ = true;
}
}
@@ -93,6 +115,14 @@ void Emulator::Initialize(gfx::IRenderer* renderer, const std::vector<uint8_t>&
}
void Emulator::Run(Rom* rom) {
if (!audio_stream_env_checked_) {
const char* env_value = std::getenv("YAZE_USE_SDL_AUDIO_STREAM");
if (env_value && std::atoi(env_value) != 0) {
set_use_sdl_audio_stream(true);
}
audio_stream_env_checked_ = true;
}
// Lazy initialization: set renderer from Controller if not set yet
if (!renderer_) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
@@ -118,6 +148,7 @@ void Emulator::Run(Rom* rom) {
} else {
LOG_INFO("Emulator", "Audio backend initialized (lazy): %s",
audio_backend_->GetBackendName().c_str());
audio_stream_config_dirty_ = true;
}
}
@@ -245,41 +276,65 @@ void Emulator::Run(Rom* rom) {
// to keep buffer at target level. This prevents pops and glitches.
if (audio_backend_) {
if (audio_stream_config_dirty_) {
if (use_sdl_audio_stream_ && audio_backend_->SupportsAudioStream()) {
audio_backend_->SetAudioStreamResampling(true, kNativeSampleRate, 2);
audio_stream_active_ = true;
} else {
audio_backend_->SetAudioStreamResampling(false, kNativeSampleRate, 2);
audio_stream_active_ = false;
}
audio_stream_config_dirty_ = false;
}
const bool use_native_stream =
use_sdl_audio_stream_ && audio_stream_active_ &&
audio_backend_->SupportsAudioStream();
auto audio_status = audio_backend_->GetStatus();
uint32_t queued_frames = audio_status.queued_frames;
// Synchronize DSP frame boundary for proper resampling
// Synchronize DSP frame boundary for resampling
snes_.apu().dsp().NewFrame();
// 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;
// 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)) {
bool queue_ok = true;
if (use_native_stream) {
const int frames_native = snes_.apu().dsp().CopyNativeFrame(
audio_buffer_, snes_.memory().pal_timing());
queue_ok = audio_backend_->QueueSamplesNative(
audio_buffer_, frames_native, 2, kNativeSampleRate);
} else {
snes_.SetSamples(audio_buffer_, wanted_samples_);
const int num_samples = wanted_samples_ * 2; // Stereo
queue_ok = audio_backend_->QueueSamples(audio_buffer_, num_samples);
}
if (!queue_ok && use_native_stream) {
snes_.SetSamples(audio_buffer_, wanted_samples_);
const int num_samples = wanted_samples_ * 2;
queue_ok = audio_backend_->QueueSamples(audio_buffer_, num_samples);
}
if (!queue_ok) {
static int error_count = 0;
if (++error_count % 300 == 0) {
LOG_WARN("Emulator", "Failed to queue audio (count: %d)", error_count);
LOG_WARN("Emulator",
"Failed to queue audio (count: %d, stream=%s)",
error_count, use_native_stream ? "SDL" : "manual");
}
}
} 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);
LOG_WARN("Emulator",
"Audio buffer overflow (count: %d, queued: %u)",
overflow_count, queued_frames);
}
}
}

View File

@@ -50,6 +50,8 @@ class Emulator {
auto set_audio_device_id(SDL_AudioDeviceID audio_device) {
audio_device_ = audio_device;
}
void set_use_sdl_audio_stream(bool enabled);
bool use_sdl_audio_stream() const { return use_sdl_audio_stream_; }
auto wanted_samples() const -> int { return wanted_samples_; }
void set_renderer(gfx::IRenderer* renderer) { renderer_ = renderer; }
@@ -158,6 +160,10 @@ class Emulator {
bool debugging_ = false;
gfx::IRenderer* renderer_ = nullptr;
void* ppu_texture_ = nullptr;
bool use_sdl_audio_stream_ = false;
bool audio_stream_config_dirty_ = false;
bool audio_stream_active_ = false;
bool audio_stream_env_checked_ = false;
// Card visibility managed by EditorCardManager - no member variables needed!

View File

@@ -9,8 +9,10 @@
#include "app/emu/video/ppu.h"
#include "util/log.h"
#define WRITE_STATE(file, member) file.write(reinterpret_cast<const char*>(&member), sizeof(member))
#define READ_STATE(file, member) file.read(reinterpret_cast<char*>(&member), sizeof(member))
#define WRITE_STATE(file, member) \
file.write(reinterpret_cast<const char*>(&member), sizeof(member))
#define READ_STATE(file, member) \
file.read(reinterpret_cast<char*>(&member), sizeof(member))
namespace yaze {
namespace emu {
@@ -24,9 +26,10 @@ void input_latch(Input* input, bool value) {
}
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;
input->latched_state_ >>= 1;
input->latched_state_ |= 0x8000;
return ret;
@@ -34,12 +37,13 @@ uint8_t input_read(Input* input) {
} // namespace
void Snes::Init(std::vector<uint8_t>& rom_data) {
LOG_DEBUG("SNES", "Initializing emulator with ROM size %zu bytes", rom_data.size());
LOG_DEBUG("SNES", "Initializing emulator with ROM size %zu bytes",
rom_data.size());
// Initialize the CPU, PPU, and APU
ppu_.Init();
apu_.Init();
// Connect handshake tracker to APU for debugging
apu_.set_handshake_tracker(&apu_handshake_tracker_);
@@ -59,11 +63,12 @@ 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.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));
if (hard)
memset(ram, 0, sizeof(ram));
ram_adr_ = 0;
memory_.set_h_pos(0);
memory_.set_v_pos(0);
@@ -92,37 +97,40 @@ void Snes::Reset(bool hard) {
memory_.set_open_bus(0);
next_horiz_event = 16;
InitAccessTime(false);
LOG_DEBUG("SNES", "Reset complete - CPU will start at $%02X:%04X", cpu_.PB, cpu_.PC);
LOG_DEBUG("SNES", "Reset complete - CPU will start at $%02X:%04X", cpu_.PB,
cpu_.PC);
}
void Snes::RunFrame() {
// Debug: Log every 60th frame
static int frame_log_count = 0;
if (frame_log_count % 60 == 0) {
LOG_DEBUG("SNES", "Frame %d: CPU=$%02X:%04X vblank=%d frames_=%d",
frame_log_count, cpu_.PB, cpu_.PC, in_vblank_, frames_);
LOG_DEBUG("SNES", "Frame %d: CPU=$%02X:%04X vblank=%d frames_=%d",
frame_log_count, cpu_.PB, cpu_.PC, in_vblank_, frames_);
}
frame_log_count++;
// Debug: Log vblank loop entry
static int vblank_loop_count = 0;
if (in_vblank_ && vblank_loop_count++ < 10) {
LOG_DEBUG("SNES", "RunFrame: Entering vblank loop (in_vblank_=true)");
}
while (in_vblank_) {
cpu_.RunOpcode();
}
uint32_t frame = frames_;
// Debug: Log active frame loop entry
// Debug: Log active frame loop entry
static int active_loop_count = 0;
if (!in_vblank_ && active_loop_count++ < 10) {
LOG_DEBUG("SNES", "RunFrame: Entering active frame loop (in_vblank_=false, frame=%d, frames_=%d)",
frame, frames_);
LOG_DEBUG("SNES",
"RunFrame: Entering active frame loop (in_vblank_=false, "
"frame=%d, frames_=%d)",
frame, frames_);
}
while (!in_vblank_ && frame == frames_) {
cpu_.RunOpcode();
}
@@ -138,45 +146,38 @@ 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);
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);
input_latch(&input1, false);
input_latch(&input2, false);
// Read 16 bits serially for both controllers, LSB-first, packing bits as BYSTudlr
port_auto_read_[0] = 0; // input1 low 8 bits (B, Y, Select, Start, Up, Down, Left, Right)
port_auto_read_[2] = 0; // input1 high 8 bits (A, X, L, R, unused, unused, unused, unused)
port_auto_read_[1] = 0; // input2 low 8 bits
port_auto_read_[3] = 0; // input2 high 8 bits
for (int i = 0; i < 16; i++) {
uint8_t val1 = input_read(&input1);
uint8_t val2 = input_read(&input2);
if (i < 8) {
port_auto_read_[0] |= ((val1 & 1) << i); // input1 low (BYSTudlr pattern)
port_auto_read_[1] |= ((val2 & 1) << i); // input2 low
port_auto_read_[2] |= (((val1 >> 1) & 1) << i); // input1 high (A, X, L, R...)
port_auto_read_[3] |= (((val2 >> 1) & 1) << i); // input2 high
}
// Optional: for i >= 8, you can skip; 16-bits are clocked out, but SNES only expects the first 16
// If needed for other pads or 4 player, adapt here
uint8_t val = input_read(&input1);
port_auto_read_[0] |= ((val & 1) << (15 - i));
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]);
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]);
}
}
}
@@ -203,15 +204,18 @@ void Snes::RunCycle() {
switch (memory_.h_pos()) {
case 16: {
next_horiz_event = 512;
if (memory_.v_pos() == 0) memory_.init_hdma_request();
if (memory_.v_pos() == 0)
memory_.init_hdma_request();
} break;
case 512: {
next_horiz_event = 1104;
// render the line halfway of the screen for better compatibility
if (!in_vblank_ && memory_.v_pos() > 0) ppu_.RunLine(memory_.v_pos());
if (!in_vblank_ && memory_.v_pos() > 0)
ppu_.RunLine(memory_.v_pos());
} break;
case 1104: {
if (!in_vblank_) memory_.run_hdma_request();
if (!in_vblank_)
memory_.run_hdma_request();
if (!memory_.pal_timing()) {
// line 240 of odd frame with no interlace is 4 cycles shorter
next_horiz_event = (memory_.v_pos() == 240 && !ppu_.even_frame &&
@@ -257,7 +261,10 @@ void Snes::RunCycle() {
// end of vblank
static int vblank_end_count = 0;
if (vblank_end_count++ < 10) {
LOG_DEBUG("SNES", "VBlank END - v_pos=0, setting in_vblank_=false at frame %d", frames_);
LOG_DEBUG(
"SNES",
"VBlank END - v_pos=0, setting in_vblank_=false at frame %d",
frames_);
}
in_vblank_ = false;
in_nmi_ = false;
@@ -269,7 +276,8 @@ void Snes::RunCycle() {
} else if (memory_.v_pos() == 240) {
// if we are not yet in vblank, we had an overscan frame, set
// starting_vblank
if (!in_vblank_) starting_vblank = true;
if (!in_vblank_)
starting_vblank = true;
}
if (starting_vblank) {
// catch up the apu at end of emulated frame (we end frame @ start of
@@ -282,31 +290,36 @@ void Snes::RunCycle() {
apu_.dsp().NewFrame();
// we are starting vblank
ppu_.HandleVblank();
static int vblank_start_count = 0;
if (vblank_start_count++ < 10) {
LOG_DEBUG("SNES", "VBlank START - v_pos=%d, setting in_vblank_=true at frame %d",
memory_.v_pos(), frames_);
LOG_DEBUG(
"SNES",
"VBlank START - v_pos=%d, setting in_vblank_=true at frame %d",
memory_.v_pos(), frames_);
}
in_vblank_ = true;
in_nmi_ = true;
if (auto_joy_read_) {
// 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]);
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) {
LOG_DEBUG("SNES", "VBlank NMI check: nmi_enabled_=%d, calling Nmi()=%s",
nmi_enabled_, nmi_enabled_ ? "YES" : "NO");
LOG_DEBUG("SNES",
"VBlank NMI check: nmi_enabled_=%d, calling Nmi()=%s",
nmi_enabled_, nmi_enabled_ ? "YES" : "NO");
}
if (nmi_enabled_) {
cpu_.Nmi();
@@ -316,7 +329,8 @@ void Snes::RunCycle() {
}
}
// handle auto_joy_read_-timer
if (auto_joy_timer_ > 0) auto_joy_timer_ -= 2;
if (auto_joy_timer_ > 0)
auto_joy_timer_ -= 2;
}
void Snes::RunCycles(int cycles) {
@@ -350,12 +364,18 @@ uint8_t Snes::ReadBBus(uint8_t adr) {
// Log port reads when value changes or during critical phase
static int cpu_port_read_count = 0;
static uint8_t last_f4 = 0xFF, last_f5 = 0xFF;
bool value_changed = ((adr & 0x3) == 0 && val != last_f4) || ((adr & 0x3) == 1 && val != last_f5);
bool value_changed = ((adr & 0x3) == 0 && val != last_f4) ||
((adr & 0x3) == 1 && val != last_f5);
if (value_changed || cpu_port_read_count++ < 5) {
LOG_DEBUG("SNES", "CPU read APU port $21%02X (F%d) = $%02X at PC=$%02X:%04X [AFTER CatchUp: APU_cycles=%llu CPU_cycles=%llu]",
0x40 + (adr & 0x3), (adr & 0x3) + 4, val, cpu_.PB, cpu_.PC, apu_.GetCycles(), cycles_);
if ((adr & 0x3) == 0) last_f4 = val;
if ((adr & 0x3) == 1) last_f5 = val;
LOG_DEBUG("SNES",
"CPU read APU port $21%02X (F%d) = $%02X at PC=$%02X:%04X "
"[AFTER CatchUp: APU_cycles=%llu CPU_cycles=%llu]",
0x40 + (adr & 0x3), (adr & 0x3) + 4, val, cpu_.PB, cpu_.PC,
apu_.GetCycles(), cycles_);
if ((adr & 0x3) == 0)
last_f4 = val;
if ((adr & 0x3) == 1)
last_f5 = val;
}
return val;
}
@@ -410,8 +430,11 @@ uint8_t Snes::ReadReg(uint16_t adr) {
// 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);
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;
}
@@ -423,8 +446,11 @@ uint8_t Snes::ReadReg(uint16_t adr) {
// 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);
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;
}
@@ -458,8 +484,10 @@ uint8_t Snes::Rread(uint32_t adr) {
// 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);
LOG_DEBUG(
"SNES",
">>> Rread($%04X) from bank=$%02X PC=$%04X - calling ReadReg <<<",
adr, bank, cpu_.PC);
}
return ReadReg(adr); // internal registers
}
@@ -485,20 +513,21 @@ void Snes::WriteBBus(uint8_t adr, uint8_t val) {
if (adr < 0x80) {
CatchUpApu(); // catch up the apu before writing
apu_.in_ports_[adr & 0x3] = val;
// Track CPU port writes for handshake debugging
uint32_t full_pc = (static_cast<uint32_t>(cpu_.PB) << 16) | cpu_.PC;
apu_handshake_tracker_.OnCpuPortWrite(adr & 0x3, val, full_pc);
static int cpu_port_write_count = 0;
if (cpu_port_write_count++ < 10) { // Reduced to prevent crash
LOG_DEBUG("SNES", "CPU wrote APU port $21%02X (F%d) = $%02X at PC=$%02X:%04X",
0x40 + (adr & 0x3), (adr & 0x3) + 4, val, cpu_.PB, cpu_.PC);
LOG_DEBUG("SNES",
"CPU wrote APU port $21%02X (F%d) = $%02X at PC=$%02X:%04X",
0x40 + (adr & 0x3), (adr & 0x3) + 4, val, cpu_.PB, cpu_.PC);
}
// NOTE: Auto-reset disabled - relying on complete IPL ROM with counter protocol
// The IPL ROM will handle multi-upload sequences via its transfer loop
return;
}
switch (adr) {
@@ -528,20 +557,24 @@ void Snes::WriteReg(uint16_t adr, uint8_t val) {
// Log ALL writes to $4200 unconditionally
static int write_4200_count = 0;
if (write_4200_count++ < 20) {
LOG_DEBUG("SNES", "Write $%02X to $4200 at PC=$%02X:%04X (NMI=%d IRQ_H=%d IRQ_V=%d JOY=%d)",
val, cpu_.PB, cpu_.PC, (val & 0x80) ? 1 : 0, (val & 0x10) ? 1 : 0,
(val & 0x20) ? 1 : 0, (val & 0x01) ? 1 : 0);
LOG_DEBUG("SNES",
"Write $%02X to $4200 at PC=$%02X:%04X (NMI=%d IRQ_H=%d "
"IRQ_V=%d JOY=%d)",
val, cpu_.PB, cpu_.PC, (val & 0x80) ? 1 : 0,
(val & 0x10) ? 1 : 0, (val & 0x20) ? 1 : 0,
(val & 0x01) ? 1 : 0);
}
auto_joy_read_ = val & 0x1;
if (!auto_joy_read_) auto_joy_timer_ = 0;
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);
auto_joy_read_ ? "ENABLED" : "DISABLED", cpu_.PB, cpu_.PC);
last_auto_joy = auto_joy_read_;
}
h_irq_enabled_ = val & 0x10;
@@ -557,8 +590,8 @@ void Snes::WriteReg(uint16_t adr, uint8_t val) {
bool old_nmi = nmi_enabled_;
nmi_enabled_ = val & 0x80;
if (old_nmi != nmi_enabled_) {
LOG_DEBUG("SNES", ">>> NMI enabled CHANGED: %d -> %d <<<",
old_nmi, nmi_enabled_);
LOG_DEBUG("SNES", ">>> NMI enabled CHANGED: %d -> %d <<<", old_nmi,
nmi_enabled_);
}
cpu_.set_int_delay(true);
break;
@@ -666,9 +699,11 @@ int Snes::GetAccessTime(uint32_t adr) {
adr &= 0xffff;
if ((bank < 0x40 || (bank >= 0x80 && bank < 0xc0)) && adr < 0x8000) {
// 00-3f,80-bf:0-7fff
if (adr < 0x2000 || adr >= 0x6000) return 8; // 0-1fff, 6000-7fff
if (adr < 0x4000 || adr >= 0x4200) return 6; // 2000-3fff, 4200-5fff
return 12; // 4000-41ff
if (adr < 0x2000 || adr >= 0x6000)
return 8; // 0-1fff, 6000-7fff
if (adr < 0x4000 || adr >= 0x4200)
return 6; // 2000-3fff, 4200-5fff
return 12; // 4000-41ff
}
// 40-7f,co-ff:0000-ffff, 00-3f,80-bf:8000-ffff
return (fast_mem_ && bank >= 0x80) ? 6
@@ -704,21 +739,24 @@ void Snes::SetSamples(int16_t* sample_data, int wanted_samples) {
apu_.dsp().GetSamples(sample_data, wanted_samples, memory_.pal_timing());
}
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) {
// 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
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);

View File

@@ -66,14 +66,14 @@ void RenderNavBar(Emulator* emu) {
// Play/Pause button with icon
bool is_running = emu->running();
if (is_running) {
if (ImGui::Button(ICON_MD_PAUSE " Pause", ImVec2(100, kButtonHeight))) {
if (ImGui::Button(ICON_MD_PAUSE, ImVec2(50, kButtonHeight))) {
emu->set_running(false);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Pause emulation (Space)");
}
} else {
if (ImGui::Button(ICON_MD_PLAY_ARROW " Play", ImVec2(100, kButtonHeight))) {
if (ImGui::Button(ICON_MD_PLAY_ARROW, ImVec2(50, kButtonHeight))) {
emu->set_running(true);
}
if (ImGui::IsItemHovered()) {
@@ -84,7 +84,7 @@ void RenderNavBar(Emulator* emu) {
ImGui::SameLine();
// Step button
if (ImGui::Button(ICON_MD_SKIP_NEXT " Step", ImVec2(80, kButtonHeight))) {
if (ImGui::Button(ICON_MD_SKIP_NEXT, ImVec2(50, kButtonHeight))) {
if (!is_running) {
emu->snes().RunFrame();
}
@@ -96,7 +96,7 @@ void RenderNavBar(Emulator* emu) {
ImGui::SameLine();
// Reset button
if (ImGui::Button(ICON_MD_RESTART_ALT " Reset", ImVec2(80, kButtonHeight))) {
if (ImGui::Button(ICON_MD_RESTART_ALT, ImVec2(50, kButtonHeight))) {
emu->snes().Reset();
LOG_INFO("Emulator", "System reset");
}
@@ -219,6 +219,16 @@ void RenderNavBar(Emulator* emu) {
audio_status.queued_frames,
audio_status.is_playing ? "YES" : "NO");
}
ImGui::SameLine();
static bool use_sdl_audio_stream = emu->use_sdl_audio_stream();
if (ImGui::Checkbox(ICON_MD_SETTINGS " SDL Audio Stream", &use_sdl_audio_stream)) {
emu->set_use_sdl_audio_stream(use_sdl_audio_stream);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Use SDL audio stream for audio");
}
} else {
ImGui::TextColored(ConvertColorToImVec4(theme.error),
ICON_MD_VOLUME_OFF " No Backend");