feat: Implement audio backend and APU debugging features

- Introduced a new audio backend system with SDL2 support, allowing for flexible audio management and improved performance.
- Added APU handshake tracking capabilities to monitor CPU-APU communication during audio program uploads, enhancing debugging and transfer diagnostics.
- Updated the Emulator class to integrate the new audio backend, ensuring compatibility with existing audio handling.
- Implemented an APU Debugger UI for real-time monitoring of handshake status, port activity, and transfer progress, improving user experience for debugging audio issues.
- Refactored audio-related code to streamline audio sample queuing and management, enhancing overall emulator performance.
This commit is contained in:
scawful
2025-10-08 20:57:43 -04:00
parent 7f4a0f546c
commit 3125ff4b76
14 changed files with 1057 additions and 122 deletions

View File

@@ -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;

View File

@@ -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<int16_t>(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<int16_t>(
new int16_t[buffer_size],
std::default_delete<int16_t[]>());
// 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<float>(event.window.data1);
io.DisplaySize.y = static_cast<float>(event.window.data2);
g_window_is_resizing = true;
core::g_window_is_resizing = true;
break;
case SDL_WINDOWEVENT_MINIMIZED:
case SDL_WINDOWEVENT_HIDDEN:

View File

@@ -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]",

View File

@@ -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, 3> timer_;
// Audio debugging
debug::ApuHandshakeTracker* handshake_tracker_ = nullptr;
ApuCallbacks callbacks_ = {
[&](uint16_t adr, uint8_t val) { SpcWrite(adr, val); },

View File

@@ -0,0 +1,201 @@
// audio_backend.cc - Audio Backend Implementation
#include "app/emu/audio/audio_backend.h"
#include <SDL.h>
#include <algorithm>
#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<int16_t> scaled_samples(num_samples);
for (int i = 0; i < num_samples; ++i) {
int32_t scaled = static_cast<int32_t>(samples[i] * volume_);
scaled_samples[i] = static_cast<int16_t>(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<int16_t> 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<int16_t>(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<IAudioBackend> AudioBackendFactory::Create(BackendType type) {
switch (type) {
case BackendType::SDL2:
return std::make_unique<SDL2AudioBackend>();
case BackendType::NULL_BACKEND:
// TODO: Implement null backend for testing
LOG_WARN("AudioBackend", "NULL backend not yet implemented, using SDL2");
return std::make_unique<SDL2AudioBackend>();
default:
LOG_ERROR("AudioBackend", "Unknown backend type, using SDL2");
return std::make_unique<SDL2AudioBackend>();
}
}
} // namespace audio
} // namespace emu
} // namespace yaze

View File

@@ -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 <cstdint>
#include <memory>
#include <string>
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<IAudioBackend> Create(BackendType type);
};
} // namespace audio
} // namespace emu
} // namespace yaze
#endif // YAZE_APP_EMU_AUDIO_AUDIO_BACKEND_H

View File

@@ -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<int>(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<int>(PB) << ":" << std::hex << PC;
std::cout << " \033[1;32m"
<< ": 0x" << std::hex << std::uppercase << std::setw(2)
<< std::setfill('0') << static_cast<int>(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<int>(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<int>(A);
std::cout << " X:" << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(X);
std::cout << " Y:" << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(Y);
std::cout << " S:" << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(status);
std::cout << " DB:" << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(DB);
std::cout << " D:" << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(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());
}
}

View File

@@ -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<uint16_t>(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

View File

@@ -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 <cstdint>
#include <string>
#include <vector>
#include <deque>
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<PortWrite>& GetPortHistory() const { return port_history_; }
const std::vector<TransferBlock>& 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<TransferBlock> blocks_;
std::deque<PortWrite> 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

View File

@@ -5,6 +5,7 @@
#include <vector>
#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<uint8_t>&
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<int>(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

View File

@@ -5,6 +5,7 @@
#include <vector>
#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::IAudioBackend> audio_backend_;
Snes snes_;
bool initialized_ = false;

View File

@@ -36,6 +36,9 @@ void Snes::Init(std::vector<uint8_t>& 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<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",

View File

@@ -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