feat(pointing): declare scroll resolution in the report descriptor - #3483
feat(pointing): declare scroll resolution in the report descriptor#3483alex-shattu wants to merge 1 commit into
Conversation
The report descriptor left the physical range of the wheel and AC Pan
axes at zero, so it claimed no scroll resolution at all. Hosts then fall
back to a default that assumes a notched wheel, and a device scrolling
from a sensor is misread badly.
On macOS the default is 9 counts per inch
(kDefaultScrollFixedResolution, IOHIDEventService.cpp) and it has two
effects. The high resolution scroll path is gated on the declared
resolution exceeding twice that default, so it stays off, along with the
consume threshold whose purpose is to stop fine movement turning into a
succession of single line events. And the scroll acceleration curve
normalises velocity by resolution/report_rate, so at 9 counts per inch
against a 60 Hz rate the velocity fed to the curve is inflated more than
sixfold. A trackball emitting 1600 counts per inch is therefore treated
as a wheel moving far faster than it is: scrolling arrives in coarse
steps and accelerates away, no matter how fine the stream from the
device is.
Add CONFIG_ZMK_POINTING_SCROLL_RESOLUTION, in counts per inch, and
express it as a physical range with a unit exponent of -1, which is how
a host derives resolution:
resolution = (logical range * 10^-unit_exponent) / physical range
The default of 0 declares nothing and keeps the previous descriptor byte
for byte, so nothing changes for anyone who does not set it.
Note that this is the only lever available on macOS, which does not
implement HID Resolution Multipliers at all: the usage appears nowhere in
IOHIDFamily outside its usage tables, so the feature report ZMK relies
on for CONFIG_ZMK_POINTING_SMOOTH_SCROLLING is never written there and
the multipliers keep their default. The Kconfig help now says so.
Verified against the algorithm in IOHIDEventService::determineResolution:
the emitted descriptor bytes decode back to the intended resolution
exactly up to about 800 counts per inch and within a fraction of a
percent above it. The build assert rejects the values the descriptor
cannot express, including the case where the physical range reaches the
end of the logical range, which would silently revert the host to its
notched wheel default rather than merely round.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Can you clarify what this is actually useful for? I can only tell from the pages of description above that it prevents smooth scrolling from acting badly on macOS (but it still doesn't do smooth scroll)? |
|
Short answer: it is for any ZMK device that scrolls from a sensor — a trackball or a trackpad — on macOS. It is independent of On "it still doesn't do smooth scroll" — on macOS this is the thing that turns smooth scrolling on. macOS never reads the Resolution Multiplier, so isHighResScroll = res > (IOFixed)(SCROLL_DEFAULT_RESOLUTION * 2); // 9 * 2With nothing declared There is a second use even where none of that applies: it is a scroll speed control that costs no precision. Today the only knob is a Scope, plainly: verified on macOS only, with a PMW3360 trackball. I changed two things at once on hardware — declared 1600 and dropped the scaler — so on attribution: dropping the scaler by itself would have made scrolling ten times faster against the assumed 9 counts per inch, i.e. clearly worse, so the declaration is what is doing the work. And you are right that the description is far too long for what this is. Happy to cut it to roughly the above and fold the source-diving into a collapsed section if that makes review easier. |
|
The "Why" section talks about technical implementation details after Claude already decided this is the fix, trying to justify the changes after the fact. It doesn't explain the actual "why is this needed" at all. The descriptor, code or math is not why. What's the scroll behavior before and after this change? To put it plainly — I see you added a 👍 to #3383, is this related? |
|
Fair on all three. Language. This was written with Claude's help and it shows. Rewritten here and in the PR body. What changes for the user. Take a keyboard with a trackball mapped to scroll, on macOS. Before:
After, with the resolution declared:
That is the point of the change. The descriptor and the arithmetic are only how the host gets told, and you are right that they do not answer "why". This only affects devices that scroll from a sensor, so trackballs and trackpads. A rotary encoder really is a notched wheel, and 9 counts per inch is a reasonable guess for one. The default of 0 leaves those alone. #3383. Related, but it does not replace this one. It fixes a real bug. I hit the same symptom, built that patch, and it made no difference on my machine. Its divisor is So #3383 matters for hosts that negotiate multipliers, and this PR matters for macOS, which does not negotiate them. The two do not overlap. That also answers the "requested testing" note in #3383, and I am happy to post it over there if it helps. |
PR check-list
app/testscovers the HID report descriptor, so there was no harness to extend. The arithmetic was checked against Apple's own algorithm instead, see the collapsed section. Happy to add a test if you can say what shape you want.clang-formatwas run with the repo's config and reports the touched files clean. The markdown table was aligned by hand the way prettier would. The full suite was not available locally, so please say if CI disagrees.docs/docs/config/pointing.md.What this fixes
A trackball mapped to scroll is close to unusable on macOS today.
Move the ball slowly and nothing happens for a while, then the page jumps by a big
chunk. There is no small scroll step to be had. Move it at a normal speed and the page
flies much further than the movement should give.
The only knob for this right now is the divider on
zip_xy_scaler. A bigger dividerslows scrolling down, but it also makes the smallest possible step bigger, because it
rounds the stream to whole counts and drops the rest. So you can have slow scrolling
or fine scrolling, but not both.
With the resolution declared, slow movement scrolls a small amount, normal movement
scrolls in proportion, and the speed can be set without losing the small steps.
This only matters for devices that scroll from a sensor, so trackballs and trackpads.
A rotary encoder is a notched wheel, and the host's guess is fine for one. The default
of
0leaves those unchanged.What the patch does
Adds
CONFIG_ZMK_POINTING_SCROLL_RESOLUTION, the number of counts the device sendsper inch of travel, and puts it in the report descriptor as a physical range on the
WheelandAC Panaxes.Those axes currently declare a physical range of zero, which tells the host nothing.
The host then falls back to guessing, and macOS guesses 9 counts per inch, the figure
for a notched wheel. That guess is what produces the behaviour above. Details of how
the guess turns into coarse jumps are in the collapsed section.
The default of
0declares nothing and leaves the descriptor byte for byte as it istoday, so nothing changes unless you set it.
CONFIG_ZMK_POINTING_SMOOTH_SCROLLINGdoes not help here, because macOS does notimplement HID Resolution Multipliers at all. The new option is independent of it and
works with it turned off.
Usage
Set it to what the device sends per inch, after the input processors have run. A 1600
CPI sensor feeding
&zip_xy_to_scroll_mapperdirectly sends 1600. With&zip_xy_scaler 1 10in front of it, 160. Bigger numbers scroll slower.Testing
Tested on hardware, a Charybdis with a PMW3360 trackball on a dongle, with
CONFIG_ZMK_POINTING_SCROLL_RESOLUTION=1600and no scaler on the scroll listener.Slow scrolling works, and normal speed no longer runs away.
macOS only. Windows and Linux drive high resolution scrolling from the Resolution
Multiplier instead of the physical range, so a declaration they do not read should not
affect them. I cannot test either, so a check would be welcome.
Why the host's guess produces coarse jumps, and how the numbers were checked
Where the guess comes from
No descriptor field states resolution directly. The host works it out from the ranges,
in
IOHIDEventService::determineResolution:A physical range of zero gives nothing, so
IOHIDEventService.cppfalls back tokDefaultScrollFixedResolution = (9 << 16). The pointer equivalent iskDefaultFixedResolution = (400 << 16), which is why cursor movement has always feltright while scrolling did not.
This patch declares the physical range with a unit exponent of -1, split evenly either
side of zero, since these are relative axes.
What 9 counts per inch does
In
IOHIPointing.cpp:At
res = 9the first line is false, so the high resolution path stays off. Thesecond works out to 0, which switches off the throttle as well. Apple's comment on
that throttle describes the symptom:
The newer userland path has a second problem.
IOHIDAccelerationAlgorithm:At
resolution = 9andrate = 60the scale is 0.15, so the speed handed to theacceleration curve is about 6.7 times too high. That is the runaway at normal speed.
From
ioregon the dongle before the change:HIDScrollResolution = 589824, which is9 in 16.16 fixed point. The MacBook trackpad next to it reports
26214400, or 400.Resolution Multipliers on macOS
ResolutionMultiplierappears once in the whole IOHIDFamily tree, askHIDUsage_GD_ResolutionMultiplier = 0x48inIOHIDUsageTables.h. Nothing reads itand nothing writes the feature report, so the multipliers keep their defaults and
CONFIG_ZMK_POINTING_SMOOTH_SCROLLINGhas no effect there. The Kconfig help says sonow.
Checking the numbers
determineResolutionwas transcribed and run over the descriptor bytes the macrosproduce. The bytes decode back correctly, and the computed resolution is exact up to
about 800 counts per inch and within a fraction of a percent above that.
The build assert rejects values the descriptor cannot express. One of those is worth
calling out: if the physical range reaches the end of the logical range, the host
throws the declaration away and goes back to guessing. It does not round, it reverts,
and silently. The bound was checked over the whole input range.
On the hardware result, two things changed at once, the declaration and dropping the
scaler. Dropping the scaler on its own would have made scrolling ten times faster
against the 9 counts per inch guess, so clearly worse. The declaration is what did the
work.
Unrelated, spotted while reading this code
apply_resolution_scaling()ininput_listener.cdivides by16 - multiplier. Thedescriptor maps logical
0..15onto physical1..16, so logical value L means amultiplier of L+1, and the divisor should be
16 / (L + 1). The two agree at the endsonly: L=15 gives 1 both ways, L=0 gives 16 both ways. In between they differ, so L=7
should divide by 2 and divides by 9 instead. Left alone here, since it only affects
hosts that write the feature report and I cannot test those. Happy to open a separate
issue.