BizHawk/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/Boards/NSFBoard.cs

401 lines
10 KiB
C#

using System;
using BizHawk.Common;
//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.
//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
//NSF:
//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?
//TODO - some NSF players know when a song ends and skip to the next one.. how do they know?
namespace BizHawk.Emulation.Cores.Nintendo.NES
{
[NES.INESBoardImplCancel]
public sealed class NSFBoard : NES.NESBoardBase
{
//------------------------------
//configuration
internal NSFFormat nsf;
/// <summary>
/// Whether the NSF is bankswitched
/// </summary>
bool BankSwitched;
/// <summary>
/// the bankswitch values to be used before the INIT routine is called
/// </summary>
byte[] InitBankSwitches = new byte[8];
/// <summary>
/// An image of the entire PRG space where the unmapped files are located
/// </summary>
byte[] FakePRG = new byte[32768];
//------------------------------
//state
/// <summary>
/// PRG bankswitching
/// </summary>
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;
/// <summary>
/// Current 1-indexed song number (1 is the first song)
/// </summary>
int CurrentSong;
/// <summary>
/// Whether the INIT routine needs to be called
/// </summary>
bool InitPending;
/// <summary>
/// Previous button state for button press handling
/// </summary>
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();
}
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);
}
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);
//analyze bankswitch configuration. fix broken configurations
BankSwitched = false;
for (int i = 0; i < 8; i++)
{
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)
BankSwitched = true;
}
//if bit bankswitched, set up the fake PRG with the NSF data at the correct load address
if (!BankSwitched)
{
//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);
}
CurrentSong = nsf.StartingSong;
ReplayInit();
}
void ReplayInit()
{
Console.WriteLine("NSF: Playing track {0}/{1}", CurrentSong, nsf.TotalSongs-1);
InitPending = true;
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)
{
switch (addr)
{
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);
}
}
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;
//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
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
0xA9,0x00,
//3816:TAX
0xAA,
//3817:TAY
0xA8,
//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
0x18,
//3821:BCC $XX24
0x90,0xFE,
};
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)
{
if (CurrentSong < nsf.TotalSongs - 1)
{
CurrentSong++;
reset = true;
}
}
if (left)
{
if (CurrentSong > 0)
{
CurrentSong--;
reset = true;
}
}
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)
{
int bank_4k = addr >> 12;
int ofs = addr & ((1 << 12) - 1);
bank_4k = prg_banks_4k[bank_4k];
addr = (bank_4k << 12) | ofs;
//rom data began at 0x80 of the NSF file
addr += 0x80;
return ROM[addr];
}
else
{
return FakePRG[addr];
}
}
}
}
}