NES -- rework autodetection code in preparation for iNES 2.0 support. this commit likely breaks some things; exhaustive testing to come

This commit is contained in:
goyuken 2014-04-09 18:13:19 +00:00
parent 2834e80dfa
commit 58079850ae
3 changed files with 157 additions and 186 deletions

View File

@ -408,13 +408,13 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
/// </summary> /// </summary>
public class CartInfo public class CartInfo
{ {
public NESGameInfo game;
public GameInfo DB_GameInfo; public GameInfo DB_GameInfo;
public string name;
public short chr_size; public int chr_size;
public short prg_size; public int prg_size;
public short wram_size, vram_size; public int wram_size, vram_size;
public byte pad_h, pad_v, mapper; public byte pad_h, pad_v;
public bool wram_battery; public bool wram_battery;
public bool bad; public bool bad;
/// <summary>in [0,3]; combination of bits 0 and 3 of flags6. try not to use; will be null for bootgod-identified roms always</summary> /// <summary>in [0,3]; combination of bits 0 and 3 of flags6. try not to use; will be null for bootgod-identified roms always</summary>
@ -429,19 +429,10 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
public override string ToString() 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);
} }
} }
/// <summary>
/// Logical game information. May exist in form of several carts (different revisions)
/// </summary>
public class NESGameInfo
{
public string name;
public List<CartInfo> carts = new List<CartInfo>();
}
/// <summary> /// <summary>
/// finds a board class which can handle the provided cart /// finds a board class which can handle the provided cart
/// </summary> /// </summary>
@ -503,15 +494,11 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
var gi = Database.CheckDatabase(hash); var gi = Database.CheckDatabase(hash);
if (gi == null) return null; if (gi == null) return null;
NESGameInfo game = new NESGameInfo();
CartInfo cart = new CartInfo(); CartInfo cart = new CartInfo();
game.carts.Add(cart);
//try generating a bootgod cart descriptor from the game database //try generating a bootgod cart descriptor from the game database
var dict = gi.GetOptionsDict(); var dict = gi.GetOptionsDict();
game.name = gi.Name;
cart.DB_GameInfo = gi; cart.DB_GameInfo = gi;
cart.game = game;
if (!dict.ContainsKey("board")) if (!dict.ContainsKey("board"))
throw new Exception("NES gamedb entries must have a board identifier!"); throw new Exception("NES gamedb entries must have a board identifier!");
cart.board_type = dict["board"]; 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 //in anticipation of any slowness annoying people, and just for shits and giggles, i made a super fast parser
int state=0; int state=0;
var xmlreader = XmlReader.Create(new MemoryStream(GetDatabaseBytes())); var xmlreader = XmlReader.Create(new MemoryStream(GetDatabaseBytes()));
NESGameInfo currGame = null;
CartInfo currCart = null; CartInfo currCart = null;
string currName = null;
while (xmlreader.Read()) while (xmlreader.Read())
{ {
switch (state) switch (state)
@ -594,8 +581,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
case 0: case 0:
if (xmlreader.NodeType == XmlNodeType.Element && xmlreader.Name == "game") if (xmlreader.NodeType == XmlNodeType.Element && xmlreader.Name == "game")
{ {
currGame = new NESGameInfo(); currName = xmlreader.GetAttribute("name");
currGame.name = xmlreader.GetAttribute("name");
state = 1; state = 1;
} }
break; break;
@ -606,7 +592,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
currCart.pcb = xmlreader.GetAttribute("pcb"); currCart.pcb = xmlreader.GetAttribute("pcb");
int mapper = int.Parse(xmlreader.GetAttribute("mapper")); int mapper = int.Parse(xmlreader.GetAttribute("mapper"));
if (validate && mapper > 255) throw new Exception("didnt expect mapper>255!"); 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; state = 3;
} }
break; break;
@ -646,7 +632,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
case 4: case 4:
if (xmlreader.NodeType == XmlNodeType.EndElement && xmlreader.Name == "cartridge") if (xmlreader.NodeType == XmlNodeType.EndElement && xmlreader.Name == "cartridge")
{ {
currGame.carts.Add(currCart); sha1_table[currCart.sha1].Add(currCart);
currCart = null; currCart = null;
state = 5; state = 5;
} }
@ -656,33 +642,23 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
if (xmlreader.NodeType == XmlNodeType.Element && xmlreader.Name == "cartridge") if (xmlreader.NodeType == XmlNodeType.Element && xmlreader.Name == "cartridge")
{ {
currCart = new CartInfo(); currCart = new CartInfo();
currCart.game = currGame;
currCart.system = xmlreader.GetAttribute("system"); currCart.system = xmlreader.GetAttribute("system");
currCart.sha1 = "sha1:" + xmlreader.GetAttribute("sha1"); currCart.sha1 = "sha1:" + xmlreader.GetAttribute("sha1");
currCart.name = currName;
state = 2; state = 2;
} }
if (xmlreader.NodeType == XmlNodeType.EndElement && xmlreader.Name == "game") if (xmlreader.NodeType == XmlNodeType.EndElement && xmlreader.Name == "game")
{ {
games.Add(currGame); currName = null;
currGame = null;
state = 0; state = 0;
} }
break; break;
} }
} //end xmlreader loop } //end xmlreader loop
//analyze
foreach (NESGameInfo game in games)
{
foreach (CartInfo cart in game.carts)
{
sha1_table[cart.sha1].Add(cart);
}
}
} }
List<NESGameInfo> games = new List<NESGameInfo>(); //maybe we dont need to track this
Bag<string, CartInfo> sha1_table = new Bag<string, CartInfo>(); Bag<string, CartInfo> sha1_table = new Bag<string, CartInfo>();
public List<CartInfo> Identify(string sha1) public List<CartInfo> Identify(string sha1)

