iris: the fling curve was the identity function, and the keyboard was a targetSdk

Iris's 2026-09-07 phone report on ed04d4c: the resume glyph corruption is
fixed (item 4 closed with her evidence), flinging "seems to just be linear
velocity with an abrupt stop", and the keyboard still does not push
anything up. docs/RUST.md's new "The 2026-09-07 phone report" section has
the derivation and every number.

**The fling was arithmetically linear.** `android_fling_spline::
distance_fraction(t)` returned `t` for every `t`. Two halves of AOSP's
`SplineOverScroller` static initialiser had been transposed -- the
bisection solved the tension curve and the sample evaluated the P1/P2 one,
where AOSP does the opposite -- which made SPLINE_POSITION and SPLINE_TIME
identical; the lookup then bracketed `t` between SPLINE_TIME entries
instead of between even time steps, and the two cancelled to the identity.
Ported exactly now from OverScroller.java and androidx.compose.animation
1.12.0's SplineBasedDecay.kt, which agree line for line, as one table
indexed by even steps of time (AOSP's second table serves only
`adjustDuration`, which nothing here has, so it is deliberately not built
-- one array, one indexing rule). `FlingCalculator::velocity_at` is new
beside `position_at`, and `List::tick_fling` logs `iris fling tick:` with
the per-frame delta and speed.

Every existing test compared the calculator with itself -- monotonic,
signed, integrates to the closed form, deltas non-increasing -- and all of
them pass on a straight line. iris/benches/fling_spline_reference.py is an
independent hand transcription of both sources and supplies the numbers
now checked into `the_spline_matches_aosps_own_table` and
`a_flick_decelerates_the_way_aosp_says_it_does`;
`tick_fling_applies_shrinking_incremental_deltas` went from
"non-increasing" to "the last delta is under 80% of the first". Negative
control: with `sample` forced back to `t`, exactly those three fail.

Emulator (API 36, debug, force-gles): a released v=3750 decelerates
3746 -> 2624 -> 1834 -> 1144 -> 752 -> 449 -> 243 -> 83px/s over 32 frames
to t=0.664s; a flick into the end of the list stops there in one tick with
no overshoot; a tap 200ms into a fling ends it at 11 ticks.

**The keyboard: `targetSdk = 34`** in iris/android-app/app/build.gradle,
against compileSdk 37 and the Compose app's 37 -- and that app's keyboard
does push up on her phone. Below target 35 a window keeps the legacy
behaviour where adjustResize shrinks it for the IME, so
getInsets(ime()).bottom measures an already-shrunk window and is zero;
setDecorFitsSystemWindows(false) opts out of that and still takes on the
API 36 emulator here, which is why every test run passed. Now targetSdk 37.

That is a reading and not a measurement, so the other half is making the
phone able to answer it. MainActivity also registers a
WindowInsetsAnimation.Callback (onEnd re-reads getRootWindowInsets, so an
interrupted animation cannot freeze a value), which delivers the height
where only the animation path carries it and makes the push-up animate:
ime_bottom now arrives 509, 663, 833, 881, 883 instead of one jump.
`insets::Shared::updates` counts every dispatch and
`AndroidUiState::insets_report()` puts it in the Diagnostics pane --
screenshot-verified, `insets: dispatches=27 left=0 top=142 right=0
bottom=63 ime_bottom=0 ime_visible=false`. Iris has no logcat, and "the
listener never fired" and "it fired with a zero height" are otherwise the
same picture; dispatches=0 says so in words rather than showing defaults.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-07 12:44:01 -04:00
1 parent 7f4ea7e8fd
commit 73f956f8e0
11 files changed
+720 -120

No files matched your search

