Skip to content

Add ElanAP: Precision Touchpad absolute positioning#3

Open
quanq026 wants to merge 1 commit into
InfinityGhost:masterfrom
quanq026:add-elanap
Open

Add ElanAP: Precision Touchpad absolute positioning#3
quanq026 wants to merge 1 commit into
InfinityGhost:masterfrom
quanq026:add-elanap

Conversation

@quanq026

@quanq026 quanq026 commented Mar 9, 2026

Copy link
Copy Markdown

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

  • Uses Win32 Raw Input API (RegisterRawInputDevices with RIDEV_INPUTSINK) to read HID Precision Touchpad data
  • HidSharp 2.1.0 for HID descriptor parsing only (no exclusive device access)
  • Maps absolute touchpad coordinates to screen position, just like SynAP

Changes

  • Added ElanAP/ project (WPF, .NET 4.6.1, C# 5 compatible)
  • Added ElanAP to SynAP.sln
  • Updated README.md to cover both projects

Compatibility

  • Windows 10/11
  • .NET Framework 4.6.1+
  • Any Windows Precision Touchpad

- 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
Copilot AI review requested due to automatic review settings March 9, 2026 13:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ElanAP to SynAP.sln so both projects build from the same solution.
  • Updated README.md to 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.

Comment thread ElanAP/Driver.cs
Comment on lines +67 to +76
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);

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread ElanAP/API.cs
Comment on lines +124 to +127
catch { }
}
throw new Exception("No HID Precision Touchpad found. Ensure Elan driver is installed.");
}

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread ElanAP/MainWindow.xaml.cs
Comment on lines +71 to +74
StartButton.IsEnabled = API.IsAvailable;
if (!API.IsAvailable)
Console.Log(API, "API is unavailable. Please ensure an Elan HID touchpad is connected.");
}

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread ElanAP/API.cs
Comment on lines +186 to +234
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);
}

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread ElanAP/NotifyIcon.cs
Comment on lines +12 to +20
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;
}

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +63
public static readonly DependencyProperty ForegroundAreaProperty = DependencyProperty.Register(
"ForegroundArea", typeof(Area), typeof(MapArea));

public static readonly DependencyProperty BackgroundAreaProperty = DependencyProperty.Register(
"BackgroundArea", typeof(Area), typeof(MapArea));

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Copilot uses AI. Check for mistakes.
Comment thread ElanAP/Driver.cs
Comment on lines +120 to +128
public void Start(IntPtr hwnd)
{
if (API.IsAvailable)
{
FireOutput("Starting...");

ScaleX = ScreenArea.Width / TouchpadArea.Width;
ScaleY = ScreenArea.Height / TouchpadArea.Height;

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
@quanq026 quanq026 changed the title Add ElanAP: Precision Touchpad absolute positioning for osu! Add ElanAP: Precision Touchpad absolute positioning Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants