libretro handling cleanup, reorg some of this, fix some input cases, better domain names

funsie found in this cleanup: can't use `in` params with the BizInvoker as it doesn't like the read only semantics (results in some exception in CreateType)
This commit is contained in:
CasualPokePlayer 2022-12-10 04:45:00 -08:00
parent dc774ed8be
commit 9e4836d300
15 changed files with 795 additions and 680 deletions

View File

@ -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))

View File

@ -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))
{

View File

@ -2837,7 +2837,7 @@ namespace BizHawk.Client.EmuHawk
items.Add(CreateCoreSubmenu(VSystemCategory.Consoles, CoreNames.Gpgx, CreateGenericCoreConfigItem<GPGX>(CoreNames.Gpgx)));
// Handy
items.Add(CreateCoreSubmenu(VSystemCategory.Handhelds, CoreNames.Handy, CreateGenericCoreConfigItem<Lynx>(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<Lynx>(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<HyperNyma>(CoreNames.HyperNyma, HyperNyma.CachedSettingsInfo)));
@ -2852,7 +2852,7 @@ namespace BizHawk.Client.EmuHawk
items.Add(CreateCoreSubmenu(
VSystemCategory.Other,
CoreNames.Libretro,
CreateGenericCoreConfigItem<LibretroEmulator>(CoreNames.Libretro))); // as Libretro doesn't implement `IStatable<,>`, this opens an empty `GenericCoreConfig`, which is dumb, but matches the existing behaviour
CreateGenericCoreConfigItem<LibretroHost>(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<TurboNyma>(CoreNames.TurboNyma, TurboNyma.CachedSettingsInfo)));
// uzem
items.Add(CreateCoreSubmenu(VSystemCategory.Consoles, CoreNames.Uzem, CreateGenericCoreConfigItem<Uzem>(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<Uzem>(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<VectrexHawk>(CoreNames.VectrexHawk)));

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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<LibretroBridge>(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<LibretroApi>(
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<IReadOnlyList<string>> 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<string> retroKeyboard = new();
var p0 = (List<string>) 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;
}
}
}

View File

@ -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<string> _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<IReadOnlyList<string>> 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<string> retroKeyboard = new();
var p0 = (List<string>) 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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -5,14 +5,22 @@ using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Libretro
{
public partial class LibretroEmulator
public partial class LibretroHost
{
private readonly List<MemoryDomain> _memoryDomains = new();
private IMemoryDomains MemoryDomains { get; set; }
private MemoryDomainList _memoryDomains;
private static readonly IReadOnlyDictionary<LibretroApi.RETRO_MEMORY, string> _domainNames
= new Dictionary<LibretroApi.RETRO_MEMORY, string>()
{
[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<MemoryDomain> 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<IMemoryDomains>(_memoryDomains);
}
}
}

View File

@ -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;

View File

@ -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<MemoryDomainIntPtr> _saveramAreas = new();
private long _saveramSize = 0;

View File

@ -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<short>(); // 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);
}

View File

@ -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();
}
}
}

View File

@ -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; }

View File

@ -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<LibretroBridge>(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<LibretroApi>(
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;
}
}
}