diff --git a/src/BizHawk.Client.Common/RomLoader.cs b/src/BizHawk.Client.Common/RomLoader.cs index 60bbcff038..6a634f60e5 100644 --- a/src/BizHawk.Client.Common/RomLoader.cs +++ b/src/BizHawk.Client.Common/RomLoader.cs @@ -637,7 +637,7 @@ namespace BizHawk.Client.Common // must be done before LoadNoGame (which triggers retro_init and the paths to be consumed by the core) // game name == name of core Game = game = new GameInfo { Name = Path.GetFileNameWithoutExtension(launchLibretroCore), System = VSystemID.Raw.Libretro }; - var retro = new LibretroEmulator(nextComm, game, launchLibretroCore); + var retro = new LibretroHost(nextComm, game, launchLibretroCore); nextEmulator = retro; if (retro.Description.SupportsNoGame && string.IsNullOrEmpty(path)) diff --git a/src/BizHawk.Client.Common/movie/bk2/Bk2MnemonicLookup.cs b/src/BizHawk.Client.Common/movie/bk2/Bk2MnemonicLookup.cs index 1c970d3df5..14c3e410da 100644 --- a/src/BizHawk.Client.Common/movie/bk2/Bk2MnemonicLookup.cs +++ b/src/BizHawk.Client.Common/movie/bk2/Bk2MnemonicLookup.cs @@ -23,7 +23,7 @@ namespace BizHawk.Client.Common key = key.Substring(3); } } - key = key.RemovePrefix(LibretroEmulator.LibretroControllerDef.PFX_RETROPAD); + key = key.RemovePrefix(LibretroHost.LibretroControllerDef.PFX_RETROPAD); if (SystemOverrides.TryGetValue(systemId, out var overridesForSys) && overridesForSys.TryGetValue(key, out var c)) { diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs index 36d4310911..02186e11d4 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Events.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Events.cs @@ -2837,7 +2837,7 @@ namespace BizHawk.Client.EmuHawk items.Add(CreateCoreSubmenu(VSystemCategory.Consoles, CoreNames.Gpgx, CreateGenericCoreConfigItem(CoreNames.Gpgx))); // Handy - items.Add(CreateCoreSubmenu(VSystemCategory.Handhelds, CoreNames.Handy, CreateGenericCoreConfigItem(CoreNames.Handy))); // as Handy doesn't implement `IStatable<,>`, this opens an empty `GenericCoreConfig`, which is dumb, but matches the existing behaviour + items.Add(CreateCoreSubmenu(VSystemCategory.Handhelds, CoreNames.Handy, CreateGenericCoreConfigItem(CoreNames.Handy))); // as Handy doesn't implement `ISettable<,>`, this opens an empty `GenericCoreConfig`, which is dumb, but matches the existing behaviour // HyperNyma items.Add(CreateCoreSubmenu(VSystemCategory.Consoles, CoreNames.HyperNyma, CreateGenericNymaCoreConfigItem(CoreNames.HyperNyma, HyperNyma.CachedSettingsInfo))); @@ -2852,7 +2852,7 @@ namespace BizHawk.Client.EmuHawk items.Add(CreateCoreSubmenu( VSystemCategory.Other, CoreNames.Libretro, - CreateGenericCoreConfigItem(CoreNames.Libretro))); // as Libretro doesn't implement `IStatable<,>`, this opens an empty `GenericCoreConfig`, which is dumb, but matches the existing behaviour + CreateGenericCoreConfigItem(CoreNames.Libretro))); // as Libretro doesn't implement `ISettable<,>`, this opens an empty `GenericCoreConfig`, which is dumb, but matches the existing behaviour // MAME var mameSettingsItem = CreateSettingsItem("Settings...", (_, _) => OpenGenericCoreConfig()); @@ -3016,7 +3016,7 @@ namespace BizHawk.Client.EmuHawk items.Add(CreateCoreSubmenu(VSystemCategory.Consoles, CoreNames.TurboNyma, CreateGenericNymaCoreConfigItem(CoreNames.TurboNyma, TurboNyma.CachedSettingsInfo))); // uzem - items.Add(CreateCoreSubmenu(VSystemCategory.Consoles, CoreNames.Uzem, CreateGenericCoreConfigItem(CoreNames.Uzem))); // as uzem doesn't implement `IStatable<,>`, this opens an empty `GenericCoreConfig`, which is dumb, but matches the existing behaviour + items.Add(CreateCoreSubmenu(VSystemCategory.Consoles, CoreNames.Uzem, CreateGenericCoreConfigItem(CoreNames.Uzem))); // as uzem doesn't implement `ISettable<,>`, this opens an empty `GenericCoreConfig`, which is dumb, but matches the existing behaviour // VectrexHawk items.Add(CreateCoreSubmenu(VSystemCategory.Consoles, CoreNames.VectrexHawk, CreateGenericCoreConfigItem(CoreNames.VectrexHawk))); diff --git a/src/BizHawk.Client.EmuHawk/OpenAdvancedChooser.cs b/src/BizHawk.Client.EmuHawk/OpenAdvancedChooser.cs index 3cc4c8feb9..99394c69ee 100644 --- a/src/BizHawk.Client.EmuHawk/OpenAdvancedChooser.cs +++ b/src/BizHawk.Client.EmuHawk/OpenAdvancedChooser.cs @@ -83,7 +83,7 @@ namespace BizHawk.Client.EmuHawk try { var coreComm = _createCoreComm(); - using var retro = new LibretroEmulator(coreComm, _game, core, true); + using var retro = new LibretroHost(coreComm, _game, core, true); btnLibretroLaunchGame.Enabled = true; if (retro.Description.SupportsNoGame) btnLibretroLaunchNoGame.Enabled = true; diff --git a/src/BizHawk.Emulation.Cores/Libretro/Libretro.Api.cs b/src/BizHawk.Emulation.Cores/Libretro/Libretro.Api.cs index 438f712397..038315cf95 100644 --- a/src/BizHawk.Emulation.Cores/Libretro/Libretro.Api.cs +++ b/src/BizHawk.Emulation.Cores/Libretro/Libretro.Api.cs @@ -359,10 +359,12 @@ namespace BizHawk.Emulation.Cores.Libretro public abstract uint retro_api_version(); [BizImport(cc)] - public abstract void retro_get_system_info(IntPtr retro_system_info); + public abstract void retro_get_system_info(out retro_system_info retro_system_info); + + // this is allowed to not initialize every variable, so ref is used instead of out [BizImport(cc)] - public abstract void retro_get_system_av_info(IntPtr retro_system_av_info); + public abstract void retro_get_system_av_info(ref retro_system_av_info retro_system_av_info); [BizImport(cc)] public abstract void retro_set_environment(IntPtr retro_environment); @@ -395,22 +397,27 @@ namespace BizHawk.Emulation.Cores.Libretro public abstract long retro_serialize_size(); [BizImport(cc)] - public abstract bool retro_serialize(IntPtr data, long size); + public abstract bool retro_serialize(byte[] data, long size); [BizImport(cc)] - public abstract bool retro_unserialize(IntPtr data, long size); + public abstract bool retro_unserialize(byte[] data, long size); [BizImport(cc)] public abstract void retro_cheat_reset(); [BizImport(cc)] - public abstract void retro_cheat_set(uint index, bool enabled, IntPtr code); + public abstract void retro_cheat_set(uint index, bool enabled, string code); + + // maybe it would be better if retro_game_info was just a class instead of a struct? + + [BizImport(cc, EntryPoint = "retro_load_game")] + public abstract bool retro_load_no_game(IntPtr no_game_info = default); // don't send anything here [BizImport(cc)] - public abstract bool retro_load_game(IntPtr retro_game_info); + public abstract bool retro_load_game(ref retro_game_info retro_game_info); [BizImport(cc)] - public abstract bool retro_load_game_special(uint game_type, IntPtr retro_game_info, long num_info); + public abstract bool retro_load_game_special(uint game_type, ref retro_game_info retro_game_info, long num_info); [BizImport(cc)] public abstract void retro_unload_game(); @@ -448,22 +455,22 @@ namespace BizHawk.Emulation.Cores.Libretro public abstract bool LibretroBridge_GetRetroTimingInfo(IntPtr cbHandler, ref LibretroApi.retro_system_timing t); [BizImport(cc)] - public abstract void LibretroBridge_GetRetroMessage(IntPtr cbHandler, ref LibretroApi.retro_message m); + public abstract void LibretroBridge_GetRetroMessage(IntPtr cbHandler, out LibretroApi.retro_message m); [BizImport(cc)] - public abstract void LibretroBridge_SetDirectories(IntPtr cbHandler, byte[] systemDirectory, byte[] saveDirectory, byte[] coreDirectory, byte[] coreAssetsDirectory); + public abstract void LibretroBridge_SetDirectories(IntPtr cbHandler, string systemDirectory, string saveDirectory, string coreDirectory, string coreAssetsDirectory); [BizImport(cc)] public abstract void LibretroBridge_SetVideoSize(IntPtr cbHandler, int sz); [BizImport(cc)] - public abstract void LibretroBridge_GetVideo(IntPtr cbHandler, ref int width, ref int height, int[] videoBuf); + public abstract void LibretroBridge_GetVideo(IntPtr cbHandler, out int width, out int height, int[] videoBuf); [BizImport(cc)] public abstract uint LibretroBridge_GetAudioSize(IntPtr cbHandler); [BizImport(cc)] - public abstract void LibretroBridge_GetAudio(IntPtr cbHandler, ref int numSamples, short[] sampleBuf); + public abstract void LibretroBridge_GetAudio(IntPtr cbHandler, out int numSamples, short[] sampleBuf); [BizImport(cc)] public abstract void LibretroBridge_SetInput(IntPtr cbHandler, LibretroApi.RETRO_DEVICE device, int port, short[] input); @@ -479,6 +486,6 @@ namespace BizHawk.Emulation.Cores.Libretro } [BizImport(cc)] - public abstract void LibretroBridge_GetRetroProcs(ref retro_procs cb_procs); + public abstract void LibretroBridge_GetRetroProcs(out retro_procs cb_procs); } } diff --git a/src/BizHawk.Emulation.Cores/Libretro/Libretro.Emulator.cs b/src/BizHawk.Emulation.Cores/Libretro/Libretro.Emulator.cs deleted file mode 100644 index a9ca7cdcc9..0000000000 --- a/src/BizHawk.Emulation.Cores/Libretro/Libretro.Emulator.cs +++ /dev/null @@ -1,385 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; - -using BizHawk.BizInvoke; -using BizHawk.Common; -using BizHawk.Emulation.Common; - -namespace BizHawk.Emulation.Cores.Libretro -{ - [PortedCore(CoreNames.Libretro, "CasualPokePlayer", singleInstance: true, isReleased: false)] - [ServiceNotApplicable(new[] { typeof(IDriveLight) })] - public partial class LibretroEmulator : IEmulator - { - private static readonly LibretroBridge bridge; - private static readonly LibretroBridge.retro_procs cb_procs; - - static LibretroEmulator() - { - var resolver = new DynamicLibraryImportResolver( - OSTailoredCode.IsUnixHost ? "libLibretroBridge.so" : "libLibretroBridge.dll", hasLimitedLifetime: false); - - bridge = BizInvoker.GetInvoker(resolver, CallingConventionAdapters.Native); - - cb_procs = new(); - bridge.LibretroBridge_GetRetroProcs(ref cb_procs); - } - - private readonly LibretroApi api; - - private readonly BasicServiceProvider _serviceProvider; - public IEmulatorServiceProvider ServiceProvider => _serviceProvider; - - private readonly IntPtr cbHandler; - - // please call this before calling any retro functions - private void UpdateCallbackHandler() - { - bridge.LibretroBridge_SetGlobalCallbackHandler(cbHandler); - } - - public LibretroEmulator(CoreComm comm, IGameInfo game, string corePath, bool analysis = false) - { - try - { - cbHandler = bridge.LibretroBridge_CreateCallbackHandler(); - - if (cbHandler == IntPtr.Zero) - { - throw new Exception("Failed to create callback handler!"); - } - - UpdateCallbackHandler(); - - api = BizInvoker.GetInvoker( - new DynamicLibraryImportResolver(corePath, hasLimitedLifetime: false), CallingConventionAdapters.Native); - - _serviceProvider = new(this); - Comm = comm; - - if (api.retro_api_version() != 1) - { - throw new InvalidOperationException("Unsupported Libretro API version (or major error in interop)"); - } - - var SystemDirectory = RetroString(Comm.CoreFileProvider.GetRetroSystemPath(game)); - var SaveDirectory = RetroString(Comm.CoreFileProvider.GetRetroSaveRAMDirectory(game)); - var CoreDirectory = RetroString(Path.GetDirectoryName(corePath)); - var CoreAssetsDirectory = RetroString(Path.GetDirectoryName(corePath)); - - bridge.LibretroBridge_SetDirectories(cbHandler, SystemDirectory, SaveDirectory, CoreDirectory, CoreAssetsDirectory); - - ControllerDefinition = ControllerDef; - - // check if we're just analysing the core and the core path matches the loaded core path anyways - if (analysis && corePath == LoadedCorePath) - { - Description = CalculateDescription(); - Description.SupportsNoGame = LoadedCoreSupportsNoGame; - // don't set init, we don't want the core deinit later - } - else - { - api.retro_set_environment(cb_procs.retro_environment_proc); - Description = CalculateDescription(); - } - - if (!analysis) - { - LoadedCorePath = corePath; - LoadedCoreSupportsNoGame = Description.SupportsNoGame; - } - } - catch - { - Dispose(); - throw; - } - } - - private class RetroData - { - private readonly GCHandle _handle; - - public IntPtr PinnedData => _handle.AddrOfPinnedObject(); - public long Length { get; } - - public RetroData(object o, long len = 0) - { - _handle = GCHandle.Alloc(o, GCHandleType.Pinned); - Length = len; - } - - ~RetroData() => _handle.Free(); - } - - private byte[] RetroString(string managedString) - { - var s = Encoding.UTF8.GetBytes(managedString); - var ret = new byte[s.Length + 1]; - Array.Copy(s, ret, s.Length); - ret[s.Length] = 0; - return ret; - } - - private LibretroApi.retro_system_av_info av_info; - - private bool inited = false; - - public void Dispose() - { - UpdateCallbackHandler(); - - if (inited) - { - api.retro_unload_game(); - api.retro_deinit(); - inited = false; - } - - bridge.LibretroBridge_DestroyCallbackHandler(cbHandler); - - _blipL?.Dispose(); - _blipR?.Dispose(); - } - - public RetroDescription Description { get; } - - // single instance hacks - private static string LoadedCorePath { get; set; } - private static bool LoadedCoreSupportsNoGame { get; set; } - - public enum RETRO_LOAD - { - DATA, - PATH, - NO_GAME, - } - - public bool LoadData(byte[] data, string id) => LoadHandler(RETRO_LOAD.DATA, new(RetroString(id)), new(data, data.LongLength)); - - public bool LoadPath(string path) => LoadHandler(RETRO_LOAD.PATH, new(RetroString(path))); - - public bool LoadNoGame() => LoadHandler(RETRO_LOAD.NO_GAME); - - private unsafe bool LoadHandler(RETRO_LOAD which, RetroData path = null, RetroData data = null) - { - UpdateCallbackHandler(); - - var game = new LibretroApi.retro_game_info(); - var gameptr = (IntPtr)(&game); - - if (which == RETRO_LOAD.NO_GAME) - { - gameptr = IntPtr.Zero; - } - else - { - game.path = path.PinnedData; - if (which == RETRO_LOAD.DATA) - { - game.data = data.PinnedData; - game.size = data.Length; - } - } - - api.retro_init(); - bool success = api.retro_load_game(gameptr); - if (!success) - { - api.retro_deinit(); - return false; - } - - var av = new LibretroApi.retro_system_av_info(); - api.retro_get_system_av_info((IntPtr)(&av)); - av_info = av; - - api.retro_set_video_refresh(cb_procs.retro_video_refresh_proc); - api.retro_set_audio_sample(cb_procs.retro_audio_sample_proc); - api.retro_set_audio_sample_batch(cb_procs.retro_audio_sample_batch_proc); - api.retro_set_input_poll(cb_procs.retro_input_poll_proc); - api.retro_set_input_state(cb_procs.retro_input_state_proc); - - _stateBuf = new byte[_stateLen = api.retro_serialize_size()]; - - _region = api.retro_get_region(); - - //this stuff can only happen after the game is loaded - - //allocate a video buffer which will definitely be large enough - InitVideoBuffer((int)av.geometry.base_width, (int)av.geometry.base_height, (int)(av.geometry.max_width * av.geometry.max_height)); - - // TODO: more precise - VsyncNumerator = (int)(10000000 * av.timing.fps); - VsyncDenominator = 10000000; - - SetupResampler(av.timing.fps, av.timing.sample_rate); - - InitMemoryDomains(); // im going to assume this should happen when a game is loaded - - inited = true; - - return true; - } - - private LibretroApi.retro_message retro_msg = new(); - - private CoreComm Comm { get; } - - private void FrameAdvancePrep(IController controller) - { - UpdateInput(controller); - - if (controller.IsPressed("Reset")) - { - api.retro_reset(); - } - } - - private void FrameAdvancePost(bool render, bool renderSound) - { - if (bridge.LibretroBridge_GetRetroGeometryInfo(cbHandler, ref av_info.geometry)) - { - vidBuffer = new int[av_info.geometry.max_width * av_info.geometry.max_height]; - } - - if (bridge.LibretroBridge_GetRetroTimingInfo(cbHandler, ref av_info.timing)) - { - VsyncNumerator = (int)(10000000 * av_info.timing.fps); - _blipL.SetRates(av_info.timing.sample_rate, 44100); - _blipR.SetRates(av_info.timing.sample_rate, 44100); - } - - if (render) - { - UpdateVideoBuffer(); - } - - ProcessSound(); - if (!renderSound) - { - DiscardSamples(); - } - - bridge.LibretroBridge_GetRetroMessage(cbHandler, ref retro_msg); - if (retro_msg.frames > 0) - { - Comm.Notify(Mershul.PtrToStringUtf8(retro_msg.msg)); - } - } - - public bool FrameAdvance(IController controller, bool render, bool renderSound = true) - { - UpdateCallbackHandler(); - - FrameAdvancePrep(controller); - api.retro_run(); - FrameAdvancePost(render, renderSound); - - Frame++; - - return true; - } - - private static readonly LibretroControllerDef ControllerDef = new(); - - public class LibretroControllerDef : ControllerDefinition - { - private const string CAT_KEYBOARD = "RetroKeyboard"; - - public const string PFX_RETROPAD = "RetroPad "; - - public LibretroControllerDef() - : base(name: "LibRetro Controls"/*for compatibility*/) - { - for (var player = 1; player <= 2; player++) foreach (var button in new[] { "Up", "Down", "Left", "Right", "Select", "Start", "Y", "B", "X", "A", "L", "R" }) - { - BoolButtons.Add($"P{player} {PFX_RETROPAD}{button}"); - } - - BoolButtons.Add("Pointer Pressed"); - this.AddXYPair("Pointer {0}", AxisPairOrientation.RightAndUp, (-32767).RangeTo(32767), 0); - - foreach (var s in new[] { - "Backspace", "Tab", "Clear", "Return", "Pause", "Escape", - "Space", "Exclaim", "QuoteDbl", "Hash", "Dollar", "Ampersand", "Quote", "LeftParen", "RightParen", "Asterisk", "Plus", "Comma", "Minus", "Period", "Slash", - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", - "Colon", "Semicolon", "Less", "Equals", "Greater", "Question", "At", "LeftBracket", "Backslash", "RightBracket", "Caret", "Underscore", "Backquote", - "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", - "Delete", - "KP0", "KP1", "KP2", "KP3", "KP4", "KP5", "KP6", "KP7", "KP8", "KP9", - "KP_Period", "KP_Divide", "KP_Multiply", "KP_Minus", "KP_Plus", "KP_Enter", "KP_Equals", - "Up", "Down", "Right", "Left", "Insert", "Home", "End", "PageUp", "PageDown", - "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14", "F15", - "NumLock", "CapsLock", "ScrollLock", "RShift", "LShift", "RCtrl", "LCtrl", "RAlt", "LAlt", "RMeta", "LMeta", "LSuper", "RSuper", "Mode", "Compose", - "Help", "Print", "SysReq", "Break", "Menu", "Power", "Euro", "Undo" - }) - { - var buttonName = $"Key {s}"; - BoolButtons.Add(buttonName); - CategoryLabels[buttonName] = CAT_KEYBOARD; - } - - BoolButtons.Add("Reset"); - - MakeImmutable(); - } - - protected override IReadOnlyList> GenOrderedControls() - { - // all this is to remove the keyboard buttons from P0 and put them in P3 so they appear at the end of the input display - var players = base.GenOrderedControls().ToList(); - List retroKeyboard = new(); - var p0 = (List) players[0]; - for (var i = 0; i < p0.Count; /* incremented in body */) - { - var buttonName = p0[i]; - if (CategoryLabels.TryGetValue(buttonName, out var v) && v is CAT_KEYBOARD) - { - retroKeyboard.Add(buttonName); - p0.RemoveAt(i); - } - else - { - i++; - } - } - players.Add(retroKeyboard); - return players; - } - } - - public ControllerDefinition ControllerDefinition { get; } - public int Frame { get; set; } - public string SystemId => VSystemID.Raw.Libretro; - public bool DeterministicEmulation => false; - - public void ResetCounters() - { - Frame = 0; - LagCount = 0; - IsLagFrame = false; - } - - public unsafe RetroDescription CalculateDescription() - { - UpdateCallbackHandler(); - - var descr = new RetroDescription(); - var sys_info = new LibretroApi.retro_system_info(); - api.retro_get_system_info((IntPtr)(&sys_info)); - descr.LibraryName = Mershul.PtrToStringUtf8(sys_info.library_name); - descr.LibraryVersion = Mershul.PtrToStringUtf8(sys_info.library_version); - descr.ValidExtensions = Mershul.PtrToStringUtf8(sys_info.valid_extensions); - descr.NeedsRomAsPath = sys_info.need_fullpath; - descr.NeedsArchives = sys_info.block_extract; - descr.SupportsNoGame = bridge.LibretroBridge_GetSupportsNoGame(cbHandler); - return descr; - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Libretro/Libretro.IEmulator.cs b/src/BizHawk.Emulation.Cores/Libretro/Libretro.IEmulator.cs new file mode 100644 index 0000000000..eb0f65a634 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Libretro/Libretro.IEmulator.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using BizHawk.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Libretro +{ + public partial class LibretroHost : IEmulator + { + private readonly BasicServiceProvider _serviceProvider; + public IEmulatorServiceProvider ServiceProvider => _serviceProvider; + + private LibretroApi.retro_message retro_msg = default; + + private readonly Action _notify; + + private void FrameAdvancePrep(IController controller) + { + UpdateInput(controller); + + if (controller.IsPressed("Reset")) + { + api.retro_reset(); + } + } + + private void FrameAdvancePost(bool render, bool renderSound) + { + if (bridge.LibretroBridge_GetRetroGeometryInfo(cbHandler, ref av_info.geometry)) + { + _vidBuffer = new int[av_info.geometry.max_width * av_info.geometry.max_height]; + } + + if (bridge.LibretroBridge_GetRetroTimingInfo(cbHandler, ref av_info.timing)) + { + VsyncNumerator = checked((int)(10000000 * av_info.timing.fps)); + _blipL.SetRates(av_info.timing.sample_rate, 44100); + _blipR.SetRates(av_info.timing.sample_rate, 44100); + } + + if (render) + { + UpdateVideoBuffer(); + } + + ProcessSound(); + if (!renderSound) + { + DiscardSamples(); + } + + bridge.LibretroBridge_GetRetroMessage(cbHandler, out retro_msg); + if (retro_msg.frames > 0) + { + // TODO: pass frames for duration? + _notify(Mershul.PtrToStringUtf8(retro_msg.msg)); + } + + Frame++; + } + + public bool FrameAdvance(IController controller, bool render, bool renderSound = true) + { + FrameAdvancePrep(controller); + api.retro_run(); + FrameAdvancePost(render, renderSound); + return true; + } + + private static readonly LibretroControllerDef ControllerDef = new(); + + public class LibretroControllerDef : ControllerDefinition + { + private const string CAT_KEYBOARD = "RetroKeyboard"; + + public const string PFX_RETROPAD = "RetroPad "; + + public LibretroControllerDef() + : base(name: "LibRetro Controls"/*for compatibility*/) + { + for (var player = 1; player <= 2; player++) foreach (var button in new[] { "Up", "Down", "Left", "Right", "Select", "Start", "Y", "B", "X", "A", "L", "R", "L2", "R2", "L3", "R3", }) + { + BoolButtons.Add($"P{player} {PFX_RETROPAD}{button}"); + } + + BoolButtons.Add("Pointer Pressed"); + this.AddXYPair("Pointer {0}", AxisPairOrientation.RightAndUp, (-32767).RangeTo(32767), 0); + + foreach (var s in new[] { + "Backspace", "Tab", "Clear", "Return", "Pause", "Escape", + "Space", "Exclaim", "QuoteDbl", "Hash", "Dollar", "Ampersand", "Quote", "LeftParen", "RightParen", "Asterisk", "Plus", "Comma", "Minus", "Period", "Slash", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + "Colon", "Semicolon", "Less", "Equals", "Greater", "Question", "At", "LeftBracket", "Backslash", "RightBracket", "Caret", "Underscore", "Backquote", + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", + "Delete", + "KP0", "KP1", "KP2", "KP3", "KP4", "KP5", "KP6", "KP7", "KP8", "KP9", + "KP_Period", "KP_Divide", "KP_Multiply", "KP_Minus", "KP_Plus", "KP_Enter", "KP_Equals", + "Up", "Down", "Right", "Left", "Insert", "Home", "End", "PageUp", "PageDown", + "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14", "F15", + "NumLock", "CapsLock", "ScrollLock", "RShift", "LShift", "RCtrl", "LCtrl", "RAlt", "LAlt", "RMeta", "LMeta", "LSuper", "RSuper", "Mode", "Compose", + "Help", "Print", "SysReq", "Break", "Menu", "Power", "Euro", "Undo" + }) + { + var buttonName = $"Key {s}"; + BoolButtons.Add(buttonName); + CategoryLabels[buttonName] = CAT_KEYBOARD; + } + + BoolButtons.Add("Reset"); + + MakeImmutable(); + } + + protected override IReadOnlyList> GenOrderedControls() + { + // all this is to remove the keyboard buttons from P0 and put them in P3 so they appear at the end of the input display + var players = base.GenOrderedControls().ToList(); + List retroKeyboard = new(); + var p0 = (List) players[0]; + for (var i = 0; i < p0.Count; /* incremented in body */) + { + var buttonName = p0[i]; + if (CategoryLabels.TryGetValue(buttonName, out var v) && v is CAT_KEYBOARD) + { + retroKeyboard.Add(buttonName); + p0.RemoveAt(i); + } + else + { + i++; + } + } + players.Add(retroKeyboard); + return players; + } + } + + public ControllerDefinition ControllerDefinition { get; } + public int Frame { get; set; } + public string SystemId => VSystemID.Raw.Libretro; + public bool DeterministicEmulation => false; + + public void ResetCounters() + { + Frame = 0; + LagCount = 0; + IsLagFrame = false; + } + + private bool inited = false; + + public void Dispose() + { + if (inited) + { + api.retro_unload_game(); + api.retro_deinit(); + inited = false; + } + + bridge.LibretroBridge_DestroyCallbackHandler(cbHandler); + + _blipL?.Dispose(); + _blipL = null; + _blipR?.Dispose(); + _blipR = null; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Libretro/Libretro.IInputPollable.cs b/src/BizHawk.Emulation.Cores/Libretro/Libretro.IInputPollable.cs index c0e8d91a31..4602b3c836 100644 --- a/src/BizHawk.Emulation.Cores/Libretro/Libretro.IInputPollable.cs +++ b/src/BizHawk.Emulation.Cores/Libretro/Libretro.IInputPollable.cs @@ -1,248 +1,249 @@ using System; using BizHawk.Emulation.Common; +using static BizHawk.Emulation.Cores.Libretro.LibretroApi; namespace BizHawk.Emulation.Cores.Libretro { - public partial class LibretroEmulator : IInputPollable + public partial class LibretroHost : IInputPollable { + // TBD + // we could actually remove IInputPollable here + // although that would prevent tastudio use entirely + // maybe better overall as libretro has no place for movies + public int LagCount { get; set; } public bool IsLagFrame { get; set; } [FeatureNotImplemented] public IInputCallbackSystem InputCallbacks => throw new NotImplementedException(); - // todo: make this better + private readonly short[] _joypad0States = new short[(int)RETRO_DEVICE_ID_JOYPAD.LAST]; + private readonly short[] _joypad1States = new short[(int)RETRO_DEVICE_ID_JOYPAD.LAST]; + private readonly short[] _pointerStates = new short[(int)RETRO_DEVICE_ID_POINTER.LAST]; + private readonly short[] _keyStates = new short[(int)RETRO_KEY.LAST]; + + // todo + // implement more input types + // limit inputs according to user selection / core limitations (something with RETRO_ENVIRONMENT_SET_CONTROLLER_INFO?) void UpdateInput(IController controller) { - short[] input = new short[(int)LibretroApi.RETRO_DEVICE_ID_JOYPAD.LAST]; - // joypad port 0 - for (uint i = 0; i < input.Length; i++) + SetInputs(controller, RETRO_DEVICE.JOYPAD, 0, _joypad0States); + SetInputs(controller, RETRO_DEVICE.JOYPAD, 1, _joypad1States); + SetInputs(controller, RETRO_DEVICE.POINTER, 0, _pointerStates); + SetInputs(controller, RETRO_DEVICE.KEYBOARD, 0, _keyStates); + } + + private void SetInputs(IController controller, RETRO_DEVICE device, int port, short[] inputBuffer) + { + // index is 0 always except for ANALOG devices, which we don't handle yet (impl TBD) + for (int i = 0; i < inputBuffer.Length; i++) { - input[i] = retro_input_state(controller, 0, (uint)LibretroApi.RETRO_DEVICE.JOYPAD, 0, i); + inputBuffer[i] = InputState(controller, port, device, 0, i); } - bridge.LibretroBridge_SetInput(cbHandler, LibretroApi.RETRO_DEVICE.JOYPAD, 0, input); - // joypad port 1 - for (uint i = 0; i < input.Length; i++) - { - input[i] = retro_input_state(controller, 1, (uint)LibretroApi.RETRO_DEVICE.JOYPAD, 0, i); - } - bridge.LibretroBridge_SetInput(cbHandler, LibretroApi.RETRO_DEVICE.JOYPAD, 1, input); - input = new short[(int)LibretroApi.RETRO_DEVICE_ID_POINTER.LAST]; - // pointer port 0 - for (uint i = 0; i < input.Length; i++) - { - input[i] = retro_input_state(controller, 0, (uint)LibretroApi.RETRO_DEVICE.POINTER, 0, i); - } - bridge.LibretroBridge_SetInput(cbHandler, LibretroApi.RETRO_DEVICE.POINTER, 0, input); - input = new short[(int)LibretroApi.RETRO_KEY.LAST]; - // keyboard port 0 - for (uint i = 0; i < input.Length; i++) - { - input[i] = retro_input_state(controller, 0, (uint)LibretroApi.RETRO_DEVICE.KEYBOARD, 0, i); - } - bridge.LibretroBridge_SetInput(cbHandler, LibretroApi.RETRO_DEVICE.KEYBOARD, 0, input); + bridge.LibretroBridge_SetInput(cbHandler, device, port, inputBuffer); } //meanings (they are kind of hazy, but once we're done implementing this it will be completely defined by example) //port = console physical port? //device = logical device type - //index = sub device index? (multitap?) + //index = sub device index? (multitap?) (only actually used for the ANALOG device however?) //id = button id (or key id) - private static short retro_input_state(IController controller, uint port, uint device, uint index, uint id) + private static short InputState(IController controller, int port, RETRO_DEVICE device, int index, int id) { - //helpful debugging - //Console.WriteLine("{0} {1} {2} {3}", port, device, index, id); - - switch ((LibretroApi.RETRO_DEVICE)device) + switch (device) { - case LibretroApi.RETRO_DEVICE.POINTER: + case RETRO_DEVICE.POINTER: { - return (LibretroApi.RETRO_DEVICE_ID_POINTER)id switch + return (RETRO_DEVICE_ID_POINTER)id switch { - LibretroApi.RETRO_DEVICE_ID_POINTER.X => (short)controller.AxisValue("Pointer X"), - LibretroApi.RETRO_DEVICE_ID_POINTER.Y => (short)controller.AxisValue("Pointer Y"), - LibretroApi.RETRO_DEVICE_ID_POINTER.PRESSED => (short)(controller.IsPressed("Pointer Pressed") ? 1 : 0), - _ => 0, + RETRO_DEVICE_ID_POINTER.X => (short)controller.AxisValue("Pointer X"), + RETRO_DEVICE_ID_POINTER.Y => (short)controller.AxisValue("Pointer Y"), + RETRO_DEVICE_ID_POINTER.PRESSED => (short)(controller.IsPressed("Pointer Pressed") ? 1 : 0), + RETRO_DEVICE_ID_POINTER.COUNT => (short)(controller.IsPressed("Pointer Pressed") ? 1 : 0), // i think this means "number of presses"? we don't support multitouch anyways so + _ => throw new InvalidOperationException($"Invalid {nameof(RETRO_DEVICE_ID_POINTER)}") }; } - case LibretroApi.RETRO_DEVICE.KEYBOARD: + case RETRO_DEVICE.KEYBOARD: { - string button = (LibretroApi.RETRO_KEY)id switch + var button = (RETRO_KEY)id switch { - LibretroApi.RETRO_KEY.BACKSPACE => "Backspace", - LibretroApi.RETRO_KEY.TAB => "Tab", - LibretroApi.RETRO_KEY.CLEAR => "Clear", - LibretroApi.RETRO_KEY.RETURN => "Return", - LibretroApi.RETRO_KEY.PAUSE => "Pause", - LibretroApi.RETRO_KEY.ESCAPE => "Escape", - LibretroApi.RETRO_KEY.SPACE => "Space", - LibretroApi.RETRO_KEY.EXCLAIM => "Exclaim", - LibretroApi.RETRO_KEY.QUOTEDBL => "QuoteDbl", - LibretroApi.RETRO_KEY.HASH => "Hash", - LibretroApi.RETRO_KEY.DOLLAR => "Dollar", - LibretroApi.RETRO_KEY.AMPERSAND => "Ampersand", - LibretroApi.RETRO_KEY.QUOTE => "Quote", - LibretroApi.RETRO_KEY.LEFTPAREN => "LeftParen", - LibretroApi.RETRO_KEY.RIGHTPAREN => "RightParen", - LibretroApi.RETRO_KEY.ASTERISK => "Asterisk", - LibretroApi.RETRO_KEY.PLUS => "Plus", - LibretroApi.RETRO_KEY.COMMA => "Comma", - LibretroApi.RETRO_KEY.MINUS => "Minus", - LibretroApi.RETRO_KEY.PERIOD => "Period", - LibretroApi.RETRO_KEY.SLASH => "Slash", - LibretroApi.RETRO_KEY._0 => "0", - LibretroApi.RETRO_KEY._1 => "1", - LibretroApi.RETRO_KEY._2 => "2", - LibretroApi.RETRO_KEY._3 => "3", - LibretroApi.RETRO_KEY._4 => "4", - LibretroApi.RETRO_KEY._5 => "5", - LibretroApi.RETRO_KEY._6 => "6", - LibretroApi.RETRO_KEY._7 => "7", - LibretroApi.RETRO_KEY._8 => "8", - LibretroApi.RETRO_KEY._9 => "9", - LibretroApi.RETRO_KEY.COLON => "Colon", - LibretroApi.RETRO_KEY.SEMICOLON => "Semicolon", - LibretroApi.RETRO_KEY.LESS => "Less", - LibretroApi.RETRO_KEY.EQUALS => "Equals", - LibretroApi.RETRO_KEY.GREATER => "Greater", - LibretroApi.RETRO_KEY.QUESTION => "Question", - LibretroApi.RETRO_KEY.AT => "At", - LibretroApi.RETRO_KEY.LEFTBRACKET => "LeftBracket", - LibretroApi.RETRO_KEY.BACKSLASH => "Backslash", - LibretroApi.RETRO_KEY.RIGHTBRACKET => "RightBracket", - LibretroApi.RETRO_KEY.CARET => "Caret", - LibretroApi.RETRO_KEY.UNDERSCORE => "Underscore", - LibretroApi.RETRO_KEY.BACKQUOTE => "Backquote", - LibretroApi.RETRO_KEY.a => "A", - LibretroApi.RETRO_KEY.b => "B", - LibretroApi.RETRO_KEY.c => "C", - LibretroApi.RETRO_KEY.d => "D", - LibretroApi.RETRO_KEY.e => "E", - LibretroApi.RETRO_KEY.f => "F", - LibretroApi.RETRO_KEY.g => "G", - LibretroApi.RETRO_KEY.h => "H", - LibretroApi.RETRO_KEY.i => "I", - LibretroApi.RETRO_KEY.j => "J", - LibretroApi.RETRO_KEY.k => "K", - LibretroApi.RETRO_KEY.l => "L", - LibretroApi.RETRO_KEY.m => "M", - LibretroApi.RETRO_KEY.n => "N", - LibretroApi.RETRO_KEY.o => "O", - LibretroApi.RETRO_KEY.p => "P", - LibretroApi.RETRO_KEY.q => "Q", - LibretroApi.RETRO_KEY.r => "R", - LibretroApi.RETRO_KEY.s => "S", - LibretroApi.RETRO_KEY.t => "T", - LibretroApi.RETRO_KEY.u => "U", - LibretroApi.RETRO_KEY.v => "V", - LibretroApi.RETRO_KEY.w => "W", - LibretroApi.RETRO_KEY.x => "X", - LibretroApi.RETRO_KEY.y => "Y", - LibretroApi.RETRO_KEY.z => "Z", - LibretroApi.RETRO_KEY.DELETE => "Delete", - LibretroApi.RETRO_KEY.KP0 => "KP0", - LibretroApi.RETRO_KEY.KP1 => "KP1", - LibretroApi.RETRO_KEY.KP2 => "KP2", - LibretroApi.RETRO_KEY.KP3 => "KP3", - LibretroApi.RETRO_KEY.KP4 => "KP4", - LibretroApi.RETRO_KEY.KP5 => "KP5", - LibretroApi.RETRO_KEY.KP6 => "KP6", - LibretroApi.RETRO_KEY.KP7 => "KP7", - LibretroApi.RETRO_KEY.KP8 => "KP8", - LibretroApi.RETRO_KEY.KP9 => "KP9", - LibretroApi.RETRO_KEY.KP_PERIOD => "KP_Period", - LibretroApi.RETRO_KEY.KP_DIVIDE => "KP_Divide", - LibretroApi.RETRO_KEY.KP_MULTIPLY => "KP_Multiply", - LibretroApi.RETRO_KEY.KP_MINUS => "KP_Minus", - LibretroApi.RETRO_KEY.KP_PLUS => "KP_Plus", - LibretroApi.RETRO_KEY.KP_ENTER => "KP_Enter", - LibretroApi.RETRO_KEY.KP_EQUALS => "KP_Equals", - LibretroApi.RETRO_KEY.UP => "Up", - LibretroApi.RETRO_KEY.DOWN => "Down", - LibretroApi.RETRO_KEY.RIGHT => "Right", - LibretroApi.RETRO_KEY.LEFT => "Left", - LibretroApi.RETRO_KEY.INSERT => "Insert", - LibretroApi.RETRO_KEY.HOME => "Home", - LibretroApi.RETRO_KEY.END => "End", - LibretroApi.RETRO_KEY.PAGEUP => "PageUp", - LibretroApi.RETRO_KEY.PAGEDOWN => "PageDown", - LibretroApi.RETRO_KEY.F1 => "F1", - LibretroApi.RETRO_KEY.F2 => "F2", - LibretroApi.RETRO_KEY.F3 => "F3", - LibretroApi.RETRO_KEY.F4 => "F4", - LibretroApi.RETRO_KEY.F5 => "F5", - LibretroApi.RETRO_KEY.F6 => "F6", - LibretroApi.RETRO_KEY.F7 => "F7", - LibretroApi.RETRO_KEY.F8 => "F8", - LibretroApi.RETRO_KEY.F9 => "F9", - LibretroApi.RETRO_KEY.F10 => "F10", - LibretroApi.RETRO_KEY.F11 => "F11", - LibretroApi.RETRO_KEY.F12 => "F12", - LibretroApi.RETRO_KEY.F13 => "F13", - LibretroApi.RETRO_KEY.F14 => "F14", - LibretroApi.RETRO_KEY.F15 => "F15", - LibretroApi.RETRO_KEY.NUMLOCK => "NumLock", - LibretroApi.RETRO_KEY.CAPSLOCK => "CapsLock", - LibretroApi.RETRO_KEY.SCROLLOCK => "ScrollLock", - LibretroApi.RETRO_KEY.RSHIFT => "RShift", - LibretroApi.RETRO_KEY.LSHIFT => "LShift", - LibretroApi.RETRO_KEY.RCTRL => "RCtrl", - LibretroApi.RETRO_KEY.LCTRL => "LCtrl", - LibretroApi.RETRO_KEY.RALT => "RAlt", - LibretroApi.RETRO_KEY.LALT => "LAlt", - LibretroApi.RETRO_KEY.RMETA => "RMeta", - LibretroApi.RETRO_KEY.LMETA => "LMeta", - LibretroApi.RETRO_KEY.LSUPER => "LSuper", - LibretroApi.RETRO_KEY.RSUPER => "RSuper", - LibretroApi.RETRO_KEY.MODE => "Mode", - LibretroApi.RETRO_KEY.COMPOSE => "Compose", - LibretroApi.RETRO_KEY.HELP => "Help", - LibretroApi.RETRO_KEY.PRINT => "Print", - LibretroApi.RETRO_KEY.SYSREQ => "SysReq", - LibretroApi.RETRO_KEY.BREAK => "Break", - LibretroApi.RETRO_KEY.MENU => "Menu", - LibretroApi.RETRO_KEY.POWER => "Power", - LibretroApi.RETRO_KEY.EURO => "Euro", - LibretroApi.RETRO_KEY.UNDO => "Undo", - _ => "", + RETRO_KEY.BACKSPACE => "Backspace", + RETRO_KEY.TAB => "Tab", + RETRO_KEY.CLEAR => "Clear", + RETRO_KEY.RETURN => "Return", + RETRO_KEY.PAUSE => "Pause", + RETRO_KEY.ESCAPE => "Escape", + RETRO_KEY.SPACE => "Space", + RETRO_KEY.EXCLAIM => "Exclaim", + RETRO_KEY.QUOTEDBL => "QuoteDbl", + RETRO_KEY.HASH => "Hash", + RETRO_KEY.DOLLAR => "Dollar", + RETRO_KEY.AMPERSAND => "Ampersand", + RETRO_KEY.QUOTE => "Quote", + RETRO_KEY.LEFTPAREN => "LeftParen", + RETRO_KEY.RIGHTPAREN => "RightParen", + RETRO_KEY.ASTERISK => "Asterisk", + RETRO_KEY.PLUS => "Plus", + RETRO_KEY.COMMA => "Comma", + RETRO_KEY.MINUS => "Minus", + RETRO_KEY.PERIOD => "Period", + RETRO_KEY.SLASH => "Slash", + RETRO_KEY._0 => "0", + RETRO_KEY._1 => "1", + RETRO_KEY._2 => "2", + RETRO_KEY._3 => "3", + RETRO_KEY._4 => "4", + RETRO_KEY._5 => "5", + RETRO_KEY._6 => "6", + RETRO_KEY._7 => "7", + RETRO_KEY._8 => "8", + RETRO_KEY._9 => "9", + RETRO_KEY.COLON => "Colon", + RETRO_KEY.SEMICOLON => "Semicolon", + RETRO_KEY.LESS => "Less", + RETRO_KEY.EQUALS => "Equals", + RETRO_KEY.GREATER => "Greater", + RETRO_KEY.QUESTION => "Question", + RETRO_KEY.AT => "At", + RETRO_KEY.LEFTBRACKET => "LeftBracket", + RETRO_KEY.BACKSLASH => "Backslash", + RETRO_KEY.RIGHTBRACKET => "RightBracket", + RETRO_KEY.CARET => "Caret", + RETRO_KEY.UNDERSCORE => "Underscore", + RETRO_KEY.BACKQUOTE => "Backquote", + RETRO_KEY.a => "A", + RETRO_KEY.b => "B", + RETRO_KEY.c => "C", + RETRO_KEY.d => "D", + RETRO_KEY.e => "E", + RETRO_KEY.f => "F", + RETRO_KEY.g => "G", + RETRO_KEY.h => "H", + RETRO_KEY.i => "I", + RETRO_KEY.j => "J", + RETRO_KEY.k => "K", + RETRO_KEY.l => "L", + RETRO_KEY.m => "M", + RETRO_KEY.n => "N", + RETRO_KEY.o => "O", + RETRO_KEY.p => "P", + RETRO_KEY.q => "Q", + RETRO_KEY.r => "R", + RETRO_KEY.s => "S", + RETRO_KEY.t => "T", + RETRO_KEY.u => "U", + RETRO_KEY.v => "V", + RETRO_KEY.w => "W", + RETRO_KEY.x => "X", + RETRO_KEY.y => "Y", + RETRO_KEY.z => "Z", + RETRO_KEY.DELETE => "Delete", + RETRO_KEY.KP0 => "KP0", + RETRO_KEY.KP1 => "KP1", + RETRO_KEY.KP2 => "KP2", + RETRO_KEY.KP3 => "KP3", + RETRO_KEY.KP4 => "KP4", + RETRO_KEY.KP5 => "KP5", + RETRO_KEY.KP6 => "KP6", + RETRO_KEY.KP7 => "KP7", + RETRO_KEY.KP8 => "KP8", + RETRO_KEY.KP9 => "KP9", + RETRO_KEY.KP_PERIOD => "KP_Period", + RETRO_KEY.KP_DIVIDE => "KP_Divide", + RETRO_KEY.KP_MULTIPLY => "KP_Multiply", + RETRO_KEY.KP_MINUS => "KP_Minus", + RETRO_KEY.KP_PLUS => "KP_Plus", + RETRO_KEY.KP_ENTER => "KP_Enter", + RETRO_KEY.KP_EQUALS => "KP_Equals", + RETRO_KEY.UP => "Up", + RETRO_KEY.DOWN => "Down", + RETRO_KEY.RIGHT => "Right", + RETRO_KEY.LEFT => "Left", + RETRO_KEY.INSERT => "Insert", + RETRO_KEY.HOME => "Home", + RETRO_KEY.END => "End", + RETRO_KEY.PAGEUP => "PageUp", + RETRO_KEY.PAGEDOWN => "PageDown", + RETRO_KEY.F1 => "F1", + RETRO_KEY.F2 => "F2", + RETRO_KEY.F3 => "F3", + RETRO_KEY.F4 => "F4", + RETRO_KEY.F5 => "F5", + RETRO_KEY.F6 => "F6", + RETRO_KEY.F7 => "F7", + RETRO_KEY.F8 => "F8", + RETRO_KEY.F9 => "F9", + RETRO_KEY.F10 => "F10", + RETRO_KEY.F11 => "F11", + RETRO_KEY.F12 => "F12", + RETRO_KEY.F13 => "F13", + RETRO_KEY.F14 => "F14", + RETRO_KEY.F15 => "F15", + RETRO_KEY.NUMLOCK => "NumLock", + RETRO_KEY.CAPSLOCK => "CapsLock", + RETRO_KEY.SCROLLOCK => "ScrollLock", + RETRO_KEY.RSHIFT => "RShift", + RETRO_KEY.LSHIFT => "LShift", + RETRO_KEY.RCTRL => "RCtrl", + RETRO_KEY.LCTRL => "LCtrl", + RETRO_KEY.RALT => "RAlt", + RETRO_KEY.LALT => "LAlt", + RETRO_KEY.RMETA => "RMeta", + RETRO_KEY.LMETA => "LMeta", + RETRO_KEY.LSUPER => "LSuper", + RETRO_KEY.RSUPER => "RSuper", + RETRO_KEY.MODE => "Mode", + RETRO_KEY.COMPOSE => "Compose", + RETRO_KEY.HELP => "Help", + RETRO_KEY.PRINT => "Print", + RETRO_KEY.SYSREQ => "SysReq", + RETRO_KEY.BREAK => "Break", + RETRO_KEY.MENU => "Menu", + RETRO_KEY.POWER => "Power", + RETRO_KEY.EURO => "Euro", + RETRO_KEY.UNDO => "Undo", + _ => "", // annoyingly a lot of gaps are present in RETRO_KEY, so can't just throw here }; return (short)(controller.IsPressed("Key " + button) ? 1 : 0); } - case LibretroApi.RETRO_DEVICE.JOYPAD: + case RETRO_DEVICE.JOYPAD: { - string button = (LibretroApi.RETRO_DEVICE_ID_JOYPAD)id switch + var button = (RETRO_DEVICE_ID_JOYPAD)id switch { - LibretroApi.RETRO_DEVICE_ID_JOYPAD.A => "A", - LibretroApi.RETRO_DEVICE_ID_JOYPAD.B => "B", - LibretroApi.RETRO_DEVICE_ID_JOYPAD.X => "X", - LibretroApi.RETRO_DEVICE_ID_JOYPAD.Y => "Y", - LibretroApi.RETRO_DEVICE_ID_JOYPAD.UP => "Up", - LibretroApi.RETRO_DEVICE_ID_JOYPAD.DOWN => "Down", - LibretroApi.RETRO_DEVICE_ID_JOYPAD.LEFT => "Left", - LibretroApi.RETRO_DEVICE_ID_JOYPAD.RIGHT => "Right", - LibretroApi.RETRO_DEVICE_ID_JOYPAD.L => "L", - LibretroApi.RETRO_DEVICE_ID_JOYPAD.R => "R", - LibretroApi.RETRO_DEVICE_ID_JOYPAD.SELECT => "Select", - LibretroApi.RETRO_DEVICE_ID_JOYPAD.START => "Start", - _ => "", + RETRO_DEVICE_ID_JOYPAD.A => "A", + RETRO_DEVICE_ID_JOYPAD.B => "B", + RETRO_DEVICE_ID_JOYPAD.X => "X", + RETRO_DEVICE_ID_JOYPAD.Y => "Y", + RETRO_DEVICE_ID_JOYPAD.UP => "Up", + RETRO_DEVICE_ID_JOYPAD.DOWN => "Down", + RETRO_DEVICE_ID_JOYPAD.LEFT => "Left", + RETRO_DEVICE_ID_JOYPAD.RIGHT => "Right", + RETRO_DEVICE_ID_JOYPAD.L => "L", + RETRO_DEVICE_ID_JOYPAD.R => "R", + RETRO_DEVICE_ID_JOYPAD.SELECT => "Select", + RETRO_DEVICE_ID_JOYPAD.START => "Start", + RETRO_DEVICE_ID_JOYPAD.L2 => "L2", + RETRO_DEVICE_ID_JOYPAD.R2 => "R2", + RETRO_DEVICE_ID_JOYPAD.L3 => "L3", + RETRO_DEVICE_ID_JOYPAD.R3 => "R3", + _ => throw new InvalidOperationException($"Invalid {nameof(RETRO_DEVICE_ID_JOYPAD)}"), }; return (short)(GetButton(controller, port + 1, "RetroPad", button) ? 1 : 0); } default: - return 0; + throw new InvalidOperationException($"Invalid or unimplemented {nameof(RETRO_DEVICE)}"); } } - private static bool GetButton(IController controller, uint pnum, string type, string button) + private static bool GetButton(IController controller, int pnum, string type, string button) { - string key = $"P{pnum} {type} {button}"; + var key = $"P{pnum} {type} {button}"; return controller.IsPressed(key); } - } } diff --git a/src/BizHawk.Emulation.Cores/Libretro/Libretro.IMemoryDomains.cs b/src/BizHawk.Emulation.Cores/Libretro/Libretro.IMemoryDomains.cs index 76e28c0f6b..1f26a62054 100644 --- a/src/BizHawk.Emulation.Cores/Libretro/Libretro.IMemoryDomains.cs +++ b/src/BizHawk.Emulation.Cores/Libretro/Libretro.IMemoryDomains.cs @@ -5,14 +5,22 @@ using BizHawk.Emulation.Common; namespace BizHawk.Emulation.Cores.Libretro { - public partial class LibretroEmulator + public partial class LibretroHost { - private readonly List _memoryDomains = new(); - private IMemoryDomains MemoryDomains { get; set; } + private MemoryDomainList _memoryDomains; + + private static readonly IReadOnlyDictionary _domainNames + = new Dictionary() + { + [LibretroApi.RETRO_MEMORY.SAVE_RAM] = "SaveRAM", + [LibretroApi.RETRO_MEMORY.RTC] = "RTC", + [LibretroApi.RETRO_MEMORY.SYSTEM_RAM] = "RAM", + [LibretroApi.RETRO_MEMORY.VIDEO_RAM] = "VRAM", + }; private void InitMemoryDomains() { - UpdateCallbackHandler(); + List md = new(); foreach (LibretroApi.RETRO_MEMORY m in Enum.GetValues(typeof(LibretroApi.RETRO_MEMORY))) { @@ -20,8 +28,9 @@ namespace BizHawk.Emulation.Cores.Libretro var sz = api.retro_get_memory_size(m); if (mem != IntPtr.Zero && sz > 0) { - var d = new MemoryDomainIntPtr(Enum.GetName(m.GetType(), m), MemoryDomain.Endian.Little, mem, sz, true, 1); - _memoryDomains.Add(d); + MemoryDomainIntPtr d = new(_domainNames[m], MemoryDomain.Endian.Unknown, mem, sz, true, 1); + md.Add(d); + if (m is LibretroApi.RETRO_MEMORY.SAVE_RAM or LibretroApi.RETRO_MEMORY.RTC) { _saveramAreas.Add(d); @@ -30,8 +39,19 @@ namespace BizHawk.Emulation.Cores.Libretro } } - MemoryDomains = new MemoryDomainList(_memoryDomains); - _serviceProvider.Register(MemoryDomains); + // no domains to register... + if (md.Count == 0) + { + return; + } + + _memoryDomains = new(md); + // if RAM is somehow not available, _memoryDomains["RAM"] just returns null (effective no-op) + // what is considered main memory then is whatever was added first + // priority implicitly then going to RETRO_MEMORY ordering (so SaveRAM->RTC->VRAM) + _memoryDomains.MainMemory = _memoryDomains["RAM"]; + + _serviceProvider.Register(_memoryDomains); } } } diff --git a/src/BizHawk.Emulation.Cores/Libretro/Libretro.IRegionable.cs b/src/BizHawk.Emulation.Cores/Libretro/Libretro.IRegionable.cs index bc97a67ac3..769ddbf005 100644 --- a/src/BizHawk.Emulation.Cores/Libretro/Libretro.IRegionable.cs +++ b/src/BizHawk.Emulation.Cores/Libretro/Libretro.IRegionable.cs @@ -2,7 +2,7 @@ using BizHawk.Emulation.Common; namespace BizHawk.Emulation.Cores.Libretro { - public partial class LibretroEmulator : IRegionable + public partial class LibretroHost : IRegionable { private LibretroApi.RETRO_REGION _region = LibretroApi.RETRO_REGION.NTSC; diff --git a/src/BizHawk.Emulation.Cores/Libretro/Libretro.ISaveRam.cs b/src/BizHawk.Emulation.Cores/Libretro/Libretro.ISaveRam.cs index 4f61a75d28..f72e54b3b9 100644 --- a/src/BizHawk.Emulation.Cores/Libretro/Libretro.ISaveRam.cs +++ b/src/BizHawk.Emulation.Cores/Libretro/Libretro.ISaveRam.cs @@ -5,7 +5,7 @@ using BizHawk.Emulation.Common; namespace BizHawk.Emulation.Cores.Libretro { - public partial class LibretroEmulator : ISaveRam + public partial class LibretroHost : ISaveRam { private readonly List _saveramAreas = new(); private long _saveramSize = 0; diff --git a/src/BizHawk.Emulation.Cores/Libretro/Libretro.ISoundProvider.cs b/src/BizHawk.Emulation.Cores/Libretro/Libretro.ISoundProvider.cs index 98f7a5bafd..e2557be88d 100644 --- a/src/BizHawk.Emulation.Cores/Libretro/Libretro.ISoundProvider.cs +++ b/src/BizHawk.Emulation.Cores/Libretro/Libretro.ISoundProvider.cs @@ -4,28 +4,24 @@ using BizHawk.Emulation.Common; namespace BizHawk.Emulation.Cores.Libretro { - public partial class LibretroEmulator : ISoundProvider + public partial class LibretroHost : ISoundProvider { - private BlipBuffer _blipL; - private BlipBuffer _blipR; + private const int OUT_SAMPLE_RATE = 44100; - private short[] _inSampBuf = new short[0]; - private short[] _outSampBuf = new short[0]; + private BlipBuffer _blipL, _blipR; + private int _latchL, _latchR; + + private short[] _inSampBuf = Array.Empty(); // variable size, will grow as needed + + private readonly short[] _outSampBuf = new short[OUT_SAMPLE_RATE * 2]; // big enough private int _outSamps; - private int _latchL = 0; - private int _latchR = 0; - - private void SetupResampler(double fps, double sps) + private void SetupResampler(double sps) { - Console.WriteLine("FPS {0} SPS {1}", fps, sps); - - _outSampBuf = new short[44100]; // big enough - - _blipL = new BlipBuffer(44100); - _blipL.SetRates(sps, 44100); - _blipR = new BlipBuffer(44100); - _blipR.SetRates(sps, 44100); + _blipL = new(OUT_SAMPLE_RATE); + _blipL.SetRates(sps, OUT_SAMPLE_RATE); + _blipR = new(OUT_SAMPLE_RATE); + _blipR.SetRates(sps, OUT_SAMPLE_RATE); } private void ProcessSound() @@ -35,30 +31,44 @@ namespace BizHawk.Emulation.Cores.Libretro { return; } + + // skip resampling if in sample rate == out sample rate + if (av_info.timing.sample_rate == OUT_SAMPLE_RATE) + { + if (len > (OUT_SAMPLE_RATE * 2)) + { + throw new Exception("Audio buffer overflow!"); + } + + // copy directly to our output buffer + bridge.LibretroBridge_GetAudio(cbHandler, out _outSamps, _outSampBuf); + return; + } + if (len > _inSampBuf.Length) { _inSampBuf = new short[len]; } - var ns = 0; - bridge.LibretroBridge_GetAudio(cbHandler, ref ns, _inSampBuf); + + bridge.LibretroBridge_GetAudio(cbHandler, out var ns, _inSampBuf); for (uint i = 0; i < ns; i++) { - int curr = _inSampBuf[i * 2]; + int cur = _inSampBuf[i * 2]; - if (curr != _latchL) + if (cur != _latchL) { - int diff = _latchL - curr; - _latchL = curr; + int diff = _latchL - cur; + _latchL = cur; _blipL.AddDelta(i, diff); } - curr = _inSampBuf[(i * 2) + 1]; + cur = _inSampBuf[(i * 2) + 1]; - if (curr != _latchR) + if (cur != _latchR) { - int diff = _latchR - curr; - _latchR = curr; + int diff = _latchR - cur; + _latchR = cur; _blipR.AddDelta(i, diff); } } @@ -66,6 +76,12 @@ namespace BizHawk.Emulation.Cores.Libretro _blipL.EndFrame((uint)ns); _blipR.EndFrame((uint)ns); _outSamps = _blipL.SamplesAvailable(); + + if (_outSamps > OUT_SAMPLE_RATE) + { + throw new Exception("Audio buffer overflow!"); + } + _blipL.ReadSamplesLeft(_outSampBuf, _outSamps); _blipR.ReadSamplesRight(_outSampBuf, _outSamps); } diff --git a/src/BizHawk.Emulation.Cores/Libretro/Libretro.IStatable.cs b/src/BizHawk.Emulation.Cores/Libretro/Libretro.IStatable.cs index e938480459..b1bda8c231 100644 --- a/src/BizHawk.Emulation.Cores/Libretro/Libretro.IStatable.cs +++ b/src/BizHawk.Emulation.Cores/Libretro/Libretro.IStatable.cs @@ -5,48 +5,55 @@ using BizHawk.Emulation.Common; namespace BizHawk.Emulation.Cores.Libretro { - public partial class LibretroEmulator : IStatable + // not all Libretro cores implement savestates + // we use this so we can optionally register IStatable + // todo: this can probably be genericized + public class StatableLibretro : IStatable { - private byte[] _stateBuf; - private long _stateLen; + private readonly LibretroHost _host; + private readonly LibretroApi _api; + private readonly byte[] _stateBuf; + + public StatableLibretro(LibretroHost host, LibretroApi api, int maxSize) + { + _host = host; + _api = api; + _stateBuf = new byte[maxSize]; + } public void SaveStateBinary(BinaryWriter writer) { - UpdateCallbackHandler(); - - _stateLen = api.retro_serialize_size(); - if (_stateBuf.LongLength != _stateLen) + var len = checked((int)_api.retro_serialize_size()); + if (len > _stateBuf.Length) { - _stateBuf = new byte[_stateLen]; + throw new Exception("Core attempted to grow state size. This is not allowed per the libretro API."); } - var d = new RetroData(_stateBuf, _stateLen); - api.retro_serialize(d.PinnedData, d.Length); - writer.Write(_stateBuf.Length); - writer.Write(_stateBuf); - // other variables - writer.Write(Frame); - writer.Write(LagCount); - writer.Write(IsLagFrame); + _api.retro_serialize(_stateBuf, len); + writer.Write(len); + writer.Write(_stateBuf, 0, len); + + // host variables + writer.Write(_host.Frame); + writer.Write(_host.LagCount); + writer.Write(_host.IsLagFrame); } public void LoadStateBinary(BinaryReader reader) { - UpdateCallbackHandler(); - - var newlen = reader.ReadInt32(); - if (newlen > _stateBuf.Length) + var len = reader.ReadInt32(); + if (len > _stateBuf.Length) { - throw new Exception("Unexpected buffer size"); + throw new Exception("State buffer size exceeded the core's maximum state size!"); } - reader.Read(_stateBuf, 0, newlen); - var d = new RetroData(_stateBuf, _stateLen); - api.retro_unserialize(d.PinnedData, d.Length); - // other variables - Frame = reader.ReadInt32(); - LagCount = reader.ReadInt32(); - IsLagFrame = reader.ReadBoolean(); + reader.Read(_stateBuf, 0, len); + _api.retro_unserialize(_stateBuf, len); + + // host variables + _host.Frame = reader.ReadInt32(); + _host.LagCount = reader.ReadInt32(); + _host.IsLagFrame = reader.ReadBoolean(); } } } diff --git a/src/BizHawk.Emulation.Cores/Libretro/Libretro.IVideoProvider.cs b/src/BizHawk.Emulation.Cores/Libretro/Libretro.IVideoProvider.cs index ed89241256..4fc76ac3f0 100644 --- a/src/BizHawk.Emulation.Cores/Libretro/Libretro.IVideoProvider.cs +++ b/src/BizHawk.Emulation.Cores/Libretro/Libretro.IVideoProvider.cs @@ -2,26 +2,26 @@ using BizHawk.Emulation.Common; namespace BizHawk.Emulation.Cores.Libretro { - public partial class LibretroEmulator : IVideoProvider + public partial class LibretroHost : IVideoProvider { - private int[] vidBuffer; - private int vidWidth = -1, vidHeight = -1; + private int[] _vidBuffer; + private int _vidWidth, _vidHeight; private void InitVideoBuffer(int width, int height, int maxSize) { - vidBuffer = new int[maxSize]; - vidWidth = width; - vidHeight = height; + _vidBuffer = new int[maxSize]; + _vidWidth = width; + _vidHeight = height; bridge.LibretroBridge_SetVideoSize(cbHandler, maxSize); } private void UpdateVideoBuffer() { - bridge.LibretroBridge_GetVideo(cbHandler, ref vidWidth, ref vidHeight, vidBuffer); + bridge.LibretroBridge_GetVideo(cbHandler, out _vidWidth, out _vidHeight, _vidBuffer); } public int BackgroundColor => 0; - public int[] GetVideoBuffer() => vidBuffer; + public int[] GetVideoBuffer() => _vidBuffer; public int VirtualWidth { @@ -30,13 +30,13 @@ namespace BizHawk.Emulation.Cores.Libretro var dar = av_info.geometry.aspect_ratio; if (dar <= 0) { - return vidWidth; + return _vidWidth; } if (dar > 1.0f) { - return (int)(vidHeight * dar); + return (int)(_vidHeight * dar); } - return vidWidth; + return _vidWidth; } } @@ -47,18 +47,18 @@ namespace BizHawk.Emulation.Cores.Libretro var dar = av_info.geometry.aspect_ratio; if (dar <= 0) { - return vidHeight; + return _vidHeight; } if (dar < 1.0f) { - return (int)(vidWidth / dar); + return (int)(_vidWidth / dar); } - return vidHeight; + return _vidHeight; } } - public int BufferWidth => vidWidth; - public int BufferHeight => vidHeight; + public int BufferWidth => _vidWidth; + public int BufferHeight => _vidHeight; public int VsyncNumerator { get; private set; } public int VsyncDenominator { get; private set; } diff --git a/src/BizHawk.Emulation.Cores/Libretro/Libretro.cs b/src/BizHawk.Emulation.Cores/Libretro/Libretro.cs new file mode 100644 index 0000000000..a6d163e5d0 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Libretro/Libretro.cs @@ -0,0 +1,278 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +using BizHawk.BizInvoke; +using BizHawk.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Libretro +{ + // nb: multiple libretro cores could theoretically be ran at once + // but all of them would need to be different cores, a core itself is single instance + [PortedCore(CoreNames.Libretro, "CasualPokePlayer", singleInstance: true, isReleased: false)] + [ServiceNotApplicable(new[] { typeof(IDriveLight) })] + public partial class LibretroHost + { + private static readonly LibretroBridge bridge; + private static readonly LibretroBridge.retro_procs cb_procs; + + static LibretroHost() + { + var resolver = new DynamicLibraryImportResolver( + OSTailoredCode.IsUnixHost ? "libLibretroBridge.so" : "libLibretroBridge.dll", hasLimitedLifetime: false); + + bridge = BizInvoker.GetInvoker(resolver, CallingConventionAdapters.Native); + bridge.LibretroBridge_GetRetroProcs(out cb_procs); + } + + private readonly LibretroApi api; + private IStatable _stateWrapper; + + private readonly IntPtr cbHandler; + private readonly BridgeGuard _guard; + + private class BridgeGuard : IMonitor + { + private static readonly object _sync = new(); + private static IntPtr _activeHandler; + private static int _refCount; + + private readonly IntPtr _parentHandler; + + public BridgeGuard(IntPtr parentHandler) + => _parentHandler = parentHandler; + + public void Enter() + { + lock (_sync) + { + if (_activeHandler == IntPtr.Zero) + { + _activeHandler = _parentHandler; + bridge.LibretroBridge_SetGlobalCallbackHandler(_parentHandler); + } + else if (_activeHandler != _parentHandler) + { + throw new InvalidOperationException("Multiple callback handlers cannot be active at once!"); + } + + _refCount++; + } + } + + public void Exit() + { + lock (_sync) + { + if (_refCount <= 0) + { + throw new InvalidOperationException($"Invalid {nameof(_refCount)}"); + } + else + { + _refCount--; + if (_refCount == 0) + { + _activeHandler = IntPtr.Zero; + bridge.LibretroBridge_SetGlobalCallbackHandler(IntPtr.Zero); + } + } + } + } + } + + public LibretroHost(CoreComm comm, IGameInfo game, string corePath, bool analysis = false) + { + try + { + cbHandler = bridge.LibretroBridge_CreateCallbackHandler(); + + if (cbHandler == IntPtr.Zero) + { + throw new Exception("Failed to create callback handler!"); + } + + _guard = new(cbHandler); + + api = BizInvoker.GetInvoker( + new DynamicLibraryImportResolver(corePath, hasLimitedLifetime: false), _guard, CallingConventionAdapters.Native); + + _serviceProvider = new(this); + + if (api.retro_api_version() != 1) + { + throw new InvalidOperationException("Unsupported Libretro API version (or major error in interop)"); + } + + bridge.LibretroBridge_SetDirectories(cbHandler, + comm.CoreFileProvider.GetRetroSystemPath(game), + comm.CoreFileProvider.GetRetroSaveRAMDirectory(game), + Path.GetDirectoryName(corePath), + Path.GetDirectoryName(corePath)); + + ControllerDefinition = ControllerDef; + _notify = comm.Notify; + + // check if we're just analysing the core and the core path matches the loaded core path anyways + if (analysis && corePath == LoadedCorePath) + { + Description = CalculateDescription(); + Description.SupportsNoGame = LoadedCoreSupportsNoGame; + } + else + { + api.retro_set_environment(cb_procs.retro_environment_proc); + Description = CalculateDescription(); + } + + if (!analysis) + { + LoadedCorePath = corePath; + LoadedCoreSupportsNoGame = Description.SupportsNoGame; + } + } + catch + { + Dispose(); + throw; + } + } + + private class RetroData : IDisposable + { + private readonly GCHandle _handle; + + public IntPtr PinnedData => _handle.AddrOfPinnedObject(); + public long Length { get; } + + public RetroData(object o, long len = 0) + { + _handle = GCHandle.Alloc(o, GCHandleType.Pinned); + Length = len; + } + + public void Dispose() => _handle.Free(); + } + + private byte[] RetroString(string managedString) + { + var ret = Encoding.UTF8.GetBytes(managedString); + Array.Resize(ref ret, ret.Length + 1); + return ret; + } + + private LibretroApi.retro_system_av_info av_info; + + public RetroDescription Description { get; } + + // single instance hacks + private static string LoadedCorePath { get; set; } + private static bool LoadedCoreSupportsNoGame { get; set; } + + private enum RETRO_LOAD + { + DATA, + PATH, + NO_GAME, + } + + public bool LoadData(byte[] data, string id) + { + using RetroData retroPath = new(RetroString(id)); + using RetroData retroData = new(data, data.Length); + return LoadHandler(RETRO_LOAD.DATA, retroPath, retroData); + } + + public bool LoadPath(string path) + { + using RetroData retroPath = new(RetroString(path)); + return LoadHandler(RETRO_LOAD.PATH, retroPath); + } + + public bool LoadNoGame() => LoadHandler(RETRO_LOAD.NO_GAME); + + private bool LoadHandler(RETRO_LOAD which, RetroData path = null, RetroData data = null) + { + api.retro_init(); + bool success; + LibretroApi.retro_game_info game; + + switch (which) + { + case RETRO_LOAD.NO_GAME: + success = api.retro_load_no_game(); + break; + case RETRO_LOAD.PATH: + game = new() { path = path.PinnedData }; + success = api.retro_load_game(ref game); + break; + case RETRO_LOAD.DATA: + game = new() { path = path.PinnedData, data = data.PinnedData, size = data.Length }; + success = api.retro_load_game(ref game); + break; + default: + api.retro_deinit(); + throw new InvalidOperationException($"Invalid {nameof(RETRO_LOAD)} sent?"); + } + + if (!success) + { + api.retro_deinit(); + return false; + } + + inited = true; + + av_info = default; + api.retro_get_system_av_info(ref av_info); + + api.retro_set_video_refresh(cb_procs.retro_video_refresh_proc); + api.retro_set_audio_sample(cb_procs.retro_audio_sample_proc); + api.retro_set_audio_sample_batch(cb_procs.retro_audio_sample_batch_proc); + api.retro_set_input_poll(cb_procs.retro_input_poll_proc); + api.retro_set_input_state(cb_procs.retro_input_state_proc); + + var len = checked((int)api.retro_serialize_size()); + if (len > 0) + { + _stateWrapper = new StatableLibretro(this, api, len); + _serviceProvider.Register(_stateWrapper); + } + + _region = api.retro_get_region(); + + // this stuff can only happen after the game is loaded + + // allocate a video buffer which will definitely be large enough (if it isn't, that's the core's fault) + InitVideoBuffer((int)av_info.geometry.base_width, (int)av_info.geometry.base_height, + (int)(av_info.geometry.max_width * av_info.geometry.max_height)); + + // this is no good if fps is >= 215 + VsyncNumerator = checked((int)(10000000 * av_info.timing.fps)); + VsyncDenominator = 10000000; + + SetupResampler(av_info.timing.sample_rate); + + Console.WriteLine("FPS {0} SPS {1}", av_info.timing.fps, av_info.timing.sample_rate); + + InitMemoryDomains(); // im going to assume this should happen when a game is loaded + + return true; + } + + public RetroDescription CalculateDescription() + { + var descr = new RetroDescription(); + api.retro_get_system_info(out var sys_info); + descr.LibraryName = Mershul.PtrToStringUtf8(sys_info.library_name); + descr.LibraryVersion = Mershul.PtrToStringUtf8(sys_info.library_version); + descr.ValidExtensions = Mershul.PtrToStringUtf8(sys_info.valid_extensions); + descr.NeedsRomAsPath = sys_info.need_fullpath; + descr.NeedsArchives = sys_info.block_extract; + descr.SupportsNoGame = bridge.LibretroBridge_GetSupportsNoGame(cbHandler); + return descr; + } + } +}