diff --git a/src/app/emu/audio/audio_backend.cc b/src/app/emu/audio/audio_backend.cc index 51053cd3..579f1db0 100644 --- a/src/app/emu/audio/audio_backend.cc +++ b/src/app/emu/audio/audio_backend.cc @@ -4,6 +4,7 @@ #include #include +#include #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(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(sizeof(int16_t)); + if (static_cast(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 AudioBackendFactory::Create(BackendType type) { } // namespace audio } // namespace emu } // namespace yaze - diff --git a/src/app/emu/audio/audio_backend.h b/src/app/emu/audio/audio_backend.h index 604b2802..d3126c00 100644 --- a/src/app/emu/audio/audio_backend.h +++ b/src/app/emu/audio/audio_backend.h @@ -4,9 +4,12 @@ #ifndef YAZE_APP_EMU_AUDIO_AUDIO_BACKEND_H #define YAZE_APP_EMU_AUDIO_AUDIO_BACKEND_H +#include + #include #include #include +#include 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 stream_buffer_; }; /** @@ -125,4 +150,3 @@ class AudioBackendFactory { } // namespace yaze #endif // YAZE_APP_EMU_AUDIO_AUDIO_BACKEND_H - diff --git a/src/app/emu/audio/dsp.cc b/src/app/emu/audio/dsp.cc index 23675365..a82ed2ba 100644 --- a/src/app/emu/audio/dsp.cc +++ b/src/app/emu/audio/dsp.cc @@ -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( + (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 diff --git a/src/app/emu/audio/dsp.h b/src/app/emu/audio/dsp.h index 67a072c7..688a08b4 100644 --- a/src/app/emu/audio/dsp.h +++ b/src/app/emu/audio/dsp.h @@ -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; diff --git a/src/app/emu/emulator.cc b/src/app/emu/emulator.cc index d20780cf..722da9c9 100644 --- a/src/app/emu/emulator.cc +++ b/src/app/emu/emulator.cc @@ -1,5 +1,6 @@ #include "app/emu/emulator.h" +#include #include #include #include @@ -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& 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& } 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& } 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); } } } diff --git a/src/app/emu/emulator.h b/src/app/emu/emulator.h index 323ae631..e066e97a 100644 --- a/src/app/emu/emulator.h +++ b/src/app/emu/emulator.h @@ -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! diff --git a/src/app/emu/snes.cc b/src/app/emu/snes.cc index c6de768b..a2566151 100644 --- a/src/app/emu/snes.cc +++ b/src/app/emu/snes.cc @@ -9,8 +9,10 @@ #include "app/emu/video/ppu.h" #include "util/log.h" -#define WRITE_STATE(file, member) file.write(reinterpret_cast(&member), sizeof(member)) -#define READ_STATE(file, member) file.read(reinterpret_cast(&member), sizeof(member)) +#define WRITE_STATE(file, member) \ + file.write(reinterpret_cast(&member), sizeof(member)) +#define READ_STATE(file, member) \ + file.read(reinterpret_cast(&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& 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(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); diff --git a/src/app/emu/ui/emulator_ui.cc b/src/app/emu/ui/emulator_ui.cc index 92b55bcd..75674f46 100644 --- a/src/app/emu/ui/emulator_ui.cc +++ b/src/app/emu/ui/emulator_ui.cc @@ -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");