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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user