using System; using System.Diagnostics; using System.IO; using System.Collections.Generic; using System.Runtime.InteropServices; using BizHawk.Common; using BizHawk.Emulation.Cores.Waterbox; using BizHawk.Common.BizInvoke; using BizHawk.Emulation.Common; namespace BizHawk.Emulation.Cores.Nintendo.SNES { public abstract unsafe class CoreImpl { [BizImport(CallingConvention.Cdecl, Compatibility = true)] public abstract IntPtr DllInit(); [BizImport(CallingConvention.Cdecl, Compatibility = true)] public abstract void Message(LibsnesApi.eMessage msg); [BizImport(CallingConvention.Cdecl, Compatibility = true)] public abstract void CopyBuffer(int id, void* ptr, int size); [BizImport(CallingConvention.Cdecl, Compatibility = true)] public abstract void SetBuffer(int id, void* ptr, int size); [BizImport(CallingConvention.Cdecl)] public abstract void PostLoadState(); } public unsafe partial class LibsnesApi : IDisposable, IMonitor, IBinaryStateable { static LibsnesApi() { if (sizeof(CommStruct) != 232) { throw new InvalidOperationException("sizeof(comm)"); } } private PeRunner _exe; private CoreImpl _core; private bool _disposed; private CommStruct* _comm; private readonly Dictionary _sharedMemoryBlocks = new Dictionary(); private bool _sealed = false; public void Enter() { _exe.Enter(); } public void Exit() { _exe.Exit(); } private readonly List _readonlyFiles = new List(); public void AddReadonlyFile(byte[] data, string name) { _exe.AddReadonlyFile(data, name); _readonlyFiles.Add(name); } public LibsnesApi(string dllPath) { _exe = new PeRunner(new PeRunnerOptions { Filename = "libsnes.wbx", Path = dllPath, SbrkHeapSizeKB = 4 * 1024, InvisibleHeapSizeKB = 8 * 1024, MmapHeapSizeKB = 32 * 1024, // TODO: see if we can safely make libco stacks smaller PlainHeapSizeKB = 2 * 1024, // TODO: wasn't there more in here? SealedHeapSizeKB = 128 * 1024 }); using (_exe.EnterExit()) { // Marshal checks that function pointers passed to GetDelegateForFunctionPointer are // _currently_ valid when created, even though they don't need to be valid until // the delegate is later invoked. so GetInvoker needs to be acquired within a lock. _core = BizInvoker.GetInvoker(_exe, _exe, CallingConventionAdapters.Waterbox); _comm = (CommStruct*)_core.DllInit().ToPointer(); } } public void Dispose() { if (!_disposed) { _disposed = true; _exe.Dispose(); _exe = null; _core = null; _comm = null; } } /// /// Copy an ascii string into libretro. It keeps the copy. /// public void CopyAscii(int id, string str) { fixed (byte* cp = System.Text.Encoding.ASCII.GetBytes(str + "\0")) { _core.CopyBuffer(id, cp, str.Length + 1); } } /// /// Copy a buffer into libretro. It keeps the copy. /// public void CopyBytes(int id, byte[] bytes) { fixed (byte* bp = bytes) { _core.CopyBuffer(id, bp, bytes.Length); } } /// /// Locks a buffer and sets it into libretro. You must pass a delegate to be executed while that buffer is locked. /// This is meant to be used for avoiding a memcpy for large roms (which the core is then just going to memcpy again on its own) /// The memcpy has to happen at some point (libretro semantics specify [not literally, the docs dont say] that the core should finish using the buffer before its init returns) /// but this limits it to once. /// Moreover, this keeps the c++ side from having to free strings when they're no longer used (and memory management is trickier there, so we try to avoid it) /// public void SetBytes(int id, byte[] bytes, Action andThen) { if (_sealed) throw new InvalidOperationException("Init period is over"); fixed (byte* bp = bytes) { _core.SetBuffer(id, bp, bytes.Length); andThen(); } } /// /// see SetBytes /// public void SetAscii(int id, string str, Action andThen) { if (_sealed) throw new InvalidOperationException("Init period is over"); fixed (byte* cp = System.Text.Encoding.ASCII.GetBytes(str + "\0")) { _core.SetBuffer(id, cp, str.Length + 1); andThen(); } } public Action ReadHook, ExecHook; public Action WriteHook; public Action ReadHook_SMP, ExecHook_SMP; public Action WriteHook_SMP; public enum eCDLog_AddrType { CARTROM, CARTRAM, WRAM, APURAM, SGB_CARTROM, SGB_CARTRAM, SGB_WRAM, SGB_HRAM, NUM }; public enum eTRACE : uint { CPU = 0, SMP = 1, GB = 2 } public enum eCDLog_Flags { ExecFirst = 0x01, ExecOperand = 0x02, CPUData = 0x04, DMAData = 0x08, //not supported yet BRR = 0x80, }; snes_video_refresh_t video_refresh; snes_input_poll_t input_poll; snes_input_state_t input_state; snes_input_notify_t input_notify; snes_audio_sample_t audio_sample; snes_scanlineStart_t scanlineStart; snes_path_request_t pathRequest; snes_trace_t traceCallback; public void QUERY_set_video_refresh(snes_video_refresh_t video_refresh) { this.video_refresh = video_refresh; } public void QUERY_set_input_poll(snes_input_poll_t input_poll) { this.input_poll = input_poll; } public void QUERY_set_input_state(snes_input_state_t input_state) { this.input_state = input_state; } public void QUERY_set_input_notify(snes_input_notify_t input_notify) { this.input_notify = input_notify; } public void QUERY_set_path_request(snes_path_request_t pathRequest) { this.pathRequest = pathRequest; } public delegate void snes_video_refresh_t(int* data, int width, int height); public delegate void snes_input_poll_t(); public delegate short snes_input_state_t(int port, int device, int index, int id); public delegate void snes_input_notify_t(int index); public delegate void snes_audio_sample_t(ushort left, ushort right); public delegate void snes_scanlineStart_t(int line); public delegate string snes_path_request_t(int slot, string hint); public delegate void snes_trace_t(uint which, string msg); [StructLayout(LayoutKind.Explicit)] public struct CPURegs { [FieldOffset(0)] public uint pc; [FieldOffset(4)] public ushort a, x, y, z, s, d, vector; //7x [FieldOffset(18)] public byte p, nothing; [FieldOffset(20)] public uint aa, rd; [FieldOffset(28)] public byte sp, dp, db, mdr; [FieldOffset(32)] public ushort v, h; } [StructLayout(LayoutKind.Sequential)] public struct LayerEnables { byte _BG1_Prio0, _BG1_Prio1; byte _BG2_Prio0, _BG2_Prio1; byte _BG3_Prio0, _BG3_Prio1; byte _BG4_Prio0, _BG4_Prio1; byte _Obj_Prio0, _Obj_Prio1, _Obj_Prio2, _Obj_Prio3; public bool BG1_Prio0 { get { return _BG1_Prio0 != 0; } set { _BG1_Prio0 = (byte)(value ? 1 : 0); } } public bool BG1_Prio1 { get { return _BG1_Prio1 != 0; } set { _BG1_Prio1 = (byte)(value ? 1 : 0); } } public bool BG2_Prio0 { get { return _BG2_Prio0 != 0; } set { _BG2_Prio0 = (byte)(value ? 1 : 0); } } public bool BG2_Prio1 { get { return _BG2_Prio1 != 0; } set { _BG2_Prio1 = (byte)(value ? 1 : 0); } } public bool BG3_Prio0 { get { return _BG3_Prio0 != 0; } set { _BG3_Prio0 = (byte)(value ? 1 : 0); } } public bool BG3_Prio1 { get { return _BG3_Prio1 != 0; } set { _BG3_Prio1 = (byte)(value ? 1 : 0); } } public bool BG4_Prio0 { get { return _BG4_Prio0 != 0; } set { _BG4_Prio0 = (byte)(value ? 1 : 0); } } public bool BG4_Prio1 { get { return _BG4_Prio1 != 0; } set { _BG4_Prio1 = (byte)(value ? 1 : 0); } } public bool Obj_Prio0 { get { return _Obj_Prio0 != 0; } set { _Obj_Prio0 = (byte)(value ? 1 : 0); } } public bool Obj_Prio1 { get { return _Obj_Prio1 != 0; } set { _Obj_Prio1 = (byte)(value ? 1 : 0); } } public bool Obj_Prio2 { get { return _Obj_Prio2 != 0; } set { _Obj_Prio2 = (byte)(value ? 1 : 0); } } public bool Obj_Prio3 { get { return _Obj_Prio3 != 0; } set { _Obj_Prio3 = (byte)(value ? 1 : 0); } } } [StructLayout(LayoutKind.Explicit)] struct CommStruct { [FieldOffset(0)] //the cmd being executed public eMessage cmd; [FieldOffset(4)] //the status of the core public eStatus status; [FieldOffset(8)] //the SIG or BRK that the core is halted in public eMessage reason; //flexible in/out parameters //these are all "overloaded" a little so it isn't clear what's used for what in for any particular message.. //but I think it will beat having to have some kind of extremely verbose custom layouts for every message [FieldOffset(16)] public sbyte* str; [FieldOffset(24)] public void* ptr; [FieldOffset(32)] public uint id; [FieldOffset(36)] public uint addr; [FieldOffset(40)] public uint value; [FieldOffset(44)] public uint size; [FieldOffset(48)] public int port; [FieldOffset(52)] public int device; [FieldOffset(56)] public int index; [FieldOffset(60)] public int slot; [FieldOffset(64)] public int width; [FieldOffset(68)] public int height; [FieldOffset(72)] public int scanline; [FieldOffset(76)] public fixed int inports[2]; [FieldOffset(88)] //this should always be used in pairs public fixed long buf[3]; //ACTUALLY A POINTER but can't marshal it :( [FieldOffset(112)] public fixed int buf_size[3]; [FieldOffset(128)] //bleck. this is a long so that it can be a 32/64bit pointer public fixed long cdl_ptr[4]; [FieldOffset(160)] public fixed int cdl_size[4]; [FieldOffset(176)] public CPURegs cpuregs; [FieldOffset(212)] public LayerEnables layerEnables; [FieldOffset(224)] //static configuration-type information which can be grabbed off the core at any time without even needing a QUERY command public SNES_REGION region; [FieldOffset(228)] public SNES_MAPPER mapper; //utilities //TODO: make internal, wrap on the API instead of the comm public unsafe string GetAscii() { return _getAscii(str); } public bool GetBool() { return value != 0; } private unsafe string _getAscii(sbyte* ptr) { int len = 0; sbyte* junko = (sbyte*)ptr; while (junko[len] != 0) len++; return new string((sbyte*)str, 0, len, System.Text.Encoding.ASCII); } } public SNES_REGION Region { get { using (_exe.EnterExit()) { return _comm->region; } } } public SNES_MAPPER Mapper { get { using (_exe.EnterExit()) { return _comm->mapper; } } } public void SetLayerEnables(ref LayerEnables enables) { using (_exe.EnterExit()) { _comm->layerEnables = enables; QUERY_set_layer_enable(); } } public void SetInputPortBeforeInit(int port, SNES_INPUT_PORT type) { using (_exe.EnterExit()) { _comm->inports[port] = (int)type; } } public void Seal() { /* Cothreads can very easily acquire "pointer poison"; because their stack and even registers * are part of state, any poisoned pointer that's used even temporarily might be persisted longer * than needed. Most of the libsnes core cothreads handle internal matters only and aren't very * vulnerable to pointer poison, but the main boss cothread is used heavily during init, when * many syscalls happen and many kinds of poison can end up on the stack. so here, we call * _core.DllInit() again, which recreates that cothread, zeroing out all of the memory first, * as well as zeroing out the comm struct. */ _core.DllInit(); _exe.Seal(); _sealed = true; foreach (var s in _readonlyFiles) { _exe.RemoveReadonlyFile(s); } _readonlyFiles.Clear(); } public void SaveStateBinary(BinaryWriter writer) { _exe.SaveStateBinary(writer); } public void LoadStateBinary(BinaryReader reader) { _exe.LoadStateBinary(reader); _core.PostLoadState(); } } }