Fix trackpad becoming unresponsive after wake or hotplug - #1
Open
cescox wants to merge 1 commit into
Open
Conversation
cescox
force-pushed
the
fix/wake-from-sleep
branch
2 times, most recently
from
May 10, 2026 14:57
c74ef36 to
745cd43
Compare
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
force-pushed
the
fix/wake-from-sleep
branch
from
May 12, 2026 06:23
745cd43 to
3ec439b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 startednever fires for the affected trackpad. Other (still-working) trackpads continue normally.Repro on macOS Tahoe 26.x, Apple Silicon MacBook:
Lid cycle:
Trackpad cycle:
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
AppleMultitouchDevicein the kernel is represented by anIOService. 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 oldIOServiceand creates a new one. Similarly, when a Bluetooth Magic Trackpad is powered off itsIOServiceis terminated; powering it back on creates a new instance.The
MTDevicepointer the app obtained fromMTDeviceCreateListreferences the oldIOService. After re-creation, frames flow on the newIOService's UserClient, not the one our pointer is bound to. The framework gives no signal that the pointer is stale.The existing 5-second
accessibilityTimerrecovery (if registeredDevices.isEmpty) doesn't fire becauseregisteredDevicesstays populated with the stale pointers.Fix
Subscribe to IOKit
kIOFirstMatchNotificationforAppleMultitouchDevice. Each event indicates a newIOServicematching our criteria has finished starting. SchedulerestartMonitoring()0.5s later, which:MTUnregisterContactFrameCallbackandMTDeviceStopon each existing handle (without these, every restart leaks anAppleMultitouchDeviceUserClientin kernel space).registeredDevices.MTDeviceCreateListto 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,MTDeviceDriverIsReadyall keep returning their pre-event values after the handle goes stale.MTDeviceCreateListreturns freshMTDeviceRefpointers on every call, so pointer-identity comparison is meaningless.NSWorkspace.didWakeNotificationcovers lid-close but does not fire on Bluetooth trackpad power-cycle, so it can't be the trigger.NSWorkspace.screensDidWakeNotificationleaks 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 viaioreg -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:
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.