349 lines
9.3 KiB
C#
349 lines
9.3 KiB
C#
using System;
|
|
|
|
using BizHawk.Common.StringExtensions;
|
|
using BizHawk.Emulation.Common;
|
|
using BizHawk.Emulation.Common.Components;
|
|
using BizHawk.Emulation.Cores.Components;
|
|
using BizHawk.Emulation.Cores.Components.Z80;
|
|
|
|
/*****************************************************
|
|
TODO:
|
|
+ HCounter
|
|
+ Try to clean up the organization of the source code.
|
|
+ Lightgun/Paddle/etc if I get really bored
|
|
+ Mode 1 not implemented in VDP TMS modes. (I dont have a test case in SG1000 or Coleco)
|
|
|
|
**********************************************************/
|
|
|
|
namespace BizHawk.Emulation.Cores.Sega.MasterSystem
|
|
{
|
|
[CoreAttributes(
|
|
"SMSHawk",
|
|
"Vecna",
|
|
isPorted: false,
|
|
isReleased: true)]
|
|
[ServiceNotApplicable(typeof(IDriveLight))]
|
|
public sealed partial class SMS : IEmulator, ISaveRam, IStatable, IInputPollable, IRegionable,
|
|
IDebuggable, ISettable<SMS.SMSSettings, SMS.SMSSyncSettings>, ICodeDataLogger
|
|
{
|
|
[CoreConstructor("SMS", "SG", "GG")]
|
|
public SMS(CoreComm comm, GameInfo game, byte[] rom, object settings, object syncSettings)
|
|
{
|
|
ServiceProvider = new BasicServiceProvider(this);
|
|
Settings = (SMSSettings)settings ?? new SMSSettings();
|
|
SyncSettings = (SMSSyncSettings)syncSettings ?? new SMSSyncSettings();
|
|
CoreComm = comm;
|
|
MemoryCallbacks = new MemoryCallbackSystem();
|
|
|
|
IsGameGear = game.System == "GG";
|
|
IsSG1000 = game.System == "SG";
|
|
RomData = rom;
|
|
|
|
if (RomData.Length % BankSize != 0)
|
|
{
|
|
Array.Resize(ref RomData, ((RomData.Length / BankSize) + 1) * BankSize);
|
|
}
|
|
|
|
RomBanks = (byte)(RomData.Length / BankSize);
|
|
|
|
Region = DetermineDisplayType(SyncSettings.DisplayType, game.Region);
|
|
if (game["PAL"] && Region != DisplayType.PAL)
|
|
{
|
|
Region = DisplayType.PAL;
|
|
CoreComm.Notify("Display was forced to PAL mode for game compatibility.");
|
|
}
|
|
|
|
if (IsGameGear)
|
|
{
|
|
Region = DisplayType.NTSC; // all game gears run at 60hz/NTSC mode
|
|
}
|
|
|
|
RegionStr = SyncSettings.ConsoleRegion;
|
|
if (RegionStr == "Auto")
|
|
{
|
|
RegionStr = DetermineRegion(game.Region);
|
|
}
|
|
|
|
if (game["Japan"] && RegionStr != "Japan")
|
|
{
|
|
RegionStr = "Japan";
|
|
CoreComm.Notify("Region was forced to Japan for game compatibility.");
|
|
}
|
|
|
|
if ((game.NotInDatabase || game["FM"]) && SyncSettings.EnableFM && !IsGameGear)
|
|
{
|
|
HasYM2413 = true;
|
|
}
|
|
|
|
Cpu = new Z80A
|
|
{
|
|
RegisterSP = 0xDFF0,
|
|
ReadHardware = ReadPort,
|
|
WriteHardware = WritePort,
|
|
MemoryCallbacks = MemoryCallbacks
|
|
};
|
|
|
|
Vdp = new VDP(this, Cpu, IsGameGear ? VdpMode.GameGear : VdpMode.SMS, Region);
|
|
(ServiceProvider as BasicServiceProvider).Register<IVideoProvider>(Vdp);
|
|
PSG = new SN76489();
|
|
YM2413 = new YM2413();
|
|
SoundMixer = new SoundMixer(YM2413, PSG);
|
|
if (HasYM2413 && game["WhenFMDisablePSG"])
|
|
{
|
|
SoundMixer.DisableSource(PSG);
|
|
}
|
|
|
|
ActiveSoundProvider = HasYM2413 ? (IAsyncSoundProvider)SoundMixer : PSG;
|
|
_fakeSyncSound = new FakeSyncSound(ActiveSoundProvider, 735);
|
|
(ServiceProvider as BasicServiceProvider).Register<ISoundProvider>(_fakeSyncSound);
|
|
|
|
SystemRam = new byte[0x2000];
|
|
|
|
if (game["CMMapper"])
|
|
InitCodeMastersMapper();
|
|
else if (game["CMMapperWithRam"])
|
|
InitCodeMastersMapperRam();
|
|
else if (game["ExtRam"])
|
|
InitExt2kMapper(int.Parse(game.OptionValue("ExtRam")));
|
|
else if (game["KoreaMapper"])
|
|
InitKoreaMapper();
|
|
else if (game["MSXMapper"])
|
|
InitMSXMapper();
|
|
else if (game["NemesisMapper"])
|
|
InitNemesisMapper();
|
|
else if (game["TerebiOekaki"])
|
|
InitTerebiOekaki();
|
|
else
|
|
InitSegaMapper();
|
|
|
|
if (Settings.ForceStereoSeparation && !IsGameGear)
|
|
{
|
|
if (game["StereoByte"])
|
|
{
|
|
ForceStereoByte = byte.Parse(game.OptionValue("StereoByte"));
|
|
}
|
|
|
|
PSG.StereoPanning = ForceStereoByte;
|
|
}
|
|
|
|
if (SyncSettings.AllowOverlock && game["OverclockSafe"])
|
|
Vdp.IPeriod = 512;
|
|
|
|
if (Settings.SpriteLimit)
|
|
Vdp.SpriteLimit = true;
|
|
|
|
if (game["3D"])
|
|
IsGame3D = true;
|
|
|
|
if (game["BIOS"])
|
|
{
|
|
Port3E = 0xF7; // Disable cartridge, enable BIOS rom
|
|
InitBiosMapper();
|
|
}
|
|
else if (game.System == "SMS")
|
|
{
|
|
BiosRom = comm.CoreFileProvider.GetFirmware("SMS", RegionStr, false);
|
|
|
|
if (BiosRom == null)
|
|
{
|
|
throw new MissingFirmwareException("No BIOS found");
|
|
}
|
|
else if (!game["RequireBios"] && !SyncSettings.UseBIOS)
|
|
{
|
|
// we are skipping the BIOS
|
|
// but only if it won't break the game
|
|
}
|
|
else
|
|
{
|
|
Port3E = 0xF7;
|
|
}
|
|
}
|
|
|
|
if (game["SRAM"])
|
|
SaveRAM = new byte[int.Parse(game.OptionValue("SRAM"))];
|
|
else if (game.NotInDatabase)
|
|
SaveRAM = new byte[0x8000];
|
|
|
|
SetupMemoryDomains();
|
|
|
|
//this manages the linkage between the cpu and mapper callbacks so it needs running before bootup is complete
|
|
((ICodeDataLogger)this).SetCDL(null);
|
|
|
|
InputCallbacks = new InputCallbackSystem();
|
|
|
|
Tracer = new TraceBuffer { Header = Cpu.TraceHeader };
|
|
|
|
var serviceProvider = ServiceProvider as BasicServiceProvider;
|
|
serviceProvider.Register<ITraceable>(Tracer);
|
|
serviceProvider.Register<IDisassemblable>(new Disassembler());
|
|
Vdp.ProcessOverscan();
|
|
}
|
|
|
|
// Constants
|
|
private const int BankSize = 16384;
|
|
|
|
// ROM
|
|
private byte[] RomData;
|
|
private byte RomBank0, RomBank1, RomBank2, RomBank3;
|
|
private byte RomBanks;
|
|
private byte[] BiosRom;
|
|
|
|
// Machine resources
|
|
private Z80A Cpu;
|
|
private byte[] SystemRam;
|
|
public VDP Vdp;
|
|
private SN76489 PSG;
|
|
private YM2413 YM2413;
|
|
public bool IsGameGear { get; set; }
|
|
public bool IsSG1000 { get; set; }
|
|
|
|
private bool HasYM2413 = false;
|
|
private IController _controller;
|
|
|
|
private int _frame = 0;
|
|
|
|
private byte Port01 = 0xFF;
|
|
private byte Port02 = 0xFF;
|
|
private byte Port3E = 0xAF;
|
|
private byte Port3F = 0xFF;
|
|
|
|
private byte ForceStereoByte = 0xAD;
|
|
private bool IsGame3D = false;
|
|
|
|
public DisplayType Region { get; set; }
|
|
|
|
|
|
private ITraceable Tracer { get; set; }
|
|
|
|
string DetermineRegion(string gameRegion)
|
|
{
|
|
if (gameRegion == null)
|
|
return "Export";
|
|
if (gameRegion.IndexOf("USA") >= 0)
|
|
return "Export";
|
|
if (gameRegion.IndexOf("Europe") >= 0)
|
|
return "Export";
|
|
if (gameRegion.IndexOf("World") >= 0)
|
|
return "Export";
|
|
if (gameRegion.IndexOf("Brazil") >= 0)
|
|
return "Export";
|
|
if (gameRegion.IndexOf("Australia") >= 0)
|
|
return "Export";
|
|
return "Japan";
|
|
}
|
|
|
|
private DisplayType DetermineDisplayType(string display, string region)
|
|
{
|
|
if (display == "NTSC") return DisplayType.NTSC;
|
|
if (display == "PAL") return DisplayType.PAL;
|
|
if (region != null && region == "Europe") return DisplayType.PAL;
|
|
return DisplayType.NTSC;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The ReadMemory callback for the mapper
|
|
/// </summary>
|
|
private Func<ushort, byte> ReadMemory;
|
|
|
|
/// <summary>
|
|
/// The WriteMemory callback for the wrapper
|
|
/// </summary>
|
|
private Action<ushort, byte> WriteMemory;
|
|
|
|
/// <summary>
|
|
/// A dummy FetchMemory that simply reads the memory
|
|
/// </summary>
|
|
private byte FetchMemory_StubThunk(ushort address, bool first)
|
|
{
|
|
return ReadMemory(address);
|
|
}
|
|
|
|
private byte ReadPort(ushort port)
|
|
{
|
|
port &= 0xFF;
|
|
if (port < 0x40) // General IO ports
|
|
{
|
|
switch (port)
|
|
{
|
|
case 0x00: return ReadPort0();
|
|
case 0x01: return Port01;
|
|
case 0x02: return Port02;
|
|
case 0x03: return 0x00;
|
|
case 0x04: return 0xFF;
|
|
case 0x05: return 0x00;
|
|
case 0x06: return 0xFF;
|
|
case 0x3E: return Port3E;
|
|
default: return 0xFF;
|
|
}
|
|
}
|
|
if (port < 0x80) // VDP Vcounter/HCounter
|
|
{
|
|
if ((port & 1) == 0)
|
|
return Vdp.ReadVLineCounter();
|
|
else
|
|
return 0x50; // TODO Vdp.ReadHLineCounter();
|
|
}
|
|
if (port < 0xC0) // VDP data/control ports
|
|
{
|
|
if ((port & 1) == 0)
|
|
return Vdp.ReadData();
|
|
else
|
|
return Vdp.ReadVdpStatus();
|
|
}
|
|
switch (port)
|
|
{
|
|
case 0xC0:
|
|
case 0xDC: return ReadControls1();
|
|
case 0xC1:
|
|
case 0xDD: return ReadControls2();
|
|
case 0xF2: return HasYM2413 ? YM2413.DetectionValue : (byte)0xFF;
|
|
default: return 0xFF;
|
|
}
|
|
}
|
|
|
|
private void WritePort(ushort port, byte value)
|
|
{
|
|
port &= 0xFF;
|
|
if (port < 0x40) // general IO ports
|
|
{
|
|
switch (port & 0xFF)
|
|
{
|
|
case 0x01: Port01 = value; break;
|
|
case 0x02: Port02 = value; break;
|
|
case 0x06: PSG.StereoPanning = value; break;
|
|
case 0x3E: Port3E = value; break;
|
|
case 0x3F: Port3F = value; break;
|
|
}
|
|
}
|
|
else if (port < 0x80) // PSG
|
|
PSG.WritePsgData(value, Cpu.TotalExecutedCycles);
|
|
else if (port < 0xC0) // VDP
|
|
{
|
|
if ((port & 1) == 0)
|
|
Vdp.WriteVdpData(value);
|
|
else
|
|
Vdp.WriteVdpControl(value);
|
|
}
|
|
else if (port == 0xF0 && HasYM2413) YM2413.RegisterLatch = value;
|
|
else if (port == 0xF1 && HasYM2413) YM2413.Write(value);
|
|
else if (port == 0xF2 && HasYM2413) YM2413.DetectionValue = value;
|
|
}
|
|
|
|
private string _region;
|
|
private string RegionStr
|
|
{
|
|
get { return _region; }
|
|
set
|
|
{
|
|
if (value.NotIn(validRegions))
|
|
{
|
|
throw new Exception("Passed value " + value + " is not a valid region!");
|
|
}
|
|
|
|
_region = value;
|
|
}
|
|
}
|
|
|
|
private readonly string[] validRegions = { "Export", "Japan", "Auto" };
|
|
}
|
|
} |