summaryrefslogtreecommitdiff
path: root/plugins/gme/game-music-emu-0.6pre/gme/Nsf_Impl.cpp
blob: 53b3a41670c7de96fef7f27a3145b618c751dd7e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
// Game_Music_Emu 0.6-pre. http://www.slack.net/~ant/

#include "Nsf_Impl.h"

#include "blargg_endian.h"

/* Copyright (C) 2003-2008 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.h"

// number of frames until play interrupts init
int const initial_play_delay = 7; // KikiKaikai needed this to work
int const bank_size = 0x1000;
int const rom_addr  = 0x8000;

int Nsf_Impl::read_code( addr_t addr ) const
{
	return *cpu.get_code( addr );
}

int Nsf_Impl::pcm_read( void* self, int addr )
{
	return STATIC_CAST(Nsf_Impl*,self)->read_code( addr );
}

Nsf_Impl::Nsf_Impl() : rom( bank_size )
{
	apu.dmc_reader( pcm_read, this );
	assert( offsetof (header_t,unused [4]) == header_t::size );
}

void Nsf_Impl::unload()
{
	rom.clear();
	high_ram.clear();
	Gme_Loader::unload();
}

Nsf_Impl::~Nsf_Impl() { unload(); }

bool nsf_header_t::valid_tag() const
{
	return 0 == memcmp( tag, "NESM\x1A", 5 );
}

double nsf_header_t::clock_rate() const
{
	return pal_only() ? 1662607.125 : 1789772.727272727;
}

int nsf_header_t::play_period() const
{
	// NTSC
	int         clocks   = 29780;
	int         value    = 0x411A;
	byte const* rate_ptr = ntsc_speed;
	
	// PAL
	if ( pal_only() )
	{
		clocks   = 33247;
		value    = 0x4E20;
		rate_ptr = pal_speed;
	}
	
	// Default rate
	int rate = get_le16( rate_ptr );
	if ( rate == 0 )
		rate = value;
	
	// Custom rate
	if ( rate != value )
		clocks = (int) (rate * clock_rate() * (1.0/1000000.0));
	
	return clocks;
}

// Gets address, given pointer to it in file header. If zero, returns rom_addr.
Nsf_Impl::addr_t Nsf_Impl::get_addr( byte const in [] )
{
	addr_t addr = get_le16( in );
	if ( addr == 0 )
		addr = rom_addr;
	return addr;
}

blargg_err_t Nsf_Impl::load_( Data_Reader& in )
{
	// pad ROM data with 0
	RETURN_ERR( rom.load( in, header_.size, &header_, 0 ) );
	
	if ( !header_.valid_tag() )
		return blargg_err_file_type;
	
	RETURN_ERR( high_ram.resize( (fds_enabled() ? fdsram_offset + fdsram_size : fdsram_offset) ) );

	addr_t load_addr = get_addr( header_.load_addr );
	if ( load_addr < (fds_enabled() ? sram_addr : rom_addr) )
		set_warning( "Load address is too low" );
	
	rom.set_addr( load_addr % bank_size );
	
	if ( header_.vers != 1 )
		set_warning( "Unknown file version" );
	
	set_play_period( header_.play_period() );
	
	return blargg_ok;
}

void Nsf_Impl::write_bank( int bank, int data )
{
	// Find bank in ROM
	int offset = rom.mask_addr( data * bank_size );
	if ( offset >= rom.size() )
		special_event( "invalid bank" );
	void const* rom_data = rom.at_addr( offset );
	
	#if !NSF_EMU_APU_ONLY
		if ( bank < bank_count - fds_banks && fds_enabled() )
		{
			// TODO: FDS bank switching is kind of hacky, might need to
			// treat ROM as RAM so changes won't get lost when switching.
			byte* out = sram();
			if ( bank >= fds_banks )
			{
				out = fdsram();
				bank -= fds_banks;
			}
			memcpy( &out [bank * bank_size], rom_data, bank_size );
			return;
		}
	#endif
	
	if ( bank >= fds_banks )
		cpu.map_code( (bank + 6) * bank_size, bank_size, rom_data );
}

void Nsf_Impl::map_memory()
{
	// Map standard things
	cpu.reset( unmapped_code() );
	cpu.map_code( 0, 0x2000, low_ram, low_ram_size ); // mirrored four times
	cpu.map_code( sram_addr, sram_size, sram() );
	
	// Determine initial banks
	byte banks [bank_count];
	static byte const zero_banks [sizeof header_.banks] = { 0 };
	if ( memcmp( header_.banks, zero_banks, sizeof zero_banks ) )
	{
		banks [0] = header_.banks [6];
		banks [1] = header_.banks [7];
		memcpy( banks + fds_banks, header_.banks, sizeof header_.banks );
	}
	else
	{
		// No initial banks, so assign them based on load_addr
		int first_bank = (get_addr( header_.load_addr ) - sram_addr) / bank_size;
		unsigned total_banks = rom.size() / bank_size;
		for ( int i = bank_count; --i >= 0; )
		{
			int bank = i - first_bank;
			if ( (unsigned) bank >= total_banks )
				bank = 0;
			banks [i] = bank;
		}
	}
	
	// Map banks
	for ( int i = (fds_enabled() ? 0 : fds_banks); i < bank_count; ++i )
		write_bank( i, banks [i] );
	
	// Map FDS RAM
	if ( fds_enabled() )
		cpu.map_code( rom_addr, fdsram_size, fdsram() );
}

inline void Nsf_Impl::push_byte( int b )
{
	low_ram [0x100 + cpu.r.sp--] = b;
}

// Jumps to routine, given pointer to address in file header. Pushes idle_addr
// as return address, NOT old PC.
void Nsf_Impl::jsr_then_stop( byte const addr [] )
{
	cpu.r.pc = get_addr( addr );
	push_byte( (idle_addr - 1) >> 8 );
	push_byte( (idle_addr - 1) );
}

blargg_err_t Nsf_Impl::start_track( int track )
{
	int speed_flags = 0;
	#if NSF_EMU_EXTRA_FLAGS
		speed_flags = header().speed_flags;
	#endif
	
	apu.reset( header().pal_only(), (speed_flags & 0x20) ? 0x3F : 0 );
	apu.write_register( 0, 0x4015, 0x0F );
	apu.write_register( 0, 0x4017, (speed_flags & 0x10) ? 0x80 : 0 );
	
	// Clear memory
	memset( unmapped_code(), Nes_Cpu::halt_opcode, unmapped_size );
	memset( low_ram, 0, low_ram_size );
	memset( sram(), 0, sram_size );
	
	map_memory();
	
	// Arrange time of first call to play routine
	play_extra = 0;
	next_play  = play_period;
	
	play_delay = initial_play_delay;
	saved_state.pc = idle_addr;
	
	// Setup for call to init routine
	cpu.r.a  = track;
	cpu.r.x  = header_.pal_only();
	cpu.r.sp = 0xFF;
	jsr_then_stop( header_.init_addr );
	if ( cpu.r.pc < get_addr( header_.load_addr ) )
		set_warning( "Init address < load address" );
	
	return blargg_ok;
}

void Nsf_Impl::unmapped_write( addr_t addr, int data )
{
	dprintf( "Unmapped write $%04X <- %02X\n", (int) addr, data );
}

int Nsf_Impl::unmapped_read( addr_t addr )
{
	dprintf( "Unmapped read $%04X\n", (int) addr );
	return addr >> 8;
}

void Nsf_Impl::special_event( const char str [] )
{
	dprintf( "%s\n", str );
}

void Nsf_Impl::run_once( time_t end )
{
	// Emulate until next play call if possible
	if ( run_cpu_until( min( next_play, end ) ) )
	{
		// Halt instruction encountered
		
		if ( cpu.r.pc != idle_addr )
		{
			special_event( "illegal instruction" );
			cpu.count_error();
			cpu.set_time( cpu.end_time() );
			return;
		}

		// Init/play routine returned
		play_delay = 1; // play can now be called regularly
		
		if ( saved_state.pc == idle_addr )
		{
			// nothing to run
			time_t t = cpu.end_time();
			if ( cpu.time() < t )
				cpu.set_time( t );
		}
		else
		{
			// continue init routine that was interrupted by play routine
			cpu.r = saved_state;
			saved_state.pc = idle_addr;
		}
	}
	
	if ( time() >= next_play )
	{
		// Calculate time of next call to play routine
		play_extra ^= 1; // extra clock every other call
		next_play += play_period + play_extra;
		
		// Call routine if ready
		if ( play_delay && !--play_delay )
		{
			// Save state if init routine is still running
			if ( cpu.r.pc != idle_addr )
			{
				check( saved_state.pc == idle_addr );
				saved_state = cpu.r;
				special_event( "play called during init" );
			}
			
			jsr_then_stop( header_.play_addr );
		}
	}
}

void Nsf_Impl::run_until( time_t end )
{
	while ( time() < end )
		run_once( end );
}

void Nsf_Impl::end_frame( time_t end )
{
	if ( time() < end )
		run_until( end );
	cpu.adjust_time( -end );
	
	// Localize to new time frame
	next_play -= end;
	check( next_play >= 0 );
	if ( next_play < 0 )
		next_play = 0;
	
	apu.end_frame( end );
}