ppu open bus emulation
cpu_dummy_writes_ppumem - passes ppu_open_bus - passes
This commit is contained in:
parent
ec27890aba
commit
ff4feb5ee8
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
|
@ -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.
|
Loading…
Reference in New Issue