view src/console/Snes_Spc.cxx @ 254:cdf9756d1528 trunk

[svn] Port to NewVFS file probe.
author chainsaw
date Sat, 18 Nov 2006 11:00:22 -0800
parents 3da1b8942b8b
children fb513e10174e
line wrap: on
line source


// Game_Music_Emu 0.3.0. http://www.slack.net/~ant/

#include "Snes_Spc.h"

#include <assert.h>
#include <string.h>

/* Copyright (C) 2004-2006 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 */

#include BLARGG_SOURCE_BEGIN

// always in the future (CPU time can go over 0, but not by this much)
int const timer_disabled_time = 127;

Snes_Spc::Snes_Spc() : dsp( ram ), cpu( this, ram )
{
	timer [0].shift = 7; // 8 kHz
	timer [1].shift = 7; // 8 kHz
	timer [2].shift = 4; // 64 kHz
	
	// Put STOP instruction past end of memory to catch PC overflow.
	memset( ram + ram_size, 0xff, (sizeof ram) - ram_size );
}

// Load

blargg_err_t Snes_Spc::load_spc( const void* data, long size, bool clear_echo_ )
{
	struct spc_file_t {
		char    signature [27];
		char    unused [10];
		uint8_t pc [2];
		uint8_t a;
		uint8_t x;
		uint8_t y;
		uint8_t status;
		uint8_t sp;
		char    unused2 [212];
		uint8_t ram [0x10000];
		uint8_t dsp [128];
	};
	BOOST_STATIC_ASSERT( sizeof (spc_file_t) == spc_file_size );
	
	const spc_file_t* spc = (spc_file_t*) data;
	
	if ( size < spc_file_size )
		return "Not an SPC file";
	
	if ( strncmp( spc->signature, "SNES-SPC700 Sound File Data", 27 ) != 0 )
		return "Not an SPC file";
	
	registers_t regs;
	regs.pc = spc->pc [1] * 0x100 + spc->pc [0];
	regs.a = spc->a;
	regs.x = spc->x;
	regs.y = spc->y;
	regs.status = spc->status;
	regs.sp = spc->sp;
	
	const char* error = load_state( regs, spc->ram, spc->dsp );
	
	echo_accessed = false;
	
	if ( clear_echo_ )
		clear_echo();
	
	return error;
}

void Snes_Spc::clear_echo()
{
	if ( !(dsp.read( 0x6c ) & 0x20) )
	{
		unsigned addr = 0x100 * dsp.read( 0x6d );
		unsigned size = 0x800 * dsp.read( 0x7d );
		unsigned limit = ram_size - addr;
		memset( ram + addr, 0xff, (size < limit) ? size : limit );
	}
}

// Handle other file formats (emulator save states) in user code, not here.

blargg_err_t Snes_Spc::load_state( const registers_t& cpu_state, const void* new_ram,
		const void* dsp_state )
{
	// cpu
	cpu.r = cpu_state;
	
	// Allow DSP to generate one sample before code starts
	// (Tengai Makyo Zero, Tenjin's Table Toss first notes are lost since it
	// clears KON 31 cycles from starting execution. It works on the SNES
	// since the SPC player adds a few extra cycles delay after restoring
	// KON from the DSP registers at the end of an SPC file).
	extra_cycles = 32; 
	
	// ram
	memcpy( ram, new_ram, ram_size );
	memcpy( extra_ram, ram + rom_addr, sizeof extra_ram );
	
	// boot rom (have to force enable_rom() to update it)
	rom_enabled = !(ram [0xf1] & 0x80);
	enable_rom( !rom_enabled );
	
	// dsp
	dsp.reset();
	int i;
	for ( i = 0; i < Spc_Dsp::register_count; i++ )
		dsp.write( i, ((uint8_t*) dsp_state) [i] );
	
	// timers
	for ( i = 0; i < timer_count; i++ )
	{
		Timer& t = timer [i];
		
		t.next_tick = 0;
		t.enabled = (ram [0xf1] >> i) & 1;
		if ( !t.enabled )
			t.next_tick = timer_disabled_time;
		t.count = 0;
		t.counter = ram [0xfd + i] & 15;
		
		int p = ram [0xfa + i];
		t.period = p ? p : 0x100;
	}
	
	// Handle registers which already give 0 when read by setting RAM and not changing it.
	// Put STOP instruction in registers which can be read, to catch attempted CPU execution.
	ram [0xf0] = 0;
	ram [0xf1] = 0;
	ram [0xf3] = 0xff;
	ram [0xfa] = 0;
	ram [0xfb] = 0;
	ram [0xfc] = 0;
	ram [0xfd] = 0xff;
	ram [0xfe] = 0xff;
	ram [0xff] = 0xff;
	
	return NULL; // success
}

