From 58079850ae62012c68f1620fbfc7b6132de1d141 Mon Sep 17 00:00:00 2001 From: goyuken Date: Wed, 9 Apr 2014 18:13:19 +0000 Subject: [PATCH] NES -- rework autodetection code in preparation for iNES 2.0 support. this commit likely breaks some things; exhaustive testing to come --- .../Consoles/Nintendo/NES/BoardSystem.cs | 50 ++--- .../Consoles/Nintendo/NES/NES.cs | 121 ++++++------ .../Consoles/Nintendo/NES/iNES.cs | 172 ++++++++---------- 3 files changed, 157 insertions(+), 186 deletions(-) diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/BoardSystem.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/BoardSystem.cs index 9bce79c363..3ca902eb77 100644 --- a/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/BoardSystem.cs +++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/BoardSystem.cs @@ -408,13 +408,13 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES /// public class CartInfo { - public NESGameInfo game; public GameInfo DB_GameInfo; + public string name; - public short chr_size; - public short prg_size; - public short wram_size, vram_size; - public byte pad_h, pad_v, mapper; + 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 @@ -429,19 +429,10 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES public override string ToString() { - return string.Format("map={0},pr={1},ch={2},wr={3},vr={4},ba={5},pa={6}|{7},brd={8},sys={9}", mapper, prg_size, chr_size, wram_size, vram_size, wram_battery ? 1 : 0, pad_h, pad_v, board_type, system); + 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); } } - /// - /// Logical game information. May exist in form of several carts (different revisions) - /// - public class NESGameInfo - { - public string name; - public List carts = new List(); - } - /// /// finds a board class which can handle the provided cart /// @@ -503,15 +494,11 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES var gi = Database.CheckDatabase(hash); if (gi == null) return null; - NESGameInfo game = new NESGameInfo(); CartInfo cart = new CartInfo(); - game.carts.Add(cart); //try generating a bootgod cart descriptor from the game database var dict = gi.GetOptionsDict(); - game.name = gi.Name; cart.DB_GameInfo = gi; - cart.game = game; if (!dict.ContainsKey("board")) throw new Exception("NES gamedb entries must have a board identifier!"); cart.board_type = dict["board"]; @@ -585,8 +572,8 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES //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())); - NESGameInfo currGame = null; CartInfo currCart = null; + string currName = null; while (xmlreader.Read()) { switch (state) @@ -594,8 +581,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES case 0: if (xmlreader.NodeType == XmlNodeType.Element && xmlreader.Name == "game") { - currGame = new NESGameInfo(); - currGame.name = xmlreader.GetAttribute("name"); + currName = xmlreader.GetAttribute("name"); state = 1; } break; @@ -606,7 +592,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES currCart.pcb = xmlreader.GetAttribute("pcb"); int mapper = int.Parse(xmlreader.GetAttribute("mapper")); if (validate && mapper > 255) throw new Exception("didnt expect mapper>255!"); - currCart.mapper = (byte)mapper; + // we don't actually use this value at all; only the board name state = 3; } break; @@ -646,7 +632,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES case 4: if (xmlreader.NodeType == XmlNodeType.EndElement && xmlreader.Name == "cartridge") { - currGame.carts.Add(currCart); + sha1_table[currCart.sha1].Add(currCart); currCart = null; state = 5; } @@ -656,33 +642,23 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES if (xmlreader.NodeType == XmlNodeType.Element && xmlreader.Name == "cartridge") { currCart = new CartInfo(); - currCart.game = currGame; currCart.system = xmlreader.GetAttribute("system"); currCart.sha1 = "sha1:" + xmlreader.GetAttribute("sha1"); + currCart.name = currName; state = 2; } if (xmlreader.NodeType == XmlNodeType.EndElement && xmlreader.Name == "game") { - games.Add(currGame); - currGame = null; + currName = null; state = 0; } break; } } //end xmlreader loop - //analyze - foreach (NESGameInfo game in games) - { - foreach (CartInfo cart in game.carts) - { - sha1_table[cart.sha1].Add(cart); - } - } + } - - List games = new List(); //maybe we dont need to track this Bag sha1_table = new Bag(); public List Identify(string sha1) diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NES.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NES.cs index 89110ea40b..b770cf8a23 100644 --- a/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NES.cs +++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/NES.cs @@ -432,6 +432,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES Type boardType = null; CartInfo choice = null; CartInfo iNesHeaderInfo = null; + CartInfo iNesHeaderInfoV2 = null; List hash_sha1_several = new List(); string hash_sha1 = null, hash_md5 = null; Unif unif = null; @@ -444,6 +445,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES if (file.Take(4).SequenceEqual(System.Text.Encoding.ASCII.GetBytes("UNIF"))) { LoadWriteLine("Found UNIF header:"); + LoadWriteLine(unif.GetCartInfo()); LoadWriteLine("Since this is UNIF we can confidently parse PRG/CHR banks to hash."); unif = new Unif(new MemoryStream(file)); hash_sha1 = unif.GetCartInfo().sha1; @@ -480,41 +482,45 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES } else { - fixed (byte* bfile = &file[0]) + byte[] nesheader = new byte[16]; + Buffer.BlockCopy(file, 0, nesheader, 0, 16); + + if (!DetectFromINES(nesheader, out iNesHeaderInfo, out iNesHeaderInfoV2)) + throw new InvalidOperationException("iNES header not found"); + + //now that we know we have an iNES header, we can try to ignore it. + + hash_sha1 = "sha1:" + Util.Hash_SHA1(file, 16, file.Length - 16); + hash_sha1_several.Add(hash_sha1); + hash_md5 = "md5:" + Util.Hash_MD5(file, 16, file.Length - 16); + + LoadWriteLine("Found iNES header:"); + LoadWriteLine(iNesHeaderInfo.ToString()); + if (iNesHeaderInfoV2 != null) { - var header = (iNES_HEADER*)bfile; - if (!header->CheckID()) throw new InvalidOperationException("iNES header not found"); - header->Cleanup(); + LoadWriteLine("Found iNES V2 header:"); + LoadWriteLine(iNesHeaderInfoV2); + } + LoadWriteLine("Since this is iNES we can (somewhat) confidently parse PRG/CHR banks to hash."); - //now that we know we have an iNES header, we can try to ignore it. + LoadWriteLine("headerless rom hash: {0}", hash_sha1); + LoadWriteLine("headerless rom hash: {0}", hash_md5); - hash_sha1 = "sha1:" + Util.Hash_SHA1(file, 16, file.Length - 16); - hash_sha1_several.Add(hash_sha1); - hash_md5 = "md5:" + Util.Hash_MD5(file, 16, file.Length - 16); - - LoadWriteLine("Found iNES header:"); - iNesHeaderInfo = header->Analyze(new MyWriter(LoadReport)); - LoadWriteLine("Since this is iNES we can (somewhat) confidently parse PRG/CHR banks to hash."); - - LoadWriteLine("headerless rom hash: {0}", hash_sha1); - LoadWriteLine("headerless rom hash: {0}", hash_md5); - - if (iNesHeaderInfo.prg_size == 16) - { - //8KB prg can't be stored in iNES format, which counts 16KB prg banks. - //so a correct hash will include only 8KB. - LoadWriteLine("Since this rom has a 16 KB PRG, we'll hash it as 8KB too for bootgod's DB:"); - var msTemp = new MemoryStream(); - msTemp.Write(file, 16, 8 * 1024); //add prg - msTemp.Write(file, 16 + 16 * 1024, iNesHeaderInfo.chr_size * 1024); //add chr - msTemp.Flush(); - var bytes = msTemp.ToArray(); - var hash = "sha1:" + Util.Hash_SHA1(bytes, 0, bytes.Length); - LoadWriteLine(" PRG (8KB) + CHR hash: {0}", hash); - hash_sha1_several.Add(hash); - hash = "md5:" + Util.Hash_MD5(bytes, 0, bytes.Length); - LoadWriteLine(" PRG (8KB) + CHR hash: {0}", hash); - } + if (iNesHeaderInfo.prg_size == 16) + { + //8KB prg can't be stored in iNES format, which counts 16KB prg banks. + //so a correct hash will include only 8KB. + LoadWriteLine("Since this rom has a 16 KB PRG, we'll hash it as 8KB too for bootgod's DB:"); + var msTemp = new MemoryStream(); + msTemp.Write(file, 16, 8 * 1024); //add prg + msTemp.Write(file, 16 + 16 * 1024, iNesHeaderInfo.chr_size * 1024); //add chr + msTemp.Flush(); + var bytes = msTemp.ToArray(); + var hash = "sha1:" + Util.Hash_SHA1(bytes, 0, bytes.Length); + LoadWriteLine(" PRG (8KB) + CHR hash: {0}", hash); + hash_sha1_several.Add(hash); + hash = "md5:" + Util.Hash_MD5(bytes, 0, bytes.Length); + LoadWriteLine(" PRG (8KB) + CHR hash: {0}", hash); } } @@ -570,46 +576,49 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES { LoadWriteLine("Using information from UNIF header"); choice = unif.GetCartInfo(); - choice.game = new NESGameInfo(); - choice.game.name = gameInfo.Name; origin = EDetectionOrigin.UNIF; } if (iNesHeaderInfo != null) { LoadWriteLine("Attempting inference from iNES header"); - choice = iNesHeaderInfo; - string iNES_board = iNESBoardDetector.Detect(choice); - if (iNES_board == null) - throw new Exception("couldnt identify NES rom"); - choice.board_type = iNES_board; - - //try spinning up a board with 8K wram and with 0K wram to see if one answers - try + // try to spin up V2 header first, then V1 header + if (iNesHeaderInfoV2 != null) { - boardType = FindBoard(choice, origin, InitialMapperRegisterValues); - } - catch { } - if (boardType == null) - { - if (choice.wram_size == 8) choice.wram_size = 0; - else if (choice.wram_size == 0) choice.wram_size = 8; try { - boardType = FindBoard(choice, origin, InitialMapperRegisterValues); + boardType = FindBoard(iNesHeaderInfoV2, origin, InitialMapperRegisterValues); } catch { } - if (boardType != null) - LoadWriteLine("Ambiguous iNES wram size resolved as {0}k", choice.wram_size); + if (boardType == null) + LoadWriteLine("Failed to load as iNES V2"); + else + choice = iNesHeaderInfoV2; + + // V2 might fail but V1 might succeed because we don't have most V2 aliases setup; and there's + // no reason to do so except when needed + } + if (boardType == null) + { + try + { + boardType = FindBoard(iNesHeaderInfo, origin, InitialMapperRegisterValues); + } + catch {} + if (boardType == null) + LoadWriteLine("Failed to load as iNES V1"); + else + choice = iNesHeaderInfo; + + // do not further meddle in wram sizes. a board that is being loaded from a "MAPPERxxx" + // entry should know and handle the situation better for the individual board } - LoadWriteLine("Chose board from iNES heuristics: " + iNES_board); - choice.game.name = gameInfo.Name; + LoadWriteLine("Chose board from iNES heuristics:"); + LoadWriteLine(choice); origin = EDetectionOrigin.INES; } } - //TODO - generate better name with region and system - game_name = choice.game.name; //find a INESBoard to handle this boardType = FindBoard(choice, origin, InitialMapperRegisterValues); diff --git a/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/iNES.cs b/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/iNES.cs index aed69be847..8092a11fd1 100644 --- a/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/iNES.cs +++ b/BizHawk.Emulation.Cores/Consoles/Nintendo/NES/iNES.cs @@ -1,117 +1,103 @@ +using System; using System.IO; using BizHawk.Common; +using System.Linq; +using System.Text; namespace BizHawk.Emulation.Cores.Nintendo.NES { partial class NES { - /// - /// attempts to classify a rom based on iNES header information. - /// this used to be way more complex. but later, we changed to have a board class implement a "MAPPERXXX" virtual board type and all hacks will be in there - /// so theres nothing to do here but pick the board type corresponding to the cart - /// - static class iNESBoardDetector + private static int iNES2Wram(int i) { - public static string Detect(CartInfo cartInfo) - { - return string.Format("MAPPER{0:d3}",cartInfo.mapper); - } + if (i == 0) return 0; + if (i == 15) throw new InvalidDataException(); + return 1 << (i + 6); } - unsafe struct iNES_HEADER + public static bool DetectFromINES(byte[] data, out CartInfo Cart, out CartInfo CartV2) { - public fixed byte ID[4]; /*NES^Z*/ - public byte ROM_size; - public byte VROM_size; - public byte ROM_type; - public byte ROM_type2; - public byte wram_size; - public byte flags9, flags10; - public byte zero11, zero12, zero13, zero14, zero15; - - - public bool CheckID() + byte[] ID = new byte[4]; + Buffer.BlockCopy(data, 0, ID, 0, 4); + if (!ID.SequenceEqual(Encoding.ASCII.GetBytes("NES\x1A"))) { - fixed (iNES_HEADER* self = &this) - return 0 == Util.Memcmp(self, "NES\x1A", 4); + Cart = null; + CartV2 = null; + return false; } - //some cleanup code recommended by fceux - public void Cleanup() + if ((data[7] & 0x0c) == 0x08) { - fixed (iNES_HEADER* self = &this) + // process as iNES v2 + CartV2 = new CartInfo(); + + CartV2.prg_size = data[4] | data[9] << 8 & 0xf00; + CartV2.chr_size = data[5] | data[9] << 4 & 0xf00; + CartV2.prg_size *= 16; + CartV2.chr_size *= 8; + + int wrambat = iNES2Wram(data[10] >> 4); + int wramnon = iNES2Wram(data[10] & 15); + CartV2.wram_battery = wrambat > 0; + // fixme - doesn't handle sizes not divisible by 1024 + CartV2.wram_size = (short)((wrambat + wramnon) / 1024); + + int mapper = data[6] >> 4 | data[7] & 0xf0 | data[8] << 8 & 0xf00; + int submapper = data[8] >> 4; + CartV2.board_type = string.Format("MAPPER{0:d4}-{1:d2}", mapper, submapper); + + int vrambat = iNES2Wram(data[11] >> 4); + int vramnon = iNES2Wram(data[11] & 15); + // hopefully a game with battery backed vram understands what to do internally + CartV2.wram_battery |= vrambat > 0; + CartV2.vram_size = (vrambat + vramnon) / 1024; + + CartV2.inesmirroring = data[6] & 1 | data[6] >> 2 & 2; + switch (CartV2.inesmirroring) { - if (0 == Util.Memcmp((byte*)(self) + 0x7, "DiskDude", 8)) - { - Util.Memset((byte*)(self) + 0x7, 0, 0x9); - } - - if (0 == Util.Memcmp((byte*)(self) + 0x7, "demiforce", 9)) - { - Util.Memset((byte*)(self) + 0x7, 0, 0x9); - } - - if (0 == Util.Memcmp((byte*)(self) + 0x8, "blargg", 6)) //found a test rom with this in there, mucking up the wram size - { - Util.Memset((byte*)(self) + 0x8, 0, 6); - } - - if (0 == Util.Memcmp((byte*)(self) + 0xA, "Ni03", 4)) - { - if (0 == Util.Memcmp((byte*)(self) + 0x7, "Dis", 3)) - Util.Memset((byte*)(self) + 0x7, 0, 0x9); - else - Util.Memset((byte*)(self) + 0xA, 0, 0x6); - } + case 0: CartV2.pad_v = 1; break; + case 1: CartV2.pad_h = 1; break; } + + } + else + { + CartV2 = null; } - public CartInfo Analyze(TextWriter report) + // process as iNES v1 + // the DiskDude cleaning is no longer; get better roms + Cart = new CartInfo(); + + Cart.prg_size = data[4]; + Cart.chr_size = data[5]; + if (Cart.prg_size == 0) + Cart.prg_size = 256; + Cart.prg_size *= 16; + Cart.chr_size *= 8; + + + Cart.wram_battery = (data[6] & 2) != 0; + Cart.wram_size = 8; // should be data[8], but that never worked + { - var ret = new CartInfo(); - ret.game = new NESGameInfo(); - int mapper = (ROM_type >> 4); - mapper |= (ROM_type2 & 0xF0); - ret.mapper = (byte)mapper; - int mirroring = (ROM_type & 1); - if ((ROM_type & 8) != 0) mirroring += 2; - if (mirroring == 0) ret.pad_v = 1; - else if (mirroring == 1) ret.pad_h = 1; - ret.inesmirroring = mirroring; - ret.prg_size = (short)(ROM_size * 16); - if (ret.prg_size == 0) - ret.prg_size = 256 * 16; - ret.chr_size = (short)(VROM_size * 8); - ret.wram_battery = (ROM_type & 2) != 0; - if (ROM_type.Bit(2)) - report.WriteLine("DANGER: According to the flags, this iNES has a trainer in it! We don't support this garbage."); - - if(wram_size != 0 || flags9 != 0 || flags10 != 0 || zero11 != 0 || zero12 != 0 || zero13 != 0 || zero14 != 0 || zero15 != 0) - { - report.WriteLine("Looks like you have an iNES 2.0 header, or some other kind of weird garbage."); - report.WriteLine("We haven't bothered to support iNES 2.0."); - report.WriteLine("We might, if we can find anyone who uses it. Let us know."); - } - - ret.wram_size = (short)(wram_size * 8); - //0 is supposed to mean 8KB (for compatibility, as this is an extension to original iNES format) - if (ret.wram_size == 0) - { - report.WriteLine("iNES wr=0 interpreted as wr=8"); - ret.wram_size = 8; - } - - //iNES wants us to assume that no chr -> 8KB vram - if (ret.chr_size == 0) ret.vram_size = 8; - - //let's not put a lot of hacks in here. that's what the databases are for. - //for example of one not to add: videomation hack to change vram = 8 -> 16 - - string mirror_memo = mirroring == 0 ? "horz" : (mirroring == 1 ? "vert" : "4screen"); - report.WriteLine("map={0},pr={1},ch={2},wr={3},vr={4},ba={5},mir={6}({7})", ret.mapper, ret.prg_size, ret.chr_size, ret.wram_size, ret.vram_size, ret.wram_battery ? 1 : 0, mirroring, mirror_memo); - - return ret; + int mapper = data[6] >> 4 | data[7] & 0xf0; + Cart.board_type = string.Format("MAPPER{0:d3}", mapper); } + + Cart.vram_size = Cart.chr_size > 0 ? 0 : 8; + + Cart.inesmirroring = data[6] & 1 | data[6] >> 2 & 2; + switch (Cart.inesmirroring) + { + case 0: Cart.pad_v = 1; break; + case 1: Cart.pad_h = 1; break; + } + + if (data[6].Bit(2)) + Console.WriteLine("DANGER: According to the flags, this iNES has a trainer in it! We don't support this garbage."); + + return true; } }