// Core SPC emulation: CPU, timers, SMP registers, memory // snes_spc 0.9.0. http://www.slack.net/~ant/ #include #include #include "SNES_SPC.h" /* Copyright (C) 2004-2007 Shay Green. This module is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This module is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this module; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ //// Timers #if SPC_DISABLE_TEMPO template static inline T TIMER_DIV(SNES_SPC::Timer *t, const T &n) { return n >> t->prescaler; } template static inline T TIMER_MUL(SNES_SPC::Timer *t, const T &n) { return n << t->prescaler; } #else template static inline T TIMER_DIV(SNES_SPC::Timer *t, const T &n) { return n / t->prescaler; } template static inline T TIMER_MUL(SNES_SPC::Timer *t, const T &n) { return n * t->prescaler; } #endif auto SNES_SPC::run_timer_(Timer *t, rel_time_t time) -> Timer * { int elapsed = TIMER_DIV(t, time - t->next_time) + 1; t->next_time += TIMER_MUL(t, elapsed); if (t->enabled) { int remain = IF_0_THEN_256(t->period - t->divider); int divider = t->divider + elapsed; int over = elapsed - remain; if (over >= 0) { int n = over / t->period; t->counter = (t->counter + 1 + n) & 0x0F; divider = over - n * t->period; } t->divider = static_cast(divider); } return t; } auto SNES_SPC::run_timer(Timer *t, rel_time_t time) -> Timer * { if (time >= t->next_time) t = this->run_timer_(t, time); return t; } //// ROM void SNES_SPC::enable_rom(bool enable) { if (this->m.rom_enabled != enable) { this->m.rom_enabled = this->dsp.rom_enabled = enable; if (enable) std::copy_n(&this->m.ram.ram[rom_addr], static_cast(rom_size), &this->m.hi_ram[0]); auto data = enable ? &this->m.rom[0] : &this->m.hi_ram[0]; std::copy_n(&data[0], static_cast(rom_size), &this->m.ram.ram[rom_addr]); // TODO: ROM can still get overwritten when DSP writes to echo buffer } } //// DSP void SNES_SPC::RUN_DSP(rel_time_t time) { int count = time - this->m.dsp_time; if (count) { assert(count > 0); this->m.dsp_time = time; this->dsp.run(count); } } int SNES_SPC::dsp_read(rel_time_t time) { this->RUN_DSP(time); int result = this->dsp.read(this->m.smp_regs[0][r_dspaddr] & 0x7F); #ifdef SPC_DSP_READ_HOOK SPC_DSP_READ_HOOK(spc_time + time, this->m.smp_regs[0][r_dspaddr] & 0x7F, result); #endif return result; } void SNES_SPC::dsp_write(int data, rel_time_t time) { this->RUN_DSP(time); #ifdef SPC_DSP_WRITE_HOOK SPC_DSP_WRITE_HOOK(this->m.spc_time + time, this->m.smp_regs[0][r_dspaddr], static_cast(data)); #endif if (this->m.smp_regs[0][r_dspaddr] <= 0x7F) this->dsp.write(this->m.smp_regs[0][r_dspaddr], data); } //// CPU write // divided into multiple functions to keep rarely-used functionality separate // so often-used functionality can be optimized better by compiler // If write isn't preceded by read, data has this added to it static const int no_read_before_write = 0x2000; void SNES_SPC::cpu_write_smp_reg_(int data, rel_time_t time, int addr) { switch (addr) { case r_t0target: case r_t1target: case r_t2target: { auto t = &this->m.timers[addr - r_t0target]; int period = IF_0_THEN_256(data); if (t->period != period) { t = this->run_timer(t, time); t->period = period; } break; } case r_t0out: case r_t1out: case r_t2out: if (data < no_read_before_write / 2) this->run_timer(&this->m.timers[addr - r_t0out], time - 1)->counter = 0; break; // Registers that act like RAM case 0x8: case 0x9: this->m.smp_regs[1][addr] = static_cast(data); break; case r_test: break; case r_control: // port clears if (data & 0x10) { this->m.smp_regs[1][r_cpuio0] = 0; this->m.smp_regs[1][r_cpuio1] = 0; } if (data & 0x20) { this->m.smp_regs[1][r_cpuio2] = 0; this->m.smp_regs[1][r_cpuio3] = 0; } // timers for (int i = 0; i < timer_count; ++i) { auto t = &this->m.timers[i]; bool enabled = !!((data >> i) & 1); if (t->enabled != enabled) { t = this->run_timer(t, time); t->enabled = enabled; if (enabled) t->divider = t->counter = 0; } } this->enable_rom(!!(data & 0x80)); } } void SNES_SPC::cpu_write_smp_reg(int data, rel_time_t time, int addr) { if (addr == r_dspdata) // 99% this->dsp_write(data, time); else this->cpu_write_smp_reg_(data, time, addr); } void SNES_SPC::cpu_write_high(int data, int i, rel_time_t time) { if (i < rom_size) { this->m.hi_ram [i] = static_cast(data); if (this->m.rom_enabled) this->m.ram.ram[i + rom_addr] = this->m.rom[i]; // restore overwritten ROM } else { assert(this->m.ram.ram[i + rom_addr] == static_cast(data)); this->m.ram.ram[i + rom_addr] = cpu_pad_fill; // restore overwritten padding this->cpu_write(data, i + rom_addr - 0x10000, time); } } static const int bits_in_int = CHAR_BIT * sizeof(int); void SNES_SPC::cpu_write(int data, int addr, rel_time_t time) { // RAM this->m.ram.ram[addr] = static_cast(data); int reg = addr - 0xF0; if (reg >= 0) // 64% { // $F0-$FF if (reg < reg_count) // 87% { this->m.smp_regs[0][reg] = static_cast(data); // Ports #ifdef SPC_PORT_WRITE_HOOK if (static_cast(reg - r_cpuio0) < port_count) SPC_PORT_WRITE_HOOK(this->m.spc_time + time, (reg - r_cpuio0), static_cast(data), &this->m.smp_regs[0][r_cpuio0]); #endif // Registers other than $F2 and $F4-$F7 //if ( reg != 2 && reg != 4 && reg != 5 && reg != 6 && reg != 7 ) // TODO: this is a bit on the fragile side if (((~0x2F00 << (bits_in_int - 16)) << reg) < 0) // 36% this->cpu_write_smp_reg(data, time, reg); } // High mem/address wrap-around else { reg -= rom_addr - 0xF0; if (reg >= 0) // 1% in IPL ROM area or address wrapped around this->cpu_write_high(data, reg, time); } } } //// CPU read int SNES_SPC::cpu_read_smp_reg(int reg, rel_time_t time) { int result = this->m.smp_regs[1][reg]; reg -= r_dspaddr; // DSP addr and data if (static_cast(reg) <= 1) // 4% 0xF2 and 0xF3 { result = this->m.smp_regs[0][r_dspaddr]; if (static_cast(reg) == 1) result = this->dsp_read(time); // 0xF3 } return result; } int SNES_SPC::cpu_read(int addr, rel_time_t time) { // RAM int result = this->m.ram.ram[addr]; int reg = addr - 0xF0; if (reg >= 0) // 40% { reg -= 0x10; if (static_cast(reg) >= 0xFF00) // 21% { reg += 0x10 - r_t0out; // Timers if (static_cast(reg) < timer_count) // 90% { auto t = &this->m.timers[reg]; if (time >= t->next_time) t = this->run_timer_(t, time); result = t->counter; t->counter = 0; } // Other registers else if (reg < 0) // 10% result = this->cpu_read_smp_reg(reg + r_t0out, time); else // 1% { assert(reg + (r_t0out + 0xF0 - 0x10000) < 0x100); result = this->cpu_read(reg + (r_t0out + 0xF0 - 0x10000), time); } } } return result; } //// Run static const int cpu_lag_max = 12 - 1; // DIV YA,X takes 12 clocks void SNES_SPC::end_frame(time_t end_time) { // Catch CPU up to as close to end as possible. If final instruction // would exceed end, does NOT execute it and leaves m.spc_time < end. if (end_time > this->m.spc_time) this->run_until_(end_time); this->m.spc_time -= end_time; this->m.extra_clocks += end_time; // Greatest number of clocks early that emulation can stop early due to // not being able to execute current instruction without going over // allowed time. assert(-cpu_lag_max <= this->m.spc_time && this->m.spc_time <= cpu_lag_max); // Catch timers up to CPU for (int i = 0; i < timer_count; ++i) this->run_timer(&this->m.timers [i], 0); // Catch DSP up to CPU if (this->m.dsp_time < 0) this->RUN_DSP(0); // Save any extra samples beyond what should be generated if (this->m.buf_begin) this->save_extra(); } // Inclusion here allows static memory access functions and better optimization #include "SPC_CPU.h"