Refactor Snes class and enhance Apu functionality
- Updated Snes constructor to initialize CPU callbacks directly, improving readability and maintainability. - Removed unnecessary CpuCallbacks parameter from Cpu constructor, streamlining the class design. - Added new methods in Apu for retrieving cycles, status, control, and handling DMA transfers, enhancing audio processing capabilities. - Introduced unit tests for Apu to validate initialization, sample generation, and handshake timing, ensuring robust audio functionality.
This commit is contained in:
@@ -65,6 +65,18 @@ class Apu {
|
||||
auto dsp() -> Dsp & { return dsp_; }
|
||||
auto spc700() -> Spc700 & { return spc700_; }
|
||||
|
||||
uint64_t GetCycles() const { return cycles_; }
|
||||
uint8_t GetStatus() const { return ram[0x00]; }
|
||||
uint8_t GetControl() const { return ram[0x01]; }
|
||||
void GetSamples(int16_t *buffer, int count, bool loop = false) {
|
||||
dsp_.GetSamples(buffer, count, loop);
|
||||
}
|
||||
void WriteDma(uint16_t address, const uint8_t *data, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
ram[address + i] = data[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Port buffers (equivalent to $2140 to $2143 for the main CPU)
|
||||
std::array<uint8_t, 6> in_ports_; // includes 2 bytes of ram
|
||||
std::array<uint8_t, 4> out_ports_;
|
||||
|
||||
@@ -31,10 +31,12 @@ class InstructionEntry {
|
||||
|
||||
class Cpu {
|
||||
public:
|
||||
explicit Cpu(Memory& mem, CpuCallbacks& callbacks)
|
||||
: memory(mem), callbacks_(callbacks) {}
|
||||
explicit Cpu(Memory& mem) : memory(mem) {}
|
||||
void Reset(bool hard = false);
|
||||
|
||||
auto& callbacks() { return callbacks_; }
|
||||
const auto& callbacks() const { return callbacks_; }
|
||||
|
||||
void RunOpcode();
|
||||
|
||||
void ExecuteInstruction(uint8_t opcode);
|
||||
@@ -781,8 +783,8 @@ class Cpu {
|
||||
bool int_wanted_ = false;
|
||||
bool int_delay_ = false;
|
||||
|
||||
CpuCallbacks callbacks_;
|
||||
Memory& memory;
|
||||
CpuCallbacks callbacks_;
|
||||
};
|
||||
|
||||
} // namespace emu
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#define YAZE_APP_EMU_SNES_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/emu/cpu/cpu.h"
|
||||
@@ -21,7 +23,11 @@ struct Input {
|
||||
|
||||
class Snes {
|
||||
public:
|
||||
Snes() = default;
|
||||
Snes() {
|
||||
cpu_.callbacks().read_byte = [this](uint32_t adr) { return CpuRead(adr); };
|
||||
cpu_.callbacks().write_byte = [this](uint32_t adr, uint8_t val) { CpuWrite(adr, val); };
|
||||
cpu_.callbacks().idle = [this](bool waiting) { CpuIdle(waiting); };
|
||||
}
|
||||
~Snes() = default;
|
||||
|
||||
void Init(std::vector<uint8_t>& rom_data);
|
||||
@@ -61,14 +67,11 @@ class Snes {
|
||||
auto get_ram() -> uint8_t* { return ram; }
|
||||
auto mutable_cycles() -> uint64_t& { return cycles_; }
|
||||
|
||||
bool fast_mem_ = false;
|
||||
|
||||
private:
|
||||
MemoryImpl memory_;
|
||||
CpuCallbacks cpu_callbacks_ = {
|
||||
[&](uint32_t adr) { return CpuRead(adr); },
|
||||
[&](uint32_t adr, uint8_t val) { CpuWrite(adr, val); },
|
||||
[&](bool waiting) { CpuIdle(waiting); },
|
||||
};
|
||||
Cpu cpu_{memory_, cpu_callbacks_};
|
||||
Cpu cpu_{memory_};
|
||||
Ppu ppu_{memory_};
|
||||
Apu apu_{memory_};
|
||||
|
||||
@@ -111,12 +114,9 @@ class Snes {
|
||||
bool auto_joy_read_ = false;
|
||||
uint16_t auto_joy_timer_ = 0;
|
||||
bool ppu_latch_;
|
||||
|
||||
bool fast_mem_ = false;
|
||||
};
|
||||
|
||||
} // namespace emu
|
||||
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EMU_SNES_H
|
||||
|
||||
@@ -24,6 +24,11 @@ add_executable(
|
||||
zelda3/message_test.cc
|
||||
zelda3/overworld_test.cc
|
||||
zelda3/sprite_builder_test.cc
|
||||
emu/cpu_test.cc
|
||||
emu/ppu_test.cc
|
||||
emu/spc700_test.cc
|
||||
emu/audio/apu_test.cc
|
||||
emu/audio/ipl_handshake_test.cc
|
||||
integration/dungeon_editor_test.cc
|
||||
zelda3/object_parser_test.cc
|
||||
zelda3/object_parser_structs_test.cc
|
||||
|
||||
134
test/emu/audio/apu_test.cc
Normal file
134
test/emu/audio/apu_test.cc
Normal file
@@ -0,0 +1,134 @@
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
|
||||
#include <gmock/gmock-nice-strict.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using testing::_;
|
||||
using testing::Return;
|
||||
using yaze::emu::Apu;
|
||||
using yaze::emu::MemoryImpl;
|
||||
|
||||
class ApuTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
memory_ = std::make_unique<MemoryImpl>();
|
||||
apu_ = std::make_unique<Apu>(*memory_);
|
||||
apu_->Init();
|
||||
}
|
||||
|
||||
std::unique_ptr<MemoryImpl> memory_;
|
||||
std::unique_ptr<Apu> apu_;
|
||||
};
|
||||
|
||||
// Test the IPL ROM handshake sequence timing
|
||||
TEST_F(ApuTest, IplRomHandshakeTiming) {
|
||||
// 1. Initial state check
|
||||
EXPECT_EQ(apu_->Read(0x00) & 0x80, 0); // Ready bit should be clear
|
||||
|
||||
// 2. Start handshake
|
||||
apu_->Write(0x00, 0x80); // Set control register bit 7
|
||||
|
||||
// 3. Wait for APU ready signal with cycle counting
|
||||
int cycles = 0;
|
||||
const int max_cycles = 1000; // Maximum expected cycles for handshake
|
||||
while (!(apu_->Read(0x00) & 0x80) && cycles < max_cycles) {
|
||||
apu_->RunCycles(1);
|
||||
cycles++;
|
||||
}
|
||||
|
||||
// 4. Verify timing constraints
|
||||
EXPECT_LT(cycles, max_cycles); // Should complete within max cycles
|
||||
EXPECT_GT(cycles, 0); // Should take some cycles
|
||||
EXPECT_TRUE(apu_->Read(0x00) & 0x80); // Ready bit should be set
|
||||
|
||||
// 5. Verify handshake completion
|
||||
EXPECT_EQ(apu_->GetStatus() & 0x80, 0x80); // Ready bit in status register
|
||||
}
|
||||
|
||||
// Test APU initialization sequence
|
||||
TEST_F(ApuTest, ApuInitialization) {
|
||||
// 1. Check initial state
|
||||
EXPECT_EQ(apu_->GetStatus(), 0x00);
|
||||
EXPECT_EQ(apu_->GetControl(), 0x00);
|
||||
|
||||
// 2. Initialize APU
|
||||
apu_->Init();
|
||||
|
||||
// 3. Verify initialization
|
||||
EXPECT_EQ(apu_->GetStatus(), 0x00);
|
||||
EXPECT_EQ(apu_->GetControl(), 0x00);
|
||||
|
||||
// 4. Check DSP registers are initialized
|
||||
for (int i = 0; i < 128; i++) {
|
||||
EXPECT_EQ(apu_->Read(0x00 + i), 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
// Test sample generation and timing
|
||||
TEST_F(ApuTest, SampleGenerationTiming) {
|
||||
// 1. Generate samples
|
||||
const int sample_count = 1024;
|
||||
std::vector<int16_t> samples(sample_count);
|
||||
|
||||
// 2. Measure timing
|
||||
uint64_t start_cycles = apu_->GetCycles();
|
||||
apu_->GetSamples(samples.data(), sample_count, false);
|
||||
uint64_t end_cycles = apu_->GetCycles();
|
||||
|
||||
// 3. Verify timing
|
||||
EXPECT_GT(end_cycles - start_cycles, 0);
|
||||
|
||||
// 4. Verify samples
|
||||
bool has_non_zero = false;
|
||||
for (int i = 0; i < sample_count; ++i) {
|
||||
if (samples[i] != 0) {
|
||||
has_non_zero = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(has_non_zero);
|
||||
}
|
||||
|
||||
// Test DSP register access timing
|
||||
TEST_F(ApuTest, DspRegisterAccessTiming) {
|
||||
// 1. Write to DSP registers
|
||||
const uint8_t test_value = 0x42;
|
||||
uint64_t start_cycles = apu_->GetCycles();
|
||||
|
||||
apu_->Write(0x00, 0x80); // Set control register
|
||||
apu_->Write(0x01, test_value); // Write to DSP address
|
||||
|
||||
uint64_t end_cycles = apu_->GetCycles();
|
||||
|
||||
// 2. Verify timing
|
||||
EXPECT_GT(end_cycles - start_cycles, 0);
|
||||
|
||||
// 3. Verify register access
|
||||
EXPECT_EQ(apu_->Read(0x01), test_value);
|
||||
}
|
||||
|
||||
// Test DMA transfer timing
|
||||
TEST_F(ApuTest, DmaTransferTiming) {
|
||||
// 1. Prepare DMA data
|
||||
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04};
|
||||
|
||||
// 2. Measure DMA timing
|
||||
uint64_t start_cycles = apu_->GetCycles();
|
||||
apu_->WriteDma(0x00, data, sizeof(data));
|
||||
uint64_t end_cycles = apu_->GetCycles();
|
||||
|
||||
// 3. Verify timing
|
||||
EXPECT_GT(end_cycles - start_cycles, 0);
|
||||
|
||||
// 4. Verify DMA transfer
|
||||
EXPECT_EQ(apu_->Read(0x00), 0x01);
|
||||
EXPECT_EQ(apu_->Read(0x01), 0x02);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
122
test/emu/audio/ipl_handshake_test.cc
Normal file
122
test/emu/audio/ipl_handshake_test.cc
Normal file
@@ -0,0 +1,122 @@
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
|
||||
#include <gmock/gmock-nice-strict.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using testing::_;
|
||||
using testing::Return;
|
||||
using yaze::emu::Apu;
|
||||
using yaze::emu::MemoryImpl;
|
||||
|
||||
class IplHandshakeTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
memory_ = std::make_unique<MemoryImpl>();
|
||||
apu_ = std::make_unique<Apu>(*memory_);
|
||||
apu_->Init();
|
||||
}
|
||||
|
||||
std::unique_ptr<MemoryImpl> memory_;
|
||||
std::unique_ptr<Apu> apu_;
|
||||
};
|
||||
|
||||
// Test IPL ROM handshake timing with exact cycle counts
|
||||
TEST_F(IplHandshakeTest, ExactCycleTiming) {
|
||||
// 1. Initial state
|
||||
EXPECT_EQ(apu_->Read(0x00) & 0x80, 0); // Ready bit should be clear
|
||||
|
||||
// 2. Start handshake
|
||||
apu_->Write(0x00, 0x80); // Set control register bit 7
|
||||
|
||||
// 3. Run exact number of cycles for handshake
|
||||
const int expected_cycles = 64; // Expected cycle count for handshake
|
||||
apu_->RunCycles(expected_cycles);
|
||||
|
||||
// 4. Verify handshake completed
|
||||
EXPECT_TRUE(apu_->Read(0x00) & 0x80); // Ready bit should be set
|
||||
EXPECT_EQ(apu_->GetStatus() & 0x80, 0x80); // Ready bit in status register
|
||||
}
|
||||
|
||||
// Test IPL ROM handshake timing with cycle range
|
||||
TEST_F(IplHandshakeTest, CycleRange) {
|
||||
// 1. Initial state
|
||||
EXPECT_EQ(apu_->Read(0x00) & 0x80, 0); // Ready bit should be clear
|
||||
|
||||
// 2. Start handshake
|
||||
apu_->Write(0x00, 0x80); // Set control register bit 7
|
||||
|
||||
// 3. Wait for handshake with cycle counting
|
||||
int cycles = 0;
|
||||
const int min_cycles = 32; // Minimum expected cycles
|
||||
const int max_cycles = 96; // Maximum expected cycles
|
||||
|
||||
while (!(apu_->Read(0x00) & 0x80) && cycles < max_cycles) {
|
||||
apu_->RunCycles(1);
|
||||
cycles++;
|
||||
}
|
||||
|
||||
// 4. Verify timing constraints
|
||||
EXPECT_GE(cycles, min_cycles); // Should take at least min_cycles
|
||||
EXPECT_LE(cycles, max_cycles); // Should complete within max_cycles
|
||||
EXPECT_TRUE(apu_->Read(0x00) & 0x80); // Ready bit should be set
|
||||
}
|
||||
|
||||
// Test IPL ROM handshake with multiple attempts
|
||||
TEST_F(IplHandshakeTest, MultipleAttempts) {
|
||||
const int num_attempts = 10;
|
||||
std::vector<int> cycle_counts;
|
||||
|
||||
for (int i = 0; i < num_attempts; i++) {
|
||||
// Reset APU
|
||||
apu_->Init();
|
||||
|
||||
// Start handshake
|
||||
apu_->Write(0x00, 0x80);
|
||||
|
||||
// Count cycles until ready
|
||||
int cycles = 0;
|
||||
while (!(apu_->Read(0x00) & 0x80) && cycles < 1000) {
|
||||
apu_->RunCycles(1);
|
||||
cycles++;
|
||||
}
|
||||
|
||||
// Record cycle count
|
||||
cycle_counts.push_back(cycles);
|
||||
|
||||
// Verify handshake completed
|
||||
EXPECT_TRUE(apu_->Read(0x00) & 0x80);
|
||||
}
|
||||
|
||||
// Verify cycle count consistency
|
||||
int min_cycles = *std::min_element(cycle_counts.begin(), cycle_counts.end());
|
||||
int max_cycles = *std::max_element(cycle_counts.begin(), cycle_counts.end());
|
||||
EXPECT_LE(max_cycles - min_cycles, 2); // Cycle count should be consistent
|
||||
}
|
||||
|
||||
// Test IPL ROM handshake with interrupts
|
||||
TEST_F(IplHandshakeTest, WithInterrupts) {
|
||||
// 1. Initial state
|
||||
EXPECT_EQ(apu_->Read(0x00) & 0x80, 0);
|
||||
|
||||
// 2. Enable interrupts
|
||||
apu_->Write(0x00, 0x80 | 0x40); // Set control register bits 7 and 6
|
||||
|
||||
// 3. Run cycles with interrupts
|
||||
int cycles = 0;
|
||||
while (!(apu_->Read(0x00) & 0x80) && cycles < 1000) {
|
||||
apu_->RunCycles(1);
|
||||
cycles++;
|
||||
}
|
||||
|
||||
// 4. Verify handshake completed
|
||||
EXPECT_TRUE(apu_->Read(0x00) & 0x80);
|
||||
EXPECT_EQ(apu_->GetStatus() & 0x80, 0x80);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
@@ -13,7 +13,6 @@ namespace test {
|
||||
using yaze::emu::AsmParser;
|
||||
using yaze::emu::Cpu;
|
||||
using yaze::emu::CpuCallbacks;
|
||||
using yaze::emu::MockClock;
|
||||
using yaze::emu::MockMemory;
|
||||
|
||||
/**
|
||||
@@ -29,9 +28,8 @@ class CpuTest : public ::testing::Test {
|
||||
|
||||
AsmParser asm_parser;
|
||||
MockMemory mock_memory;
|
||||
MockClock mock_clock;
|
||||
CpuCallbacks cpu_callbacks;
|
||||
Cpu cpu{mock_memory, mock_clock, cpu_callbacks};
|
||||
Cpu cpu{mock_memory};
|
||||
};
|
||||
|
||||
using ::testing::_;
|
||||
@@ -42,7 +40,6 @@ using ::testing::Return;
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CpuTest, AsmParserTokenizerOk) {
|
||||
AsmParser asm_parser;
|
||||
std::string instruction = R"(
|
||||
ADC.b #$01
|
||||
LDA.b #$FF
|
||||
@@ -58,7 +55,6 @@ TEST_F(CpuTest, AsmParserTokenizerOk) {
|
||||
}
|
||||
|
||||
TEST_F(CpuTest, AsmParserSingleInstructionOk) {
|
||||
AsmParser asm_parser;
|
||||
std::string instruction = "ADC.b #$01";
|
||||
std::vector<std::string> tokens = asm_parser.Tokenize(instruction);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user