View File

@ -432,6 +432,7 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
Type boardType = null; Type boardType = null;
CartInfo choice = null; CartInfo choice = null;
CartInfo iNesHeaderInfo = null; CartInfo iNesHeaderInfo = null;
CartInfo iNesHeaderInfoV2 = null;
List<string> hash_sha1_several = new List<string>(); List<string> hash_sha1_several = new List<string>();
string hash_sha1 = null, hash_md5 = null; string hash_sha1 = null, hash_md5 = null;
Unif unif = 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"))) if (file.Take(4).SequenceEqual(System.Text.Encoding.ASCII.GetBytes("UNIF")))
{ {
LoadWriteLine("Found UNIF header:"); LoadWriteLine("Found UNIF header:");
LoadWriteLine(unif.GetCartInfo());
LoadWriteLine("Since this is UNIF we can confidently parse PRG/CHR banks to hash."); LoadWriteLine("Since this is UNIF we can confidently parse PRG/CHR banks to hash.");
unif = new Unif(new MemoryStream(file)); unif = new Unif(new MemoryStream(file));
hash_sha1 = unif.GetCartInfo().sha1; hash_sha1 = unif.GetCartInfo().sha1;
@ -480,11 +482,11 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
} }
else else
{ {
fixed (byte* bfile = &file[0]) byte[] nesheader = new byte[16];
{ Buffer.BlockCopy(file, 0, nesheader, 0, 16);
var header = (iNES_HEADER*)bfile;
if (!header->CheckID()) throw new InvalidOperationException("iNES header not found"); if (!DetectFromINES(nesheader, out iNesHeaderInfo, out iNesHeaderInfoV2))
header->Cleanup(); throw new InvalidOperationException("iNES header not found");
//now that we know we have an iNES header, we can try to ignore it. //now that we know we have an iNES header, we can try to ignore it.
@ -493,7 +495,12 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
hash_md5 = "md5:" + Util.Hash_MD5(file, 16, file.Length - 16); hash_md5 = "md5:" + Util.Hash_MD5(file, 16, file.Length - 16);
LoadWriteLine("Found iNES header:"); LoadWriteLine("Found iNES header:");
iNesHeaderInfo = header->Analyze(new MyWriter(LoadReport)); LoadWriteLine(iNesHeaderInfo.ToString());
if (iNesHeaderInfoV2 != null)
{
LoadWriteLine("Found iNES V2 header:");
LoadWriteLine(iNesHeaderInfoV2);
}
LoadWriteLine("Since this is iNES we can (somewhat) confidently parse PRG/CHR banks to hash."); 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_sha1);
@ -516,7 +523,6 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
LoadWriteLine(" PRG (8KB) + CHR hash: {0}", hash); LoadWriteLine(" PRG (8KB) + CHR hash: {0}", hash);
} }
} }
}
if (USE_DATABASE) if (USE_DATABASE)
{ {
@ -570,46 +576,49 @@ namespace BizHawk.Emulation.Cores.Nintendo.NES
{ {
LoadWriteLine("Using information from UNIF header"); LoadWriteLine("Using information from UNIF header");
choice = unif.GetCartInfo(); choice = unif.GetCartInfo();
choice.game = new NESGameInfo();
choice.game.name = gameInfo.Name;
origin = EDetectionOrigin.UNIF; origin = EDetectionOrigin.UNIF;
} }
if (iNesHeaderInfo != null) if (iNesHeaderInfo != null)
{ {
LoadWriteLine("Attempting inference from iNES header"); LoadWriteLine("Attempting inference from iNES header");
choice = iNesHeaderInfo; // try to spin up V2 header first, then V1 header
string iNES_board = iNESBoardDetector.Detect(choice); if (iNesHeaderInfoV2 != null)
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
{ {
boardType = FindBoard(choice, origin, InitialMapperRegisterValues); boardType = FindBoard(iNesHeaderInfoV2, origin, InitialMapperRegisterValues);
} }
catch { } catch { }
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) if (boardType == null)
{ {
if (choice.wram_size == 8) choice.wram_size = 0;
else if (choice.wram_size == 0) choice.wram_size = 8;
try try
{ {
boardType = FindBoard(choice, origin, InitialMapperRegisterValues); boardType = FindBoard(iNesHeaderInfo, origin, InitialMapperRegisterValues);
} }
catch {} catch {}
if (boardType != null) if (boardType == null)
LoadWriteLine("Ambiguous iNES wram size resolved as {0}k", choice.wram_size); 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); LoadWriteLine("Chose board from iNES heuristics:");
choice.game.name = gameInfo.Name; LoadWriteLine(choice);
origin = EDetectionOrigin.INES; origin = EDetectionOrigin.INES;
} }
} }
//TODO - generate better name with region and system
game_name = choice.game.name;
//find a INESBoard to handle this //find a INESBoard to handle this
boardType = FindBoard(choice, origin, InitialMapperRegisterValues); boardType = FindBoard(choice, origin, InitialMapperRegisterValues);

