diff --git a/src/BizHawk.Bizware.Input/DirectX/DKeyInput.cs b/src/BizHawk.Bizware.Input/DirectX/DKeyInput.cs index c88a1e8a87..8fbd178464 100644 --- a/src/BizHawk.Bizware.Input/DirectX/DKeyInput.cs +++ b/src/BizHawk.Bizware.Input/DirectX/DKeyInput.cs @@ -82,7 +82,8 @@ namespace BizHawk.Bizware.Input return VKeyToDKeyMap.GetValueOrDefault(virtualKey, DInputKey.Unknown); } - private static readonly IReadOnlyDictionary KeyEnumMap = new Dictionary + // DInputKey is just a scancode so it's used with RAWKeyInput + /*private*/ internal static readonly IReadOnlyDictionary KeyEnumMap = new Dictionary { [DInputKey.D0] = DistinctKey.D0, [DInputKey.D1] = DistinctKey.D1, @@ -231,7 +232,7 @@ namespace BizHawk.Bizware.Input [DInputKey.Unknown] = DistinctKey.Unknown }; - private static readonly IReadOnlyDictionary VKeyToDKeyMap = new Dictionary + /*private*/ internal static readonly IReadOnlyDictionary VKeyToDKeyMap = new Dictionary { [0x30] = DInputKey.D0, [0x31] = DInputKey.D1, diff --git a/src/BizHawk.Bizware.Input/OSTailoredKeyInputAdapter.cs b/src/BizHawk.Bizware.Input/OSTailoredKeyInputAdapter.cs index 10b22fd1e8..3366147cdc 100644 --- a/src/BizHawk.Bizware.Input/OSTailoredKeyInputAdapter.cs +++ b/src/BizHawk.Bizware.Input/OSTailoredKeyInputAdapter.cs @@ -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() }; diff --git a/src/BizHawk.Bizware.Input/RAWINPUT/RAWKeyInput.cs b/src/BizHawk.Bizware.Input/RAWINPUT/RAWKeyInput.cs new file mode 100644 index 0000000000..db60af4d88 --- /dev/null +++ b/src/BizHawk.Bizware.Input/RAWINPUT/RAWKeyInput.cs @@ -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 _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()) == -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()) == -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()); + } + } + + 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())) + { + 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 Update(Config config) + { + lock (_lockObj) + { + if (!_isInit) + { + return Enumerable.Empty(); + } + + 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; + } + } + } +} diff --git a/src/BizHawk.Common/Win32/Win32Imports.cs b/src/BizHawk.Common/Win32/Win32Imports.cs index a629eb9d85..951e6584d1 100644 --- a/src/BizHawk.Common/Win32/Win32Imports.cs +++ b/src/BizHawk.Common/Win32/Win32Imports.cs @@ -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);