Begin to add raw mouse input to input system

native api side done. trying to map it probably interferes with absolute inputs, needs some global relative mouse sensitivity setting for input translation purposes. hopefully everything else doesnt break
This commit is contained in:
CasualPokePlayer 2025-02-22 09:19:19 -08:00
parent efcbe54f36
commit 2f39991b44
25 changed files with 562 additions and 189 deletions

View File

@ -9,6 +9,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.SDL2-CS" ExcludeAssets="native;contentFiles" />
<ProjectReference Include="$(ProjectDir)../BizHawk.Client.Common/BizHawk.Client.Common.csproj" />
<ProjectReference Include="$(ProjectDir)../BizHawk.Common/BizHawk.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
#nullable enable
namespace BizHawk.Bizware.Input
{
[Flags]
public enum HostInputFocus
{
None = 0,
Mouse = 1,
Keyboard = 2,
Pad = 4
}
}

View File

@ -2,7 +2,7 @@
using System.Collections.Generic;
namespace BizHawk.Client.Common
namespace BizHawk.Bizware.Input
{
/// <remarks>this was easier than trying to make static classes instantiable...</remarks>
/// TODO: Reconsider if we want to hand over the main form handle
@ -18,17 +18,17 @@ namespace BizHawk.Client.Common
/// <remarks>keys are pad prefixes "X# "/"J# " (with the trailing space)</remarks>
IReadOnlyDictionary<string, IReadOnlyCollection<string>> GetHapticsChannels();
void ReInitGamepads(IntPtr mainFormHandle);
void PreprocessHostGamepads();
void ProcessHostGamepads(Action<string?, bool, ClientInputFocus> handleButton, Action<string?, int> handleAxis);
void ProcessHostGamepads(Action<string?, bool, HostInputFocus> handleButton, Action<string?, int> handleAxis);
IEnumerable<KeyEvent> ProcessHostKeyboards();
(int DeltaX, int DeltaY) ProcessHostMice();
/// <remarks>implementors may store this for use during the next <see cref="ProcessHostGamepads"/> call</remarks>
void SetHaptics(IReadOnlyCollection<(string Name, int Strength)> hapticsSnapshot);
void UpdateConfig(Config config);
void SetAlternateKeyboardLayoutEnableCallback(Func<bool> getHandleAlternateKeyboardLayouts);
}
}

View File

@ -4,8 +4,6 @@ using System.IO;
using System.IO.Pipes;
using System.Threading;
using BizHawk.Client.Common;
// this is not a very safe or pretty protocol, I'm not proud of it
namespace BizHawk.Bizware.Input
{
@ -21,7 +19,7 @@ namespace BizHawk.Bizware.Input
}
}
private static readonly List<KeyEvent> PendingKeyEvents = new();
private static readonly List<KeyEvent> PendingKeyEvents = [ ];
private static bool IPCActive;
private static void IPCThread()

View File

@ -1,13 +0,0 @@
#nullable enable
using System.Collections.Generic;
using BizHawk.Client.Common;
namespace BizHawk.Bizware.Input
{
internal interface IKeyInput : IDisposable
{
IEnumerable<KeyEvent> Update(bool handleAltKbLayouts);
}
}

View File

@ -1,17 +0,0 @@
#nullable enable
using BizHawk.Common;
namespace BizHawk.Bizware.Input
{
internal static class KeyInputFactory
{
public static IKeyInput CreateKeyInput() => OSTailoredCode.CurrentOS switch
{
OSTailoredCode.DistinctOS.Linux => new X11KeyInput(),
OSTailoredCode.DistinctOS.macOS => new QuartzKeyInput(),
OSTailoredCode.DistinctOS.Windows => new RawKeyInput(),
_ => throw new InvalidOperationException("Unknown OS"),
};
}
}

View File

