iris: a fling starts at Compose's velocity, which is a curve fit and not an average

Iris, from the phone on the 4274b8b build: "flinging now actually works
but is slower than Compose's immediately after releasing the flick (the
slow down seems correct)." The spline was already AOSP's; the initial
velocity was not.

`VelocityTracker` held per-frame pan deltas and answered their sum over
the sample span -- an average, which cannot tell an accelerating flick
from a steady drag. Ported from the `-sources.jar` of
androidx.compose.ui:ui-android:1.12.0 and
androidx.compose.foundation:foundation-android:1.12.0 (the versions the
Compose app builds against) rather than from memory, and the reading
corrected the plan twice:

  * The touch path is not `Strategy.Impulse`. `scrollable`/`draggable`
    release through the 2D `VelocityTracker`, which on Android is two
    `VelocityTracker1D(strategy = Lsq2)` over absolute positions -- a
    degree-2 least-squares fit differentiated at the newest sample.
    Impulse is reached only by `DifferentialVelocityTracker`, whose one
    caller is `NonTouchScrollingLogic`: wheel and trackpad.
  * There is no minimum fling velocity. `ViewConfiguration`'s 50dp/s is
    used only by `NestedScrollInteropConnection`; `DefaultFlingBehavior`
    skips `abs(v) <= 1f`, and says in its own comment that this is to
    dodge a NaN out of the spline. So `List::fling` caps at 8000dp/s
    against its own density and floors at 1px/s, and no threshold
    Compose does not have was added.

So the tracker holds positions rather than deltas (Lsq2 refuses
differential data in Compose too), 20 of them, with Compose's 100ms
horizon and 40ms stopped-gap; `DragGesture` feeds the raw window
coordinate along the drag axis at the press and every `Pan` frame.

`iris/benches/velocity_reference.py` is the independent transcription
the checked-in numbers come from, as `fling_spline_reference.py` is for
the curve. On `flick-120hz.touch`: 11750px/s before, 15250px/s after. On
an accelerating flick -- the shape a real finger makes, which that 16ms
recording is too short to show -- 1080 before, 2445 after. An average
also flings from a standstill (2533px/s where Compose says 0) and flings
from two points that describe no curve.

Negative control: reverting `velocity` to `total / span` fails exactly
seven tests, all of them about the estimator, and leaves the steady
drag, the tap, the selection release, the sixteen arbiter tests and the
rest of phone_screen.rs passing.

`iris drag release:` keeps its info line and gains a debug
`iris drag release samples:` with every held sample as `t_ms:position`,
so a flick that felt wrong on a phone with no logcat can be replayed at
layer 1 or pasted into the reference script.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-07 16:24:29 -04:00
1 parent 452c44249f
commit 84a13e806b
7 files changed
+897 -150

No files matched your search

+26 -13
View File
@@ -1019,20 +1019,33 @@ do not duplicate it there.
programmatic scroll all go through it) and end a fling that hits the
clamp. Test at layer 1: a drag past either end leaves the offset at
the end; a fling into the end stops there.
- [ ] **"Flinging now actually works but is slower than Compose's
- [x] **"Flinging now actually works but is slower than Compose's
immediately after releasing the flick (the slow down seems
correct)."** The curve is right, so the *initial velocity* is low.
iris's `VelocityTracker::velocity` is total motion over the window's
span -- an average -- where Compose's `VelocityTracker` (`compose.ui`
`VelocityTracker.kt`, `VelocityTracker1D` with `Strategy.Impulse`,
100ms horizon, 20 samples, `AssumePointerMoveStoppedMilliseconds =
40`) weights the last samples, so a flick that accelerates into the
release reads faster. Port the impulse strategy from source with
checked-in reference values from an independent transcription, as
the spline was; also apply Compose's min/max fling velocity
(`ViewConfiguration`'s 50dp/s and 8000dp/s) so a slow release does
not fling and a wild one is capped. Layer-1 test on
`flick-120hz.touch` asserting the number Compose's code gives.
correct)."** Done; RUST.md's "The fling started too slow" has the
derivation and the table. On `flick-120hz.touch` the release velocity
goes from **11750px/s to 15250px/s**, and on an accelerating flick --
the shape a real finger makes, and what the recording is too short to
show -- from 1080 to 2445px/s. The curve was right; `VelocityTracker`
was averaging total motion over the sample span, which cannot tell an
accelerating flick from a steady drag.
**Two things the plan for this item had wrong, both found by reading
the sources rather than remembering them.** Compose's touch path is
**not** `Strategy.Impulse`: `scrollable`/`draggable` release through
the 2D `VelocityTracker`, which on Android is two
`VelocityTracker1D(strategy = Lsq2)` over absolute *positions* -- a
degree-2 least-squares fit, differentiated at the newest sample.
Impulse is reached only by `DifferentialVelocityTracker`, for mouse
wheel and trackpad. And there is **no minimum** fling velocity on that
path: `ViewConfiguration.minimumFlingVelocity`'s 50dp/s is used only by
`NestedScrollInteropConnection`, while `DefaultFlingBehavior` skips
`abs(v) <= 1f` to dodge a NaN from the spline. So iris ports Lsq2, caps
at 8000dp/s, and floors at 1px/s -- no 50dp/s threshold Compose does
not have. `iris/benches/velocity_reference.py` is the independent
transcription the checked-in numbers come from; the negative control
(reverting to the average) fails exactly the seven tests about the
estimator and none of the rest. The release log gains a debug
`iris drag release samples:` line so a flick reported from the phone can
be replayed at layer 1.
- [ ] **Input-event and timing report from the phone.** Iris: "add
another button to copy input event info so that I can do some stuff
manually and then send the event log to you ... instrument a lot of