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; /// /// 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); /// /// 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 /// bool Patch_Vectors; /// /// Current 1-indexed song number (1 is the first song) /// int CurrentSong; /// /// 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) { 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); } ReplayInit(); CurrentSong = nsf.StartingSong; } void ReplayInit() { 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) { CurrentSong++; reset = true; } if (left) { 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]; } } } } }