2015-03-11 21:19:47 +00:00
using System ;
2015-03-11 09:46:27 +00:00
using BizHawk.Common ;
2015-03-11 21:19:47 +00:00
//NSF ROM and general approaches are heavily derived from FCEUX. the general ideas:
//1. Have a hardcoded NSF driver rom loaded to 0x3800
//2. Have fake registers at $3FFx for the NSF driver to use
//3. These addresses are chosen because no known NSF could possibly use them for anything.
2015-06-16 10:00:11 +00:00
//4. Patch the PRG with our own IRQ vectors when the NSF play and init routines aren't running.
// That way we can use NMI for overall control and cause our code to be the NMI handler without breaking the NSF data by corrupting the last few bytes
2015-03-11 09:46:27 +00:00
2015-03-11 21:19:47 +00:00
//NSF:
2015-03-11 09:46:27 +00:00
//check nsfspec.txt for more on why FDS is weird. lets try not following FCEUX too much there.
//TODO - add a sleep mode to the cpu and patch the rom program to use it?
2015-03-11 21:19:47 +00:00
//TODO - some NSF players know when a song ends and skip to the next one.. how do they know?
2015-03-11 09:46:27 +00:00
namespace BizHawk.Emulation.Cores.Nintendo.NES
{
[NES.INESBoardImplCancel]
public sealed class NSFBoard : NES . NESBoardBase
{
2015-03-11 21:19:47 +00:00
//------------------------------
2015-03-11 09:46:27 +00:00
//configuration
2015-03-11 21:19:47 +00:00
2015-03-11 09:46:27 +00:00
internal NSFFormat nsf ;
2015-03-11 21:19:47 +00:00
/// <summary>
/// Whether the NSF is bankswitched
/// </summary>
bool BankSwitched ;
/// <summary>
/// the bankswitch values to be used before the INIT routine is called
/// </summary>
2015-03-11 09:46:27 +00:00
byte [ ] InitBankSwitches = new byte [ 8 ] ;
2015-03-11 21:19:47 +00:00
/// <summary>
/// An image of the entire PRG space where the unmapped files are located
/// </summary>
2015-03-11 09:46:27 +00:00
byte [ ] FakePRG = new byte [ 32768 ] ;
//------------------------------
//state
2015-03-11 21:19:47 +00:00
/// <summary>
/// PRG bankswitching
/// </summary>
2015-03-11 09:46:27 +00:00
IntBuffer prg_banks_4k = new IntBuffer ( 8 ) ;
/// <summary>
/// whether vectors are currently patched. they should not be patched when running init/play routines because data from the ends of banks might get used
/// </summary>
bool Patch_Vectors ;
2015-03-11 21:19:47 +00:00
/// <summary>
/// Current 1-indexed song number (1 is the first song)
/// </summary>
2015-03-11 09:46:27 +00:00
int CurrentSong ;
2015-03-11 21:19:47 +00:00
/// <summary>
/// Whether the INIT routine needs to be called
/// </summary>
bool InitPending ;
/// <summary>
/// Previous button state for button press handling
/// </summary>
2015-03-11 09:46:27 +00:00
int ButtonState ;
public override bool Configure ( NES . EDetectionOrigin origin )
{
Cart . wram_size = 8 ;
return true ;
}
public override void Dispose ( )
{
prg_banks_4k . Dispose ( ) ;
base . Dispose ( ) ;
}
2015-03-11 21:19:47 +00:00
public override void SyncState ( Serializer ser )
{
base . SyncState ( ser ) ;
ser . Sync ( "prg_banks_4k" , ref prg_banks_4k ) ;
ser . Sync ( "Patch_Vectors" , ref Patch_Vectors ) ;
ser . Sync ( "CurrentSong" , ref CurrentSong ) ;
ser . Sync ( "InitPending" , ref InitPending ) ;
ser . Sync ( "ButtonState" , ref ButtonState ) ;
}
2015-03-11 09:46:27 +00:00
public void InitNSF ( NSFFormat nsf )
{
this . nsf = nsf ;
//patch the NSF rom with the init and play addresses
NSFROM [ 0x12 ] = ( byte ) ( nsf . InitAddress ) ;
NSFROM [ 0x13 ] = ( byte ) ( nsf . InitAddress > > 8 ) ;
NSFROM [ 0x19 ] = ( byte ) ( nsf . PlayAddress ) ;
NSFROM [ 0x1A ] = ( byte ) ( nsf . PlayAddress > > 8 ) ;
2015-03-11 21:19:47 +00:00
//analyze bankswitch configuration. fix broken configurations
2015-03-11 09:46:27 +00:00
BankSwitched = false ;
for ( int i = 0 ; i < 8 ; i + + )
{
2015-03-11 21:19:47 +00:00
int bank = nsf . BankswitchInitValues [ i ] ;
//discard out of range bankswitches.. for example, Balloon Fight is 3120B but has initial bank settings set to 0,0,0,0,0,1,0
if ( bank * 4096 > nsf . NSFData . Length - 0x80 )
bank = 0 ;
InitBankSwitches [ i ] = ( byte ) bank ;
if ( bank ! = 0 )
2015-03-11 09:46:27 +00:00
BankSwitched = true ;
}
2015-03-11 21:19:47 +00:00
//if bit bankswitched, set up the fake PRG with the NSF data at the correct load address
2015-03-11 09:46:27 +00:00
if ( ! BankSwitched )
{
2015-03-11 21:19:47 +00:00
//copy to load address
int load_start = nsf . LoadAddress - 0x8000 ;
int load_size = nsf . NSFData . Length - 0x80 ;
Buffer . BlockCopy ( nsf . NSFData , 0x80 , FakePRG , load_start , load_size ) ;
2015-03-11 09:46:27 +00:00
}
CurrentSong = nsf . StartingSong ;
2015-12-31 18:52:09 +00:00
ReplayInit ( ) ;
2015-03-11 09:46:27 +00:00
}
void ReplayInit ( )
{
2015-12-31 18:52:09 +00:00
Console . WriteLine ( "NSF: Playing track {0}/{1}" , CurrentSong , nsf . TotalSongs - 1 ) ;
2015-03-11 21:19:47 +00:00
InitPending = true ;
2015-03-11 09:46:27 +00:00
Patch_Vectors = true ;
}
public override void NESSoftReset ( )
{
ReplayInit ( ) ;
}
public override void WriteEXP ( int addr , byte value )
{
switch ( addr )
{
case 0x1FF6 :
case 0x1FF7 :
//if (!(NSFHeader.SoundChip & 4)) return; //FDS
break ;
case 0x1FF8 :
case 0x1FF9 :
case 0x1FFA :
case 0x1FFB :
case 0x1FFC :
case 0x1FFD :
case 0x1FFE :
case 0x1FFF :
if ( ! BankSwitched ) break ;
addr - = 0x1FF8 ;
prg_banks_4k [ addr ] = value ;
break ;
}
}
public override byte PeekReg2xxx ( int addr )
{
if ( addr < 0x3FF0 )
return NSFROM [ addr - 0x3800 ] ;
else return base . PeekReg2xxx ( addr ) ;
}
public override byte ReadReg2xxx ( int addr )
{
if ( addr < 0x3800 )
return base . ReadReg2xxx ( addr ) ;
else if ( addr > = 0x3FF0 )
{
2015-03-11 21:19:47 +00:00
switch ( addr )
2015-03-11 09:46:27 +00:00
{
2015-03-11 21:19:47 +00:00
case 0x3FF0 :
{
byte ret = 0 ;
if ( InitPending ) ret = 1 ;
InitPending = false ;
return ret ;
}
case 0x3FF1 :
{
//kevtris's reset process seems not to work. dunno what all is going on in there
//our own innovation, should work OK..
NES . apu . NESSoftReset ( ) ;
//mostly fceux's guidance
NES . WriteMemory ( 0x4015 , 0 ) ;
for ( int i = 0 ; i < 14 ; i + + )
NES . WriteMemory ( ( ushort ) ( 0x4000 + i ) , 0 ) ;
NES . WriteMemory ( 0x4015 , 0x0F ) ;
//clearing APU misc stuff, maybe not needed with soft reset above
//NES.WriteMemory(0x4017, 0xC0);
//NES.WriteMemory(0x4017, 0xC0);
//NES.WriteMemory(0x4017, 0x40);
//important to NSF standard for ram to be cleared, otherwise replayers are confused on account of not initializing memory themselves
var ram = NES . ram ;
var wram = this . WRAM ;
int wram_size = wram . Length ;
for ( int i = 0 ; i < 0x800 ; i + + )
ram [ i ] = 0 ;
for ( int i = 0 ; i < wram_size ; i + + )
wram [ i ] = 0 ;
//store specified initial bank state
if ( BankSwitched )
for ( int i = 0 ; i < 8 ; i + + )
WriteEXP ( 0x5FF8 + i - 0x4000 , InitBankSwitches [ i ] ) ;
return ( byte ) ( CurrentSong - 1 ) ;
}
case 0x3FF2 :
return 0 ; //always return NTSC for now
case 0x3FF3 :
Patch_Vectors = false ;
return 0 ;
case 0x3FF4 :
Patch_Vectors = true ;
return 0 ;
default :
return base . ReadReg2xxx ( addr ) ;
2015-03-11 09:46:27 +00:00
}
}
else if ( addr - 0x3800 < NSFROM . Length ) return NSFROM [ addr - 0x3800 ] ;
else return base . ReadReg2xxx ( addr ) ;
}
const ushort NMI_VECTOR = 0x3800 ;
const ushort RESET_VECTOR = 0x3820 ;
2015-03-11 21:19:47 +00:00
//readable registers
//3FF0 - InitPending (cleared on read)
//3FF1 - NextSong (also performs reset process - clears APU, RAM, etc)
//3FF2 - PAL flag
//3FF3 - PatchVectors=false
//3FF4 - PatchVectors=true
2015-03-11 09:46:27 +00:00
2015-03-11 21:19:47 +00:00
byte [ ] NSFROM = new byte [ 0x23 ]
{
//@NMIVector
//Suspend vector patching
//3800:LDA $3FF3
0xAD , 0xF3 , 0x3F ,
//Initialize stack pointer
//3803:LDX #$FF
0xA2 , 0xFF ,
//3805:TXS
0x9A ,
//Check (and clear) InitPending flag
//3806:LDA $3FF0
0xAD , 0xF0 , 0x3F ,
//3809:BEQ $8014
0xF0 , 0x09 ,
//Read the next song (resetting the player) and PAL flag into A and X and then call the INIT routine
//380B:LDA $3FF1
0xAD , 0xF1 , 0x3F ,
//380E:LDX $3FF2
0xAE , 0xF2 , 0x3F ,
//3811:JSR INIT
0x20 , 0x00 , 0x00 ,
//Fall through to:
//@Play - call PLAY routine with X and Y cleared (this is not supposed to be required, but fceux did it)
//3814:LDA #$00
2015-03-11 09:46:27 +00:00
0xA9 , 0x00 ,
2015-03-11 21:19:47 +00:00
//3816:TAX
2015-03-11 09:46:27 +00:00
0xAA ,
2015-03-11 21:19:47 +00:00
//3817:TAY
2015-03-11 09:46:27 +00:00
0xA8 ,
2015-03-11 21:19:47 +00:00
//3818:JSR PLAY
0x20 , 0x00 , 0x00 ,
//Resume vector patching and infinite loop waiting for next NMI
//381B:LDA $3FF4
0xAD , 0xF4 , 0x3F ,
//381E:BCC $XX1E
0x90 , 0xFE ,
//@ResetVector - just set up an infinite loop waiting for the first NMI
//3820:CLC
2015-03-11 09:46:27 +00:00
0x18 ,
2015-03-11 21:19:47 +00:00
//3821:BCC $XX24
0x90 , 0xFE ,
2015-03-11 09:46:27 +00:00
} ;
public override void AtVsyncNMI ( )
{
if ( Patch_Vectors )
NES . cpu . NMI = true ;
//strobe pad
NES . WriteMemory ( 0x4016 , 1 ) ;
NES . WriteMemory ( 0x4016 , 0 ) ;
//read pad and create rising edge button signals so we dont trigger events as quickly as we hold the button down
int currButtons = 0 ;
for ( int i = 0 ; i < 8 ; i + + )
{
currButtons < < = 1 ;
currButtons | = ( NES . ReadMemory ( 0x4016 ) & 1 ) ;
}
int justDown = ( ~ ButtonState ) & currButtons ;
Bit a = ( justDown > > 7 ) & 1 ;
Bit b = ( justDown > > 6 ) & 1 ;
Bit sel = ( justDown > > 5 ) & 1 ;
Bit start = ( justDown > > 4 ) & 1 ;
Bit up = ( justDown > > 3 ) & 1 ;
Bit down = ( justDown > > 2 ) & 1 ;
Bit left = ( justDown > > 1 ) & 1 ;
Bit right = ( justDown > > 0 ) & 1 ;
ButtonState = currButtons ;
//RIGHT: next song
//LEFT: prev song
//A: restart song
bool reset = false ;
if ( right )
{
2015-12-31 18:52:09 +00:00
if ( CurrentSong < nsf . TotalSongs - 1 )
{
CurrentSong + + ;
reset = true ;
}
2015-03-11 09:46:27 +00:00
}
if ( left )
{
2015-12-31 18:52:09 +00:00
if ( CurrentSong > 0 )
{
CurrentSong - - ;
reset = true ;
}
2015-03-11 09:46:27 +00:00
}
if ( a )
reset = true ;
if ( reset )
{
ReplayInit ( ) ;
}
}
public override byte ReadPPU ( int addr )
{
return 0 ;
}
public override byte ReadWRAM ( int addr )
{
return base . ReadWRAM ( addr ) ;
}
public override byte ReadPRG ( int addr )
{
//patch in vector reading
if ( Patch_Vectors )
{
if ( addr = = 0x7FFA ) return ( byte ) ( NMI_VECTOR & 0xFF ) ;
else if ( addr = = 0x7FFB ) return ( byte ) ( ( NMI_VECTOR > > 8 ) & 0xFF ) ;
else if ( addr = = 0x7FFC ) return ( byte ) ( RESET_VECTOR & 0xFF ) ;
else if ( addr = = 0x7FFD ) { return ( byte ) ( ( RESET_VECTOR > > 8 ) & 0xFF ) ; }
return NES . DB ;
}
else
{
if ( BankSwitched )
{
2015-03-11 21:19:47 +00:00
int bank_4k = addr > > 12 ;
int ofs = addr & ( ( 1 < < 12 ) - 1 ) ;
bank_4k = prg_banks_4k [ bank_4k ] ;
addr = ( bank_4k < < 12 ) | ofs ;
2015-03-11 09:46:27 +00:00
//rom data began at 0x80 of the NSF file
addr + = 0x80 ;
return ROM [ addr ] ;
}
2015-03-11 21:19:47 +00:00
else
{
return FakePRG [ addr ] ;
}
2015-03-11 09:46:27 +00:00
}
}
}
}