using System; using System.Xml; using System.IO; using System.Collections.Generic; using System.Linq; using BizHawk.Common; using BizHawk.Emulation.Common; //TODO - consider bytebuffer for mirroring //TODO - could stringpool the bootgod DB for a pedantic optimization namespace BizHawk.Emulation.Cores.Nintendo.NES { partial class NES { public interface INESBoard : IDisposable { //base class pre-configuration void Create(NES nes); //one-time inherited classes configuration bool Configure(NES.EDetectionOrigin origin); //one-time base class configuration (which can take advantage of any information setup by the more-informed Configure() method) void PostConfigure(); //gets called once per PPU clock, for boards with complex behaviour which must be monitoring clock (i.e. mmc3 irq counter) void ClockPPU(); //gets called once per CPU clock; typically for boards with M2 counters void ClockCPU(); byte PeekCart(int addr); byte ReadPRG(int addr); byte ReadPPU(int addr); byte PeekPPU(int addr); void AddressPPU(int addr); byte ReadWRAM(int addr); byte ReadEXP(int addr); void WritePRG(int addr, byte value); void WritePPU(int addr, byte value); void WriteWRAM(int addr, byte value); void WriteEXP(int addr, byte value); void NESSoftReset(); byte[] SaveRam { get; } byte[] WRAM { get; set; } byte[] VRAM { get; set; } byte[] ROM { get; set; } byte[] VROM { get; set; } void SyncState(Serializer ser); bool IRQSignal { get; } //mixes the board's custom audio into the supplied sample buffer void ApplyCustomAudio(short[] samples); Dictionary InitialRegisterValues { get; set; } }; [INESBoardImpl] public abstract class NESBoardBase : INESBoard { /// /// These are used by SetMirroring() to provide the base class nametable mirroring service. /// Apparently, these are not used for internal build configuration logics /// public enum EMirrorType { Vertical, Horizontal, OneScreenA, OneScreenB, } public virtual void Create(NES nes) { this.NES = nes; } public virtual void NESSoftReset() { } Dictionary _initialRegisterValues = new Dictionary(); public Dictionary InitialRegisterValues { get { return _initialRegisterValues; } set { _initialRegisterValues = value; } } public abstract bool Configure(NES.EDetectionOrigin origin); public virtual void ClockPPU() { } public virtual void ClockCPU() { } public CartInfo Cart { get { return NES.cart; } } public NES NES { get; set; } //this is set to true when SyncState is called, so that we know the base class SyncState was used public bool SyncStateFlag = false; public virtual void SyncState(Serializer ser) { ser.Sync("vram", ref vram, true); ser.Sync("wram", ref wram, true); for (int i = 0; i < 4; i++) ser.Sync("mirroring" + i, ref mirroring[i]); ser.Sync("irq_signal", ref irq_signal); SyncStateFlag = true; } public virtual void SyncIRQ(bool flag) { IRQSignal = flag; } private bool irq_signal; public bool IRQSignal { get { return irq_signal; } set { irq_signal = value; } } public virtual void Dispose() { } int[] mirroring = new int[4]; protected void SetMirroring(int a, int b, int c, int d) { mirroring[0] = a; mirroring[1] = b; mirroring[2] = c; mirroring[3] = d; } protected void ApplyMemoryMapMask(int mask, ByteBuffer map) { byte bmask = (byte)mask; for (int i = 0; i < map.Len; i++) map[i] &= bmask; } //make sure you have bank-masked the map protected int ApplyMemoryMap(int blockSizeBits, ByteBuffer map, int addr) { int bank = addr >> blockSizeBits; int ofs = addr & ((1 << blockSizeBits) - 1); bank = map[bank]; addr = (bank << blockSizeBits) | ofs; return addr; } public static EMirrorType CalculateMirrorType(int pad_h, int pad_v) { if (pad_h == 0) if (pad_v == 0) return EMirrorType.OneScreenA; else return EMirrorType.Horizontal; else if (pad_v == 0) return EMirrorType.Vertical; else return EMirrorType.OneScreenB; } protected void SetMirrorType(int pad_h, int pad_v) { SetMirrorType(CalculateMirrorType(pad_h, pad_v)); } public void SetMirrorType(EMirrorType mirrorType) { switch (mirrorType) { case EMirrorType.Horizontal: SetMirroring(0, 0, 1, 1); break; case EMirrorType.Vertical: SetMirroring(0, 1, 0, 1); break; case EMirrorType.OneScreenA: SetMirroring(0, 0, 0, 0); break; case EMirrorType.OneScreenB: SetMirroring(1, 1, 1, 1); break; default: SetMirroring(-1, -1, -1, -1); break; //crash! } } protected int ApplyMirroring(int addr) { int block = (addr >> 10) & 3; block = mirroring[block]; int ofs = addr & 0x3FF; return (block << 10) | ofs; } protected byte HandleNormalPRGConflict(int addr, byte value) { byte old_value = value; value &= ReadPRG(addr); //Debug.Assert(old_value == value, "Found a test case of bus conflict. please report."); //report: pinball quest (J). also: double dare return value; } public virtual byte ReadPRG(int addr) { return ROM[addr]; } public virtual void WritePRG(int addr, byte value) { } public virtual void WriteWRAM(int addr, byte value) { if(wram != null) wram[addr & wram_mask] = value; } private int wram_mask; public virtual void PostConfigure() { wram_mask = (Cart.wram_size * 1024) - 1; } public virtual byte ReadWRAM(int addr) { if (wram != null) return wram[addr & wram_mask]; else return NES.DB; } public virtual void WriteEXP(int addr, byte value) { } public virtual byte ReadEXP(int addr) { return NES.DB; } public virtual void WritePPU(int addr, byte value) { if (addr < 0x2000) { if (VRAM != null) VRAM[addr] = value; } else { NES.CIRAM[ApplyMirroring(addr)] = value; } } public virtual void AddressPPU(int addr) { } public virtual byte PeekPPU(int addr) { return ReadPPU(addr); } protected virtual byte ReadPPUChr(int addr) { if (VROM != null) return VROM[addr]; else return VRAM[addr]; } public virtual byte ReadPPU(int addr) { if (addr < 0x2000) { if (VROM != null) return VROM[addr]; else return VRAM[addr]; } else { return NES.CIRAM[ApplyMirroring(addr)]; } } /// /// derived classes should override this if they have peek-unsafe logic /// public virtual byte PeekCart(int addr) { byte ret; if (addr >= 0x8000) { ret = ReadPRG(addr - 0x8000); //easy optimization, since rom reads are so common, move this up (reordering the rest of these elseifs is not easy) } else if (addr < 0x6000) { ret = ReadEXP(addr - 0x4000); } else { ret = ReadWRAM(addr - 0x6000); } return ret; } public virtual byte[] SaveRam { get { if (!Cart.wram_battery) return null; return WRAM; } } public byte[] WRAM { get { return wram; } set { wram = value; } } public byte[] VRAM { get { return vram; } set { vram = value; } } public byte[] ROM { get; set; } public byte[] VROM { get; set; } byte[] wram, vram; protected void Assert(bool test, string comment, params object[] args) { if (!test) throw new Exception(string.Format(comment, args)); } protected void Assert(bool test) { if (!test) throw new Exception("assertion failed in board setup!"); } protected void AssertPrg(params int[] prg) { Assert_memtype(Cart.prg_size, "prg", prg); } protected void AssertChr(params int[] chr) { Assert_memtype(Cart.chr_size, "chr", chr); } protected void AssertWram(params int[] wram) { Assert_memtype(Cart.wram_size, "wram", wram); } protected void AssertVram(params int[] vram) { Assert_memtype(Cart.vram_size, "vram", vram); } protected void Assert_memtype(int value, string name, int[] valid) { // only disable vram and wram asserts, as UNIF knows its prg and chr sizes if (DisableConfigAsserts && (name == "wram" || name == "vram")) return; foreach (int i in valid) if (value == i) return; Assert(false, "unhandled {0} size of {1}", name,value); } protected void AssertBattery(bool has_bat) { Assert(Cart.wram_battery == has_bat); } public virtual void ApplyCustomAudio(short[] samples) { } public bool DisableConfigAsserts = false; } //this will be used to track classes that implement boards [AttributeUsage(AttributeTargets.Class)] public class INESBoardImplAttribute : Attribute { } //this tracks derived boards that shouldnt be used by the implementation scanner [AttributeUsage(AttributeTargets.Class)] public class INESBoardImplCancelAttribute : Attribute { } static List INESBoardImplementors = new List(); //flags it as being priority, i.e. in the top of the list [AttributeUsage(AttributeTargets.Class)] public class INESBoardImplPriorityAttribute : Attribute { } static INESBoard CreateBoardInstance(Type boardType) { var board = (INESBoard)Activator.CreateInstance(boardType); lock (INESBoardImplementors) { //put the one we chose at the top of the list, for quicker access in the future int x = INESBoardImplementors.IndexOf(boardType); //(swap) var temp = INESBoardImplementors[0]; INESBoardImplementors[0] = boardType; INESBoardImplementors[x] = temp; } return board; } public string BoardName { get { return board.GetType().Name; } } void BoardSystemHardReset() { INESBoard newboard; // fds has a unique activation setup if (board is FDS) { var newfds = new FDS(); var oldfds = board as FDS; newfds.biosrom = oldfds.biosrom; newfds.SetDiskImage(oldfds.GetDiskImage()); newboard = newfds; } else { newboard = CreateBoardInstance(board.GetType()); } newboard.Create(this); // i suppose the old board could have changed its initial register values, although it really shouldn't // you can't use SyncSettings.BoardProperties here because they very well might be different than before // in case the user actually changed something in the UI newboard.InitialRegisterValues = board.InitialRegisterValues; newboard.Configure(origin); newboard.ROM = board.ROM; newboard.VROM = board.VROM; if (board.WRAM != null) newboard.WRAM = new byte[board.WRAM.Length]; if (board.VRAM != null) newboard.VRAM = new byte[board.VRAM.Length]; newboard.PostConfigure(); // the old board's sram must be restored if (newboard is FDS) { var newfds = newboard as FDS; var oldfds = board as FDS; newfds.StoreSaveRam(oldfds.ReadSaveRam()); } else if (board.SaveRam != null) { Buffer.BlockCopy(board.SaveRam, 0, newboard.SaveRam, 0, board.SaveRam.Length); } board.Dispose(); board = newboard; } static NES() { var highPriority = new List(); var normalPriority = new List(); //scan types in this assembly to find ones that implement boards to add them to the list foreach (Type type in typeof(NES).Assembly.GetTypes()) { var attrs = type.GetCustomAttributes(typeof(INESBoardImplAttribute), true); if (attrs.Length == 0) continue; if (type.IsAbstract) continue; var cancelAttrs = type.GetCustomAttributes(typeof(INESBoardImplCancelAttribute), true); if (cancelAttrs.Length != 0) continue; var priorityAttrs = type.GetCustomAttributes(typeof(INESBoardImplPriorityAttribute), true); if (priorityAttrs.Length != 0) highPriority.Add(type); else normalPriority.Add(type); } INESBoardImplementors.AddRange(highPriority); INESBoardImplementors.AddRange(normalPriority); } /// /// All information necessary for a board to set itself up /// public class CartInfo { public GameInfo DB_GameInfo; public string name; public int chr_size; public int prg_size; public int wram_size, vram_size; public byte pad_h, pad_v; public bool wram_battery; public bool bad; /// in [0,3]; combination of bits 0 and 3 of flags6. try not to use; will be null for bootgod-identified roms always public int? inesmirroring; public string board_type; public string pcb; public string sha1; public string system; public List chips = new List(); public override string ToString() { return string.Format("pr={1},ch={2},wr={3},vr={4},ba={5},pa={6}|{7},brd={8},sys={9}", board_type, prg_size, chr_size, wram_size, vram_size, wram_battery ? 1 : 0, pad_h, pad_v, board_type, system); } } /// /// finds a board class which can handle the provided cart /// static Type FindBoard(CartInfo cart, EDetectionOrigin origin, Dictionary properties) { NES nes = new NES(); nes.cart = cart; Type ret = null; lock(INESBoardImplementors) foreach (var type in INESBoardImplementors) { using (NESBoardBase board = (NESBoardBase)Activator.CreateInstance(type)) { //unif demands that the boards set themselves up with expected legal values based on the board size //except, i guess, for the rom/chr sizes. go figure. //so, disable the asserts here if (origin == EDetectionOrigin.UNIF) board.DisableConfigAsserts = true; board.Create(nes); board.InitialRegisterValues = properties; if (board.Configure(origin)) { #if DEBUG if (ret != null) throw new Exception(string.Format("Boards {0} and {1} both responded to Configure!", ret, type)); else ret = type; #else return type; #endif } } } return ret; } /// /// looks up from the bootgod DB /// CartInfo IdentifyFromBootGodDB(IEnumerable hash_sha1) { BootGodDB.Initialize(); foreach (var hash in hash_sha1) { List choices = BootGodDB.Instance.Identify(hash); //pick the first board for this hash arbitrarily. it probably doesn't make a difference if (choices.Count != 0) return choices[0]; } return null; } /// /// looks up from the game DB /// CartInfo IdentifyFromGameDB(string hash) { var gi = Database.CheckDatabase(hash); if (gi == null) return null; CartInfo cart = new CartInfo(); //try generating a bootgod cart descriptor from the game database var dict = gi.GetOptionsDict(); cart.DB_GameInfo = gi; if (!dict.ContainsKey("board")) throw new Exception("NES gamedb entries must have a board identifier!"); cart.board_type = dict["board"]; if (dict.ContainsKey("system")) cart.system = dict["system"]; cart.prg_size = -1; cart.vram_size = -1; cart.wram_size = -1; cart.chr_size = -1; if (dict.ContainsKey("PRG")) cart.prg_size = short.Parse(dict["PRG"]); if (dict.ContainsKey("CHR")) cart.chr_size = short.Parse(dict["CHR"]); if(dict.ContainsKey("VRAM")) cart.vram_size = short.Parse(dict["VRAM"]); if (dict.ContainsKey("WRAM")) cart.wram_size = short.Parse(dict["WRAM"]); if (dict.ContainsKey("PAD_H")) cart.pad_h = byte.Parse(dict["PAD_H"]); if (dict.ContainsKey("PAD_V")) cart.pad_v = byte.Parse(dict["PAD_V"]); if(dict.ContainsKey("MIR")) if (dict["MIR"] == "H") { cart.pad_v = 1; cart.pad_h = 0; } else if (dict["MIR"] == "V") { cart.pad_h = 1; cart.pad_v = 0; } if (dict.ContainsKey("BAD")) cart.bad = true; if (dict.ContainsKey("MMC3")) cart.chips.Add(dict["MMC3"]); if (dict.ContainsKey("PCB")) cart.pcb = dict["PCB"]; if (dict.ContainsKey("BATT")) cart.wram_battery = bool.Parse(dict["BATT"]); return cart; } public class BootGodDB { static object staticsyncroot = new object(); object syncroot = new object(); bool validate = true; private static BootGodDB _Instance; public static BootGodDB Instance { get { lock (staticsyncroot) { return _Instance; } } } private static Func _GetDatabaseBytes; public static Func GetDatabaseBytes { set { lock (staticsyncroot) { _GetDatabaseBytes = value; } } } public static void Initialize() { lock (staticsyncroot) { if (_Instance == null) _Instance = new BootGodDB(); } } int ParseSize(string str) { int temp = 0; if(validate) if (!str.EndsWith("k")) throw new Exception(); int len=str.Length-1; for (int i = 0; i < len; i++) { temp *= 10; temp += (str[i] - '0'); } return temp; } public BootGodDB() { //notes: there can be multiple each of prg,chr,wram,vram //we arent tracking the individual hashes yet. //in anticipation of any slowness annoying people, and just for shits and giggles, i made a super fast parser int state=0; var xmlreader = XmlReader.Create(new MemoryStream(_GetDatabaseBytes())); CartInfo currCart = null; string currName = null; while (xmlreader.Read()) { switch (state) { case 0: if (xmlreader.NodeType == XmlNodeType.Element && xmlreader.Name == "game") { currName = xmlreader.GetAttribute("name"); state = 1; } break; case 2: if (xmlreader.NodeType == XmlNodeType.Element && xmlreader.Name == "board") { currCart.board_type = xmlreader.GetAttribute("type"); currCart.pcb = xmlreader.GetAttribute("pcb"); int mapper = int.Parse(xmlreader.GetAttribute("mapper")); if (validate && mapper > 255) throw new Exception("didnt expect mapper>255!"); // we don't actually use this value at all; only the board name state = 3; } break; case 3: if (xmlreader.NodeType == XmlNodeType.Element) { switch(xmlreader.Name) { case "prg": currCart.prg_size += (short)ParseSize(xmlreader.GetAttribute("size")); break; case "chr": currCart.chr_size += (short)ParseSize(xmlreader.GetAttribute("size")); break; case "vram": currCart.vram_size += (short)ParseSize(xmlreader.GetAttribute("size")); break; case "wram": currCart.wram_size += (short)ParseSize(xmlreader.GetAttribute("size")); if (xmlreader.GetAttribute("battery") != null) currCart.wram_battery = true; break; case "pad": currCart.pad_h = byte.Parse(xmlreader.GetAttribute("h")); currCart.pad_v = byte.Parse(xmlreader.GetAttribute("v")); break; case "chip": currCart.chips.Add(xmlreader.GetAttribute("type")); break; } } else if (xmlreader.NodeType == XmlNodeType.EndElement && xmlreader.Name == "board") { state = 4; } break; case 4: if (xmlreader.NodeType == XmlNodeType.EndElement && xmlreader.Name == "cartridge") { sha1_table[currCart.sha1].Add(currCart); currCart = null; state = 5; } break; case 5: case 1: if (xmlreader.NodeType == XmlNodeType.Element && xmlreader.Name == "cartridge") { currCart = new CartInfo(); currCart.system = xmlreader.GetAttribute("system"); currCart.sha1 = "sha1:" + xmlreader.GetAttribute("sha1"); currCart.name = currName; state = 2; } if (xmlreader.NodeType == XmlNodeType.EndElement && xmlreader.Name == "game") { currName = null; state = 0; } break; } } //end xmlreader loop } Bag sha1_table = new Bag(); public List Identify(string sha1) { lock (syncroot) { if (!sha1_table.ContainsKey(sha1)) return new List(); else return sha1_table[sha1]; } } } } [AttributeUsage(AttributeTargets.Field)] public class MapperPropAttribute : Attribute { public string Name { get; private set; } public MapperPropAttribute(string Name) { this.Name = Name; } public MapperPropAttribute() { this.Name = null; } } public static class AutoMapperProps { public static void Apply(NES.INESBoard board) { var fields = board.GetType().GetFields(); foreach (var field in fields) { var attribs = field.GetCustomAttributes(false); foreach (var attrib in attribs) { if (attrib is MapperPropAttribute) { string Name = ((MapperPropAttribute)attrib).Name ?? field.Name; string Value; if (board.InitialRegisterValues.TryGetValue(Name, out Value)) { try { field.SetValue(board, Convert.ChangeType(Value, field.FieldType)); } catch (Exception e) { if (e is InvalidCastException || e is FormatException || e is OverflowException) throw new InvalidDataException("Auto Mapper Properties were in a bad format!", e); else throw e; } } break; } } } } } }