[NES] ppu fixes and improved rom classification infrastructure

This commit is contained in:
zeromus 2011-02-28 06:16:20 +00:00
parent d7ba739b30
commit 4b177ca8d1
14 changed files with 292 additions and 92 deletions

View File

@ -55,6 +55,7 @@
<ItemGroup>
<Compile Include="Consoles\Calculator\TI83.cs" />
<Compile Include="Consoles\Nintendo\NES\Boards\NROM.cs" />
<Compile Include="Consoles\Nintendo\NES\BoardDetector.cs" />
<Compile Include="Consoles\Nintendo\NES\NES.cs" />
<Compile Include="Consoles\Nintendo\NES\Palettes.cs" />
<Compile Include="Consoles\Nintendo\NES\PPU.cs" />

View File

@ -0,0 +1,47 @@
//http://nesdev.parodius.com/bbs/viewtopic.php?p=4571&sid=db4c7e35316cc5d734606dd02f11dccb
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Collections.Generic;
using BizHawk.Emulation.CPUs.M6502;
namespace BizHawk.Emulation.Consoles.Nintendo
{
partial class NES
{
static class BoardDetector
{
public static string Detect(RomInfo romInfo)
{
string key = string.Format("{0} {1} {2}",romInfo.MapperNumber,romInfo.PRG_Size,romInfo.CHR_Size);
string board;
Table.TryGetValue(key, out board);
return board;
}
public static Dictionary<string,string> Table = new Dictionary<string,string>();
static BoardDetector()
{
var sr = new StringReader(ClassifyTable);
string line;
while ((line = sr.ReadLine()) != null)
{
var parts = line.Split('\t');
if (parts.Length < 4) continue;
line = line.Replace(parts[0],"");
line = line.TrimStart('\t');
Table[line] = parts[0];
}
}
//board MAP PRG CHR
static string ClassifyTable = @"
NROM 0 1 1
NROM 0 2 1
";
}
}
}

View File

