using System; using System.Collections.Generic; using System.IO; using BizHawk.Common; using BizHawk.Common.NumberExtensions; using BizHawk.Common.StringExtensions; using BizHawk.Emulation.Common; using BizHawk.Emulation.Common.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 { // Constants public const int BankSize = 16384; // ROM public byte[] RomData; public byte RomBank0, RomBank1, RomBank2, RomBank3; public byte RomBanks; // SaveRAM public byte[] SaveRAM; public byte SaveRamBank; public byte[] BiosRom; public byte[] CloneSaveRam() { if (SaveRAM != null) return (byte[])SaveRAM.Clone(); else return null; } public void StoreSaveRam(byte[] data) { if (SaveRAM != null) Array.Copy(data, SaveRAM, data.Length); } public bool SaveRamModified { get; private set; } // Machine resources public Z80A Cpu; public byte[] SystemRam; public VDP Vdp; public SN76489 PSG; public YM2413 YM2413; public SoundMixer SoundMixer; public bool IsGameGear = false; public bool IsSG1000 = false; public bool HasYM2413 = false; int frame = 0; int lagCount = 0; bool lagged = true; bool isLag = false; public int Frame { get { return frame; } set { frame = value; } } public int LagCount { get { return lagCount; } set { lagCount = value; } } public bool IsLagFrame { get { return isLag; } } private readonly InputCallbackSystem _inputCallbacks = new InputCallbackSystem(); public IInputCallbackSystem InputCallbacks { get { return _inputCallbacks; } } public IMemoryCallbackSystem MemoryCallbacks { get; private set; } byte Port01 = 0xFF; byte Port02 = 0xFF; byte Port3E = 0xAF; byte Port3F = 0xFF; byte ForceStereoByte = 0xAD; bool IsGame3D = false; public DisplayType Region { get; set; } public bool DeterministicEmulation { get { return true; } } [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; Tracer = new TraceBuffer(); (ServiceProvider as BasicServiceProvider).Register(Tracer); 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 CoreComm.VsyncNum = Region == DisplayType.NTSC ? 60 : 50; CoreComm.VsyncDen = 1; 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; if (Controller == null) Controller = NullController.GetNullController(); Cpu = new Z80A(); Cpu.RegisterSP = 0xDFF0; Cpu.ReadHardware = ReadPort; Cpu.WriteHardware = WritePort; Cpu.MemoryCallbacks = MemoryCallbacks; Vdp = new VDP(this, Cpu, IsGameGear ? VdpMode.GameGear : VdpMode.SMS, Region); (ServiceProvider as BasicServiceProvider).Register(Vdp); PSG = new SN76489(); YM2413 = new YM2413(); SoundMixer = new SoundMixer(YM2413, PSG); if (HasYM2413 && game["WhenFMDisablePSG"]) SoundMixer.DisableSource(PSG); ActiveSoundProvider = HasYM2413 ? (ISoundProvider)SoundMixer : PSG; 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 && (game["RequireBios"] || SyncSettings.UseBIOS)) Port3E = 0xF7; if (BiosRom == null && game["RequireBios"]) throw new MissingFirmwareException("BIOS image not available. This game requires BIOS to function."); if (SyncSettings.UseBIOS && BiosRom == null) CoreComm.Notify("BIOS was selected, but rom image not available. BIOS not enabled."); } if (game["SRAM"]) SaveRAM = new byte[int.Parse(game.OptionValue("SRAM"))]; else if (game.NotInDatabase) SaveRAM = new byte[0x8000]; SetupMemoryDomains(); (ServiceProvider as BasicServiceProvider).Register(new Disassembler()); } public IEmulatorServiceProvider ServiceProvider { get; private 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"; } 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; } public void ResetCounters() { Frame = 0; lagCount = 0; isLag = false; } public 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; } } public 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; } public void FrameAdvance(bool render, bool rendersound) { lagged = true; Frame++; PSG.BeginFrame(Cpu.TotalExecutedCycles); Cpu.Debug = Tracer.Enabled; if (!IsGameGear) PSG.StereoPanning = Settings.ForceStereoSeparation ? ForceStereoByte : (byte) 0xFF; if (Cpu.Debug && Cpu.Logger == null) // TODO, lets not do this on each frame. But lets refactor CoreComm/CoreComm first Cpu.Logger = (s) => Tracer.Put(s); if (IsGameGear == false) Cpu.NonMaskableInterrupt = Controller["Pause"]; if (IsGame3D && Settings.Fix3D) Vdp.ExecFrame((Frame & 1) == 0); else Vdp.ExecFrame(render); PSG.EndFrame(Cpu.TotalExecutedCycles); if (lagged) { lagCount++; isLag = true; } else isLag = false; } public bool BinarySaveStatesPreferred { get { return false; } } public void SaveStateBinary(BinaryWriter bw) { SyncState(Serializer.CreateBinaryWriter(bw)); } public void LoadStateBinary(BinaryReader br) { SyncState(Serializer.CreateBinaryReader(br)); } public void SaveStateText(TextWriter tw) { SyncState(Serializer.CreateTextWriter(tw)); } public void LoadStateText(TextReader tr) { SyncState(Serializer.CreateTextReader(tr)); } void SyncState(Serializer ser) { ser.BeginSection("SMS"); Cpu.SyncState(ser); Vdp.SyncState(ser); PSG.SyncState(ser); ser.Sync("RAM", ref SystemRam, false); ser.Sync("RomBank0", ref RomBank0); ser.Sync("RomBank1", ref RomBank1); ser.Sync("RomBank2", ref RomBank2); ser.Sync("RomBank3", ref RomBank3); ser.Sync("Port01", ref Port01); ser.Sync("Port02", ref Port02); ser.Sync("Port3E", ref Port3E); ser.Sync("Port3F", ref Port3F); if (SaveRAM != null) { ser.Sync("SaveRAM", ref SaveRAM, false); ser.Sync("SaveRamBank", ref SaveRamBank); } if (ExtRam != null) ser.Sync("ExtRAM", ref ExtRam, true); if (HasYM2413) YM2413.SyncState(ser); ser.Sync("Frame", ref frame); ser.Sync("LagCount", ref lagCount); ser.Sync("IsLag", ref isLag); ser.EndSection(); } byte[] stateBuffer; public byte[] SaveStateBinary() { if (stateBuffer == null) { var stream = new MemoryStream(); var writer = new BinaryWriter(stream); SaveStateBinary(writer); stateBuffer = stream.ToArray(); writer.Close(); return stateBuffer; } else { var stream = new MemoryStream(stateBuffer); var writer = new BinaryWriter(stream); SaveStateBinary(writer); writer.Close(); return stateBuffer; } } public CoreComm CoreComm { get; private set; } ISoundProvider ActiveSoundProvider; public ISoundProvider SoundProvider { get { return ActiveSoundProvider; } } public ISyncSoundProvider SyncSoundProvider { get { return new FakeSyncSound(ActiveSoundProvider, 735); } } public bool StartAsyncSound() { return true; } public void EndAsyncSound() { } public string SystemId { get { return "SMS"; } } public string BoardName { get { return null; } } string region; public string RegionStr { get { return region; } set { if (value.NotIn(validRegions)) throw new Exception("Passed value " + value + " is not a valid region!"); region = value; } } readonly string[] validRegions = { "Export", "Japan", "Auto" }; MemoryDomainList memoryDomains; void SetupMemoryDomains() { var domains = new List(3); var MainMemoryDomain = new MemoryDomain("Main RAM", SystemRam.Length, MemoryDomain.Endian.Little, addr => SystemRam[addr], (addr, value) => SystemRam[addr] = value); var VRamDomain = new MemoryDomain("Video RAM", Vdp.VRAM.Length, MemoryDomain.Endian.Little, addr => Vdp.VRAM[addr], (addr, value) => Vdp.VRAM[addr] = value); var SystemBusDomain = new MemoryDomain("System Bus", 0x10000, MemoryDomain.Endian.Little, (addr) => { if (addr < 0 || addr >= 65536) throw new ArgumentOutOfRangeException(); return Cpu.ReadMemory((ushort)addr); }, (addr, value) => { if (addr < 0 || addr >= 65536) throw new ArgumentOutOfRangeException(); Cpu.WriteMemory((ushort)addr, value); }); domains.Add(MainMemoryDomain); domains.Add(VRamDomain); domains.Add(SystemBusDomain); if (SaveRAM != null) { var SaveRamDomain = new MemoryDomain("Save RAM", SaveRAM.Length, MemoryDomain.Endian.Little, addr => SaveRAM[addr], (addr, value) => { SaveRAM[addr] = value; SaveRamModified = true; }); domains.Add(SaveRamDomain); } if (ExtRam != null) { var ExtRamDomain = new MemoryDomain("Cart (Volatile) RAM", ExtRam.Length, MemoryDomain.Endian.Little, addr => ExtRam[addr], (addr, value) => { ExtRam[addr] = value; }); domains.Add(ExtRamDomain); } memoryDomains = new MemoryDomainList(domains); (ServiceProvider as BasicServiceProvider).Register(memoryDomains); } public IDictionary GetCpuFlagsAndRegisters() { return new Dictionary { { "A", Cpu.RegisterA }, { "AF", Cpu.RegisterAF }, { "B", Cpu.RegisterB }, { "BC", Cpu.RegisterBC }, { "C", Cpu.RegisterC }, { "D", Cpu.RegisterD }, { "DE", Cpu.RegisterDE }, { "E", Cpu.RegisterE }, { "F", Cpu.RegisterF }, { "H", Cpu.RegisterH }, { "HL", Cpu.RegisterHL }, { "I", Cpu.RegisterI }, { "IX", Cpu.RegisterIX }, { "IY", Cpu.RegisterIY }, { "L", Cpu.RegisterL }, { "PC", Cpu.RegisterPC }, { "R", Cpu.RegisterR }, { "Shadow AF", Cpu.RegisterShadowAF }, { "Shadow BC", Cpu.RegisterShadowBC }, { "Shadow DE", Cpu.RegisterShadowDE }, { "Shadow HL", Cpu.RegisterShadowHL }, { "SP", Cpu.RegisterSP }, { "Flag C", Cpu.RegisterF.Bit(0) }, { "Flag N", Cpu.RegisterF.Bit(1) }, { "Flag P/V", Cpu.RegisterF.Bit(2) }, { "Flag 3rd", Cpu.RegisterF.Bit(3) }, { "Flag H", Cpu.RegisterF.Bit(4) }, { "Flag 5th", Cpu.RegisterF.Bit(5) }, { "Flag Z", Cpu.RegisterF.Bit(6) }, { "Flag S", Cpu.RegisterF.Bit(7) }, }; } public void SetCpuRegister(string register, int value) { switch (register) { default: throw new InvalidOperationException(); case "A": Cpu.RegisterA = (byte)value; break; case "AF": Cpu.RegisterAF = (byte)value; break; case "B": Cpu.RegisterB = (byte)value; break; case "BC": Cpu.RegisterBC = (byte)value; break; case "C": Cpu.RegisterC = (byte)value; break; case "D": Cpu.RegisterD = (byte)value; break; case "DE": Cpu.RegisterDE = (byte)value; break; case "E": Cpu.RegisterE = (byte)value; break; case "F": Cpu.RegisterF = (byte)value; break; case "H": Cpu.RegisterH = (byte)value; break; case "HL": Cpu.RegisterHL = (byte)value; break; case "I": Cpu.RegisterI = (byte)value; break; case "IX": Cpu.RegisterIX = (byte)value; break; case "IY": Cpu.RegisterIY = (byte)value; break; case "L": Cpu.RegisterL = (byte)value; break; case "PC": Cpu.RegisterPC = (ushort)value; break; case "R": Cpu.RegisterR = (byte)value; break; case "Shadow AF": Cpu.RegisterShadowAF = (byte)value; break; case "Shadow BC": Cpu.RegisterShadowBC = (byte)value; break; case "Shadow DE": Cpu.RegisterShadowDE = (byte)value; break; case "Shadow HL": Cpu.RegisterShadowHL = (byte)value; break; case "SP": Cpu.RegisterSP = (byte)value; break; } } public bool CanStep(StepType type) { return false; } [FeatureNotImplemented] public void Step(StepType type) { throw new NotImplementedException(); } public void Dispose() { } public SMSSettings GetSettings() { return Settings.Clone(); } public SMSSyncSettings GetSyncSettings() { return SyncSettings.Clone(); } public bool PutSettings(SMSSettings o) { bool ret = SMSSettings.RebootNeeded(Settings, o); Settings = o; return ret; } public bool PutSyncSettings(SMSSyncSettings o) { bool ret = SMSSyncSettings.RebootNeeded(SyncSettings, o); SyncSettings = o; return ret; } public SMSSettings Settings; public SMSSyncSettings SyncSettings; public class SMSSettings { // Game settings public bool ForceStereoSeparation = false; public bool SpriteLimit = false; public bool Fix3D = true; // GG settings public bool ShowClippedRegions = false; public bool HighlightActiveDisplayRegion = false; // graphics settings public bool DispBG = true; public bool DispOBJ = true; public SMSSettings Clone() { return (SMSSettings)MemberwiseClone(); } public static bool RebootNeeded(SMSSettings x, SMSSettings y) { return false; } } public class SMSSyncSettings { public bool EnableFM = true; public bool AllowOverlock = false; public bool UseBIOS = false; public string ConsoleRegion = "Export"; public string DisplayType = "NTSC"; public SMSSyncSettings Clone() { return (SMSSyncSettings)MemberwiseClone(); } public static bool RebootNeeded(SMSSyncSettings x, SMSSyncSettings y) { return x.EnableFM != y.EnableFM || x.AllowOverlock != y.AllowOverlock || x.UseBIOS != y.UseBIOS || x.ConsoleRegion != y.ConsoleRegion || x.DisplayType != y.DisplayType; } } } }