UIKit delivers Pencil events at the PANEL's cadence, and the deadline
link's rate hint pins the panel to the stream rate (min = preferred =
stream Hz, deliberately — the anti-idle-stall floor). Net effect: a 60 fps
stream on a 120 Hz iPad silently halved pencil sampling, 8.3 → 16.7 ms per
event batch — exactly the workload (drawing) that feels it most.
FrameRateHint gains a boost that lifts min/preferred to the range ceiling
while a Pencil is in range (hover or contact): PencilStream surfaces
proximity transitions from its emit() choke point, the view controller
debounces release by 2 s (edge-of-canvas hover flicker must not thrash the
link), and Stage2Pipeline/SessionPresenter stage it like the rate hint —
applied from the link's own thread, no-op under arrival/glass pacing.
Presents still pace at stream rate (extra updates vend into the
newest-wins stash), so the cost is empty link wakes scoped to proximity.
pf.present logs engage/release for on-glass verification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three drawing-path defects on the pen clients:
- The Apple in-range heartbeat was re-armed on every emit (~120 run-loop
mutations/s during a stroke) and lived in .default run-loop mode only — a
tracking-mode excursion >200 ms with a stationary pen crossed the host's
force-release failsafe and dropped the held stroke. Now one long-lived
50 ms timer in .common mode, resending only after ≥50 ms of send silence.
- Both clients TRUNCATED an over-8-sample coalesced run instead of splitting
it into consecutive batches (the send_pen contract): Apple suffix(8)
dropped the oldest samples, the Android JNI clamped count. An over-cap run
means the UI thread hitched — exactly when dropping stroke geometry hurts
most. Apple splits in emit(); the Android JNI loops send_pen over ≤8-sample
chunks (Kotlin's per-emit ceiling is now 64 = a >250 ms stall).
- The iOS letterbox mapper did a lock+FFI currentMode() read per coalesced
sample on the main thread, contending the ABI lock the batch send takes
next — now a 250 ms TTL cache (mid-stream requestMode still tracked).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The flagship leg of design/pen-tablet-input.md §7: against a HOST_CAP_PEN host
the Pencil splits out of the finger path (independent of touch-input mode) into
PencilStream — state-full samples with force→pressure, altitude→polar tilt,
azimuth (+90° to the wire's north-clockwise convention), Pencil Pro rollAngle→
barrel roll (17.5+), coalesced touches batched ≤8/send for full 240Hz fidelity,
and Pencil hover (zOffset>0 distinguishes it from trackpad hover) as in-range
samples with distance. Squeeze holds barrel 1, double-tap clicks barrel 2 (the
Pencil has no hardware buttons/eraser — these are how host apps get those
affordances). Implements the ≤100ms heartbeat wire contract (80ms timer) so a
stationary held stroke survives the host's 200ms dead-client failsafe. Toward a
pen-less host nothing changes (Pencil stays a finger).
Also fixes hostSupportsCursor testing the STALE 0x04 bit — HOST_CAP_CURSOR
moved to 0x08 when TEXT_INPUT claimed 0x04; the old test would mistake a
text-input host (e.g. Windows) for a cursor grant and double-render the pointer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>