From 452c44249fd50fa5c377d796df0946c882133a79 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Mon, 7 Sep 2026 16:22:29 -0400 Subject: [PATCH] docs/RUST.md: queue -- logging landed; iris app enrolment replaces the build-time log destination; build-apk.sh traps Co-Authored-By: Claude Fable 5.1 --- docs/RUST.md | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/docs/RUST.md b/docs/RUST.md index 1d16949..d5f9807 100644 --- a/docs/RUST.md +++ b/docs/RUST.md @@ -487,6 +487,27 @@ closes it. - [ ] 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). +- [ ] Iris app enrolment (decided 2026-09-07): the bench APK's log + destination is baked from `AI_APP_LOG_*` at build time, which cannot + work for Iris's phone -- the APK is built in the VM, whose CA and + token are not the host's, and no secret may go in a repo or a + delivered artifact. Replace with the mechanism the Compose app and + `desktop-app` already use: the iris Android app registers the + `aiapp://enroll?host&port&token` VIEW intent, stores the enrolment + the way `client_core::config` does, pins the CA the same way (the + Compose app reads it at build time from the *building* machine, which + is the same problem -- so for the phone the CA has to arrive with the + link or the build has to happen on the host; check how Dev Updater's + Enroll button and `ai-server --enroll-link` present the link and + whether the CA fingerprint can ride in it as a query parameter, which + `wg-app-link` may already support). `log_upload` then takes its + destination from the enrolment at runtime, `AI_APP_LOG_*` is removed, + and Dev Updater's enrol screen offers the link as a tappable link so + Android hands it to the iris app. Rejected: building the iris APK on + the host (needs cargo-ndk and an NDK there, and still bakes a token). +- [ ] `iris/android-app/build-apk.sh`: clear Gradle's merged-native-libs + cache when the ABI changes (the x86_64 trap), and make the debug bench + APK installable (648 MB) -- RUST.md's logging section names both. - [ ] Input-event and timing instrumentation into the log ring, copied by the report button. After the logging route lands (same ring). - [ ] Masks with a shape -- docs/LAYOUT.md "Masks with a shape (decided @@ -744,6 +765,117 @@ the targetSdk reading was wrong and the window is still being resized. different fault from either. For the fling, a flick should now visibly slow before it stops rather than running out at speed. +### The fling started too slow: Compose fits a curve, iris averaged (2026-09-07) + +Iris 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 curve was right -- that was the previous +fix -- so the wrong number was the *initial velocity*. + +**What iris did.** `VelocityTracker` held the last 100ms of per-frame +pan *deltas* and answered their sum over the span between the oldest and +newest: an average. An average cannot tell an accelerating flick from a +steady drag, and a flick is by definition accelerating, so every fling +started at roughly the speed of the middle of the gesture rather than +the speed at the release. Every test it had asserted the average's own +definition back at it, which is the same shape of self-grading the +spline shipped a straight line through. + +**What Compose does, which is not what it is remembered as.** Read out +of the `-sources.jar` of `androidx.compose.ui:ui-android:1.12.0` and +`androidx.compose.foundation:foundation-android:1.12.0` on +dl.google.com -- the versions `app/gradle/libs.versions.toml` builds the +Compose app against, which is the app being compared with: + +- `scrollable`/`draggable` release through `DragGestureNode. + sendDragStopped`, which calls the **2D `VelocityTracker`**. On Android + that delegates to `Lsq2VelocityTracker` -- two + `VelocityTracker1D(strategy = Lsq2)` over **absolute positions**, + fitting a degree-2 polynomial by least squares (`polyFitLeastSquares`, + Gram-Schmidt QR) and taking its derivative at the newest sample. +- **`Strategy.Impulse` is not on the touch path.** Its only route in is + `DifferentialVelocityTracker`, whose only caller is + `NonTouchScrollingLogic`: mouse wheel and trackpad. + `AndroidComposeUiFlags.isFrameworkVelocityTrackerEnabled`, which would + swap in the platform's own (impulse) tracker, defaults to `false`. + This was the surprise of the port, and the reason to read rather than + remember -- the plan for this task named Impulse. +- Constants: `HistorySize = 20`, `HorizonMilliseconds = 100`, + `AssumePointerMoveStoppedMilliseconds = 40`, `minSampleSize = 3` for + Lsq2. The walk back from the newest sample stops at the first sample + older than the horizon **or** separated from its neighbour by more + than the stopped gap. +- **Which samples.** `sendDragStart` adds the DOWN change; every + subsequent MOVE, historical samples included, is added by + `sendDragEvent`. The **UP position is never added** -- + `Lsq2VelocityTracker.addPointerInputChange` wraps its `addPosition` + calls in `if (!event.changedToUpIgnoreConsumed())`, and the UP branch + only resets the tracker when 40ms have passed since the last MOVE + (b/238654963). So a finger that comes to rest before lifting reads as + a stop, not as a decelerating tail. + +**The clamps, both checked rather than assumed.** Maximum: +`ViewConfiguration.getScaledMaximumFlingVelocity()`, 8000 dp/s, passed +into `calculateVelocity(maximumVelocity)` at the release -- 20400px/s at +the phone's density 2.55, which this flick does not reach. Minimum: +**there is none on the fling path.** `ViewConfiguration. +minimumFlingVelocity` (AOSP's 50 dp/s) exists in Compose's +`ViewConfiguration` interface, but its only use in either artifact is +`NestedScrollInteropConnection`, for View interop. +`DefaultFlingBehavior.performFling` guards with +`abs(initialVelocity) > 1f` and says why in its own comment ("we need it +since spline curve gives us NaNs"). So iris applies 1px/s, not 50dp/s: a +50dp/s floor would swallow slow deliberate releases that Compose flings. +Both live in `List::fling`, which is the only place that knows the +density the dp figure has to be multiplied by. + +**What changed.** `VelocityTracker` holds **positions**, not deltas +(Lsq2 refuses differential data in Compose too), capped at 20 samples; +`add_sample(delta, at)` is now `add_position(position, at)`, and +`DragGesture` feeds the raw window coordinate along the drag axis at the +press and at every `Pan` frame -- the same set Compose feeds, minus the +one MOVE that crosses the touch slop, which Compose drops only because +`sendDragStart` happens to add just the DOWN. `poly_fit_least_squares` +is Compose's `polyFitLeastSquares` on fixed-size arrays. + +**`iris/benches/velocity_reference.py`** is the independent +transcription, same role as `fling_spline_reference.py`, and prints +every number the tests assert. On the phone's own recording +(`transcript-fixture/touch/flick-120hz.touch`, five samples in 16ms): + +| sample set | shipped (average) | Compose (Lsq2) | +| --- | --- | --- | +| `flick-120hz.touch` | 11750 px/s | **15250 px/s** | +| steady drag, 5px/10ms | 500 px/s | 500 px/s | +| accelerating flick, deltas doubling | 1080 px/s | **2445 px/s** | +| old fast burst then 1px/10ms | 9182 px/s | 100 px/s | +| stopped 48ms, then released | 2533 px/s | 0 px/s | +| press + one move frame | 11750 px/s | 0 px/s | + +The recording understates the change (1.30x) because it is only 16ms +long; the accelerating set (2.26x) is the shape of a real finger flick +and is what Iris was feeling. The last three rows are the cases an +average gets not just low but *wrong*: it flings from a standstill, and +it flings from two points that describe no curve. + +**Tests.** Eight in `sense::velocity_tracker_tests`, two rewritten in +`sense::drag_gesture_tests` (three samples is the fewest that can fling; +one move frame answers 0, as Compose does), and `phone_screen.rs` now +asserts -15250px/s rather than "more than 1000". **Negative control +run**: with `velocity` reverted to `total / span`, exactly seven fail -- +the flick recording, the accelerating flick, the horizon, the stopped +finger, the minimum sample count, both `drag_gesture` flick tests -- and +the steady drag, the tap, the selection release, all sixteen arbiter +tests and the whole of `phone_screen.rs` bar the flick pass unchanged. +That is the half the change had no reason to touch. + +**`iris drag release:` keeps its info line and gains a debug one**, +`iris drag release samples:`, printing every held sample as +`t_ms:position` relative to the first. Iris has no logcat, so that is +the only way a flick that felt wrong on her screen becomes something +replayable: paste it into a `touch/*.touch` file for layer 1, or +straight into `velocity_reference.py`. + ### The 22:16 phone report, worked 2026-09-06/07 Iris's four items are listed in docs/IRIS_TODO.md's "From the phone,