ppu open bus emulation

cpu_dummy_writes_ppumem - passes
ppu_open_bus - passes
This commit is contained in:
alyosha-tas 2016-06-21 17:11:06 -04:00 committed by GitHub
parent ec27890aba
commit ff4feb5ee8
4 changed files with 2470 additions and 0 deletions

672
NES.Core.cs Normal file
View File

@ -0,0 +1,672 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BizHawk.Common;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores.Components.M6502;
#pragma warning disable 162
namespace BizHawk.Emulation.Cores.Nintendo.NES
{
public partial class NES : IEmulator
{
//hardware/state
public MOS6502X cpu;
int cpu_accumulate; //cpu timekeeper
public PPU ppu;
public APU apu;
public byte[] ram;
NESWatch[] sysbus_watch = new NESWatch[65536];
public byte[] CIRAM; //AKA nametables
string game_name = string.Empty; //friendly name exposed to user and used as filename base
CartInfo cart; //the current cart prototype. should be moved into the board, perhaps
internal INESBoard Board; //the board hardware that is currently driving things
EDetectionOrigin origin = EDetectionOrigin.None;
int sprdma_countdown;
bool _irq_apu; //various irq signals that get merged to the cpu irq pin
/// <summary>clock speed of the main cpu in hz</summary>
public int cpuclockrate { get; private set; }
//irq state management
public bool irq_apu { get { return _irq_apu; } set { _irq_apu = value; } }
//user configuration
int[] palette_compiled = new int[64*8];
// new input system
NESControlSettings ControllerSettings; // this is stored internally so that a new change of settings won't replace
IControllerDeck ControllerDeck;
byte latched4016;
private DisplayType _display_type = DisplayType.NTSC;
//Sound config
public void SetSquare1(int v) { apu.Square1V = v; }
public void SetSquare2(int v) { apu.Square2V = v; }
public void SetTriangle(int v) { apu.TriangleV = v; }
public void SetNoise(int v) { apu.NoiseV = v; }
public void SetDMC(int v) { apu.DMCV = v; }
/// <summary>
/// for debugging only!
/// </summary>
/// <returns></returns>
public INESBoard GetBoard()
{
return Board;
}
public void Dispose()
{
if (magicSoundProvider != null)
magicSoundProvider.Dispose();
magicSoundProvider = null;
}
class MagicSoundProvider : ISoundProvider, ISyncSoundProvider, IDisposable
{
BlipBuffer blip;
NES nes;
const int blipbuffsize = 4096;
public MagicSoundProvider(NES nes, uint infreq)
{
this.nes = nes;
blip = new BlipBuffer(blipbuffsize);
blip.SetRates(infreq, 44100);
//var actualMetaspu = new Sound.MetaspuSoundProvider(Sound.ESynchMethod.ESynchMethod_V);
//1.789773mhz NTSC
//resampler = new Sound.Utilities.SpeexResampler(2, infreq, 44100 * APU.DECIMATIONFACTOR, infreq, 44100, actualMetaspu.buffer.enqueue_samples);
//output = new Sound.Utilities.DCFilter(actualMetaspu);
}
public void GetSamples(short[] samples)
{
//Console.WriteLine("Sync: {0}", nes.apu.dlist.Count);
int nsamp = samples.Length / 2;
if (nsamp > blipbuffsize) // oh well.
nsamp = blipbuffsize;
uint targetclock = (uint)blip.ClocksNeeded(nsamp);
uint actualclock = nes.apu.sampleclock;
foreach (var d in nes.apu.dlist)
blip.AddDelta(d.time * targetclock / actualclock, d.value);
nes.apu.dlist.Clear();
blip.EndFrame(targetclock);
nes.apu.sampleclock = 0;
blip.ReadSamples(samples, nsamp, true);
// duplicate to stereo
for (int i = 0; i < nsamp * 2; i += 2)
samples[i + 1] = samples[i];
//mix in the cart's extra sound circuit
nes.Board.ApplyCustomAudio(samples);
}
public void GetSamples(out short[] samples, out int nsamp)
{
//Console.WriteLine("ASync: {0}", nes.apu.dlist.Count);
foreach (var d in nes.apu.dlist)
blip.AddDelta(d.time, d.value);
nes.apu.dlist.Clear();
blip.EndFrame(nes.apu.sampleclock);
nes.apu.sampleclock = 0;
nsamp = blip.SamplesAvailable();
samples = new short[nsamp * 2];
blip.ReadSamples(samples, nsamp, true);
// duplicate to stereo
for (int i = 0; i < nsamp * 2; i += 2)
samples[i + 1] = samples[i];
nes.Board.ApplyCustomAudio(samples);
}
public void DiscardSamples()
{
nes.apu.dlist.Clear();
nes.apu.sampleclock = 0;
}
public int MaxVolume { get; set; }
public void Dispose()
{
if (blip != null)
{
blip.Dispose();
blip = null;
}
}
}
MagicSoundProvider magicSoundProvider;
public void HardReset()
{
cpu = new MOS6502X();
cpu.SetCallbacks(ReadMemory, ReadMemory, PeekMemory, WriteMemory);
cpu.BCD_Enabled = false;
cpu.OnExecFetch = ExecFetch;
ppu = new PPU(this);
ram = new byte[0x800];
CIRAM = new byte[0x800];
// wire controllers
// todo: allow changing this
ControllerDeck = ControllerSettings.Instantiate(ppu.LightGunCallback);
// set controller definition first time only
if (ControllerDefinition == null)
{
ControllerDefinition = new ControllerDefinition(ControllerDeck.GetDefinition());
ControllerDefinition.Name = "NES Controller";
// controls other than the deck
ControllerDefinition.BoolButtons.Add("Power");
ControllerDefinition.BoolButtons.Add("Reset");
if (Board is FDS)
{
var b = Board as FDS;
ControllerDefinition.BoolButtons.Add("FDS Eject");
for (int i = 0; i < b.NumSides; i++)
ControllerDefinition.BoolButtons.Add("FDS Insert " + i);
}
}
// don't replace the magicSoundProvider on reset, as it's not needed
// if (magicSoundProvider != null) magicSoundProvider.Dispose();
// set up region
switch (_display_type)
{
case Common.DisplayType.PAL:
apu = new APU(this, apu, true);
ppu.region = PPU.Region.PAL;
CoreComm.VsyncNum = 50;
CoreComm.VsyncDen = 1;
cpuclockrate = 1662607;
cpu_sequence = cpu_sequence_PAL;
_display_type = DisplayType.PAL;
break;
case Common.DisplayType.NTSC:
apu = new APU(this, apu, false);
ppu.region = PPU.Region.NTSC;
CoreComm.VsyncNum = 39375000;
CoreComm.VsyncDen = 655171;
cpuclockrate = 1789773;
cpu_sequence = cpu_sequence_NTSC;
break;
// this is in bootgod, but not used at all
case Common.DisplayType.DENDY:
apu = new APU(this, apu, false);
ppu.region = PPU.Region.Dendy;
CoreComm.VsyncNum = 50;
CoreComm.VsyncDen = 1;
cpuclockrate = 1773448;
cpu_sequence = cpu_sequence_NTSC;
_display_type = DisplayType.DENDY;
break;
default:
throw new Exception("Unknown displaytype!");
}
if (magicSoundProvider == null)
magicSoundProvider = new MagicSoundProvider(this, (uint)cpuclockrate);
BoardSystemHardReset();
//check fceux's PowerNES and FCEU_MemoryRand function for more information:
//relevant games: Cybernoid; Minna no Taabou no Nakayoshi Daisakusen; Huang Di; and maybe mechanized attack
for(int i=0;i<0x800;i++) if((i&4)!=0) ram[i] = 0xFF; else ram[i] = 0x00;
SetupMemoryDomains();
//in this emulator, reset takes place instantaneously
cpu.PC = (ushort)(ReadMemory(0xFFFC) | (ReadMemory(0xFFFD) << 8));
cpu.P = 0x34;
cpu.S = 0xFD;
}
bool resetSignal;
bool hardResetSignal;
public void FrameAdvance(bool render, bool rendersound)
{
if (Tracer.Enabled)
cpu.TraceCallback = (s) => Tracer.Put(s);
else
cpu.TraceCallback = null;
lagged = true;
if (resetSignal)
{
Board.NESSoftReset();
cpu.NESSoftReset();
apu.NESSoftReset();
ppu.NESSoftReset();
}
else if (hardResetSignal)
{
HardReset();
}
Frame++;
//if (resetSignal)
//Controller.UnpressButton("Reset"); TODO fix this
resetSignal = Controller["Reset"];
hardResetSignal = Controller["Power"];
if (Board is FDS)
{
var b = Board as FDS;
if (Controller["FDS Eject"])
b.Eject();
for (int i = 0; i < b.NumSides; i++)
if (Controller["FDS Insert " + i])
b.InsertSide(i);
}
ppu.FrameAdvance();
if (lagged)
{
_lagcount++;
islag = true;
}
else
islag = false;
videoProvider.FillFrameBuffer();
}
//PAL:
//0 15 30 45 60 -> 12 27 42 57 -> 9 24 39 54 -> 6 21 36 51 -> 3 18 33 48 -> 0
//sequence of ppu clocks per cpu clock: 3,3,3,3,4
//at least it should be, but something is off with that (start up time?) so it is 3,3,3,4,3 for now
//NTSC:
//sequence of ppu clocks per cpu clock: 3
ByteBuffer cpu_sequence;
static ByteBuffer cpu_sequence_NTSC = new ByteBuffer(new byte[]{3,3,3,3,3});
static ByteBuffer cpu_sequence_PAL = new ByteBuffer(new byte[]{3,3,3,4,3});
public int cpu_step, cpu_stepcounter, cpu_deadcounter;
#if VS2012
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
internal void RunCpuOne()
{
cpu_stepcounter++;
if (cpu_stepcounter == cpu_sequence[cpu_step])
{
cpu_step++;
if(cpu_step == 5) cpu_step=0;
cpu_stepcounter = 0;
if (sprdma_countdown > 0)
{
sprdma_countdown--;
if (sprdma_countdown == 0)
{
//its weird that this is 514.. normally itd be 512 (and people would say its wrong) or 513 (and people would say its right)
//but 514 passes test 4-irq_and_dma
// according to nesdev wiki, http://wiki.nesdev.com/w/index.php/PPU_OAM this is 513 on even cycles and 514 on odd cycles
// TODO: Implement that
cpu_deadcounter += 514;
}
}
if (apu.dmc_dma_countdown>0)
{
cpu.RDY = false;
apu.dmc_dma_countdown--;
if (apu.dmc_dma_countdown==0)
{
apu.RunDMCFetch();
cpu.RDY = true;
}
if (apu.dmc_dma_countdown==0)
{
apu.dmc_dma_countdown = -1;
}
}
if (cpu_deadcounter > 0)
{
cpu_deadcounter--;
}
else
{
cpu.IRQ = _irq_apu || Board.IRQSignal;
cpu.ExecuteOne();
}
ppu.ppu_open_bus_decay(0);
apu.RunOne();
Board.ClockCPU();
ppu.PostCpuInstructionOne();
}
}
#if VS2012
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public byte ReadReg(int addr)
{
switch (addr)
{
case 0x4000: case 0x4001: case 0x4002: case 0x4003:
case 0x4004: case 0x4005: case 0x4006: case 0x4007:
case 0x4008: case 0x4009: case 0x400A: case 0x400B:
case 0x400C: case 0x400D: case 0x400E: case 0x400F:
case 0x4010: case 0x4011: case 0x4012: case 0x4013:
return apu.ReadReg(addr);
case 0x4014: /*OAM DMA*/ break;
case 0x4015: return apu.ReadReg(addr);
case 0x4016:
case 0x4017:
return read_joyport(addr);
default:
//Console.WriteLine("read register: {0:x4}", addr);
break;
}
return 0xFF;
}
public byte PeekReg(int addr)
{
switch (addr)
{
case 0x4000: case 0x4001: case 0x4002: case 0x4003:
case 0x4004: case 0x4005: case 0x4006: case 0x4007:
case 0x4008: case 0x4009: case 0x400A: case 0x400B:
case 0x400C: case 0x400D: case 0x400E: case 0x400F:
case 0x4010: case 0x4011: case 0x4012: case 0x4013:
return apu.PeekReg(addr);
case 0x4014: /*OAM DMA*/ break;
case 0x4015: return apu.PeekReg(addr);
case 0x4016:
case 0x4017:
return peek_joyport(addr);
default:
//Console.WriteLine("read register: {0:x4}", addr);
break;
}
return 0xFF;
}
void WriteReg(int addr, byte val)
{
switch (addr)
{
case 0x4000: case 0x4001: case 0x4002: case 0x4003:
case 0x4004: case 0x4005: case 0x4006: case 0x4007:
case 0x4008: case 0x4009: case 0x400A: case 0x400B:
case 0x400C: case 0x400D: case 0x400E: case 0x400F:
case 0x4010: case 0x4011: case 0x4012: case 0x4013:
apu.WriteReg(addr, val);
break;
case 0x4014: Exec_OAMDma(val); break;
case 0x4015: apu.WriteReg(addr, val); break;
case 0x4016:
write_joyport(val);
break;
case 0x4017: apu.WriteReg(addr, val); break;
default:
//Console.WriteLine("wrote register: {0:x4} = {1:x2}", addr, val);
break;
}
}
void write_joyport(byte value)
{
var si = new StrobeInfo(latched4016, value);
ControllerDeck.Strobe(si, Controller);
latched4016 = value;
}
byte read_joyport(int addr)
{
InputCallbacks.Call();
lagged = false;
byte ret = addr == 0x4016 ? ControllerDeck.ReadA(Controller) : ControllerDeck.ReadB(Controller);
ret &= 0x1f;
ret |= (byte)(0xe0 & DB);
return ret;
}
byte peek_joyport(int addr)
{
// at the moment, the new system doesn't support peeks
return 0;
}
void Exec_OAMDma(byte val)
{
ushort addr = (ushort)(val << 8);
for (int i = 0; i < 256; i++)
{
byte db = ReadMemory((ushort)addr);
WriteMemory(0x2004, db);
addr++;
}
//schedule a sprite dma event for beginning 1 cycle in the future.
//this receives 2 because thats just the way it works out.
sprdma_countdown = 2;
}
/// <summary>
/// Sets the provided palette as current.
/// Applies the current deemph settings if needed to expand a 64-entry palette to 512
/// </summary>
private void SetPalette(byte[,] pal)
{
int nColors = pal.GetLength(0);
int nElems = pal.GetLength(1);
if (nColors == 512)
{
//just copy the palette directly
for (int c = 0; c < 64 * 8; c++)
{
int r = pal[c, 0];
int g = pal[c, 1];
int b = pal[c, 2];
palette_compiled[c] = (int)unchecked((int)0xFF000000 | (r << 16) | (g << 8) | b);
}
}
else
{
//expand using deemph
for (int i = 0; i < 64 * 8; i++)
{
int d = i >> 6;
int c = i & 63;
int r = pal[c, 0];
int g = pal[c, 1];
int b = pal[c, 2];
Palettes.ApplyDeemphasis(ref r, ref g, ref b, d);
palette_compiled[i] = (int)unchecked((int)0xFF000000 | (r << 16) | (g << 8) | b);
}
}
}
/// <summary>
/// looks up an internal NES pixel value to an rgb int (applying the core's current palette and assuming no deemph)
/// </summary>
public int LookupColor(int pixel)
{
return palette_compiled[pixel];
}
public byte DummyReadMemory(ushort addr) { return 0; }
private void ApplySystemBusPoke(int addr, byte value)
{
if (addr < 0x2000)
{
ram[(addr & 0x7FF)] = value;
}
else if (addr < 0x4000)
{
ppu.WriteReg((addr & 0x07), value);
}
else if (addr < 0x4020)
{
WriteReg(addr, value);
}
else
{
ApplyGameGenie(addr, value, null); //Apply a cheat to the remaining regions since they have no direct access, this may not be the best way to handle this situation
}
}
public byte PeekMemory(ushort addr)
{
byte ret;
if (addr >= 0x4020)
{
ret = Board.PeekCart(addr); //easy optimization, since rom reads are so common, move this up (reordering the rest of these elseifs is not easy)
}
else if (addr < 0x0800)
{
ret = ram[addr];
}
else if (addr < 0x2000)
{
ret = ram[addr & 0x7FF];
}
else if (addr < 0x4000)
{
ret = Board.PeekReg2xxx(addr);
}
else if (addr < 0x4020)
{
ret = PeekReg(addr); //we're not rebasing the register just to keep register names canonical
}
else
{
throw new Exception("Woopsie-doodle!");
ret = 0xFF;
}
return ret;
}
//old data bus values from previous reads
public byte DB;
public void ExecFetch(ushort addr)
{
MemoryCallbacks.CallExecutes(addr);
}
public byte ReadMemory(ushort addr)
{
byte ret;
if (addr >= 0x8000)
{
ret = Board.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 < 0x0800)
{
ret = ram[addr];
}
else if (addr < 0x2000)
{
ret = ram[addr & 0x7FF];
}
else if (addr < 0x4000)
{
ret = Board.ReadReg2xxx(addr);
}
else if (addr < 0x4020)
{
ret = ReadReg(addr); //we're not rebasing the register just to keep register names canonical
}
else if (addr < 0x6000)
{
ret = Board.ReadEXP(addr - 0x4000);
}
else
{
ret = Board.ReadWRAM(addr - 0x6000);
}
//handle breakpoints and stuff.
//the idea is that each core can implement its own watch class on an address which will track all the different kinds of monitors and breakpoints and etc.
//but since freeze is a common case, it was implemented through its own mechanisms
if (sysbus_watch[addr] != null)
{
sysbus_watch[addr].Sync();
ret = sysbus_watch[addr].ApplyGameGenie(ret);
}
MemoryCallbacks.CallReads(addr);
DB = ret;
return ret;
}
public void ApplyGameGenie(int addr, byte value, byte? compare)
{
if (addr < sysbus_watch.Length)
{
GetWatch(NESWatch.EDomain.Sysbus, addr).SetGameGenie(compare, value);
}
}
public void RemoveGameGenie(int addr)
{
if (addr < sysbus_watch.Length)
{
GetWatch(NESWatch.EDomain.Sysbus, addr).RemoveGameGenie();
}
}
public void WriteMemory(ushort addr, byte value)
{
if (addr < 0x0800)
{
ram[addr] = value;
}
else if (addr < 0x2000)
{
ram[addr & 0x7FF] = value;
}
else if (addr < 0x4000)
{
Board.WriteReg2xxx(addr,value);
}
else if (addr < 0x4020)
{
WriteReg(addr, value); //we're not rebasing the register just to keep register names canonical
}
else if (addr < 0x6000)
{
Board.WriteEXP(addr - 0x4000, value);
}
else if (addr < 0x8000)
{
Board.WriteWRAM(addr - 0x6000, value);
}
else
{
Board.WritePRG(addr - 0x8000, value);
}
MemoryCallbacks.CallWrites(addr);
}
}
}

