Skip to content

Fix trackpad becoming unresponsive after wake or hotplug - #1

Open
cescox wants to merge 1 commit into
is-harshul:mainfrom
cescox:fix/wake-from-sleep
Open

Fix trackpad becoming unresponsive after wake or hotplug#1
cescox wants to merge 1 commit into
is-harshul:mainfrom
cescox:fix/wake-from-sleep

Conversation

@cescox

@cescox cescox commented May 8, 2026

Copy link
Copy Markdown

Problem

After closing and reopening the laptop lid, MacGesture stops receiving touch events from the internal trackpad. After turning an external Bluetooth Magic Trackpad off and back on, MacGesture stops receiving touch events from it. The app keeps running, the menu bar icon is fine, but 👆 N-finger touch started never fires for the affected trackpad. Other (still-working) trackpads continue normally.

Repro on macOS Tahoe 26.x, Apple Silicon MacBook:

Lid cycle:

  1. Launch MacGesture. A 3-finger tap on the internal trackpad fires the configured action.
  2. Close lid for 30 seconds, reopen.
  3. 3-finger tap on internal: nothing happens, no log activity.
  4. Quit and relaunch MacGesture, internal works again.

Trackpad cycle:

  1. With an external Magic Trackpad attached, confirm a 3-finger tap on it fires the configured action.
  2. Turn the Magic Trackpad off, wait a few seconds, turn it back on.
  3. 3-finger tap on external: nothing happens, no log activity.
  4. Quit and relaunch MacGesture, external works again.

The same lid-close bug shows up in other apps that hook the private MultitouchSupport.framework. The BetterTouchTool community thread is the same symptom on Sonoma 14.2 in 2023; BTT shipped a fix in v4.307+.

Root cause

Each AppleMultitouchDevice in the kernel is represented by an IOService. The internal trackpad sits on the SPI bus (AppleHIDTransportProtocolHIDSPI); when the laptop lid closes, SPI is power-gated. On reopen, the device re-enumerates: the kernel destroys the old IOService and creates a new one. Similarly, when a Bluetooth Magic Trackpad is powered off its IOService is terminated; powering it back on creates a new instance.

The MTDevice pointer the app obtained from MTDeviceCreateList references the old IOService. After re-creation, frames flow on the new IOService's UserClient, not the one our pointer is bound to. The framework gives no signal that the pointer is stale.

The existing 5-second accessibilityTimer recovery (if registeredDevices.isEmpty) doesn't fire because registeredDevices stays populated with the stale pointers.

Fix

Subscribe to IOKit kIOFirstMatchNotification for AppleMultitouchDevice. Each event indicates a new IOService matching our criteria has finished starting. Schedule restartMonitoring() 0.5s later, which:

  • Calls MTUnregisterContactFrameCallback and MTDeviceStop on each existing handle (without these, every restart leaks an AppleMultitouchDeviceUserClient in kernel space).
  • Clears registeredDevices.
  • Calls MTDeviceCreateList to obtain fresh handles and registers + starts each.

Covers both scenarios with a single mechanism. The initial iterator is drained inline at setup time, so pre-existing devices already registered by startMultitouchMonitoring() don't trigger a spurious launch-time restart.

+52 / −2 lines, one file.

Diagnostic dead-ends (so you don't re-investigate)

  • MTDeviceIsAlive, MTDeviceIsRunning, MTDeviceDriverIsReady all keep returning their pre-event values after the handle goes stale.
  • MTDeviceCreateList returns fresh MTDeviceRef pointers on every call, so pointer-identity comparison is meaningless.
  • NSWorkspace.didWakeNotification covers lid-close but does not fire on Bluetooth trackpad power-cycle, so it can't be the trigger.
  • Adding NSWorkspace.screensDidWakeNotification leaks a UserClient per display-only idle-timeout sleep (where SPI never power-gates and no restart is needed). ~100 accumulated leaks over a day of normal use break frame delivery to all multitouch apps. Verified via ioreg -rxn AppleMultitouchDevice: leaked state showed 95 UserClients on the external trackpad vs 2 normally.

Testing

Verified on an M-series MacBook running macOS Tahoe 26.x. For each event, the log shows:

🔌 1 multitouch device(s) appeared — restarting monitoring
*** Recognized (0xNN) family*** ...
✅ Device 0: started
✅ Device 1: started
📱 Monitoring 2/2 device(s)

Tested: 30-second lid-close, 5-minute lid-close, close-and-immediate-reopen, external trackpad off/on, multiple consecutive cycles. Internal and external trackpads recover in all cases. UserClient counts (ioreg -rxn AppleMultitouchDevice) stay stable at 2 per device.

@cescox
cescox force-pushed the fix/wake-from-sleep branch 2 times, most recently from c74ef36 to 745cd43 Compare May 10, 2026 14:57
The MTDevice handle held by the app becomes stale when the underlying
AppleMultitouchDevice IOService is destroyed and recreated. This happens
on lid-close (internal trackpad SPI bus power-gates and re-enumerates
on reopen) and on Bluetooth trackpad power-cycle. Frames stop flowing
for the stale handle, the framework gives no signal, and the app keeps
running but silently ignores gestures from the affected trackpad.

Subscribe to IOKit kIOFirstMatchNotification for AppleMultitouchDevice.
On each event, schedule restartMonitoring() 0.5s later, which tears
down existing handles (MTUnregisterContactFrameCallback + MTDeviceStop)
and re-enumerates via MTDeviceCreateList.

The initial iterator is drained inline (without invoking the callback)
so the pre-existing devices already registered by startMultitouchMonitoring
don't trigger a spurious launch-time restart.

Diagnostic dead-ends:
- MTDeviceIsAlive, MTDeviceIsRunning, MTDeviceDriverIsReady all keep
  returning their pre-event values after the handle goes stale, so the
  framework gives no detectable signal.
- MTDeviceCreateList returns fresh MTDeviceRef pointers on every call,
  so pointer-identity comparison is meaningless.
- NSWorkspace.didWakeNotification covers lid-close but does not fire on
  Bluetooth trackpad power-cycle. Adding screensDidWakeNotification too
  leaks a UserClient per display-only idle-timeout sleep (~100 leaks/day
  break frame delivery to all multitouch apps in the process). IOKit
  notifications fire only on actual IOService re-creation, so they
  cover both scenarios without the leak.
- Skipping MTDeviceStop + MTUnregisterContactFrameCallback on existing
  handles before re-enumeration leaks a UserClient per restart.
@cescox
cescox force-pushed the fix/wake-from-sleep branch from 745cd43 to 3ec439b Compare May 12, 2026 06:23
@cescox cescox changed the title Fix internal trackpad becoming unresponsive after wake Fix trackpad becoming unresponsive after wake or hotplug May 12, 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.

1 participant