@ -1,11 +1,18 @@
using System;
using System.Diagnostics;
namespace BizHawk.Emulation.Consoles.Nintendo.Boards
{
public class NROM : NES.NESBoardBase
{
public virtual void Initialize(NES.RomInfo romInfo, NES nes)
{
base.Initialize(romInfo, nes);
Debug.Assert(romInfo.PRG_Size < 3);
}
public override byte ReadPRG(int addr)
{
addr &= (RomInfo.PRG_Size << 14) - 1;
return RomInfo.ROM[addr];
}
}

View File

@ -8,25 +8,53 @@ namespace BizHawk.Emulation.Consoles.Nintendo
{
public partial class NES : IEmulator
{
//the main rom class that contains all information necessary for the board to operate
public class RomInfo
{
public enum EHeaderType
{
None, INes
}
public enum EMirrorType
{
Vertical, Horizontal, Other
}
public EHeaderType HeaderType;
public int PRG_Size = -1, CHR_Size = -1, PRAM_Size = -1;
public string BoardName;
public EMirrorType MirrorType;
public bool Battery;
public int MapperNumber; //it annoys me that this junky mapper number is even in here. it might be nice to wrap this class in something else to contain the MapperNumber
public string MD5;
public byte[] ROM, VROM;
}
public interface INESBoard
{
byte ReadPRG(int addr);
byte ReadPPU(int addr);
byte ReadPRAM(int addr);
void WritePRG(int addr, byte value);
void WritePPU(int addr, byte value);
void WritePRAM(int addr, byte value);
void Initialize(RomInfo romInfo, NES nes);
};
public abstract class NESBoardBase : INESBoard
{
public void Initialize(RomInfo romInfo, NES nes)
public virtual void Initialize(RomInfo romInfo, NES nes)
{
this.RomInfo = romInfo;
this.NES = nes;
switch (romInfo.Mirroring)
switch (romInfo.MirrorType)
{
case 0: SetMirroring(0, 0, 1, 1); break;
case 1: SetMirroring(0, 1, 0, 1); break;
case RomInfo.EMirrorType.Horizontal: SetMirroring(0, 0, 1, 1); break;
case RomInfo.EMirrorType.Vertical: SetMirroring(0, 1, 0, 1); break;
default: SetMirroring(-1, -1, -1, -1); break; //crash!
}
}
@ -42,10 +70,21 @@ namespace BizHawk.Emulation.Consoles.Nintendo
mirroring[3] = d;
}
int ApplyMirroring(int addr)
{
int block = (addr >> 10) & 3;
block = mirroring[block];
int ofs = addr & 0x3FF;
return (block << 10) | ofs | 0x2000;
}
public virtual byte ReadPRG(int addr) { return RomInfo.ROM[addr];}
public virtual void WritePRG(int addr, byte value) { }
public virtual void WritePRAM(int addr, byte value) { }
public virtual byte ReadPRAM(int addr) { return 0xFF; }
public virtual void WritePPU(int addr, byte value)
{
@ -54,10 +93,7 @@ namespace BizHawk.Emulation.Consoles.Nintendo
}
else
{
int block = (addr >> 10) & 3;
block = mirroring[block];
int ofs = addr & 0x3FF;
NES.ppu.NTARAM[(block << 10) | ofs] = value;
NES.ppu.ppu_defaultWrite(ApplyMirroring(addr), value);
}
}
@ -69,10 +105,7 @@ namespace BizHawk.Emulation.Consoles.Nintendo
}
else
{
int block = (addr >> 10)&3;
block = mirroring[block];
int ofs = addr & 0x3FF;
return NES.ppu.NTARAM[(block << 10) | ofs];
return NES.ppu.ppu_defaultRead(ApplyMirroring(addr));
}
}
}
@ -438,9 +471,10 @@ namespace BizHawk.Emulation.Consoles.Nintendo
return input;
}
public class RomInfo
public class RomHeaderInfo
{
public int MapperNo, Mirroring, Num_PRG_Banks, Num_CHR_Banks;
public int MapperNo, Mirroring, Num_PRG_Banks, Num_CHR_Banks, Num_PRAM_Banks;
public bool Battery;
public byte[] ROM, VROM;
}
@ -486,15 +520,25 @@ namespace BizHawk.Emulation.Consoles.Nintendo
public RomInfo Analyze()
{
var ret = new RomInfo();
ret.MapperNo = (ROM_type>>4);
ret.MapperNo|=(ROM_type2&0xF0);
ret.Mirroring = (ROM_type&1);
if((ROM_type&8)!=0) ret.Mirroring=2;
ret.Num_PRG_Banks = ROM_size;
if (ret.Num_PRG_Banks == 0)
ret.Num_PRG_Banks = 256;
ret.Num_CHR_Banks = VROM_size;
ret.MapperNumber = (ROM_type>>4);
ret.MapperNumber |= (ROM_type2 & 0xF0);
int mirroring = (ROM_type&1);
if((ROM_type&8)!=0) mirroring=2;
if (mirroring == 0) ret.MirrorType = RomInfo.EMirrorType.Horizontal;
else if (mirroring == 1) ret.MirrorType = RomInfo.EMirrorType.Vertical;
else ret.MirrorType = RomInfo.EMirrorType.Other;
ret.PRG_Size = ROM_size;
if (ret.PRG_Size == 0)
ret.PRG_Size = 256;
ret.CHR_Size = VROM_size;
ret.Battery = (ROM_type & 2) != 0;
fixed (iNES_HEADER* self = &this) ret.PRAM_Size = self->reserve[0];
//0 is supposed to mean 1 (for compatibility, as this is an extension to original iNES format)
//but we're not worrying about that for now)
Console.WriteLine("iNES header: map:{0}, mirror:{1}, PRG:{2}, CHR:{3}, PRAM:{4}, bat:{5}", ret.MapperNumber, ret.MirrorType, ret.PRG_Size, ret.CHR_Size, ret.PRAM_Size, ret.Battery ? 1 : 0);
//fceux calls uppow2(PRG_Banks) here, and also ups the chr size as well
//then it does something complicated that i don't understand with making sure it doesnt read too much data
//fceux only allows this condition for mappers in the list "not_power2" which is only 228
@ -503,20 +547,11 @@ namespace BizHawk.Emulation.Consoles.Nintendo
}
}
INESBoard Classify(RomInfo info)
{
//you may think that this should be table driven.. but im not so sure.
//i think this should be a backstop eventually, with other classification happening from the game database.
//if the gamedatabase has an exact answer for a game then the board can be determined..
//otherwise we might try to find a general case handler below.
if (info.MapperNo == 0 && info.Num_CHR_Banks == 1 && info.Num_PRG_Banks == 2 && info.Mirroring == 1) return new Boards.NROM();
return null;
}
const bool ENABLE_DB = true;
public unsafe void LoadGame(IGame game)
{
byte[] file = game.GetRomData();
byte[] file = game.GetFileData();
if (file.Length < 16) throw new InvalidOperationException("Alleged NES rom too small to be anything useful");
fixed (byte* bfile = &file[0])
{
@ -524,14 +559,59 @@ namespace BizHawk.Emulation.Consoles.Nintendo
if (!header->CheckID()) throw new InvalidOperationException("iNES header not found");
header->Cleanup();
romInfo = header->Analyze();
board = Classify(romInfo);
//now that we know we have an iNES header, we can try to ignore it.
string hash;
using (var md5 = System.Security.Cryptography.MD5.Create())
{
md5.TransformFinalBlock(file, 16, file.Length - 16);
hash = Util.BytesToHexString(md5.Hash);
}
Console.WriteLine("headerless rom hash: {0}", hash);
GameInfo gi = null;
if (ENABLE_DB) gi = Database.CheckDatabase(hash);
else Console.WriteLine("database check disabled");
if (gi == null)
{
romInfo = header->Analyze();
string board = BoardDetector.Detect(romInfo);
if (board == null)
throw new InvalidOperationException("Couldn't detect board type");
romInfo.BoardName = board;
Console.WriteLine("board detected as " + board);
}
else
{
Console.WriteLine("found game in database: {0}", gi.Name);
romInfo = new RomInfo();
romInfo.MD5 = hash;
var dict = gi.ParseOptionsDictionary();
if (dict.ContainsKey("board"))
romInfo.BoardName = dict["board"];
switch (dict["mirror"])
{
case "V": romInfo.MirrorType = RomInfo.EMirrorType.Vertical; break;
case "H": romInfo.MirrorType = RomInfo.EMirrorType.Horizontal; break;
}
if (dict.ContainsKey("PRG"))
romInfo.PRG_Size = int.Parse(dict["PRG"]);
if (dict.ContainsKey("CHR"))
romInfo.CHR_Size = int.Parse(dict["CHR"]);
}
//construct board (todo)
switch (romInfo.BoardName)
{
case "NROM": board = new Boards.NROM(); break;
}
if (board == null) throw new InvalidOperationException("Couldn't classify NES rom");
board.Initialize(romInfo, this);
//we're going to go ahead and copy these out, just in case we need to pad them alter
romInfo.ROM = new byte[romInfo.Num_PRG_Banks * 16 * 1024];
romInfo.VROM = new byte[romInfo.Num_CHR_Banks * 8 * 1024];
romInfo.ROM = new byte[romInfo.PRG_Size * 16 * 1024];
romInfo.VROM = new byte[romInfo.CHR_Size * 8 * 1024];
Array.Copy(file, 16, romInfo.ROM, 0, romInfo.ROM.Length);
Array.Copy(file, 16 + romInfo.ROM.Length, romInfo.VROM, 0, romInfo.VROM.Length);
}
@ -539,4 +619,8 @@ namespace BizHawk.Emulation.Consoles.Nintendo
HardReset();
}
}
}
}
//todo
//http://blog.ntrq.net/?p=428
//cpu bus junk bits

