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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]",
|
||||
|
||||
@@ -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); },
|
||||
|
||||
201
src/app/emu/audio/audio_backend.cc
Normal file
201
src/app/emu/audio/audio_backend.cc
Normal 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
|
||||
|
||||
128
src/app/emu/audio/audio_backend.h
Normal file
128
src/app/emu/audio/audio_backend.h
Normal 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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
215
src/app/emu/debug/apu_debugger.cc
Normal file
215
src/app/emu/debug/apu_debugger.cc
Normal 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
|
||||
|
||||
101
src/app/emu/debug/apu_debugger.h
Normal file
101
src/app/emu/debug/apu_debugger.h
Normal 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user