+36
View File
@@ -8,6 +8,42 @@ 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: the fling curve was the identity function
You said the fling "seems to just be linear velocity with an abrupt stop."
It was, exactly: `android_fling_spline`'s lookup returned `t` for every
`t`. Two halves of AOSP's spline build loop had been transposed, which made
its two tables identical, and the lookup interpolated one against the
other -- which reduces algebraically to `t`. So a fling coasted at its
release speed for the whole (correctly computed) duration and stopped dead
at the end of it.
Ported exactly now from `OverScroller.java` and Compose's
`SplineBasedDecay.kt`, which agree line for line. One public addition:
**`FlingCalculator::velocity_at(velocity, elapsed) -> f32`**, beside the
existing `position_at` -- AOSP's `mCurrVelocity` and Compose's
`FlingInfo.velocity`. It is what makes "is this decelerating" answerable
rather than inferred, and it is what `List::tick_fling`'s new
`iris fling tick:` debug line reports each frame.
The lesson worth keeping, since it cost two builds on your phone: every
test the calculator had compared it with itself -- monotonic, correctly
signed, integrates to the closed form, per-tick deltas non-increasing --
and **all of them pass on a straight line**. The numbers now come from
`iris/benches/fling_spline_reference.py`, a separate hand transcription of
the two sources, checked in beside the tests.
## 2026-09-07: the Android insets bridge counts its own dispatches
`AndroidUiState::insets_report() -> String` is new, and the bench app's
Diagnostics pane shows it. It carries the last insets plus **how many times
the platform has delivered any**, because "the keyboard did not push
anything up" has two causes that look identical on screen -- the listener
never fired, or it fired with a zero height -- and you have no logcat on
the phone. `dispatches=0` prints a sentence saying so rather than the
numbers, which would be defaults rather than measurements.
## 2026-09-07: widgets can animate, and a fling finally moves
Iris's phone said "fling still doesn't work" twice. The velocity was only
+50 -17
View File
@@ -351,13 +351,27 @@ agent ticks it here with the evidence.
Iris's report, verbatim, with a screenshot. Phone: Mali-G715 (Vulkan),
`content_scale: 2.55`, 120Hz. Open until ticked with phone-side evidence.
- [ ] **"Fling still doesn't work."** *(Three defects fixed 2026-09-07;
open until the phone says so. **The line to look for:**
`adb logcat | grep "iris drag release"` -- `samples=1` or `span=0.0ms`
means the batched samples are not reaching the tracker there, while a
sensible span with `v=` in the thousands and
`outcome=Released(Some(…))` means the gesture was measured right and
anything still wrong is downstream.)* Second report; the emulator's
- [ ] **"Fling still doesn't work."** -> on `ed04d4c`, 2026-09-07:
*"flinging now does technically do something, but it seems to just be
linear velocity with an abrupt stop."* **It was exactly that, and the
arithmetic said so.** `distance_fraction(t)` returned `t` for every `t`
-- a constant-speed slide for the whole duration, then a stop at full
distance -- because two halves of AOSP's spline build loop were
transposed, which made `SPLINE_POSITION` and `SPLINE_TIME` identical, and
the lookup bracketed `t` between `SPLINE_TIME` entries rather than
between even time steps. The two cancelled to the identity. Ported
exactly now from `OverScroller.java` and
`androidx.compose.animation:animation:1.12.0`'s `SplineBasedDecay.kt`
(they agree line for line), with `iris/benches/fling_spline_reference.py`
as an independent transcription supplying the numbers the tests assert
on. Emulator, 2026-09-07: a released `v=3750` decelerates
`3746 -> 2624 -> 1834 -> 1144 -> 752 -> 449 -> 243 -> 83px/s` across 32
frames; a flick into the end of the list stops there in one tick with no
overshoot; a tap 200ms into a fling ends it at 11 ticks instead of 32.
**Open until the phone says so** -- a flick should now visibly slow
before it stops. Its earlier three defects (the velocity, the missing
animation registration, the 56x coefficient) are all still fixed and were
never the linear part.* Second report; the emulator's
`ui-trace` swipe flings (verified 2026-09-06 with `render()` counts),
a finger on the phone does not. What differs: a real flick at 120Hz is
batched by Android into few `MotionEvent`s with *historical* samples
@@ -383,11 +397,29 @@ Iris's report, verbatim, with a screenshot. Phone: Mali-G715 (Vulkan),
`EditText` shows the IME on every tap of a focused field; do the same
(`FocusHost`: a tap on a focused field requests the IME, idempotent
when it is already shown).
- [x] **"Message box does not push up the scroll area."** *(Fixed
2026-09-07: height and visibility are two JNI values now. Emulator:
`iris insets: … bottom=883 ime_bottom=883 ime_visible=true`, composer
box `31,2277..1048,2329` -> `31,1457..1048,1509`, and the list follows
because it is `rest(1)` in the same `Span`.)* Since
- [ ] **"Message box does not push up the scroll area."**
**Reopened by the phone on 2026-09-07** -- *"similarly, the keyboard
raising up does not push things upwards"* -- after being ticked on
emulator evidence the day before (`ime_bottom=883`, composer box
`31,2277..1048,2329` -> `31,1457..1048,1509`). The JNI half was right;
what was wrong is one line of `iris/android-app/app/build.gradle`:
**`targetSdk = 34`** against `compileSdk = 37`, while the Compose app in
`app/` targets 37 and *does* push up on her phone. Below target 35 the
window keeps the legacy behaviour, where `adjustResize` shrinks it for
the IME and `getInsets(ime()).bottom` therefore measures zero;
`setDecorFitsSystemWindows(false)` opts out of that and still takes on
the API 36 emulator here, which is why every test run passed. Now
`targetSdk = 37`, plus a `WindowInsetsAnimation.Callback` for the devices
where only the animation path carries the height -- which also makes the
push-up animate (`ime_bottom=509, 663, 833, 881, 883` instead of one
jump). **This is a reading, not a measurement**: no Android 17 device is
reachable from here. So the Diagnostics pane now prints
`insets: dispatches=N left=… ime_bottom=… ime_visible=…` --
**screenshot that line with the keyboard open.** `ime_bottom` in the
hundreds and the composer risen means fixed; `dispatches` climbing with
`ime_bottom=0` means the reading was wrong and the window is still being
resized; `dispatches=0` means the listener is not firing at all, which is
a third thing again.* Since
`MainActivity` went edge-to-edge (`e12c708`), `adjustResize` no
longer resizes the window, so the app owns the IME inset -- but
`ime_bottom` is passed through JNI as the boolean `1`/`0` (the
@@ -418,11 +450,12 @@ Iris's report, verbatim, with a screenshot. Phone: Mali-G715 (Vulkan),
screenshotted the emulator's GLES path, where a resume may not
destroy the surface at all.
**Fixed in `ba2afba`, with `clearing_the_atlas_re_renders_cached_text_
instead_of_reusing_it` run and passing (2026-09-07). Ticked on the
code; still wants phone-side confirmation** -- no emulator here has a
Vulkan adapter, and the GLES path may not destroy the surface at all,
so the emulator cannot reproduce the state Iris photographed.
**Fixed in `ba2afba` and confirmed on the phone (Iris, 2026-09-07:
"the resume glyph corruption is fixed").** Closed. The emulator could
never have settled it -- no Vulkan adapter here, and the GLES path may
not destroy the surface at all -- so the phone was the only place this
could be answered, and it has been. `clearing_the_atlas_re_renders_
cached_text_instead_of_reusing_it` is what keeps it.
The reading above is right and the mechanism is one step narrower than
"cached text primitives". `IrisViewPeer::surface_changed`
+123
View File
@@ -115,6 +115,129 @@ velocity and a moved scroll offset; one command opens the same screen in
a phone-shaped window and screenshots it. Record the commands here when
it lands.
### The 2026-09-07 phone report on `ed04d4c`: the fling was linear, and the keyboard is a targetSdk
Iris's three lines on the `ed04d4c` build (Pixel 9 Pro XL, GrapheneOS
Android 17, Mali-G715, `content_scale` 2.55, 120Hz): item 4 (the resume
glyph corruption) **is fixed**, confirmed on the phone; "flinging now does
technically do something, but it seems to just be linear velocity with an
abrupt stop"; and "similarly, the keyboard raising up does not push things
upwards."
**The fling was exactly, arithmetically linear.** Not approximately.
`android_fling_spline::distance_fraction(t)` returned `t` for every `t`,
which is a constant-speed slide for the full `duration()` and then a stop
at full distance -- Iris's sentence, read straight off the code. Two
transposed halves of one AOSP loop did it, and they compounded:
1. AOSP's `SplineOverScroller` static initialiser **solves** the bisection
on the `P1`/`P2` curve and **samples** `SPLINE_POSITION[i]` from the
tension curve (`coef * ((1-x) * START_TENSION + x) + x³`); the second
half of the loop does the reverse to build `SPLINE_TIME`. iris had both
halves solving on the tension curve and sampling `P1`/`P2` -- so its two
loops were the *same computation*, and `SPLINE_POSITION == SPLINE_TIME`
element for element.
2. The lookup then bracketed `t` between **`SPLINE_TIME` entries** and
interpolated `SPLINE_POSITION`. AOSP brackets between the even time
steps `index / N` and `(index + 1) / N` (`SPLINE_TIME` is used only by
`adjustDuration`, which iris has no analogue of). With the two arrays
identical, `d_inf + (d_sup - d_inf)(t - t_inf)/(t_sup - t_inf)` reduces
to `t_inf + (t - t_inf)` = `t`.
Every test the calculator had compared it with itself -- monotonic, signed,
integrates to the closed form, per-tick deltas non-increasing -- and all of
them pass on a linear curve. That is the shape to distrust: `<=` is not
deceleration.
**Sources, read rather than remembered.** `frameworks/base`
`core/java/android/widget/OverScroller.java` from
`android.googlesource.com` (`?format=TEXT`, base64), and
`androidx.compose.animation:animation:1.12.0`'s `SplineBasedDecay.kt` and
`FlingCalculator.kt` out of the `-sources.jar` on
`dl.google.com/dl/android/maven2` (there is no androidx checkout here and
`cs.android.com` is JS-only; `androidx.tech` is now a parked domain serving
an unrelated site). The two agree line for line, which is why iris ports
one curve rather than two. The formulas, for the record:
P1 = START_TENSION * INFLEXION = 0.5 * 0.35
P2 = 1 - END_TENSION * (1 - INFLEXION) = 1 - 1 * 0.65
SPLINE_POSITION[i]: solve coef*((1-x)P1 + xP2) + x³ = i/100 for x,
then take coef*((1-x)ST + x) + x³
physical_coeff = 9.80665 * 39.37 * density * 160 * 0.84
l = ln(0.35 * |v| / (0.015 * physical_coeff))
distance = 0.015 * physical_coeff * exp(rate/(rate-1) * l)
duration = exp(l / (rate - 1)), rate = ln(0.78)/ln(0.9)
at time t: index = floor(100 * t/duration)
vcoef = (POS[index+1] - POS[index]) * 100
position = distance * (POS[index] + (t/duration - index/100) * vcoef)
speed = vcoef * distance / duration
**What changed.** `iris/src/sense.rs`'s `android_fling_spline` builds one
table, indexed by even time steps, and `sample(t)` answers AOSP's
`distanceCoef`/`velocityCoef` pair; `FlingCalculator` gained `velocity_at`
beside `position_at`. `iris/benches/fling_spline_reference.py` is an
independent hand transcription of both sources and prints the numbers the
tests assert on -- checked in because "numbers computed by the code under
test" is exactly how the last three tests passed through this defect.
Tests: `the_spline_matches_aosps_own_table` (the curve is not the
identity: 27.4% of the distance at a tenth of the time, 85.8% at half),
`a_flick_decelerates_the_way_aosp_says_it_does` (11064px/s at density
2.55: 6334px over 1.636s, speed 9202 -> 4733 -> 2650 -> 951px/s), and
`tick_fling_applies_shrinking_incremental_deltas` strengthened from
"non-increasing" to "the last delta is under 80% of the first". **Negative
control run**: with `sample` forced back to returning `t`, exactly those
three fail and the other eleven pass.
**Emulator evidence (API 36 AVD, debug, `force-gles`, 2026-09-07).** A
`ui-trace` swipe of 900px in 120ms releases at `v=3750` and the new
`iris fling tick:` debug line reports, frame by frame,
`speed=-3746 -> -2624 -> -1834 -> -1144 -> -752 -> -449 -> -243 -> -83px/s`
over 32 frames ending at `t=0.664s`, with the per-frame `dy` falling
`94 -> 34 -> 20 -> 13 -> 8 -> 4.4px`. **The end**: the same flick in the
other direction, from a list already at its newest end, produces exactly
one tick and stops -- no overshoot. **A finger during a fling**: swipe,
then a tap 200ms later, ends the fling at `t=0.248s` and 11 ticks instead
of running its full 0.55s.
**The keyboard: the bench app targeted SDK 34.** `app/build.gradle` said
`targetSdk = 34` while `compileSdk` was 37 and the Compose app in `app/`
targets 37 -- and that Compose app's keyboard *does* push its transcript up
on Iris's phone. Below target 35 a window keeps the legacy behaviour, where
`adjustResize` shrinks the window for the IME so `getInsets(ime()).bottom`
measures the overlap with an already-shrunk window and is zero;
`MainActivity`'s `setDecorFitsSystemWindows(false)` opts out of that and
still takes on the API 36 emulator here, which is why every test run showed
the push-up working. It is deprecated as of API 35 and Android 17 is where
it appears no longer to. Fixed by `targetSdk = 37`, where edge-to-edge is
not opt-in.
That is a *reading*, not a measurement -- no Android 17 device is reachable
from here -- so the second half of the change is making the phone able to
answer it. `MainActivity` now also registers a
`WindowInsetsAnimation.Callback` (`DISPATCH_MODE_CONTINUE_ON_SUBTREE`,
`onProgress` forwarding, `onEnd` re-reading `getRootWindowInsets` so an
interrupted animation cannot leave a frozen value), which delivers the IME
height on devices where only the animation path carries it and, on every
device, makes the push-up *animate* with the keyboard: the emulator log now
shows `ime_bottom=509, 663, 833, 881, 883` instead of one jump to 883. And
`insets::Shared::updates` counts every dispatch, which
`AndroidUiState::insets_report()` puts in the **Diagnostics pane**:
insets: dispatches=27 left=0 top=142 right=0 bottom=63 ime_bottom=0 ime_visible=false
Screenshot-verified on the emulator. Iris has no logcat, and "the listener
never fired" and "it fired with a zero height" look identical on screen;
`dispatches=0` prints its own sentence instead of the numbers, since those
would be defaults rather than measurements.
**What to look at on the next build.** Open the keyboard, press
`Diagnostics`, screenshot the `insets:` line. `ime_bottom` in the hundreds
with the composer risen: fixed. `dispatches` climbing but `ime_bottom=0`:
the targetSdk reading was wrong and the window is still being resized.
`dispatches=0`: the listener is not being called at all, which is a
different fault from either. For the fling, a flick should now visibly
slow before it stops rather than running out at speed.
### 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,