diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index acd174a7..a0852df9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,12 +1,14 @@ set( YAZE_APP_EMU_SRC app/emu/audio/apu.cc + app/emu/audio/audio_backend.cc app/emu/audio/spc700.cc app/emu/audio/dsp.cc app/emu/audio/internal/addressing.cc app/emu/audio/internal/instructions.cc app/emu/cpu/internal/addressing.cc app/emu/cpu/internal/instructions.cc + app/emu/debug/apu_debugger.cc app/emu/debug/disassembly_viewer.cc app/emu/debug/breakpoint_manager.cc app/emu/debug/watchpoint_manager.cc @@ -884,6 +886,8 @@ source_group("Application\\Emulator" FILES source_group("Application\\Emulator\\Audio" FILES app/emu/audio/apu.cc app/emu/audio/apu.h + app/emu/audio/audio_backend.cc + app/emu/audio/audio_backend.h app/emu/audio/spc700.cc app/emu/audio/spc700.h app/emu/audio/dsp.cc @@ -920,6 +924,18 @@ source_group("Application\\Emulator\\Video" FILES app/emu/video/ppu_registers.h ) +# Debug System +source_group("Application\\Emulator\\Debug" FILES + app/emu/debug/apu_debugger.cc + app/emu/debug/apu_debugger.h + app/emu/debug/breakpoint_manager.cc + app/emu/debug/breakpoint_manager.h + app/emu/debug/disassembly_viewer.cc + app/emu/debug/disassembly_viewer.h + app/emu/debug/watchpoint_manager.cc + app/emu/debug/watchpoint_manager.h +) + # Graphics System source_group("Application\\Graphics" FILES app/gfx/arena.cc diff --git a/src/app/core/features.h b/src/app/core/features.h index a86aeb56..a6cfb399 100644 --- a/src/app/core/features.h +++ b/src/app/core/features.h @@ -14,7 +14,8 @@ class FeatureFlags { public: struct Flags { // Log instructions to the GUI debugger. - bool kLogInstructions = true; + // WARNING: Setting this to true causes SEVERE performance degradation + bool kLogInstructions = false; // Flag to enable the saving of all palettes to the Rom. bool kSaveAllPalettes = false; diff --git a/src/app/core/window.cc b/src/app/core/window.cc index 32b472ab..2c4f5489 100644 --- a/src/app/core/window.cc +++ b/src/app/core/window.cc @@ -113,24 +113,22 @@ absl::Status CreateWindow(Window& window, gfx::IRenderer* renderer, int flags) { // Apply original YAZE colors as fallback, then try to load theme system gui::ColorsYaze(); - // Initialize audio if not already initialized + // Audio is now handled by IAudioBackend in Emulator class + // Keep legacy buffer allocation for backwards compatibility if (window.audio_device_ == 0) { const int audio_frequency = 48000; - SDL_AudioSpec want, have; - SDL_memset(&want, 0, sizeof(want)); - want.freq = audio_frequency; - want.format = AUDIO_S16; - want.channels = 2; - want.samples = 2048; - want.callback = NULL; // Uses the queue - window.audio_device_ = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); - if (window.audio_device_ == 0) { - LOG_ERROR("Window", "Failed to open audio: %s", SDL_GetError()); - // Don't fail - audio is optional - } else { - window.audio_buffer_ = std::make_shared(audio_frequency / 50 * 4); - SDL_PauseAudioDevice(window.audio_device_, 0); - } + const size_t buffer_size = (audio_frequency / 50) * 2; // 1920 int16_t for stereo PAL + + // CRITICAL FIX: Allocate buffer as ARRAY, not single value + // Use new[] with shared_ptr custom deleter for proper array allocation + window.audio_buffer_ = std::shared_ptr( + new int16_t[buffer_size], + std::default_delete()); + + // Note: Actual audio device is created by Emulator's IAudioBackend + // This maintains compatibility with existing code paths + LOG_INFO("Window", "Audio buffer allocated: %zu int16_t samples (backend in Emulator)", + buffer_size); } return absl::OkStatus(); @@ -203,7 +201,7 @@ absl::Status HandleEvents(Window& window) { // Update display size for both resize and size_changed events io.DisplaySize.x = static_cast(event.window.data1); io.DisplaySize.y = static_cast(event.window.data2); - g_window_is_resizing = true; + core::g_window_is_resizing = true; break; case SDL_WINDOWEVENT_MINIMIZED: case SDL_WINDOWEVENT_HIDDEN: diff --git a/src/app/emu/audio/apu.cc b/src/app/emu/audio/apu.cc index 81eabd65..ba76671c 100644 --- a/src/app/emu/audio/apu.cc +++ b/src/app/emu/audio/apu.cc @@ -8,6 +8,7 @@ #include "app/emu/audio/dsp.h" #include "app/emu/audio/spc700.h" #include "app/emu/memory/memory.h" +#include "emu/debug/apu_debugger.h" #include "util/log.h" namespace yaze { @@ -59,6 +60,12 @@ void Apu::Reset() { timer_[i].counter = 0; timer_[i].enabled = false; } + + // Reset handshake tracker + if (handshake_tracker_) { + handshake_tracker_->Reset(); + } + LOG_DEBUG("APU", "Reset complete - IPL ROM readable, PC will be at $%04X", spc700_.read_word(0xFFFE)); } @@ -236,6 +243,13 @@ void Apu::Write(uint16_t adr, uint8_t val) { if (old_rom_readable != rom_readable_) { LOG_DEBUG("APU", "Control register $F1 = $%02X - IPL ROM %s at PC=$%04X", val, rom_readable_ ? "ENABLED" : "DISABLED", spc700_.PC); + + // Track IPL ROM disable for handshake debugging + if (handshake_tracker_ && !rom_readable_) { + // IPL ROM disabled means audio driver uploaded successfully + handshake_tracker_->OnSpcPCChange(spc700_.PC, spc700_.PC); + } + // When IPL ROM is disabled, reset transfer tracking if (!rom_readable_) { in_transfer_ = false; @@ -257,6 +271,12 @@ void Apu::Write(uint16_t adr, uint8_t val) { case 0xf6: case 0xf7: { out_ports_[adr - 0xf4] = val; + + // Track SPC port writes for handshake debugging + if (handshake_tracker_) { + handshake_tracker_->OnSpcPortWrite(adr - 0xf4, val, spc700_.PC); + } + port_write_count++; if (port_write_count < 10) { // Reduced to prevent logging overflow crash LOG_DEBUG("APU", "SPC wrote port $%04X (F%d) = $%02X at PC=$%04X [APU_cycles=%llu]", diff --git a/src/app/emu/audio/apu.h b/src/app/emu/audio/apu.h index 33b8eace..35da00a3 100644 --- a/src/app/emu/audio/apu.h +++ b/src/app/emu/audio/apu.h @@ -12,6 +12,11 @@ namespace yaze { namespace emu { +// Forward declaration +namespace debug { +class ApuHandshakeTracker; +} + typedef struct Timer { uint8_t cycles; uint8_t divider; @@ -66,6 +71,11 @@ class Apu { auto spc700() -> Spc700 & { return spc700_; } uint64_t GetCycles() const { return cycles_; } + + // Audio debugging + void set_handshake_tracker(debug::ApuHandshakeTracker* tracker) { + handshake_tracker_ = tracker; + } uint8_t GetStatus() const { return ram[0x00]; } uint8_t GetControl() const { return ram[0x01]; } void GetSamples(int16_t *buffer, int count, bool loop = false) { @@ -94,6 +104,9 @@ class Apu { MemoryImpl &memory_; std::array timer_; + + // Audio debugging + debug::ApuHandshakeTracker* handshake_tracker_ = nullptr; ApuCallbacks callbacks_ = { [&](uint16_t adr, uint8_t val) { SpcWrite(adr, val); }, diff --git a/src/app/emu/audio/audio_backend.cc b/src/app/emu/audio/audio_backend.cc new file mode 100644 index 00000000..56074642 --- /dev/null +++ b/src/app/emu/audio/audio_backend.cc @@ -0,0 +1,201 @@ +// audio_backend.cc - Audio Backend Implementation + +#include "app/emu/audio/audio_backend.h" + +#include +#include +#include "util/log.h" + +namespace yaze { +namespace emu { +namespace audio { + +// ============================================================================ +// SDL2AudioBackend Implementation +// ============================================================================ + +SDL2AudioBackend::~SDL2AudioBackend() { + Shutdown(); +} + +bool SDL2AudioBackend::Initialize(const AudioConfig& config) { + if (initialized_) { + LOG_WARN("AudioBackend", "Already initialized, shutting down first"); + Shutdown(); + } + + config_ = config; + + SDL_AudioSpec want, have; + SDL_memset(&want, 0, sizeof(want)); + + want.freq = config.sample_rate; + want.format = (config.format == SampleFormat::INT16) ? AUDIO_S16 : AUDIO_F32; + want.channels = config.channels; + want.samples = config.buffer_frames; + want.callback = nullptr; // Use queue-based audio + + device_id_ = SDL_OpenAudioDevice(nullptr, 0, &want, &have, 0); + + if (device_id_ == 0) { + LOG_ERROR("AudioBackend", "Failed to open SDL audio device: %s", SDL_GetError()); + return false; + } + + // Verify we got what we asked for + if (have.freq != want.freq || have.channels != want.channels) { + LOG_WARN("AudioBackend", + "Audio spec mismatch - wanted %dHz %dch, got %dHz %dch", + want.freq, want.channels, have.freq, have.channels); + // Update config with actual values + config_.sample_rate = have.freq; + config_.channels = have.channels; + } + + LOG_INFO("AudioBackend", "SDL2 audio initialized: %dHz, %d channels, %d samples buffer", + have.freq, have.channels, have.samples); + + initialized_ = true; + + // Start playback immediately (unpause) + SDL_PauseAudioDevice(device_id_, 0); + + return true; +} + +void SDL2AudioBackend::Shutdown() { + if (!initialized_) return; + + if (device_id_ != 0) { + SDL_PauseAudioDevice(device_id_, 1); + SDL_CloseAudioDevice(device_id_); + device_id_ = 0; + } + + initialized_ = false; + LOG_INFO("AudioBackend", "SDL2 audio shut down"); +} + +void SDL2AudioBackend::Play() { + if (!initialized_) return; + SDL_PauseAudioDevice(device_id_, 0); +} + +void SDL2AudioBackend::Pause() { + if (!initialized_) return; + SDL_PauseAudioDevice(device_id_, 1); +} + +void SDL2AudioBackend::Stop() { + if (!initialized_) return; + Clear(); + SDL_PauseAudioDevice(device_id_, 1); +} + +void SDL2AudioBackend::Clear() { + if (!initialized_) return; + SDL_ClearQueuedAudio(device_id_); +} + +bool SDL2AudioBackend::QueueSamples(const int16_t* samples, int num_samples) { + if (!initialized_ || !samples) return false; + + // Apply volume scaling + if (volume_ != 1.0f) { + std::vector scaled_samples(num_samples); + for (int i = 0; i < num_samples; ++i) { + int32_t scaled = static_cast(samples[i] * volume_); + scaled_samples[i] = static_cast(std::clamp(scaled, -32768, 32767)); + } + + int result = SDL_QueueAudio(device_id_, scaled_samples.data(), + num_samples * sizeof(int16_t)); + if (result < 0) { + LOG_ERROR("AudioBackend", "SDL_QueueAudio failed: %s", SDL_GetError()); + return false; + } + } else { + int result = SDL_QueueAudio(device_id_, samples, num_samples * sizeof(int16_t)); + if (result < 0) { + LOG_ERROR("AudioBackend", "SDL_QueueAudio failed: %s", SDL_GetError()); + return false; + } + } + + return true; +} + +bool SDL2AudioBackend::QueueSamples(const float* samples, int num_samples) { + if (!initialized_ || !samples) return false; + + // Convert float to int16 + std::vector int_samples(num_samples); + for (int i = 0; i < num_samples; ++i) { + float scaled = std::clamp(samples[i] * volume_, -1.0f, 1.0f); + int_samples[i] = static_cast(scaled * 32767.0f); + } + + return QueueSamples(int_samples.data(), num_samples); +} + +AudioStatus SDL2AudioBackend::GetStatus() const { + AudioStatus status; + + if (!initialized_) return status; + + status.is_playing = (SDL_GetAudioDeviceStatus(device_id_) == SDL_AUDIO_PLAYING); + status.queued_bytes = SDL_GetQueuedAudioSize(device_id_); + + // Calculate queued frames (each frame = channels * sample_size) + int bytes_per_frame = config_.channels * + (config_.format == SampleFormat::INT16 ? 2 : 4); + status.queued_frames = status.queued_bytes / bytes_per_frame; + + // Check for underrun (queue too low while playing) + if (status.is_playing && status.queued_frames < 100) { + status.has_underrun = true; + } + + return status; +} + +bool SDL2AudioBackend::IsInitialized() const { + return initialized_; +} + +AudioConfig SDL2AudioBackend::GetConfig() const { + return config_; +} + +void SDL2AudioBackend::SetVolume(float volume) { + volume_ = std::clamp(volume, 0.0f, 1.0f); +} + +float SDL2AudioBackend::GetVolume() const { + return volume_; +} + +// ============================================================================ +// AudioBackendFactory Implementation +// ============================================================================ + +std::unique_ptr AudioBackendFactory::Create(BackendType type) { + switch (type) { + case BackendType::SDL2: + return std::make_unique(); + + case BackendType::NULL_BACKEND: + // TODO: Implement null backend for testing + LOG_WARN("AudioBackend", "NULL backend not yet implemented, using SDL2"); + return std::make_unique(); + + default: + LOG_ERROR("AudioBackend", "Unknown backend type, using SDL2"); + return std::make_unique(); + } +} + +} // namespace audio +} // namespace emu +} // namespace yaze + diff --git a/src/app/emu/audio/audio_backend.h b/src/app/emu/audio/audio_backend.h new file mode 100644 index 00000000..604b2802 --- /dev/null +++ b/src/app/emu/audio/audio_backend.h @@ -0,0 +1,128 @@ +// audio_backend.h - Audio Backend Abstraction Layer +// Provides interface for swapping audio implementations (SDL2, SDL3, other libs) + +#ifndef YAZE_APP_EMU_AUDIO_AUDIO_BACKEND_H +#define YAZE_APP_EMU_AUDIO_AUDIO_BACKEND_H + +#include +#include +#include + +namespace yaze { +namespace emu { +namespace audio { + +// Audio sample format +enum class SampleFormat { + INT16, // 16-bit signed PCM + FLOAT32 // 32-bit float +}; + +// Audio configuration +struct AudioConfig { + int sample_rate = 48000; + int channels = 2; // Stereo + int buffer_frames = 1024; + SampleFormat format = SampleFormat::INT16; +}; + +// Audio backend status +struct AudioStatus { + bool is_playing = false; + uint32_t queued_bytes = 0; + uint32_t queued_frames = 0; + bool has_underrun = false; +}; + +/** + * @brief Abstract audio backend interface + * + * Allows swapping between SDL2, SDL3, or custom audio implementations + * without changing emulator/music editor code. + */ +class IAudioBackend { + public: + virtual ~IAudioBackend() = default; + + // Initialization + virtual bool Initialize(const AudioConfig& config) = 0; + virtual void Shutdown() = 0; + + // Playback control + virtual void Play() = 0; + virtual void Pause() = 0; + virtual void Stop() = 0; + virtual void Clear() = 0; + + // Audio data + virtual bool QueueSamples(const int16_t* samples, int num_samples) = 0; + virtual bool QueueSamples(const float* samples, int num_samples) = 0; + + // Status queries + virtual AudioStatus GetStatus() const = 0; + virtual bool IsInitialized() const = 0; + virtual AudioConfig GetConfig() const = 0; + + // Volume control (0.0 to 1.0) + virtual void SetVolume(float volume) = 0; + virtual float GetVolume() const = 0; + + // Get backend name for debugging + virtual std::string GetBackendName() const = 0; +}; + +/** + * @brief SDL2 audio backend implementation + */ +class SDL2AudioBackend : public IAudioBackend { + public: + SDL2AudioBackend() = default; + ~SDL2AudioBackend() override; + + bool Initialize(const AudioConfig& config) override; + void Shutdown() override; + + void Play() override; + void Pause() override; + void Stop() override; + void Clear() override; + + bool QueueSamples(const int16_t* samples, int num_samples) override; + bool QueueSamples(const float* samples, int num_samples) override; + + AudioStatus GetStatus() const override; + bool IsInitialized() const override; + AudioConfig GetConfig() const override; + + void SetVolume(float volume) override; + float GetVolume() const override; + + std::string GetBackendName() const override { return "SDL2"; } + + private: + uint32_t device_id_ = 0; + AudioConfig config_; + bool initialized_ = false; + float volume_ = 1.0f; +}; + +/** + * @brief Factory for creating audio backends + */ +class AudioBackendFactory { + public: + enum class BackendType { + SDL2, + SDL3, // Future + NULL_BACKEND // For testing/headless + }; + + static std::unique_ptr Create(BackendType type); +}; + +} // namespace audio +} // namespace emu +} // namespace yaze + +#endif // YAZE_APP_EMU_AUDIO_AUDIO_BACKEND_H + diff --git a/src/app/emu/cpu/cpu.cc b/src/app/emu/cpu/cpu.cc index bd51cd49..0768d995 100644 --- a/src/app/emu/cpu/cpu.cc +++ b/src/app/emu/cpu/cpu.cc @@ -1938,17 +1938,18 @@ bool immediate, bool accumulator_mode) { const std::string& mnemonic = opcode_to_mnemonic.at(opcode); // NEW: Call recording callback if set (for DisassemblyViewer) + // DisassemblyViewer uses sparse address-map recording (Mesen-style) + // - Only records each unique address ONCE + // - Increments execution_count on re-visits + // - No performance impact even with millions of instructions if (on_instruction_executed_) { on_instruction_executed_(full_address, opcode, operand_bytes, mnemonic, operand_str); } - // Legacy: Record to old disassembly viewer if feature flag enabled + // DEPRECATED: Legacy instruction_log_ kept for backwards compatibility only + // This is the old, inefficient logging that stores EVERY execution. + // Use DisassemblyViewer instead - it's always enabled and much more efficient. if (core::FeatureFlags::get().kLogInstructions) { - // Record to new disassembly viewer - disassembly_viewer().RecordInstruction(full_address, opcode, operand_bytes, - mnemonic, operand_str); - - // Also maintain legacy log for compatibility std::ostringstream oss; oss << "$" << std::uppercase << std::setw(2) << std::setfill('0') << static_cast(PB) << ":" << std::hex << PC << ": 0x" @@ -1958,82 +1959,15 @@ bool immediate, bool accumulator_mode) { InstructionEntry entry(PC, opcode, operand_str, oss.str()); instruction_log_.push_back(entry); - // Also emit to the central logger for user/agent-controlled sinks - util::LogManager::instance().log(util::LogLevel::YAZE_DEBUG, "CPU", - oss.str()); - } else { - // Log the address and opcode. - std::cout << "\033[1;36m" - << "$" << std::uppercase << std::setw(2) << std::setfill('0') - << static_cast(PB) << ":" << std::hex << PC; - std::cout << " \033[1;32m" - << ": 0x" << std::hex << std::uppercase << std::setw(2) - << std::setfill('0') << static_cast(opcode) << " "; - std::cout << " \033[1;35m" << opcode_to_mnemonic.at(opcode) << " " - << "\033[0m"; - - // Log the operand. - if (operand) { - if (immediate) { - std::cout << "#"; - } - std::cout << "$"; - if (accumulator_mode) { - std::cout << std::hex << std::setw(2) << std::setfill('0') << operand; - } else { - std::cout << std::hex << std::setw(4) << std::setfill('0') - << static_cast(operand); - } - - bool x_indexing, y_indexing; - auto x_indexed_instruction_opcodes = {0x15, 0x16, 0x17, 0x55, 0x56, - 0x57, 0xD5, 0xD6, 0xD7, 0xF5, - 0xF6, 0xF7, 0xBD}; - auto y_indexed_instruction_opcodes = {0x19, 0x97, 0x1D, 0x59, 0x5D, 0x99, - 0x9D, 0xB9, 0xD9, 0xDD, 0xF9, 0xFD}; - if (std::find(x_indexed_instruction_opcodes.begin(), - x_indexed_instruction_opcodes.end(), - opcode) != x_indexed_instruction_opcodes.end()) { - x_indexing = true; - } else { - x_indexing = false; - } - if (std::find(y_indexed_instruction_opcodes.begin(), - y_indexed_instruction_opcodes.end(), - opcode) != y_indexed_instruction_opcodes.end()) { - y_indexing = true; - } else { - y_indexing = false; - } - - if (x_indexing) { - std::cout << ", X"; - } - - if (y_indexing) { - std::cout << ", Y"; - } + // PERFORMANCE: Cap to prevent unbounded growth + constexpr size_t kMaxInstructionLogSize = 10000; + if (instruction_log_.size() > kMaxInstructionLogSize) { + instruction_log_.erase(instruction_log_.begin(), + instruction_log_.begin() + kMaxInstructionLogSize / 2); } - - // Log the registers and flags. - std::cout << std::right; - std::cout << "\033[1;33m" - << " A:" << std::hex << std::setw(2) << std::setfill('0') - << static_cast(A); - std::cout << " X:" << std::hex << std::setw(2) << std::setfill('0') - << static_cast(X); - std::cout << " Y:" << std::hex << std::setw(2) << std::setfill('0') - << static_cast(Y); - std::cout << " S:" << std::hex << std::setw(2) << std::setfill('0') - << static_cast(status); - std::cout << " DB:" << std::hex << std::setw(2) << std::setfill('0') - << static_cast(DB); - std::cout << " D:" << std::hex << std::setw(2) << std::setfill('0') - << static_cast(D); - std::cout << " SP:" << std::hex << std::setw(4) << std::setfill('0') - << SP(); - - std::cout << std::endl; + + // Also emit to central logger + util::LogManager::instance().log(util::LogLevel::YAZE_DEBUG, "CPU", oss.str()); } } diff --git a/src/app/emu/debug/apu_debugger.cc b/src/app/emu/debug/apu_debugger.cc new file mode 100644 index 00000000..327cfc15 --- /dev/null +++ b/src/app/emu/debug/apu_debugger.cc @@ -0,0 +1,215 @@ +// apu_debugger.cc - APU Handshake Tracker Implementation + +#include "app/emu/debug/apu_debugger.h" + +#include "absl/strings/str_format.h" +#include "util/log.h" + +namespace yaze { +namespace emu { +namespace debug { + +ApuHandshakeTracker::ApuHandshakeTracker() { + Reset(); +} + +void ApuHandshakeTracker::Reset() { + phase_ = Phase::RESET; + handshake_complete_ = false; + ipl_rom_enabled_ = true; + transfer_counter_ = 0; + total_bytes_transferred_ = 0; + + memset(cpu_ports_, 0, sizeof(cpu_ports_)); + memset(spc_ports_, 0, sizeof(spc_ports_)); + + blocks_.clear(); + port_history_.clear(); + + LOG_DEBUG("APU_DEBUG", "Handshake tracker reset"); +} + +void ApuHandshakeTracker::OnCpuPortWrite(uint8_t port, uint8_t value, uint32_t pc) { + if (port > 3) return; + + cpu_ports_[port] = value; + + // Check for handshake acknowledge + if (phase_ == Phase::WAITING_BBAA && port == 0 && value == 0xCC) { + UpdatePhase(Phase::HANDSHAKE_CC); + handshake_complete_ = true; + LogPortWrite(true, port, value, pc, "HANDSHAKE ACKNOWLEDGE"); + LOG_INFO("APU_DEBUG", "✓ CPU sent handshake $CC at PC=$%06X", pc); + return; + } + + // Track transfer counter writes + if (phase_ == Phase::HANDSHAKE_CC || phase_ == Phase::TRANSFER_ACTIVE) { + if (port == 0) { + transfer_counter_ = value; + UpdatePhase(Phase::TRANSFER_ACTIVE); + LogPortWrite(true, port, value, pc, + absl::StrFormat("Counter=%d", transfer_counter_)); + } else if (port == 1) { + // F5 = continuation flag (0=more blocks, 1=final block) + bool is_final = (value & 0x01) != 0; + LogPortWrite(true, port, value, pc, + is_final ? "FINAL BLOCK" : "More blocks"); + } else if (port == 2 || port == 3) { + // F6:F7 = destination address + LogPortWrite(true, port, value, pc, "Dest addr"); + } + } else { + LogPortWrite(true, port, value, pc, ""); + } +} + +void ApuHandshakeTracker::OnSpcPortWrite(uint8_t port, uint8_t value, uint16_t pc) { + if (port > 3) return; + + spc_ports_[port] = value; + + // Check for ready signal ($BBAA in F4:F5) + if (phase_ == Phase::IPL_BOOT && port == 0 && value == 0xAA) { + if (spc_ports_[1] == 0xBB || port == 1) { // Check if both ready + UpdatePhase(Phase::WAITING_BBAA); + LogPortWrite(false, port, value, pc, "READY SIGNAL $BBAA"); + LOG_INFO("APU_DEBUG", "✓ SPC ready signal: F4=$AA F5=$BB at PC=$%04X", pc); + return; + } + } + + if (phase_ == Phase::IPL_BOOT && port == 1 && value == 0xBB) { + if (spc_ports_[0] == 0xAA) { + UpdatePhase(Phase::WAITING_BBAA); + LogPortWrite(false, port, value, pc, "READY SIGNAL $BBAA"); + LOG_INFO("APU_DEBUG", "✓ SPC ready signal: F4=$AA F5=$BB at PC=$%04X", pc); + return; + } + } + + // Track counter echo during transfer + if (phase_ == Phase::TRANSFER_ACTIVE && port == 0) { + int echoed_counter = value; + if (echoed_counter == transfer_counter_) { + total_bytes_transferred_++; + LogPortWrite(false, port, value, pc, + absl::StrFormat("Echo counter=%d (byte %d)", + echoed_counter, total_bytes_transferred_)); + } else { + LogPortWrite(false, port, value, pc, + absl::StrFormat("Counter mismatch! Expected=%d Got=%d", + transfer_counter_, echoed_counter)); + LOG_WARN("APU_DEBUG", "Counter mismatch at PC=$%04X: expected %d, got %d", + pc, transfer_counter_, echoed_counter); + } + } else { + LogPortWrite(false, port, value, pc, ""); + } +} + +void ApuHandshakeTracker::OnSpcPCChange(uint16_t old_pc, uint16_t new_pc) { + // Detect IPL ROM boot sequence + if (phase_ == Phase::RESET && new_pc >= 0xFFC0 && new_pc <= 0xFFFF) { + UpdatePhase(Phase::IPL_BOOT); + LOG_INFO("APU_DEBUG", "✓ SPC entered IPL ROM at PC=$%04X", new_pc); + } + + // Detect IPL ROM disable (jump to uploaded driver) + if (ipl_rom_enabled_ && new_pc < 0xFFC0) { + ipl_rom_enabled_ = false; + if (phase_ == Phase::TRANSFER_ACTIVE) { + UpdatePhase(Phase::TRANSFER_DONE); + LOG_INFO("APU_DEBUG", "✓ Transfer complete! SPC jumped to $%04X (audio driver entry)", + new_pc); + } + UpdatePhase(Phase::RUNNING); + } +} + +void ApuHandshakeTracker::UpdatePhase(Phase new_phase) { + if (phase_ != new_phase) { + LOG_DEBUG("APU_DEBUG", "Phase change: %s → %s", + GetPhaseString().c_str(), + [new_phase]() { + switch (new_phase) { + case Phase::RESET: return "RESET"; + case Phase::IPL_BOOT: return "IPL_BOOT"; + case Phase::WAITING_BBAA: return "WAITING_BBAA"; + case Phase::HANDSHAKE_CC: return "HANDSHAKE_CC"; + case Phase::TRANSFER_ACTIVE: return "TRANSFER_ACTIVE"; + case Phase::TRANSFER_DONE: return "TRANSFER_DONE"; + case Phase::RUNNING: return "RUNNING"; + default: return "UNKNOWN"; + } + }()); + phase_ = new_phase; + } +} + +void ApuHandshakeTracker::LogPortWrite(bool is_cpu, uint8_t port, uint8_t value, + uint32_t pc, const std::string& desc) { + PortWrite entry; + entry.timestamp = port_history_.size(); + entry.pc = static_cast(pc & 0xFFFF); + entry.port = port; + entry.value = value; + entry.is_cpu = is_cpu; + entry.description = desc; + + port_history_.push_back(entry); + + // Keep history bounded + if (port_history_.size() > kMaxHistorySize) { + port_history_.pop_front(); + } +} + +std::string ApuHandshakeTracker::GetPhaseString() const { + switch (phase_) { + case Phase::RESET: return "RESET"; + case Phase::IPL_BOOT: return "IPL_BOOT"; + case Phase::WAITING_BBAA: return "WAITING_BBAA"; + case Phase::HANDSHAKE_CC: return "HANDSHAKE_CC"; + case Phase::TRANSFER_ACTIVE: return "TRANSFER_ACTIVE"; + case Phase::TRANSFER_DONE: return "TRANSFER_DONE"; + case Phase::RUNNING: return "RUNNING"; + default: return "UNKNOWN"; + } +} + +std::string ApuHandshakeTracker::GetStatusSummary() const { + return absl::StrFormat( + "Phase: %s | Handshake: %s | Bytes: %d | Blocks: %d", + GetPhaseString(), + handshake_complete_ ? "✓" : "✗", + total_bytes_transferred_, + blocks_.size()); +} + +std::string ApuHandshakeTracker::GetTransferProgress() const { + if (phase_ != Phase::TRANSFER_ACTIVE && phase_ != Phase::TRANSFER_DONE) { + return ""; + } + + // Estimate progress (typical ALTTP upload is ~8KB) + int estimated_total = 8192; + int percent = (total_bytes_transferred_ * 100) / estimated_total; + percent = std::min(percent, 100); + + int bar_width = 20; + int filled = (percent * bar_width) / 100; + + std::string bar = "["; + for (int i = 0; i < bar_width; ++i) { + bar += (i < filled) ? "█" : "░"; + } + bar += absl::StrFormat("] %d%%", percent); + + return bar; +} + +} // namespace debug +} // namespace emu +} // namespace yaze + diff --git a/src/app/emu/debug/apu_debugger.h b/src/app/emu/debug/apu_debugger.h new file mode 100644 index 00000000..c73a2797 --- /dev/null +++ b/src/app/emu/debug/apu_debugger.h @@ -0,0 +1,101 @@ +// apu_debugger.h - APU Handshake and Transfer Debugging + +#ifndef YAZE_APP_EMU_DEBUG_APU_DEBUGGER_H +#define YAZE_APP_EMU_DEBUG_APU_DEBUGGER_H + +#include +#include +#include +#include + +namespace yaze { +namespace emu { +namespace debug { + +/** + * @brief IPL ROM handshake tracker + * + * Monitors CPU-APU communication during audio program upload to diagnose + * handshake failures and transfer issues. + */ +class ApuHandshakeTracker { + public: + enum class Phase { + RESET, // Initial state + IPL_BOOT, // SPC700 executing IPL ROM + WAITING_BBAA, // CPU waiting for SPC ready signal ($BBAA) + HANDSHAKE_CC, // CPU sent $CC acknowledge + TRANSFER_ACTIVE, // Data transfer in progress + TRANSFER_DONE, // Audio driver uploaded + RUNNING // SPC executing audio driver + }; + + struct PortWrite { + uint64_t timestamp; + uint16_t pc; // CPU or SPC program counter + uint8_t port; // 0-3 (F4-F7) + uint8_t value; + bool is_cpu; // true = CPU write, false = SPC write + std::string description; + }; + + struct TransferBlock { + uint16_t size; + uint16_t dest_address; + int bytes_transferred; + bool is_final; + }; + + ApuHandshakeTracker(); + + // Event tracking + void OnCpuPortWrite(uint8_t port, uint8_t value, uint32_t pc); + void OnSpcPortWrite(uint8_t port, uint8_t value, uint16_t pc); + void OnSpcPCChange(uint16_t old_pc, uint16_t new_pc); + + // State queries + Phase GetPhase() const { return phase_; } + bool IsHandshakeComplete() const { return handshake_complete_; } + bool IsTransferActive() const { return phase_ == Phase::TRANSFER_ACTIVE; } + int GetBytesTransferred() const { return total_bytes_transferred_; } + int GetBlockCount() const { return blocks_.size(); } + + // Get port write history + const std::deque& GetPortHistory() const { return port_history_; } + const std::vector& GetBlocks() const { return blocks_; } + + // Visualization + std::string GetPhaseString() const; + std::string GetStatusSummary() const; + std::string GetTransferProgress() const; // Returns progress bar string + + // Reset tracking + void Reset(); + + private: + void UpdatePhase(Phase new_phase); + void LogPortWrite(bool is_cpu, uint8_t port, uint8_t value, uint32_t pc, + const std::string& desc); + + Phase phase_ = Phase::RESET; + bool handshake_complete_ = false; + bool ipl_rom_enabled_ = true; + + uint8_t cpu_ports_[4] = {0}; // CPU → SPC (in_ports from SPC perspective) + uint8_t spc_ports_[4] = {0}; // SPC → CPU (out_ports from SPC perspective) + + int transfer_counter_ = 0; + int total_bytes_transferred_ = 0; + + std::vector blocks_; + std::deque port_history_; // Keep last 1000 writes + + static constexpr size_t kMaxHistorySize = 1000; +}; + +} // namespace debug +} // namespace emu +} // namespace yaze + +#endif // YAZE_APP_EMU_DEBUG_APU_DEBUGGER_H + diff --git a/src/app/emu/emulator.cc b/src/app/emu/emulator.cc index 5a7586ae..554d16e2 100644 --- a/src/app/emu/emulator.cc +++ b/src/app/emu/emulator.cc @@ -5,6 +5,7 @@ #include #include "app/core/window.h" +#include "util/log.h" namespace yaze::core { extern bool g_window_is_resizing; @@ -83,6 +84,25 @@ void Emulator::Initialize(gfx::IRenderer* renderer, const std::vector& running_ = false; snes_initialized_ = false; + // Initialize audio backend if not already done + if (!audio_backend_) { + audio_backend_ = audio::AudioBackendFactory::Create( + audio::AudioBackendFactory::BackendType::SDL2); + + audio::AudioConfig config; + config.sample_rate = 48000; + config.channels = 2; + config.buffer_frames = 1024; + config.format = audio::SampleFormat::INT16; + + if (!audio_backend_->Initialize(config)) { + LOG_ERROR("Emulator", "Failed to initialize audio backend"); + } else { + LOG_INFO("Emulator", "Audio backend initialized: %s", + audio_backend_->GetBackendName().c_str()); + } + } + // Set up CPU breakpoint callback snes_.cpu().on_breakpoint_hit_ = [this](uint32_t pc) -> bool { return breakpoint_manager_.ShouldBreakOnExecute(pc, BreakpointManager::CpuType::CPU_65816); @@ -107,6 +127,25 @@ void Emulator::Run(Rom* rom) { return; } + // Initialize audio backend if not already done (lazy initialization) + if (!audio_backend_) { + audio_backend_ = audio::AudioBackendFactory::Create( + audio::AudioBackendFactory::BackendType::SDL2); + + audio::AudioConfig config; + config.sample_rate = 48000; + config.channels = 2; + config.buffer_frames = 1024; + config.format = audio::SampleFormat::INT16; + + if (!audio_backend_->Initialize(config)) { + LOG_ERROR("Emulator", "Failed to initialize audio backend"); + } else { + LOG_INFO("Emulator", "Audio backend initialized (lazy): %s", + audio_backend_->GetBackendName().c_str()); + } + } + // Initialize SNES and create PPU texture on first run // This happens lazily when user opens the emulator window if (!snes_initialized_ && rom->is_loaded()) { @@ -217,19 +256,93 @@ void Emulator::Run(Rom* rom) { // Only render and handle audio on the last frame if (should_render) { - // Generate and queue audio samples with improved buffering + // Generate and queue audio samples using audio backend snes_.SetSamples(audio_buffer_, wanted_samples_); - uint32_t queued = SDL_GetQueuedAudioSize(audio_device_); - uint32_t target_buffer = wanted_samples_ * 4 * 2; // Target 2 frames buffered - uint32_t max_buffer = wanted_samples_ * 4 * 6; // Max 6 frames - - if (queued < target_buffer) { - // Buffer is low, queue more audio - SDL_QueueAudio(audio_device_, audio_buffer_, wanted_samples_ * 4); - } else if (queued > max_buffer) { - // Buffer is too full, clear it to prevent lag - SDL_ClearQueuedAudio(audio_device_); - SDL_QueueAudio(audio_device_, audio_buffer_, wanted_samples_ * 4); + + // AUDIO DEBUG: Comprehensive diagnostics at regular intervals + static int audio_debug_counter = 0; + audio_debug_counter++; + + // Log at frames 60 (1sec), 300 (5sec), 600 (10sec), then every 600 frames + bool should_debug = (audio_debug_counter == 60 || audio_debug_counter == 300 || + audio_debug_counter == 600 || (audio_debug_counter % 600 == 0)); + + if (should_debug) { + // Check if buffer exists + if (!audio_buffer_) { + printf("[AUDIO ERROR] audio_buffer_ is NULL!\n"); + } else { + // Check for audio samples + bool has_audio = false; + int16_t max_sample = 0; + int non_zero_count = 0; + for (int i = 0; i < wanted_samples_ * 2 && i < 100; i++) { + if (audio_buffer_[i] != 0) { + has_audio = true; + non_zero_count++; + if (std::abs(audio_buffer_[i]) > std::abs(max_sample)) { + max_sample = audio_buffer_[i]; + } + } + } + + // Backend status + auto audio_status = audio_backend_ ? audio_backend_->GetStatus() : audio::AudioStatus{}; + bool backend_playing = audio_status.is_playing; + + printf("\n[AUDIO DEBUG] Frame=%d (~%.1f sec)\n", audio_debug_counter, audio_debug_counter / 60.0f); + printf(" Backend: %s (Playing: %s)\n", + audio_backend_ ? audio_backend_->GetBackendName().c_str() : "NULL", + backend_playing ? "YES" : "NO"); + printf(" Queued: %u frames\n", audio_status.queued_frames); + printf(" Buffer: wanted_samples=%d, non_zero=%d/%d, max=%d\n", + wanted_samples_, non_zero_count, std::min(wanted_samples_ * 2, 100), max_sample); + printf(" Samples: %s\n", has_audio ? "YES" : "SILENCE"); + + // APU state + if (snes_.running()) { + uint64_t apu_cycles = snes_.apu().GetCycles(); + uint16_t spc_pc = snes_.apu().spc700().PC; + bool ipl_rom_active = (spc_pc >= 0xFFC0 && spc_pc <= 0xFFFF); + + printf(" APU: %llu cycles, PC=$%04X %s\n", + apu_cycles, spc_pc, ipl_rom_active ? "(IPL ROM)" : "(Game Code)"); + + // Handshake status + auto& tracker = snes_.apu_handshake_tracker(); + printf(" Handshake: %s\n", tracker.GetPhaseString().c_str()); + + if (ipl_rom_active && audio_debug_counter > 300) { + printf(" ⚠️ SPC700 STUCK IN IPL ROM - Handshake not completing!\n"); + } + } else { + printf(" ⚠️ SNES not running!\n"); + } + + printf("\n"); + } + } + + // Smart buffer management using audio backend + if (audio_backend_) { + auto status = audio_backend_->GetStatus(); + int num_samples = wanted_samples_ * 2; // Stereo + + if (status.queued_frames < 2) { + // Buffer is low, queue more audio + if (!audio_backend_->QueueSamples(audio_buffer_, num_samples)) { + if (frame_count_ % 300 == 0) { + LOG_WARN("Emulator", "Failed to queue audio samples"); + } + } + } else if (status.queued_frames > 6) { + // Buffer is too full, clear it to prevent lag + audio_backend_->Clear(); + audio_backend_->QueueSamples(audio_buffer_, num_samples); + } else { + // Normal operation - queue samples + audio_backend_->QueueSamples(audio_buffer_, num_samples); + } } // Update PPU texture only on rendered frames @@ -267,6 +380,7 @@ void Emulator::RenderEmulatorInterface() { static bool show_ai_agent_ = false; static bool show_save_states_ = false; static bool show_keyboard_config_ = false; + static bool show_apu_debugger_ = true; // Create session-aware cards gui::EditorCard cpu_card(ICON_MD_MEMORY " CPU Debugger", ICON_MD_MEMORY); @@ -282,6 +396,8 @@ void Emulator::RenderEmulatorInterface() { gui::EditorCard save_states_card(ICON_MD_SAVE " Save States", ICON_MD_SAVE); gui::EditorCard keyboard_card(ICON_MD_KEYBOARD " Keyboard Config", ICON_MD_KEYBOARD); + gui::EditorCard apu_debug_card(ICON_MD_MUSIC_NOTE " APU Debugger", + ICON_MD_MUSIC_NOTE); // Configure default positions static bool cards_configured = false; @@ -310,6 +426,9 @@ void Emulator::RenderEmulatorInterface() { keyboard_card.SetDefaultSize(450, 400); keyboard_card.SetPosition(gui::EditorCard::Position::Floating); + apu_debug_card.SetDefaultSize(500, 400); + apu_debug_card.SetPosition(gui::EditorCard::Position::Floating); + cards_configured = true; } @@ -377,6 +496,14 @@ void Emulator::RenderEmulatorInterface() { keyboard_card.End(); } + // APU Debugger Card + if (show_apu_debugger_) { + if (apu_debug_card.Begin(&show_apu_debugger_)) { + RenderApuDebugger(); + } + apu_debug_card.End(); + } + } catch (const std::exception& e) { // Fallback to basic UI if theming fails ImGui::Text("Error loading emulator UI: %s", e.what()); @@ -504,9 +631,12 @@ void Emulator::RenderNavBar() { } SameLine(); - uint32_t audio_queued = SDL_GetQueuedAudioSize(audio_device_); - uint32_t audio_frames = audio_queued / (wanted_samples_ * 4); - ImGui::Text("| Audio: %u frames", audio_frames); + if (audio_backend_) { + auto audio_status = audio_backend_->GetStatus(); + ImGui::Text("| Audio: %u frames", audio_status.queued_frames); + } else { + ImGui::Text("| Audio: N/A"); + } static bool show_memory_viewer = false; @@ -838,7 +968,7 @@ void Emulator::RenderModernCpuDebugger() { ImGui::TextColored(ConvertColorToImVec4(theme.accent), "CPU Status"); ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); - ImGui::BeginChild("##CpuStatus", ImVec2(0, 120), true); + ImGui::BeginChild("##CpuStatus", ImVec2(0, 200), true); // Compact register display in a table if (ImGui::BeginTable( @@ -920,7 +1050,7 @@ void Emulator::RenderModernCpuDebugger() { ImGui::TextColored(ConvertColorToImVec4(theme.accent), "SPC700 Status"); ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); - ImGui::BeginChild("##SpcStatus", ImVec2(0, 80), true); + ImGui::BeginChild("##SpcStatus", ImVec2(0, 160), true); if (ImGui::BeginTable( "SPCRegisters", 4, @@ -1023,14 +1153,17 @@ void Emulator::RenderPerformanceMonitor() { } // Audio Status - uint32_t audio_queued = SDL_GetQueuedAudioSize(audio_device_); - uint32_t audio_frames = audio_queued / (wanted_samples_ * 4); ImGui::Text("Audio Queue:"); ImGui::SameLine(); - ImVec4 audio_color = (audio_frames >= 2 && audio_frames <= 6) - ? ConvertColorToImVec4(theme.success) - : ConvertColorToImVec4(theme.warning); - ImGui::TextColored(audio_color, "%u frames", audio_frames); + if (audio_backend_) { + auto audio_status = audio_backend_->GetStatus(); + ImVec4 audio_color = (audio_status.queued_frames >= 2 && audio_status.queued_frames <= 6) + ? ConvertColorToImVec4(theme.success) + : ConvertColorToImVec4(theme.warning); + ImGui::TextColored(audio_color, "%u frames", audio_status.queued_frames); + } else { + ImGui::TextColored(ConvertColorToImVec4(theme.error), "No backend"); + } ImGui::NextColumn(); @@ -1415,5 +1548,157 @@ void Emulator::RenderKeyboardConfig() { } } +void Emulator::RenderApuDebugger() { + try { + auto& theme_manager = gui::ThemeManager::Get(); + const auto& theme = theme_manager.GetCurrentTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::BeginChild("##ApuDebugger", ImVec2(0, 0), true); + + // Handshake Status + if (ImGui::CollapsingHeader("APU Handshake Status", ImGuiTreeNodeFlags_DefaultOpen)) { + auto& tracker = snes_.apu_handshake_tracker(); + + // Phase indicator with color + ImGui::Text("Phase:"); + ImGui::SameLine(); + + auto phase_str = tracker.GetPhaseString(); + ImVec4 phase_color; + if (phase_str == "RUNNING") { + phase_color = ConvertColorToImVec4(theme.success); + } else if (phase_str == "TRANSFER_ACTIVE") { + phase_color = ConvertColorToImVec4(theme.info); + } else if (phase_str == "WAITING_BBAA" || phase_str == "IPL_BOOT") { + phase_color = ConvertColorToImVec4(theme.warning); + } else { + phase_color = ConvertColorToImVec4(theme.text_primary); + } + ImGui::TextColored(phase_color, "%s", phase_str.c_str()); + + // Handshake complete indicator + ImGui::Text("Handshake:"); + ImGui::SameLine(); + if (tracker.IsHandshakeComplete()) { + ImGui::TextColored(ConvertColorToImVec4(theme.success), "✓ Complete"); + } else { + ImGui::TextColored(ConvertColorToImVec4(theme.error), "✗ Waiting"); + } + + // Transfer progress + if (tracker.IsTransferActive() || tracker.GetBytesTransferred() > 0) { + ImGui::Text("Bytes Transferred: %d", tracker.GetBytesTransferred()); + ImGui::Text("Blocks: %d", tracker.GetBlockCount()); + + auto progress = tracker.GetTransferProgress(); + if (!progress.empty()) { + ImGui::TextColored(ConvertColorToImVec4(theme.info), "%s", progress.c_str()); + } + } + + // Status summary + ImGui::Separator(); + ImGui::TextWrapped("%s", tracker.GetStatusSummary().c_str()); + } + + // Port Activity Log + if (ImGui::CollapsingHeader("Port Activity Log")) { + ImGui::BeginChild("##PortLog", ImVec2(0, 200), true); + + auto& tracker = snes_.apu_handshake_tracker(); + const auto& history = tracker.GetPortHistory(); + + if (history.empty()) { + ImGui::TextColored(ConvertColorToImVec4(theme.text_disabled), + "No port activity yet"); + } else { + // Show last 50 entries (most recent at bottom) + int start_idx = std::max(0, static_cast(history.size()) - 50); + for (size_t i = start_idx; i < history.size(); ++i) { + const auto& entry = history[i]; + + ImVec4 color = entry.is_cpu ? ConvertColorToImVec4(theme.accent) + : ConvertColorToImVec4(theme.info); + + ImGui::TextColored(color, "[%04llu] %s F%d = $%02X @ PC=$%04X %s", + entry.timestamp, + entry.is_cpu ? "CPU→" : "SPC→", + entry.port + 4, + entry.value, + entry.pc, + entry.description.c_str()); + } + + // Auto-scroll to bottom + if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { + ImGui::SetScrollHereY(1.0f); + } + } + + ImGui::EndChild(); + } + + // Current Port Values + if (ImGui::CollapsingHeader("Current Port Values", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::BeginTable("APU_Ports", 4, ImGuiTableFlags_Borders)) { + ImGui::TableSetupColumn("Port"); + ImGui::TableSetupColumn("CPU → SPC"); + ImGui::TableSetupColumn("SPC → CPU"); + ImGui::TableSetupColumn("Addr"); + ImGui::TableHeadersRow(); + + for (int i = 0; i < 4; ++i) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("F%d", i + 4); + + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.accent), "$%02X", + snes_.apu().in_ports_[i]); + + ImGui::TableNextColumn(); + ImGui::TextColored(ConvertColorToImVec4(theme.info), "$%02X", + snes_.apu().out_ports_[i]); + + ImGui::TableNextColumn(); + ImGui::TextDisabled("$214%d / $F%d", i, i + 4); + } + + ImGui::EndTable(); + } + } + + // Quick Actions + if (ImGui::CollapsingHeader("Quick Actions")) { + if (ImGui::Button("Force Handshake ($CC)", ImVec2(-1, 30))) { + snes_.Write(0x002140, 0xCC); + LOG_INFO("APU_DEBUG", "Manually forced handshake by writing $CC to F4"); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Manually trigger CPU handshake (for testing)"); + } + + if (ImGui::Button("Reset APU", ImVec2(-1, 30))) { + snes_.apu().Reset(); + LOG_INFO("APU_DEBUG", "APU manually reset"); + } + + if (ImGui::Button("Clear Port History", ImVec2(-1, 30))) { + snes_.apu_handshake_tracker().Reset(); + LOG_INFO("APU_DEBUG", "Port history cleared"); + } + } + + ImGui::EndChild(); + ImGui::PopStyleColor(); + } catch (const std::exception& e) { + try { + ImGui::PopStyleColor(); + } catch (...) {} + ImGui::Text("APU Debugger Error: %s", e.what()); + } +} + } // namespace emu } // namespace yaze diff --git a/src/app/emu/emulator.h b/src/app/emu/emulator.h index 8227d96b..7bfeaab3 100644 --- a/src/app/emu/emulator.h +++ b/src/app/emu/emulator.h @@ -5,6 +5,7 @@ #include #include "app/emu/snes.h" +#include "app/emu/audio/audio_backend.h" #include "app/emu/debug/breakpoint_manager.h" #include "app/emu/debug/disassembly_viewer.h" #include "app/rom.h" @@ -49,6 +50,9 @@ class Emulator { auto snes() -> Snes& { return snes_; } auto running() const -> bool { return running_; } + + // Audio backend access + audio::IAudioBackend* audio_backend() { return audio_backend_.get(); } void set_audio_buffer(int16_t* audio_buffer) { audio_buffer_ = audio_buffer; } auto set_audio_device_id(SDL_AudioDeviceID audio_device) { audio_device_ = audio_device; @@ -105,6 +109,7 @@ class Emulator { void RenderAIAgentPanel(); void RenderSaveStates(); void RenderKeyboardConfig(); + void RenderApuDebugger(); struct Bookmark { std::string name; @@ -139,6 +144,9 @@ class Emulator { int16_t* audio_buffer_; SDL_AudioDeviceID audio_device_; + + // Audio backend abstraction + std::unique_ptr audio_backend_; Snes snes_; bool initialized_ = false; diff --git a/src/app/emu/snes.cc b/src/app/emu/snes.cc index 80340c4e..cdf4508e 100644 --- a/src/app/emu/snes.cc +++ b/src/app/emu/snes.cc @@ -36,6 +36,9 @@ void Snes::Init(std::vector& rom_data) { // Initialize the CPU, PPU, and APU ppu_.Init(); apu_.Init(); + + // Connect handshake tracker to APU for debugging + apu_.set_handshake_tracker(&apu_handshake_tracker_); // Load the ROM into memory and set up the memory mapping memory_.Initialize(rom_data); @@ -419,6 +422,11 @@ 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", diff --git a/src/app/emu/snes.h b/src/app/emu/snes.h index b44bacb9..c40b9288 100644 --- a/src/app/emu/snes.h +++ b/src/app/emu/snes.h @@ -7,6 +7,7 @@ #include "app/emu/audio/apu.h" #include "app/emu/cpu/cpu.h" +#include "app/emu/debug/apu_debugger.h" #include "app/emu/memory/memory.h" #include "app/emu/video/ppu.h" @@ -69,6 +70,9 @@ class Snes { auto memory() -> MemoryImpl& { return memory_; } auto get_ram() -> uint8_t* { return ram; } auto mutable_cycles() -> uint64_t& { return cycles_; } + + // Audio debugging + auto apu_handshake_tracker() -> debug::ApuHandshakeTracker& { return apu_handshake_tracker_; } bool fast_mem_ = false; @@ -117,6 +121,9 @@ class Snes { bool auto_joy_read_ = false; uint16_t auto_joy_timer_ = 0; bool ppu_latch_; + + // Audio debugging + debug::ApuHandshakeTracker apu_handshake_tracker_; }; } // namespace emu