985 lines
24 KiB
C#
985 lines
24 KiB
C#
using System;
|
|
using System.Linq;
|
|
|
|
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, ICycleTiming
|
|
{
|
|
//hardware/state
|
|
public MOS6502X cpu;
|
|
public PPU ppu;
|
|
public APU apu;
|
|
public byte[] ram;
|
|
NESWatch[] sysbus_watch = new NESWatch[65536];
|
|
public byte[] CIRAM; //AKA nametables
|
|
string game_name = ""; //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; }
|
|
|
|
//user configuration
|
|
int[] palette_compiled = new int[64 * 8];
|
|
|
|
//variable set when VS system games are running
|
|
internal bool _isVS = false;
|
|
//some VS games have a ppu that switches 2000 and 2001, so keep trcak of that
|
|
public byte _isVS2c05 = 0;
|
|
//since prg reg for VS System is set in the controller regs, it is convenient to have it here
|
|
//instead of in the board
|
|
public byte VS_chr_reg;
|
|
public byte VS_prg_reg;
|
|
//various VS controls
|
|
public byte[] VS_dips = new byte[8];
|
|
public byte VS_service = 0;
|
|
public byte VS_coin_inserted=0;
|
|
public byte VS_ROM_control;
|
|
|
|
// cheat addr index tracker
|
|
// disables all cheats each frame
|
|
public int[] cheat_indexes = new int[500];
|
|
public int num_cheats;
|
|
|
|
// 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 SetVol1(int v) { apu.m_vol = 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, 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 bool CanProvideAsync
|
|
{
|
|
get { return false; }
|
|
}
|
|
|
|
public SyncSoundMode SyncMode
|
|
{
|
|
get { return SyncSoundMode.Sync; }
|
|
}
|
|
|
|
public void SetSyncMode(SyncSoundMode mode)
|
|
{
|
|
if (mode != SyncSoundMode.Sync)
|
|
{
|
|
throw new NotSupportedException("Only sync mode is supported");
|
|
}
|
|
}
|
|
|
|
public void GetSamplesAsync(short[] samples)
|
|
{
|
|
throw new NotSupportedException("Async not supported");
|
|
}
|
|
|
|
public void GetSamplesSync(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 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);
|
|
}
|
|
|
|
if (_isVS)
|
|
{
|
|
ControllerDefinition.BoolButtons.Add("Insert Coin P1");
|
|
ControllerDefinition.BoolButtons.Add("Insert Coin P2");
|
|
ControllerDefinition.BoolButtons.Add("Service Switch");
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
VsyncNum = 50;
|
|
VsyncDen = 1;
|
|
cpuclockrate = 1662607;
|
|
cpu_sequence = cpu_sequence_PAL;
|
|
_display_type = DisplayType.PAL;
|
|
ClockRate = 5320342.5;
|
|
break;
|
|
case Common.DisplayType.NTSC:
|
|
apu = new APU(this, apu, false);
|
|
ppu.region = PPU.Region.NTSC;
|
|
VsyncNum = 39375000;
|
|
VsyncDen = 655171;
|
|
cpuclockrate = 1789773;
|
|
cpu_sequence = cpu_sequence_NTSC;
|
|
ClockRate = 5369318.1818181818181818181818182;
|
|
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;
|
|
VsyncNum = 50;
|
|
VsyncDen = 1;
|
|
cpuclockrate = 1773448;
|
|
cpu_sequence = cpu_sequence_NTSC;
|
|
_display_type = DisplayType.Dendy;
|
|
ClockRate = 5320342.5;
|
|
break;
|
|
default:
|
|
throw new Exception("Unknown displaytype!");
|
|
}
|
|
if (magicSoundProvider == null)
|
|
magicSoundProvider = new MagicSoundProvider(this, (uint)cpuclockrate);
|
|
|
|
BoardSystemHardReset();
|
|
|
|
// apu has some specific power up bahaviour that we will emulate here
|
|
apu.NESHardReset();
|
|
|
|
if (SyncSettings.InitialWRamStatePattern != null && SyncSettings.InitialWRamStatePattern.Any())
|
|
{
|
|
for (int i = 0; i < 0x800; i++)
|
|
{
|
|
ram[i] = SyncSettings.InitialWRamStatePattern[i % SyncSettings.InitialWRamStatePattern.Count];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 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();
|
|
|
|
// some boards cannot have specific values in RAM upon initialization
|
|
// Let's hard code those cases here
|
|
// these will be defined through the gameDB exclusively for now.
|
|
|
|
if (cart.DB_GameInfo!=null)
|
|
{
|
|
|
|
if (cart.DB_GameInfo.Hash == "60FC5FA5B5ACCAF3AEFEBA73FC8BFFD3C4DAE558" // Camerica Golden 5
|
|
|| cart.DB_GameInfo.Hash == "BAD382331C30B22A908DA4BFF2759C25113CC26A" // Camerica Golden 5
|
|
|| cart.DB_GameInfo.Hash == "40409FEC8249EFDB772E6FFB2DCD41860C6CCA23" // Camerica Pegasus 4-in-1
|
|
)
|
|
{
|
|
ram[0x701] = 0xFF;
|
|
}
|
|
|
|
if (cart.DB_GameInfo.Hash == "68ABE1E49C9E9CCEA978A48232432C252E5912C0") // Dancing Blocks
|
|
{
|
|
ram[0xEC] = 0;
|
|
ram[0xED] = 0;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public long CycleCount => ppu.TotalCycles;
|
|
public double ClockRate { get; private set; }
|
|
|
|
private int VsyncNum { get; set; }
|
|
private int VsyncDen { get; set; }
|
|
|
|
private IController _controller;
|
|
|
|
bool resetSignal;
|
|
bool hardResetSignal;
|
|
public void FrameAdvance(IController controller, bool render, bool rendersound)
|
|
{
|
|
_controller = controller;
|
|
|
|
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.IsPressed("Reset");
|
|
hardResetSignal = controller.IsPressed("Power");
|
|
|
|
if (Board is FDS)
|
|
{
|
|
var b = Board as FDS;
|
|
if (controller.IsPressed("FDS Eject"))
|
|
b.Eject();
|
|
for (int i = 0; i < b.NumSides; i++)
|
|
if (controller.IsPressed("FDS Insert " + i))
|
|
b.InsertSide(i);
|
|
}
|
|
|
|
if (_isVS)
|
|
{
|
|
if (controller.IsPressed("Service Switch"))
|
|
VS_service = 1;
|
|
else
|
|
VS_service = 0;
|
|
|
|
if (controller.IsPressed("Insert Coin P1"))
|
|
VS_coin_inserted |= 1;
|
|
else
|
|
VS_coin_inserted &= 2;
|
|
|
|
if (controller.IsPressed("Insert Coin P2"))
|
|
VS_coin_inserted |= 2;
|
|
else
|
|
VS_coin_inserted &= 1;
|
|
}
|
|
|
|
if (ppu.ppudead > 0)
|
|
{
|
|
while (ppu.ppudead > 0)
|
|
{
|
|
ppu.NewDeadPPU();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ppu.ppu_init_frame();
|
|
|
|
ppu.do_vbl = true;
|
|
ppu.do_active_sl = true;
|
|
ppu.do_pre_vbl = true;
|
|
|
|
// do the vbl ticks seperate, that will save us a few checks that don't happen in active region
|
|
while (ppu.do_vbl)
|
|
{
|
|
ppu.TickPPU_VBL();
|
|
}
|
|
|
|
// now do the rest of the frame
|
|
while (ppu.do_active_sl)
|
|
{
|
|
ppu.TickPPU_active();
|
|
}
|
|
|
|
// now do the pre-NMI lines
|
|
while (ppu.do_pre_vbl)
|
|
{
|
|
ppu.TickPPU_preVBL();
|
|
}
|
|
}
|
|
|
|
if (lagged)
|
|
{
|
|
_lagcount++;
|
|
islag = true;
|
|
}
|
|
else
|
|
islag = false;
|
|
|
|
videoProvider.FillFrameBuffer();
|
|
|
|
//turn off all cheats
|
|
for (int d=0;d<num_cheats;d++)
|
|
{
|
|
RemoveGameGenie(cheat_indexes[d]);
|
|
}
|
|
num_cheats = 0;
|
|
}
|
|
|
|
//PAL:
|
|
//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
|
|
public 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_deadcounter;
|
|
|
|
public int oam_dma_index;
|
|
public bool oam_dma_exec = false;
|
|
public ushort oam_dma_addr;
|
|
public byte oam_dma_byte;
|
|
public bool dmc_dma_exec = false;
|
|
public bool dmc_realign;
|
|
public bool IRQ_delay;
|
|
public bool special_case_delay; // very ugly but the only option
|
|
public bool do_the_reread;
|
|
public byte DB; //old data bus values from previous reads
|
|
|
|
internal void RunCpuOne()
|
|
{
|
|
///////////////////////////
|
|
// OAM DMA start
|
|
///////////////////////////
|
|
|
|
if (sprdma_countdown > 0)
|
|
{
|
|
sprdma_countdown--;
|
|
if (sprdma_countdown == 0)
|
|
{
|
|
if (cpu.TotalExecutedCycles % 2 == 0)
|
|
{
|
|
cpu_deadcounter = 2;
|
|
}
|
|
else
|
|
{
|
|
cpu_deadcounter = 1;
|
|
}
|
|
oam_dma_exec = true;
|
|
cpu.RDY = false;
|
|
oam_dma_index = 0;
|
|
special_case_delay = true;
|
|
}
|
|
}
|
|
|
|
if (oam_dma_exec && apu.dmc_dma_countdown != 1 && !dmc_realign)
|
|
{
|
|
if (cpu_deadcounter == 0)
|
|
{
|
|
|
|
if (oam_dma_index % 2 == 0)
|
|
{
|
|
oam_dma_byte = ReadMemory(oam_dma_addr);
|
|
oam_dma_addr++;
|
|
}
|
|
else
|
|
{
|
|
WriteMemory(0x2004, oam_dma_byte);
|
|
}
|
|
oam_dma_index++;
|
|
if (oam_dma_index == 512) oam_dma_exec = false;
|
|
|
|
}
|
|
else
|
|
{
|
|
cpu_deadcounter--;
|
|
}
|
|
}
|
|
else if (apu.dmc_dma_countdown == 1)
|
|
{
|
|
dmc_realign = true;
|
|
}
|
|
else if (dmc_realign)
|
|
{
|
|
dmc_realign = false;
|
|
}
|
|
/////////////////////////////
|
|
// OAM DMA end
|
|
/////////////////////////////
|
|
|
|
|
|
/////////////////////////////
|
|
// dmc dma start
|
|
/////////////////////////////
|
|
|
|
if (apu.dmc_dma_countdown > 0)
|
|
{
|
|
cpu.RDY = false;
|
|
dmc_dma_exec = true;
|
|
apu.dmc_dma_countdown--;
|
|
if (apu.dmc_dma_countdown == 0)
|
|
{
|
|
apu.RunDMCFetch();
|
|
dmc_dma_exec = false;
|
|
apu.dmc_dma_countdown = -1;
|
|
do_the_reread = true;
|
|
}
|
|
}
|
|
|
|
/////////////////////////////
|
|
// dmc dma end
|
|
/////////////////////////////
|
|
apu.RunOne(true);
|
|
|
|
if (cpu.RDY && !IRQ_delay)
|
|
{
|
|
cpu.IRQ = _irq_apu || Board.IRQSignal;
|
|
}
|
|
else if (special_case_delay || apu.dmc_dma_countdown == 3)
|
|
{
|
|
cpu.IRQ = _irq_apu || Board.IRQSignal;
|
|
special_case_delay = false;
|
|
}
|
|
|
|
cpu.ExecuteOne();
|
|
apu.RunOne(false);
|
|
|
|
if (ppu.double_2007_read > 0)
|
|
ppu.double_2007_read--;
|
|
|
|
if (do_the_reread && cpu.RDY)
|
|
do_the_reread = false;
|
|
|
|
if (IRQ_delay)
|
|
IRQ_delay = false;
|
|
|
|
if (!dmc_dma_exec && !oam_dma_exec && !cpu.RDY)
|
|
{
|
|
cpu.RDY = true;
|
|
IRQ_delay = true;
|
|
}
|
|
|
|
Board.ClockCPU();
|
|
}
|
|
|
|
public byte ReadReg(int addr)
|
|
{
|
|
byte ret_spec;
|
|
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 DB;
|
|
//return apu.ReadReg(addr);
|
|
case 0x4014: /*OAM DMA*/ break;
|
|
case 0x4015: return (byte)((byte)(apu.ReadReg(addr) & 0xDF) + (byte)(DB & 0x20));
|
|
case 0x4016:
|
|
if (_isVS)
|
|
{
|
|
byte ret = 0;
|
|
ret = read_joyport(0x4016);
|
|
ret &= 1;
|
|
ret = (byte)(ret | (VS_service << 2) | (VS_dips[0] << 3) | (VS_dips[1] << 4) | (VS_coin_inserted << 5) | (VS_ROM_control<<7));
|
|
|
|
return ret;
|
|
}
|
|
else
|
|
{
|
|
// special hardware glitch case
|
|
ret_spec = read_joyport(addr);
|
|
if (do_the_reread && ppu.region==PPU.Region.NTSC)
|
|
{
|
|
ret_spec = read_joyport(addr);
|
|
do_the_reread = false;
|
|
}
|
|
return ret_spec;
|
|
|
|
}
|
|
case 0x4017:
|
|
if (_isVS)
|
|
{
|
|
byte ret = 0;
|
|
ret = read_joyport(0x4017);
|
|
ret &= 1;
|
|
|
|
ret = (byte)(ret | (VS_dips[2] << 2) | (VS_dips[3] << 3) | (VS_dips[4] << 4) | (VS_dips[5] << 5) | (VS_dips[6] << 6) | (VS_dips[7] << 7));
|
|
|
|
return ret;
|
|
|
|
}
|
|
else
|
|
{
|
|
// special hardware glitch case
|
|
ret_spec = read_joyport(addr);
|
|
if (do_the_reread && ppu.region == PPU.Region.NTSC)
|
|
{
|
|
ret_spec = read_joyport(addr);
|
|
do_the_reread = false;
|
|
}
|
|
return ret_spec;
|
|
}
|
|
default:
|
|
//Console.WriteLine("read register: {0:x4}", addr);
|
|
break;
|
|
|
|
}
|
|
return DB;
|
|
}
|
|
|
|
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:
|
|
//schedule a sprite dma event for beginning 1 cycle in the future.
|
|
//this receives 2 because thats just the way it works out.
|
|
oam_dma_addr = (ushort)(val << 8);
|
|
sprdma_countdown = 1;
|
|
break;
|
|
case 0x4015: apu.WriteReg(addr, val); break;
|
|
case 0x4016:
|
|
if (_isVS)
|
|
{
|
|
write_joyport(val);
|
|
VS_chr_reg = (byte)((val & 0x4)>>2);
|
|
|
|
//TODO: does other stuff for dual system
|
|
|
|
//this is actually different then assignment
|
|
VS_prg_reg = (byte)((val & 0x4)>>2);
|
|
|
|
}
|
|
else
|
|
{
|
|
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 = 0;
|
|
if (_isVS)
|
|
{
|
|
// for whatever reason, in VS left and right controller have swapped regs
|
|
ret = addr == 0x4017 ? ControllerDeck.ReadA(_controller) : ControllerDeck.ReadB(_controller);
|
|
}
|
|
else
|
|
{
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the provided palette as current.
|
|
/// Applies the current deemph settings if needed to expand a 64-entry palette to 512
|
|
/// </summary>
|
|
public 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, 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;
|
|
}
|
|
|
|
public void ExecFetch(ushort addr)
|
|
{
|
|
MemoryCallbacks.CallExecutes(addr, "System Bus");
|
|
}
|
|
|
|
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, "System Bus");
|
|
|
|
DB = ret;
|
|
return ret;
|
|
}
|
|
|
|
public void ApplyGameGenie(int addr, byte value, byte? compare)
|
|
{
|
|
if (addr < sysbus_watch.Length)
|
|
{
|
|
cheat_indexes[num_cheats] = addr;
|
|
num_cheats++;
|
|
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, "System Bus");
|
|
}
|
|
|
|
// the palette for each VS game needs to be chosen explicitly since there are 6 different ones.
|
|
public void PickVSPalette(CartInfo cart)
|
|
{
|
|
switch (cart.palette)
|
|
{
|
|
case "2C05": SetPalette(Palettes.palette_2c03_2c05); ppu.CurrentLuma = PPU.PaletteLuma2C03; break;
|
|
case "2C04-1": SetPalette(Palettes.palette_2c04_001); ppu.CurrentLuma = PPU.PaletteLuma2C04_1; break;
|
|
case "2C04-2": SetPalette(Palettes.palette_2c04_002); ppu.CurrentLuma = PPU.PaletteLuma2C04_2; break;
|
|
case "2C04-3": SetPalette(Palettes.palette_2c04_003); ppu.CurrentLuma = PPU.PaletteLuma2C04_3; break;
|
|
case "2C04-4": SetPalette(Palettes.palette_2c04_004); ppu.CurrentLuma = PPU.PaletteLuma2C04_4; break;
|
|
}
|
|
|
|
//since this will run for every VS game, let's get security setting too
|
|
//values below 16 are for the 2c05 PPU
|
|
//values 16,32,48 are for Namco games and dealt with in mapper 206
|
|
_isVS2c05 = (byte)(cart.vs_security & 15);
|
|
}
|
|
|
|
}
|
|
}
|