test: HID haptics for the Apple SPI trackpad - #1
Open
Chromatischer wants to merge 13 commits into
Open
Chromatischer wants to merge 13 commits into
Chromatischer wants to merge 13 commits into
Conversation
dchid_set_report() always sends HID_OUTPUT_REPORT regardless of the report type requested by the caller. Inline it into its sole caller and forward rtype directly, so SET_REPORT works correctly for feature reports too, not just output reports, and to simplify function calls. Signed-off-by: Yicong Hui <yicong@ycstuff.net>
The J313 trackpad firmware reports five HID interfaces, but the driver
clamps the count to three and only ever queries mngt, kbd and tp. The
fourth one is the trackpad's haptic actuator:
iface 3: name 'Actuator', country 0x0, in 16 out 64 ctrl 1024
Its report descriptor declares output report 0x53, which is the report a
waveform is played with, so dropping the interface puts the taptic engine
out of reach entirely.
Raise the cap to four and give interface 3 a spihid_interface of its own
so that it is queried, registered as a HID device and torn down with the
others. The "expecting at least 3" sanity check tested against the same
constant, so give it its own SPIHID_MIN_DEVICES; otherwise raising the cap
would reject a machine that reports exactly three interfaces.
Interface 4 is still not enumerated. What it is has not been established.
Assisted-by: Claude:claude-opus-5
Assisted-by: DeepSeek:deepseek-v4.1
Signed-off-by: Chromatischer <dominik@hildania.de>
apple_ll_raw_request() refused any SET_REPORT whose report number differed
from the interface id, and apple_ll_output_report() tagged its request with
the interface id rather than the report id in buf[0]. The comment on the
latter already suspected it: "second idev->id should maybe be buf[0]?".
Both survived only because the report number and the interface id happened
to coincide for every report the driver sent: the keyboard's LED report is
report 1 on interface 1, and the SPI trackpad's multitouch enable is report
2 on interface 2.
The actuator on interface 3 takes reports 0x21 and 0x53, so neither held.
Its mode switch was rejected before it was sent,
spi-hid-apple-of spi2.0: device:3 reportnum:33 mismatch
and its waveforms went out labelled report 3, which the firmware ignored
silently: every write was accepted and nothing moved.
With both corrected the actuator latches host-controlled mode, feature
report 0x21 reads back 01, and waveforms play on a J313.
Assisted-by: Claude:claude-opus-5
Assisted-by: DeepSeek:deepseek-v4.1
Signed-off-by: Chromatischer <dominik@hildania.de>
Add a driver to control the actuator of the MTP trackpad, for M2 and later devices. It plays a waveform with output report 0x53 and switches the actuator between device-controlled and host-controlled mode with feature report 0x21. The specific hardware commands and their fields have been reverse engineered on my own machine (Macbook Pro 2023 M2 Max) using the m1n1 hypervisor. Signed-off-by: Yicong Hui <yicong@ycstuff.net> Co-developed-by: Chromatischer <dominik@hildania.de> Signed-off-by: Chromatischer <dominik@hildania.de>
The actuator on the SPI machines takes the same output report 0x53 and the same mode-switch feature report 0x21 as the MTP one, so the driver needs no protocol change to drive it; it simply never matched. Its id table covers BUS_HOST only, and its .match insists on the name "Apple MTP actuator", while spi-hid names the interface "Apple SPI Actuator". Nothing else claims the device either: hid-apple's id table matches every Apple SPI device, which makes hid_generic_match() stand aside, and hid-apple's probe then rejects anything that is not HID_TYPE_SPI_KEYBOARD. The actuator is left bound to no driver at all. Match BUS_SPI Apple devices as well and accept either name, via a helper in the actuator header so hid-magicmouse can use the same test. Start with HID_CONNECT_HIDRAW so the waveform parameters can be exercised from userspace: the strength and softness that suit this engine are not known yet, and the MTP defaults feel harsh on a J313. Assisted-by: Claude:claude-opus-5 Assisted-by: DeepSeek:deepseek-v4.1 Signed-off-by: Chromatischer <dominik@hildania.de>
Integrate the MTP actuator driver with the MTP trackpad driver to expose
new functionality that allows userspace to control the haptic effects
of the touchpad actuator as a force feedback device, following the
convention for haptic touchpad functionality as specified in [1].
Set the INPUT_PROP_PRESSUREPAD property within the MTP trackpad device
indicating that this device is a haptic touchpad, and set the FF_HAPTIC
capability to indicate that force feedback haptic effects can be
uploaded to the trackpad [2]. Userspace can thus choose to upload force
feedback effects to the touchpad, which would trigger it to switch into
"Host controlled" mode, where the touchpad will no longer click unless
signalled to do so by userspace.
This patch uses an unconventional means of doing so, by writing a
separate loadable module for the haptic actuator. This is because
the haptic actuator is considered a separate device on MTP trackpads,
whereas haptics for HUTRR63 (and the other kernel drivers) considers the
haptic actuator and the touchpad to be the very same device. Therefore,
to conform to the standardized interface in [1] this patch creates a
separate driver for the separate HID device, and is designed such that
commands to the actuator are sent through the touchpad.
This changeset is relevant because allowing userspace control of the
haptics allows for user control of:
1. Click feedback intensity
2. Sensitivity of when clicks trigger
3. Configuring "Force Touch"-like deep click functionality
The actuator is a sibling HID device that the transport can register
after the trackpad, so it is resolved lazily on the first effect upload.
The force feedback device is created before the input device is
registered and destroyed explicitly before the driver state goes away,
and the whole integration is compiled out when the actuator driver is
not reachable, so this driver does not depend on it.
[1] https://lore.kernel.org/all/20250818-support-forcepads-v3-0-e4f9ab0add84@google.com/T/#m12f9be6691015fb9b2e834248c368e5f90
[2] https://docs.kernel.org/input/event-codes.html#input-prop-pressurepad
Signed-off-by: Yicong Hui <yicong@ycstuff.net>
Co-developed-by: Chromatischer <dominik@hildania.de>
Signed-off-by: Chromatischer <dominik@hildania.de>
magicmouse_init_haptics() requires BUS_HOST, and match_actuator() looks for the MTP actuator by name, so the SPI trackpads were left without haptics even though their machines have an actuator that speaks the same protocol. Accept BUS_SPI as well and use the shared name test. The lookup itself needs no change: the actuator is a sibling HID device under the same SPI parent, which is what magicmouse_get_actuator() already searches. This does not change how a click feels by default. The actuator stays in device-controlled mode, where its firmware provides the click feedback, and only moves to host-controlled mode when userspace uploads an effect. Assisted-by: Claude:claude-opus-5 Assisted-by: DeepSeek:deepseek-v4.1 Signed-off-by: Chromatischer <dominik@hildania.de>
The waveform sent to the actuator carries a strength and a softness. Both were fixed: softness at 0x90, and strength at the full 255 scaled by the effect's intensity. Those suit the MTP actuator, but on a J313 the same numbers produce a hard knock rather than a click. Judged by feel there, a press wants roughly strength 0x28 at softness 0xf0, far weaker than the MTP default, while a deep click wants full strength 0xff at a softness of 0xd0. Add four parameters for the two waveforms, each defaulting per transport so the MTP path keeps the values its firmware was tuned with. Strength now names what 100% intensity maps to, so an effect asking for half intensity still gets half of it. They are writable at runtime and take effect on the next effect upload, which is what makes tuning by feel practical: the values that suit a given actuator cannot be derived, only tried. Assisted-by: Claude:claude-opus-5 Assisted-by: DeepSeek:deepseek-v4.1 Signed-off-by: Chromatischer <dominik@hildania.de>
The actuator's firmware provides the feedback for a physical press, and how that press feels cannot be changed while it does. Playing the waveform from the host instead makes it adjustable, but it cannot be done by reacting to the button the trackpad reports: in host-controlled mode the firmware stops detecting clicks altogether. It keeps reporting fingers and never sets the button again, because deciding that a press happened belongs to whoever owns the actuator. Hooking the button bit therefore works exactly once — for the press already in flight when the mode changed — and then the pad has no click feedback and no button at all. So derive the click here. Sum the pressure of the fingers in each frame, call a press when it rises past haptic_press_threshold and a release when it falls below haptic_release_threshold, report BTN_MOUSE from that, and play the waveform from the haptics workqueue, which is where it can sleep. The thresholds default to 120 and 75, measured on a J313 against its own firmware: it asserts the button between 130 and 243 and releases between 26 and 73. The gap between the two is what stops a click chattering. This is off by default. Turning it off returns the actuator to the firmware on the next frame, and any failure while playing does the same, so a pad that cannot play a waveform falls back to working feedback rather than none. What the parameters do on an SPI actuator is narrower than the report layout suggests. The fifteen waveform bytes are ignored: zeroing them feels identical. Strength is close to on or off, with 0x00 silent and the values above it differing little. Softness is what shapes the pulse; swept from 0x10 to 0xff at a fixed strength it runs from a faint tick to a hard knock. A click that wants more weight than softness gives is made of more pulses, which is what haptic_press_pulses is for. Assisted-by: Claude:claude-opus-5 Assisted-by: DeepSeek:deepseek-v4.1 Signed-off-by: Chromatischer <dominik@hildania.de>
The pad reports finger pressure well past the point where a click is asserted, so a second, firmer press inside one click can be told apart from an ordinary one. That is what Force Touch does, and the hardware already carries the signal. Fire once per click at haptic_deep_threshold, with its own pulse count, gap and waveform shape so it can be made to feel distinct from the first click, and clear the flag only on release so holding at depth does not repeat it. Linux has no key for a force click, so haptic_deep_keycode names one and nothing is reported unless it is set; the capability has to be advertised at probe, so that parameter is read-only. Assisted-by: Claude:claude-opus-5 Assisted-by: DeepSeek:deepseek-v4.1 Signed-off-by: Chromatischer <dominik@hildania.de>
|
added 3 commits
September 17, 2026 10:14
The firmware reports the button only while it owns the actuator. The derived click covered haptic_press, but a force-feedback effect uploaded by userspace also takes host-controlled mode, and the mode cannot be released while an effect is loaded. In that state the driver reported the firmware's button, which never changes again, so the pad stopped clicking until every effect was erased. Derive the button whenever the actuator is in host-controlled mode, whatever put it there, and keep asking for it back when the waveform is not ours to play. Found by a review bot on the pull request. Assisted-by: Claude:claude-opus-5 Assisted-by: DeepSeek:deepseek-v4.1 Signed-off-by: Chromatischer <dominik@hildania.de>
The actuator leaves system sleep in device-controlled mode. The transport resumes the keyboard and the trackpad, not the actuator, and nothing tells the driver the mode changed, so msc->haptics->mode still reads host-controlled. The next press then skips the switch and plays a pulse while the firmware plays its own, which is felt as two clicks per press. On a J313 across an s2idle cycle the driver says nothing between 'host-driven click feedback active' before the suspend and the first press after it: it never re-took the mode because it believed it still held it. Drop the cached mode in reset_resume, which the transport does call for the trackpad, and drop the click state with it. The next press takes host-controlled mode again if it needs it. Assisted-by: Claude:claude-opus-5 Assisted-by: DeepSeek:deepseek-v4.1 Signed-off-by: Chromatischer <dominik@hildania.de>
The actuator keeps its mode across a warm reboot. Every user of it starts out believing it is device-controlled, so one left in host-controlled mode by a previous boot is silent: the firmware plays nothing because it no longer owns the actuator, and the host that did is gone. The trackpad then has no click feedback at all, with nothing in the log to say why. Only a cold boot clears it. Assert device-controlled mode at probe instead of inheriting whatever the last boot left. The switch is not fatal if it fails, since that leaves the actuator exactly where it already was. Assisted-by: Claude:claude-opus-5 Assisted-by: DeepSeek:deepseek-v4.1 Signed-off-by: Chromatischer <dominik@hildania.de>
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.
Review target, not for merge. Same ten commits as aurora-silicon#4; this exists so a review bot has somewhere to run.