// Hardware

// Current time starts negative and ends at 0
inline spc_time_t Snes_Spc::time() const
{
	return -cpu.remain();
}

// Keep track of next time to run and avoid a function call if it hasn't been reached.

// Timers

void Snes_Spc::Timer::run_until_( spc_time_t time )
{
	if ( !enabled )
		dprintf( "next_tick: %ld, time: %ld", (long) next_tick, (long) time );
	assert( enabled ); // when disabled, next_tick should always be in the future
	
	int elapsed = ((time - next_tick) >> shift) + 1;
	next_tick += elapsed << shift;
	elapsed += count;
	if ( elapsed >= period ) { // avoid costly divide
		int n = elapsed / period;
		elapsed -= n * period;
		counter = (counter + n) & 15;
	}
	count = elapsed;
}

// DSP

const int clocks_per_sample = 32; // 1.024 MHz CPU clock / 32000 samples per second

void Snes_Spc::run_dsp_( spc_time_t time )
{
	int count = ((time - next_dsp) >> 5) + 1; // divide by clocks_per_sample
	sample_t* buf = sample_buf;
	if ( buf ) {
		sample_buf = buf + count * 2; // stereo
		assert( sample_buf <= buf_end );
	}
	next_dsp += count * clocks_per_sample;
	dsp.run( count, buf );
}

inline void Snes_Spc::run_dsp( spc_time_t time )
{
	if ( time >= next_dsp )
		run_dsp_( time );
}

// Debug-only check for read/write within echo buffer, since this might result in
// inaccurate emulation due to the DSP not being caught up to the present.
inline void Snes_Spc::check_for_echo_access( spc_addr_t addr )
{
	if ( !echo_accessed && !(dsp.read( 0x6c ) & 0x20) )
	{
		// ** If echo accesses are found that require running the DSP, cache
		// the start and end address on DSP writes to speed up checking.
		
		unsigned start = 0x100 * dsp.read( 0x6d );
		unsigned end = start + 0x800 * dsp.read( 0x7d );
		if ( start <= addr && addr < end ) {
			echo_accessed = true;
			dprintf( "Read/write at $%04X within echo buffer\n", (unsigned) addr );
		}
	}
}

// Read

int Snes_Spc::read( spc_addr_t addr )
{
	// zero page ram is used most often
	if ( addr < 0xf0 )
		return ram [addr];
	
	// dsp
	if ( addr == 0xf3 ) {
		run_dsp( time() );
		if ( ram [0xf2] >= Spc_Dsp::register_count )
			dprintf( "DSP read from $%02X\n", (int) ram [0xf2] );
		return dsp.read( ram [0xf2] & 0x7f );
	}
	
	// counters
	unsigned i = addr - 0xfd; // negative converts to large positive unsigned
	if ( i < timer_count ) {
		Timer& t = timer [i];
		t.run_until( time() );
		int result = t.counter;
		t.counter = 0;
		return result;
	}
	
	if ( addr == 0xf0 || addr == 0xf1 || addr == 0xf8 ||
			addr == 0xf9 || addr == 0xfa )
		dprintf( "Read from register $%02X\n", (int) addr );
	
	// Registers which always read as 0 are handled by setting ram [reg] to 0
	// at startup then never changing that value.
	
	check(( check_for_echo_access( addr ), true ));
	
	// ram
	return ram [addr];
}


// Write

const unsigned char Snes_Spc::boot_rom [rom_size] = { // verified
	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
};

void Snes_Spc::enable_rom( bool enable )
{
	if ( rom_enabled != enable )
	{
		rom_enabled = enable;
		memcpy( ram + rom_addr, (enable ? boot_rom : extra_ram), rom_size );
	}
}