View File

@ -13,16 +13,32 @@ namespace BizHawk.Emulation.Consoles.Nintendo
{
partial class PPU
{
//when the ppu issues a write it goes through here and into the game board
void ppubus_write(int addr, byte value)
{
nes.board.WritePPU(addr, value);
}
//when the ppu issues a read it goes through here and into the game board
byte ppubus_read(int addr)
{
return nes.board.ReadPPU(addr);
}
//boards may not respond to a read, in which case this will get called. please apply mirroring logic beforehand
public byte ppu_defaultRead(int addr)
{
addr &= 0x7FF;
return NTARAM[addr];
}
//boards may not respond to a write, in which case this will get called. please apply mirroring logic beforehand
public void ppu_defaultWrite(int addr, byte value)
{
addr &= 0x7FF;
NTARAM[addr] = value;
}
enum PPUPHASE {
VBL, BG, OBJ
};

View File

@ -68,7 +68,7 @@ namespace BizHawk.Emulation.Consoles.Nintendo
public fixed byte oam[4];
public fixed byte patterns[2];
public byte index;
byte pad;
public byte present;
}
//TODO - check flashing sirens in werewolf
@ -249,34 +249,40 @@ namespace BizHawk.Emulation.Consoles.Nintendo
//look for sprites (was supposed to run concurrent with bg rendering)
oamcounts[scanslot] = 0;
oamcount = 0;
if (sl == 0xb1)
{
int zzz = 9;
}
int spriteHeight = reg_2000.obj_size_16 ? 16 : 8;
for (int i = 0; i < 64; i++)
fixed (TempOAM* oam = &oams[scanslot, i])
oam->present = 0;
for (int i = 0; i < 64; i++)
{
int spr = i * 4;
if (yp >= OAM[spr] && yp < OAM[spr] + spriteHeight)
{
//if we already have maxsprites, then this new one causes an overflow,
//set the flag and bail out.
if (oamcount >= 8 && reg_2001.PPUON)
if (yp >= OAM[spr] && yp < OAM[spr] + spriteHeight)
{
Reg2002_objoverflow = true;
if (SPRITELIMIT)
break;
}
//if we already have maxsprites, then this new one causes an overflow,
//set the flag and bail out.
if (oamcount >= 8 && reg_2001.PPUON)
{
Reg2002_objoverflow = true;
if (SPRITELIMIT)
break;
}
//just copy some bytes into the internal sprite buffer
for (int j = 0; j < 4; j++)
//just copy some bytes into the internal sprite buffer
fixed (TempOAM* oam = &oams[scanslot, oamcount])
oam->oam[j] = OAM[spr + j];
{
for (int j = 0; j < 4; j++)
oam->oam[j] = OAM[spr + j];
oam->present = 1;
}
//note that we stuff the oam index into [6].
//i need to turn this into a struct so we can have fewer magic numbers
oams[scanslot, oamcount].index = (byte)i;
oamcount++;
//note that we stuff the oam index into [6].
//i need to turn this into a struct so we can have fewer magic numbers
oams[scanslot, oamcount].index = (byte)i;
oamcount++;
}
}
}
oamcounts[scanslot] = oamcount;
@ -303,11 +309,6 @@ namespace BizHawk.Emulation.Consoles.Nintendo
//fetch sprite patterns
for (int s = 0; s < MAXSPRITES; s++)
{
if (sl == 0x9E && s == 1)
{
int zzz = 9;
}
//if we have hit our eight sprite pattern and we dont have any more sprites, then bail
if (s == oamcount && s >= 8)
break;
@ -327,17 +328,24 @@ namespace BizHawk.Emulation.Consoles.Nintendo
int patternNumber = oam->oam[1];
int patternAddress;
//create deterministic dummy fetch pattern
if (oam->present==0)
{
patternNumber = 0;
line = 0;
}
//8x16 sprite handling:
if (reg_2000.obj_size_16)
{
int bank = (patternNumber & 1) << 12;
patternNumber = patternNumber & ~1;
patternNumber |= (line >> 3);
patternNumber |= (line >> 3)&1;
patternAddress = (patternNumber << 4) | bank;
}
else
{
patternAddress = (patternNumber << 4) | (reg_2000.obj_pattern_hi << 9);
patternAddress = (patternNumber << 4) | (reg_2000.obj_pattern_hi << 12);
}
//offset into the pattern for the current line.
@ -347,6 +355,7 @@ namespace BizHawk.Emulation.Consoles.Nintendo
//garbage nametable fetches
//reset the scroll counter, happens at cycle 304
//TODO - compact this logic
if (realSprite)
{
if ((sl == 0) && reg_2001.PPUON)

View File

@ -25,12 +25,32 @@ namespace BizHawk
return new string[0];
return MetaData.Split(';').Where(opt => string.IsNullOrEmpty(opt) == false).ToArray();
}
public Dictionary<string, string> ParseOptionsDictionary()
{
var ret = new Dictionary<string, string>();
foreach (var opt in GetOptions())
{
var parts = opt.Split('=');
var key = parts[0];
var value = parts.Length > 1 ? parts[1] : "";
ret[key] = value;
}
return ret;
}
}
public static class Database
{
private static Dictionary<string, GameInfo> db = new Dictionary<string, GameInfo>();
public static GameInfo CheckDatabase(string hash)
{
GameInfo ret = null;
db.TryGetValue(hash, out ret);
return ret;
}
public static void LoadDatabase(string path)
{
using (var reader = new StreamReader(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
@ -46,7 +66,7 @@ namespace BizHawk
string[] items = line.Split('\t');
var Game = new GameInfo();
Game.hash = items[0];
Game.hash = items[0].ToUpper();
Game.Name = items[2];
Game.System = items[3];
Game.MetaData = items.Length >= 6 ? items[5] : null;
@ -61,13 +81,14 @@ namespace BizHawk
public static GameInfo GetGameInfo(byte[] RomData, string fileName)
{
GameInfo ret;
string hash = string.Format("{0:X8}", CRC32.Calculate(RomData));
if (db.ContainsKey(hash))
return db[hash];
if (db.TryGetValue(hash, out ret))
return ret;
hash = Util.BytesToHexString(System.Security.Cryptography.MD5.Create().ComputeHash(RomData));
if (db.ContainsKey(hash))
return db[hash];
if (db.TryGetValue(hash, out ret))
return ret;
// rom is not in database. make some best-guesses
var Game = new GameInfo();

View File

@ -37,6 +37,8 @@ namespace BizHawk
}
}
public byte[] GetFileData() { return null; }
public byte[] GetRomData()
{
return RomData;

View File

@ -65,6 +65,7 @@ namespace BizHawk
return output;
}
public byte[] GetFileData() { return null; }
public byte[] GetRomData()
{
return RomData;

View File

@ -5,6 +5,7 @@ namespace BizHawk
public interface IGame
{
byte[] GetRomData();
byte[] GetFileData();
IList<string> GetOptions();
string Name { get; }
}

View File

@ -317,7 +317,7 @@
</ItemGroup>
<ItemGroup>
<Content Include="config\ControllerImages\NESController.PNG" />
<Content Include="gamedb.txt" />
<Content Include="output\gamedb.txt" />
<None Include="images\Refresh.bmp" />
<None Include="images\TruncateFromRW.png" />
<None Include="images\TruncateFromFile.png" />

View File

@ -12,12 +12,13 @@ namespace BizHawk.MultiClient
try
{
var file = new FileInfo(filepath);
using (var reader = file.OpenText())
{
var s = new JsonSerializer();
var r = new JsonReader(reader);
config = (T) s.Deserialize(r, typeof (T));
}
if(file.Exists)
using (var reader = file.OpenText())
{
var s = new JsonSerializer();
var r = new JsonReader(reader);
config = (T) s.Deserialize(r, typeof (T));
}
}
catch { }
return config;

View File

@ -7,6 +7,7 @@ namespace BizHawk.MultiClient
public class RomGame : IGame
{
public byte[] RomData;
public byte[] FileData;
public string System;
private string name;
@ -24,22 +25,17 @@ namespace BizHawk.MultiClient
var stream = file.GetStream();
if (file.Extension == "NES")
{
RomData = Util.ReadAllBytes(stream);
}
else
{
int header = (int)(stream.Length % BankSize);
stream.Position = header;
int length = (int)stream.Length - header;
FileData = Util.ReadAllBytes(stream);
RomData = new byte[length];
stream.Read(RomData, 0, length);
int header = (int)(stream.Length % BankSize);
stream.Position = header;
int length = (int)stream.Length - header;
if (file.Extension == "SMD")
RomData = DeInterleaveSMD(RomData);
}
RomData = new byte[length];
stream.Read(RomData, 0, length);
if (file.Extension == "SMD")
RomData = DeInterleaveSMD(RomData);
var info = Database.GetGameInfo(RomData, file.FullName);
name = info.Name;
@ -105,6 +101,7 @@ namespace BizHawk.MultiClient
}
public byte[] GetRomData() { return RomData; }
public byte[] GetFileData() { return FileData; }
public IList<string> GetOptions() { return options; }
public string Name { get { return name; } }

View File

@ -2257,3 +2257,16 @@ B486A8ED Dai Makai Mura SGX
1F041166 Madoo Granzort SGX
D4448D09BBFDE687C04F9E3310E023AB ti83_1.rom TI83 initPC=6ce
8E3630186E35D477231BF8FD50E54CDD Super Mario Bros. (U) NES board=NROM;mirror=V;PRG=2;CHR=1
1457475741846A01399DC663135A60C1 Balloon Fight (JU) NES board=NROM;mirror=H;PRG=1;CHR=1
576AB245B4F04C670AC312AB0D441697 Baseball (UE) NES board=NROM;mirror=H;PRG=1;CHR=1
0F9C8D3D3099C70368411F6DF9EF49C1 Bomberman (U) NES board=NROM;mirror=V;PRG=1;CHR=1
D2C42603A8EC74F51265C085AE26B9BB Burger Time (U) NES board=NROM;mirror=H;PRG=1;CHR=1
EEBA6EF8992074C5EBBDF0ECD9468E10 Clu Clu Land (JU) NES board=NROM;mirror=H;PRG=1;CHR=1
E36BC14876DA0E25C4EE2BBA193002C4 Defender 2 (U) NES board=NROM;mirror=V;PRG=1;CHR=1
A2B5BDDB4C7A5A39C8FAC13E64494C9A Donkey Kong 3 (JUE) board=NROM;mirror=V;PRG=1;CHR=1
6D4A94C344463E562344249E18E9B99F Donkey Kong (JU) NES board=NROM;mirror=H;PRG=1;CHR=1
8B7C1E5B55A9E5FA23E895DF2D682914 Donkey Kong Jr (JU) NES board=NROM;mirror=H;PRG=1;CHR=1
1CA706896A8D4F2A2B5480D941130A4A Donkey Kong Jr Math (U) NES board=NROM;mirror=V;PRG=1;CHR=1
FA382374EB4A93A719064CA6C5A4E78C Duck Hunt (JUE) NES board=NROM;mirror=V;PRG=1;CHR=1