@ -3,7 +3,7 @@
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable UnusedMember.Global
namespace BizHawk.Client.Common
namespace BizHawk.Bizware.Input
{
/// <summary>values are one-to-one with <c>System.Windows.Input.Key</c> except <see cref="NumPadEnter"/> and <see cref="Unknown"/> which were added for this project</summary>
/// <remarks>copied from MIT-licensed WPF source: https://github.com/dotnet/wpf/blob/49bb41ad83abeb5ae22e4c59d0f43c1287acac00/src/Microsoft.DotNet.Wpf/src/WindowsBase/System/Windows/Input/Key.cs</remarks>

View File

@ -0,0 +1,13 @@
#nullable enable
using System.Collections.Generic;
namespace BizHawk.Bizware.Input
{
internal interface IKeyMouseInput : IDisposable
{
IEnumerable<KeyEvent> UpdateKeyInputs(bool handleAltKbLayouts);
(int DeltaX, int DeltaY) UpdateMouseInput();
}
}

View File

@ -0,0 +1,6 @@
#nullable enable
namespace BizHawk.Bizware.Input
{
public readonly record struct KeyEvent(DistinctKey Key, bool Pressed);
}

View File

@ -0,0 +1,17 @@
#nullable enable
using BizHawk.Common;
namespace BizHawk.Bizware.Input
{
internal static class KeyMouseInputFactory
{
public static IKeyMouseInput CreateKeyMouseInput() => OSTailoredCode.CurrentOS switch
{
OSTailoredCode.DistinctOS.Linux => new X11KeyMouseInput(),
OSTailoredCode.DistinctOS.macOS => new QuartzKeyMouseInput(),
OSTailoredCode.DistinctOS.Windows => new RawKeyMouseInput(),
_ => throw new InvalidOperationException("Unknown OS"),
};
}
}

View File

@ -2,13 +2,11 @@
using System.Collections.Generic;
using BizHawk.Client.Common;
using static BizHawk.Common.QuartzImports;
namespace BizHawk.Bizware.Input
{
internal sealed class QuartzKeyInput : IKeyInput
internal sealed class QuartzKeyMouseInput : IKeyMouseInput
{
private readonly bool[] LastKeyState = new bool[0x7F];
@ -16,7 +14,7 @@ namespace BizHawk.Bizware.Input
{
}
public IEnumerable<KeyEvent> Update(bool handleAltKbLayouts)
public IEnumerable<KeyEvent> UpdateKeyInputs(bool handleAltKbLayouts)
{
var keyEvents = new List<KeyEvent>();
for (var keycode = 0; keycode < 0x7F; keycode++)
@ -28,7 +26,7 @@ namespace BizHawk.Bizware.Input
{
if (KeyEnumMap.TryGetValue((CGKeyCode)keycode, out var key))
{
keyEvents.Add(new(key, pressed: keystate));
keyEvents.Add(new(key, Pressed: keystate));
LastKeyState[keycode] = keystate;
}
}
@ -37,6 +35,13 @@ namespace BizHawk.Bizware.Input
return keyEvents;
}
public (int DeltaX, int DeltaY) UpdateMouseInput()
{
// probably wrong, need to recheck when we actually get macos support
CGGetLastMouseDelta(out var deltaX, out var deltaY);
return (deltaX, deltaY);
}
private static readonly IReadOnlyDictionary<CGKeyCode, DistinctKey> KeyEnumMap = new Dictionary<CGKeyCode, DistinctKey>
{
[CGKeyCode.kVK_ANSI_A] = DistinctKey.A,

View File

@ -1,9 +1,9 @@
#nullable enable
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using BizHawk.Client.Common;
using BizHawk.Common;
using BizHawk.Common.CollectionExtensions;
@ -13,10 +13,10 @@ using static BizHawk.Common.WmImports;
namespace BizHawk.Bizware.Input
{
/// <summary>
/// Note: Only 1 window per device class (i.e. keyboards) is actually allowed to RAWINPUT (last one to call RegisterRawInputDevices)
/// Note: Only 1 window per device class (e.g. keyboards) is actually allowed to use RAWINPUT (last one to call RegisterRawInputDevices)
/// So only one instance can actually be used at the same time
/// </summary>
internal sealed class RawKeyInput : IKeyInput
internal sealed class RawKeyMouseInput : IKeyMouseInput
{
private const int WM_CLOSE = 0x0010;
private const int WM_INPUT = 0x00FF;
@ -24,6 +24,8 @@ namespace BizHawk.Bizware.Input
private IntPtr RawInputWindow;
private bool _handleAltKbLayouts;
private List<KeyEvent> _keyEvents = [ ];
private (int X, int Y) _mouseDelta;
private (int X, int Y) _lastMouseAbsPos;
private readonly object _lockObj = new();
private bool _disposed;
@ -38,7 +40,7 @@ namespace BizHawk.Bizware.Input
var wc = default(WNDCLASSW);
wc.lpfnWndProc = _wndProc;
wc.hInstance = LoaderApiImports.GetModuleHandleW(null);
wc.lpszClassName = "RawKeyInputClass";
wc.lpszClassName = "RawKeyMouseInputClass";
var atom = RegisterClassW(ref wc);
if (atom == IntPtr.Zero)
@ -58,15 +60,15 @@ namespace BizHawk.Bizware.Input
}
GCHandle handle;
RawKeyInput rawKeyInput;
RawKeyMouseInput rawKeyMouseInput;
if (uMsg != WM_INPUT)
{
if (uMsg == WM_CLOSE)
{
SetWindowLongPtrW(hWnd, GWLP_USERDATA, IntPtr.Zero);
handle = GCHandle.FromIntPtr(ud);
rawKeyInput = (RawKeyInput)handle.Target;
Marshal.FreeCoTaskMem(rawKeyInput.RawInputBuffer);
rawKeyMouseInput = (RawKeyMouseInput)handle.Target;
Marshal.FreeCoTaskMem(rawKeyMouseInput.RawInputBuffer);
handle.Free();
}
@ -86,7 +88,7 @@ namespace BizHawk.Bizware.Input
: stackalloc IntPtr[(size + sizeof(IntPtr) - 1) / sizeof(IntPtr)];
handle = GCHandle.FromIntPtr(ud);
rawKeyInput = (RawKeyInput)handle.Target;
rawKeyMouseInput = (RawKeyMouseInput)handle.Target;
fixed (IntPtr* p = buffer)
{
@ -99,14 +101,19 @@ namespace BizHawk.Bizware.Input
if (input->header.dwType == RAWINPUTHEADER.RIM_TYPE.KEYBOARD)
{
rawKeyInput.AddKeyInput(&input->data.keyboard);
rawKeyMouseInput.AddKeyInput(&input->data.keyboard);
}
if (input->header.dwType == RAWINPUTHEADER.RIM_TYPE.MOUSE)
{
rawKeyMouseInput.AddMouseInput(&input->data.mouse);
}
}
while (true)
{
var rawInputBuffer = (RAWINPUT*)rawKeyInput.RawInputBuffer;
size = rawKeyInput.RawInputBufferSize;
var rawInputBuffer = (RAWINPUT*)rawKeyMouseInput.RawInputBuffer;
size = rawKeyMouseInput.RawInputBufferSize;
var count = GetRawInputBuffer(rawInputBuffer, ref size, sizeof(RAWINPUTHEADER));
if (count == 0)
{
@ -121,8 +128,8 @@ namespace BizHawk.Bizware.Input
const int ERROR_INSUFFICIENT_BUFFER = 0x7A;
if (Marshal.GetLastWin32Error() == ERROR_INSUFFICIENT_BUFFER)
{
rawKeyInput.RawInputBufferSize *= 2;
rawKeyInput.RawInputBuffer = Marshal.ReAllocCoTaskMem(rawKeyInput.RawInputBuffer, rawKeyInput.RawInputBufferSize);
rawKeyMouseInput.RawInputBufferSize *= 2;
rawKeyMouseInput.RawInputBuffer = Marshal.ReAllocCoTaskMem(rawKeyMouseInput.RawInputBuffer, rawKeyMouseInput.RawInputBufferSize);
continue;
}
@ -133,8 +140,14 @@ namespace BizHawk.Bizware.Input
{
if (rawInputBuffer->header.dwType == RAWINPUTHEADER.RIM_TYPE.KEYBOARD)
{
var keyboard = (RAWKEYBOARD*)((byte*)&rawInputBuffer->data.keyboard + rawKeyInput.RawInputBufferDataOffset);
rawKeyInput.AddKeyInput(keyboard);
var keyboard = (RAWKEYBOARD*)((byte*)&rawInputBuffer->data.keyboard + rawKeyMouseInput.RawInputBufferDataOffset);
rawKeyMouseInput.AddKeyInput(keyboard);
}
if (rawInputBuffer->header.dwType == RAWINPUTHEADER.RIM_TYPE.MOUSE)
{
var mouse = (RAWMOUSE*)((byte*)&rawInputBuffer->data.mouse + rawKeyMouseInput.RawInputBufferDataOffset);
rawKeyMouseInput.AddMouseInput(mouse);
}
var packetSize = rawInputBuffer->header.dwSize;
@ -174,6 +187,22 @@ namespace BizHawk.Bizware.Input
}
}
private unsafe void AddMouseInput(RAWMOUSE* mouse)
{
// raw input usually doesn't report absolute inputs, only in odd cases with e.g. touchscreen and rdp screen
if ((mouse->usFlags & RAWMOUSE.MOUSE_FLAGS.MOVE_ABSOLUTE) == RAWMOUSE.MOUSE_FLAGS.MOVE_ABSOLUTE)
{
_mouseDelta.X += mouse->lLastX - _lastMouseAbsPos.X;
_mouseDelta.Y += mouse->lLastY - _lastMouseAbsPos.Y;
_lastMouseAbsPos = (mouse->lLastX, mouse->lLastY);
}
else // ((mouse->usFlags & RAWMOUSE.MOUSE_FLAGS.MOVE_ABSOLUTE) == RAWMOUSE.MOUSE_FLAGS.MOVE_RELATIVE)
{
_mouseDelta.X += mouse->lLastX;
_mouseDelta.Y += mouse->lLastY;
}
}
private static IntPtr CreateRawInputWindow()
{
const int WS_CHILD = 0x40000000;
@ -196,22 +225,29 @@ namespace BizHawk.Bizware.Input
throw new InvalidOperationException("Failed to create RAWINPUT window");
}
var rid = default(RAWINPUTDEVICE);
rid.usUsagePage = RAWINPUTDEVICE.HidUsagePage.GENERIC;
rid.usUsage = RAWINPUTDEVICE.HidUsageId.GENERIC_KEYBOARD;
rid.dwFlags = RAWINPUTDEVICE.RIDEV.INPUTSINK;
rid.hwndTarget = window;
if (!RegisterRawInputDevices(ref rid, 1, Marshal.SizeOf<RAWINPUTDEVICE>()))
unsafe
{
DestroyWindow(window);
throw new InvalidOperationException("Failed to register RAWINPUTDEVICE");
var rid = stackalloc RAWINPUTDEVICE[2];
rid[0].usUsagePage = RAWINPUTDEVICE.HidUsagePage.GENERIC;
rid[0].usUsage = RAWINPUTDEVICE.HidUsageId.GENERIC_KEYBOARD;
rid[0].dwFlags = RAWINPUTDEVICE.RIDEV.INPUTSINK;
rid[0].hwndTarget = window;
rid[1].usUsagePage = RAWINPUTDEVICE.HidUsagePage.GENERIC;
rid[1].usUsage = RAWINPUTDEVICE.HidUsageId.GENERIC_MOUSE;
rid[1].dwFlags = RAWINPUTDEVICE.RIDEV.INPUTSINK;
rid[1].hwndTarget = window;
if (!RegisterRawInputDevices(rid, 2, sizeof(RAWINPUTDEVICE)))
{
DestroyWindow(window);
throw new InvalidOperationException("Failed to register RAWINPUTDEVICE");
}
}
return window;
}
public RawKeyInput()
public RawKeyMouseInput()
{
if (OSTailoredCode.IsUnixHost)
{
@ -236,7 +272,7 @@ namespace BizHawk.Bizware.Input
RawInputBufferDataOffset = isWow64 ? 8 : 0;
}
RawInputBufferSize = (Marshal.SizeOf<RAWINPUT>() + RawInputBufferDataOffset) * 16;
RawInputBufferSize = (Unsafe.SizeOf<RAWINPUT>() + RawInputBufferDataOffset) * 16;
RawInputBuffer = Marshal.AllocCoTaskMem(RawInputBufferSize);
}
@ -261,7 +297,7 @@ namespace BizHawk.Bizware.Input
}
}
public IEnumerable<KeyEvent> Update(bool handleAltKbLayouts)
public IEnumerable<KeyEvent> UpdateKeyInputs(bool handleAltKbLayouts)
{
lock (_lockObj)
{
@ -291,6 +327,34 @@ namespace BizHawk.Bizware.Input
}
}
public (int DeltaX, int DeltaY) UpdateMouseInput()
{
lock (_lockObj)
{
if (_disposed)
{
return default;
}
if (RawInputWindow == IntPtr.Zero)
{
RawInputWindow = CreateRawInputWindow();
var handle = GCHandle.Alloc(this, GCHandleType.Normal);
SetWindowLongPtrW(RawInputWindow, GWLP_USERDATA, GCHandle.ToIntPtr(handle));
}
while (PeekMessageW(out var msg, RawInputWindow, 0, 0, PM_REMOVE))
{
TranslateMessage(ref msg);
DispatchMessageW(ref msg);
}
var ret = _mouseDelta;
_mouseDelta = default;
return ret;
}
}
private static readonly RawKey[] _rawKeysNoTranslation =
[
RawKey.NUMPAD0,

View File

@ -1,26 +1,28 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using BizHawk.Client.Common;
using BizHawk.Common;
using BizHawk.Common.CollectionExtensions;
using static BizHawk.Common.XInput2Imports;
using static BizHawk.Common.XlibImports;
// a lot of this code is taken from OpenTK
namespace BizHawk.Bizware.Input
{
internal sealed class X11KeyInput : IKeyInput
internal sealed class X11KeyMouseInput : IKeyMouseInput
{
private IntPtr Display;
private readonly bool[] LastKeyState = new bool[256];
private readonly object LockObj = new();
private readonly DistinctKey[] KeyEnumMap = new DistinctKey[256];
private readonly bool _supportsXInput2;
private readonly int _xi2Opcode;
public X11KeyInput()
public X11KeyMouseInput()
{
if (OSTailoredCode.CurrentOS != OSTailoredCode.DistinctOS.Linux)
{
@ -32,24 +34,58 @@ namespace BizHawk.Bizware.Input
if (Display == IntPtr.Zero)
{
// There doesn't seem to be a convention for what exception type to throw in these situations. Can't use NRE. Well...
// _ = Unsafe.AsRef<X11.Display>()!; // hmm
// _ = Unsafe.AsRef<X11.Display>()!; // hmm
// InvalidOperationException doesn't match. Exception it is. --yoshi
throw new Exception("Could not open XDisplay");
}
using (new XLock(Display))
{
// check if we can use XKb
int major = 1, minor = 0;
var supportsXkb = XkbQueryExtension(Display, out _, out _, out _, ref major, ref minor);
// check if we can use XKb
int major = 1, minor = 0;
var supportsXkb = XkbQueryExtension(Display, out _, out _, out _, ref major, ref minor);
if (supportsXkb)
if (supportsXkb)
{
// we generally want this behavior
XkbSetDetectableAutoRepeat(Display, true, out _);
}
CreateKeyMap(supportsXkb);
_supportsXInput2 = XQueryExtension(Display, "XInputExtension", out _xi2Opcode, out _, out _);
if (_supportsXInput2)
{
try
{
// we generally want this behavior
XkbSetDetectableAutoRepeat(Display, true, out _);
(major, minor) = (2, 1);
if (XIQueryVersion(Display, ref major, ref minor) != 0
|| major * 100 + minor < 201)
{
_supportsXInput2 = false;
}
}
catch
{
// libXi.so.6 might not be present
_supportsXInput2 = false;
}
CreateKeyMap(supportsXkb);
if (_supportsXInput2)
{
Span<byte> maskBuf = stackalloc byte[((int)XIEvents.XI_LASTEVENT + 7) / 8];
maskBuf.Clear();
XISetMask(maskBuf, (int)XIEvents.XI_RawMotion);
unsafe
{
fixed (byte* maskBufPtr = maskBuf)
{
XIEventMask eventMask;
eventMask.deviceid = XIAllMasterDevices;
eventMask.mask = (IntPtr)maskBufPtr;
eventMask.mask_len = maskBuf.Length;
_ = XISelectEvents(Display, XDefaultRootWindow(Display), ref eventMask, 1);
}
}
}
}
}
@ -65,23 +101,19 @@ namespace BizHawk.Bizware.Input
}
}
public unsafe IEnumerable<KeyEvent> Update(bool handleAltKbLayouts)
public unsafe IEnumerable<KeyEvent> UpdateKeyInputs(bool handleAltKbLayouts)
{
lock (LockObj)
{
// Can't update without a display connection
if (Display == IntPtr.Zero)
{
return Enumerable.Empty<KeyEvent>();
return [ ];
}
var keys = stackalloc byte[32];
using (new XLock(Display))
{
// this apparently always returns 1 no matter what?
_ = XQueryKeymap(Display, keys);
}
// this apparently always returns 1 no matter what?
_ = XQueryKeymap(Display, keys);
var keyEvents = new List<KeyEvent>();
for (var keycode = 0; keycode < 256; keycode++)
@ -92,7 +124,7 @@ namespace BizHawk.Bizware.Input
var keystate = (keys[keycode >> 3] >> (keycode & 0x07) & 0x01) != 0;
if (LastKeyState[keycode] != keystate)
{
keyEvents.Add(new(key, pressed: keystate));
keyEvents.Add(new(key, Pressed: keystate));
LastKeyState[keycode] = keystate;
}
}
@ -102,6 +134,86 @@ namespace BizHawk.Bizware.Input
}
}
public (int DeltaX, int DeltaY) UpdateMouseInput()
{
lock (LockObj)
{
// Can't update without a display connection
if (Display == IntPtr.Zero)
{
return default;
}
// need XInput2 support for this
if (!_supportsXInput2)
{
return default;
}
(double mouseDeltaX, double mouseDeltaY) = (0, 0);
while (XPending(Display) > 0)
{
_ = XNextEvent(Display, out var evt);
if (evt.xcookie.type != XEventTypes.GenericEvent
|| evt.xcookie.extension != _xi2Opcode)
{
continue;
}
if (!XGetEventData(Display, ref evt.xcookie))
{
continue;
}
if ((XIEvents)evt.xcookie.evtype == XIEvents.XI_RawMotion)
{
unsafe
{
var xiRawEvent = (XIRawEvent*)evt.xcookie.data;
var valuatorsMask = new Span<byte>(xiRawEvent->valuators.mask, xiRawEvent->valuators.mask_len);
if (!valuatorsMask.IsEmpty)
{
var rawValueIndex = 0;
// not implemented until netcore / netstandard2.1
// copied from modern runtime
static bool IsNormal(double d)
{
var bits = BitConverter.DoubleToInt64Bits(d);
bits &= 0x7FFFFFFFFFFFFFFF;
return (bits < 0x7FF0000000000000) && (bits != 0) && ((bits & 0x7FF0000000000000) == 0);
}
if (XIMaskIsSet(valuatorsMask, 0))
{
var deltaX = xiRawEvent->raw_values[rawValueIndex];
if (IsNormal(deltaX))
{
mouseDeltaX += deltaX;
}
rawValueIndex++;
}
if (XIMaskIsSet(valuatorsMask, 1))
{
var deltaY = xiRawEvent->raw_values[rawValueIndex];
if (IsNormal(deltaY))
{
mouseDeltaY += deltaY;
}
}
}
}
}
_ = XFreeEventData(Display, ref evt.xcookie);
}
return ((int)mouseDeltaX, (int)mouseDeltaY);
}
}
private unsafe void CreateKeyMap(bool supportsXkb)
{
for (var i = 0; i < KeyEnumMap.Length; i++)

View File

@ -3,54 +3,53 @@
using System.Collections.Generic;
using System.Linq;
using BizHawk.Client.Common;
namespace BizHawk.Bizware.Input
{
/// <summary>
/// Abstract class which only handles keyboard input
/// Abstract class which only handles keyboard and mouse input
/// Uses OS specific functionality, as there is no good cross platform way to do this
/// (Mostly as all the available cross-platform options require a focused window, arg!)
/// TODO: Doesn't work for Wayland yet (must use XWayland, which Wayland users need to use anyways for BizHawk)
/// </summary>
public abstract class OSTailoredKeyInputAdapter : IHostInputAdapter
public abstract class OSTailoredKeyMouseInputAdapter : IHostInputAdapter
{
private IKeyInput? _keyInput;
protected Config? _config;
private IKeyMouseInput? _keyMouseInput;
protected Func<bool>? _getHandleAlternateKeyboardLayouts;
public abstract string Desc { get; }
public virtual void DeInitAll()
=> _keyInput!.Dispose();
=> _keyMouseInput!.Dispose();
public virtual void FirstInitAll(IntPtr mainFormHandle)
{
_keyInput = KeyInputFactory.CreateKeyInput();
_keyMouseInput = KeyMouseInputFactory.CreateKeyMouseInput();
IPCKeyInput.Initialize(); // why not? this isn't necessarily OS specific
}
public abstract IReadOnlyDictionary<string, IReadOnlyCollection<string>> GetHapticsChannels();
public abstract void ReInitGamepads(IntPtr mainFormHandle);
public abstract void PreprocessHostGamepads();
public abstract void ProcessHostGamepads(Action<string?, bool, ClientInputFocus> handleButton, Action<string?, int> handleAxis);
public abstract void ProcessHostGamepads(Action<string?, bool, HostInputFocus> handleButton, Action<string?, int> handleAxis);
public virtual IEnumerable<KeyEvent> ProcessHostKeyboards()
{
if (_config is null)
if (_getHandleAlternateKeyboardLayouts is null)
{
throw new InvalidOperationException(nameof(ProcessHostKeyboards) + " called before the global config was passed");
throw new InvalidOperationException(nameof(ProcessHostKeyboards) + " called before alternate keyboard layout enable callback was set");
}
var ret = _keyInput!.Update(_config.HandleAlternateKeyboardLayouts);
var ret = _keyMouseInput!.UpdateKeyInputs(_getHandleAlternateKeyboardLayouts());
return ret.Concat(IPCKeyInput.Update());
}
public virtual (int DeltaX, int DeltaY) ProcessHostMice()
=> _keyMouseInput!.UpdateMouseInput();
public abstract void SetHaptics(IReadOnlyCollection<(string Name, int Strength)> hapticsSnapshot);
public virtual void UpdateConfig(Config config)
=> _config = config;
public virtual void SetAlternateKeyboardLayoutEnableCallback(Func<bool> getHandleAlternateKeyboardLayouts)
=> _getHandleAlternateKeyboardLayouts = getHandleAlternateKeyboardLayouts;
}
}

View File

@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using BizHawk.Client.Common;
using BizHawk.Common;
using BizHawk.Common.CollectionExtensions;
#if BIZHAWKBUILD_DEBUG_RUMBLE
@ -16,9 +15,9 @@ using static SDL2.SDL;
namespace BizHawk.Bizware.Input
{
public sealed class SDL2InputAdapter : OSTailoredKeyInputAdapter
public sealed class SDL2InputAdapter : OSTailoredKeyMouseInputAdapter
{
private static readonly IReadOnlyCollection<string> SDL2_HAPTIC_CHANNEL_NAMES = new[] { "Left", "Right" };
private static readonly IReadOnlyCollection<string> SDL2_HAPTIC_CHANNEL_NAMES = [ "Left", "Right" ];
private IReadOnlyDictionary<string, int> _lastHapticsSnapshot = new Dictionary<string, int>();
@ -130,11 +129,7 @@ namespace BizHawk.Bizware.Input
.Where(pad => pad.HasRumble)
.Select(pad => pad.InputNamePrefix)
.ToDictionary(s => s, _ => SDL2_HAPTIC_CHANNEL_NAMES)
: new();
}
public override void ReInitGamepads(IntPtr mainFormHandle)
{
: [ ];
}
public override void PreprocessHostGamepads()
@ -153,15 +148,15 @@ namespace BizHawk.Bizware.Input
DoSDLEventLoop();
}
public override void ProcessHostGamepads(Action<string?, bool, ClientInputFocus> handleButton, Action<string?, int> handleAxis)
public override void ProcessHostGamepads(Action<string?, bool, HostInputFocus> handleButton, Action<string?, int> handleAxis)
{
if (!_isInit) return;
foreach (var pad in SDL2Gamepad.EnumerateDevices())
{
foreach (var but in pad.ButtonGetters)
foreach (var (ButtonName, GetIsPressed) in pad.ButtonGetters)
{
handleButton(pad.InputNamePrefix + but.ButtonName, but.GetIsPressed(), ClientInputFocus.Pad);
handleButton(pad.InputNamePrefix + ButtonName, GetIsPressed(), HostInputFocus.Pad);
}
foreach (var (axisID, f) in pad.GetAxes())
@ -188,7 +183,7 @@ namespace BizHawk.Bizware.Input
{
return _isInit
? base.ProcessHostKeyboards()
: Enumerable.Empty<KeyEvent>();
: [ ];
}
public override void SetHaptics(IReadOnlyCollection<(string Name, int Strength)> hapticsSnapshot)

View File

@ -16,6 +16,7 @@
<PackageReference Include="System.CommandLine" />
<ProjectReference Include="$(ProjectDir)../BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj" />
<ProjectReference Include="$(ProjectDir)../BizHawk.Bizware.Graphics/BizHawk.Bizware.Graphics.csproj" />
<ProjectReference Include="$(ProjectDir)../BizHawk.Bizware.Input/BizHawk.Bizware.Input.csproj" />
<EmbeddedResource Include="Resources/**/*" />
</ItemGroup>
<ItemGroup>

View File

@ -1,8 +1,10 @@
using BizHawk.Bizware.Input;
namespace BizHawk.Client.Common
{
public static class DistinctKeyNameOverrides
{
public static string GetName(in DistinctKey k)
public static string GetName(DistinctKey k)
=> k switch
{
DistinctKey.Back => "Backspace",

View File

@ -3,24 +3,17 @@
using System.Collections.Generic;
using System.Text;
using BizHawk.Bizware.Input;
namespace BizHawk.Client.Common
{
[Flags]
public enum ClientInputFocus
{
None = 0,
Mouse = 1,
Keyboard = 2,
Pad = 4
}
public class InputEvent
{
public InputEventType EventType;
public LogicalButton LogicalButton;
public ClientInputFocus Source;
public HostInputFocus Source;
public override string ToString() => $"{EventType}:{LogicalButton}";
}

View File

@ -1,17 +0,0 @@
#nullable enable
namespace BizHawk.Client.Common
{
public readonly struct KeyEvent
{
public readonly DistinctKey Key;
public readonly bool Pressed;
public KeyEvent(DistinctKey key, bool pressed)
{
Key = key;
Pressed = pressed;
}
}
}

View File

@ -18,10 +18,10 @@ namespace BizHawk.Client.EmuHawk
/// Why is this receiving a control, but actually using it as a Form (where the WantingMouseFocus is checked?)
/// Because later we might change it to work off the control, specifically, if a control is supplied (normally actually a Form will be supplied)
/// </summary>
public void ControlInputFocus(Control c, ClientInputFocus types, bool wants)
public void ControlInputFocus(Control c, HostInputFocus types, bool wants)
{
if (types.HasFlag(ClientInputFocus.Mouse) && wants) _wantingMouseFocus.Add(c);
if (types.HasFlag(ClientInputFocus.Mouse) && !wants) _wantingMouseFocus.Remove(c);
if (types.HasFlag(HostInputFocus.Mouse) && wants) _wantingMouseFocus.Add(c);
if (types.HasFlag(HostInputFocus.Mouse) && !wants) _wantingMouseFocus.Remove(c);
}
private readonly HashSet<Control> _wantingMouseFocus = new HashSet<Control>();
@ -48,7 +48,7 @@ namespace BizHawk.Client.EmuHawk
Adapter = new SDL2InputAdapter();
Console.WriteLine($"Using {Adapter.Desc} for host input (keyboard + gamepads)");
Adapter.UpdateConfig(_currentConfig);
Adapter.SetAlternateKeyboardLayoutEnableCallback(() => _currentConfig.HandleAlternateKeyboardLayouts);
Adapter.FirstInitAll(mainFormHandle);
_updateThread = new Thread(UpdateThreadProc)
{
@ -96,7 +96,7 @@ namespace BizHawk.Client.EmuHawk
["Shift"] = "LeftShift",
};
private void HandleButton(string button, bool newState, ClientInputFocus source)
private void HandleButton(string button, bool newState, HostInputFocus source)
{
if (!(_currentConfig.MergeLAndRModifierKeys && ModifierKeyPreMap.TryGetValue(button, out var button1))) button1 = button;
var modIndex = _currentConfig.ModifierKeysEffective.IndexOf(button1);
@ -135,7 +135,7 @@ namespace BizHawk.Client.EmuHawk
private void HandleAxis(string axis, int newValue)
{
if (ShouldSwallow(MainFormInputAllowedCallback(false), ClientInputFocus.Pad))
if (ShouldSwallow(MainFormInputAllowedCallback(false), HostInputFocus.Pad))
return;
if (_trackDeltas)
@ -195,9 +195,9 @@ namespace BizHawk.Client.EmuHawk
{
_currentConfig = _getConfigCallback();
UpdateModifierKeysEffective();
Adapter.UpdateConfig(_currentConfig);
var keyEvents = Adapter.ProcessHostKeyboards();
var (mouseDeltaX, mouseDeltaY) = Adapter.ProcessHostMice();
Adapter.PreprocessHostGamepads();
//this block is going to massively modify data structures that the binding method uses, so we have to lock it all
@ -208,7 +208,7 @@ namespace BizHawk.Client.EmuHawk
//analyze keys
foreach (var ke in keyEvents)
{
HandleButton(DistinctKeyNameOverrides.GetName(in ke.Key), ke.Pressed, ClientInputFocus.Keyboard);
HandleButton(DistinctKeyNameOverrides.GetName(ke.Key), ke.Pressed, HostInputFocus.Keyboard);
}
lock (_axisValues)
@ -217,7 +217,6 @@ namespace BizHawk.Client.EmuHawk
Adapter.ProcessHostGamepads(HandleButton, HandleAxis);
// analyze moose
// other sorts of mouse api (raw input) could easily be added as a separate listing under a different class
if (_wantingMouseFocus.Contains(Form.ActiveForm))
{
var mousePos = Control.MousePosition;
@ -235,11 +234,15 @@ namespace BizHawk.Client.EmuHawk
_axisValues["WMouse Y"] = mousePos.Y;
var mouseBtns = Control.MouseButtons;
HandleButton("WMouse L", (mouseBtns & MouseButtons.Left) != 0, ClientInputFocus.Mouse);
HandleButton("WMouse M", (mouseBtns & MouseButtons.Middle) != 0, ClientInputFocus.Mouse);
HandleButton("WMouse R", (mouseBtns & MouseButtons.Right) != 0, ClientInputFocus.Mouse);
HandleButton("WMouse 1", (mouseBtns & MouseButtons.XButton1) != 0, ClientInputFocus.Mouse);
HandleButton("WMouse 2", (mouseBtns & MouseButtons.XButton2) != 0, ClientInputFocus.Mouse);
HandleButton("WMouse L", (mouseBtns & MouseButtons.Left) != 0, HostInputFocus.Mouse);
HandleButton("WMouse M", (mouseBtns & MouseButtons.Middle) != 0, HostInputFocus.Mouse);
HandleButton("WMouse R", (mouseBtns & MouseButtons.Right) != 0, HostInputFocus.Mouse);
HandleButton("WMouse 1", (mouseBtns & MouseButtons.XButton1) != 0, HostInputFocus.Mouse);
HandleButton("WMouse 2", (mouseBtns & MouseButtons.XButton2) != 0, HostInputFocus.Mouse);
// raw (relative) mouse input
_axisValues["RMouse X"] = mouseDeltaX;
_axisValues["RMouse Y"] = mouseDeltaY;
}
else
{
@ -279,9 +282,9 @@ namespace BizHawk.Client.EmuHawk
}
}
private static bool ShouldSwallow(AllowInput allowInput, ClientInputFocus inputFocus)
private static bool ShouldSwallow(AllowInput allowInput, HostInputFocus inputFocus)
{
return allowInput == AllowInput.None || (allowInput == AllowInput.OnlyController && inputFocus != ClientInputFocus.Pad);
return allowInput == AllowInput.None || (allowInput == AllowInput.OnlyController && inputFocus != HostInputFocus.Pad);
}
public void StartListeningForAxisEvents()

View File

@ -15,6 +15,7 @@ using System.Security.Principal;
using System.IO.Pipes;
using BizHawk.Bizware.Graphics;
using BizHawk.Bizware.Input;
using BizHawk.Common;
using BizHawk.Common.BufferExtensions;
@ -58,9 +59,9 @@ namespace BizHawk.Client.EmuHawk
private readonly ToolStripMenuItemEx NullHawkVSysSubmenu = new() { Enabled = false, Text = "—" };
private void MainForm_Load(object sender, EventArgs e)
{
{
UpdateWindowTitle();
{
for (int i = 1; i <= WINDOW_SCALE_MAX; i++)
{
@ -1163,12 +1164,12 @@ namespace BizHawk.Client.EmuHawk
protected override void OnActivated(EventArgs e)
{
base.OnActivated(e);
Input.Instance.ControlInputFocus(this, ClientInputFocus.Mouse, true);
Input.Instance.ControlInputFocus(this, HostInputFocus.Mouse, true);
}
protected override void OnDeactivate(EventArgs e)
{
Input.Instance.ControlInputFocus(this, ClientInputFocus.Mouse, false);
Input.Instance.ControlInputFocus(this, HostInputFocus.Mouse, false);
base.OnDeactivate(e);
}
@ -2801,8 +2802,6 @@ namespace BizHawk.Client.EmuHawk
// Alt key hacks
protected override void WndProc(ref Message m)
{
if (m.Msg == WmDeviceChange) Input.Instance.Adapter.ReInitGamepads(Handle);
// this is necessary to trap plain alt keypresses so that only our hotkey system gets them
if (m.Msg == 0x0112) // WM_SYSCOMMAND
{

View File

@ -0,0 +1,96 @@
using System.Runtime.InteropServices;
namespace BizHawk.Common
{
public static class XInput2Imports
{
private const string XI2 = "libXi.so.6";
[DllImport(XI2)]
public static extern int XIQueryVersion(IntPtr display, ref int major_version_inout, ref int minor_version_inout);
public enum XIEvents
{
XI_DeviceChanged = 1,
XI_KeyPress = 2,
XI_KeyRelease = 3,
XI_ButtonPress = 4,
XI_ButtonRelease = 5,
XI_Motion = 6,
XI_Enter = 7,
XI_Leave = 8,
XI_FocusIn = 9,
XI_FocusOut = 10,
XI_HierarchyChanged = 11,
XI_PropertyEvent = 12,
XI_RawKeyPress = 13,
XI_RawKeyRelease = 14,
XI_RawButtonPress = 15,
XI_RawButtonRelease = 16,
XI_RawMotion = 17,
XI_TouchBegin = 18, // XI 2.2
XI_TouchUpdate = 19,
XI_TouchEnd = 20,
XI_TouchOwnership = 21,
XI_RawTouchBegin = 22,
XI_RawTouchUpdate = 23,
XI_RawTouchEnd = 24,
XI_BarrierHit = 25, // XI 2.3
XI_BarrierLeave = 26,
XI_GesturePinchBegin = 27, // XI 2.4
XI_GesturePinchUpdate = 28,
XI_GesturePinchEnd = 29,
XI_GestureSwipeBegin = 30,
XI_GestureSwipeUpdate = 31,
XI_GestureSwipeEnd = 32,
XI_LASTEVENT = XI_GestureSwipeEnd
}
// these are normally macros in XI2.h
public static void XISetMask(Span<byte> maskBuf, int evt)
=> maskBuf[evt >> 3] |= (byte)(1 << (evt & 7));
public static bool XIMaskIsSet(Span<byte> maskBuf, int evt)
=> (maskBuf[evt >> 3] & (byte)(1 << (evt & 7))) != 0;
public const int XIAllDevices = 0;
public const int XIAllMasterDevices = 1;
[StructLayout(LayoutKind.Sequential)]
public struct XIEventMask
{
public int deviceid;
public int mask_len;
public IntPtr mask;
}
[DllImport(XI2)]
public static extern int XISelectEvents(IntPtr display, IntPtr win, ref XIEventMask masks, int num_masks);
[StructLayout(LayoutKind.Sequential)]
public unsafe struct XIValuatorState
{
public int mask_len;
public byte* mask;
public double* values;
}
[StructLayout(LayoutKind.Sequential)]
public struct XIRawEvent
{
public int type;
public nuint serial;
public int send_event;
public IntPtr display;
public int extension;
public int evtype;
public nuint time;
public int deviceid;
public int sourceid;
public int detail;
public int flags;
public XIValuatorState valuators;
public unsafe double* raw_values;
}
}
}

View File

@ -6,18 +6,18 @@ namespace BizHawk.Common
{
public static class XlibImports
{
private const string LIB = "libX11.so.6";
private const string XLIB = "libX11.so.6";
[DllImport(LIB)]
[DllImport(XLIB)]
public static extern IntPtr XOpenDisplay(string? display_name);
[DllImport(LIB)]
[DllImport(XLIB)]
public static extern int XCloseDisplay(IntPtr display);
[DllImport(LIB)]
[DllImport(XLIB)]
public static extern void XLockDisplay(IntPtr display);
[DllImport(LIB)]
[DllImport(XLIB)]
public static extern void XUnlockDisplay(IntPtr display);
// helper struct for XLockDisplay/XUnlockDisplay
@ -47,7 +47,98 @@ namespace BizHawk.Common
}
}
[DllImport(LIB)]
[DllImport(XLIB)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool XQueryExtension(IntPtr display, string name, out int major_opcode_return, out int first_event_return, out int first_error_return);
[DllImport(XLIB)]
public static extern IntPtr XDefaultRootWindow(IntPtr display);
[DllImport(XLIB)]
public static extern int XPending(IntPtr display);
public enum XEventTypes : int
{
KeyPress = 2,
KeyRelease = 3,
ButtonPress = 4,
ButtonRelease = 5,
MotionNotify = 6,
EnterNotify = 7,
LeaveNotify = 8,
FocusIn = 9,
FocusOut = 10,
KeymapNotify = 11,
Expose = 12,
GraphicsExpose = 13,
NoExpose = 14,
VisibilityNotify = 15,
CreateNotify = 16,
DestroyNotify = 17,
UnmapNotify = 18,
MapNotify = 19,
MapRequest = 20,
ReparentNotify = 21,
ConfigureNotify = 22,
ConfigureRequest = 23,
GravityNotify = 24,
ResizeRequest = 25,
CirculateNotify = 26,
CirculateRequest = 27,
PropertyNotify = 28,
SelectionClear = 29,
SelectionRequest = 30,
SelectionNotify = 31,
ColormapNotify = 32,
ClientMessage = 33,
MappingNotify = 34,
GenericEvent = 35,
LASTEvent = 36
}
[StructLayout(LayoutKind.Sequential)]
public struct XGenericEventCookie
{
public XEventTypes type;
public nuint serial;
public int send_event;
public IntPtr display;
public int extension;
public int evtype;
public uint cookie;
public IntPtr data;
}
[StructLayout(LayoutKind.Sequential)]
public struct XEventPadding
{
public nint pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7;
public nint pad8, pad9, pad10, pad11, pad12, pad13, pad14, pad15;
public nint pad16, pad17, pad18, pad19, pad20, pad21, pad22, pad23;
}
[StructLayout(LayoutKind.Explicit)]
public struct XEvent
{
[FieldOffset(0)]
public XEventTypes type;
[FieldOffset(0)]
public XGenericEventCookie xcookie;
[FieldOffset(0)]
public XEventPadding pad;
}
[DllImport(XLIB)]
public static extern int XNextEvent(IntPtr display, out XEvent event_return);
[DllImport(XLIB)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool XGetEventData(IntPtr display, ref XGenericEventCookie cookie);
[DllImport(XLIB)]
public static extern int XFreeEventData(IntPtr display, ref XGenericEventCookie cookie);
[DllImport(XLIB)]
public static extern unsafe int XQueryKeymap(IntPtr display, byte* keys_return);
// copied from OpenTK
@ -464,15 +555,15 @@ namespace BizHawk.Common
public bool same_screen;
}
[DllImport(LIB)]
[DllImport(XLIB)]
[return: MarshalAs(UnmanagedType.SysUInt)]
public static extern Keysym XLookupKeysym(ref XKeyEvent key_event, int index);
[DllImport(LIB)]
[DllImport(XLIB)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool XkbQueryExtension(IntPtr display, out int opcode_rtrn, out int event_rtrn, out int error_rtrn, ref int major_in_out, ref int minor_in_out);
[DllImport(LIB)]
[DllImport(XLIB)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool XkbSetDetectableAutoRepeat(IntPtr display, [MarshalAs(UnmanagedType.Bool)] bool detectable, [MarshalAs(UnmanagedType.Bool)] out bool supported_rtrn);
@ -531,16 +622,16 @@ namespace BizHawk.Common
public IntPtr geom;
}
[DllImport(LIB)]
[DllImport(XLIB)]
public static extern unsafe XkbDescRec* XkbAllocKeyboard(IntPtr display);
[DllImport(LIB)]
[DllImport(XLIB)]
public static extern unsafe void XkbFreeKeyboard(XkbDescRec* xkb, int which, [MarshalAs(UnmanagedType.Bool)] bool free_all);
[DllImport(LIB)]
[DllImport(XLIB)]
public static extern unsafe int XkbGetNames(IntPtr display, uint which, XkbDescRec* xkb);
[DllImport(LIB)]
[DllImport(XLIB)]
public static extern Keysym XkbKeycodeToKeysym(IntPtr display, int keycode, int group, int level);
}
}

View File

@ -427,12 +427,22 @@ namespace BizHawk.Common
[StructLayout(LayoutKind.Sequential)]
public struct RAWMOUSE
{
public ushort usFlags;
public MOUSE_FLAGS usFlags;
public uint ulButtons;
public uint ulRawButtons;
public int lLastX;
public int lLastY;
public uint ulExtraInformation;
[Flags]
public enum MOUSE_FLAGS : ushort
{
MOVE_RELATIVE = 0,
MOVE_ABSOLUTE = 1,
VIRTUAL_DESKTOP = 2,
ATTRIBUTES_CHANGED = 4,
MOVE_NOCOALESCE = 8,
}
}
[StructLayout(LayoutKind.Sequential)]
@ -492,7 +502,7 @@ namespace BizHawk.Common
[DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool RegisterRawInputDevices(ref RAWINPUTDEVICE pRawInputDevice, uint uiNumDevices, int cbSize);
public static extern unsafe bool RegisterRawInputDevices(RAWINPUTDEVICE* pRawInputDevice, uint uiNumDevices, int cbSize);
[DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]

View File

@ -130,5 +130,8 @@ namespace BizHawk.Common
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
[return: MarshalAs(UnmanagedType.U1)]
public static extern bool CGEventSourceKeyState(CGEventSourceStateID stateID, CGKeyCode key);
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
public static extern void CGGetLastMouseDelta(out int deltaX, out int deltaY);
}
}