feat: Refactor Emulator to Accept ROM Parameter and Enhance Logging
- Updated Emulator::Run method to accept a Rom* parameter, improving flexibility in ROM handling. - Refactored texture creation and ROM data initialization to utilize the new parameter. - Enhanced logging in Snes class to provide detailed information during initialization, reset, and frame processing, aiding in debugging and performance monitoring. - Introduced cycle tracking in Apu and Spc700 classes for accurate synchronization and debugging. - Added unit tests for APU DSP functionality and IPL ROM handshake to ensure reliability and correctness of audio processing.
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
#include "app/emu/audio/dsp.h"
|
||||
#include "app/emu/audio/spc700.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
#include "util/log.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
@@ -15,38 +16,38 @@ namespace emu {
|
||||
static const double apuCyclesPerMaster = (32040 * 32) / (1364 * 262 * 60.0);
|
||||
static const double apuCyclesPerMasterPal = (32040 * 32) / (1364 * 312 * 50.0);
|
||||
|
||||
// Standard SNES IPL ROM (64 bytes at $FFC0-$FFFF)
|
||||
// Verified correct - matches hardware dumps
|
||||
static const uint8_t bootRom[0x40] = {
|
||||
0xcd, 0xef, 0xbd, 0xe8, 0x00, 0xc6, 0x1d, 0xd0, 0xfc, 0x8f, 0xaa,
|
||||
0xf4, 0x8f, 0xbb, 0xf5, 0x78, 0xcc, 0xf4, 0xd0, 0xfb, 0x2f, 0x19,
|
||||
0xeb, 0xf4, 0xd0, 0xfc, 0x7e, 0xf4, 0xd0, 0x0b, 0xe4, 0xf5, 0xcb,
|
||||
0xf4, 0xd7, 0x00, 0xfc, 0xd0, 0xf3, 0xab, 0x01, 0x10, 0xef, 0x7e,
|
||||
0xf4, 0x10, 0xeb, 0xba, 0xf6, 0xda, 0x00, 0xba, 0xf4, 0xc4, 0xf4,
|
||||
0xdd, 0x5d, 0xd0, 0xdb, 0x1f, 0x00, 0x00, 0xc0, 0xff};
|
||||
0xf4, 0x8f, 0xbb, 0xf5, 0xe4, 0xf4, 0x68, 0xcc, 0xd0, 0xfa, 0x2f,
|
||||
0x19, 0xeb, 0xf4, 0xd0, 0xfc, 0x7e, 0xf4, 0xd0, 0x0b, 0xe4, 0xf5,
|
||||
0xcb, 0xf4, 0xd7, 0x00, 0xfc, 0xd0, 0xf3, 0xab, 0x01, 0x10, 0xef,
|
||||
0x7e, 0xf4, 0x10, 0xeb, 0xba, 0xf6, 0xda, 0x00, 0xba, 0xf4, 0xc4,
|
||||
0xf4, 0xdd, 0x5d, 0xd0, 0xdb, 0x1f, 0x00, 0xc0, 0xff};
|
||||
|
||||
// Helper to reset the cycle tracking on emulator reset
|
||||
static uint64_t g_last_master_cycles = 0;
|
||||
static void ResetCycleTracking() { g_last_master_cycles = 0; }
|
||||
|
||||
void Apu::Init() {
|
||||
ram.resize(0x10000);
|
||||
for (int i = 0; i < 0x10000; i++) {
|
||||
ram[i] = 0;
|
||||
}
|
||||
// Copy the boot rom into the ram at ffc0
|
||||
for (int i = 0; i < 0x40; i++) {
|
||||
ram[0xffc0 + i] = bootRom[i];
|
||||
}
|
||||
}
|
||||
|
||||
void Apu::Reset() {
|
||||
LOG_INFO("APU", "Reset called");
|
||||
spc700_.Reset(true);
|
||||
dsp_.Reset();
|
||||
for (int i = 0; i < 0x10000; i++) {
|
||||
ram[i] = 0;
|
||||
}
|
||||
// Copy the boot rom into the ram at ffc0
|
||||
for (int i = 0; i < 0x40; i++) {
|
||||
ram[0xffc0 + i] = bootRom[i];
|
||||
}
|
||||
rom_readable_ = true;
|
||||
dsp_adr_ = 0;
|
||||
cycles_ = 0;
|
||||
ResetCycleTracking(); // Reset the master cycle delta tracking
|
||||
std::fill(in_ports_.begin(), in_ports_.end(), 0);
|
||||
std::fill(out_ports_.begin(), out_ports_.end(), 0);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
@@ -56,15 +57,59 @@ void Apu::Reset() {
|
||||
timer_[i].counter = 0;
|
||||
timer_[i].enabled = false;
|
||||
}
|
||||
LOG_INFO("APU", "Reset complete - IPL ROM readable, PC will be at $%04X",
|
||||
spc700_.read_word(0xFFFE));
|
||||
}
|
||||
|
||||
void Apu::RunCycles(uint64_t cycles) {
|
||||
uint64_t sync_to =
|
||||
(uint64_t)cycles *
|
||||
(memory_.pal_timing() ? apuCyclesPerMasterPal : apuCyclesPerMaster);
|
||||
void Apu::RunCycles(uint64_t master_cycles) {
|
||||
// Convert CPU master cycles to APU cycles target and step SPC/DSP accordingly.
|
||||
const double ratio = memory_.pal_timing() ? apuCyclesPerMasterPal : apuCyclesPerMaster;
|
||||
|
||||
// Track last master cycles to only advance by the delta
|
||||
uint64_t master_delta = master_cycles - g_last_master_cycles;
|
||||
g_last_master_cycles = master_cycles;
|
||||
|
||||
const uint64_t target_apu_cycles = cycles_ + static_cast<uint64_t>(master_delta * ratio);
|
||||
|
||||
while (cycles_ < sync_to) {
|
||||
// Watchdog to detect infinite loops
|
||||
static uint64_t last_log_cycle = 0;
|
||||
static uint16_t last_pc = 0;
|
||||
static int stuck_counter = 0;
|
||||
|
||||
while (cycles_ < target_apu_cycles) {
|
||||
// Execute one SPC700 opcode (variable cycles) then advance APU cycles accordingly.
|
||||
uint16_t current_pc = spc700_.PC;
|
||||
|
||||
// Detect if SPC is stuck in tight loop
|
||||
if (current_pc == last_pc) {
|
||||
stuck_counter++;
|
||||
if (stuck_counter > 10000 && cycles_ - last_log_cycle > 10000) {
|
||||
LOG_WARN("APU", "SPC700 stuck at PC=$%04X for %d iterations",
|
||||
current_pc, stuck_counter);
|
||||
LOG_WARN("APU", "Port Status: F4=$%02X F5=$%02X F6=$%02X F7=$%02X",
|
||||
in_ports_[0], in_ports_[1], in_ports_[2], in_ports_[3]);
|
||||
LOG_WARN("APU", "Out Ports: F4=$%02X F5=$%02X F6=$%02X F7=$%02X",
|
||||
out_ports_[0], out_ports_[1], out_ports_[2], out_ports_[3]);
|
||||
LOG_WARN("APU", "IPL ROM enabled: %s", rom_readable_ ? "YES" : "NO");
|
||||
last_log_cycle = cycles_;
|
||||
stuck_counter = 0;
|
||||
}
|
||||
} else {
|
||||
stuck_counter = 0;
|
||||
}
|
||||
last_pc = current_pc;
|
||||
|
||||
spc700_.RunOpcode();
|
||||
|
||||
// Get the actual cycle count from the last opcode execution
|
||||
// This is critical for proper IPL ROM handshake timing
|
||||
int spc_cycles = spc700_.GetLastOpcodeCycles();
|
||||
|
||||
// Advance APU cycles based on actual SPC700 opcode timing
|
||||
// The SPC700 runs at 1.024 MHz, and we need to synchronize with the DSP/timers
|
||||
for (int i = 0; i < spc_cycles; ++i) {
|
||||
Cycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +139,7 @@ void Apu::Cycle() {
|
||||
}
|
||||
|
||||
uint8_t Apu::Read(uint16_t adr) {
|
||||
static int port_read_count = 0;
|
||||
switch (adr) {
|
||||
case 0xf0:
|
||||
case 0xf1:
|
||||
@@ -111,10 +157,19 @@ uint8_t Apu::Read(uint16_t adr) {
|
||||
case 0xf4:
|
||||
case 0xf5:
|
||||
case 0xf6:
|
||||
case 0xf7:
|
||||
case 0xf7: {
|
||||
uint8_t val = in_ports_[adr - 0xf4];
|
||||
port_read_count++;
|
||||
if (port_read_count < 100) { // Increased limit to see full handshake
|
||||
LOG_INFO("APU", "SPC read port $%04X (F%d) = $%02X at PC=$%04X",
|
||||
adr, adr - 0xf4 + 4, val, spc700_.PC);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
case 0xf8:
|
||||
case 0xf9: {
|
||||
return in_ports_[adr - 0xf4];
|
||||
// Not I/O ports on real hardware; treat as general RAM region.
|
||||
return ram[adr];
|
||||
}
|
||||
case 0xfd:
|
||||
case 0xfe:
|
||||
@@ -131,11 +186,18 @@ uint8_t Apu::Read(uint16_t adr) {
|
||||
}
|
||||
|
||||
void Apu::Write(uint16_t adr, uint8_t val) {
|
||||
static int port_write_count = 0;
|
||||
// Debug: Log ALL writes to F4 to diagnose missing echo
|
||||
static int f4_write_count = 0;
|
||||
if (adr == 0xF4 && f4_write_count++ < 10) {
|
||||
LOG_INFO("APU", "Write() called for $F4 = $%02X at PC=$%04X", val, spc700_.PC);
|
||||
}
|
||||
switch (adr) {
|
||||
case 0xf0: {
|
||||
break; // test register
|
||||
}
|
||||
case 0xf1: {
|
||||
bool old_rom_readable = rom_readable_;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (!timer_[i].enabled && (val & (1 << i))) {
|
||||
timer_[i].divider = 0;
|
||||
@@ -151,7 +213,12 @@ void Apu::Write(uint16_t adr, uint8_t val) {
|
||||
in_ports_[2] = 0;
|
||||
in_ports_[3] = 0;
|
||||
}
|
||||
rom_readable_ = val & 0x80;
|
||||
// IPL ROM mapping: initially enabled; writing 1 to bit7 disables IPL ROM.
|
||||
rom_readable_ = (val & 0x80) == 0;
|
||||
if (old_rom_readable != rom_readable_) {
|
||||
LOG_INFO("APU", "Control register $F1 = $%02X - IPL ROM %s at PC=$%04X",
|
||||
val, rom_readable_ ? "ENABLED" : "DISABLED", spc700_.PC);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0xf2: {
|
||||
@@ -167,11 +234,16 @@ void Apu::Write(uint16_t adr, uint8_t val) {
|
||||
case 0xf6:
|
||||
case 0xf7: {
|
||||
out_ports_[adr - 0xf4] = val;
|
||||
port_write_count++;
|
||||
if (port_write_count < 100) { // Increased limit to see full handshake
|
||||
LOG_INFO("APU", "SPC wrote port $%04X (F%d) = $%02X at PC=$%04X [APU_cycles=%llu]",
|
||||
adr, adr - 0xf4 + 4, val, spc700_.PC, cycles_);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0xf8:
|
||||
case 0xf9: {
|
||||
in_ports_[adr - 0xf4] = val;
|
||||
// General RAM
|
||||
break;
|
||||
}
|
||||
case 0xfa:
|
||||
|
||||
@@ -123,6 +123,7 @@ void Dsp::Reset() {
|
||||
memset(firBufferR, 0, sizeof(firBufferR));
|
||||
memset(sampleBuffer, 0, sizeof(sampleBuffer));
|
||||
sampleOffset = 0;
|
||||
lastFrameBoundary = 0;
|
||||
}
|
||||
|
||||
void Dsp::NewFrame() {
|
||||
@@ -146,9 +147,10 @@ void Dsp::Cycle() {
|
||||
sampleOutL = 0;
|
||||
sampleOutR = 0;
|
||||
}
|
||||
// put final sample in the samplebuffer
|
||||
// put final sample in the ring buffer and advance pointer
|
||||
sampleBuffer[(sampleOffset & 0x3ff) * 2] = sampleOutL;
|
||||
sampleBuffer[(sampleOffset++ & 0x3ff) * 2 + 1] = sampleOutR;
|
||||
sampleBuffer[(sampleOffset & 0x3ff) * 2 + 1] = sampleOutR;
|
||||
sampleOffset = (sampleOffset + 1) & 0x3ff;
|
||||
}
|
||||
|
||||
static int clamp16(int val) {
|
||||
@@ -616,14 +618,18 @@ void Dsp::Write(uint8_t adr, uint8_t val) {
|
||||
|
||||
void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame,
|
||||
bool pal_timing) {
|
||||
// resample from 534 / 641 samples per frame to wanted value
|
||||
float wantedSamples = (pal_timing ? 641.0 : 534.0);
|
||||
double adder = wantedSamples / samples_per_frame;
|
||||
double location = lastFrameBoundary - wantedSamples;
|
||||
// Resample from native samples-per-frame (NTSC: ~534, PAL: ~641)
|
||||
const double native_per_frame = pal_timing ? 641.0 : 534.0;
|
||||
const double step = native_per_frame / static_cast<double>(samples_per_frame);
|
||||
// Start reading one native frame behind the frame boundary
|
||||
double location = static_cast<double>((lastFrameBoundary + 0x400) & 0x3ff);
|
||||
location -= native_per_frame;
|
||||
|
||||
for (int i = 0; i < samples_per_frame; i++) {
|
||||
sample_data[i * 2] = sample_buffer_[(((int)location) & 0x3ff) * 2];
|
||||
sample_data[i * 2 + 1] = sample_buffer_[(((int)location) & 0x3ff) * 2 + 1];
|
||||
location += adder;
|
||||
const int idx = static_cast<int>(location) & 0x3ff;
|
||||
sample_data[(i * 2) + 0] = sampleBuffer[(idx * 2) + 0];
|
||||
sample_data[(i * 2) + 1] = sampleBuffer[(idx * 2) + 1];
|
||||
location += step;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,8 +105,9 @@ class Dsp {
|
||||
void GetSamples(int16_t* sample_data, int samples_per_frame, bool pal_timing);
|
||||
|
||||
private:
|
||||
int16_t sample_buffer_[0x400 * 2]; // (1024 samples, *2 for stereo)
|
||||
int16_t sample_offset_; // current offset in samplebuffer
|
||||
// sample ring buffer (1024 samples, *2 for stereo)
|
||||
int16_t sampleBuffer[0x400 * 2];
|
||||
uint16_t sampleOffset; // current offset in samplebuffer
|
||||
|
||||
std::vector<uint8_t>& aram_;
|
||||
|
||||
@@ -143,9 +144,6 @@ class Dsp {
|
||||
int8_t firValues[8];
|
||||
int16_t firBufferL[8];
|
||||
int16_t firBufferR[8];
|
||||
// sample ring buffer (1024 samples, *2 for stereo)
|
||||
int16_t sampleBuffer[0x400 * 2];
|
||||
uint16_t sampleOffset; // current offset in samplebuffer
|
||||
uint32_t lastFrameBoundary;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "app/emu/audio/spc700.h"
|
||||
#include "util/log.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
@@ -18,22 +19,30 @@ void Spc700::MOVY(uint16_t adr) {
|
||||
}
|
||||
|
||||
void Spc700::MOVS(uint16_t adr) {
|
||||
static int movs_log = 0;
|
||||
// Log all MOVS to F4 port
|
||||
if (adr == 0x00F4 || movs_log++ < 20) {
|
||||
LOG_INFO("SPC", "MOVS BEFORE: bstep=%d adr=$%04X A=$%02X", bstep, adr, A);
|
||||
}
|
||||
switch (bstep) {
|
||||
case 0: read(adr); break;
|
||||
case 0: read(adr); bstep++; break;
|
||||
case 1: write(adr, A); bstep = 0; break;
|
||||
}
|
||||
if (adr == 0x00F4 || movs_log < 20) {
|
||||
LOG_INFO("SPC", "MOVS AFTER: bstep=%d", bstep);
|
||||
}
|
||||
}
|
||||
|
||||
void Spc700::MOVSX(uint16_t adr) {
|
||||
switch (bstep) {
|
||||
case 0: read(adr); break;
|
||||
case 0: read(adr); bstep++; break;
|
||||
case 1: write(adr, X); bstep = 0; break;
|
||||
}
|
||||
}
|
||||
|
||||
void Spc700::MOVSY(uint16_t adr) {
|
||||
switch (bstep) {
|
||||
case 0: read(adr); break;
|
||||
case 0: read(adr); bstep++; break;
|
||||
case 1: write(adr, Y); bstep = 0; break;
|
||||
}
|
||||
}
|
||||
|
||||
307
src/app/emu/audio/internal/spc700_cycles.h
Normal file
307
src/app/emu/audio/internal/spc700_cycles.h
Normal file
@@ -0,0 +1,307 @@
|
||||
#ifndef YAZE_APP_EMU_AUDIO_INTERNAL_SPC700_CYCLES_H
|
||||
#define YAZE_APP_EMU_AUDIO_INTERNAL_SPC700_CYCLES_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
|
||||
// SPC700 opcode cycle counts
|
||||
// Reference: https://problemkaputt.de/fullsnes.htm#snesapucpu
|
||||
// Note: Some opcodes have variable cycles depending on page boundary crossing
|
||||
// These are baseline cycles; actual cycles may be higher
|
||||
constexpr int spc700_cycles[256] = {
|
||||
// 0x00-0x0F
|
||||
2, // 00 NOP
|
||||
8, // 01 TCALL 0
|
||||
4, // 02 SET1 dp, 0
|
||||
5, // 03 BBS dp, 0, rel
|
||||
3, // 04 OR A, dp
|
||||
4, // 05 OR A, abs
|
||||
3, // 06 OR A, (X)
|
||||
6, // 07 OR A, (dp+X)
|
||||
2, // 08 OR A, #imm
|
||||
6, // 09 OR dp, dp
|
||||
5, // 0A OR1 C, abs.bit
|
||||
4, // 0B ASL dp
|
||||
5, // 0C ASL abs
|
||||
4, // 0D PUSH PSW
|
||||
6, // 0E TSET1 abs
|
||||
8, // 0F BRK
|
||||
|
||||
// 0x10-0x1F
|
||||
2, // 10 BPL rel
|
||||
8, // 11 TCALL 1
|
||||
4, // 12 CLR1 dp, 0
|
||||
5, // 13 BBC dp, 0, rel
|
||||
4, // 14 OR A, dp+X
|
||||
5, // 15 OR A, abs+X
|
||||
5, // 16 OR A, abs+Y
|
||||
6, // 17 OR A, (dp)+Y
|
||||
5, // 18 OR dp, #imm
|
||||
5, // 19 OR (X), (Y)
|
||||
5, // 1A DECW dp
|
||||
5, // 1B ASL dp+X
|
||||
2, // 1C ASL A
|
||||
2, // 1D DEC X
|
||||
4, // 1E CMP X, abs
|
||||
6, // 1F JMP (abs+X)
|
||||
|
||||
// 0x20-0x2F
|
||||
2, // 20 CLRP
|
||||
8, // 21 TCALL 2
|
||||
4, // 22 SET1 dp, 1
|
||||
5, // 23 BBS dp, 1, rel
|
||||
3, // 24 AND A, dp
|
||||
4, // 25 AND A, abs
|
||||
3, // 26 AND A, (X)
|
||||
6, // 27 AND A, (dp+X)
|
||||
2, // 28 AND A, #imm
|
||||
6, // 29 AND dp, dp
|
||||
5, // 2A OR1 C, /abs.bit
|
||||
4, // 2B ROL dp
|
||||
5, // 2C ROL abs
|
||||
4, // 2D PUSH A
|
||||
5, // 2E CBNE dp, rel
|
||||
4, // 2F BRA rel
|
||||
|
||||
// 0x30-0x3F
|
||||
2, // 30 BMI rel
|
||||
8, // 31 TCALL 3
|
||||
4, // 32 CLR1 dp, 1
|
||||
5, // 33 BBC dp, 1, rel
|
||||
4, // 34 AND A, dp+X
|
||||
5, // 35 AND A, abs+X
|
||||
5, // 36 AND A, abs+Y
|
||||
6, // 37 AND A, (dp)+Y
|
||||
5, // 38 AND dp, #imm
|
||||
5, // 39 AND (X), (Y)
|
||||
5, // 3A INCW dp
|
||||
5, // 3B ROL dp+X
|
||||
2, // 3C ROL A
|
||||
2, // 3D INC X
|
||||
3, // 3E CMP X, dp
|
||||
8, // 3F CALL abs
|
||||
|
||||
// 0x40-0x4F
|
||||
2, // 40 SETP
|
||||
8, // 41 TCALL 4
|
||||
4, // 42 SET1 dp, 2
|
||||
5, // 43 BBS dp, 2, rel
|
||||
3, // 44 EOR A, dp
|
||||
4, // 45 EOR A, abs
|
||||
3, // 46 EOR A, (X)
|
||||
6, // 47 EOR A, (dp+X)
|
||||
2, // 48 EOR A, #imm
|
||||
6, // 49 EOR dp, dp
|
||||
4, // 4A AND1 C, abs.bit
|
||||
4, // 4B LSR dp
|
||||
5, // 4C LSR abs
|
||||
4, // 4D PUSH X
|
||||
6, // 4E TCLR1 abs
|
||||
6, // 4F PCALL dp
|
||||
|
||||
// 0x50-0x5F
|
||||
2, // 50 BVC rel
|
||||
8, // 51 TCALL 5
|
||||
4, // 52 CLR1 dp, 2
|
||||
5, // 53 BBC dp, 2, rel
|
||||
4, // 54 EOR A, dp+X
|
||||
5, // 55 EOR A, abs+X
|
||||
5, // 56 EOR A, abs+Y
|
||||
6, // 57 EOR A, (dp)+Y
|
||||
5, // 58 EOR dp, #imm
|
||||
5, // 59 EOR (X), (Y)
|
||||
4, // 5A CMPW YA, dp
|
||||
5, // 5B LSR dp+X
|
||||
2, // 5C LSR A
|
||||
2, // 5D MOV X, A
|
||||
4, // 5E CMP Y, abs
|
||||
3, // 5F JMP abs
|
||||
|
||||
// 0x60-0x6F
|
||||
2, // 60 CLRC
|
||||
8, // 61 TCALL 6
|
||||
4, // 62 SET1 dp, 3
|
||||
5, // 63 BBS dp, 3, rel
|
||||
3, // 64 CMP A, dp
|
||||
4, // 65 CMP A, abs
|
||||
3, // 66 CMP A, (X)
|
||||
6, // 67 CMP A, (dp+X)
|
||||
2, // 68 CMP A, #imm
|
||||
6, // 69 CMP dp, dp
|
||||
4, // 6A AND1 C, /abs.bit
|
||||
4, // 6B ROR dp
|
||||
5, // 6C ROR abs
|
||||
4, // 6D PUSH Y
|
||||
5, // 6E DBNZ dp, rel
|
||||
5, // 6F RET
|
||||
|
||||
// 0x70-0x7F
|
||||
2, // 70 BVS rel
|
||||
8, // 71 TCALL 7
|
||||
4, // 72 CLR1 dp, 3
|
||||
5, // 73 BBC dp, 3, rel
|
||||
4, // 74 CMP A, dp+X
|
||||
5, // 75 CMP A, abs+X
|
||||
5, // 76 CMP A, abs+Y
|
||||
6, // 77 CMP A, (dp)+Y
|
||||
5, // 78 CMP dp, #imm
|
||||
5, // 79 CMP (X), (Y)
|
||||
5, // 7A ADDW YA, dp
|
||||
5, // 7B ROR dp+X
|
||||
2, // 7C ROR A
|
||||
2, // 7D MOV A, X
|
||||
3, // 7E CMP Y, dp
|
||||
6, // 7F RETI
|
||||
|
||||
// 0x80-0x8F
|
||||
2, // 80 SETC
|
||||
8, // 81 TCALL 8
|
||||
4, // 82 SET1 dp, 4
|
||||
5, // 83 BBS dp, 4, rel
|
||||
3, // 84 ADC A, dp
|
||||
4, // 85 ADC A, abs
|
||||
3, // 86 ADC A, (X)
|
||||
6, // 87 ADC A, (dp+X)
|
||||
2, // 88 ADC A, #imm
|
||||
6, // 89 ADC dp, dp
|
||||
5, // 8A EOR1 C, abs.bit
|
||||
4, // 8B DEC dp
|
||||
5, // 8C DEC abs
|
||||
2, // 8D MOV Y, #imm
|
||||
4, // 8E POP PSW
|
||||
5, // 8F MOV dp, #imm
|
||||
|
||||
// 0x90-0x9F
|
||||
2, // 90 BCC rel
|
||||
8, // 91 TCALL 9
|
||||
4, // 92 CLR1 dp, 4
|
||||
5, // 93 BBC dp, 4, rel
|
||||
4, // 94 ADC A, dp+X
|
||||
5, // 95 ADC A, abs+X
|
||||
5, // 96 ADC A, abs+Y
|
||||
6, // 97 ADC A, (dp)+Y
|
||||
5, // 98 ADC dp, #imm
|
||||
5, // 99 ADC (X), (Y)
|
||||
5, // 9A SUBW YA, dp
|
||||
5, // 9B DEC dp+X
|
||||
2, // 9C DEC A
|
||||
2, // 9D MOV X, SP
|
||||
12, // 9E DIV YA, X
|
||||
5, // 9F XCN A
|
||||
|
||||
// 0xA0-0xAF
|
||||
3, // A0 EI
|
||||
8, // A1 TCALL 10
|
||||
4, // A2 SET1 dp, 5
|
||||
5, // A3 BBS dp, 5, rel
|
||||
3, // A4 SBC A, dp
|
||||
4, // A5 SBC A, abs
|
||||
3, // A6 SBC A, (X)
|
||||
6, // A7 SBC A, (dp+X)
|
||||
2, // A8 SBC A, #imm
|
||||
6, // A9 SBC dp, dp
|
||||
4, // AA MOV1 C, abs.bit
|
||||
4, // AB INC dp
|
||||
5, // AC INC abs
|
||||
2, // AD CMP Y, #imm
|
||||
4, // AE POP A
|
||||
4, // AF MOV (X)+, A
|
||||
|
||||
// 0xB0-0xBF
|
||||
2, // B0 BCS rel
|
||||
8, // B1 TCALL 11
|
||||
4, // B2 CLR1 dp, 5
|
||||
5, // B3 BBC dp, 5, rel
|
||||
4, // B4 SBC A, dp+X
|
||||
5, // B5 SBC A, abs+X
|
||||
5, // B6 SBC A, abs+Y
|
||||
6, // B7 SBC A, (dp)+Y
|
||||
5, // B8 SBC dp, #imm
|
||||
5, // B9 SBC (X), (Y)
|
||||
5, // BA MOVW YA, dp
|
||||
5, // BB INC dp+X
|
||||
2, // BC INC A
|
||||
2, // BD MOV SP, X
|
||||
3, // BE DAS A
|
||||
4, // BF MOV A, (X)+
|
||||
|
||||
// 0xC0-0xCF
|
||||
3, // C0 DI
|
||||
8, // C1 TCALL 12
|
||||
4, // C2 SET1 dp, 6
|
||||
5, // C3 BBS dp, 6, rel
|
||||
3, // C4 MOV dp, A
|
||||
4, // C5 MOV abs, A
|
||||
3, // C6 MOV (X), A
|
||||
6, // C7 MOV (dp+X), A
|
||||
2, // C8 CMP X, #imm
|
||||
4, // C9 MOV abs, X
|
||||
6, // CA MOV1 abs.bit, C
|
||||
3, // CB MOV dp, Y
|
||||
4, // CC MOV abs, Y
|
||||
2, // CD MOV X, #imm
|
||||
4, // CE POP X
|
||||
9, // CF MUL YA
|
||||
|
||||
// 0xD0-0xDF
|
||||
2, // D0 BNE rel
|
||||
8, // D1 TCALL 13
|
||||
4, // D2 CLR1 dp, 6
|
||||
5, // D3 BBC dp, 6, rel
|
||||
4, // D4 MOV dp+X, A
|
||||
5, // D5 MOV abs+X, A
|
||||
5, // D6 MOV abs+Y, A
|
||||
6, // D7 MOV (dp)+Y, A
|
||||
3, // D8 MOV dp, X
|
||||
4, // D9 MOV dp+Y, X
|
||||
5, // DA MOVW dp, YA
|
||||
4, // DB MOV dp+X, Y
|
||||
2, // DC DEC Y
|
||||
2, // DD MOV A, Y
|
||||
6, // DE CBNE dp+X, rel
|
||||
3, // DF DAA A
|
||||
|
||||
// 0xE0-0xEF
|
||||
2, // E0 CLRV
|
||||
8, // E1 TCALL 14
|
||||
4, // E2 SET1 dp, 7
|
||||
5, // E3 BBS dp, 7, rel
|
||||
3, // E4 MOV A, dp
|
||||
4, // E5 MOV A, abs
|
||||
3, // E6 MOV A, (X)
|
||||
6, // E7 MOV A, (dp+X)
|
||||
2, // E8 MOV A, #imm
|
||||
4, // E9 MOV X, abs
|
||||
5, // EA NOT1 abs.bit
|
||||
3, // EB MOV Y, dp
|
||||
4, // EC MOV Y, abs
|
||||
3, // ED NOTC
|
||||
4, // EE POP Y
|
||||
3, // EF SLEEP
|
||||
|
||||
// 0xF0-0xFF
|
||||
2, // F0 BEQ rel
|
||||
8, // F1 TCALL 15
|
||||
4, // F2 CLR1 dp, 7
|
||||
5, // F3 BBC dp, 7, rel
|
||||
4, // F4 MOV A, dp+X
|
||||
5, // F5 MOV A, abs+X
|
||||
5, // F6 MOV A, abs+Y
|
||||
6, // F7 MOV A, (dp)+Y
|
||||
3, // F8 MOV X, dp
|
||||
4, // F9 MOV X, dp+Y
|
||||
6, // FA MOV dp, dp
|
||||
4, // FB MOV Y, dp+X
|
||||
2, // FC INC Y
|
||||
2, // FD MOV Y, A
|
||||
4, // FE DBNZ Y, rel
|
||||
3 // FF STOP
|
||||
};
|
||||
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_EMU_AUDIO_INTERNAL_SPC700_CYCLES_H
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include "util/log.h"
|
||||
#include "app/core/features.h"
|
||||
|
||||
#include "app/emu/audio/internal/opcodes.h"
|
||||
#include "app/emu/audio/internal/spc700_cycles.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
@@ -25,6 +28,11 @@ void Spc700::Reset(bool hard) {
|
||||
}
|
||||
|
||||
void Spc700::RunOpcode() {
|
||||
static int entry_log = 0;
|
||||
if ((PC >= 0xFFF0 && PC <= 0xFFFF) && entry_log++ < 30) {
|
||||
LOG_INFO("SPC", "RunOpcode ENTRY: PC=$%04X step=%d bstep=%d", PC, step, bstep);
|
||||
}
|
||||
|
||||
if (reset_wanted_) {
|
||||
// based on 6502, brk without writes
|
||||
reset_wanted_ = false;
|
||||
@@ -36,20 +44,62 @@ void Spc700::RunOpcode() {
|
||||
callbacks_.idle(false);
|
||||
PSW.I = false;
|
||||
PC = read_word(0xfffe);
|
||||
last_opcode_cycles_ = 8; // Reset sequence takes 8 cycles
|
||||
return;
|
||||
}
|
||||
if (stopped_) {
|
||||
// Allow timers/DSP to continue advancing while SPC is stopped/sleeping.
|
||||
callbacks_.idle(true);
|
||||
last_opcode_cycles_ = 2; // Stopped state consumes minimal cycles
|
||||
return;
|
||||
}
|
||||
if (step == 0) {
|
||||
bstep = 0;
|
||||
opcode = ReadOpcode();
|
||||
// Debug: Log SPC execution in IPL ROM range and multi-step state
|
||||
static int spc_exec_count = 0;
|
||||
if ((PC >= 0xFFCF && PC <= 0xFFFF) && spc_exec_count++ < 50) {
|
||||
LOG_INFO("SPC", "Execute: PC=$%04X step=0 bstep=%d", PC, bstep);
|
||||
}
|
||||
|
||||
// Only read new opcode if previous instruction is complete
|
||||
if (bstep == 0) {
|
||||
opcode = ReadOpcode();
|
||||
// Set base cycle count from lookup table
|
||||
last_opcode_cycles_ = spc700_cycles[opcode];
|
||||
} else {
|
||||
LOG_INFO("SPC", "Continuing multi-step: PC=$%04X bstep=%d opcode=$%02X", PC, bstep, opcode);
|
||||
}
|
||||
step = 1;
|
||||
return;
|
||||
}
|
||||
// Emit instruction log via util logger to align with CPU logging controls.
|
||||
if (core::FeatureFlags::get().kLogInstructions) {
|
||||
try {
|
||||
LogInstruction(PC, opcode);
|
||||
} catch (...) {
|
||||
// ignore mapping failures
|
||||
}
|
||||
}
|
||||
|
||||
static int exec_log = 0;
|
||||
if ((PC >= 0xFFF0 && PC <= 0xFFFF) && exec_log++ < 30) {
|
||||
LOG_INFO("SPC", "About to ExecuteInstructions: PC=$%04X step=%d bstep=%d opcode=$%02X", PC, step, bstep, opcode);
|
||||
}
|
||||
|
||||
ExecuteInstructions(opcode);
|
||||
if (step == 1) step = 0; // reset step for non cycle-stepped opcodes.
|
||||
// Only reset step if instruction is complete (bstep back to 0)
|
||||
static int reset_log = 0;
|
||||
if (step == 1) {
|
||||
if (bstep == 0) {
|
||||
if ((PC >= 0xFFF0 && PC <= 0xFFFF) && reset_log++ < 20) {
|
||||
LOG_INFO("SPC", "Resetting step: PC=$%04X opcode=$%02X bstep=%d", PC, opcode, bstep);
|
||||
}
|
||||
step = 0;
|
||||
} else {
|
||||
if ((PC >= 0xFFF0 && PC <= 0xFFFF) || reset_log++ < 20) {
|
||||
LOG_INFO("SPC", "NOT resetting step: PC=$%04X opcode=$%02X bstep=%d", PC, opcode, bstep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Spc700::ExecuteInstructions(uint8_t opcode) {
|
||||
@@ -1010,6 +1060,7 @@ void Spc700::ExecuteInstructions(uint8_t opcode) {
|
||||
break;
|
||||
}
|
||||
case 0xc4: { // movs dp
|
||||
LOG_INFO("SPC", "Case 0xC4 reached: bstep=%d PC=$%04X", bstep, PC);
|
||||
MOVS(dp());
|
||||
break;
|
||||
}
|
||||
@@ -1216,9 +1267,10 @@ void Spc700::ExecuteInstructions(uint8_t opcode) {
|
||||
break;
|
||||
}
|
||||
case 0xef: { // sleep imp
|
||||
// Emulate low-power idle without halting the core permanently.
|
||||
// Advance timers/DSP via idle callbacks, but do not set stopped_.
|
||||
read(PC);
|
||||
callbacks_.idle(false);
|
||||
stopped_ = true; // no interrupts, so sleeping stops as well
|
||||
for (int i = 0; i < 4; ++i) callbacks_.idle(true);
|
||||
break;
|
||||
}
|
||||
case 0xf0: { // beq rel
|
||||
@@ -1294,31 +1346,21 @@ void Spc700::ExecuteInstructions(uint8_t opcode) {
|
||||
}
|
||||
|
||||
void Spc700::LogInstruction(uint16_t initial_pc, uint8_t opcode) {
|
||||
std::string mnemonic = spc_opcode_map.at(opcode);
|
||||
const std::string& mnemonic = spc_opcode_map.at(opcode);
|
||||
|
||||
std::stringstream log_entry_stream;
|
||||
log_entry_stream << "\033[1;36m$" << std::hex << std::setw(4)
|
||||
<< std::setfill('0') << initial_pc << "\033[0m";
|
||||
log_entry_stream << " \033[1;32m" << std::hex << std::setw(2)
|
||||
<< std::setfill('0') << static_cast<int>(opcode) << "\033[0m"
|
||||
<< " \033[1;35m" << std::setw(18) << std::left
|
||||
<< std::setfill(' ') << mnemonic << "\033[0m";
|
||||
std::stringstream ss;
|
||||
ss << "$" << std::hex << std::setw(4) << std::setfill('0') << initial_pc
|
||||
<< ": 0x" << std::setw(2) << std::setfill('0')
|
||||
<< static_cast<int>(opcode) << " " << mnemonic
|
||||
<< " A:" << std::setw(2) << std::setfill('0') << std::hex
|
||||
<< static_cast<int>(A)
|
||||
<< " X:" << std::setw(2) << std::setfill('0') << std::hex
|
||||
<< static_cast<int>(X)
|
||||
<< " Y:" << std::setw(2) << std::setfill('0') << std::hex
|
||||
<< static_cast<int>(Y);
|
||||
|
||||
log_entry_stream << " \033[1;33mA: " << std::hex << std::setw(2)
|
||||
<< std::setfill('0') << std::right << static_cast<int>(A)
|
||||
<< "\033[0m";
|
||||
log_entry_stream << " \033[1;33mX: " << std::hex << std::setw(2)
|
||||
<< std::setfill('0') << std::right << static_cast<int>(X)
|
||||
<< "\033[0m";
|
||||
log_entry_stream << " \033[1;33mY: " << std::hex << std::setw(2)
|
||||
<< std::setfill('0') << std::right << static_cast<int>(Y)
|
||||
<< "\033[0m";
|
||||
std::string log_entry = log_entry_stream.str();
|
||||
|
||||
std::cerr << log_entry << std::endl;
|
||||
|
||||
// Append the log entry to the log
|
||||
// log_.push_back(log_entry);
|
||||
util::LogManager::instance().log(util::LogLevel::YAZE_DEBUG, "SPC700",
|
||||
ss.str());
|
||||
}
|
||||
|
||||
} // namespace emu
|
||||
|
||||
@@ -82,6 +82,9 @@ class Spc700 {
|
||||
uint8_t dat;
|
||||
uint16_t dat16;
|
||||
uint8_t param;
|
||||
|
||||
// Cycle tracking for accurate APU synchronization
|
||||
int last_opcode_cycles_ = 0;
|
||||
|
||||
const uint8_t ipl_rom_[64]{
|
||||
0xCD, 0xEF, 0xBD, 0xE8, 0x00, 0xC6, 0x1D, 0xD0, 0xFC, 0x8F, 0xAA,
|
||||
@@ -135,6 +138,9 @@ class Spc700 {
|
||||
void Reset(bool hard = false);
|
||||
|
||||
void RunOpcode();
|
||||
|
||||
// Get the number of cycles consumed by the last opcode execution
|
||||
int GetLastOpcodeCycles() const { return last_opcode_cycles_; }
|
||||
|
||||
void ExecuteInstructions(uint8_t opcode);
|
||||
void LogInstruction(uint16_t initial_pc, uint8_t opcode);
|
||||
@@ -143,8 +149,8 @@ class Spc700 {
|
||||
uint8_t read(uint16_t address) { return callbacks_.read(address); }
|
||||
|
||||
uint16_t read_word(uint16_t address) {
|
||||
uint8_t adrl = address;
|
||||
uint8_t adrh = address + 1;
|
||||
uint16_t adrl = address;
|
||||
uint16_t adrh = address + 1;
|
||||
uint8_t value = callbacks_.read(adrl);
|
||||
return value | (callbacks_.read(adrh) << 8);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "app/core/features.h"
|
||||
#include "app/emu/cpu/internal/opcodes.h"
|
||||
#include "util/log.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
@@ -55,15 +56,31 @@ void Cpu::RunOpcode() {
|
||||
SetFlags(status); // updates x and m flags, clears
|
||||
// upper half of x and y if needed
|
||||
PB = 0;
|
||||
PC = ReadWord(0xfffc, 0xfffd);
|
||||
|
||||
// Debug: Log reset vector read
|
||||
uint8_t low_byte = ReadByte(0xfffc);
|
||||
uint8_t high_byte = ReadByte(0xfffd);
|
||||
PC = low_byte | (high_byte << 8);
|
||||
LOG_INFO("CPU", "Reset vector: $FFFC=$%02X $FFFD=$%02X -> PC=$%04X",
|
||||
low_byte, high_byte, PC);
|
||||
return;
|
||||
}
|
||||
if (stopped_) {
|
||||
static int stopped_log_count = 0;
|
||||
if (stopped_log_count++ < 5) {
|
||||
LOG_WARN("CPU", "CPU is STOPPED at $%02X:%04X (STP instruction executed)", PB, PC);
|
||||
}
|
||||
callbacks_.idle(true);
|
||||
return;
|
||||
}
|
||||
if (waiting_) {
|
||||
static int waiting_log_count = 0;
|
||||
if (waiting_log_count++ < 5) {
|
||||
LOG_WARN("CPU", "CPU is WAITING at $%02X:%04X - irq_wanted=%d nmi_wanted=%d int_flag=%d",
|
||||
PB, PC, irq_wanted_, nmi_wanted_, GetInterruptFlag());
|
||||
}
|
||||
if (irq_wanted_ || nmi_wanted_) {
|
||||
LOG_INFO("CPU", "CPU waking from WAIT - irq=%d nmi=%d", irq_wanted_, nmi_wanted_);
|
||||
waiting_ = false;
|
||||
callbacks_.idle(false);
|
||||
CheckInt();
|
||||
@@ -80,6 +97,40 @@ void Cpu::RunOpcode() {
|
||||
DoInterrupt();
|
||||
} else {
|
||||
uint8_t opcode = ReadOpcode();
|
||||
|
||||
// Debug: Log key instructions during boot
|
||||
static int instruction_count = 0;
|
||||
instruction_count++;
|
||||
|
||||
// Log first 500 fully, then every 10th until 3000, then stop
|
||||
bool should_log = (instruction_count < 500) ||
|
||||
(instruction_count < 3000 && instruction_count % 10 == 0);
|
||||
|
||||
if (should_log) {
|
||||
LOG_INFO("CPU", "Exec #%d: $%02X:%04X opcode=$%02X",
|
||||
instruction_count, PB, PC - 1, opcode);
|
||||
}
|
||||
|
||||
// Debug: Log if stuck at same PC for extended period (after first 200 instructions)
|
||||
static uint16_t last_stuck_pc = 0xFFFF;
|
||||
static int stuck_count = 0;
|
||||
if (instruction_count >= 200) {
|
||||
if (PC - 1 == last_stuck_pc) {
|
||||
stuck_count++;
|
||||
if (stuck_count == 100 || stuck_count == 1000 || stuck_count == 10000) {
|
||||
LOG_WARN("CPU", "Stuck at $%02X:%04X opcode=$%02X for %d iterations",
|
||||
PB, PC - 1, opcode, stuck_count);
|
||||
}
|
||||
} else {
|
||||
if (stuck_count > 50) {
|
||||
LOG_INFO("CPU", "Moved from $%02X:%04X (was stuck %d times) to $%02X:%04X",
|
||||
PB, last_stuck_pc, stuck_count, PB, PC - 1);
|
||||
}
|
||||
stuck_count = 0;
|
||||
last_stuck_pc = PC - 1;
|
||||
}
|
||||
}
|
||||
|
||||
ExecuteInstruction(opcode);
|
||||
}
|
||||
}
|
||||
@@ -1831,6 +1882,9 @@ void Cpu::LogInstructions(uint16_t PC, uint8_t opcode, uint16_t operand,
|
||||
|
||||
InstructionEntry entry(PC, opcode, ops, 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"
|
||||
|
||||
@@ -46,9 +46,9 @@ using ImGui::Separator;
|
||||
using ImGui::TableNextColumn;
|
||||
using ImGui::Text;
|
||||
|
||||
void Emulator::Run() {
|
||||
void Emulator::Run(Rom* rom) {
|
||||
static bool loaded = false;
|
||||
if (!snes_.running() && rom()->is_loaded()) {
|
||||
if (!snes_.running() && rom->is_loaded()) {
|
||||
ppu_texture_ = SDL_CreateTexture(core::Renderer::Get().renderer(),
|
||||
SDL_PIXELFORMAT_ARGB8888,
|
||||
SDL_TEXTUREACCESS_STREAMING, 512, 480);
|
||||
@@ -56,7 +56,7 @@ void Emulator::Run() {
|
||||
printf("Failed to create texture: %s\n", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
rom_data_ = rom()->vector();
|
||||
rom_data_ = rom->vector();
|
||||
snes_.Init(rom_data_);
|
||||
wanted_frames_ = 1.0 / (snes_.memory().pal_timing() ? 50.0 : 60.0);
|
||||
wanted_samples_ = 48000 / (snes_.memory().pal_timing() ? 50 : 60);
|
||||
|
||||
@@ -38,7 +38,8 @@ struct EmulatorKeybindings {
|
||||
class Emulator {
|
||||
public:
|
||||
Emulator() = default;
|
||||
void Run();
|
||||
~Emulator() = default;
|
||||
void Run(Rom* rom);
|
||||
|
||||
auto snes() -> Snes& { return snes_; }
|
||||
auto running() const -> bool { return running_; }
|
||||
@@ -47,8 +48,6 @@ class Emulator {
|
||||
audio_device_ = audio_device;
|
||||
}
|
||||
auto wanted_samples() const -> int { return wanted_samples_; }
|
||||
auto rom() { return rom_; }
|
||||
auto mutable_rom() { return rom_; }
|
||||
|
||||
private:
|
||||
void RenderNavBar();
|
||||
@@ -88,7 +87,6 @@ class Emulator {
|
||||
int16_t* audio_buffer_;
|
||||
SDL_AudioDeviceID audio_device_;
|
||||
|
||||
Rom* rom_;
|
||||
Snes snes_;
|
||||
SDL_Texture* ppu_texture_;
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
|
||||
@@ -39,6 +41,14 @@ void MemoryImpl::Initialize(const std::vector<uint8_t>& rom_data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Log reset vector location
|
||||
uint8_t reset_low = memory_[0x00FFFC];
|
||||
uint8_t reset_high = memory_[0x00FFFD];
|
||||
LOG_INFO("Memory", "LoROM reset vector at $00:FFFC = $%02X%02X (from ROM offset $%04X)",
|
||||
reset_high, reset_low, 0x7FFC);
|
||||
LOG_INFO("Memory", "ROM data at offset $7FFC = $%02X $%02X",
|
||||
rom_data[0x7FFC], rom_data[0x7FFD]);
|
||||
}
|
||||
|
||||
uint8_t MemoryImpl::cart_read(uint8_t bank, uint16_t adr) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "app/emu/memory/dma.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
#include "app/emu/video/ppu.h"
|
||||
#include "util/log.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
@@ -26,6 +27,8 @@ uint8_t input_read(Input* input) {
|
||||
} // namespace
|
||||
|
||||
void Snes::Init(std::vector<uint8_t>& rom_data) {
|
||||
LOG_INFO("SNES", "Initializing emulator with ROM size %zu bytes", rom_data.size());
|
||||
|
||||
// Initialize the CPU, PPU, and APU
|
||||
ppu_.Init();
|
||||
apu_.Init();
|
||||
@@ -35,9 +38,11 @@ void Snes::Init(std::vector<uint8_t>& rom_data) {
|
||||
Reset(true);
|
||||
|
||||
running_ = true;
|
||||
LOG_INFO("SNES", "Emulator initialization complete");
|
||||
}
|
||||
|
||||
void Snes::Reset(bool hard) {
|
||||
LOG_INFO("SNES", "Reset called (hard=%d)", hard);
|
||||
cpu_.Reset(hard);
|
||||
apu_.Reset();
|
||||
ppu_.Reset();
|
||||
@@ -75,19 +80,47 @@ void Snes::Reset(bool hard) {
|
||||
memory_.set_open_bus(0);
|
||||
next_horiz_event = 16;
|
||||
InitAccessTime(false);
|
||||
LOG_INFO("SNES", "Reset complete - CPU will start at $%02X:%04X", cpu_.PB, cpu_.PC);
|
||||
}
|
||||
|
||||
void Snes::RunFrame() {
|
||||
// Debug: Log every 60th frame
|
||||
static int frame_log_count = 0;
|
||||
if (frame_log_count % 60 == 0) {
|
||||
LOG_INFO("SNES", "Frame %d: CPU=$%02X:%04X vblank=%d frames_=%d",
|
||||
frame_log_count, cpu_.PB, cpu_.PC, in_vblank_, frames_);
|
||||
}
|
||||
frame_log_count++;
|
||||
|
||||
// Debug: Log vblank loop entry
|
||||
static int vblank_loop_count = 0;
|
||||
if (in_vblank_ && vblank_loop_count++ < 10) {
|
||||
LOG_INFO("SNES", "RunFrame: Entering vblank loop (in_vblank_=true)");
|
||||
}
|
||||
|
||||
while (in_vblank_) {
|
||||
cpu_.RunOpcode();
|
||||
}
|
||||
|
||||
uint32_t frame = frames_;
|
||||
|
||||
// Debug: Log active frame loop entry
|
||||
static int active_loop_count = 0;
|
||||
if (!in_vblank_ && active_loop_count++ < 10) {
|
||||
LOG_INFO("SNES", "RunFrame: Entering active frame loop (in_vblank_=false, frame=%d, frames_=%d)",
|
||||
frame, frames_);
|
||||
}
|
||||
|
||||
while (!in_vblank_ && frame == frames_) {
|
||||
cpu_.RunOpcode();
|
||||
}
|
||||
}
|
||||
|
||||
void Snes::CatchUpApu() { apu_.RunCycles(cycles_); }
|
||||
void Snes::CatchUpApu() {
|
||||
// Bring APU up to the same master cycle count since last catch-up.
|
||||
// cycles_ is monotonically increasing in RunCycle().
|
||||
apu_.RunCycles(cycles_);
|
||||
}
|
||||
|
||||
void Snes::HandleInput() {
|
||||
memset(port_auto_read_, 0, sizeof(port_auto_read_));
|
||||
@@ -180,6 +213,10 @@ void Snes::RunCycle() {
|
||||
bool starting_vblank = false;
|
||||
if (memory_.v_pos() == 0) {
|
||||
// end of vblank
|
||||
static int vblank_end_count = 0;
|
||||
if (vblank_end_count++ < 10) {
|
||||
LOG_INFO("SNES", "VBlank END - v_pos=0, setting in_vblank_=false at frame %d", frames_);
|
||||
}
|
||||
in_vblank_ = false;
|
||||
in_nmi_ = false;
|
||||
ppu_.HandleFrameStart();
|
||||
@@ -203,6 +240,13 @@ void Snes::RunCycle() {
|
||||
apu_.dsp().NewFrame();
|
||||
// we are starting vblank
|
||||
ppu_.HandleVblank();
|
||||
|
||||
static int vblank_start_count = 0;
|
||||
if (vblank_start_count++ < 10) {
|
||||
LOG_INFO("SNES", "VBlank START - v_pos=%d, setting in_vblank_=true at frame %d",
|
||||
memory_.v_pos(), frames_);
|
||||
}
|
||||
|
||||
in_vblank_ = true;
|
||||
in_nmi_ = true;
|
||||
if (auto_joy_read_) {
|
||||
@@ -210,6 +254,11 @@ void Snes::RunCycle() {
|
||||
auto_joy_timer_ = 4224;
|
||||
HandleInput();
|
||||
}
|
||||
static int nmi_log_count = 0;
|
||||
if (nmi_log_count++ < 10) {
|
||||
LOG_INFO("SNES", "VBlank NMI check: nmi_enabled_=%d, calling Nmi()=%s",
|
||||
nmi_enabled_, nmi_enabled_ ? "YES" : "NO");
|
||||
}
|
||||
if (nmi_enabled_) {
|
||||
cpu_.Nmi();
|
||||
}
|
||||
@@ -248,7 +297,18 @@ uint8_t Snes::ReadBBus(uint8_t adr) {
|
||||
}
|
||||
if (adr < 0x80) {
|
||||
CatchUpApu(); // catch up the apu before reading
|
||||
return apu_.out_ports_[adr & 0x3];
|
||||
uint8_t val = apu_.out_ports_[adr & 0x3];
|
||||
// Log port reads when value changes or during critical phase
|
||||
static int cpu_port_read_count = 0;
|
||||
static uint8_t last_f4 = 0xFF, last_f5 = 0xFF;
|
||||
bool value_changed = ((adr & 0x3) == 0 && val != last_f4) || ((adr & 0x3) == 1 && val != last_f5);
|
||||
if (value_changed || cpu_port_read_count++ < 50) {
|
||||
LOG_INFO("SNES", "CPU read APU port $21%02X (F%d) = $%02X at PC=$%02X:%04X [AFTER CatchUp: APU_cycles=%llu CPU_cycles=%llu]",
|
||||
0x40 + (adr & 0x3), (adr & 0x3) + 4, val, cpu_.PB, cpu_.PC, apu_.GetCycles(), cycles_);
|
||||
if ((adr & 0x3) == 0) last_f4 = val;
|
||||
if ((adr & 0x3) == 1) last_f5 = val;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
if (adr == 0x80) {
|
||||
uint8_t ret = ram[ram_adr_++];
|
||||
@@ -355,6 +415,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;
|
||||
static int cpu_port_write_count = 0;
|
||||
if (cpu_port_write_count++ < 200) { // Increased to see full boot sequence
|
||||
LOG_INFO("SNES", "CPU wrote APU port $21%02X (F%d) = $%02X at PC=$%02X:%04X",
|
||||
0x40 + (adr & 0x3), (adr & 0x3) + 4, val, cpu_.PB, cpu_.PC);
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (adr) {
|
||||
@@ -381,6 +446,14 @@ void Snes::WriteBBus(uint8_t adr, uint8_t val) {
|
||||
void Snes::WriteReg(uint16_t adr, uint8_t val) {
|
||||
switch (adr) {
|
||||
case 0x4200: {
|
||||
// Log ALL writes to $4200 unconditionally
|
||||
static int write_4200_count = 0;
|
||||
if (write_4200_count++ < 20) {
|
||||
LOG_INFO("SNES", "Write $%02X to $4200 at PC=$%02X:%04X (NMI=%d IRQ_H=%d IRQ_V=%d JOY=%d)",
|
||||
val, cpu_.PB, cpu_.PC, (val & 0x80) ? 1 : 0, (val & 0x10) ? 1 : 0,
|
||||
(val & 0x20) ? 1 : 0, (val & 0x01) ? 1 : 0);
|
||||
}
|
||||
|
||||
auto_joy_read_ = val & 0x1;
|
||||
if (!auto_joy_read_) auto_joy_timer_ = 0;
|
||||
h_irq_enabled_ = val & 0x10;
|
||||
@@ -393,7 +466,12 @@ void Snes::WriteReg(uint16_t adr, uint8_t val) {
|
||||
if (!nmi_enabled_ && (val & 0x80) && in_nmi_) {
|
||||
cpu_.Nmi();
|
||||
}
|
||||
bool old_nmi = nmi_enabled_;
|
||||
nmi_enabled_ = val & 0x80;
|
||||
if (old_nmi != nmi_enabled_) {
|
||||
LOG_INFO("SNES", ">>> NMI enabled CHANGED: %d -> %d <<<",
|
||||
old_nmi, nmi_enabled_);
|
||||
}
|
||||
cpu_.set_int_delay(true);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ if(YAZE_BUILD_TESTS AND NOT YAZE_BUILD_TESTS STREQUAL "OFF")
|
||||
add_executable(
|
||||
yaze_test
|
||||
yaze_test_ci.cc
|
||||
# Emulator unit tests
|
||||
unit/emu/apu_dsp_test.cc
|
||||
unit/emu/spc700_reset_test.cc
|
||||
test_editor.cc
|
||||
test_editor.h
|
||||
testing.h
|
||||
@@ -85,6 +88,9 @@ if(YAZE_BUILD_TESTS AND NOT YAZE_BUILD_TESTS STREQUAL "OFF")
|
||||
add_executable(
|
||||
yaze_test
|
||||
yaze_test.cc
|
||||
# Emulator unit tests
|
||||
unit/emu/apu_dsp_test.cc
|
||||
unit/emu/spc700_reset_test.cc
|
||||
test_editor.cc
|
||||
test_editor.h
|
||||
testing.h
|
||||
|
||||
74
test/unit/emu/apu_dsp_test.cc
Normal file
74
test/unit/emu/apu_dsp_test.cc
Normal file
@@ -0,0 +1,74 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
|
||||
class ApuDspTest : public ::testing::Test {
|
||||
protected:
|
||||
MemoryImpl mem;
|
||||
Apu* apu;
|
||||
|
||||
void SetUp() override {
|
||||
std::vector<uint8_t> dummy_rom(0x200000, 0);
|
||||
mem.Initialize(dummy_rom);
|
||||
apu = new Apu(mem);
|
||||
apu->Init();
|
||||
apu->Reset();
|
||||
}
|
||||
|
||||
void TearDown() override { delete apu; }
|
||||
};
|
||||
|
||||
TEST_F(ApuDspTest, DspRegistersReadWriteMirror) {
|
||||
// Select register 0x0C (MVOLL)
|
||||
apu->Write(0xF2, 0x0C);
|
||||
apu->Write(0xF3, 0x7F);
|
||||
// Read back
|
||||
apu->Write(0xF2, 0x0C);
|
||||
uint8_t mvoll = apu->Read(0xF3);
|
||||
EXPECT_EQ(mvoll, 0x7F);
|
||||
|
||||
// Select register 0x1C (MVOLR)
|
||||
apu->Write(0xF2, 0x1C);
|
||||
apu->Write(0xF3, 0x40);
|
||||
apu->Write(0xF2, 0x1C);
|
||||
uint8_t mvolr = apu->Read(0xF3);
|
||||
EXPECT_EQ(mvolr, 0x40);
|
||||
}
|
||||
|
||||
TEST_F(ApuDspTest, TimersEnableAndReadback) {
|
||||
// Enable timers 0 and 1, clear in-ports, map IPL off for RAM access
|
||||
apu->Write(0xF1, 0x03);
|
||||
|
||||
// Set timer targets
|
||||
apu->Write(0xFA, 0x04); // timer0 target
|
||||
apu->Write(0xFB, 0x02); // timer1 target
|
||||
|
||||
// Run enough SPC cycles via APU cycle stepping
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
apu->Cycle();
|
||||
}
|
||||
|
||||
// Read counters (auto-clears)
|
||||
uint8_t t0 = apu->Read(0xFD);
|
||||
uint8_t t1 = apu->Read(0xFE);
|
||||
// Should be within 0..15 and non-zero under these cycles
|
||||
EXPECT_LE(t0, 0x0F);
|
||||
EXPECT_LE(t1, 0x0F);
|
||||
}
|
||||
|
||||
TEST_F(ApuDspTest, GetSamplesReturnsSilenceAfterReset) {
|
||||
int16_t buffer[2 * 256]{};
|
||||
apu->dsp().GetSamples(buffer, 256, /*pal=*/false);
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
EXPECT_EQ(buffer[i * 2 + 0], 0);
|
||||
EXPECT_EQ(buffer[i * 2 + 1], 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
153
test/unit/emu/apu_ipl_handshake_test.cc
Normal file
153
test/unit/emu/apu_ipl_handshake_test.cc
Normal file
@@ -0,0 +1,153 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
#include "app/emu/audio/spc700.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
|
||||
class ApuIplHandshakeTest : public ::testing::Test {
|
||||
protected:
|
||||
MemoryImpl mem;
|
||||
Apu* apu;
|
||||
|
||||
void SetUp() override {
|
||||
std::vector<uint8_t> dummy_rom(0x200000, 0);
|
||||
mem.Initialize(dummy_rom);
|
||||
apu = new Apu(mem);
|
||||
apu->Init();
|
||||
apu->Reset();
|
||||
}
|
||||
|
||||
void TearDown() override { delete apu; }
|
||||
};
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, SPC700StartsAtIplRomEntry) {
|
||||
// After reset, PC should be at IPL ROM reset vector
|
||||
uint16_t reset_vector = apu->spc700().read(0xFFFE) |
|
||||
(apu->spc700().read(0xFFFF) << 8);
|
||||
|
||||
// The IPL ROM reset vector should point to 0xFFC0 (start of IPL ROM)
|
||||
EXPECT_EQ(reset_vector, 0xFFC0);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, IplRomReadable) {
|
||||
// IPL ROM should be readable at 0xFFC0-0xFFFF after reset
|
||||
uint8_t first_byte = apu->Read(0xFFC0);
|
||||
|
||||
// First byte of IPL ROM should be 0xCD (CMP Y, #$EF)
|
||||
EXPECT_EQ(first_byte, 0xCD);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, CycleTrackingWorks) {
|
||||
// Execute one SPC700 opcode
|
||||
apu->spc700().RunOpcode();
|
||||
|
||||
// GetLastOpcodeCycles should return a valid cycle count (2-12 typically)
|
||||
int cycles = apu->spc700().GetLastOpcodeCycles();
|
||||
EXPECT_GT(cycles, 0);
|
||||
EXPECT_LE(cycles, 12);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, PortReadWrite) {
|
||||
// Write to input port from CPU side (simulating CPU writes to $2140-$2143)
|
||||
apu->in_ports_[0] = 0xAA;
|
||||
apu->in_ports_[1] = 0xBB;
|
||||
|
||||
// SPC should be able to read these ports at $F4-$F7
|
||||
EXPECT_EQ(apu->Read(0xF4), 0xAA);
|
||||
EXPECT_EQ(apu->Read(0xF5), 0xBB);
|
||||
|
||||
// Write to output ports from SPC side
|
||||
apu->Write(0xF4, 0xCC);
|
||||
apu->Write(0xF5, 0xDD);
|
||||
|
||||
// CPU should be able to read these (simulating reads from $2140-$2143)
|
||||
EXPECT_EQ(apu->out_ports_[0], 0xCC);
|
||||
EXPECT_EQ(apu->out_ports_[1], 0xDD);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, IplRomDisableViaControlRegister) {
|
||||
// IPL ROM is readable by default
|
||||
EXPECT_EQ(apu->Read(0xFFC0), 0xCD);
|
||||
|
||||
// Write to control register ($F1) to disable IPL ROM (bit 7 = 1)
|
||||
apu->Write(0xF1, 0x80);
|
||||
|
||||
// Now $FFC0-$FFFF should read from RAM instead of IPL ROM
|
||||
// RAM is initialized to 0, so we should read 0
|
||||
EXPECT_EQ(apu->Read(0xFFC0), 0x00);
|
||||
|
||||
// Write something to RAM
|
||||
apu->ram[0xFFC0] = 0x42;
|
||||
EXPECT_EQ(apu->Read(0xFFC0), 0x42);
|
||||
|
||||
// Re-enable IPL ROM (bit 7 = 0)
|
||||
apu->Write(0xF1, 0x00);
|
||||
|
||||
// Should read IPL ROM again
|
||||
EXPECT_EQ(apu->Read(0xFFC0), 0xCD);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, TimersEnableAndCount) {
|
||||
// Enable timer 0 via control register
|
||||
apu->Write(0xF1, 0x01);
|
||||
|
||||
// Set timer 0 target to 4
|
||||
apu->Write(0xFA, 0x04);
|
||||
|
||||
// Run enough cycles to trigger timer
|
||||
for (int i = 0; i < 1000; ++i) {
|
||||
apu->Cycle();
|
||||
}
|
||||
|
||||
// Read timer 0 counter (auto-clears on read)
|
||||
uint8_t counter = apu->Read(0xFD);
|
||||
|
||||
// Counter should be non-zero if timer is working
|
||||
EXPECT_GT(counter, 0);
|
||||
EXPECT_LE(counter, 0x0F);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, IplBootSequenceProgresses) {
|
||||
// This test verifies that the IPL ROM boot sequence can actually progress
|
||||
// without getting stuck in an infinite loop
|
||||
|
||||
uint16_t initial_pc = apu->spc700().PC;
|
||||
|
||||
// Run multiple opcodes to let the IPL boot sequence progress
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
apu->spc700().RunOpcode();
|
||||
apu->Cycle();
|
||||
}
|
||||
|
||||
uint16_t final_pc = apu->spc700().PC;
|
||||
|
||||
// PC should have advanced (boot sequence is progressing)
|
||||
// If it's stuck in a tight loop, PC won't change much
|
||||
EXPECT_NE(initial_pc, final_pc);
|
||||
}
|
||||
|
||||
TEST_F(ApuIplHandshakeTest, AccurateCycleCountsForCommonOpcodes) {
|
||||
// Test that specific opcodes return correct cycle counts
|
||||
|
||||
// NOP (0x00) should take 2 cycles
|
||||
apu->spc700().PC = 0x0000;
|
||||
apu->ram[0x0000] = 0x00; // NOP
|
||||
apu->spc700().RunOpcode();
|
||||
apu->spc700().RunOpcode(); // Execute
|
||||
EXPECT_EQ(apu->spc700().GetLastOpcodeCycles(), 2);
|
||||
|
||||
// MOV A, #imm (0xE8) should take 2 cycles
|
||||
apu->spc700().PC = 0x0002;
|
||||
apu->ram[0x0002] = 0xE8; // MOV A, #imm
|
||||
apu->ram[0x0003] = 0x42; // immediate value
|
||||
apu->spc700().RunOpcode();
|
||||
apu->spc700().RunOpcode();
|
||||
EXPECT_EQ(apu->spc700().GetLastOpcodeCycles(), 2);
|
||||
}
|
||||
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
30
test/unit/emu/spc700_reset_test.cc
Normal file
30
test/unit/emu/spc700_reset_test.cc
Normal file
@@ -0,0 +1,30 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace emu {
|
||||
|
||||
TEST(Spc700ResetTest, ResetVectorExecutesIplSequence) {
|
||||
MemoryImpl mem;
|
||||
std::vector<uint8_t> dummy_rom(0x200000, 0);
|
||||
mem.Initialize(dummy_rom);
|
||||
|
||||
Apu apu(mem);
|
||||
apu.Init();
|
||||
apu.Reset();
|
||||
|
||||
// After reset, running some cycles should advance SPC PC from IPL entry
|
||||
uint16_t pc_before = apu.spc700().PC;
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
apu.spc700().RunOpcode();
|
||||
apu.Cycle();
|
||||
}
|
||||
uint16_t pc_after = apu.spc700().PC;
|
||||
EXPECT_NE(pc_after, pc_before);
|
||||
}
|
||||
|
||||
} // namespace emu
|
||||
} // namespace yaze
|
||||
|
||||
Reference in New Issue
Block a user