Add ElanAP: Precision Touchpad absolute positioning#3
Conversation
- Win32 Raw Input API for universal PTP support (Elan, Synaptics, ALPS) - Deadzone blocking with low-level mouse hook - Drag-to-select area on visual map - Touchpad and screen resolution presets - Global hotkey F6 toggle - Zero-allocation hot path for minimal latency - Updated README and solution file
There was a problem hiding this comment.
Pull request overview
Adds a new WPF/.NET Framework project (ElanAP) alongside SynAP to provide absolute-positioning cursor control for osu! using Windows Precision Touchpad HID input (Raw Input + HidSharp), plus updates the solution and README to cover both projects.
Changes:
- Added
ElanAP/project implementing Raw Input HID parsing + absolute cursor mapping, UI, tray icon, and hotkey toggle. - Added
ElanAPtoSynAP.slnso both projects build from the same solution. - Updated
README.mdto document SynAP vs ElanAP, features, and build steps.
Reviewed changes
Copilot reviewed 27 out of 29 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| SynAP.sln | Adds the ElanAP project and build configurations to the solution. |
| README.md | Updates documentation to cover both SynAP and ElanAP, including requirements/features/build. |
| ElanAP/ElanAP.csproj | New WPF project definition, references, resources, and compilation items. |
| ElanAP/packages.config | Adds HidSharp and compiler package dependencies. |
| ElanAP/API.cs | Implements Raw Input registration and HID report parsing (HidSharp) for PTP coordinates. |
| ElanAP/Driver.cs | Maps touch coordinates to screen coordinates and installs a low-level mouse hook. |
| ElanAP/MainWindow.xaml(.cs) | UI for bounds selection, presets, hotkey handling, config load/save, tray behavior. |
| ElanAP/Controls/* | Map visualization + drag-to-select and an in-app console UI. |
| ElanAP/Devices/* | Screen/touchpad bounds abstraction. |
| ElanAP/Tools/* | Small helper utilities and app metadata (paths/version). |
| ElanAP/NotifyIcon.cs | System tray icon wrapper for minimizing/restoring. |
| ElanAP/Windows/AboutBox.xaml(.cs) | About dialog UI and links. |
| ElanAP/* resources | App config, assembly info, icons. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) | ||
| { | ||
| if (nCode >= 0 && (int)wParam == WM_MOUSEMOVE) | ||
| { | ||
| // Allow injected/synthetic moves (our own SetCursorPos), block hardware moves | ||
| var info = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT)); | ||
| if ((info.flags & LLMHF_INJECTED) == 0) | ||
| return (IntPtr)1; | ||
| } | ||
| return CallNextHookEx(_hookId, nCode, wParam, lParam); |
There was a problem hiding this comment.
MouseHookCallback blocks all non-injected WM_MOUSEMOVE events (it doesn’t distinguish touchpad vs. external mouse, nor does it check whether the cursor is outside the configured area). This will prevent physical mice (and any non-injected cursor movement) from working while the driver is active, which is a significant usability/operational impact. If the goal is to block only touchpad-relative movement or only outside a deadzone, add a more selective filter or make this behavior optional/togglable.
| catch { } | ||
| } | ||
| throw new Exception("No HID Precision Touchpad found. Ensure Elan driver is installed."); | ||
| } |
There was a problem hiding this comment.
The exception message hard-codes “Ensure Elan driver is installed”, but the project/README claim support for any Windows Precision Touchpad (not vendor-specific). Update this message to avoid incorrectly suggesting an Elan-only requirement (and consider including a short actionable hint like checking Windows touchpad settings).
| StartButton.IsEnabled = API.IsAvailable; | ||
| if (!API.IsAvailable) | ||
| Console.Log(API, "API is unavailable. Please ensure an Elan HID touchpad is connected."); | ||
| } |
There was a problem hiding this comment.
This log message tells users to “ensure an Elan HID touchpad is connected”, which conflicts with the stated goal of supporting any Windows Precision Touchpad. Please make the guidance vendor-neutral (e.g., “Precision Touchpad detected/enabled”) to avoid misleading troubleshooting.
| public void ProcessRawInput(IntPtr lParam) | ||
| { | ||
| if (!_running) return; | ||
|
|
||
| uint dwSize = 0; | ||
| GetRawInputData(lParam, RID_INPUT, IntPtr.Zero, ref dwSize, _headerSize); | ||
| if (dwSize == 0) return; | ||
|
|
||
| // Reuse buffer — grow only if needed | ||
| if (_rawBuffer.Length < (int)dwSize) | ||
| _rawBuffer = new byte[(int)dwSize]; | ||
|
|
||
| IntPtr buffer = Marshal.AllocHGlobal((int)dwSize); | ||
| try | ||
| { | ||
| if (GetRawInputData(lParam, RID_INPUT, buffer, ref dwSize, _headerSize) != dwSize) | ||
| return; | ||
|
|
||
| // Read dwType directly from unmanaged memory (offset 0 of RAWINPUTHEADER) | ||
| uint dwType = (uint)Marshal.ReadInt32(buffer, 0); | ||
| if (dwType != RIM_TYPEHID) return; | ||
|
|
||
| // Copy only the HID payload portion, not the full buffer | ||
| int hidOffset = (int)_headerSize; | ||
| int payloadLen = (int)dwSize - hidOffset; | ||
| if (payloadLen < 8) return; | ||
|
|
||
| Marshal.Copy(buffer + hidOffset, _rawBuffer, 0, payloadLen); | ||
|
|
||
| int dwSizeHid = BitConverter.ToInt32(_rawBuffer, 0); | ||
| int dwCount = BitConverter.ToInt32(_rawBuffer, 4); | ||
| if (dwSizeHid <= 0 || dwCount <= 0) return; | ||
|
|
||
| // Reuse report buffer — grow only if needed | ||
| if (_reportBuffer.Length < dwSizeHid) | ||
| _reportBuffer = new byte[dwSizeHid]; | ||
|
|
||
| for (int r = 0; r < dwCount; r++) | ||
| { | ||
| int offset = 8 + r * dwSizeHid; | ||
| if (offset + dwSizeHid > payloadLen) break; | ||
| Array.Copy(_rawBuffer, offset, _reportBuffer, 0, dwSizeHid); | ||
| ParseReport(_reportBuffer); | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| Marshal.FreeHGlobal(buffer); | ||
| } |
There was a problem hiding this comment.
ProcessRawInput allocates and frees unmanaged memory (Marshal.AllocHGlobal/FreeHGlobal) for every WM_INPUT message. This is on the hot path and contradicts the “zero-allocation hot path” goal; it can become a perf bottleneck under high report rates. Consider reusing a single unmanaged buffer (grow as needed) or switching to a pinned managed buffer/unsafe stackalloc pattern to avoid per-message heap allocations.
| string iconPath = @"ElanAP.Icon.ico"; | ||
| Assembly assembly = Assembly.GetExecutingAssembly(); | ||
|
|
||
| var stream = assembly.GetManifestResourceStream(iconPath); | ||
| if (stream != null) | ||
| Icon.Icon = new System.Drawing.Icon(stream); | ||
| Icon.MouseClick += NotifyIcon_Click; | ||
| Icon.Text = "ElanAP " + "v" + Info.AssemblyVersion; | ||
| } |
There was a problem hiding this comment.
GetManifestResourceStream() returns a Stream that should be disposed. Currently it’s left open after creating the Icon, which can leak a handle until process exit. Wrap the stream usage in a using/dispose block (or copy into a MemoryStream before constructing the Icon).
| public static readonly DependencyProperty ForegroundAreaProperty = DependencyProperty.Register( | ||
| "ForegroundArea", typeof(Area), typeof(MapArea)); | ||
|
|
||
| public static readonly DependencyProperty BackgroundAreaProperty = DependencyProperty.Register( | ||
| "BackgroundArea", typeof(Area), typeof(MapArea)); |
There was a problem hiding this comment.
ForegroundAreaProperty/BackgroundAreaProperty are registered without a PropertyChangedCallback. When these dependency properties are set via binding (e.g., when Config is replaced after loading a .cfg), WPF updates the DP storage without invoking the CLR wrapper setters, so UpdateCanvas() won’t run and the rectangles can get out of sync. Register the DPs with FrameworkPropertyMetadata including a change callback (and optionally AffectsRender) to call UpdateCanvas().
| public void Start(IntPtr hwnd) | ||
| { | ||
| if (API.IsAvailable) | ||
| { | ||
| FireOutput("Starting..."); | ||
|
|
||
| ScaleX = ScreenArea.Width / TouchpadArea.Width; | ||
| ScaleY = ScreenArea.Height / TouchpadArea.Height; | ||
|
|
There was a problem hiding this comment.
Start() and RefreshCache() divide by TouchpadArea.Width/Height without guarding against 0. With a missing/invalid default.cfg, Configuration initializes these to 0, and pressing Start will throw (division by zero) and/or produce invalid scaling. Consider validating ScreenArea/TouchpadArea dimensions before enabling Start (and/or early-return with a user-visible status message).
Summary
Adds ElanAP — a new project alongside SynAP that supports any Windows Precision Touchpad (Elan, Synaptics PTP, ALPS, etc.) instead of requiring Synaptics-specific drivers.
How it works
RegisterRawInputDeviceswithRIDEV_INPUTSINK) to read HID Precision Touchpad dataChanges
ElanAP/project (WPF, .NET 4.6.1, C# 5 compatible)SynAP.slnREADME.mdto cover both projectsCompatibility