827
NES.cs Normal file
View File

@ -0,0 +1,827 @@
using System;
using System.Linq;
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using BizHawk.Common;
using BizHawk.Common.BufferExtensions;
using BizHawk.Emulation.Common;
//TODO - redo all timekeeping in terms of master clock
namespace BizHawk.Emulation.Cores.Nintendo.NES
{
[CoreAttributes(
"NesHawk",
"zeromus, natt, adelikat",
isPorted: false,
isReleased: true
)]
public partial class NES : IEmulator, ISaveRam, IDebuggable, IStatable, IInputPollable, IRegionable,
ISettable<NES.NESSettings, NES.NESSyncSettings>
{
static readonly bool USE_DATABASE = true;
public RomStatus RomStatus;
[CoreConstructor("NES")]
public NES(CoreComm comm, GameInfo game, byte[] rom, object Settings, object SyncSettings)
{
var ser = new BasicServiceProvider(this);
ServiceProvider = ser;
byte[] fdsbios = comm.CoreFileProvider.GetFirmware("NES", "Bios_FDS", false);
if (fdsbios != null && fdsbios.Length == 40976)
{
comm.ShowMessage("Your FDS BIOS is a bad dump. BizHawk will attempt to use it, but no guarantees! You should find a new one.");
var tmp = new byte[8192];
Buffer.BlockCopy(fdsbios, 16 + 8192 * 3, tmp, 0, 8192);
fdsbios = tmp;
}
this.SyncSettings = (NESSyncSettings)SyncSettings ?? new NESSyncSettings();
this.ControllerSettings = this.SyncSettings.Controls;
CoreComm = comm;
MemoryCallbacks = new MemoryCallbackSystem();
BootGodDB.Initialize();
videoProvider = new MyVideoProvider(this);
Init(game, rom, fdsbios);
if (Board is FDS)
{
DriveLightEnabled = true;
(Board as FDS).SetDriveLightCallback((val) => DriveLightOn = val);
// bit of a hack: we don't have a private gamedb for FDS, but the frontend
// expects this to be set.
RomStatus = game.Status;
}
PutSettings((NESSettings)Settings ?? new NESSettings());
ser.Register<IDisassemblable>(cpu);
Tracer = new TraceBuffer { Header = cpu.TraceHeader };
ser.Register<ITraceable>(Tracer);
ser.Register<IVideoProvider>(videoProvider);
if (Board is BANDAI_FCG_1)
{
var reader = (Board as BANDAI_FCG_1).reader;
// not all BANDAI FCG 1 boards have a barcode reader
if (reader != null)
ser.Register<DatachBarcode>(reader);
}
}
public IEmulatorServiceProvider ServiceProvider { get; private set; }
private NES()
{
BootGodDB.Initialize();
}
public void WriteLogTimestamp()
{
if (ppu != null)
Console.Write("[{0:d5}:{1:d3}:{2:d3}]", Frame, ppu.ppur.status.sl, ppu.ppur.status.cycle);
}
public void LogLine(string format, params object[] args)
{
if (ppu != null)
Console.WriteLine("[{0:d5}:{1:d3}:{2:d3}] {3}", Frame, ppu.ppur.status.sl, ppu.ppur.status.cycle, string.Format(format, args));
}
public bool HasMapperProperties
{
get
{
var fields = Board.GetType().GetFields();
foreach (var field in fields)
{
var attrib = field.GetCustomAttributes(typeof(MapperPropAttribute), false).OfType<MapperPropAttribute>().SingleOrDefault();
if (attrib != null)
{
return true;
}
}
return false;
}
}
NESWatch GetWatch(NESWatch.EDomain domain, int address)
{
if (domain == NESWatch.EDomain.Sysbus)
{
NESWatch ret = sysbus_watch[address] ?? new NESWatch(this, domain, address);
sysbus_watch[address] = ret;
return ret;
}
return null;
}
class NESWatch
{
public enum EDomain
{
Sysbus
}
public NESWatch(NES nes, EDomain domain, int address)
{
Address = address;
Domain = domain;
if (domain == EDomain.Sysbus)
{
watches = nes.sysbus_watch;
}
}
public int Address;
public EDomain Domain;
public enum EFlags
{
None = 0,
GameGenie = 1,
ReadPrint = 2
}
EFlags flags;
public void Sync()
{
if (flags == EFlags.None)
watches[Address] = null;
else watches[Address] = this;
}
public void SetGameGenie(byte? compare, byte value)
{
flags |= EFlags.GameGenie;
Compare = compare;
Value = value;
Sync();
}
public bool HasGameGenie
{
get
{
return (flags & EFlags.GameGenie) != 0;
}
}
public byte ApplyGameGenie(byte curr)
{
if (!HasGameGenie)
{
return curr;
}
else if (curr == Compare || Compare == null)
{
Console.WriteLine("applied game genie");
return (byte)Value;
}
else
{
return curr;
}
}
public void RemoveGameGenie()
{
flags &= ~EFlags.GameGenie;
Sync();
}
byte? Compare;
byte Value;
NESWatch[] watches;
}
public CoreComm CoreComm { get; private set; }
public DisplayType Region { get { return _display_type; } }
class MyVideoProvider : IVideoProvider
{
//public int ntsc_top = 8;
//public int ntsc_bottom = 231;
//public int pal_top = 0;
//public int pal_bottom = 239;
public int left = 0;
public int right = 255;
NES emu;
public MyVideoProvider(NES emu)
{
this.emu = emu;
}
int[] pixels = new int[256 * 240];
public int[] GetVideoBuffer()
{
return pixels;
}
public void FillFrameBuffer()
{
int the_top;
int the_bottom;
if (emu.Region == DisplayType.NTSC)
{
the_top = emu.Settings.NTSC_TopLine;
the_bottom = emu.Settings.NTSC_BottomLine;
}
else
{
the_top = emu.Settings.PAL_TopLine;
the_bottom = emu.Settings.PAL_BottomLine;
}
int backdrop = 0;
backdrop = emu.Settings.BackgroundColor;
bool useBackdrop = (backdrop & 0xFF000000) != 0;
if (useBackdrop)
{
int width = BufferWidth;
for (int x = left; x <= right; x++)
{
for (int y = the_top; y <= the_bottom; y++)
{
short pixel = emu.ppu.xbuf[(y << 8) + x];
if ((pixel & 0x8000) != 0 && useBackdrop)
{
pixels[((y - the_top) * width) + (x - left)] = backdrop;
}
else pixels[((y - the_top) * width) + (x - left)] = emu.palette_compiled[pixel & 0x7FFF];
}
}
}
else
{
unsafe
{
fixed (int* dst_ = pixels)
fixed (short* src_ = emu.ppu.xbuf)
fixed (int* pal = emu.palette_compiled)
{
int* dst = dst_;
short* src = src_ + 256 * the_top + left;
int xcount = right - left + 1;
int srcinc = 256 - xcount;
int ycount = the_bottom - the_top + 1;
xcount /= 16;
for (int y = 0; y < ycount; y++)
{
for (int x = 0; x < xcount; x++)
{
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
*dst++ = pal[0x7fff & *src++];
}
src += srcinc;
}
}
}
}
}
public int VirtualWidth { get { return (int)(BufferWidth * 1.146); } }
public int VirtualHeight { get { return BufferHeight; } }
public int BufferWidth { get { return right - left + 1; } }
public int BackgroundColor { get { return 0; } }
public int BufferHeight
{
get
{
if (emu.Region == DisplayType.NTSC)
{
return emu.Settings.NTSC_BottomLine - emu.Settings.NTSC_TopLine + 1;
}
else
{
return emu.Settings.PAL_BottomLine - emu.Settings.PAL_TopLine + 1;
}
}
}
}
MyVideoProvider videoProvider;
public ISoundProvider SoundProvider { get { return magicSoundProvider; } }
public ISyncSoundProvider SyncSoundProvider { get { return magicSoundProvider; } }
public bool StartAsyncSound() { return true; }
public void EndAsyncSound() { }
[Obsolete] // with the changes to both nes and quicknes cores, nothing uses this anymore
public static readonly ControllerDefinition NESController =
new ControllerDefinition
{
Name = "NES Controller",
BoolButtons = {
"P1 Up", "P1 Down", "P1 Left", "P1 Right", "P1 Start", "P1 Select", "P1 B", "P1 A", "Reset", "Power",
"P2 Up", "P2 Down", "P2 Left", "P2 Right", "P2 Start", "P2 Select", "P2 B", "P2 A"
}
};
public ControllerDefinition ControllerDefinition { get; private set; }
IController controller;
public IController Controller
{
get { return controller; }
set { controller = value; }
}
int _frame;
public int Frame { get { return _frame; } set { _frame = value; } }
public void ResetCounters()
{
_frame = 0;
_lagcount = 0;
islag = false;
}
public long Timestamp { get; private set; }
public bool DeterministicEmulation { get { return true; } }
public string SystemId { get { return "NES"; } }
public string GameName { get { return game_name; } }
public enum EDetectionOrigin
{
None, BootGodDB, GameDB, INES, UNIF, FDS, NSF
}
StringWriter LoadReport;
void LoadWriteLine(string format, params object[] arg)
{
Console.WriteLine(format, arg);
LoadReport.WriteLine(format, arg);
}
void LoadWriteLine(object arg) { LoadWriteLine("{0}", arg); }
class MyWriter : StringWriter
{
public MyWriter(TextWriter _loadReport)
{
loadReport = _loadReport;
}
TextWriter loadReport;
public override void WriteLine(string format, params object[] arg)
{
Console.WriteLine(format, arg);
loadReport.WriteLine(format, arg);
}
public override void WriteLine(string value)
{
Console.WriteLine(value);
loadReport.WriteLine(value);
}
}
public void Init(GameInfo gameInfo, byte[] rom, byte[] fdsbios = null)
{
LoadReport = new StringWriter();
LoadWriteLine("------");
LoadWriteLine("BEGIN NES rom analysis:");
byte[] file = rom;
Type boardType = null;
CartInfo choice = null;
CartInfo iNesHeaderInfo = null;
CartInfo iNesHeaderInfoV2 = null;
List<string> hash_sha1_several = new List<string>();
string hash_sha1 = null, hash_md5 = null;
Unif unif = null;
Dictionary<string, string> InitialMapperRegisterValues = new Dictionary<string, string>(SyncSettings.BoardProperties);
origin = EDetectionOrigin.None;
if (file.Length < 16) throw new Exception("Alleged NES rom too small to be anything useful");
if (file.Take(4).SequenceEqual(System.Text.Encoding.ASCII.GetBytes("UNIF")))
{
unif = new Unif(new MemoryStream(file));
LoadWriteLine("Found UNIF header:");
LoadWriteLine(unif.CartInfo);
LoadWriteLine("Since this is UNIF we can confidently parse PRG/CHR banks to hash.");
hash_sha1 = unif.CartInfo.sha1;
hash_sha1_several.Add(hash_sha1);
LoadWriteLine("headerless rom hash: {0}", hash_sha1);
}
else if(file.Take(5).SequenceEqual(System.Text.Encoding.ASCII.GetBytes("NESM\x1A")))
{
origin = EDetectionOrigin.NSF;
LoadWriteLine("Loading as NSF");
var nsf = new NSFFormat();
nsf.WrapByteArray(file);
cart = new CartInfo();
var nsfboard = new NSFBoard();
nsfboard.Create(this);
nsfboard.ROM = rom;
nsfboard.InitNSF( nsf);
nsfboard.InitialRegisterValues = InitialMapperRegisterValues;
nsfboard.Configure(origin);
nsfboard.WRAM = new byte[cart.wram_size * 1024];
Board = nsfboard;
Board.PostConfigure();
AutoMapperProps.Populate(Board, SyncSettings);
Console.WriteLine("Using NTSC display type for NSF for now");
_display_type = Common.DisplayType.NTSC;
HardReset();
return;
}
else if (file.Take(4).SequenceEqual(System.Text.Encoding.ASCII.GetBytes("FDS\x1A"))
|| file.Take(4).SequenceEqual(System.Text.Encoding.ASCII.GetBytes("\x01*NI")))
{
// danger! this is a different codepath with an early return. accordingly, some
// code is duplicated twice...
// FDS roms are just fed to the board, we don't do much else with them
origin = EDetectionOrigin.FDS;
LoadWriteLine("Found FDS header.");
if (fdsbios == null)
throw new MissingFirmwareException("Missing FDS Bios");
cart = new CartInfo();
var fdsboard = new FDS();
fdsboard.biosrom = fdsbios;
fdsboard.SetDiskImage(rom);
fdsboard.Create(this);
// at the moment, FDS doesn't use the IRVs, but it could at some point in the future
fdsboard.InitialRegisterValues = InitialMapperRegisterValues;
fdsboard.Configure(origin);
Board = fdsboard;
//create the vram and wram if necessary
if (cart.wram_size != 0)
Board.WRAM = new byte[cart.wram_size * 1024];
if (cart.vram_size != 0)
Board.VRAM = new byte[cart.vram_size * 1024];
Board.PostConfigure();
AutoMapperProps.Populate(Board, SyncSettings);
Console.WriteLine("Using NTSC display type for FDS disk image");
_display_type = Common.DisplayType.NTSC;
HardReset();
return;
}
else
{
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:" + file.HashSHA1(16, file.Length - 16);
hash_sha1_several.Add(hash_sha1);
hash_md5 = "md5:" + file.HashMD5(16, file.Length - 16);
LoadWriteLine("Found iNES header:");
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("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:" + bytes.HashSHA1(0, bytes.Length);
LoadWriteLine(" PRG (8KB) + CHR hash: {0}", hash);
hash_sha1_several.Add(hash);
hash = "md5:" + bytes.HashMD5(0, bytes.Length);
LoadWriteLine(" PRG (8KB) + CHR hash: {0}", hash);
}
}
if (USE_DATABASE)
{
if (hash_md5 != null) choice = IdentifyFromGameDB(hash_md5);
if (choice == null)
choice = IdentifyFromGameDB(hash_sha1);
if (choice == null)
LoadWriteLine("Could not locate game in bizhawk gamedb");
else
{
origin = EDetectionOrigin.GameDB;
LoadWriteLine("Chose board from bizhawk gamedb: " + choice.board_type);
//gamedb entries that dont specify prg/chr sizes can infer it from the ines header
if (iNesHeaderInfo != null)
{
if (choice.prg_size == -1) choice.prg_size = iNesHeaderInfo.prg_size;
if (choice.chr_size == -1) choice.chr_size = iNesHeaderInfo.chr_size;
if (choice.vram_size == -1) choice.vram_size = iNesHeaderInfo.vram_size;
if (choice.wram_size == -1) choice.wram_size = iNesHeaderInfo.wram_size;
}
else if (unif != null)
{
if (choice.prg_size == -1) choice.prg_size = unif.CartInfo.prg_size;
if (choice.chr_size == -1) choice.chr_size = unif.CartInfo.chr_size;
// unif has no wram\vram sizes; hope the board impl can figure it out...
if (choice.vram_size == -1) choice.vram_size = 0;
if (choice.wram_size == -1) choice.wram_size = 0;
}
}
//if this is still null, we have to try it some other way. nescartdb perhaps?
if (choice == null)
{
choice = IdentifyFromBootGodDB(hash_sha1_several);
if (choice == null)
LoadWriteLine("Could not locate game in nescartdb");
else
{
LoadWriteLine("Chose board from nescartdb:");
LoadWriteLine(choice);
origin = EDetectionOrigin.BootGodDB;
}
}
}
//if choice is still null, try UNIF and iNES
if (choice == null)
{
if (unif != null)
{
LoadWriteLine("Using information from UNIF header");
choice = unif.CartInfo;
//ok, i have this Q-Boy rom with no VROM and no VRAM.
//we also certainly have games with VROM and no VRAM.
//looks like FCEUX policy is to allocate 8KB of chr ram no matter what UNLESS certain flags are set. but what's the justification for this? please leave a note if you go debugging in it again.
//well, we know we can't have much of a NES game if there's no VROM unless there's VRAM instead.
//so if the VRAM isn't set, choose 8 for it.
//TODO - unif loading code may need to use VROR flag to transform chr_size=8 to vram_size=8 (need example)
if (choice.chr_size == 0 && choice.vram_size == 0)
choice.vram_size = 8;
//(do we need to suppress this in case theres a CHR rom? probably not. nes board base will use ram if no rom is available)
origin = EDetectionOrigin.UNIF;
}
if (iNesHeaderInfo != null)
{
LoadWriteLine("Attempting inference from iNES header");
// try to spin up V2 header first, then V1 header
if (iNesHeaderInfoV2 != null)
{
try
{
boardType = FindBoard(iNesHeaderInfoV2, origin, InitialMapperRegisterValues);
}
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)
{
choice = iNesHeaderInfo; // we're out of options, really
boardType = FindBoard(iNesHeaderInfo, origin, InitialMapperRegisterValues);
if (boardType == null)
LoadWriteLine("Failed to load as iNES V1");
// 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:");
LoadWriteLine(choice);
origin = EDetectionOrigin.INES;
}
}
game_name = choice.name;
//find a INESBoard to handle this
if (choice != null)
boardType = FindBoard(choice, origin, InitialMapperRegisterValues);
else
throw new Exception("Unable to detect ROM");
if (boardType == null)
throw new Exception("No class implements the necessary board type: " + choice.board_type);
if (choice.DB_GameInfo != null)
choice.bad = choice.DB_GameInfo.IsRomStatusBad();
LoadWriteLine("Final game detection results:");
LoadWriteLine(choice);
LoadWriteLine("\"" + game_name + "\"");
LoadWriteLine("Implemented by: class " + boardType.Name);
if (choice.bad)
{
LoadWriteLine("~~ ONE WAY OR ANOTHER, THIS DUMP IS KNOWN TO BE *BAD* ~~");
LoadWriteLine("~~ YOU SHOULD FIND A BETTER FILE ~~");
}
LoadWriteLine("END NES rom analysis");
LoadWriteLine("------");
Board = CreateBoardInstance(boardType);
cart = choice;
Board.Create(this);
Board.InitialRegisterValues = InitialMapperRegisterValues;
Board.Configure(origin);
if (origin == EDetectionOrigin.BootGodDB)
{
RomStatus = RomStatus.GoodDump;
CoreComm.RomStatusAnnotation = "Identified from BootGod's database";
}
if (origin == EDetectionOrigin.UNIF)
{
RomStatus = RomStatus.NotInDatabase;
CoreComm.RomStatusAnnotation = "Inferred from UNIF header; somewhat suspicious";
}
if (origin == EDetectionOrigin.INES)
{
RomStatus = RomStatus.NotInDatabase;
CoreComm.RomStatusAnnotation = "Inferred from iNES header; potentially wrong";
}
if (origin == EDetectionOrigin.GameDB)
{
if (choice.bad)
{
RomStatus = RomStatus.BadDump;
}
else
{
RomStatus = choice.DB_GameInfo.Status;
}
}
byte[] trainer = null;
//create the board's rom and vrom
if (iNesHeaderInfo != null)
{
var ms = new MemoryStream(file, false);
ms.Seek(16, SeekOrigin.Begin); // ines header
//pluck the necessary bytes out of the file
if (iNesHeaderInfo.trainer_size != 0)
{
trainer = new byte[512];
ms.Read(trainer, 0, 512);
}
Board.ROM = new byte[choice.prg_size * 1024];
ms.Read(Board.ROM, 0, Board.ROM.Length);
if (choice.chr_size > 0)
{
Board.VROM = new byte[choice.chr_size * 1024];
int vrom_copy_size = ms.Read(Board.VROM, 0, Board.VROM.Length);
if (vrom_copy_size < Board.VROM.Length)
LoadWriteLine("Less than the expected VROM was found in the file: {0} < {1}", vrom_copy_size, Board.VROM.Length);
}
if (choice.prg_size != iNesHeaderInfo.prg_size || choice.chr_size != iNesHeaderInfo.chr_size)
LoadWriteLine("Warning: Detected choice has different filesizes than the INES header!");
}
else
{
Board.ROM = unif.PRG;
Board.VROM = unif.CHR;
}
LoadReport.Flush();
CoreComm.RomStatusDetails = LoadReport.ToString();
// IF YOU DO ANYTHING AT ALL BELOW THIS LINE, MAKE SURE THE APPROPRIATE CHANGE IS MADE TO FDS (if applicable)
//create the vram and wram if necessary
if (cart.wram_size != 0)
Board.WRAM = new byte[cart.wram_size * 1024];
if (cart.vram_size != 0)
Board.VRAM = new byte[cart.vram_size * 1024];
Board.PostConfigure();
AutoMapperProps.Populate(Board, SyncSettings);
// set up display type
NESSyncSettings.Region fromrom = DetectRegion(cart.system);
NESSyncSettings.Region fromsettings = SyncSettings.RegionOverride;
if (fromsettings != NESSyncSettings.Region.Default)
{
Console.WriteLine("Using system region override");
fromrom = fromsettings;
}
switch (fromrom)
{
case NESSyncSettings.Region.Dendy:
_display_type = Common.DisplayType.DENDY;
break;
case NESSyncSettings.Region.NTSC:
_display_type = Common.DisplayType.NTSC;
break;
case NESSyncSettings.Region.PAL:
_display_type = Common.DisplayType.PAL;
break;
default:
_display_type = Common.DisplayType.NTSC;
break;
}
Console.WriteLine("Using NES system region of {0}", _display_type);
HardReset();
if (trainer != null)
{
Console.WriteLine("Applying trainer");
for (int i = 0; i < 512; i++)
WriteMemory((ushort)(0x7000 + i), trainer[i]);
}
}
static NESSyncSettings.Region DetectRegion(string system)
{
switch (system)
{
case "NES-PAL":
case "NES-PAL-A":
case "NES-PAL-B":
return NESSyncSettings.Region.PAL;
case "NES-NTSC":
case "Famicom":
return NESSyncSettings.Region.NTSC;
// this is in bootgod, but not used at all
case "Dendy":
return NESSyncSettings.Region.Dendy;
case null:
Console.WriteLine("Rom is of unknown NES region!");
return NESSyncSettings.Region.Default;
default:
Console.WriteLine("Unrecognized region {0}", system);
return NESSyncSettings.Region.Default;
}
}
private ITraceable Tracer { get; set; }
}
}
//todo
//http://blog.ntrq.net/?p=428
//cpu bus junk bits
//UBER DOC
//http://nocash.emubase.de/everynes.htm
//A VERY NICE board assignments list
//http://personales.epsg.upv.es/~jogilmo1/nes/TEXTOS/ARXIUS/BOARDTABLE.TXT
//why not make boards communicate over the actual board pinouts
//http://wiki.nesdev.com/w/index.php/Cartridge_connector
//a mappers list
//http://tuxnes.sourceforge.net/nesmapper.txt
//some ppu tests
//http://nesdev.parodius.com/bbs/viewtopic.php?p=4571&sid=db4c7e35316cc5d734606dd02f11dccb

251
PPU.cs Normal file
View File

@ -0,0 +1,251 @@
//http://nesdev.parodius.com/bbs/viewtopic.php?p=4571&sid=db4c7e35316cc5d734606dd02f11dccb
using System;
using System.Runtime.CompilerServices;
using BizHawk.Common;
namespace BizHawk.Emulation.Cores.Nintendo.NES
{
public sealed partial class PPU
{
// this only handles region differences within the PPU
int preNMIlines;
int postNMIlines;
bool chopdot;
public enum Region { NTSC, PAL, Dendy, RGB };
Region _region;
public Region region { set { _region = value; SyncRegion(); } get { return _region; } }
void SyncRegion()
{
switch (region)
{
case Region.NTSC:
preNMIlines = 1; postNMIlines = 20; chopdot = true; break;
case Region.PAL:
preNMIlines = 1; postNMIlines = 70; chopdot = false; break;
case Region.Dendy:
preNMIlines = 51; postNMIlines = 20; chopdot = false; break;
case Region.RGB:
preNMIlines = 1; postNMIlines = 20; chopdot = false; break;
}
}
public class DebugCallback
{
public int Scanline;
//public int Dot; //not supported
public Action Callback;
}
public DebugCallback NTViewCallback;
public DebugCallback PPUViewCallback;
// true = light sensed
public bool LightGunCallback(int x, int y)
{
// the actual light gun circuit is very complex
// and this doesn't do it justice at all, as expected
const int radius = 10; // look at pixel values up to this far away, roughly
int sum = 0;
int ymin = Math.Max(Math.Max(y - radius, ppur.status.sl - 20), 0);
int ymax = Math.Min(y + radius, 239);
int xmin = Math.Max(x - radius, 0);
int xmax = Math.Min(x + radius, 255);
int ystop = ppur.status.sl - 2;
int xstop = ppur.status.cycle - 20;
for (int j = ymin; j <= ymax; j++)
{
for (int i = xmin; i <= xmax; i++)
{
if (j >= ystop && i >= xstop || j > ystop)
goto loopout;
short s = xbuf[j * 256 + i];
int lum = s & 0x30;
if ((s & 0x0f) >= 0x0e)
lum = 0;
sum += lum;
}
}
loopout:
return sum >= 2000;
}
//when the ppu issues a write it goes through here and into the game board
public void ppubus_write(int addr, byte value)
{
nes.Board.AddressPPU(addr);
nes.Board.WritePPU(addr, value);
}
//when the ppu issues a read it goes through here and into the game board
public byte ppubus_read(int addr, bool ppu)
{
//hardware doesnt touch the bus when the PPU is disabled
if (!reg_2001.PPUON && ppu)
return 0xFF;
nes.Board.AddressPPU(addr);
return nes.Board.ReadPPU(addr);
}
//debug tools peek into the ppu through this
public byte ppubus_peek(int addr)
{
return nes.Board.PeekPPU(addr);
}
public enum PPUPHASE
{
VBL, BG, OBJ
};
public PPUPHASE ppuphase;
private readonly NES nes;
public PPU(NES nes)
{
this.nes = nes;
OAM = new byte[0x100];
PALRAM = new byte[0x20];
//power-up palette verified by blargg's power_up_palette test.
//he speculates that these may differ depending on the system tested..
//and I don't see why the ppu would waste any effort setting these..
//but for the sake of uniformity, we'll do it.
Array.Copy(new byte[] {
0x09,0x01,0x00,0x01,0x00,0x02,0x02,0x0D,0x08,0x10,0x08,0x24,0x00,0x00,0x04,0x2C,
0x09,0x01,0x34,0x03,0x00,0x04,0x00,0x14,0x08,0x3A,0x00,0x02,0x00,0x20,0x2C,0x08
}, PALRAM, 0x20);
Reset();
}
public void NESSoftReset()
{
//this hasn't been brought up to date since NEShawk was first made.
//in particular http://wiki.nesdev.com/w/index.php/PPU_power_up_state should be studied, but theres no use til theres test cases
Reset();
}
//state
int ppudead; //measured in frames
bool idleSynch;
int NMI_PendingInstructions;
byte PPUGenLatch;
bool vtoggle;
byte VRAMBuffer;
public byte[] OAM;
public byte[] PALRAM;
public void SyncState(Serializer ser)
{
ser.Sync("ppudead", ref ppudead);
ser.Sync("idleSynch", ref idleSynch);
ser.Sync("NMI_PendingInstructions", ref NMI_PendingInstructions);
ser.Sync("PPUGenLatch", ref PPUGenLatch);
ser.Sync("vtoggle", ref vtoggle);
ser.Sync("VRAMBuffer", ref VRAMBuffer);
ser.Sync("ppu_addr_temp", ref ppu_addr_temp);
ser.Sync("Read_Value", ref read_value);
ser.Sync("Prev_soam_index", ref soam_index_prev);
ser.Sync("Spr_Zero_Go", ref sprite_zero_go);
ser.Sync("Spr_zero_in_Range", ref sprite_zero_in_range);
ser.Sync("Is_even_cycle", ref is_even_cycle);
ser.Sync("soam_index", ref soam_index);
ser.Sync("ppu_open_bus", ref ppu_open_bus);
ser.Sync("ppu_open_bus_decay_timer", ref ppu_open_bus_decay_timer, false);
ser.Sync("OAM", ref OAM, false);
ser.Sync("PALRAM", ref PALRAM, false);
ser.Sync("Reg2002_objoverflow", ref Reg2002_objoverflow);
ser.Sync("Reg2002_objhit", ref Reg2002_objhit);
ser.Sync("Reg2002_vblank_active", ref Reg2002_vblank_active);
ser.Sync("Reg2002_vblank_active_pending", ref Reg2002_vblank_active_pending);
ser.Sync("Reg2002_vblank_clear_pending", ref Reg2002_vblank_clear_pending);
ppur.SyncState(ser);
byte temp8 = reg_2000.Value; ser.Sync("reg_2000.Value", ref temp8); reg_2000.Value = temp8;
temp8 = reg_2001.Value; ser.Sync("reg_2001.Value", ref temp8); reg_2001.Value = temp8;
ser.Sync("reg_2003", ref reg_2003);
//don't sync framebuffer into binary (rewind) states
if(ser.IsText)
ser.Sync("xbuf", ref xbuf, false);
}
public void Reset()
{
regs_reset();
ppudead = 2;
idleSynch = true;
ppu_open_bus = 0;
ppu_open_bus_decay_timer = new int[8];
}
#if VS2012
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
void TriggerNMI()
{
nes.cpu.NMI = true;
}
//this gets called once after each cpu instruction executes.
//anything that needs to happen at instruction granularity can get checked here
//to save having to check it at ppu cycle granularity
public void PostCpuInstructionOne()
{
if (NMI_PendingInstructions > 0)
{
NMI_PendingInstructions--;
if (NMI_PendingInstructions <= 0)
{
TriggerNMI();
}
}
}
#if VS2012
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
void runppu(int x)
{
//run one ppu cycle at a time so we can interact with the ppu and clockPPU at high granularity
for (int i = 0; i < x; i++)
{
ppur.status.cycle++;
is_even_cycle = !is_even_cycle;
//might not actually run a cpu cycle if there are none to be run right now
nes.RunCpuOne();
if (Reg2002_vblank_active_pending)
{
//if (Reg2002_vblank_active_pending)
Reg2002_vblank_active = 1;
Reg2002_vblank_active_pending = false;
}
if (Reg2002_vblank_clear_pending)
{
Reg2002_vblank_active = 0;
Reg2002_vblank_clear_pending = false;
}
nes.Board.ClockPPU();
}
}
//hack
//public bool PAL = false;
//bool SPRITELIMIT = true;
}
}

720
PPU.regs.cs Normal file
View File

@ -0,0 +1,720 @@
//TODO - better sprite hit handling (be sure to test world runner)
//http://nesdev.parodius.com/bbs/viewtopic.php?t=626
//TODO - Reg2002_objoverflow is not working in the dummy reads test.. why are we setting it when nintendulator doesnt>
//blargg: Reading from $2007 when the VRAM address is $3fxx will fill the internal read buffer with the contents at VRAM address $3fxx, in addition to reading the palette RAM.
//static const byte powerUpPalette[] =
//{
// 0x3F,0x01,0x00,0x01, 0x00,0x02,0x02,0x0D, 0x08,0x10,0x08,0x24, 0x00,0x00,0x04,0x2C,
// 0x09,0x01,0x34,0x03, 0x00,0x04,0x00,0x14, 0x08,0x3A,0x00,0x02, 0x00,0x20,0x2C,0x08
//};
using System;
using BizHawk.Common;
namespace BizHawk.Emulation.Cores.Nintendo.NES
{
sealed partial class PPU
{
public sealed class Reg_2001
{
public Bit color_disable; //Color disable (0: normal color; 1: AND all palette entries with 110000, effectively producing a monochrome display)
public Bit show_bg_leftmost; //Show leftmost 8 pixels of background
public Bit show_obj_leftmost; //Show sprites in leftmost 8 pixels
public Bit show_bg; //Show background
public Bit show_obj; //Show sprites
public Bit intense_green; //Intensify greens (and darken other colors)
public Bit intense_blue; //Intensify blues (and darken other colors)
public Bit intense_red; //Intensify reds (and darken other colors)
public int intensity_lsl_6; //an optimization..
public bool PPUON { get { return show_bg || show_obj; } }
public byte Value
{
get
{
return (byte)(color_disable | (show_bg_leftmost << 1) | (show_obj_leftmost << 2) | (show_bg << 3) | (show_obj << 4) | (intense_green << 5) | (intense_blue << 6) | (intense_red << 7));
}
set
{
color_disable = (value & 1);
show_bg_leftmost = (value >> 1) & 1;
show_obj_leftmost = (value >> 2) & 1;
show_bg = (value >> 3) & 1;
show_obj = (value >> 4) & 1;
intense_green = (value >> 5) & 1;
intense_blue = (value >> 6) & 1;
intense_red = (value >> 7) & 1;
intensity_lsl_6 = ((value >> 5) & 7)<<6;
}
}
}
// this byte is used to simulate open bus reads and writes
// it should be modified by every read and write to a ppu register
public byte ppu_open_bus;
public int[] ppu_open_bus_decay_timer = new int[8];
public struct PPUSTATUS
{
public int sl;
public bool rendering { get { return sl >= 0 && sl < 241; } }
public int cycle;
}
//uses the internal counters concept at http://nesdev.icequake.net/PPU%20addressing.txt
//TODO - this should be turned into a state machine
public sealed class PPUREGS
{
PPU ppu;
public PPUREGS(PPU ppu)
{
this.ppu = ppu;
reset();
}
public void SyncState(Serializer ser)
{
ser.Sync("fv", ref fv);
ser.Sync("v", ref v);
ser.Sync("h", ref h);
ser.Sync("vt", ref vt);
ser.Sync("ht", ref ht);
ser.Sync("_fv", ref _fv);
ser.Sync("_v", ref _v);
ser.Sync("_h", ref _h);
ser.Sync("_vt", ref _vt);
ser.Sync("_ht", ref _ht);
ser.Sync("fh", ref fh);
ser.Sync("status.cycle", ref status.cycle);
int junk = 0;
ser.Sync("status.end_cycle", ref junk);
ser.Sync("status.sl", ref status.sl);
}
//normal clocked regs. as the game can interfere with these at any time, they need to be savestated
public int fv;//3
public int v;//1
public int h;//1
public int vt;//5
public int ht;//5
//temp unlatched regs (need savestating, can be written to at any time)
public int _fv, _vt, _v, _h, _ht;
//other regs that need savestating
public int fh;//3 (horz scroll)
//cached state data. these are always reset at the beginning of a frame and don't need saving
//but just to be safe, we're gonna save it
public PPUSTATUS status = new PPUSTATUS();
//public int ComputeIndex()
//{
// return fv | (v << 3) | (h << 4) | (vt << 5) | (ht << 10) | (fh << 15);
//}
//public void DecodeIndex(int index)
//{
// fv = index & 7;
// v = (index >> 3) & 1;
// h = (index >> 4) & 1;
// vt = (index >> 5) & 0x1F;
// ht = (index >> 10) & 0x1F;
// fh = (index >> 15) & 7;
//}
//const int tbl_size = 1 << 18;
//int[] tbl_increment_hsc = new int[tbl_size];
//int[] tbl_increment_vs = new int[tbl_size];
//public void BuildTables()
//{
// for (int i = 0; i < tbl_size; i++)
// {
// DecodeIndex(i);
// increment_hsc();
// tbl_increment_hsc[i] = ComputeIndex();
// DecodeIndex(i);
// increment_vs();
// tbl_increment_vs[i] = ComputeIndex();
// }
//}
public void reset()
{
fv = v = h = vt = ht = 0;
fh = 0;
_fv = _v = _h = _vt = _ht = 0;
status.cycle = 0;
status.sl = 241;
}
public void install_latches()
{
fv = _fv;
v = _v;
h = _h;
vt = _vt;
ht = _ht;
}
public void install_h_latches()
{
ht = _ht;
h = _h;
}
public void clear_latches()
{
_fv = _v = _h = _vt = _ht = 0;
fh = 0;
}
public void increment_hsc()
{
//The first one, the horizontal scroll counter, consists of 6 bits, and is
//made up by daisy-chaining the HT counter to the H counter. The HT counter is
//then clocked every 8 pixel dot clocks (or every 8/3 CPU clock cycles).
ht++;
h += (ht >> 5);
ht &= 31;
h &= 1;
}
public void increment_vs()
{
fv++;
int fv_overflow = (fv >> 3);
vt += fv_overflow;
vt &= 31; //fixed tecmo super bowl
if (vt == 30 && fv_overflow==1) //caution here (only do it at the exact instant of overflow) fixes p'radikus conflict
{
v++;
vt = 0;
}
fv &= 7;
v &= 1;
}
public int get_ntread()
{
return 0x2000 | (v << 0xB) | (h << 0xA) | (vt << 5) | ht;
}
public int get_2007access()
{
return ((fv & 3) << 0xC) | (v << 0xB) | (h << 0xA) | (vt << 5) | ht;
}
//The PPU has an internal 4-position, 2-bit shifter, which it uses for
//obtaining the 2-bit palette select data during an attribute table byte
//fetch. To represent how this data is shifted in the diagram, letters a..c
//are used in the diagram to represent the right-shift position amount to
//apply to the data read from the attribute data (a is always 0). This is why
//you only see bits 0 and 1 used off the read attribute data in the diagram.
public int get_atread()
{
return 0x2000 | (v << 0xB) | (h << 0xA) | 0x3C0 | ((vt & 0x1C) << 1) | ((ht & 0x1C) >> 2);
}
//address line 3 relates to the pattern table fetch occuring (the PPU always makes them in pairs).
public int get_ptread(int par)
{
int s = ppu.reg_2000.bg_pattern_hi;
return (s << 0xC) | (par << 0x4) | fv;
}
public void increment2007(bool rendering, bool by32)
{
if (rendering)
{
//don't do this:
//if (by32) increment_vs();
//else increment_hsc();
//do this instead:
increment_vs(); //yes, even if we're moving by 32
return;
}
//If the VRAM address increment bit (2000.2) is clear (inc. amt. = 1), all the
//scroll counters are daisy-chained (in the order of HT, VT, H, V, FV) so that
//the carry out of each counter controls the next counter's clock rate. The
//result is that all 5 counters function as a single 15-bit one. Any access to
//2007 clocks the HT counter here.
//
//If the VRAM address increment bit is set (inc. amt. = 32), the only
//difference is that the HT counter is no longer being clocked, and the VT
//counter is now being clocked by access to 2007.
if (by32)
{
vt++;
}
else
{
ht++;
vt += (ht >> 5) & 1;
}
h += (vt >> 5);
v += (h >> 1);
fv += (v >> 1);
ht &= 31;
vt &= 31;
h &= 1;
v &= 1;
fv &= 7;
}
};
public sealed class Reg_2000
{
PPU ppu;
public Reg_2000(PPU ppu)
{
this.ppu = ppu;
}
//these bits go straight into PPUR
//(00 = $2000; 01 = $2400; 02 = $2800; 03 = $2c00)
public Bit vram_incr32; //(0: increment by 1, going across; 1: increment by 32, going down)
public Bit obj_pattern_hi; //Sprite pattern table address for 8x8 sprites (0: $0000; 1: $1000)
public Bit bg_pattern_hi; //Background pattern table address (0: $0000; 1: $1000)
public Bit obj_size_16; //Sprite size (0: 8x8 sprites; 1: 8x16 sprites)
public Bit ppu_layer; //PPU layer select (should always be 0 in the NES; some Nintendo arcade boards presumably had two PPUs)
public Bit vblank_nmi_gen; //Vertical blank NMI generation (0: off; 1: on)
public byte Value
{
get
{
return (byte)(ppu.ppur._h | (ppu.ppur._v << 1) | (vram_incr32 << 2) | (obj_pattern_hi << 3) | (bg_pattern_hi << 4) | (obj_size_16 << 5) | (ppu_layer << 6) | (vblank_nmi_gen << 7));
}
set
{
ppu.ppur._h = value & 1;
ppu.ppur._v = (value >> 1) & 1;
vram_incr32 = (value >> 2) & 1;
obj_pattern_hi = (value >> 3) & 1;
bg_pattern_hi = (value >> 4) & 1;
obj_size_16 = (value >> 5) & 1;
ppu_layer = (value >> 6) & 1;
vblank_nmi_gen = (value >> 7) & 1;
}
}
}
Bit Reg2002_objoverflow; //Sprite overflow. The PPU can handle only eight sprites on one scanline and sets this bit if it starts drawing sprites.
Bit Reg2002_objhit; //Sprite 0 overlap. Set when a nonzero pixel of sprite 0 is drawn overlapping a nonzero background pixel. Used for raster timing.
Bit Reg2002_vblank_active; //Vertical blank start (0: has not started; 1: has started)
bool Reg2002_vblank_active_pending; //set if Reg2002_vblank_active is pending
bool Reg2002_vblank_clear_pending; //ppu's clear of vblank flag is pending
public PPUREGS ppur;
public Reg_2000 reg_2000;
public Reg_2001 reg_2001;
byte reg_2003;
void regs_reset()
{
//TODO - would like to reconstitute the entire PPU instead of all this..
reg_2000 = new Reg_2000(this);
reg_2001 = new Reg_2001();
ppur = new PPUREGS(this);
Reg2002_objoverflow = false;
Reg2002_objhit = false;
Reg2002_vblank_active = false;
PPUGenLatch = 0;
reg_2003 = 0;
vtoggle = false;
VRAMBuffer = 0;
}
//---------------------
//PPU CONTROL (write)
void write_2000(byte value)
{
if (!reg_2000.vblank_nmi_gen & ((value & 0x80) != 0) && (Reg2002_vblank_active) && !Reg2002_vblank_clear_pending)
{
//if we just unleashed the vblank interrupt then activate it now
NMI_PendingInstructions = 2;
}
reg_2000.Value = value;
}
byte read_2000() { return ppu_open_bus; }
byte peek_2000() { return ppu_open_bus; }
//PPU MASK (write)
void write_2001(byte value)
{
//printf("%04x:$%02x, %d\n",A,V,scanline);
reg_2001.Value = value;
}
byte read_2001() { return ppu_open_bus; }
byte peek_2001() { return ppu_open_bus; }
//PPU STATUS (read)
void write_2002(byte value) { }
byte read_2002()
{
//once we thought we clear latches here, but that caused midframe glitches.
//i think we should only reset the state machine for 2005/2006
//ppur.clear_latches();
byte ret = peek_2002();
vtoggle = false;
Reg2002_vblank_active = 0;
Reg2002_vblank_active_pending = false;
// update the open bus here
ppu_open_bus = ret;
ppu_open_bus_decay(2);
return ret;
}
byte peek_2002()
{
return (byte)((Reg2002_vblank_active << 7) | (Reg2002_objhit << 6) | (Reg2002_objoverflow << 5) | (ppu_open_bus & 0x1F));
}
void clear_2002()
{
Reg2002_objhit = Reg2002_objoverflow = 0;
Reg2002_vblank_clear_pending = true;
}
//OAM ADDRESS (write)
void write_2003(byte value)
{
//just record the oam buffer write target
reg_2003 = value;
}
byte read_2003() { return ppu_open_bus; }
byte peek_2003() { return ppu_open_bus; }
//OAM DATA (write)
void write_2004(byte value)
{
if ((reg_2003 & 3) == 2) value &= 0xE3; //some of the OAM bits are unwired so we mask them out here
//otherwise we just write this value and move on to the next oam byte
OAM[reg_2003] = value;
reg_2003++;
}
byte read_2004()
{
byte ret;
// behaviour depends on whether things are being rendered or not
if (reg_2001.show_bg || reg_2001.show_obj)
{
if (ppur.status.sl < 241)
{
if (ppur.status.cycle < 64)
{
ret = 0xFF; // during this time all reads return FF
}
else if (ppur.status.cycle < 256)
{
ret = read_value;
}
else if (ppur.status.cycle < 320)
{
ret = read_value;
}
else
{
ret = soam[0];
}
}
else
{
ret = OAM[reg_2003];
}
}
else
{
ret = OAM[reg_2003];
}
ppu_open_bus = ret;
ppu_open_bus_decay(1);
return ret;
}
byte peek_2004() { return OAM[reg_2003]; }
//SCROLL (write)
void write_2005(byte value)
{
if (!vtoggle)
{
ppur._ht= value >> 3;
ppur.fh = value & 7;
//nes.LogLine("scroll wrote ht = {0} and fh = {1}", ppur._ht, ppur.fh);
}
else
{
ppur._vt = value >> 3;
ppur._fv = value & 7;
//nes.LogLine("scroll wrote vt = {0} and fv = {1}", ppur._vt, ppur._fv);
}
vtoggle ^= true;
}
byte read_2005() { return ppu_open_bus; }
byte peek_2005() { return ppu_open_bus; }
//VRAM address register (write)
void write_2006(byte value)
{
if (!vtoggle)
{
ppur._vt &= 0x07;
ppur._vt |= (value & 0x3) << 3;
ppur._h = (value >> 2) & 1;
ppur._v = (value >> 3) & 1;
ppur._fv = (value >> 4) & 3;
//nes.LogLine("addr wrote fv = {0}", ppur._fv);
}
else
{
ppur._vt &= 0x18;
ppur._vt |= (value >> 5);
ppur._ht = value & 31;
ppur.install_latches();
//nes.LogLine("addr wrote vt = {0}, ht = {1}", ppur._vt, ppur._ht);
//normally the address isnt observed by the board till it gets clocked by a read or write.
//but maybe thats just because a ppu read/write shoves it on the address bus
//apparently this shoves it on the address bus, too, or else blargg's mmc3 tests dont pass
nes.Board.AddressPPU(ppur.get_2007access());
}
vtoggle ^= true;
}
byte read_2006() { return ppu_open_bus; }
byte peek_2006() { return ppu_open_bus; }
//VRAM data register (r/w)
void write_2007(byte value)
{
//does this take 4x longer? nestopia indicates so perhaps...
int addr = ppur.get_2007access() & 0x3FFF;
if ((addr & 0x3F00) == 0x3F00)
{
//handle palette. this is being done nestopia style, because i found some documentation for it (appendix 1)
addr &= 0x1F;
byte color = (byte)(value & 0x3F); //are these bits really unwired? can they be read back somehow?
//this little hack will help you debug things while the screen is black
//color = (byte)(addr & 0x3F);
PALRAM[addr] = color;
if ((addr & 3) == 0)
{
PALRAM[addr ^ 0x10] = color;
}
}
else
{
ppubus_write(addr, value);
}
ppur.increment2007(ppur.status.rendering && reg_2001.PPUON, reg_2000.vram_incr32 != 0);
//see comments in $2006
nes.Board.AddressPPU(ppur.get_2007access());
}
byte read_2007()
{
int addr = ppur.get_2007access() & 0x3FFF;
int bus_case = 0;
//ordinarily we return the buffered values
byte ret = VRAMBuffer;
//in any case, we read from the ppu bus
VRAMBuffer = ppubus_read(addr,false);
//but reads from the palette are implemented in the PPU and return immediately
if ((addr & 0x3F00) == 0x3F00)
{
//TODO apply greyscale shit?
ret = (byte)(PALRAM[addr & 0x1F] + ((byte)(ppu_open_bus & 0xC0)));
bus_case = 1;
}
ppur.increment2007(ppur.status.rendering && reg_2001.PPUON, reg_2000.vram_incr32 != 0);
//see comments in $2006
nes.Board.AddressPPU(ppur.get_2007access());
// update open bus here
ppu_open_bus = ret;
if (bus_case==0)
{
ppu_open_bus_decay(1);
} else
{
ppu_open_bus_decay(3);
}
return ret;
}
byte peek_2007()
{
int addr = ppur.get_2007access() & 0x3FFF;
//ordinarily we return the buffered values
byte ret = VRAMBuffer;
//in any case, we read from the ppu bus
// can't do this in peek; updates the value that will be used later
// VRAMBuffer = ppubus_peek(addr);
//but reads from the palette are implemented in the PPU and return immediately
if ((addr & 0x3F00) == 0x3F00)
{
//TODO apply greyscale shit?
ret = PALRAM[addr & 0x1F];
}
return ret;
}
//--------
public byte ReadReg(int addr)
{
switch (addr)
{
case 0: return read_2000(); case 1: return read_2001(); case 2: return read_2002(); case 3: return read_2003();
case 4: return read_2004(); case 5: return read_2005(); case 6: return read_2006(); case 7: return read_2007();
default: throw new InvalidOperationException();
}
}
public byte PeekReg(int addr)
{
switch (addr)
{
case 0: return peek_2000(); case 1: return peek_2001(); case 2: return peek_2002(); case 3: return peek_2003();
case 4: return peek_2004(); case 5: return peek_2005(); case 6: return peek_2006(); case 7: return peek_2007();
default: throw new InvalidOperationException();
}
}
public void WriteReg(int addr, byte value)
{
PPUGenLatch = value;
ppu_open_bus = value;
switch (addr)
{
case 0: write_2000(value); break; case 1: write_2001(value); break; case 2: write_2002(value); break; case 3: write_2003(value); break;
case 4: write_2004(value); break; case 5: write_2005(value); break; case 6: write_2006(value); break; case 7: write_2007(value); break;
default: throw new InvalidOperationException();
}
}
public void ppu_open_bus_decay(byte action)
{
// if there is no action, decrement the timer
if (action==0)
{
for (int i = 0; i < 8; i++)
{
if (ppu_open_bus_decay_timer[i] == 0)
{
ppu_open_bus = (byte)(ppu_open_bus & (0xff - (1 << i)));
ppu_open_bus_decay_timer[i] = 1786840; // about 1 second worth of cycles
}
else
{
ppu_open_bus_decay_timer[i]--;
}
}
}
// reset the timer for all bits (reg 2004 / 2007 (non-palette)
if (action==1)
{
for (int i=0; i<8; i++)
{
ppu_open_bus_decay_timer[i] = 1786840;
}
}
// reset the timer for high 3 bits (reg 2002)
if (action == 2)
{
ppu_open_bus_decay_timer[7] = 1786840;
ppu_open_bus_decay_timer[6] = 1786840;
ppu_open_bus_decay_timer[5] = 1786840;
}
// reset the timer for all low 6 bits (reg 2007 (palette))
if (action == 3)
{
for (int i = 0; i < 6; i++)
{
ppu_open_bus_decay_timer[i] = 1786840;
}
}
// other values of action are reserved for possibly needed expansions, but this passes
// ppu_open_bus for now.
}
}
}
//ARead[x]=A200x;
//BWrite[x]=B2000;
//ARead[x+1]=A200x;
//BWrite[x+1]=B2001;
//ARead[x+2]=A2002;
//BWrite[x+2]=B2002;
//ARead[x+3]=A200x;
//BWrite[x+3]=B2003;
//ARead[x+4]=A2004; //A2004;
//BWrite[x+4]=B2004;
//ARead[x+5]=A200x;
//BWrite[x+5]=B2005;
//ARead[x+6]=A200x;
//BWrite[x+6]=B2006;
//ARead[x+7]=A2007;
//BWrite[x+7]=B2007;
//Address Size Description
//$0000 $1000 Pattern Table 0
//$1000 $1000 Pattern Table 1
//$2000 $3C0 Name Table 0
//$23C0 $40 Attribute Table 0
//$2400 $3C0 Name Table 1
//$27C0 $40 Attribute Table 1
//$2800 $3C0 Name Table 2
//$2BC0 $40 Attribute Table 2
//$2C00 $3C0 Name Table 3
//$2FC0 $40 Attribute Table 3
//$3000 $F00 Mirror of 2000h-2EFFh
//$3F00 $10 BG Palette
//$3F10 $10 Sprite Palette
//$3F20 $E0 Mirror of 3F00h-3F1Fh
//appendix 1
//http://nocash.emubase.de/everynes.htm#ppupalettes
//Palette Memory (25 entries used)
// 3F00h Background Color (Color 0)
// 3F01h-3F03h Background Palette 0 (Color 1-3)
// 3F05h-3F07h Background Palette 1 (Color 1-3)
// 3F09h-3F0Bh Background Palette 2 (Color 1-3)
// 3F0Dh-3F0Fh Background Palette 3 (Color 1-3)
// 3F11h-3F13h Sprite Palette 0 (Color 1-3)
// 3F15h-3F17h Sprite Palette 1 (Color 1-3)
// 3F19h-3F1Bh Sprite Palette 2 (Color 1-3)
// 3F1Dh-3F1Fh Sprite Palette 3 (Color 1-3)
//Palette Gaps and Mirrors
// 3F04h,3F08h,3F0Ch - Three general purpose 6bit data registers.
// 3F10h,3F14h,3F18h,3F1Ch - Mirrors of 3F00h,3F04h,3F08h,3F0Ch.
// 3F20h-3FFFh - Mirrors of 3F00h-3F1Fh.