void Snes_Spc::write( spc_addr_t addr, int data )
{
	// first page is very common
	if ( addr < 0xf0 ) {
		ram [addr] = data;
	}
	else switch ( addr )
	{
		// RAM
		default:
			check(( check_for_echo_access( addr ), true ));
			if ( addr < rom_addr ) {
				ram [addr] = data;
			}
			else {
				extra_ram [addr - rom_addr] = data;
				if ( !rom_enabled )
					ram [addr] = data;
			}
			break;
		
		// DSP
		//case 0xf2: // mapped to RAM
		case 0xf3: {
			run_dsp( time() );
			int reg = ram [0xf2];
			if ( next_dsp > 0 ) {
				// skip mode
				
				// key press
				if ( reg == 0x4C )
					keys_pressed |= data & ~dsp.read( 0x5C );
				
				// key release
				if ( reg == 0x5C ) {
					keys_released |= data;
					keys_pressed &= ~data;
				}
			}
			if ( reg < Spc_Dsp::register_count ) {
				dsp.write( reg, data );
			}
			else {
				dprintf( "DSP write to $%02X\n", (int) reg );
			}
			break;
		}
		
		case 0xf0: // Test register
			dprintf( "Wrote $%02X to $F0\n", (int) data );
			break;
		
		// Config
		case 0xf1:
		{
			// timers
			for ( int i = 0; i < timer_count; i++ )
			{
				Timer& t = timer [i];
				if ( !(data & (1 << i)) ) {
					t.enabled = 0;
					t.next_tick = timer_disabled_time;
				}
				else if ( !t.enabled ) {
					// just enabled
					t.enabled = 1;
					t.counter = 0;
					t.count = 0;
					t.next_tick = time();
				}
			}
			
			// port clears
			if ( data & 0x10 ) {
				ram [0xf4] = 0;
				ram [0xf5] = 0;
			}
			if ( data & 0x20 ) {
				ram [0xf6] = 0;
				ram [0xf7] = 0;
			}
			
			enable_rom( data & 0x80 );
			
			break;
		}
		
		// Ports
		case 0xf4:
		case 0xf5:
		case 0xf6:
		case 0xf7:
			// to do: handle output ports
			break;
		
		//case 0xf8: // verified on SNES that these are read/write (RAM)
		//case 0xf9:
		
		// Timers
		case 0xfa:
		case 0xfb:
		case 0xfc: {
			Timer& t = timer [addr - 0xfa];
			if ( (t.period & 0xff) != data ) {
				t.run_until( time() );
				t.period = data ? data : 0x100;
			}
			break;
		}
		
		// Counters (cleared on write)
		case 0xfd:
		case 0xfe:
		case 0xff:
			dprintf( "Wrote to counter $%02X\n", (int) addr );
			timer [addr - 0xfd].counter = 0;
			break;
	}
}

// Play

blargg_err_t Snes_Spc::skip( long count )
{
	if ( count > 4 * 32000L )
	{
		// don't run DSP for long durations (2-3 times faster)
		
		const long sync_count = 32000L * 2;
		
		// keep track of any keys pressed/released (and not subsequently released)
		keys_pressed = 0;
		keys_released = 0;
		// sentinel tells play to ignore DSP
		BLARGG_RETURN_ERR( play( count - sync_count, skip_sentinel ) );
		
		// press/release keys now
		dsp.write( 0x5C, keys_released & ~keys_pressed );
		dsp.write( 0x4C, keys_pressed );
		
		clear_echo();
		
		// play the last few seconds normally to help synchronize DSP
		count = sync_count;
	}
	
	return play( count );
}

blargg_err_t Snes_Spc::play( long count, sample_t* out )
{
	require( count % 2 == 0 ); // output is always in pairs of samples
	
	// CPU time() runs from -duration to 0
	spc_time_t duration = (count / 2) * clocks_per_sample;
	
	// DSP output is made on-the-fly when the CPU reads/writes DSP registers
	sample_buf = out;
	buf_end = out + (out && out != skip_sentinel ? count : 0);
	next_dsp = (out == skip_sentinel) ? clocks_per_sample : -duration + clocks_per_sample;
	
	// Localize timer next_tick times and run them to the present to prevent a running
	// but ignored timer's next_tick from getting too far behind and overflowing.
	for ( int i = 0; i < timer_count; i++ )
	{
		Timer& t = timer [i];
		if ( t.enabled )
		{
			t.next_tick -= duration;
			t.run_until( -duration );
		}
	}
	
	// Run CPU for duration, reduced by any extra cycles from previous run
	int elapsed = cpu.run( duration - extra_cycles );
	if ( elapsed > 0 )
	{
		dprintf( "Unhandled instruction $%02X, pc = $%04X\n",
				(int) cpu.read( cpu.r.pc ), (unsigned) cpu.r.pc );
		return "Emulation error";
	}
	extra_cycles = -elapsed;
	
	// Catch DSP up to present.
	run_dsp( 0 );
	if ( out ) {
		assert( next_dsp == clocks_per_sample );
		assert( out == skip_sentinel || sample_buf - out == count );
	}
	buf_end = NULL;
	
	return blargg_success;
}