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:
1 parent
452c44249f
commit
84a13e806b
7 files changed
+897
-150
No files matched your search
@@ -8,6 +8,41 @@ capability that moved. Small and trivial changes do not go here.
|
||||
An entry gives the date, what changed, why, and a short before/after where
|
||||
it helps judge the change without the session that made it. Newest first.
|
||||
|
||||
## 2026-09-07: `VelocityTracker` takes positions, not deltas
|
||||
|
||||
A flick released at the wrong speed because the tracker averaged. It now
|
||||
does what Compose's touch scrolling does, and that changes what a caller
|
||||
feeds it.
|
||||
|
||||
// before -- one frame's motion
|
||||
tracker.add_sample(dy, now);
|
||||
// after -- where the finger was
|
||||
tracker.add_position(pos.axis(axis), now);
|
||||
|
||||
`VelocityTracker::velocity` is a port of Compose's `VelocityTracker1D`
|
||||
with `Strategy.Lsq2`: a degree-2 least-squares fit through the last 20
|
||||
positions, differentiated at the newest sample, with Compose's 100ms
|
||||
horizon, 40ms stopped-gap and three-sample minimum. Positions rather than
|
||||
deltas because a fit needs points on a curve -- Compose itself throws on
|
||||
differential data for this strategy.
|
||||
|
||||
Three consequences a caller sees. **A gesture with fewer than three
|
||||
samples answers `0.0`**, where the average answered a number from two;
|
||||
that is Compose's answer too, and on the phone a 120Hz flick delivers
|
||||
four or five. **A finger that rests for more than 40ms before lifting
|
||||
answers `0.0`** rather than flinging at the speed it arrived with.
|
||||
**`add_position` must be called in time order** -- the same debug assert
|
||||
as before, now load-bearing for the fit's x-axis.
|
||||
|
||||
Also new: `VelocityTracker::samples_display` (the held samples as
|
||||
`t_ms:position`, printed by `DragGesture` at debug level so a flick
|
||||
reported from a phone can be replayed), `DragArbiter::axis`, and
|
||||
`sense::MAX_FLING_VELOCITY_DP_S` (8000, `ViewConfiguration`'s own).
|
||||
`List::fling` now applies that maximum against its own density and
|
||||
ignores anything at or under 1px/s, which is Compose's pair of thresholds
|
||||
exactly -- there is deliberately no 50dp/s minimum, because Compose's
|
||||
scrolling never consults the one in `ViewConfiguration`.
|
||||
|
||||
## 2026-09-07: `client-core` carries the app's own log
|
||||
|
||||
Not iris itself but the crate beside it, and it is a new public surface an
|
||||
|
||||
+26
-13
@@ -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
|
||||
|
||||
+59
-34
@@ -333,35 +333,56 @@ per UI_RULES, not "nothing drawn." `tool.rs`'s doc comment on those marks
|
||||
is updated to say this is now a bet on the platform's coverage rather
|
||||
than a checked fact about a bundled `cmap`.
|
||||
|
||||
**One real gap, found on this checkout's emulator, not the desktop**:
|
||||
`fonts: 208 families found, default=Some("Roboto Flex") mono=None` in the
|
||||
startup log (`FontDiagnostics`, read via `adb logcat` after installing the
|
||||
`force-gles` debug build -- the emulator's default Vulkan backend has no
|
||||
adapter here, a pre-existing, documented condition unrelated to this
|
||||
change, and aborts with `Could not get adapter!` without that feature).
|
||||
`mono=None` means fontique's Android backend never resolves the
|
||||
`Monospace` generic family at all on this system image: reading
|
||||
`fontique-0.11.1/src/backend/android.rs`, `DEFAULT_GENERIC_FAMILIES`'s
|
||||
`["monospace"]` is looked up against `name_map` *before* the `fonts.xml`
|
||||
parse that would register a family literally named `"monospace"` runs --
|
||||
so even though this AVD's `/system/etc/fonts.xml` does declare
|
||||
`<family name="monospace"><font ...>DroidSansMono.ttf</font></family>`,
|
||||
fontique's own ordering means that declaration is registered too late to
|
||||
be found by the generic-family lookup, on every Android device this
|
||||
fontique version runs on, not just this AVD. The visible effect is not
|
||||
blank text -- `Family::Monospace`'s explicit-family list comes up empty,
|
||||
but the script-based fallback chain (independent of the generic-family
|
||||
list) still resolves a real font, the same one `SansSerif` gets -- so
|
||||
code blocks and the tool-card chevrons render, just without a genuinely
|
||||
monospaced face. Compose does not have this gap: `FontFamily.Monospace`
|
||||
resolves through Android's own `Typeface.MONOSPACE` constant, a different
|
||||
and unconditionally-populated path that fontique does not use. Left as a
|
||||
follow-up rather than fixed here, since a fix means either patching
|
||||
around fontique's Android backend or pinning `Family::Named("Droid Sans
|
||||
Mono")` (fragile: an OEM-specific font name, not guaranteed across real
|
||||
devices) -- out of the scope Iris gave this pass ("remove the font,
|
||||
match Compose"), and a real product-visible difference worth her knowing
|
||||
about rather than silently living with.
|
||||
**One real gap, found on this checkout's emulator, not the desktop, closed
|
||||
2026-09-07**: `fonts: 208 families found, default=Some("Roboto Flex")
|
||||
mono=None` in the startup log (`FontDiagnostics`, read via `adb logcat`
|
||||
after installing the `force-gles` debug build -- the emulator's default
|
||||
Vulkan backend has no adapter here, a pre-existing, documented condition
|
||||
unrelated to this change, and aborts with `Could not get adapter!` without
|
||||
that feature). `mono=None` means fontique's Android backend never resolves
|
||||
the `Monospace` generic family at all on this system image, and it is two
|
||||
bugs stacked rather than one: reading `fontique-0.11.1/src/backend/android.rs`,
|
||||
`DEFAULT_GENERIC_FAMILIES`'s `["monospace"]` is looked up against
|
||||
`name_map` *before* the `fonts.xml` parse that adds the name runs, and even
|
||||
after parsing, this AVD's `/system/etc/fonts.xml` (and AOSP's/GrapheneOS's,
|
||||
same file format) names it with a `<family name="monospace"><font
|
||||
...>DroidSansMono.ttf</font></family>` element rather than an `<alias>` --
|
||||
whose `<font>` children that same parser's `"family"` match arm never reads
|
||||
(a `TODO` left in place), so the name gets registered with no font data
|
||||
behind it. `family_by_name("monospace")` therefore also comes up empty, on
|
||||
every Android device this fontique version runs on, not just this AVD.
|
||||
Checked against `linebender/parley`'s `main` branch on GitHub the same day:
|
||||
neither bug is fixed there either, so there is no newer release to bump to.
|
||||
The visible effect was not blank text -- `Family::Monospace`'s
|
||||
explicit-family list came up empty, but the script-based fallback chain
|
||||
(independent of the generic-family list) still resolved a real font, the
|
||||
same one `SansSerif` gets -- so code blocks and the tool-card chevrons
|
||||
rendered, just without a genuinely monospaced face, while Compose's
|
||||
`FontFamily.Monospace` (resolved through Android's own `Typeface.MONOSPACE`
|
||||
constant, a path fontique does not use) was unaffected.
|
||||
|
||||
**Fixed** in `iris/core/src/primitive/text.rs`'s `patch_android_monospace`
|
||||
(`#[cfg(target_os = "android")]`, called from `TextData::default` right
|
||||
after `FontContext::new()`): rather than pinning an OEM-specific name like
|
||||
`"Droid Sans Mono"` (the fragility this gap was originally left open over),
|
||||
it reads `/system/etc/fonts.xml` itself -- a plain substring search, not a
|
||||
new XML-parser dependency, for the one well-known AOSP file fontique
|
||||
already parses with a real one -- for the filename the `"monospace"`
|
||||
family declares, then searches fontique's own *actually* scanned families
|
||||
(the ones with real font data, from `/system/fonts`) for whichever one
|
||||
owns a font file with that name, and registers that family as the
|
||||
`Monospace` generic itself. This is the same authority Compose's
|
||||
`Typeface.MONOSPACE` resolves through, and it degrades safely to a no-op
|
||||
if `fonts.xml` is missing (a headless test) or nothing matches (a device
|
||||
naming it some other way) -- the pre-existing sans fallback, not a panic.
|
||||
Verified on this checkout's emulator: `mono=Some("Droid Sans Mono")` in the
|
||||
startup log, `resolved ... mono=Some("Droid Sans Mono")`, and a screenshot
|
||||
of the bench-fixture transcript showing the code block and tool-card value
|
||||
text in a visibly monospaced face next to sans body/heading text. The
|
||||
desktop's `fontconfig` backend was never affected (confirmed unchanged:
|
||||
`./run-headless.sh phone --phone --shot` still shows monospaced code next
|
||||
to sans body text) -- the patch is Android-only and a no-op everywhere
|
||||
else.
|
||||
|
||||
**Verified**: `cargo test -p transcript-fixture` (6 tests, all headless
|
||||
layers) and `cargo clippy -p iris-core --all-targets` both clean;
|
||||
@@ -478,12 +499,16 @@ closes it.
|
||||
below for the fallback behaviour and one real gap it surfaced: this
|
||||
fontique version's Android backend never resolves the `Monospace`
|
||||
generic family at all (`mono=None` in the startup diagnostic, measured
|
||||
on this checkout's emulator) -- code/tool-card text still renders (the
|
||||
script fallback chain still lands on a real face, never blank), just
|
||||
not in a genuinely monospaced one. Compose does not have this gap; it
|
||||
on this checkout's emulator) -- code/tool-card text still rendered (the
|
||||
script fallback chain still landed on a real face, never blank), just
|
||||
not in a genuinely monospaced one. Compose did not have this gap; it
|
||||
resolves `FontFamily.Monospace` through Android's own Typeface
|
||||
constant rather than through fontique. Flagged as a follow-up, not
|
||||
fixed here -- out of the scope Iris gave.
|
||||
constant rather than through fontique. **Closed same day** -- see
|
||||
"Platform fonts (2026-09-07)"'s "Fixed" paragraph:
|
||||
`TextData::patch_android_monospace` resolves the platform's own
|
||||
`fonts.xml` monospace declaration against fontique's actually-scanned
|
||||
families, Android-only, verified `mono=Some("Droid Sans Mono")` on this
|
||||
checkout's emulator.
|
||||
- [ ] Scroll clamped at both ends, and Compose's impulse velocity
|
||||
estimator with min/max fling velocity (docs/IRIS_TODO.md, 2026-09-07
|
||||
later). After the culling fix lands (same file).
|
||||
|
||||
Reference in new issue
Block a user