View File

@ -1,117 +1,103 @@
using System;
using System.IO; using System.IO;
using BizHawk.Common; using BizHawk.Common;
using System.Linq;
using System.Text;
namespace BizHawk.Emulation.Cores.Nintendo.NES namespace BizHawk.Emulation.Cores.Nintendo.NES
{ {
partial class NES partial class NES
{ {
/// <summary> private static int iNES2Wram(int i)
/// 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
/// </summary>
static class iNESBoardDetector
{ {
public static string Detect(CartInfo cartInfo) if (i == 0) return 0;
{ if (i == 15) throw new InvalidDataException();
return string.Format("MAPPER{0:d3}",cartInfo.mapper); 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*/ byte[] ID = new byte[4];
public byte ROM_size; Buffer.BlockCopy(data, 0, ID, 0, 4);
public byte VROM_size; if (!ID.SequenceEqual(Encoding.ASCII.GetBytes("NES\x1A")))
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()
{ {
fixed (iNES_HEADER* self = &this) Cart = null;
return 0 == Util.Memcmp(self, "NES\x1A", 4); CartV2 = null;
return false;
} }
//some cleanup code recommended by fceux if ((data[7] & 0x0c) == 0x08)
public void Cleanup()
{ {
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)) case 0: CartV2.pad_v = 1; break;
{ case 1: CartV2.pad_h = 1; break;
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 else
Util.Memset((byte*)(self) + 0xA, 0, 0x6);
}
}
}
public CartInfo Analyze(TextWriter report)
{ {
var ret = new CartInfo(); CartV2 = null;
ret.game = new NESGameInfo(); }
int mapper = (ROM_type >> 4);
mapper |= (ROM_type2 & 0xF0); // process as iNES v1
ret.mapper = (byte)mapper; // the DiskDude cleaning is no longer; get better roms
int mirroring = (ROM_type & 1); Cart = new CartInfo();
if ((ROM_type & 8) != 0) mirroring += 2;
if (mirroring == 0) ret.pad_v = 1; Cart.prg_size = data[4];
else if (mirroring == 1) ret.pad_h = 1; Cart.chr_size = data[5];
ret.inesmirroring = mirroring; if (Cart.prg_size == 0)
ret.prg_size = (short)(ROM_size * 16); Cart.prg_size = 256;
if (ret.prg_size == 0) Cart.prg_size *= 16;
ret.prg_size = 256 * 16; Cart.chr_size *= 8;
ret.chr_size = (short)(VROM_size * 8);
ret.wram_battery = (ROM_type & 2) != 0;
if (ROM_type.Bit(2)) Cart.wram_battery = (data[6] & 2) != 0;
report.WriteLine("DANGER: According to the flags, this iNES has a trainer in it! We don't support this garbage."); Cart.wram_size = 8; // should be data[8], but that never worked
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."); int mapper = data[6] >> 4 | data[7] & 0xf0;
report.WriteLine("We haven't bothered to support iNES 2.0."); Cart.board_type = string.Format("MAPPER{0:d3}", mapper);
report.WriteLine("We might, if we can find anyone who uses it. Let us know.");
} }
ret.wram_size = (short)(wram_size * 8); Cart.vram_size = Cart.chr_size > 0 ? 0 : 8;
//0 is supposed to mean 8KB (for compatibility, as this is an extension to original iNES format)
if (ret.wram_size == 0) Cart.inesmirroring = data[6] & 1 | data[6] >> 2 & 2;
switch (Cart.inesmirroring)
{ {
report.WriteLine("iNES wr=0 interpreted as wr=8"); case 0: Cart.pad_v = 1; break;
ret.wram_size = 8; case 1: Cart.pad_h = 1; break;
} }
//iNES wants us to assume that no chr -> 8KB vram if (data[6].Bit(2))
if (ret.chr_size == 0) ret.vram_size = 8; Console.WriteLine("DANGER: According to the flags, this iNES has a trainer in it! We don't support this garbage.");
//let's not put a lot of hacks in here. that's what the databases are for. return true;
//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;
}
} }
} }