I'm building a virtual pen digitizer with HIDVirtualDevice (CoreHID, macOS 26.2, com.apple.developer.hid.virtual.device entitlement granted, Developer ID signed with the provisioning profile embedding the entitlement). The device is created and activated fine and shows up as expected:
hidutil list:
0xface 0xbeef UsagePage 13 Usage 2 Transport Virtual "Hej Stylus Virtual Pen"
AppleUserHIDEventService / AppleUserHIDEventDriver
Report descriptor (Digitizer/Pen application collection, Stylus physical collection, 7-byte input report):
05 0D Usage Page (Digitizer)
09 02 Usage (Pen)
A1 01 Collection (Application)
09 20 Usage (Stylus)
A1 00 Collection (Physical)
09 42 Usage (Tip Switch)
09 32 Usage (In Range)
15 00 25 01 75 01 95 02 81 02 ; 2 bits
95 06 81 03 ; 6 bits padding (Const)
05 01 09 30 09 31 ; Generic Desktop X, Y
16 00 00 26 FF 7F 75 10 95 02 81 02 ; 0..32767, 16 bit each
05 0D 09 30 ; Digitizer / Tip Pressure
16 00 00 26 FF 1F 75 10 95 01 81 02 ; 0..8191, 16 bit
C0
C0
I feed it a synthetic stream at 60 Hz (X sweep, Y fixed, tip switch down, in range, pressure ramping 0→8191) via dispatchInputReport(data:timestamp:) and observe the results with a global NSEvent monitor (.tabletProximity, .tabletPoint, .leftMouseDown/Up/Dragged, .mouseMoved) plus the raw CGEvent fields.
What works
tabletProximity:isEnteringProximity=1,pointingDeviceType=.pen,vendorID=0xFACE,tabletID=0xBEEF,systemTabletIDassigned.- All movement arrives as
tabletPoint/mouseMovedwithsubtype == .tabletPoint; positions are exact. - Tip switch maps to
leftMouseDown/leftMouseUpcorrectly (verified by toggling the tip bit with pressure held at max). capabilityMaskon the proximity event is0x407=NX_TABLET_CAPABILITY_DEVICEIDMASK | ABSXMASK | ABSYMASK | PRESSUREMASK— so the system declares pressure capability for this device.
What doesn't
NSEvent.pressure,kCGTabletEventPointPressureandkCGMouseEventPressureare always 0.000, on every event type, including with a constant maximum pressure value (8191).NSEvent.buttonMaskis1(pen tip) andkCGTabletEventPointButtonsis1, so the report is being parsed — the pressure field just never makes it into the tablet event data.
Things I've established / tried
- The pressure value is parsed and does influence touch/click: with tip switch held down and pressure held at 0, no mouse down is ever generated. With pressure ramping, the click happens at exactly 75 % of the logical range. Reading the open-source
IOHIDEventDriver::parseDigitizerTransducerElementexplains this:Tip Pressureis read withkIOHIDValueScaleTypeCalibrated, but only X/Y/Z elements get a calibration, and an uncalibrated element scales to −1…+1 — so raw 0…8191 becomes −1…+1 and the touch threshold (+0.5) sits at 75 %. ChangingLogical Minimumto −8191 (so raw 0…8191 maps to 0…1) moves the click to exactly 50 % — confirming the model. Pressure in the event data is still 0. - Adding
Physical Minimum/Maximumand aUnitto the pressure element: no change. hidutil monitorno longer exists on macOS 26, so I can't inspect theIOHIDEventdigitizer fields directly.- This looks like the same behaviour reported for kext-based digitizers since macOS 10.12 (developer.apple.com/forums thread "IOHIPointing dispatchAbsolutePointerEvent not works" and its sibling: "the pressure information is there from the transducer, the OS doesn't respond to it on 10.12+"). The tablet-pressure dispatch in the open-source
IOHIDEventService::dispatchDigitizerEventWithOrientationis commented out, and the userspaceIOHIDEventTranslationisn't open source, so I can't tell where the value is dropped.
Questions
- Is pen pressure from a
HIDVirtualDevice(or any generic HID digitizer handled byAppleUserHIDEventDriver) expected to reachNSEvent.pressure/kCGTabletEventPointPressureat all on current macOS? Or is that path reserved for vendor drivers posting tablet events themselves? - If it is supported: which descriptor properties does the digitizer→tablet translation require for pressure — specific usages (Transducer Index, Barrel Switch, Tilt, Twist), Report ID, Physical range/Unit on the pressure element, a Feature report, or a particular device property (e.g. something in
HIDVirtualDevice.Properties/kIOHIDDigitizer*keys)? - Is there a documented way to set element calibration for a virtual device so
Tip Pressurescales 0…1 instead of −1…+1 without abusingLogical Minimum? - Is there a supported diagnostic on macOS 26 to see the
IOHIDEventdigitizer fields (pressure, touch, event mask) that the event system builds from my reports, now thathidutil monitoris gone?
Happy to file a Feedback with the full project and a sysdiagnose if that helps.
Update — root cause found (macOS 26.2, disassembly + repro):
The pressure is lost inside SkyLight's HID→NX translation, not in the descriptor and not in the DriverKit driver.
-
AppleUserHIDEventDriver(HIDDriverKitIOUserHIDEventDriver::parseDigitizerElement) calibratesTip Pressureto 0…1 with saturation at Logical Min/Max (setCalibration(0, 1, logMin, logMax)), and publishesDigitizerTipThreshold = 75underHIDEventServicePropertiesif absent. So the IOHIDEvent'skIOHIDEventFieldDigitizerPressureis a normalized float 0.0…1.0. (This also explains the "click at 75 % pressure" behaviour — documented inIOHIDEventServiceKeys.h.) -
SkyLightEventTranslator::hidevent_to_nxevent_translationfills the 16-bitNXTabletPointData.pressurelike this:
add w1, w24, #0x9 ; 0xB0001 + 9 = kIOHIDEventFieldDigitizerPressure (0xB000A)
mov x0, x28
bl _IOHIDEventGetIntegerValue
strh w0, [sp, #0x166] ; NXEventExt.tablet.pressure (offset verified via offsetof against IOLLEvent.h)
IOHIDEventGetIntegerValue truncates the 0…1 float → the tablet event carries pressure 0 for every value < 1.0 and 1 for exactly 1.0, i.e. NSEvent.pressure is at most 1/65535. Repro without any device (IOHIDEventCreateDigitizerEvent(..., tipPressure: 0.999, ...) → GetIntegerValue = 0, GetDoubleValue = 0.999). The tip-threshold comparison right next to it correctly uses GetDoubleValue() * 100 >= threshold — only the tablet field is wrong. The same pattern applies to tablet.tilt (trunc(GetFloatValue) into int16) and rotation.
That matches the old kernel path, which scaled pressure to EV_MAXPRESSURE (0…65535) before handing it to the NX layer (IOHIDEventService::dispatchTabletPointerEvent, active up to IOHIDFamily-701 / 10.11, commented out since 870 / 10.12) — the translator still expects that integer domain while the DriverKit path delivers normalized floats. It also matches the 2016–2019 forum reports ("works on El Capitan, not on 10.12+").
So: no report descriptor can make a generic HID pen deliver pressure to apps on current macOS; vendor drivers work only because they post NX tablet events themselves. Filed as FB24785043 with disassembly offsets and the repro program. A one-line fix (GetDoubleValue() * 65535) would make HIDVirtualDevice pens fully usable for tablet-style apps.