using System; using BizHawk.Emulation.Common; using BizHawk.Emulation.Cores.Waterbox; using BizHawk.Common.BizInvoke; using System.Runtime.InteropServices; using System.IO; using BizHawk.Common.BufferExtensions; using System.ComponentModel; using BizHawk.Common; using System.Collections.Generic; using System.Linq; namespace BizHawk.Emulation.Cores.Nintendo.SNES9X { [CoreAttributes("Snes9x", "", true, true, "5e0319ab3ef9611250efb18255186d0dc0d7e125", "https://github.com/snes9xgit/snes9x", false)] [ServiceNotApplicable(typeof(IDriveLight))] public class Snes9x : IEmulator, IVideoProvider, ISoundProvider, IStatable, ISettable, ISaveRam, IInputPollable, IRegionable { private LibSnes9x _core; private PeRunner _exe; [CoreConstructor("SNES")] public Snes9x(CoreComm comm, byte[] rom, Settings settings, SyncSettings syncSettings) { ServiceProvider = new BasicServiceProvider(this); CoreComm = comm; settings = settings ?? new Settings(); syncSettings = syncSettings ?? new SyncSettings(); _exe = new PeRunner(new PeRunnerOptions { Path = comm.CoreFileProvider.DllPath(), Filename = "snes9x.wbx", SbrkHeapSizeKB = 1024, SealedHeapSizeKB = 12 * 1024, InvisibleHeapSizeKB = 6 * 1024, PlainHeapSizeKB = 64 }); _core = BizInvoker.GetInvoker(_exe, _exe); if (!_core.biz_init()) { throw new InvalidOperationException("Init() failed"); } if (!_core.biz_load_rom(rom, rom.Length)) { throw new InvalidOperationException("LoadRom() failed"); } _exe.Seal(); if (_core.biz_is_ntsc()) { Console.WriteLine("NTSC rom loaded"); VsyncNumerator = 21477272; VsyncDenominator = 357366; Region = DisplayType.NTSC; } else { Console.WriteLine("PAL rom loaded"); VsyncNumerator = 21281370; VsyncDenominator = 425568; Region = DisplayType.PAL; } _nsampTarget = (int)Math.Round(44100.0 * VsyncDenominator / VsyncNumerator); _nsampWarn = (int)Math.Round(1.05 * 44100.0 * VsyncDenominator / VsyncNumerator); _syncSettings = syncSettings; InitControllers(); PutSettings(settings); InitMemoryDomains(); InitSaveram(); _inputCallback = InputCallbacks.Call; } #region controller private readonly short[] _inputState = new short[16 * 8]; private List _cdums; private readonly List _controllers = new List(); private void InitControllers() { _core.biz_set_port_devices(_syncSettings.LeftPort, _syncSettings.RightPort); switch (_syncSettings.LeftPort) { case LibSnes9x.LeftPortDevice.Joypad: _controllers.Add(new Joypad()); break; } switch (_syncSettings.RightPort) { case LibSnes9x.RightPortDevice.Joypad: _controllers.Add(new Joypad()); break; case LibSnes9x.RightPortDevice.Multitap: _controllers.Add(new Joypad()); _controllers.Add(new Joypad()); _controllers.Add(new Joypad()); _controllers.Add(new Joypad()); break; case LibSnes9x.RightPortDevice.Mouse: _controllers.Add(new Mouse()); break; case LibSnes9x.RightPortDevice.SuperScope: _controllers.Add(new SuperScope()); break; case LibSnes9x.RightPortDevice.Justifier: _controllers.Add(new Justifier()); break; } ControllerDefinition = ControllerDefinitionMerger.GetMerged( _controllers.Select(c => c.Definition), out _cdums); // add buttons that the core itself will handle ControllerDefinition.BoolButtons.Add("Reset"); ControllerDefinition.BoolButtons.Add("Power"); ControllerDefinition.Name = "SNES Controller"; } private void UpdateControls(IController c) { Array.Clear(_inputState, 0, 16 * 8); for (int i = 0, offset = 0; i < _controllers.Count; i++, offset += 16) { _controllers[i].ApplyState(_cdums[i].UnMerge(c), _inputState, offset); } } private interface IControlDevice { ControllerDefinition Definition { get; } void ApplyState(IController controller, short[] input, int offset); } private class Joypad : IControlDevice { private static readonly string[] Buttons = { "0B", "0Y", "0Select", "0Start", "0Up", "0Down", "0Left", "0Right", "0A", "0X", "0L", "0R" }; private static int ButtonOrder(string btn) { var order = new Dictionary { ["0Up"] = 0, ["0Down"] = 1, ["0Left"] = 2, ["0Right"] = 3, ["0Select"] = 4, ["0Start"] = 5, ["0Y"] = 6, ["0B"] = 7, ["0X"] = 8, ["0A"] = 9, ["0L"] = 10, ["0R"] = 11 }; return order[btn]; } private static readonly ControllerDefinition _definition = new ControllerDefinition { BoolButtons = Buttons.OrderBy(ButtonOrder).ToList() }; public ControllerDefinition Definition { get; } = _definition; public void ApplyState(IController controller, short[] input, int offset) { for (int i = 0; i < Buttons.Length; i++) input[offset + i] = (short)(controller.IsPressed(Buttons[i]) ? 1 : 0); } } private abstract class Analog : IControlDevice { public abstract ControllerDefinition Definition { get; } public void ApplyState(IController controller, short[] input, int offset) { foreach (var s in Definition.FloatControls) input[offset++] = (short)(controller.GetFloat(s)); foreach (var s in Definition.BoolButtons) input[offset++] = (short)(controller.IsPressed(s) ? 1 : 0); } } private class Mouse : Analog { private static readonly ControllerDefinition _definition = new ControllerDefinition { BoolButtons = new List { "0Mouse Left", "0Mouse Right" }, FloatControls = { "0Mouse X", "0Mouse Y" }, FloatRanges = { new[] { -127f, 0f, 127f }, new[] { -127f, 0f, 127f } } }; public override ControllerDefinition Definition => _definition; } private class SuperScope : Analog { private static readonly ControllerDefinition _definition = new ControllerDefinition { BoolButtons = new List { "0Trigger", "0Cursor", "0Turbo", "0Pause" }, FloatControls = { "0Scope X", "0Scope Y" }, FloatRanges = { // snes9x is always in 224 mode new[] { 0f, 128f, 256f }, new[] { 0f, 0f, 240f } } }; public override ControllerDefinition Definition => _definition; } private class Justifier : Analog { private static readonly ControllerDefinition _definition = new ControllerDefinition { BoolButtons = new List { "0Trigger", "0Start", }, FloatControls = { "0Justifier X", "0Justifier Y", }, FloatRanges = { // snes9x is always in 224 mode new[] { 0f, 128f, 256f }, new[] { 0f, 0f, 240f }, } }; public override ControllerDefinition Definition => _definition; } public ControllerDefinition ControllerDefinition { get; private set; } #endregion private bool _disposed = false; public void Dispose() { if (!_disposed) { _exe.Dispose(); _exe = null; _disposed = true; } } public DisplayType Region { get; } public IEmulatorServiceProvider ServiceProvider { get; } public void FrameAdvance(IController controller, bool render, bool rendersound = true) { _core.biz_set_input_callback(InputCallbacks.Count > 0 ? _inputCallback : null); if (controller.IsPressed("Power")) _core.biz_hard_reset(); else if (controller.IsPressed("Reset")) _core.biz_soft_reset(); UpdateControls(controller); Frame++; LibSnes9x.frame_info frame = new LibSnes9x.frame_info(); _core.biz_run(frame, _inputState); IsLagFrame = frame.padread == 0; if (IsLagFrame) LagCount++; using (_exe.EnterExit()) { Blit(frame); Sblit(frame); } } public int Frame { get; private set; } public void ResetCounters() { Frame = 0; } public string SystemId { get { return "SNES"; } } public bool DeterministicEmulation { get { return true; } } public CoreComm CoreComm { get; private set; } #region IVideoProvider private static readonly int[] VirtualWidths = new[] { 293, 587, 587, 587 }; // 256 512 256 512 private static readonly int[] VirtualHeights = new[] { 224, 448, 448, 448 }; // 224 224 448 448 private unsafe void Blit(LibSnes9x.frame_info frame) { BufferWidth = frame.vwidth; BufferHeight = frame.vheight; int vinc = frame.vpitch / sizeof(ushort) - frame.vwidth; ushort* src = (ushort*)frame.vptr; fixed (int* _dst = _vbuff) { byte* dst = (byte*)_dst; for (int j = 0; j < frame.vheight; j++) { for (int i = 0; i < frame.vwidth; i++) { var c = *src++; *dst++ = (byte)(c << 3 & 0xf8 | c >> 2 & 7); *dst++ = (byte)(c >> 3 & 0xfa | c >> 9 & 3); *dst++ = (byte)(c >> 8 & 0xf8 | c >> 13 & 7); *dst++ = 0xff; } src += vinc; } } VirtualHeight = BufferHeight; VirtualWidth = BufferWidth; if (VirtualHeight * 2 < VirtualWidth) VirtualHeight *= 2; if (VirtualHeight > 240) VirtualWidth = 512; VirtualWidth = (int)Math.Round(VirtualWidth * 1.146); } private int[] _vbuff = new int[512 * 480]; public int[] GetVideoBuffer() { return _vbuff; } public int VirtualWidth { get; private set; } = 293; public int VirtualHeight { get; private set; } = 224; public int BufferWidth { get; private set; } = 256; public int BufferHeight { get; private set; } = 224; public int BackgroundColor { get { return unchecked((int)0xff000000); } } public int VsyncNumerator { get; } public int VsyncDenominator { get; } #endregion #region ISoundProvider private void Sblit(LibSnes9x.frame_info frame) { Marshal.Copy(frame.sptr, _sbuff, 0, frame.slen * 2); _nsamp = frame.slen; if (_nsamp > _nsampWarn) { Console.WriteLine($"Warn: Long frame! {_nsamp} > {_nsampTarget}"); } } private readonly int _nsampWarn; private readonly int _nsampTarget; private int _nsamp; private short[] _sbuff = new short[8192]; public void GetSamplesSync(out short[] samples, out int nsamp) { samples = _sbuff; nsamp = _nsamp; } public void DiscardSamples() { // Nothing to do } public void SetSyncMode(SyncSoundMode mode) { if (mode == SyncSoundMode.Async) { throw new NotSupportedException("Async mode is not supported."); } } public bool CanProvideAsync { get { return false; } } public SyncSoundMode SyncMode { get { return SyncSoundMode.Sync; } } public void GetSamplesAsync(short[] samples) { throw new InvalidOperationException("Async mode is not supported."); } #endregion private LibSnes9x.InputCallback _inputCallback; public int LagCount { get; set; } public bool IsLagFrame { get; set; } public IInputCallbackSystem InputCallbacks { get; } = new InputCallbackSystem(); #region IStatable public bool BinarySaveStatesPreferred { get { return true; } } public void SaveStateText(TextWriter writer) { var temp = SaveStateBinary(); temp.SaveAsHexFast(writer); // write extra copy of stuff we don't use writer.WriteLine("Frame {0}", Frame); } public void LoadStateText(TextReader reader) { string hex = reader.ReadLine(); byte[] state = new byte[hex.Length / 2]; state.ReadFromHexFast(hex); LoadStateBinary(new BinaryReader(new MemoryStream(state))); } public void LoadStateBinary(BinaryReader reader) { _exe.LoadStateBinary(reader); // other variables Frame = reader.ReadInt32(); LagCount = reader.ReadInt32(); IsLagFrame = reader.ReadBoolean(); // any managed pointers that we sent to the core need to be resent now! _core.biz_set_input_callback(null); _core.biz_post_load_state(); } public void SaveStateBinary(BinaryWriter writer) { _exe.SaveStateBinary(writer); // other variables writer.Write(Frame); writer.Write(LagCount); writer.Write(IsLagFrame); } public byte[] SaveStateBinary() { var ms = new MemoryStream(); var bw = new BinaryWriter(ms); SaveStateBinary(bw); bw.Flush(); ms.Close(); return ms.ToArray(); } #endregion #region settings private Settings _settings; private SyncSettings _syncSettings; public Settings GetSettings() { return _settings.Clone(); } public SyncSettings GetSyncSettings() { return _syncSettings.Clone(); } public bool PutSettings(Settings o) { _settings = o; int s = 0; if (o.PlaySound0) s |= 1; if (o.PlaySound0) s |= 2; if (o.PlaySound0) s |= 4; if (o.PlaySound0) s |= 8; if (o.PlaySound0) s |= 16; if (o.PlaySound0) s |= 32; if (o.PlaySound0) s |= 64; if (o.PlaySound0) s |= 128; _core.biz_set_sound_channels(s); int l = 0; if (o.ShowBg0) l |= 1; if (o.ShowBg1) l |= 2; if (o.ShowBg2) l |= 4; if (o.ShowBg3) l |= 8; if (o.ShowSprites) l |= 16; if (o.ShowWindow) l |= 32; if (o.ShowTransparency) l |= 64; _core.biz_set_layers(l); return false; // no reboot needed } public bool PutSyncSettings(SyncSettings o) { var ret = SyncSettings.NeedsReboot(_syncSettings, o); _syncSettings = o; return ret; } public class Settings { [DefaultValue(true)] [DisplayName("Enable Sound Channel 1")] public bool PlaySound0 { get; set; } [DefaultValue(true)] [DisplayName("Enable Sound Channel 2")] public bool PlaySound1 { get; set; } [DefaultValue(true)] [DisplayName("Enable Sound Channel 3")] public bool PlaySound2 { get; set; } [DefaultValue(true)] [DisplayName("Enable Sound Channel 4")] public bool PlaySound3 { get; set; } [DefaultValue(true)] [DisplayName("Enable Sound Channel 5")] public bool PlaySound4 { get; set; } [DefaultValue(true)] [DisplayName("Enable Sound Channel 6")] public bool PlaySound5 { get; set; } [DefaultValue(true)] [DisplayName("Enable Sound Channel 7")] public bool PlaySound6 { get; set; } [DefaultValue(true)] [DisplayName("Enable Sound Channel 8")] public bool PlaySound7 { get; set; } [DefaultValue(true)] [DisplayName("Enable Background Layer 1")] public bool ShowBg0 { get; set; } [DefaultValue(true)] [DisplayName("Enable Background Layer 2")] public bool ShowBg1 { get; set; } [DefaultValue(true)] [DisplayName("Enable Background Layer 3")] public bool ShowBg2 { get; set; } [DefaultValue(true)] [DisplayName("Enable Background Layer 4")] public bool ShowBg3 { get; set; } [DefaultValue(true)] [DisplayName("Show Sprites")] public bool ShowSprites { get; set; } [DefaultValue(true)] [DisplayName("Show Window")] public bool ShowWindow { get; set; } [DefaultValue(true)] [DisplayName("Show Transparency")] public bool ShowTransparency { get; set; } public Settings() { SettingsUtil.SetDefaultValues(this); } public Settings Clone() { return (Settings)MemberwiseClone(); } } public class SyncSettings { [DefaultValue(LibSnes9x.LeftPortDevice.Joypad)] [DisplayName("Left Port")] [Description("Specifies the controller type plugged into the left controller port on the console")] public LibSnes9x.LeftPortDevice LeftPort { get; set; } [DefaultValue(LibSnes9x.RightPortDevice.Joypad)] [DisplayName("Right Port")] [Description("Specifies the controller type plugged into the right controller port on the console")] public LibSnes9x.RightPortDevice RightPort { get; set; } public SyncSettings() { SettingsUtil.SetDefaultValues(this); } public SyncSettings Clone() { return (SyncSettings)MemberwiseClone(); } public static bool NeedsReboot(SyncSettings x, SyncSettings y) { // the core can handle dynamic plugging and unplugging, but that changes // the controllerdefinition, and we're not ready for that return !DeepEquality.DeepEquals(x, y); } } #endregion #region Memory Domains private unsafe void InitMemoryDomains() { var native = new LibSnes9x.memory_area(); var domains = new List(); var names = new[] { "CARTRAM", "CARTRAM B", "RTC", "WRAM", "VRAM" }; int index = 0; foreach (var s in names) { _core.biz_get_memory_area(index++, native); if (native.ptr != IntPtr.Zero && native.size > 0) { domains.Add(new MemoryDomainIntPtrMonitor(s, MemoryDomain.Endian.Little, native.ptr, native.size, true, 2, _exe)); } } (ServiceProvider as BasicServiceProvider).Register(new MemoryDomainList(domains) { MainMemory = domains.Single(d => d.Name == "WRAM") }); } #endregion #region ISaveRam private void InitSaveram() { for (int i = 0; i < 2; i++) // SRAM A, SRAM B, RTC { var native = new LibSnes9x.memory_area(); _core.biz_get_memory_area(i, native); if (native.ptr != IntPtr.Zero && native.size > 0) _saveramMemoryAreas.Add(native); } _saveramSize = _saveramMemoryAreas.Sum(a => a.size); } private readonly List _saveramMemoryAreas = new List(); private int _saveramSize; public bool SaveRamModified => _saveramSize > 0; public byte[] CloneSaveRam() { using (_exe.EnterExit()) { var ret = new byte[_saveramSize]; var offset = 0; foreach (var area in _saveramMemoryAreas) { Marshal.Copy(area.ptr, ret, offset, area.size); offset += area.size; } return ret; } } public void StoreSaveRam(byte[] data) { using (_exe.EnterExit()) { if (data.Length != _saveramSize) throw new InvalidOperationException("Saveram size mismatch"); var offset = 0; foreach (var area in _saveramMemoryAreas) { Marshal.Copy(data, offset, area.ptr, area.size); offset += area.size; } } } #endregion } }