From d93aa344be9bbfd9547906a4a6237ea9c055cddf Mon Sep 17 00:00:00 2001 From: AlexGuo1998 Date: Mon, 24 Feb 2025 19:38:20 +0800 Subject: [PATCH 1/2] Refactor rawmouse --- .../Platform/Windows/Native/Input.cs | 2 +- .../Platform/Windows/SDL2WindowsWindow.cs | 23 ++++ .../Platform/Windows/SDL3WindowsWindow.cs | 26 ++++ .../Platform/Windows/WindowsMouseHandler.cs | 4 +- .../Windows/WindowsMouseHandler_SDL2.cs | 42 +++---- .../Windows/WindowsRawInputManager.cs | 111 ++++++++++++++++++ 6 files changed, 177 insertions(+), 31 deletions(-) create mode 100644 osu.Framework/Platform/Windows/WindowsRawInputManager.cs diff --git a/osu.Framework/Platform/Windows/Native/Input.cs b/osu.Framework/Platform/Windows/Native/Input.cs index cb1d7c736d..d3ac677443 100644 --- a/osu.Framework/Platform/Windows/Native/Input.cs +++ b/osu.Framework/Platform/Windows/Native/Input.cs @@ -19,7 +19,7 @@ public static extern bool RegisterRawInputDevices( int cbSize); [DllImport("user32.dll")] - public static extern int GetRawInputData(IntPtr hRawInput, RawInputCommand uiCommand, out RawInputData pData, ref int pcbSize, int cbSizeHeader); + public static extern int GetRawInputData(IntPtr hRawInput, RawInputCommand uiCommand, IntPtr pData, ref int pcbSize, int cbSizeHeader); internal static Rectangle VirtualScreenRect => new Rectangle( GetSystemMetrics(SM_XVIRTUALSCREEN), diff --git a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs index 21e6441917..c5550df9d9 100644 --- a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs @@ -37,6 +37,11 @@ internal class SDL2WindowsWindow : SDL2DesktopWindow, IWindowsWindow /// private readonly bool applyBorderlessWindowHack; + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable (this callback needs to be kept around) + private readonly SDL_WindowsMessageHook sdl2Callback; + + public readonly WindowsRawInputManager RawInputManager; + public SDL2WindowsWindow(GraphicsSurfaceType surfaceType, string appName) : base(surfaceType, appName) { @@ -54,6 +59,12 @@ public SDL2WindowsWindow(GraphicsSurfaceType surfaceType, string appName) if (!declareDpiAwareV2()) declareDpiAware(); + + RawInputManager = new WindowsRawInputManager(WindowHandle); + + // ReSharper disable once ConvertClosureToMethodGroup + sdl2Callback = (ptr, wnd, u, param, l) => windowsMessageHookSDL2(ptr, wnd, u, param, l); + SDL_SetWindowsMessageHook(sdl2Callback, WindowHandle); } private bool declareDpiAwareV2() @@ -92,6 +103,18 @@ public override void Create() SDL_EventState(SDL_EventType.SDL_SYSWMEVENT, SDL_ENABLE); } + private IntPtr windowsMessageHookSDL2(IntPtr userData, IntPtr hWnd, uint message, ulong wParam, long lParam) + { + if (message != Native.Input.WM_INPUT) + return IntPtr.Zero; + +#pragma warning disable CA2020 // Prevent behavioral change for IntPtr conversion + RawInputManager.ProcessWmInput((IntPtr)lParam); +#pragma warning restore CA2020 + + return IntPtr.Zero; + } + protected override void HandleEventFromFilter(SDL_Event e) { if (e.type == SDL_EventType.SDL_SYSWMEVENT) diff --git a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs index ad651d80a0..427846be2d 100644 --- a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs @@ -3,8 +3,10 @@ using System; using System.Drawing; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; +using osu.Framework.Allocation; using osu.Framework.Input; using osu.Framework.Input.Handlers.Mouse; using osu.Framework.Platform.SDL3; @@ -34,6 +36,8 @@ internal class SDL3WindowsWindow : SDL3DesktopWindow, IWindowsWindow /// private readonly bool applyBorderlessWindowHack; + private readonly WindowsRawInputManager rawInputManager; + public SDL3WindowsWindow(GraphicsSurfaceType surfaceType, string appName) : base(surfaceType, appName) { @@ -48,6 +52,13 @@ public SDL3WindowsWindow(GraphicsSurfaceType surfaceType, string appName) applyBorderlessWindowHack = false; break; } + + rawInputManager = new WindowsRawInputManager(WindowHandle); + + unsafe + { + SDL_SetWindowsMessageHook(&windowsMessageHook, ObjectHandle.Handle); + } } public override void Create() @@ -71,6 +82,21 @@ protected override bool HandleEventFromFilter(SDL_Event e) return base.HandleEventFromFilter(e); } + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static unsafe SDLBool windowsMessageHook(IntPtr userdata, MSG* msg) + { + if (msg->message != Native.Input.WM_INPUT) + return true; + + var handle = new ObjectHandle(userdata); + if (!handle.GetTarget(out SDL3WindowsWindow window)) + return true; + + window.rawInputManager.ProcessWmInput(msg->lParam); + + return true; + } + public Vector2? LastMousePosition { get; set; } /// diff --git a/osu.Framework/Platform/Windows/WindowsMouseHandler.cs b/osu.Framework/Platform/Windows/WindowsMouseHandler.cs index 4abe150a6e..c88cb84db6 100644 --- a/osu.Framework/Platform/Windows/WindowsMouseHandler.cs +++ b/osu.Framework/Platform/Windows/WindowsMouseHandler.cs @@ -25,8 +25,8 @@ public override bool Initialize(GameHost host) window = windowsWindow; - if (window is SDL2WindowsWindow) - initialiseSDL2(host); + if (window is SDL2WindowsWindow sdl2WindowsWindow) + initialiseSDL2(sdl2WindowsWindow); return base.Initialize(host); } diff --git a/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs b/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs index cf568ee19d..54908f1de7 100644 --- a/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs +++ b/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs @@ -1,14 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Drawing; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Input.StateChanges; using osu.Framework.Platform.Windows.Native; using osu.Framework.Statistics; using osuTK; -using static SDL2.SDL; namespace osu.Framework.Platform.Windows { @@ -23,16 +21,18 @@ internal partial class WindowsMouseHandler private const int raw_input_coordinate_space = 65535; - private SDL_WindowsMessageHook sdl2Callback = null!; - - private void initialiseSDL2(GameHost host) + private void initialiseSDL2(SDL2WindowsWindow window) { - // ReSharper disable once ConvertClosureToMethodGroup - sdl2Callback = (ptr, wnd, u, param, l) => onWndProcSDL2(ptr, wnd, u, param, l); - Enabled.BindValueChanged(enabled => { - host.InputThread.Scheduler.Add(() => SDL_SetWindowsMessageHook(enabled.NewValue ? sdl2Callback : null, IntPtr.Zero)); + if (enabled.NewValue) + { + window.RawInputManager.RawMouse += onRawMouse; + } + else + { + window.RawInputManager.RawMouse -= onRawMouse; + } }, true); } @@ -47,38 +47,26 @@ protected override void HandleMouseMoveRelative(Vector2 delta) base.HandleMouseMoveRelative(delta); } - private unsafe IntPtr onWndProcSDL2(IntPtr userData, IntPtr hWnd, uint message, ulong wParam, long lParam) + private void onRawMouse(RawInputData data) { if (!Enabled.Value) - return IntPtr.Zero; - - if (message != Native.Input.WM_INPUT) - return IntPtr.Zero; + return; if (Native.Input.IsTouchEvent(Native.Input.GetMessageExtraInfo())) { // sometimes GetMessageExtraInfo returns 0, so additionally, mouse.ExtraInformation is checked below. // touch events are handled by TouchHandler statistic_dropped_touch_inputs.Value++; - return IntPtr.Zero; + return; } - int payloadSize = sizeof(RawInputData); - -#pragma warning disable CA2020 // Prevent behavioral change for IntPtr conversion - Native.Input.GetRawInputData((IntPtr)lParam, RawInputCommand.Input, out var data, ref payloadSize, sizeof(RawInputHeader)); -#pragma warning restore CA2020 - - if (data.Header.Type != RawInputType.Mouse) - return IntPtr.Zero; - var mouse = data.Mouse; // `ExtraInformation` doesn't have the MI_WP_SIGNATURE set, so we have to rely solely on the touch flag. if (Native.Input.HasTouchFlag(mouse.ExtraInformation)) { statistic_dropped_touch_inputs.Value++; - return IntPtr.Zero; + return; } //TODO: this isn't correct. @@ -103,7 +91,7 @@ private unsafe IntPtr onWndProcSDL2(IntPtr userData, IntPtr hWnd, uint message, if (mouse.LastX == 0 && mouse.LastY == 0) { // not sure if this is the case for all tablets, but on osu!tablet these can appear and are noise. - return IntPtr.Zero; + return; } // i am not sure what this 64 flag is, but it's set on the osu!tablet at very least. @@ -137,8 +125,6 @@ private unsafe IntPtr onWndProcSDL2(IntPtr userData, IntPtr hWnd, uint message, PendingInputs.Enqueue(new MousePositionRelativeInput { Delta = new Vector2(mouse.LastX, mouse.LastY) * sensitivity }); statistic_relative_events.Value++; } - - return IntPtr.Zero; } } } diff --git a/osu.Framework/Platform/Windows/WindowsRawInputManager.cs b/osu.Framework/Platform/Windows/WindowsRawInputManager.cs new file mode 100644 index 0000000000..1e525f7457 --- /dev/null +++ b/osu.Framework/Platform/Windows/WindowsRawInputManager.cs @@ -0,0 +1,111 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Runtime.InteropServices; +using osu.Framework.Logging; +using osu.Framework.Platform.Windows.Native; + +namespace osu.Framework.Platform.Windows +{ + public class WindowsRawInputManager + { + /// Fired when raw mouse input is detected in . + public event Action? RawMouse + { + add + { + if (rawMouse == null) + register(HIDUsagePage.Generic, HIDUsage.Mouse, true); + rawMouse += value; + } + remove + { + rawMouse -= value; + if (rawMouse == null) + register(HIDUsagePage.Generic, HIDUsage.Mouse, false); + } + } + + private event Action? rawMouse; + + /// The HWND associated with the manager. Used on registering. + public readonly IntPtr WindowHandle; + + public WindowsRawInputManager(IntPtr windowHandle) + { + WindowHandle = windowHandle; + } + + /// + /// Register a HID device to use with raw input. + /// + /// Usage page of the HID device. + /// Usage of the HID device (meaning of the value depend on the page). + /// Register (true) or unregister (false). + private unsafe void register(HIDUsagePage usagePage, HIDUsage usage, bool enable) + { + var registration = new RawInputDevice + { + UsagePage = usagePage, + Usage = usage, + Flags = enable ? RawInputDeviceFlags.None : RawInputDeviceFlags.Remove, + WindowHandle = WindowHandle + }; + + bool r = Native.Input.RegisterRawInputDevices([registration], 1, sizeof(RawInputDevice)); + + if (!r) + { + Logger.Log($"RegisterRawInputDevices failed ({Marshal.GetLastWin32Error()}): touchpad reading not possible", + LoggingTarget.Input, LogLevel.Error); + } + } + + /// + /// Process the WM_INPUT message. Only lParam is needed. + /// + /// A HRAWINPUT handle to the RAWINPUT structure that contains the raw input from the device. + public unsafe void ProcessWmInput(IntPtr lParam) + { + int size = 0; + Native.Input.GetRawInputData(lParam, RawInputCommand.Input, IntPtr.Zero, ref size, sizeof(RawInputHeader)); + IntPtr buffer = Marshal.AllocHGlobal(size); + + try + { + if (Native.Input.GetRawInputData(lParam, RawInputCommand.Input, buffer, ref size, sizeof(RawInputHeader)) < 0) + { + Logger.Log($"GetRawInputData failed ({Marshal.GetLastWin32Error()})", LoggingTarget.Input, LogLevel.Error); + return; + } + + RawInputType rawInputType = Marshal.PtrToStructure(buffer).Type; + + switch (rawInputType) + { + case RawInputType.Mouse: + { + if (size < sizeof(RawInputData)) + { + Logger.Log($"Raw mouse buffer too small ({size} < {sizeof(RawInputData)})", LoggingTarget.Input, LogLevel.Error); + return; + } + + rawMouse?.Invoke(Marshal.PtrToStructure(buffer)); + break; + } + + case RawInputType.Keyboard: + case RawInputType.HID: + default: + break; + } + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + } +} From 03ec844deccc3f43f994c83eebfd343307bc3190 Mon Sep 17 00:00:00 2001 From: AlexGuo1998 Date: Mon, 24 Feb 2025 19:39:03 +0800 Subject: [PATCH 2/2] Add Windows precision touchpad (PTP) support (PoC) --- .../Handlers/Touchpad/TouchpadHandler.cs | 112 +++++++ osu.Framework/Platform/IHasTouchpadInput.cs | 77 +++++ osu.Framework/Platform/SDLGameHost.cs | 3 + osu.Framework/Platform/Windows/Native/Hid.cs | 159 ++++++++++ .../Platform/Windows/Native/Input.cs | 122 +++++++- .../Platform/Windows/SDL2WindowsWindow.cs | 6 +- .../Platform/Windows/SDL3WindowsWindow.cs | 6 +- .../Windows/WindowsRawInputManager.cs | 50 +++- .../Platform/Windows/WindowsTouchpadReader.cs | 274 ++++++++++++++++++ 9 files changed, 795 insertions(+), 14 deletions(-) create mode 100644 osu.Framework/Input/Handlers/Touchpad/TouchpadHandler.cs create mode 100644 osu.Framework/Platform/IHasTouchpadInput.cs create mode 100644 osu.Framework/Platform/Windows/Native/Hid.cs create mode 100644 osu.Framework/Platform/Windows/WindowsTouchpadReader.cs diff --git a/osu.Framework/Input/Handlers/Touchpad/TouchpadHandler.cs b/osu.Framework/Input/Handlers/Touchpad/TouchpadHandler.cs new file mode 100644 index 0000000000..bd70171b48 --- /dev/null +++ b/osu.Framework/Input/Handlers/Touchpad/TouchpadHandler.cs @@ -0,0 +1,112 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Framework.Input.StateChanges; +using osu.Framework.Platform; +using osu.Framework.Statistics; +using osuTK; +using osuTK.Input; + +namespace osu.Framework.Input.Handlers.Touchpad +{ + /// + /// For implementing . Translate the touchpad events to mouse events. + /// + public class TouchpadHandler : InputHandler, IHasCursorSensitivity + { + private static readonly GlobalStatistic statistic_total_events = GlobalStatistics.Get(StatisticGroupFor(), "Total events"); + + public override string Description => "Touchpad"; + + public override bool IsActive => true; + + public BindableDouble Sensitivity { get; } = new BindableDouble(1) + { + MinValue = 1, + MaxValue = 10, + Precision = 0.01 + }; + + private IHasTouchpadInput? window; + + public override bool Initialize(GameHost host) + { + if (!base.Initialize(host)) + return false; + + if (host.Window is not IHasTouchpadInput hasTouchpadInput) + return false; + + window = hasTouchpadInput; + + Enabled.BindValueChanged(enabled => + { + if (enabled.NewValue) + { + window!.TouchpadDataUpdate += handleTouchpadUpdate; + } + else + { + window!.TouchpadDataUpdate -= handleTouchpadUpdate; + } + }, true); + + return true; + } + + public override void Reset() + { + Sensitivity.SetDefault(); + base.Reset(); + } + + private void handleTouchpadUpdate(TouchpadData data) + { + // We just use the first reported point (For PoC). + // This might not be the first finger touched. + foreach (var point in data.Points) + { + if (!point.Valid || !point.Confidence) continue; + + var position = mapToWindow(data.Info, point); + enqueueInput(new MousePositionAbsoluteInput { Position = position }); + break; + } + + // TODO Real mouse button event should be suppressed (???) otherwise tapping can be converted to clicks by the OS + // TODO only enqueue when state changed + enqueueInput(new MouseButtonInput(MouseButton.Left, data.ButtonDown)); + } + + private Vector2 mapToWindow(TouchpadInfo info, TouchpadPoint point) + { + var center = window!.Size / 2; + + // centered (-range/2 ~ range/2) + int x = point.X - info.XMin - info.XRange / 2; + int y = point.Y - info.YMin - info.YRange / 2; + + // Minimum ratio to cover the whole window + float minimumRatio = Math.Max( + (float)window.Size.Width / info.XRange, + (float)window.Size.Height / info.YRange); + var toWindow = new Vector2( + center.Width + x * minimumRatio * (float)Sensitivity.Value, + center.Height + y * minimumRatio * (float)Sensitivity.Value); + + return Vector2.Clamp( + toWindow, + Vector2.Zero, + new Vector2(window.Size.Width - 1, window.Size.Height - 1)); + } + + private void enqueueInput(IInput input) + { + PendingInputs.Enqueue(input); + FrameStatistics.Increment(StatisticsCounterType.MouseEvents); + statistic_total_events.Value++; + } + } +} diff --git a/osu.Framework/Platform/IHasTouchpadInput.cs b/osu.Framework/Platform/IHasTouchpadInput.cs new file mode 100644 index 0000000000..9f1ea64a7c --- /dev/null +++ b/osu.Framework/Platform/IHasTouchpadInput.cs @@ -0,0 +1,77 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; + +namespace osu.Framework.Platform +{ + /// + /// Window has touchpad input reported + /// + internal interface IHasTouchpadInput : IWindow + { + /// + /// Publish the touchpad data. Read by . + /// + public event Action? TouchpadDataUpdate; + } + + /// Information for the whole touchpad. + public struct TouchpadData + { + /// Static information. + public readonly TouchpadInfo Info; + + /// Valid touch points. + public readonly List Points; + + /// Is the touchpad pressed down? + public readonly bool ButtonDown; + + public TouchpadData(TouchpadInfo info, List points, bool buttonDown) + { + Info = info; + Points = points; + ButtonDown = buttonDown; + } + } + + /// Information for the whole touchpad. + public struct TouchpadInfo + { + /// Arbitrary numerical value to differentiate individual touchpads. + public IntPtr Handle; + + /// The limit for the raw XY values. + /// + /// To map the values to 0~1, use `(value-min)/range`. + /// The YRange/XRange value is the aspect ratio of the touchpad. + /// + public int XMin, YMin, XRange, YRange; + } + + /// Information for every touch point. + public struct TouchpadPoint + { + public int X, Y; + + /// Unique ID for the contact. + /// + /// The position of one contact in the array may and will change, if fingers are added or removed. + /// + public int ContactId; + + /// Is finger in contact with the touchpad? + /// + /// If false, the XY coordinate may still be valid, but the finger can be in a hover state. + /// + public bool Valid; + + /// Is touch point too large to be considered invalid? + /// + /// If false, this contact may be a palm-touchpad contact. + /// + public bool Confidence; + } +} diff --git a/osu.Framework/Platform/SDLGameHost.cs b/osu.Framework/Platform/SDLGameHost.cs index fdc3e6ab4d..631a05c92e 100644 --- a/osu.Framework/Platform/SDLGameHost.cs +++ b/osu.Framework/Platform/SDLGameHost.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Handlers.Pen; using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Input.Handlers.Touch; +using osu.Framework.Input.Handlers.Touchpad; using osu.Framework.Platform.SDL2; using osu.Framework.Platform.SDL3; using SixLabors.ImageSharp.Formats.Png; @@ -50,6 +51,8 @@ protected override IEnumerable CreateAvailableInputHandlers() if (FrameworkEnvironment.UseSDL3) yield return new PenHandler(); + // Touchpad should get priority over mouse, same reason as tablets. + yield return new TouchpadHandler(); yield return new MouseHandler(); yield return new TouchHandler(); yield return new JoystickHandler(); diff --git a/osu.Framework/Platform/Windows/Native/Hid.cs b/osu.Framework/Platform/Windows/Native/Hid.cs new file mode 100644 index 0000000000..bede574723 --- /dev/null +++ b/osu.Framework/Platform/Windows/Native/Hid.cs @@ -0,0 +1,159 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Runtime.InteropServices; + +// ReSharper disable InconsistentNaming +// (We are using the original names from the Windows API with type prefix removed.) + +namespace osu.Framework.Platform.Windows.Native +{ + internal class Hid + { + public const long HIDP_STATUS_SUCCESS = 0x00110000; + + [DllImport("hid.dll")] + public static extern long HidP_GetCaps(IntPtr PreparsedData, out HIDP_CAPS Capabilities); + + [DllImport("hid.dll")] + public static extern long HidP_GetValueCaps(HIDP_REPORT_TYPE ReportType, [Out] HIDP_VALUE_CAPS[] ValueCaps, ref ulong ValueCapsLength, IntPtr PreparsedData); + + [DllImport("hid.dll")] + public static extern long HidP_GetLinkCollectionNodes([Out] HIDP_LINK_COLLECTION_NODE[] LinkCollectionNodes, ref ulong LinkCollectionNodesLength, IntPtr PreparsedData); + + [DllImport("hid.dll")] + public static extern long HidP_GetUsageValue( + HIDP_REPORT_TYPE ReportType, ushort UsagePage, ushort LinkCollection, ushort Usage, out ulong UsageValue, + IntPtr PreparsedData, byte[] Report, ulong ReportLength); + + [DllImport("hid.dll")] + public static extern long HidP_GetUsagesEx( + HIDP_REPORT_TYPE ReportType, ushort LinkCollection, [Out] USAGE_AND_PAGE[] ButtonList, ref ulong UsageLength, + IntPtr PreparsedData, byte[] Report, ulong ReportLength); + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct HIDP_CAPS + { + public ushort Usage; + public ushort UsagePage; + public ushort InputReportByteLength; + public ushort OutputReportByteLength; + public ushort FeatureReportByteLength; + private unsafe fixed ushort Reserved[17]; + + public ushort NumberLinkCollectionNodes; + + public ushort NumberInputButtonCaps; + public ushort NumberInputValueCaps; + public ushort NumberInputDataIndices; + + public ushort NumberOutputButtonCaps; + public ushort NumberOutputValueCaps; + public ushort NumberOutputDataIndices; + + public ushort NumberFeatureButtonCaps; + public ushort NumberFeatureValueCaps; + public ushort NumberFeatureDataIndices; + } + + public enum HIDP_REPORT_TYPE + { + HidP_Input, + HidP_Output, + HidP_Feature + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct HIDP_VALUE_CAPS + { + public ushort UsagePage; + public byte ReportID; + public byte IsAlias; + + public ushort BitField; + public ushort LinkCollection; + + public ushort LinkUsage; + public ushort LinkUsagePage; + + public byte IsRange; + public byte IsStringRange; + public byte IsDesignatorRange; + public byte IsAbsolute; + + public byte HasNull; + private byte Reserved; + public ushort BitSize; + + public ushort ReportCount; + private unsafe fixed ushort Reserved2[5]; + + public uint UnitsExp; + public uint Units; + + public int LogicalMin, LogicalMax; + public int PhysicalMin, PhysicalMax; + public Union union; + + [StructLayout(LayoutKind.Explicit, Pack = 4)] + public struct Union + { + [FieldOffset(0)] + public _Range Range; + + [FieldOffset(0)] + public _NotRange NotRange; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct _Range + { + public ushort UsageMin, UsageMax; + public ushort StringMin, StringMax; + public ushort DesignatorMin, DesignatorMax; + public ushort DataIndexMin, DataIndexMax; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct _NotRange + { + public ushort Usage; + private ushort Reserved1; + public ushort StringIndex; + private ushort Reserved2; + public ushort DesignatorIndex; + private ushort Reserved3; + public ushort DataIndex; + private ushort Reserved4; + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct HIDP_LINK_COLLECTION_NODE + { + public ushort LinkUsage; + public ushort LinkUsagePage; + public ushort Parent; + public ushort NumberOfChildren; + public ushort NextSibling; + public ushort FirstChild; + + // The original definition is: + // ULONG CollectionType: 8; // As defined in 6.2.2.6 of HID spec + // ULONG IsAlias : 1; // This link node is an allias of the next link node. + // ULONG Reserved: 23; + // Fortunately the value is not used here. Don't bother parsing the bitfield now. + public UInt32 _bitfield; + + public IntPtr UserContext; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct USAGE_AND_PAGE + { + public ushort Usage; + public ushort UsagePage; + } +} diff --git a/osu.Framework/Platform/Windows/Native/Input.cs b/osu.Framework/Platform/Windows/Native/Input.cs index d3ac677443..103855a85b 100644 --- a/osu.Framework/Platform/Windows/Native/Input.cs +++ b/osu.Framework/Platform/Windows/Native/Input.cs @@ -21,6 +21,12 @@ public static extern bool RegisterRawInputDevices( [DllImport("user32.dll")] public static extern int GetRawInputData(IntPtr hRawInput, RawInputCommand uiCommand, IntPtr pData, ref int pcbSize, int cbSizeHeader); + [DllImport("user32.dll")] + public static extern int GetRawInputDeviceInfo(IntPtr hDevice, RawInputDeviceInfoCommand uiCommand, ref RID_DEVICE_INFO info, ref int pcbSize); + + [DllImport("user32.dll")] + public static extern int GetRawInputDeviceInfo(IntPtr hDevice, RawInputDeviceInfoCommand uiCommand, IntPtr pData, ref int pcbSize); + internal static Rectangle VirtualScreenRect => new Rectangle( GetSystemMetrics(SM_XVIRTUALSCREEN), GetSystemMetrics(SM_YVIRTUALSCREEN), @@ -146,6 +152,22 @@ public struct RawMouse public uint ExtraInformation; } + /// Header of the raw input data if is a HID device. + /// + /// This is just a header, because variable sized HID reports is following this. + /// + public struct RawInputDataHidHeader + { + /// Header for the data. + public RawInputHeader Header; + + /// Size of each HID report. + public int SizeHid; + + /// Count of HID reports. + public int Count; + } + /// /// Enumeration containing the flags for raw mouse data. /// @@ -317,17 +339,10 @@ public enum RawInputDeviceFlags AppKeys = 0x00000400 } - public enum HIDUsage : ushort - { - Pointer = 0x01, - Mouse = 0x02, - Joystick = 0x04, - Gamepad = 0x05, - Keyboard = 0x06, - Keypad = 0x07, - SystemControl = 0x80, - } - + /// HID usage page values. + /// + /// See "HID Usage Tables" on the USB-IF website for a full list of UsagePages and the corresponding Usages. + /// public enum HIDUsagePage : ushort { Undefined = 0x00, @@ -360,6 +375,21 @@ public enum HIDUsagePage : ushort MSR = 0x8E } + /// HID usage values. + /// + /// The meaning of every numeral Usage value is dependent on the UsagePage. + /// + public enum HIDUsage : ushort + { + Undefined = 0x00, + + /// For . + Mouse = 0x02, + + /// For . + TouchPad = 0x05, + } + public enum FeedbackType { TouchContactVisualization = 1, @@ -374,4 +404,74 @@ public enum FeedbackType TouchRightTap = 10, GesturePressAndTap = 11, } + + /// What information to retrieve for a raw input device. + public enum RawInputDeviceInfoCommand + { + /// Get preparsed data for APIs. + PreparsedData = 0x20000005, + + /// Get device path for opening directly (if possible). + DeviceName = 0x20000007, + + /// Get . + DeviceInfo = 0x2000000B, + } + + /// Raw input device info. + [StructLayout(LayoutKind.Sequential)] + public struct RID_DEVICE_INFO + { + /// Size of this structure, must be filled before calling GetRawInputDeviceInfo. + public int Size; + + /// Type of this device. + public RawInputType Type; + + /// Detailed information, depending on the device type. + public Union union; + + [StructLayout(LayoutKind.Explicit)] + public struct Union + { + [FieldOffset(0)] + public RID_DEVICE_INFO_MOUSE mouse; + + [FieldOffset(0)] + public RID_DEVICE_INFO_KEYBOARD keyboard; + + [FieldOffset(0)] + public RID_DEVICE_INFO_HID hid; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RID_DEVICE_INFO_MOUSE + { + public uint Id; + public uint NumberOfButtons; + public uint SampleRate; + public uint HasHorizontalWheel; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RID_DEVICE_INFO_KEYBOARD + { + public uint Type; + public uint SubType; + public uint KeyboardMode; + public uint NumberOfFunctionKeys; + public uint NumberOfIndicators; + public uint NumberOfKeysTotal; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RID_DEVICE_INFO_HID + { + public uint VendorId; + public uint ProductId; + public uint VersionNumber; + public ushort UsagePage; + public ushort Usage; + } + } } diff --git a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs index c5550df9d9..36d2ed4efd 100644 --- a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs @@ -18,7 +18,7 @@ namespace osu.Framework.Platform.Windows { [SupportedOSPlatform("windows")] - internal class SDL2WindowsWindow : SDL2DesktopWindow, IWindowsWindow + internal class SDL2WindowsWindow : SDL2DesktopWindow, IWindowsWindow, IHasTouchpadInput { private const int seticon_message = 0x0080; private const int icon_big = 1; @@ -41,6 +41,8 @@ internal class SDL2WindowsWindow : SDL2DesktopWindow, IWindowsWindow private readonly SDL_WindowsMessageHook sdl2Callback; public readonly WindowsRawInputManager RawInputManager; + private readonly WindowsTouchpadReader touchpadReader; + public event Action? TouchpadDataUpdate; public SDL2WindowsWindow(GraphicsSurfaceType surfaceType, string appName) : base(surfaceType, appName) @@ -61,6 +63,8 @@ public SDL2WindowsWindow(GraphicsSurfaceType surfaceType, string appName) declareDpiAware(); RawInputManager = new WindowsRawInputManager(WindowHandle); + touchpadReader = new WindowsTouchpadReader(RawInputManager); + touchpadReader.TouchpadDataUpdate += data => TouchpadDataUpdate?.Invoke(data); // ReSharper disable once ConvertClosureToMethodGroup sdl2Callback = (ptr, wnd, u, param, l) => windowsMessageHookSDL2(ptr, wnd, u, param, l); diff --git a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs index 427846be2d..134663e690 100644 --- a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs @@ -19,7 +19,7 @@ namespace osu.Framework.Platform.Windows { [SupportedOSPlatform("windows")] - internal class SDL3WindowsWindow : SDL3DesktopWindow, IWindowsWindow + internal class SDL3WindowsWindow : SDL3DesktopWindow, IWindowsWindow, IHasTouchpadInput { private const int seticon_message = 0x0080; private const int icon_big = 1; @@ -37,6 +37,8 @@ internal class SDL3WindowsWindow : SDL3DesktopWindow, IWindowsWindow private readonly bool applyBorderlessWindowHack; private readonly WindowsRawInputManager rawInputManager; + private readonly WindowsTouchpadReader? touchpadReader; + public event Action? TouchpadDataUpdate; public SDL3WindowsWindow(GraphicsSurfaceType surfaceType, string appName) : base(surfaceType, appName) @@ -54,6 +56,8 @@ public SDL3WindowsWindow(GraphicsSurfaceType surfaceType, string appName) } rawInputManager = new WindowsRawInputManager(WindowHandle); + touchpadReader = new WindowsTouchpadReader(rawInputManager); + touchpadReader.TouchpadDataUpdate += TouchpadDataUpdate; unsafe { diff --git a/osu.Framework/Platform/Windows/WindowsRawInputManager.cs b/osu.Framework/Platform/Windows/WindowsRawInputManager.cs index 1e525f7457..0d95ba4df3 100644 --- a/osu.Framework/Platform/Windows/WindowsRawInputManager.cs +++ b/osu.Framework/Platform/Windows/WindowsRawInputManager.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using osu.Framework.Logging; using osu.Framework.Platform.Windows.Native; @@ -27,7 +28,24 @@ public event Action? RawMouse } } + public event Action>? RawTouchpad + { + add + { + if (rawTouchpad == null) + register(HIDUsagePage.Digitizer, HIDUsage.TouchPad, true); + rawTouchpad += value; + } + remove + { + rawTouchpad -= value; + if (rawTouchpad == null) + register(HIDUsagePage.Digitizer, HIDUsage.TouchPad, false); + } + } + private event Action? rawMouse; + private event Action>? rawTouchpad; /// The HWND associated with the manager. Used on registering. public readonly IntPtr WindowHandle; @@ -96,8 +114,38 @@ public unsafe void ProcessWmInput(IntPtr lParam) break; } - case RawInputType.Keyboard: case RawInputType.HID: + { + if (size < sizeof(RawInputDataHidHeader)) + { + Logger.Log($"Raw HID header buffer too small ({size} < {sizeof(RawInputDataHidHeader)})", LoggingTarget.Input, LogLevel.Error); + return; + } + + var hidHeader = Marshal.PtrToStructure(buffer); + + if (size < sizeof(RawInputDataHidHeader) + hidHeader.SizeHid * hidHeader.Count) + { + Logger.Log($"Raw HID data buffer too small ({size} < {sizeof(RawInputDataHidHeader)} + {hidHeader.SizeHid} * {hidHeader.Count})", LoggingTarget.Input, LogLevel.Error); + return; + } + + List buffers = new List(); + + for (int i = 0; i < hidHeader.Count; i++) + { + IntPtr dataPtr = IntPtr.Add(buffer, sizeof(RawInputDataHidHeader) + hidHeader.SizeHid * i); + byte[] report = new byte[hidHeader.SizeHid]; + Marshal.Copy(dataPtr, report, 0, hidHeader.SizeHid); + buffers.Add(report); + } + + // TODO maybe invoke one by one rather than as a `List`? + rawTouchpad?.Invoke(hidHeader, buffers); + break; + } + + case RawInputType.Keyboard: default: break; } diff --git a/osu.Framework/Platform/Windows/WindowsTouchpadReader.cs b/osu.Framework/Platform/Windows/WindowsTouchpadReader.cs new file mode 100644 index 0000000000..3bd4119b43 --- /dev/null +++ b/osu.Framework/Platform/Windows/WindowsTouchpadReader.cs @@ -0,0 +1,274 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using osu.Framework.Logging; +using osu.Framework.Platform.Windows.Native; + +namespace osu.Framework.Platform.Windows +{ + internal class WindowsTouchpadReader + { + public event Action? TouchpadDataUpdate; + + public WindowsTouchpadReader(WindowsRawInputManager rawInputManager) + { + rawInputManager.RawTouchpad += readTouchpad; + } + + /// The actual report reader for one single device, contains parsed information. + private TouchpadInstanceReader? reader; + + private void readTouchpad(RawInputDataHidHeader header, List reports) + { + if (reader?.Info.Handle != header.Header.Device) + { + // TODO: If we enable more raw input devices later, it can be possible to run this code every time when reports for another device is received. + // For the best result, extract the device manager code into a new class (and cache all connected devices) + if (!prepareDevice(header.Header.Device)) + return; + } + + TouchpadData? data = null; + + foreach (byte[] report in reports) + { + data = reader!.ReadRawInput(report); + } + + if (data.HasValue) + TouchpadDataUpdate?.Invoke(data.Value); + } + + private unsafe bool prepareDevice(IntPtr hDevice) + { + RID_DEVICE_INFO deviceInfo = new RID_DEVICE_INFO(); + int size = deviceInfo.Size = sizeof(RID_DEVICE_INFO); + + if (Native.Input.GetRawInputDeviceInfo(hDevice, RawInputDeviceInfoCommand.DeviceInfo, ref deviceInfo, ref size) == -1) + { + Logger.Log($"GetRawInputDeviceInfo failed ({Marshal.GetLastWin32Error()})", LoggingTarget.Input, LogLevel.Error); + return false; + } + + if (deviceInfo.Type != RawInputType.HID + || deviceInfo.union.hid.UsagePage != (int)HIDUsagePage.Digitizer + || deviceInfo.union.hid.Usage != (int)HIDUsage.TouchPad) + return false; + + try + { + reader = new TouchpadInstanceReader(hDevice); + } + catch (TouchpadInstanceReader.ParseException e) + { + Logger.Error(e, "TouchpadReader creation failed: cannot parse device info", LoggingTarget.Input); + return false; + } + + return true; + } + + private class TouchpadInstanceReader + { + public class ParseException : Exception + { + public ParseException(string? message) + : base(message) + { + } + } + + public class ReadException : Exception + { + public ReadException(string? message) + : base(message) + { + } + } + + /// Information read from the touchpad. + public readonly TouchpadInfo Info; + + private readonly IntPtr preparsedData; + + /// Which LCs are corresponding to the fingers + private readonly List fingerLinkCollections; + + private readonly USAGE_AND_PAGE[] usageAndPageBuffer; + + public TouchpadInstanceReader(IntPtr hDevice) + { + Info.Handle = hDevice; + + // Read preparsed_data + int size = 0; + if (Native.Input.GetRawInputDeviceInfo(hDevice, RawInputDeviceInfoCommand.PreparsedData, IntPtr.Zero, ref size) != 0) + throw new ParseException($"GetRawInputDeviceInfo(RIDI_PREPARSEDDATA): {Marshal.GetLastWin32Error()}"); + + preparsedData = Marshal.AllocHGlobal(size); + if (Native.Input.GetRawInputDeviceInfo(hDevice, RawInputDeviceInfoCommand.PreparsedData, preparsedData, ref size) == -1) + throw new ParseException($"GetRawInputDeviceInfo(RIDI_PREPARSEDDATA)2: {Marshal.GetLastWin32Error()}"); + + // Read caps (i.e. summary information of the device) + HIDP_CAPS caps; + if (Hid.HidP_GetCaps(preparsedData, out caps) != Hid.HIDP_STATUS_SUCCESS) + throw new ParseException($"HidP_GetCaps: {Marshal.GetLastWin32Error()}"); + + // Read valueCaps (i.e. information about every reported numeral value) + ulong valueCapsLength = caps.NumberInputValueCaps; + var valueCaps = new HIDP_VALUE_CAPS[valueCapsLength]; + if (Hid.HidP_GetValueCaps(HIDP_REPORT_TYPE.HidP_Input, valueCaps, ref valueCapsLength, preparsedData) != Hid.HIDP_STATUS_SUCCESS) + throw new ParseException($"HidP_GetValueCaps: {Marshal.GetLastWin32Error()}"); + if (valueCapsLength != caps.NumberInputValueCaps) + throw new ParseException($"NumberInputValueCaps mismatch, before: {caps.NumberInputValueCaps} after: {valueCapsLength}"); + + // Read linkCollections to find out which LC corresponds to fingers. + // (LC: a collection of UsagePage and Usages, one for each finger contact, and one for the touchpad global info) + ulong linkCollectionLength = caps.NumberLinkCollectionNodes; + var lcList = new HIDP_LINK_COLLECTION_NODE[linkCollectionLength]; + if (Hid.HidP_GetLinkCollectionNodes(lcList, ref linkCollectionLength, preparsedData) != Hid.HIDP_STATUS_SUCCESS) + throw new ParseException($"HidP_GetLinkCollectionNodes: {Marshal.GetLastWin32Error()}"); + if (linkCollectionLength != caps.NumberLinkCollectionNodes) + throw new ParseException($"NumberLinkCollectionNodes mismatch, before: {caps.NumberLinkCollectionNodes} after: {linkCollectionLength}"); + + fingerLinkCollections = new List(); + ushort index = lcList[0].FirstChild; + + while (index != 0) + { + var lc = lcList[index]; + + // 0x0D, 0x22: Finger + if (lc.LinkUsagePage == 0x0D && lc.LinkUsage == 0x22) + fingerLinkCollections.Add(index); + + index = lc.NextSibling; + } + + fingerLinkCollections.Sort(); + + usageAndPageBuffer = new USAGE_AND_PAGE[caps.NumberInputButtonCaps]; + + int maxFingerCount = fingerLinkCollections.Count; + if (maxFingerCount <= 0) + throw new ParseException($"Invalid finger count: {maxFingerCount}"); + + foreach (var valueCap in valueCaps) + { + // Just read the XY ranges for the first finger. + // Never seen a device with different ranges for fingers. + if (valueCap.LinkCollection != fingerLinkCollections[0]) continue; + + // 0x01, 0x30: X + if (checkUsageMatch(valueCap, 0x01, 0x30)) + { + Info.XMin = valueCap.LogicalMin; + Info.XRange = valueCap.LogicalMax - valueCap.LogicalMin; + } + + // 0x01, 0x31: Y + if (checkUsageMatch(valueCap, 0x01, 0x31)) + { + Info.YMin = valueCap.LogicalMin; + Info.YRange = valueCap.LogicalMax - valueCap.LogicalMin; + } + } + } + + private static bool checkUsageMatch(HIDP_VALUE_CAPS valueCap, ushort usagePage, ushort usage) + { + if (valueCap.UsagePage != usagePage) + return false; + + return valueCap.IsRange != 0 + ? valueCap.union.Range.UsageMin <= usage && usage <= valueCap.union.Range.UsageMax + : valueCap.union.NotRange.Usage == usage; + } + + ~TouchpadInstanceReader() + { + Marshal.FreeHGlobal(preparsedData); + } + + public TouchpadData ReadRawInput(byte[] report) + { + uint reportLen = (uint)report.Length; + + // TODO: comment on the usage values + ulong fingerCount; + if (Hid.HidP_GetUsageValue(HIDP_REPORT_TYPE.HidP_Input, 0x0D, 0, 0x54, out fingerCount, preparsedData, report, reportLen) != Hid.HIDP_STATUS_SUCCESS) + throw new ReadException($"HidP_GetUsageValue (lc=0,0D:54): {Marshal.GetLastWin32Error()}"); + + int validPointCount = Math.Min((int)fingerCount, fingerLinkCollections.Count); + + // TODO do we care about scan_time? + // 0D:56 scan time in 100us units, can be unavailable + + ulong caplen = (ulong)usageAndPageBuffer.Length; + + if (Hid.HidP_GetUsagesEx(HIDP_REPORT_TYPE.HidP_Input, 0, usageAndPageBuffer, ref caplen, preparsedData, report, reportLen) != Hid.HIDP_STATUS_SUCCESS) + throw new ReadException($"HidP_GetUsagesEx (lc=0): {Marshal.GetLastWin32Error()}"); + + bool buttonDown = false; + + for (ulong i = 0; i < caplen; i++) + { + ushort usagePage = usageAndPageBuffer[i].UsagePage; + ushort usage = usageAndPageBuffer[i].Usage; + + if (usagePage == 0x09 && usage == 0x01) + buttonDown = true; + } + + List points = new List(); + + for (int i = 0; i < validPointCount; i++) + { + TouchpadPoint point = new TouchpadPoint(); + ushort lc = fingerLinkCollections[i]; + ulong temp; + + if (Hid.HidP_GetUsageValue(HIDP_REPORT_TYPE.HidP_Input, 0x01, lc, 0x30, out temp, preparsedData, report, reportLen) != Hid.HIDP_STATUS_SUCCESS) + throw new ReadException($"HidP_GetUsageValue (lc={lc},01:30): {Marshal.GetLastWin32Error()}"); + + point.X = (int)temp; + + if (Hid.HidP_GetUsageValue(HIDP_REPORT_TYPE.HidP_Input, 0x01, lc, 0x31, out temp, preparsedData, report, reportLen) != Hid.HIDP_STATUS_SUCCESS) + throw new ReadException($"HidP_GetUsageValue (lc={lc},01:31): {Marshal.GetLastWin32Error()}"); + + point.Y = (int)temp; + + if (Hid.HidP_GetUsageValue(HIDP_REPORT_TYPE.HidP_Input, 0x0D, lc, 0x51, out temp, preparsedData, report, reportLen) != Hid.HIDP_STATUS_SUCCESS) + throw new ReadException($"HidP_GetUsageValue (lc={lc},0D:51): {Marshal.GetLastWin32Error()}"); + + point.ContactId = (int)temp; + + caplen = (ulong)usageAndPageBuffer.Length; + + if (Hid.HidP_GetUsagesEx(HIDP_REPORT_TYPE.HidP_Input, lc, usageAndPageBuffer, ref caplen, preparsedData, report, reportLen) != Hid.HIDP_STATUS_SUCCESS) + throw new ReadException($"HidP_GetUsagesEx (lc={lc}): {Marshal.GetLastWin32Error()}"); + + point.Valid = point.Confidence = false; + + for (int j = 0; j < (int)caplen; j++) + { + ushort usagePage = usageAndPageBuffer[j].UsagePage; + ushort usage = usageAndPageBuffer[j].Usage; + + if (usagePage == 0x0D && usage == 0x42) + point.Valid = true; + if (usagePage == 0x0D && usage == 0x47) + point.Confidence = true; + } + + points.Add(point); + } + + return new TouchpadData(Info, points, buttonDown); + } + } + } +}