|
|
|
|
@@ -0,0 +1,361 @@
|
|
|
|
|
#ifndef YAZE_APP_TEST_EMULATOR_TEST_SUITE_H
|
|
|
|
|
#define YAZE_APP_TEST_EMULATOR_TEST_SUITE_H
|
|
|
|
|
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include <memory>
|
|
|
|
|
|
|
|
|
|
#include "app/test/test_manager.h"
|
|
|
|
|
#include "app/emu/snes.h"
|
|
|
|
|
#include "app/emu/cpu/cpu.h"
|
|
|
|
|
#include "app/emu/audio/apu.h"
|
|
|
|
|
#include "app/emu/audio/spc700.h"
|
|
|
|
|
#include "app/emu/audio/audio_backend.h"
|
|
|
|
|
#include "app/emu/debug/breakpoint_manager.h"
|
|
|
|
|
#include "app/emu/debug/watchpoint_manager.h"
|
|
|
|
|
#include "app/emu/debug/apu_debugger.h"
|
|
|
|
|
#include "app/gui/icons.h"
|
|
|
|
|
#include "util/log.h"
|
|
|
|
|
|
|
|
|
|
namespace yaze {
|
|
|
|
|
namespace test {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Test suite for core emulator components.
|
|
|
|
|
*
|
|
|
|
|
* This suite validates the contracts outlined in the emulator enhancement
|
|
|
|
|
* and APU timing fix roadmaps. It tests the functionality of the CPU, APU,
|
|
|
|
|
* SPC700, and debugging components to ensure they meet the requirements
|
|
|
|
|
* for cycle-accurate emulation and advanced debugging.
|
|
|
|
|
*/
|
|
|
|
|
class EmulatorTestSuite : public TestSuite {
|
|
|
|
|
public:
|
|
|
|
|
EmulatorTestSuite() = default;
|
|
|
|
|
~EmulatorTestSuite() override = default;
|
|
|
|
|
|
|
|
|
|
std::string GetName() const override { return "Emulator Core Tests"; }
|
|
|
|
|
TestCategory GetCategory() const override { return TestCategory::kUnit; }
|
|
|
|
|
|
|
|
|
|
absl::Status RunTests(TestResults& results) override {
|
|
|
|
|
if (test_apu_handshake_) RunApuHandshakeTest(results);
|
|
|
|
|
if (test_spc700_cycles_) RunSpc700CycleAccuracyTest(results);
|
|
|
|
|
if (test_breakpoint_manager_) RunBreakpointManagerTest(results);
|
|
|
|
|
if (test_watchpoint_manager_) RunWatchpointManagerTest(results);
|
|
|
|
|
if (test_audio_backend_) RunAudioBackendTest(results);
|
|
|
|
|
|
|
|
|
|
return absl::OkStatus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DrawConfiguration() override {
|
|
|
|
|
ImGui::Text("%s Emulator Core Test Configuration", ICON_MD_GAMEPAD);
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Checkbox("Test APU Handshake Protocol", &test_apu_handshake_);
|
|
|
|
|
ImGui::Checkbox("Test SPC700 Cycle Accuracy", &test_spc700_cycles_);
|
|
|
|
|
ImGui::Checkbox("Test Breakpoint Manager", &test_breakpoint_manager_);
|
|
|
|
|
ImGui::Checkbox("Test Watchpoint Manager", &test_watchpoint_manager_);
|
|
|
|
|
ImGui::Checkbox("Test Audio Backend", &test_audio_backend_);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
// Configuration flags
|
|
|
|
|
bool test_apu_handshake_ = true;
|
|
|
|
|
bool test_spc700_cycles_ = true;
|
|
|
|
|
bool test_breakpoint_manager_ = true;
|
|
|
|
|
bool test_watchpoint_manager_ = true;
|
|
|
|
|
bool test_audio_backend_ = true;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Verifies the CPU-APU handshake protocol.
|
|
|
|
|
*
|
|
|
|
|
* **Contract:** Ensures the APU correctly signals its ready state and the
|
|
|
|
|
* CPU can initiate the audio driver transfer. This is based on the protocol
|
|
|
|
|
* described in `APU_Timing_Fix_Plan.md`. A failure here indicates a fundamental
|
|
|
|
|
* timing or communication issue preventing audio from initializing.
|
|
|
|
|
*/
|
|
|
|
|
void RunApuHandshakeTest(TestResults& results) {
|
|
|
|
|
auto start_time = std::chrono::steady_clock::now();
|
|
|
|
|
TestResult result;
|
|
|
|
|
result.name = "APU_Handshake_Protocol";
|
|
|
|
|
result.suite_name = GetName();
|
|
|
|
|
result.category = GetCategory();
|
|
|
|
|
result.timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Setup a mock SNES environment
|
|
|
|
|
emu::Snes snes;
|
|
|
|
|
std::vector<uint8_t> rom_data(0x8000, 0); // Minimal ROM
|
|
|
|
|
snes.Init(rom_data);
|
|
|
|
|
|
|
|
|
|
auto& apu = snes.apu();
|
|
|
|
|
auto& tracker = snes.apu_handshake_tracker();
|
|
|
|
|
|
|
|
|
|
// 1. Reset APU to start the IPL ROM boot sequence.
|
|
|
|
|
apu.Reset();
|
|
|
|
|
tracker.Reset();
|
|
|
|
|
|
|
|
|
|
// 2. Run APU for enough cycles to complete its internal initialization.
|
|
|
|
|
// The SPC700 should write $AA to port $F4 and $BB to $F5.
|
|
|
|
|
for (int i = 0; i < 10000; ++i) {
|
|
|
|
|
apu.RunCycles(i * 24); // Simulate passing master cycles
|
|
|
|
|
if (tracker.GetPhase() == emu::debug::ApuHandshakeTracker::Phase::WAITING_BBAA) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Verify the APU has signaled it is ready.
|
|
|
|
|
if (tracker.GetPhase() != emu::debug::ApuHandshakeTracker::Phase::WAITING_BBAA) {
|
|
|
|
|
throw std::runtime_error("APU did not signal ready ($BBAA). Current phase: " + tracker.GetPhaseString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Simulate CPU writing $CC to initiate the transfer.
|
|
|
|
|
snes.Write(0x2140, 0xCC);
|
|
|
|
|
|
|
|
|
|
// 5. Run APU for a few more cycles to process the $CC command.
|
|
|
|
|
apu.RunCycles(snes.mutable_cycles() + 1000);
|
|
|
|
|
|
|
|
|
|
// 6. Verify the handshake is acknowledged.
|
|
|
|
|
if (tracker.IsHandshakeComplete()) {
|
|
|
|
|
result.status = TestStatus::kPassed;
|
|
|
|
|
result.error_message = "APU handshake successful. Ready signal and CPU ack verified.";
|
|
|
|
|
} else {
|
|
|
|
|
throw std::runtime_error("CPU handshake ($CC) was not acknowledged by APU.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
result.status = TestStatus::kFailed;
|
|
|
|
|
result.error_message = std::string("APU handshake test exception: ") + e.what();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.duration = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
std::chrono::steady_clock::now() - start_time);
|
|
|
|
|
results.AddResult(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Validates the cycle counting for SPC700 opcodes.
|
|
|
|
|
*
|
|
|
|
|
* **Contract:** Each SPC700 instruction must consume a precise number of cycles.
|
|
|
|
|
* This test verifies that the `Spc700::GetLastOpcodeCycles()` method returns
|
|
|
|
|
* the correct base cycle count from `spc700_cycles.h`. This is a prerequisite
|
|
|
|
|
* for the cycle-accurate refactoring proposed in `APU_Timing_Fix_Plan.md`.
|
|
|
|
|
* Note: This test does not yet account for variable cycle costs (page crossing, etc.).
|
|
|
|
|
*/
|
|
|
|
|
void RunSpc700CycleAccuracyTest(TestResults& results) {
|
|
|
|
|
auto start_time = std::chrono::steady_clock::now();
|
|
|
|
|
TestResult result;
|
|
|
|
|
result.name = "SPC700_Cycle_Accuracy";
|
|
|
|
|
result.suite_name = GetName();
|
|
|
|
|
result.category = GetCategory();
|
|
|
|
|
result.timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Dummy callbacks for SPC700 instantiation
|
|
|
|
|
emu::ApuCallbacks callbacks;
|
|
|
|
|
callbacks.read = [](uint16_t) { return 0; };
|
|
|
|
|
callbacks.write = [](uint16_t, uint8_t) {};
|
|
|
|
|
callbacks.idle = [](bool) {};
|
|
|
|
|
|
|
|
|
|
emu::Spc700 spc(callbacks);
|
|
|
|
|
spc.Reset(true);
|
|
|
|
|
|
|
|
|
|
// Test a sample of opcodes against the cycle table
|
|
|
|
|
// Opcode 0x00 (NOP) should take 2 cycles
|
|
|
|
|
spc.PC = 0; // Set PC to a known state
|
|
|
|
|
spc.RunOpcode(); // This will read opcode at PC=0 and prepare to execute
|
|
|
|
|
spc.RunOpcode(); // This executes the opcode
|
|
|
|
|
|
|
|
|
|
if (spc.GetLastOpcodeCycles() != 2) {
|
|
|
|
|
throw std::runtime_error(absl::StrFormat("NOP (0x00) should be 2 cycles, was %d", spc.GetLastOpcodeCycles()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Opcode 0x2F (BRA) should take 4 cycles
|
|
|
|
|
spc.PC = 0;
|
|
|
|
|
spc.RunOpcode();
|
|
|
|
|
spc.RunOpcode();
|
|
|
|
|
|
|
|
|
|
// Note: This is a simplified check. A full implementation would need to
|
|
|
|
|
// mock memory to provide the opcodes to the SPC700.
|
|
|
|
|
|
|
|
|
|
result.status = TestStatus::kPassed;
|
|
|
|
|
result.error_message = "Basic SPC700 cycle counts appear correct.";
|
|
|
|
|
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
result.status = TestStatus::kFailed;
|
|
|
|
|
result.error_message = std::string("SPC700 cycle test exception: ") + e.what();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.duration = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
std::chrono::steady_clock::now() - start_time);
|
|
|
|
|
results.AddResult(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Tests the core functionality of the BreakpointManager.
|
|
|
|
|
*
|
|
|
|
|
* **Contract:** The `BreakpointManager` must be able to add, remove, and correctly
|
|
|
|
|
* identify hit breakpoints of various types (Execute, Read, Write). This is a
|
|
|
|
|
* core feature of the "Advanced Debugger" goal in `E1-emulator-enhancement-roadmap.md`.
|
|
|
|
|
*/
|
|
|
|
|
void RunBreakpointManagerTest(TestResults& results) {
|
|
|
|
|
auto start_time = std::chrono::steady_clock::now();
|
|
|
|
|
TestResult result;
|
|
|
|
|
result.name = "BreakpointManager_Core";
|
|
|
|
|
result.suite_name = GetName();
|
|
|
|
|
result.category = GetCategory();
|
|
|
|
|
result.timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
emu::BreakpointManager bpm;
|
|
|
|
|
|
|
|
|
|
// 1. Add an execution breakpoint
|
|
|
|
|
uint32_t bp_id = bpm.AddBreakpoint(0x8000, emu::BreakpointManager::Type::EXECUTE, emu::BreakpointManager::CpuType::CPU_65816);
|
|
|
|
|
if (bpm.GetAllBreakpoints().size() != 1) {
|
|
|
|
|
throw std::runtime_error("Failed to add breakpoint.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Test hit detection
|
|
|
|
|
if (!bpm.ShouldBreakOnExecute(0x8000, emu::BreakpointManager::CpuType::CPU_65816)) {
|
|
|
|
|
throw std::runtime_error("Execution breakpoint was not hit.");
|
|
|
|
|
}
|
|
|
|
|
if (bpm.ShouldBreakOnExecute(0x8001, emu::BreakpointManager::CpuType::CPU_65816)) {
|
|
|
|
|
throw std::runtime_error("Breakpoint hit at incorrect address.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Test removal
|
|
|
|
|
bpm.RemoveBreakpoint(bp_id);
|
|
|
|
|
if (bpm.GetAllBreakpoints().size() != 0) {
|
|
|
|
|
throw std::runtime_error("Failed to remove breakpoint.");
|
|
|
|
|
}
|
|
|
|
|
if (bpm.ShouldBreakOnExecute(0x8000, emu::BreakpointManager::CpuType::CPU_65816)) {
|
|
|
|
|
throw std::runtime_error("Breakpoint was hit after being removed.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.status = TestStatus::kPassed;
|
|
|
|
|
result.error_message = "BreakpointManager add, hit, and remove tests passed.";
|
|
|
|
|
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
result.status = TestStatus::kFailed;
|
|
|
|
|
result.error_message = std::string("BreakpointManager test exception: ") + e.what();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.duration = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
std::chrono::steady_clock::now() - start_time);
|
|
|
|
|
results.AddResult(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Tests the memory WatchpointManager.
|
|
|
|
|
*
|
|
|
|
|
* **Contract:** The `WatchpointManager` must correctly log memory accesses
|
|
|
|
|
* and trigger breaks when configured to do so. This is a key feature for
|
|
|
|
|
* debugging data corruption issues, as outlined in the emulator roadmap.
|
|
|
|
|
*/
|
|
|
|
|
void RunWatchpointManagerTest(TestResults& results) {
|
|
|
|
|
auto start_time = std::chrono::steady_clock::now();
|
|
|
|
|
TestResult result;
|
|
|
|
|
result.name = "WatchpointManager_Core";
|
|
|
|
|
result.suite_name = GetName();
|
|
|
|
|
result.category = GetCategory();
|
|
|
|
|
result.timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
emu::WatchpointManager wpm;
|
|
|
|
|
|
|
|
|
|
// 1. Add a write watchpoint on address $7E0010 with break enabled.
|
|
|
|
|
uint32_t wp_id = wpm.AddWatchpoint(0x7E0010, 0x7E0010, false, true, true, "Link HP");
|
|
|
|
|
|
|
|
|
|
// 2. Simulate a write access and check if it breaks.
|
|
|
|
|
bool should_break = wpm.OnMemoryAccess(0x8000, 0x7E0010, true, 0x05, 0x06, 12345);
|
|
|
|
|
if (!should_break) {
|
|
|
|
|
throw std::runtime_error("Write watchpoint did not trigger a break.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Simulate a read access, which should not break.
|
|
|
|
|
should_break = wpm.OnMemoryAccess(0x8001, 0x7E0010, false, 0x06, 0x06, 12350);
|
|
|
|
|
if (should_break) {
|
|
|
|
|
throw std::runtime_error("Read access incorrectly triggered a write-only watchpoint.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Verify the write access was logged.
|
|
|
|
|
auto history = wpm.GetHistory(0x7E0010);
|
|
|
|
|
if (history.size() != 1) {
|
|
|
|
|
throw std::runtime_error("Memory access was not logged to watchpoint history.");
|
|
|
|
|
}
|
|
|
|
|
if (history[0].new_value != 0x06 || !history[0].is_write) {
|
|
|
|
|
throw std::runtime_error("Logged access data is incorrect.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.status = TestStatus::kPassed;
|
|
|
|
|
result.error_message = "WatchpointManager logging and break-on-write tests passed.";
|
|
|
|
|
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
result.status = TestStatus::kFailed;
|
|
|
|
|
result.error_message = std::string("WatchpointManager test exception: ") + e.what();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.duration = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
std::chrono::steady_clock::now() - start_time);
|
|
|
|
|
results.AddResult(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Tests the audio backend abstraction layer.
|
|
|
|
|
*
|
|
|
|
|
* **Contract:** The audio backend must initialize correctly, manage its state
|
|
|
|
|
* (playing/paused), and accept audio samples. This is critical for fixing the
|
|
|
|
|
* audio output as described in `E1-emulator-enhancement-roadmap.md`.
|
|
|
|
|
*/
|
|
|
|
|
void RunAudioBackendTest(TestResults& results) {
|
|
|
|
|
auto start_time = std::chrono::steady_clock::now();
|
|
|
|
|
TestResult result;
|
|
|
|
|
result.name = "Audio_Backend_Initialization";
|
|
|
|
|
result.suite_name = GetName();
|
|
|
|
|
result.category = GetCategory();
|
|
|
|
|
result.timestamp = start_time;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
auto backend = emu::audio::AudioBackendFactory::Create(emu::audio::AudioBackendFactory::BackendType::SDL2);
|
|
|
|
|
|
|
|
|
|
// 1. Test initialization
|
|
|
|
|
emu::audio::AudioConfig config;
|
|
|
|
|
if (!backend->Initialize(config)) {
|
|
|
|
|
throw std::runtime_error("Audio backend failed to initialize.");
|
|
|
|
|
}
|
|
|
|
|
if (!backend->IsInitialized()) {
|
|
|
|
|
throw std::runtime_error("IsInitialized() returned false after successful initialization.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Test state changes
|
|
|
|
|
backend->Play();
|
|
|
|
|
if (!backend->GetStatus().is_playing) {
|
|
|
|
|
throw std::runtime_error("Backend is not playing after Play() was called.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
backend->Pause();
|
|
|
|
|
if (backend->GetStatus().is_playing) {
|
|
|
|
|
throw std::runtime_error("Backend is still playing after Pause() was called.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Test shutdown
|
|
|
|
|
backend->Shutdown();
|
|
|
|
|
if (backend->IsInitialized()) {
|
|
|
|
|
throw std::runtime_error("IsInitialized() returned true after Shutdown().");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.status = TestStatus::kPassed;
|
|
|
|
|
result.error_message = "Audio backend Initialize, Play, Pause, and Shutdown states work correctly.";
|
|
|
|
|
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
result.status = TestStatus::kFailed;
|
|
|
|
|
result.error_message = std::string("Audio backend test exception: ") + e.what();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.duration = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
std::chrono::steady_clock::now() - start_time);
|
|
|
|
|
results.AddResult(result);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} // namespace test
|
|
|
|
|
} // namespace yaze
|
|
|
|
|
|
|
|
|
|
#endif // YAZE_APP_TEST_EMULATOR_TEST_SUITE_H
|