add RAWInput keyboard handler, use it for OSTailoredKeyInputAdapter / SDL2

re-removes hard DirectInput dependency
This commit is contained in:
CasualPokePlayer 2023-10-12 00:36:13 -07:00
parent a320928a6f
commit 5fc37d6aac
4 changed files with 371 additions and 8 deletions

View File

@ -82,7 +82,8 @@ namespace BizHawk.Bizware.Input
return VKeyToDKeyMap.GetValueOrDefault(virtualKey, DInputKey.Unknown);
}
private static readonly IReadOnlyDictionary<DInputKey, DistinctKey> KeyEnumMap = new Dictionary<DInputKey, DistinctKey>
// DInputKey is just a scancode so it's used with RAWKeyInput
/*private*/ internal static readonly IReadOnlyDictionary<DInputKey, DistinctKey> KeyEnumMap = new Dictionary<DInputKey, DistinctKey>
{
[DInputKey.D0] = DistinctKey.D0,
[DInputKey.D1] = DistinctKey.D1,
@ -231,7 +232,7 @@ namespace BizHawk.Bizware.Input
[DInputKey.Unknown] = DistinctKey.Unknown
};
private static readonly IReadOnlyDictionary<uint, DInputKey> VKeyToDKeyMap = new Dictionary<uint, DInputKey>
/*private*/ internal static readonly IReadOnlyDictionary<uint, DInputKey> VKeyToDKeyMap = new Dictionary<uint, DInputKey>
{
[0x30] = DInputKey.D0,
[0x31] = DInputKey.D1,

View File

@ -33,7 +33,7 @@ namespace BizHawk.Bizware.Input
//break;
throw new NotSupportedException("TODO QUARTZ");
case OSTailoredCode.DistinctOS.Windows:
DKeyInput.Cleanup();
RAWKeyInput.Deinitialize();
break;
default:
throw new InvalidOperationException();
@ -54,10 +54,7 @@ namespace BizHawk.Bizware.Input
//break;
throw new NotSupportedException("TODO QUARTZ");
case OSTailoredCode.DistinctOS.Windows:
// TODO: Consider if we want to use RAWINPUT API for keyboards instead
// Would remove DInput depenency on Windows (DInput gamepads could be considered optional in this sense)
// (also, this would be needed for keyboard support with UWP, which doesn't support DInput)
DKeyInput.Initialize(mainFormHandle);
RAWKeyInput.Initialize();
break;
default:
throw new InvalidOperationException();
@ -80,7 +77,7 @@ namespace BizHawk.Bizware.Input
{
OSTailoredCode.DistinctOS.Linux => X11KeyInput.Update(),
OSTailoredCode.DistinctOS.macOS => throw new NotSupportedException("TODO QUARTZ"),
OSTailoredCode.DistinctOS.Windows => DKeyInput.Update(_config ?? throw new(nameof(ProcessHostKeyboards) + " called before the global config was passed")),
OSTailoredCode.DistinctOS.Windows => RAWKeyInput.Update(_config ?? throw new(nameof(ProcessHostKeyboards) + " called before the global config was passed")),
_ => throw new InvalidOperationException()
};

View File

@ -0,0 +1,193 @@
#nullable enable
using System;
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.Win32Imports;
using RAWKey = Vortice.DirectInput.Key;
namespace BizHawk.Bizware.Input
{
internal static class RAWKeyInput
{
private static volatile bool _isInit;
private static IntPtr _rawInputWindowAtom;
private static IntPtr _rawInputWindow;
private static bool _handleAlternativeKeyboardLayouts;
private static List<KeyEvent> _keyEvents = new();
private static readonly WNDPROC _wndProc = WndProc;
private static readonly object _lockObj = new();
private static unsafe IntPtr WndProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam)
{
const uint WM_INPUT = 0x00FF;
if (uMsg != WM_INPUT)
{
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
if (GetRawInputData(lParam, RID.INPUT, IntPtr.Zero,
out var size, Marshal.SizeOf<RAWINPUTHEADER>()) == -1)
{
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
// don't think size should ever be this big, but just in case
var buffer = size > 1024
? new byte[size]
: stackalloc byte[size];
fixed (byte* p = buffer)
{
var input = (RAWINPUT*)p;
if (GetRawInputData(lParam, RID.INPUT, input,
ref size, Marshal.SizeOf<RAWINPUTHEADER>()) == -1)
{
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
if (input->header.dwType == RAWINPUTHEADER.RIM_TYPE.KEYBOARD && input->data.keyboard.Flags <= RAWKEYBOARD.RIM_KEY.E1)
{
var rawKey = _handleAlternativeKeyboardLayouts
? DKeyInput.VKeyToDKeyMap.GetValueOrDefault(input->data.keyboard.VKey, RAWKey.Unknown)
: (RAWKey)(input->data.keyboard.MakeCode |
(input->data.keyboard.Flags >= RAWKEYBOARD.RIM_KEY.E0 ? 0x80 : 0));
if (DKeyInput.KeyEnumMap.TryGetValue(rawKey, out var key) && key != DistinctKey.Unknown)
{
_keyEvents.Add(new(key, input->data.keyboard.Flags is RAWKEYBOARD.RIM_KEY.MAKE or RAWKEYBOARD.RIM_KEY.E0));
}
}
return DefRawInputProc(input, 0, Marshal.SizeOf<RAWINPUTHEADER>());
}
}
private static void CreateRawInputWindow()
{
const int WS_CHILD = 0x40000000;
var window = CreateWindowEx(
dwExStyle: 0,
lpClassName: _rawInputWindowAtom,
lpWindowName: "RAWKeyInput",
dwStyle: WS_CHILD,
X: 0,
Y: 0,
nWidth: 1,
nHeight: 1,
hWndParent: HWND_MESSAGE,
hMenu: IntPtr.Zero,
hInstance: GetModuleHandle(null),
lpParam: IntPtr.Zero);
if (window == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to create RAWINPUT window");
}
var ri = new RAWINPUTDEVICE[1];
ri[0].usUsagePage = RAWINPUTDEVICE.HidUsagePage.GENERIC;
ri[0].usUsage = RAWINPUTDEVICE.HidUsageId.GENERIC_KEYBOARD;
ri[0].dwFlags = RAWINPUTDEVICE.RIDEV.INPUTSINK;
ri[0].hwndTarget = window;
if (!RegisterRawInputDevices(ri, 1, Marshal.SizeOf<RAWINPUTDEVICE>()))
{
DestroyWindow(window);
throw new InvalidOperationException("Failed to register RAWINPUTDEVICE");
}
_rawInputWindow = window;
}
public static void Initialize()
{
if (OSTailoredCode.IsUnixHost)
{
throw new NotSupportedException("RAWINPUT is Windows only");
}
lock (_lockObj)
{
Deinitialize();
if (_rawInputWindowAtom == IntPtr.Zero)
{
var wc = default(WNDCLASS);
wc.lpfnWndProc = _wndProc;
wc.hInstance = GetModuleHandle(null);
wc.lpszClassName = "RAWKeyInputClass";
_rawInputWindowAtom = RegisterClass(ref wc);
if (_rawInputWindowAtom == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to register RAWINPUT window class");
}
// we can't use a window created on this thread, as Update is called on a different thread
// but we can still test window creation
CreateRawInputWindow(); // this will throw if window creation or rawinput registering fails
DestroyWindow(_rawInputWindow);
_rawInputWindow = IntPtr.Zero;
_isInit = true;
}
}
}
public static void Deinitialize()
{
lock (_lockObj)
{
if (_rawInputWindow != IntPtr.Zero)
{
// Can't use DestroyWindow, that's only allowed in the thread that created the window!
const int WM_CLOSE = 0x0010;
PostMessage(_rawInputWindow, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
_rawInputWindow = IntPtr.Zero;
}
_keyEvents.Clear();
_isInit = false;
}
}
public static IEnumerable<KeyEvent> Update(Config config)
{
lock (_lockObj)
{
if (!_isInit)
{
return Enumerable.Empty<KeyEvent>();
}
if (_rawInputWindow == IntPtr.Zero)
{
CreateRawInputWindow();
}
_handleAlternativeKeyboardLayouts = config.HandleAlternateKeyboardLayouts;
while (PeekMessage(out var msg, _rawInputWindow, 0, 0, PM_REMOVE))
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
var ret = _keyEvents;
_keyEvents = new();
return ret;
}
}
}
}

View File

@ -11,6 +11,7 @@ namespace BizHawk.Common
{
public const int MAX_PATH = 260;
public const uint PM_REMOVE = 0x0001U;
public static readonly IntPtr HWND_MESSAGE = new(-3);
public delegate int BFFCALLBACK(IntPtr hwnd, uint uMsg, IntPtr lParam, IntPtr lpData);
@ -98,6 +99,144 @@ namespace BizHawk.Common
public int y;
}
[StructLayout(LayoutKind.Sequential)]
public struct RAWINPUTDEVICE
{
public HidUsagePage usUsagePage;
public HidUsageId usUsage;
public RIDEV dwFlags;
public IntPtr hwndTarget;
public enum HidUsagePage : ushort
{
GENERIC = 1,
GAME = 5,
LED = 8,
BUTTON = 9,
}
public enum HidUsageId : ushort
{
GENERIC_POINTER = 1,
GENERIC_MOUSE = 2,
GENERIC_JOYSTICK = 4,
GENERIC_GAMEPAD = 5,
GENERIC_KEYBOARD = 6,
GENERIC_KEYPAD = 7,
GENERIC_MULTI_AXIS_CONTROLLER = 8,
}
[Flags]
public enum RIDEV : int
{
REMOVE = 0x00000001,
EXCLUDE = 0x00000010,
PAGEONLY = 0x00000020,
NOLEGACY = PAGEONLY | EXCLUDE,
INPUTSINK = 0x00000100,
CAPTUREMOUSE = 0x00000200,
NOHOTKEYS = CAPTUREMOUSE,
APPKEYS = 0x00000400,
EXINPUTSINK = 0x00001000,
DEVNOTIFY = 0x00002000,
}
}
public enum RID : uint
{
HEADER = 0x10000005,
INPUT = 0x10000003,
}
[StructLayout(LayoutKind.Sequential)]
public struct RAWINPUTHEADER
{
public RIM_TYPE dwType;
public uint dwSize;
public IntPtr hDevice;
public IntPtr wParam;
public enum RIM_TYPE : uint
{
MOUSE = 0,
KEYBOARD = 1,
HID = 2,
}
}
[StructLayout(LayoutKind.Sequential)]
public struct RAWMOUSE
{
public ushort usFlags;
public uint ulButtons;
public uint ulRawButtons;
public int lLastX;
public int lLastY;
public uint ulExtraInformation;
}
[StructLayout(LayoutKind.Sequential)]
public struct RAWKEYBOARD
{
public ushort MakeCode;
public RIM_KEY Flags;
public ushort Reserved;
public ushort VKey;
public uint Message;
public uint ExtraInformation;
public enum RIM_KEY : ushort
{
MAKE = 0,
BREAK = 1,
E0 = 2,
E1 = 3,
}
}
[StructLayout(LayoutKind.Sequential)]
public struct RAWHID
{
public uint dwSizeHid;
public uint dwCount;
public byte bRawData;
}
[StructLayout(LayoutKind.Explicit)]
public struct RAWINPUTDATA
{
[FieldOffset(0)]
public RAWMOUSE mouse;
[FieldOffset(0)]
public RAWKEYBOARD keyboard;
[FieldOffset(0)]
public RAWHID hid;
}
[StructLayout(LayoutKind.Sequential)]
public struct RAWINPUT
{
public RAWINPUTHEADER header;
public RAWINPUTDATA data;
}
public delegate IntPtr WNDPROC(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct WNDCLASS
{
public uint style;
public WNDPROC lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
public string lpszMenuName;
public string lpszClassName;
}
[Guid("00000002-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMalloc
@ -113,9 +252,23 @@ namespace BizHawk.Common
[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern uint _control87(uint @new, uint mask);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CreateWindowEx(int dwExStyle, IntPtr lpClassName, string lpWindowName,
int dwStyle, int X, int Y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
[DllImport("kernel32.dll", EntryPoint = "DeleteFileW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern bool DeleteFileW([MarshalAs(UnmanagedType.LPWStr)] string lpFileName);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern unsafe IntPtr DefRawInputProc(RAWINPUT* paRawInput, int nInput, int cbSizeHeader);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DestroyWindow(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr DispatchMessage([In] ref MSG lpMsg);
@ -125,12 +278,21 @@ namespace BizHawk.Common
[DllImport("user32.dll")]
public static extern IntPtr GetActiveWindow();
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32", SetLastError = true, EntryPoint = "GetProcAddress")]
public static extern IntPtr GetProcAddressOrdinal(IntPtr hModule, IntPtr procName);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetProcessHeap();
[DllImport("user32.dll")]
public static extern int GetRawInputData(IntPtr hRawInput, RID uiCommand, IntPtr pData, out int bSize, int cbSizeHeader);
[DllImport("user32.dll")]
public static extern unsafe int GetRawInputData(IntPtr hRawInput, RID uiCommand, RAWINPUT* pData, ref int bSize, int cbSizeHeader);
[DllImport("kernel32.dll", SetLastError = false)]
public static extern IntPtr HeapAlloc(IntPtr hHeap, uint dwFlags, int dwBytes);
@ -157,6 +319,16 @@ namespace BizHawk.Common
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr RegisterClass([In] ref WNDCLASS lpWndClass);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool RegisterRawInputDevices(RAWINPUTDEVICE[] pRawInputDevices, uint uiNumDevices, int cbSize);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, ref HDITEM lParam);