From 10dbd9bafbbed0d2f2bd0f7cb0bac21b74baafd9 Mon Sep 17 00:00:00 2001 From: zeromus Date: Wed, 11 Mar 2015 21:19:47 +0000 Subject: [PATCH] nsf - big cleanup and fix a lot of games and add savestates --- .../Consoles/Nintendo/NES/Boards/NSFBoard.cs | 324 ++++++++++-------- 1 file changed, 173 insertions(+), 151 deletions(-) diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/Boards/NSFBoard.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/Boards/NSFBoard.cs index 4bd79ffb84..e04edb220f 100644 --- a/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/Boards/NSFBoard.cs +++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/Boards/NSFBoard.cs @@ -1,27 +1,49 @@ - -using System; +using System; + using BizHawk.Common; -//NSF ROM and general approaches are taken from FCEUX. however, i've improvised/simplified/broken things so the rom is doing some pointless stuff now. +//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. +//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? -//some NSF players know when a song ends. +//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; - byte[] InitBankSwitches = new byte[8]; - byte[] FakePRG = new byte[32768]; + + /// + /// Whether the NSF is bankswitched + /// bool BankSwitched; + /// + /// the bankswitch values to be used before the INIT routine is called + /// + byte[] InitBankSwitches = new byte[8]; + + /// + /// An image of the entire PRG space where the unmapped files are located + /// + byte[] FakePRG = new byte[32768]; + //------------------------------ //state + + /// + /// PRG bankswitching + /// IntBuffer prg_banks_4k = new IntBuffer(8); /// @@ -29,8 +51,19 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES /// bool Patch_Vectors; + /// + /// Current 1-indexed song number (1 is the first song) + /// int CurrentSong; - bool ResetSignal; + + /// + /// Whether the INIT routine needs to be called + /// + bool InitPending; + + /// + /// Previous button state for button press handling + /// int ButtonState; public override bool Configure(NES.EDetectionOrigin origin) @@ -46,6 +79,16 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES 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; @@ -56,46 +99,28 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES NSFROM[0x19] = (byte)(nsf.PlayAddress); NSFROM[0x1A] = (byte)(nsf.PlayAddress >> 8); - //complicated anlysis straight from FCEUX - //apparently, it converts a non-bankswitched configuration into a bankswitched configuration - //since the non-bankswitched configuration is seemingly almost pointless. - //I'm not too sure how we would really get a non-bankswitched file, using the code below. - //It would need to be loaded below 0x8000 I think? + //analyze bankswitch configuration. fix broken configurations BankSwitched = false; for (int i = 0; i < 8; i++) { - InitBankSwitches[i] = nsf.BankswitchInitValues[i]; - if (InitBankSwitches[i] != 0) + 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) { - if ((nsf.LoadAddress & 0x7000) >= 0x7000) - { - //"Ice Climber, and other F000 base address tunes need this" - BankSwitched = true; - } - else - { - byte bankCounter = 0; - for (int x = (nsf.LoadAddress >> 12) & 0x7; x < 8; x++) - { - InitBankSwitches[x] = bankCounter; - bankCounter++; - } - BankSwitched = false; - } - } - - for (int i = 0; i < 8; i++) - if (InitBankSwitches[i] != 0) - BankSwitched = true; - - if (!BankSwitched) - { - throw new Exception("Test"); - //setup FakePRG by copying in + //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); } ReplayInit(); @@ -104,7 +129,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES void ReplayInit() { - ResetSignal = true; + InitPending = true; Patch_Vectors = true; } @@ -113,11 +138,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES ReplayInit(); } - public override void SyncState(Serializer ser) - { - base.SyncState(ser); - } - public override void WriteEXP(int addr, byte value) { switch (addr) @@ -141,20 +161,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES } } - - public override void WriteReg2xxx(int addr, byte value) - { - switch (addr) - { - case 0x3FF3: Patch_Vectors = true; break; - case 0x3FF4: Patch_Vectors = false; break; - case 0x3FF5: Patch_Vectors = true; break; - default: - base.WriteReg2xxx(addr, value); - break; - } - } - public override byte PeekReg2xxx(int addr) { if (addr < 0x3FF0) @@ -168,111 +174,124 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES return base.ReadReg2xxx(addr); else if (addr >= 0x3FF0) { - if (addr == 0x3FF0) + switch (addr) { - byte ret = 0; - if (ResetSignal) ret = 1; - ResetSignal = false; - return ret; + 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 == 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); - } - else if (addr == 0x3FF3) return 0; //always return NTSC I guess - else return base.ReadReg2xxx(addr); } else if (addr - 0x3800 < NSFROM.Length) return NSFROM[addr - 0x3800]; else return base.ReadReg2xxx(addr); } - //; @NMIVector - //00:XX00:8D F4 3F STA $3FF4 = #$00 ; clear NMI_2 (the value of A is unimportant) - //00:XX03:A2 FF LDX #$FF - //00:XX05:9A TXS ; stack pointer is initialized - //00:XX06:AD F0 3F LDA $3FF0 = #$00 ; read a flag that says whether we need to run init - //00:XX09:F0 09 BEQ $8014 ; If we dont need init, go to @PastInit - //00:XX0B:AD F1 3F LDA $3FF1 = #$00 ; reading this value causes a reset - //00:XX0E:AE F3 3F LDX $3FF3 = #$00 ; reads the PAL flag - //00:XX11:20 00 00 JSR $0000 ; JSR to INIT routine - //; @PastInit - //00:XX14:A9 00 LDA #$00 - //00:XX16:AA TAX - //00:XX17:A8 TAY ; X and Y are cleared - //00:XX18:20 00 00 JSR $0000 ; JSR to PLAY routine - //00:XX1B:8D F5 3F STA $3FF5 = #$FF ; set NMI_2 flag - //00:XX1E:90 FE BCC $XX1E ; infinite loop.. when the song is over? - //; @ResetVector - //00:XX20:8D F3 3F STA $3FF3 = #$00 ; set NMI_1 flag (the value of A is unimportant); since the rom boots here, this was needed for the initial NMI. but we also get it from having the reset signal set, so.. - //00:XX23:18 CLC - //00:XX24:90 FE BCC $XX24 ;infinite loop to wait for first NMI - const ushort NMI_VECTOR = 0x3800; const ushort RESET_VECTOR = 0x3820; - //for reasons unknown, this is a little extra long - byte[] NSFROM = new byte[0x30 + 6] + //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] { - //0x00 - NMI - 0x8D,0xF4,0x3F, //Stop play routine NMIs. - 0xA2,0xFF,0x9A, //Initialize the stack pointer. - 0xAD,0xF0,0x3F, //See if we need to init. - 0xF0,0x09, //If 0, go to play routine playing. + //@NMIVector + //Suspend vector patching + //3800:LDA $3FF3 + 0xAD,0xF3,0x3F, + + //Initialize stack pointer + //3803:LDX #$FF + 0xA2,0xFF, + //3805:TXS + 0x9A, - 0xAD,0xF1,0x3F, //Confirm and load A - 0xAE,0xF3,0x3F, //Load X with PAL/NTSC byte + //Check (and clear) InitPending flag + //3806:LDA $3FF0 + 0xAD,0xF0,0x3F, + //3809:BEQ $8014 + 0xF0,0x09, - //0x11 - 0x20,0x00,0x00, //JSR to init routine (WILL BE PATCHED) + //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, - //0x18 - 0x20,0x00,0x00, //JSR to play routine (WILL BE PATCHED) - 0x8D,0xF5,0x3F, //Start play routine NMIs. - 0x90,0xFE, //Loopie time. - - // 0x20 - 0x8D,0xF3,0x3F, //Init init NMIs + //@ResetVector - just set up an infinite loop waiting for the first NMI + //3820:CLC 0x18, - 0x90,0xFE, //Loopie time. - - //0x26 - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - //0x30 - 0x00,0x00,0x00,0x00,0x00,0x00 + //3821:BCC $XX24 + 0x90,0xFE, }; public override void AtVsyncNMI() @@ -350,19 +369,22 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES } else { - int bank_4k = addr >> 12; - int ofs = addr & ((1 << 12) - 1); - bank_4k = prg_banks_4k[bank_4k]; - addr = (bank_4k << 12) | ofs; - 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 NES.DB; + else + { + return FakePRG[addr]; + } } } }