From ff4feb5ee83fd7d5f68103733ce2e1d1bfc86075 Mon Sep 17 00:00:00 2001 From: alyosha-tas Date: Tue, 21 Jun 2016 17:11:06 -0400 Subject: [PATCH] ppu open bus emulation cpu_dummy_writes_ppumem - passes ppu_open_bus - passes --- NES.Core.cs | 672 ++++++++++++++++++++++++++++++++++++++++++ NES.cs | 827 ++++++++++++++++++++++++++++++++++++++++++++++++++++ PPU.cs | 251 ++++++++++++++++ PPU.regs.cs | 720 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 2470 insertions(+) create mode 100644 NES.Core.cs create mode 100644 NES.cs create mode 100644 PPU.cs create mode 100644 PPU.regs.cs diff --git a/NES.Core.cs b/NES.Core.cs new file mode 100644 index 0000000000..fbf81ed7cf --- /dev/null +++ b/NES.Core.cs @@ -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 + /// clock speed of the main cpu in hz + 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; } + + /// + /// for debugging only! + /// + /// + 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; + } + + /// + /// Sets the provided palette as current. + /// Applies the current deemph settings if needed to expand a 64-entry palette to 512 + /// + 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); + } + } + } + + /// + /// looks up an internal NES pixel value to an rgb int (applying the core's current palette and assuming no deemph) + /// + 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); + } + + } +} \ No newline at end of file diff --git a/NES.cs b/NES.cs new file mode 100644 index 0000000000..7d84c76b43 --- /dev/null +++ b/NES.cs @@ -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 + { + 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(cpu); + + Tracer = new TraceBuffer { Header = cpu.TraceHeader }; + ser.Register(Tracer); + ser.Register(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(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().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 hash_sha1_several = new List(); + string hash_sha1 = null, hash_md5 = null; + Unif unif = null; + + Dictionary InitialMapperRegisterValues = new Dictionary(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 \ No newline at end of file diff --git a/PPU.cs b/PPU.cs new file mode 100644 index 0000000000..ac81b90224 --- /dev/null +++ b/PPU.cs @@ -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; + + } +} diff --git a/PPU.regs.cs b/PPU.regs.cs new file mode 100644 index 0000000000..401963421c --- /dev/null +++ b/PPU.regs.cs @@ -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. \ No newline at end of file