Iris asked why `mark` existed at all -- "the font should be working if
it's working for compose and nerd fonts are bundled". It was not: the
Compose app draws its icons from its own committed Nerd Fonts subset,
while iris, which bundles no font since 2026-09-07, was setting the
disclosure mark with bare geometric codepoints (U+25B8/25BE/25B4) out of
whatever face the platform resolved -- an empty box on her phone, a dot
on this VM. The 2026-09-07 note that "iris had no equivalent icon font to
keep" is the gap: it had none because it had never had one.
So iris ships the same kind of subset. iris/core/build-icon-font.sh is
the Compose script with its own GLYPHS list, writing a 992-byte
nerd_icons.ttf with three Material Design glyphs from the Mono face;
iris::icon names the codepoints; Family::Icons is how text asks for them.
The variant names an intention rather than a font name -- only TextData
knows what the file registered as, and it resolves it during shaping --
and it is a named family, never a generic one, so nothing falls back into
it for text and an icon cannot fall back out of it onto a system face
that happens to have the codepoint.
every_icon_is_in_the_bundled_font maps each constant through the shipped
font's charmap, which is the guard the script's "the two lists have to
agree" comment asks for. FontDiagnostics gains icon_family, so a build
whose font failed to register says so instead of drawing tofu; the
emulator reports icons=Some("Symbols Nerd Font Mono").
widget/mark.rs is deleted. It drew one correct triangle, but every
further icon would have been another rasteriser, and an icon as text
takes the size, colour and baseline of the line it sits in for free.
Looked at rather than only compiled: closed and open marks in
run-headless.sh phone --phone either side of a tap, and the collapse
bar's up mark under IRIS_TOOLS_EXPANDED=1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1153 lines
72 KiB
Markdown
1153 lines
72 KiB
Markdown
# iris: known problems and things still to build
|
|
|
|
Iris's own list for the library, recorded 2026-09-04 in her words where it
|
|
matters, so the agents working through RUST.md pick these up in a sensible
|
|
order rather than rediscovering them. Each item says where it sits in the
|
|
order and what "done" looks like. Tick and date them in place.
|
|
|
|
## Fix
|
|
|
|
- [x] **`request_device` asked for compute-shader limits it never uses
|
|
(2026-09-05).** `Limits::default()` (both `iris/src/android/render.rs`
|
|
and `iris/src/default/render.rs`) requests desktop-tier compute limits
|
|
unconditionally, even though nothing in `iris`/`iris-core` creates a
|
|
`ComputePipeline` or writes a `@compute` shader stage — confirmed by
|
|
grepping the whole tree, not assumed. That crashed device creation
|
|
outright on the Android emulator's software GL path (`EMU_GPU=software`,
|
|
`--features force-gles`): SwiftShader's GL reports itself as OpenGL ES
|
|
3.0, which has no compute shaders, so the adapter's real limit is 0
|
|
against the unconditional request for 65535 — the same would happen on
|
|
any real GLES-3.0-only Android device. Fixed by a new, shared
|
|
`iris_core::device_limits()` (`iris/core/src/render/mod.rs`) that zeros
|
|
exactly the six `max_compute_*` fields rather than switching to a
|
|
downlevel `Limits` preset — `downlevel_webgl2_defaults()` also zeros
|
|
`max_storage_buffers_per_shader_stage`, which `shader.wgsl`'s vertex
|
|
stage needs (four `var<storage>` buffers), so that preset would trade
|
|
this crash for a bind-group-layout one on the same hardware.
|
|
`rigs/gpu-probe`'s own hand-mirrored `Limits` (it is deliberately its
|
|
own crate, not able to call `device_limits()` directly) was updated to
|
|
match. See `DECISIONS.md` and RUST.md's I5 box for the account,
|
|
including what could not be re-verified on-device this pass (the
|
|
emulator was in concurrent use by another session).
|
|
|
|
- [x] **Input does not fall through by input type (2026-09-04).**
|
|
`SensorUi::run_sensors` (`src/default/sense.rs`) used to set "consumed,
|
|
stop checking lower layers" from mere hover — a widget registered for
|
|
nothing but `click()` blocked a `Scroll` meant for whatever was behind
|
|
it, since "the cursor is over this widget" and "this widget handled the
|
|
event" were the same check. Fixed by judging consumption per input
|
|
kind: with no button transition and no scroll happening this frame
|
|
("momentary" activity), the topmost hovered widget still wins, same as
|
|
before; when something momentary *is* happening, only a widget whose
|
|
registered senses actually include a matching non-hover one (checked
|
|
via a new `TypeEventManager::registered`, which lists what a widget
|
|
registered without running anything) consumes it, so a widget with only
|
|
`Hovering`/click handlers can no longer block a scroll from reaching a
|
|
list underneath. `iris/src/sense_tests.rs` builds a button-over-a-list
|
|
`Stack` with a plain `HasEvents` impl (no GPU or window) and checks both
|
|
directions: a scroll over the button reaches the list, and a real click
|
|
still reaches the button — confirmed to fail on the pre-fix code and
|
|
pass after.
|
|
|
|
- [x] **Appending one image to an already-loaded list rebuilds every other
|
|
image's bind group (2026-09-05, fixed 2026-09-05).** Found by the
|
|
benchmark below: `GpuTextures::update` (`core/src/render/texture.rs`)
|
|
triggered `rebuild_image_bind_groups` — a loop over *every live
|
|
standalone image*, rebuilding its `BindGroup` — whenever the shared
|
|
`masks` or `move_offsets` GPU buffer was resized (`masks_resized ||
|
|
moves_resized` in `UiRenderNode::update`, `core/src/render/mod.rs`), and
|
|
a widget getting its *first* move-offset slot (LAYOUT.md section 2 —
|
|
every widget gets one on first draw) could be exactly what grows that
|
|
buffer. So one new message with one new image, appended to a transcript
|
|
that already has N images loaded, did not cost O(1): it cost one
|
|
`create_image` for the new image plus one `make_image_bind_group` per
|
|
*existing* image, because the new widget's own move slot pushed the
|
|
arena past its capacity. Measured directly in
|
|
`iris/examples/bench_images.rs`: appending a 1,001st image to 1,000
|
|
already-settled ones reported **1,001** bind-group creates for that one
|
|
frame, not 1 (`./run-bench.sh images`, frame 5 in the transcript below).
|
|
|
|
**Fix**: `masks`/`move_offsets` never belonged in a standalone image's own
|
|
bind group (group 2) in the first place — the group also holds that
|
|
image's own texture view, which is the only thing that is genuinely
|
|
per-image, so a buffer shared by *everything* forced a rebuild of
|
|
*every* group the moment it moved. Gave masks/move_offsets their own
|
|
bind group (group 3 in `shader.wgsl` and `UiRenderNode`: `masks_layout`/
|
|
`masks_group`), bound once per frame in `UiRenderNode::draw` rather than
|
|
once per draw call, instead of duplicating them into every per-image
|
|
group. `GpuTextures` and its image bind groups now know nothing about
|
|
either buffer — `rebuild_image_bind_groups` is called only from
|
|
`grow_array` (the atlas array texture growing, which genuinely does
|
|
change what every image's own bind group must reference) — so a
|
|
masks/move_offsets resize now touches exactly one bind group, ever,
|
|
regardless of how many images are live. Numbers after the fix, same
|
|
benchmark and command:
|
|
|
|
./run-bench.sh images
|
|
frame=1 bind_group_creates=1000 (cold load, unchanged)
|
|
frame=2 bind_group_creates=0 (was 1000 -- see the item below)
|
|
frame=3 bind_group_creates=0
|
|
frame=4 bind_group_creates=0
|
|
(append one image here)
|
|
frame=5 bind_group_creates=1 (was 1001)
|
|
frame=6 bind_group_creates=0
|
|
|
|
`run-headless.sh tabs --shot` still 27266 bytes, byte-for-byte unchanged,
|
|
confirming the bind-group restructuring changed nothing about what is
|
|
drawn.
|
|
- [x] **Bind-group creation takes two frames to reach the steady state, not
|
|
one (2026-09-05, closed by the fix above, 2026-09-05).** Same benchmark:
|
|
loading 1,000 images cold used to report 1,000 creates on frame 1
|
|
(expected — `create_image`, one per new image) *and again* 1,000 on
|
|
frame 2, before settling to 0 from frame 3. This was `rebuild_image_bind_groups`
|
|
firing a second time for the same masks/move-offsets buffer-growth
|
|
reason as the item above, confirming the guess recorded here — the two
|
|
were exactly the same root cause measured two different ways. Frame 2
|
|
now reports 0 (see the numbers above); not a separate fix.
|
|
|
|
- [ ] **A read-only text display has no widget of its own — P0's bench
|
|
report area is a `TextEdit` standing in for one (2026-09-05).** The only
|
|
way to get selectable text on screen today is `.editable(...)` plus
|
|
`.attr::<Selectable>(())` (`Selectable` is only implemented for
|
|
`TextEdit`, `iris/src/attr.rs`), which also makes the field focusable —
|
|
tapping the bench report opens the soft keyboard over text nothing lets
|
|
you type into. Harmless for a bench-only debug screen (not fixed this
|
|
pass), but a real "selectable, not editable" text primitive would
|
|
remove the keyboard side effect and is worth having before another
|
|
screen wants the same thing (P1's own transcript rows already read
|
|
their content from a `TextEdit` for the same reason).
|
|
|
|
## From the phone, 2026-09-06
|
|
|
|
Found on Iris's own phone while working RUST.md's P0 box's phone-report
|
|
follow-ups. Recorded here rather than fixed in that pass, so a follow-up
|
|
agent takes them without colliding with that pass's `bench_client.rs`/
|
|
`android/view.rs`/`android/sense.rs` changes.
|
|
|
|
- [x] **Swiping has no momentum, fixed 2026-09-06.** `List::fling`/
|
|
`VelocityTracker`/`FlingCalculator` (`iris/src/widget/list.rs`,
|
|
`iris/src/sense.rs`) -- IRIS.md's 2026-09-06 entry has the full account.
|
|
Wired through `Selection::drag`'s release path, cancelled by the next
|
|
touch-down, clamped at the loaded content's start/end. Verified by unit
|
|
test (fling distance against the closed-form spline result, cancel-on-
|
|
touch, the clamp), not yet by an on-device or emulator feel-check --
|
|
that is still open.
|
|
- [x] **Scrolling down sometimes jitters the text, fixed 2026-09-06.**
|
|
Root-caused by reading `DragArbiter::update`'s `Undecided`-to-`Panning`
|
|
transition rather than by an on-device trace (no emulator was used this
|
|
pass): it was the first named suspect, not the second. `self.last` stays
|
|
at the press origin for every `Undecided` frame (nothing pans while the
|
|
gesture might still be a selection), so the frame that finally crosses
|
|
`DRAG_SLOP` returned `Pan(dy)` with `dy` measured from `press_start` --
|
|
the *whole* pre-threshold drag, applied to the list in one step, however
|
|
many frames it had taken to get there. Fixed by applying only the
|
|
excess past `DRAG_SLOP` on that one frame (`dy - DRAG_SLOP.copysign
|
|
(dy)`), the same "consume the slop, don't replay it" rule Android's own
|
|
touch handling follows. New regression test,
|
|
`crossing_the_slop_by_a_little_pans_by_a_little` (`iris/src/sense.rs`).
|
|
**Not yet done**: an emulator trace of the real per-frame offset
|
|
confirming this was the whole story on real touch input rather than
|
|
only the arbiter's own unit tests -- worth a follow-up pass before
|
|
calling it fully closed.
|
|
- [x] **Composing text held back until a space, caret not moving, fixed
|
|
2026-09-06.** `InputMethodManager.updateSelection` was never called --
|
|
see IRIS.md's 2026-09-06 entry and RUST.md's P0 box, item 1, for the
|
|
full account and the emulator evidence.
|
|
- [x] **Swipe over the composer summons the keyboard, fixed 2026-09-06.**
|
|
`Selector`/`Selectable` now wait for a completed tap -- see IRIS.md's
|
|
2026-09-06 entry and RUST.md's P0 box, item 5. Verified via `dumpsys
|
|
input_method`'s `mInputShown` on the emulator, not yet on the phone.
|
|
- [x] **Text disappears again after leaving and returning to the app,
|
|
fixed 2026-09-06.** `GlyphAtlas::clear`/`Textures::reset` on a
|
|
genuinely new renderer -- see IRIS.md's 2026-09-06 entry and RUST.md's
|
|
P0 box, item 4. Verified on the emulator (home, reopen, screenshot);
|
|
not yet on the phone.
|
|
- [x] **Composed/typed text never becomes visible at all -- root-caused
|
|
and fixed 2026-09-06.** Not the renderer at all: **the composer's buffer
|
|
was empty the whole time.** `TextEditCtx::select` (`iris/src/widget/
|
|
text/edit.rs`) compared the tap against the *laid-out text's* box and
|
|
set `selection = None` for anything outside it -- and an empty field's
|
|
layout is a zero-width box, so tapping an empty composer granted focus
|
|
and opened the keyboard while leaving no caret; `insert_str` returns
|
|
early with no caret, so every keystroke after that was dropped in
|
|
silence. Gboard's suggestion strip is its own composing state, not a
|
|
read of our buffer, which is what made the earlier pass conclude the
|
|
buffer held the text. Fixed by letting parley clamp a tap outside the
|
|
layout to the nearest cursor position (a press that reaches `select`
|
|
has already been hit-tested to the widget, so there is no "outside"),
|
|
plus a `debug_assert!` in `insert_str` so an insert with no caret fails
|
|
at the mistake instead of dropping input -- it immediately caught
|
|
`layout_tests::composing_text_after_a_keyboard_resize_...` typing into
|
|
an unfocused field. Three new tests in `edit.rs`
|
|
(`tapping_an_empty_field_places_a_caret_so_typing_lands`,
|
|
`tapping_past_the_end_of_the_text_clamps_to_the_end`,
|
|
`dragging_without_a_previous_selection_selects_nothing`); the first
|
|
fails on the pre-fix code. Emulator evidence: `adb shell input text`
|
|
after `tap 'Message'` now shows the text in the bar
|
|
(`/tmp/final-typing.png`) and logs `iris text render: chars=5 ...
|
|
glyphs=5`, against `glyphs=0` on every keystroke before.
|
|
|
|
**The old, superseded diagnosis, kept because it was wrong in an
|
|
instructive way:** The composer bar stays empty even once the
|
|
buffer genuinely holds the typed text (confirmed indirectly: Gboard's
|
|
own suggestion strip reacts correctly to each keystroke). A new unit
|
|
test proves the widget tree's own layout math resolves the field's
|
|
region correctly across a keyboard resize, so the bug is downstream of
|
|
that -- most likely `UiRenderState::redraw`'s single-widget redraw path,
|
|
or specific to this emulator's forced `force-gles` backend (untested on
|
|
Vulkan or the real phone). RUST.md's P0 box, item 2, has the full
|
|
writeup, what was ruled out, and where to look next. **Also unverified
|
|
because of this**: item 3's composer rebuild (one `Stack`-based widget,
|
|
a capped/scrollable height, bottom padding tied to the IME/nav-bar
|
|
inset) -- structurally in place and unit-tested, but its own visual
|
|
correctness cannot be screenshotted until text actually renders.
|
|
- [x] **The composer has no touch-drag scroll for overflowing text.**
|
|
**Done 2026-09-06.** `field.scrollable().masked()` in
|
|
`transcript-ui/src/composer.rs`: a finger drag inside the bar pans the
|
|
message, the bar stays capped at six lines, and a vertical drag in the
|
|
focused field no longer extends a selection (Android `EditText`'s own
|
|
behaviour). Verified on this checkout's emulator with the
|
|
`transcript-screen bench force-gles` debug build -- six repetitions of a
|
|
13-word sentence typed in, then
|
|
`ui-trace record --do "swipe 540 1200 540 1460 300"`: the field's
|
|
`Message` box moved `31,1041..1048,1509` -> `31,1131..1048,1651` (the
|
|
content panned down with the finger) with its **height unchanged at
|
|
468px** (the bar did not grow), and the two screenshots either side show
|
|
different text in the same band.
|
|
Three real defects had to be fixed first, each with a headless
|
|
regression test in `iris/src/layout_tests.rs` and each confirmed to fail
|
|
without its fix (docs/RUST.md's plan box has the measurements):
|
|
a `MaxSize` reporting its cap as an unresolved `dp` (`Len::fold_dp`), a
|
|
`Masked` allocating a fresh mask slot per draw (`ActiveData::own_mask`),
|
|
and a panned widget's own hit box moving twice (`move_applied`).
|
|
`Scroll` itself turned out to measure the right number by a misleading
|
|
route -- it is written against `painter.px_size()` now, and the claim
|
|
below that it "measures against the window" was wrong.
|
|
**The grey background was not missing** -- that note (written here on
|
|
2026-09-06 and repeated as still open) is withdrawn. Re-measured the
|
|
same day on the same AVD by decoding the screencap rather than reading
|
|
it: the bar is `rgb(41,40,49)`, the declared `UiColor::new(40, 40, 46)`
|
|
after sRGB rounding, **full width and y2245..y2365** on 1080x2424, with
|
|
the field at `31,2277..1048,2329` and the 63px nav strip below it. It
|
|
is dark by design and sits on black, which is very likely what the
|
|
earlier reading was: at a glance the band and the background are hard
|
|
to tell apart. If it should read as a bar rather than as a slightly
|
|
different black, the colour is the thing to change, not the tree.
|
|
|
|
## From the phone, 2026-09-06, 11:39 (build delivered 02:07, commit 543f6d9)
|
|
|
|
Iris's report on the build with the composing-text, tap-vs-swipe and
|
|
atlas-reset fixes, with a screenshot, verbatim. Each is open until an
|
|
agent ticks it here with the evidence.
|
|
|
|
- [x] **"The app definitely does not start with keyboard spacing
|
|
correct. This is how it looks without me doing anything initially."**
|
|
**Not an inset bug at all -- fixed 2026-09-06.** The black third is the
|
|
bench shell's own empty *benchmark report* pane: `bench_client.rs`'s
|
|
root tree gave it `.height(rest(1))` beside `content.height(rest(2))`,
|
|
so an empty `TextEdit` reserved a third of the window at every launch
|
|
and pushed the composer up by exactly that. Measured on this checkout's
|
|
emulator at the phone's own size (1080x2424, density 420, gesture nav),
|
|
which reproduced Iris's screenshot exactly: new `iris insets:` log line
|
|
reported `bottom=63 ime_bottom=0` at launch (a nav bar, no keyboard --
|
|
so the inset the composer was fed was never large), while `ui-trace
|
|
show -m Message --field box` put the field at `31,1488..1048,1540` on a
|
|
2282px-tall surface, 789px clear of the bottom -- that pane's third.
|
|
**Unit mixing checked explicitly and cleared**: `set_bottom_inset` takes
|
|
physical px and stores `Len::abs`, `MainActivity.java`'s `1`/`0`
|
|
`ime_bottom` only ever reaches `insets.bottom.max(ime_bottom)` and
|
|
`> 0.0`, and every `dp` in the composer resolves at layout time. Fix:
|
|
the report pane is sized to its content (`.max_height(dp(260))
|
|
.scrollable()`), and moved above the transcript so it cannot eat the
|
|
composer's nav-bar clearance. After: field box `31,2277..1048,2329`,
|
|
grey bar ending at device y2361 with the 63px nav strip below it
|
|
(`/tmp/fix1.png` this pass).
|
|
The screenshot shows the composer bar (the grey band) sitting about
|
|
two thirds of the way down a 704x1568 screen, with black below it to
|
|
the bottom, and the transcript ending at "Claude / Results" just above
|
|
it -- at launch, no keyboard. So the composer's bottom padding, which
|
|
the 2026-09-06 rebuild tied to the IME/nav-bar inset, is being fed a
|
|
large value at start on the phone. Suspects, in order: the initial
|
|
inset delivery on the phone (GrapheneOS, gesture navigation) versus
|
|
the emulator; `ime_bottom` now carrying a `1`/`0` boolean through a
|
|
field the composer may still read as pixels or dp; a stale value from
|
|
before the first `on_insets_changed`. Reproduce with the phone's
|
|
screen size and density on the emulator before guessing.
|
|
- [~] **"Swiping still gets caught by the grey bar but keeps working
|
|
after I go past it."** Improved 2026-09-06 by the focused-field rule
|
|
below, still needs her phone to close. `attr.rs`'s `on_press` treated an
|
|
already-focused composer as the plain drag-to-select case, so a swipe
|
|
starting inside it dragged a highlight through the typed text for the
|
|
whole gesture; it now abandons that the moment the press passes
|
|
`DRAG_SLOP` vertically (Android `EditText`'s own rule), which removes one
|
|
of the two things that made the bar feel like it caught the swipe. The
|
|
residual `DRAG_SLOP` measured from the boundary crossing, described
|
|
below, is unchanged. Original note follows.
|
|
Not closeable from the emulator, annotated
|
|
2026-09-06 after the `DragGesture` merge. `attr.rs`'s `on_press` never
|
|
calls `capture_pointer` and never consumes a `Pressing` frame past
|
|
`DRAG_SLOP` (it just stops watching), so once the finger's *current*
|
|
position leaves the composer's box and enters the list's, `List`
|
|
starts receiving ordinary hit-tested `Pressing` frames there --
|
|
`DragArbiter::is_idle()`'s 2026-09-05 recovery (a missed `PressStart`)
|
|
picks it up rather than leaving it stuck. What this does **not** do is
|
|
what "wherever it began" implies literally: `DragArbiter::press_start`
|
|
restarts from the *boundary-crossing* position, not from the original
|
|
touch-down inside the composer, so the pan still needs a fresh
|
|
`DRAG_SLOP` of travel measured from the boundary rather than from the
|
|
start of the gesture -- composer and list are adjacent, non-overlapping
|
|
widgets (`lib.rs`'s `(list, composer_bar).span(Dir::DOWN)`), and only
|
|
the composer forwarding its own drag to the list would remove that
|
|
residual slop entirely, which is more than this pass's merge changes.
|
|
RUST.md's merge-pass box has the reasoning in full and an emulator
|
|
swipe confirming the composer's own box never moves/resizes during it;
|
|
whether the residual slop is still perceptible as "caught" needs Iris's
|
|
phone, since the emulator's per-widget boundary is a few dp wide and
|
|
easy to cross without noticing on a real screen too.
|
|
- [ ] **"Flinging still does not work."** No longer expected to reproduce
|
|
after the `DragGesture` merge (`e12c708`, pointer capture +
|
|
`CursorSense::Drop`), 2026-09-06. Emulator evidence (RUST.md's
|
|
merge-pass box, check (b)): a real `ui-trace` finger swipe followed by
|
|
screenshot-hash sampling caught a post-release frame distinct from the
|
|
drag's own last frame in one run, and every run showed 28-32
|
|
`render()` frames per gesture against an idle baseline of 0 and ~8
|
|
expected from the drag alone -- redraw kept being requested well past
|
|
the finger lifting, which only happens while a fling is still
|
|
animating. Left unticked in spirit until Iris's phone confirms it,
|
|
since only she can say whether it *feels* like a fling now; the
|
|
emulator's screenshot timing could not always catch the tail of a
|
|
fast-settling one visually (same caveat noted in RUST.md).
|
|
- [~] **"Text still disappears if I leave and come back to the app."**
|
|
**Instrumented 2026-09-06 so the phone can answer it**, since no
|
|
emulator here has a Vulkan adapter. `iris/src/android/view.rs` now logs
|
|
one `log::info!` line per surface event with the glyph/atlas counts:
|
|
`iris surface: surface_destroyed, tearing the renderer down
|
|
(glyphs_cached=387 atlas_pages=1)`, `iris surface: surface_changed
|
|
1080x2424 already_live=false glyphs_cached=387 atlas_pages=1`, `iris
|
|
surface: new renderer built (Gl), clearing glyph atlas: glyphs=387
|
|
pages=1`, plus `iris insets: ... window=(1080, 2424)` on every insets
|
|
change. That is the emulator's own healthy app-switch cycle, verified
|
|
this pass (home, reopen, screenshot: all text intact,
|
|
`/tmp/appswitch.png`). **The one line to look for on the phone is
|
|
`already_live=`**: `true` on the return from backgrounding would mean
|
|
the surface came back *without* a `surface_destroyed`, so
|
|
`surface_changed` reconfigured a renderer whose Vulkan swapchain and
|
|
atlas textures belong to a window that is gone -- the reuse branch
|
|
never clears the atlas, by design. `false` with no `new renderer built`
|
|
line after it would mean the renderer failed to rebuild. Either answer
|
|
names the fix; guessing between them from here does not.
|
|
The `GlyphAtlas::clear`/`Textures::reset` fix was verified on the
|
|
emulator under `force-gles` only; the phone runs Vulkan. So either the
|
|
reset is not reached on the phone's path (a different surface-
|
|
lifecycle sequence -- `surface_destroyed`/`surface_created` ordering,
|
|
or the renderer not being rebuilt but its textures lost), or the CPU
|
|
glyph cache and the GPU atlas still disagree after it. Needs logging
|
|
of the renderer lifecycle on the phone build, readable from `adb
|
|
logcat` when Iris next runs it, since no emulator here has a Vulkan
|
|
adapter under host GPU.
|
|
|
|
## From the phone, 2026-09-06, 22:16 (build from 20303e0, delivered via ai-app-bench 95e25fe)
|
|
|
|
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."** -> 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
|
|
(`getHistoricalX/Y/EventTime`), and can be DOWN, one or two MOVEs, UP
|
|
inside `DRAG_SLOP`'s worth of frames; a `ui-trace` swipe is many
|
|
evenly-spaced MOVEs. Suspects, in order: `android/sense.rs` reading
|
|
only each event's final position (the velocity tracker sees two
|
|
samples, or one); the release path starting a fling only from a
|
|
gesture already in `Panning`, so a flick that crosses the slop on its
|
|
last sample is treated as a tap; `ACTION_CANCEL`/pointer-capture
|
|
delivering no `Drop`. Log the release decision (samples, span,
|
|
velocity, outcome) at `info` so the next logcat settles it.
|
|
- [x] **"I can't reopen keyboard by tapping on message box after it
|
|
already happened once."** *(Fixed 2026-09-07: `attr.rs`'s already-
|
|
focused branch calls `focus_gained` on a tap inside `DRAG_SLOP`.
|
|
Emulator: first tap `mInputShown=true`, back gesture, second tap
|
|
`mInputShown=true`. Negative control with that one call removed leaves
|
|
the second tap at `false`; a horizontal and a vertical swipe over the
|
|
focused field both leave it at `false`, so the earlier "swiping over
|
|
the input bar brings up the keyboard" has not returned.)* The field stays focused after the keyboard
|
|
is dismissed (back gesture, or the IME's own hide), so `on_press`'s
|
|
already-focused branch never requests the IME again. Android's
|
|
`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).
|
|
- [ ] **"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
|
|
2026-09-06 "(b)" fix), so nothing has the inset's *height* to pad the
|
|
transcript and composer with. Pass both: `isVisible(ime())` and
|
|
`getInsets(ime()).bottom` in px; the list's bottom padding and the
|
|
composer's position follow the height, the visibility drives the
|
|
boolean the `imePadding` rule in AGENTS.md's "Things that have bitten"
|
|
describes.
|
|
- [x] **"Picture is what happens if I leave the app and come back,
|
|
which completely removes text, and then I tap on the debug info. The
|
|
textures are definitely getting cooked for some reason after leaving
|
|
the app and resuming."** Screenshot: every glyph drawn *before* the
|
|
resume is fragments; the diagnostics text drawn *after* is perfect;
|
|
the report says `atlas format: Rgba8Unorm, views live: 0`. Reading:
|
|
`Textures::reset`/`GlyphAtlas::clear` on the new renderer emptied the
|
|
GPU atlas, but the per-widget cached text primitives (`TextView`'s
|
|
render cache -- the one `c3cfc67`'s shape counter is keyed on) still
|
|
carry the old atlas coordinates and are re-submitted as-is; only
|
|
widgets drawn fresh after the resume shape and upload again. Fix: a
|
|
renderer rebuild invalidates every cached text render (one
|
|
generation counter on the atlas, checked at `TextView::render`, or
|
|
a full-tree redraw with caches dropped), with a `debug_assert!` that
|
|
no submitted glyph quad references an atlas generation older than the
|
|
live one. Reproducible on the emulator by forcing a renderer rebuild
|
|
(home + return, or `surface_destroyed`/`surface_created`) on a screen
|
|
with text already drawn -- the earlier "verified" home/reopen check
|
|
screenshotted the emulator's GLES path, where a resume may not
|
|
destroy the surface at all.
|
|
|
|
**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`
|
|
(`iris/src/android/view.rs`) *does* already force a full-tree redraw
|
|
after a rebuild: it calls `render.resize(...)` unconditionally, which
|
|
sets `UiRenderState::resized`, which makes the next `update` take
|
|
`redraw_all` rather than `redraw_updates`. So every widget's `draw`
|
|
really does run again after the resume. What survives it is one cache
|
|
further in: `TextView::render` (`iris/src/widget/text/mod.rs`) returns
|
|
its cached `RenderedText` whenever the wrap width, buffer and attrs are
|
|
unchanged -- true of every pre-resume row -- so `TextData::place` is
|
|
never reached, nothing is re-rasterised into the fresh atlas, and the
|
|
*old* atlas's `uv_min`/`uv_max`/`layer` are re-submitted verbatim. Only
|
|
text whose content changed after the resume (the diagnostics pane Iris
|
|
tapped) re-shapes, which is exactly the split in her screenshot.
|
|
`Painter::glyphs` has one call site in the whole workspace, that one,
|
|
so there is no second holder of a `RenderedText` to fix.
|
|
|
|
The fix, in `ba2afba`: `GlyphAtlas::generation`, bumped by
|
|
`GlyphAtlas::clear`; `RenderedText::generation` recording which atlas
|
|
its glyphs were placed against; `Painter::atlas_generation()`;
|
|
`TextView::render`'s cache key gains it; and a `debug_assert_eq!` in
|
|
`Painter::glyphs` that a submitted quad's generation is the live one.
|
|
Headless test
|
|
`clearing_the_atlas_re_renders_cached_text_instead_of_reusing_it`
|
|
(`iris/src/widget/text/mod.rs`): draw, `atlas.clear()`, `resize`, draw
|
|
again, and assert the atlas holds the same glyph count again -- it
|
|
stays at 0 without the fix, because the cache short-circuits before
|
|
`place`.
|
|
|
|
## Build
|
|
|
|
- [x] **Benchmarks**, not unit tests, run on demand (2026-09-05; a
|
|
`benches/` or a script under `iris/`, never in `cargo test`). The
|
|
scenario that matters most is a **message list** — chat apps and this
|
|
app's transcript alike — stressed with many messages and many images.
|
|
One case in particular: **resizing an input box** (typing enough text to
|
|
grow it) that pushes a long list of messages above it must stay very
|
|
fast and recalculate almost nothing — a move of everything above, not a
|
|
re-layout. That is exactly the O(1) move chain in LAYOUT.md; the
|
|
benchmark is what proves it. Done when the numbers are in this file with
|
|
the command, and the input-box case reports draws re-run, not just frame
|
|
time.
|
|
|
|
**Built as two rigs**, chosen per scenario by whether a real `wgpu`
|
|
device is needed (`UiRenderState`/`Widgets` touch no GPU or window, so
|
|
most of this runs as an ordinary binary — the same property
|
|
`layout_tests.rs` relies on):
|
|
|
|
- `iris/benches/message_list.rs` — a plain `Instant`-timed binary
|
|
(`[[bench]] harness = false` in `iris/Cargo.toml`), not criterion: see
|
|
the file's own header for why (short version — every scenario here
|
|
reduces to a *count* `UiRenderState::take_counters` already produces,
|
|
which criterion's statistical machinery adds nothing to and which a
|
|
new dependency is not worth pulling in for). Covers (a) first-frame
|
|
cost of a message list of N wrapped-text rows (one in 20 also carrying
|
|
a small in-memory image) for N = 100/1,000/10,000; (b) per-frame cost
|
|
of scrolling that list, 200 ticks; (c) the input-box case — a
|
|
fixed-height field at the bottom of the screen growing by a line 40
|
|
times, with the message list above it filling the rest of the screen.
|
|
Run: `cd iris && cargo bench --bench message_list` (always release —
|
|
`cargo bench` builds the `bench` profile, which is optimized).
|
|
- `iris/examples/bench_images.rs` — needs a real device, so it runs
|
|
through `iris/run-headless.sh bench_images`, printing
|
|
`UiRenderNode::take_image_bind_group_creates()` (a new counter, added
|
|
in `core/src/render/texture.rs` and `core/src/render/mod.rs`,
|
|
mirroring `UiRenderState::take_counters`) each frame. Covers (d): 1,000
|
|
image rows, checked both cold (does bind-group creation reach zero
|
|
once loaded) and after appending one more image once settled (does
|
|
*that* stay cheap) — the second question is what actually matters for
|
|
a live transcript and is what turned up the two Fix items above.
|
|
- `iris/run-bench.sh [list|images]` runs either or both and is what to
|
|
run before/after touching `Scroll`, `Span`, `Sized`, the move-offset
|
|
chain, or `GpuTextures`.
|
|
|
|
**Numbers (2026-09-05, release, `cargo bench`/`run-headless.sh`, this
|
|
VM: AMD Ryzen 7 3800X, 8 cores, rustc 1.98.0 nightly-2026-09-03):**
|
|
|
|
cd iris && cargo bench --bench message_list
|
|
(a) first frame, N=100: 30.30ms draws=227 rewrites=15 moves=0
|
|
(a) first frame, N=1000: 186.04ms draws=2252 rewrites=150 moves=0
|
|
(a) first frame, N=10000:1770.36ms draws=22502 rewrites=1500 moves=0
|
|
(b) scroll, N=100/1000/10000, 200 ticks each:
|
|
draws=200 rewrites=0 moves=200 (identical at every N)
|
|
per-tick average: 0.0002ms (identical at every N)
|
|
(c) input grows 40 lines, N=100/1000/10000 rows above it:
|
|
draws=320 rewrites=40 moves=160 (identical at every N)
|
|
per-line average: 0.0012-0.0013ms (identical at every N)
|
|
|
|
cd iris && ./run-bench.sh images (2026-09-05, before the fix)
|
|
frame=1 bind_group_creates=1000 (cold load)
|
|
frame=2 bind_group_creates=1000 (see Fix item above)
|
|
frame=3 bind_group_creates=0
|
|
frame=4 bind_group_creates=0
|
|
(append one image here)
|
|
frame=5 bind_group_creates=1001 (see Fix item above)
|
|
frame=6 bind_group_creates=0
|
|
|
|
cd iris && ./run-bench.sh images (2026-09-05, after the fix)
|
|
frame=1 bind_group_creates=1000 (cold load, unchanged -- genuine work)
|
|
frame=2 bind_group_creates=0
|
|
frame=3 bind_group_creates=0
|
|
frame=4 bind_group_creates=0
|
|
(append one image here)
|
|
frame=5 bind_group_creates=1 (one image's own create_image, O(1))
|
|
frame=6 bind_group_creates=0
|
|
|
|
**Reading it**: (a) is real, necessary work — shaping and laying out N
|
|
never-before-seen text rows — and scales with N as it must, ~10x cost
|
|
per 10x N. (b) and (c) are the pass conditions that matter: both are
|
|
**exactly flat across N = 100 to 10,000**, confirming LAYOUT.md's O(1)
|
|
move chain holds for both scrolling and for a growing input box pushing
|
|
the message list — draws/moves per tick or per line do not grow with
|
|
list size, and the per-operation cost (a fraction of a microsecond) is
|
|
nowhere near a frame budget. (d)'s cold-load and steady-state halves
|
|
behave as designed; its *append* half did not, until the fix above moved
|
|
masks/move_offsets out of the per-image bind group — now flat at O(1)
|
|
the same way (b) and (c) are.
|
|
|
|
- **I5's transcript screen (`iris/transcript-ui/`, 2026-09-05) — what it
|
|
left, each recorded at the point in the code it would go rather than
|
|
silently dropped. See RUST.md's I5 box for the full account of what
|
|
*was* built (the screen, `SpanStyle`, cross-row selection, the growing
|
|
composer).**
|
|
- [x] **Android integration for this screen — done, 2026-09-05.**
|
|
`iris-android-app`'s `transcript-screen` Cargo feature
|
|
(`transcript_client.rs`) runs this screen against a real `ai-server`
|
|
through `client-core`, confirmed on-device: real scrolling, real
|
|
touch-drag panning, tap-by-name on the composer. Two real bugs found
|
|
and fixed along the way (a missing `INTERNET` permission; a
|
|
background-thread redraw request that crashed via a `Looper`
|
|
requirement, fixed by routing through `View::post_delayed` — see
|
|
`IRIS.md`'s `Tasks::redraw_handle` entry). See RUST.md's I5 box,
|
|
"The Android integration, done 2026-09-05" for the full account.
|
|
- [x] **A render-time number for iris, comparable to Compose's
|
|
`transcript-bench.sh` report — instrumentation done and a real number
|
|
obtained, 2026-09-05 (later the same day); the clean comparable loop
|
|
is not.** `iris_core::FrameReport` (`iris/core/src/render/
|
|
frame_report.rs`, `IRIS.md`'s new entry) times every frame from
|
|
`render()`'s redraw start to after `queue.submit`+`present()`, exposed
|
|
as two named on-screen controls ("Frame report", "Reset frame
|
|
report"). Driven against a real on-device touch-drag it read
|
|
`frames=34 janky%=61.76 p50=26.5ms p90=48.0ms p99=98.1ms
|
|
worst=98.1ms` — real, not inferred, but accumulated across several
|
|
gestures rather than one clean 24-swipe loop, because of the new
|
|
finding below. See RUST.md's I5 box, "Update, 2026-09-05, later the
|
|
same day" for the full account.
|
|
- [ ] **New, 2026-09-05: intermittent touch delivery to iris's
|
|
`SurfaceView` under this checkout's `EMU_GPU=software` emulator.**
|
|
The same swipe coordinates, confirmed (by scanning a screenshot
|
|
column for the first non-black pixel) to sit over real row text,
|
|
sometimes produced 30+ real frames and a screenshot diff and
|
|
sometimes produced zero of either, across otherwise-identical
|
|
`ui-trace` invocations. Not the already-understood "already at that
|
|
scroll edge" case (reproduced with content confirmed taller than the
|
|
viewport, in both directions). Leading candidate, not yet confirmed:
|
|
this checkout's emulator was independently observed at ~78% of one
|
|
CPU core, continuously, while idle on-screen — SwiftShader's software
|
|
rasterisation is CPU-bound by design, and a synthetic touch competing
|
|
with that load for delivery is plausible but unmeasured *during* a
|
|
failing gesture (the standing rule against diagnosing from
|
|
after-the-fact measurements applies here). Needs a sampler (load,
|
|
`dumpsys input`, a `-i 0` `ui-trace` capture) running while a failing
|
|
gesture is driven, and ideally a comparison under `-gpu host` (real
|
|
Vulkan) to see whether it is specific to software rendering. This is
|
|
what blocks the clean, comparable 24-swipe loop above.
|
|
- [x] **Long-press-then-drag-to-select — confirmed on-device, 2026-09-05
|
|
(later the same day).** `ui-trace` gained a `holddrag X1 Y1 X2 Y2
|
|
HOLD_MS MOVE_MS` action (`emulator-tools`, additive, extends the same
|
|
`MotionEvent`/`injectInputEvent` mechanism `swipe` already used):
|
|
press, hold past `LONG_PRESS`, move, release, as one continuous touch.
|
|
Driven against a real row (`holddrag 300 1850 300 2050 600 300`) it
|
|
produced `iris selection: begin at row ...` then a sequence of
|
|
`iris selection: extend to row ...` log lines
|
|
(`transcript-ui/src/selection.rs`, a new small `log` dependency since
|
|
selection has no accessibility label of its own yet — see the next
|
|
item), and a screenshot taken right after shows the expected
|
|
highlighted selection spanning multiple rows. `DragArbiter`'s own
|
|
unit tests already covered this sequence against a synthetic clock;
|
|
this is the first time it has been driven by a real device touch.
|
|
- [x] **Touch-drag panning over a row's own rendered text — done,
|
|
2026-09-05.** `row.rs` used to register `CursorSense::click_or_drag()`
|
|
on each row's `TextEdit` for cross-row selection; `TextEdit::draw`'s
|
|
`painter.child_layer()` (`iris/src/widget/text/edit.rs:87`) meant that
|
|
registration won `core/src/sense.rs::run_sensors`'s per-layer
|
|
arbitration on every frame it was pressed, not just the frame the
|
|
press started, so a list pan gesture registered on `List` itself never
|
|
got a turn while a row was under the finger. Fixed with
|
|
`iris::sense::DragArbiter` (recorded in `IRIS.md`), one small state
|
|
machine per list deciding pan vs. select the way Android does (a
|
|
vertical drag pans immediately; a stationary press held `LONG_PRESS`
|
|
(500ms) starts a selection which further drag extends; a horizontal
|
|
drag while something is already selected extends immediately) —
|
|
`transcript-ui/src/selection.rs`'s `Selection::drag` is the one place
|
|
every row's drag now routes through. 8 new unit tests
|
|
(`iris/src/sense.rs`'s `drag_arbiter_tests`); `cargo fmt/clippy/test
|
|
--workspace` and `cargo ndk` (both `iris` and `transcript-ui`) all
|
|
clean; `run-headless.sh` screenshot byte-identical to before the
|
|
change (38578 bytes). See RUST.md's I5 box, "Gap closed, 2026-09-05".
|
|
- [x] **Intermittent touch-scroll dropout — root-caused and fixed,
|
|
2026-09-05.** Not the coalesced-`ACTION_MOVE` hypothesis the earlier
|
|
pass suspected (ruled out): a gesture's `ACTION_DOWN` can land on a
|
|
row's own padding/gap or its header, which no `CursorSense` covers,
|
|
so `DragArbiter` never gets `press_start` and sits in `Idle`
|
|
(answers `Undecided` forever) for that whole gesture. Fixed via a new
|
|
`DragArbiter::is_idle()` that `Selection::drag`
|
|
(`transcript-ui/src/selection.rs`) checks to recover a missed press
|
|
on the next `Pressing` frame. Four new unit tests. See RUST.md's I5
|
|
box, "Touch-scroll dropout root-caused, 2026-09-05", for the trace and
|
|
what a peer session sharing this checkout's emulator mid-pass
|
|
prevented from being re-verified end-to-end (the aggregate
|
|
`iris-scroll.sh` three-run confirmation and a re-taken FrameReport
|
|
row) — a future pass should finish that once the emulator is free.
|
|
- [ ] **Row-level accessibility names.** The composer carries
|
|
`.label("Message")`; transcript rows do not carry a `.label()` of
|
|
their own yet, so `Widgets::named()` (I4) does not include them —
|
|
`row.rs`'s `build_text_row` is where one would go, keyed to something
|
|
stable per row (its sender + a short excerpt, matching what a screen
|
|
reader announcing a chat message would say).
|
|
- [x] **A tappable link** — done 2026-09-06 (P1a). `TextEditCtx::
|
|
byte_at(pos, size)` answers which byte a tap landed on without
|
|
handing out the parley layout, `GestureOutcome::Tapped` says the
|
|
press committed to neither a pan nor a selection, and
|
|
`iris::platform::OpenUrl` is the capability each backend implements
|
|
(`xdg-open`/`open`/`start`; an `ACTION_VIEW` intent on Android,
|
|
deferred to `after_input` the way `pending_show_keyboard` is).
|
|
- [ ] **A background chip behind inline code.** Still needs per-range
|
|
glyph *geometry* — a run's boxes, not one offset — which
|
|
`TextEditCtx` does not expose outside `iris::widget::text`
|
|
(`edit.rs`'s `layout()` helper is private). The same primitive
|
|
`TextEdit::draw`'s own selection highlight uses internally,
|
|
`iris/src/widget/text/edit.rs:99`. `byte_at` above deliberately did
|
|
not open that up: a tap needs one offset and a chip needs the run.
|
|
- [ ] **`Selection`'s anchor-row shortcut.** The row a drag started in
|
|
is selected in full (`select_all`) the moment the drag leaves it,
|
|
rather than "from the click point to whichever edge points away from
|
|
the drag" — needs the same private `layout()` access as the item
|
|
above. `selection.rs`'s module doc has the exact reasoning.
|
|
- [x] **Syntax highlighting inside a fenced code block** — done
|
|
2026-09-06 (P1a). `client_core::highlight::spans_of` by language,
|
|
converted from its char indices to `SpanStyle`'s byte offsets, in
|
|
the same Catppuccin palette `Theme.kt` uses. A language the scanner
|
|
has no rules for stays plain rather than being coloured by the
|
|
nearest one's.
|
|
|
|
- [x] **Masks defined relative to each other. (Done: chaining
|
|
2026-09-07 in d507ae4, the multiply 2026-09-08.)** Built exactly
|
|
beside the move chain, as this asked: `Mask::parent` is a slot index
|
|
and the fragment stage walks it under the same bound the move chain
|
|
uses. Each step multiplies the referenced primitive's coverage into
|
|
the pixel's alpha, so a pixel inside two feathered corners is dimmed
|
|
by both — the "multiplies by something *and also* applies mask B" half.
|
|
The real widget that needed it was the transcript's code fence inside
|
|
the list. See docs/LAYOUT.md's "Masks with a shape".
|
|
- [ ] **Positions as a single float per scroll.** Iris raised, and half
|
|
rejected, letting a scroll update one float rather than positions:
|
|
input handling cares about most elements in a list, so absolute
|
|
positions must be computed on the CPU anyway. LAYOUT.md's design
|
|
already lands here (GPU walks the chain, CPU resolves on demand for
|
|
hit tests). Keep the CPU resolution lazy and per query; do not
|
|
materialise every row's absolute position per frame.
|
|
- [ ] **Animations, last.** Cosmetic, so after everything above. Must be
|
|
**modular — a piece of the library rather than a core part forced into
|
|
everything, the same way input is**. Whatever the mechanism, a widget
|
|
that does not animate must pay nothing and import nothing for it.
|
|
|
|
## Found by P1a (2026-09-06)
|
|
|
|
- [x] **`Rect` claimed to be size-independent, and it is not.** A `Rect`
|
|
fills whatever region it is handed, so `draw_inner`'s size-independent
|
|
fast path -- which rewrites primitives with
|
|
`r.outside(&from).within(®ion)` rather than redrawing -- could not
|
|
reproduce its `draw`, and a `.background(rect(..))` kept the size of
|
|
the *provisional* full-region pass `Span` does in phase 1. One fenced
|
|
code block's panel covered every block below it and every row below
|
|
that. Fixed in `iris/src/widget/rect.rs`; the reason is written at the
|
|
definition. Suspect the same cause for anything else tinted with a
|
|
background rect.
|
|
- [x] **A wrapped transcript row tripped `reposition`'s debug assert.**
|
|
Settled 2026-09-06 by giving the move slot one owner instead of two.
|
|
`mov` accumulates a delta on it, `reposition` overwrote it, and both
|
|
legitimately land on one widget in one frame: `List::place`'s
|
|
Bottom-known branch offers a row a same-size box that has *moved*
|
|
(`mov`), then corrects the placement inside it when the row's cached
|
|
height no longer matches what the row reports (`reposition`). The
|
|
slot now always means `move_applied + repositioned`
|
|
(`ActiveData::repositioned`, `iris/core/src/ui/render_state.rs`), so
|
|
`reposition` adds the move rather than dropping it -- the assert is
|
|
gone and the arithmetic is right. Test:
|
|
`a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement`
|
|
in `layout_tests.rs`, which lands the child at the *offered* position
|
|
(-100px) instead of the placement (100px) without the fix, and a
|
|
`debug_assert_eq!` in `reposition` that nothing but those two ever
|
|
writes the slot. Verified with the `.wrap(true)` repro (draws, no
|
|
panic) and an emulator bench run with assertions live.
|
|
- [ ] **Desktop colours are washed out: the winit surface is sRGB and
|
|
the shader writes the palette's bytes as linear.** Mocha Crust
|
|
(17,17,27) is drawn as (73,73,91), measured off
|
|
`run-headless.sh --shot`. Android is correct, so this is the surface
|
|
format rather than the palette -- but it makes the desktop build
|
|
useless as a colour reference, which is exactly what P1a needed it for
|
|
when the emulator could not draw glyphs.
|
|
- [x] **Every glyph was a solid box on the GLES backend -- iris's bug,
|
|
not the emulator's.** Fixed 2026-09-06. The atlas is one
|
|
`texture_2d_array` and `GpuTextures::new` created it with **one
|
|
layer**; wgpu-hal picks the GL target from the descriptor
|
|
(`(false, 1) => TEXTURE_2D`), so under GLES that array was a
|
|
`GL_TEXTURE_2D` bound to the shader's `sampler2DArray`, the unit was
|
|
incomplete, every `textureSample` returned (0,0,0,1), and
|
|
`draw_glyph`'s `color.a *= texel.a` filled the quad. `MIN_ARRAY_LAYERS
|
|
= 2` in `iris/core/src/render/texture.rs`, with a `debug_assert!` at
|
|
`create_array_texture`. Vulkan (the phone, the desktop's default
|
|
backend) was never affected. Reproduce the class in seconds without an
|
|
emulator: `iris`'s `force-gles` feature now switches the **desktop**
|
|
backend too -- `./run-headless.sh transcript --shot /tmp/x.png -- -p
|
|
transcript-ui --features iris/force-gles`.
|
|
|
|
- [ ] **The bench report pane draws over the transcript rows instead of
|
|
replacing them.** Visible on the emulator for the first time now that
|
|
glyphs render there (`/tmp/emu-final.png`, 2026-09-06): after a bench
|
|
run the report's lines and the transcript's occupy the same rows in the
|
|
top third of the screen, both legible, neither on top. Pre-existing --
|
|
the same overlap is in a screenshot taken before the move-slot fix -- so
|
|
it is its own item, most likely the report pane not masking or not
|
|
claiming its region.
|
|
|
|
## Found by P1b (2026-09-06), all with a headless repro
|
|
|
|
Each was found by looking at `iris/run-headless.sh transcript -- -p
|
|
transcript-ui` rather than at a diff, and each is worked around in
|
|
`transcript-ui/src/tool.rs` rather than fixed here. docs/RUST.md's P1b box
|
|
has the fuller account.
|
|
|
|
- [ ] **A `Span` of `Pad`ded children inside another `Span` places those
|
|
children a slot out of step.** Each child drew its content one sibling's
|
|
height below its own box. Repro: `IRIS_TOOLS_EXPANDED=1
|
|
iris/run-headless.sh transcript --shot /tmp/x.png -- -p transcript-ui`
|
|
with `tool.rs`'s group built as `Span(DOWN)[header, Pad(Span(DOWN)
|
|
[cards]), bar]` instead of the single `Span` it uses now. Bisected:
|
|
removing the inner `Span` fixes it, and so does removing the children's
|
|
own `Pad`; the background `Stack`, the `Sized` wrappers and the
|
|
`WidgetPtr` per child make no difference. **Not** the `mov`-vs-
|
|
`reposition` fault f5b8893 fixed -- it survives that commit. The
|
|
workaround costs the group the 4dp inset its Compose counterpart holds
|
|
its cards off the edge by, so this is worth fixing.
|
|
- [x] **`scrollable_on(Axis::X)` on a non-editable `Text` draws nothing.**
|
|
The panel is drawn and the text inside it is not. A markdown fence does
|
|
the same to a `TextEdit` and is fine, so it is the widget kind rather
|
|
than the chain. `tool.rs`'s `raw_block` is `masked()` only until this is
|
|
fixed, which means a long command is clipped rather than pannable.
|
|
**Not reproducible on 2026-09-08**: `raw_block` was changed to
|
|
`.scrollable_on(Axis::X).pad(..).masked_by(..)` and the command draws
|
|
normally (`IRIS_TOOLS_EXPANDED=1 iris/run-headless.sh transcript --shot`,
|
|
the `rm -rf target` card). Something between 09-06 and 09-08 fixed it --
|
|
the shaped-mask work (`.masked_by`, 38bf630) is the likeliest, since the
|
|
old chain was `.masked()` *inside* the padding. Left ticked with the
|
|
original symptom recorded rather than deleted, in case it comes back.
|
|
- [ ] **No overflow ellipsis.** `TextAttrs` can wrap or not wrap; there is
|
|
no "one line, ellipsised" the way `maxLines = 1` + `TextOverflow.
|
|
Ellipsis` gives Compose. A tool card's summary is clipped instead, so
|
|
nothing on screen says it was cut. Whichever end is cut has to be a
|
|
choice when this lands: a path is identified by its tail, a command by
|
|
its head.
|
|
- [x] **A chevron the platform cannot fail to have.** **Done
|
|
2026-09-08**, twice. First as `iris::widget::mark(dir, dp, colour)`,
|
|
which rasterised an antialiased triangle into the ordinary texture path
|
|
-- correct, but one bespoke shape, and it built a texture *per widget*,
|
|
which is what crashed the bench (RUST.md's 2026-09-08 evening entry).
|
|
Then, on Iris's question -- "why does mark exist? The font should be
|
|
working if it's working for compose and nerd fonts are bundled" -- as
|
|
what the Compose app has always done: **iris ships its own Nerd Fonts
|
|
subset** (`iris/core/build-icon-font.sh` -> `iris/core/assets/fonts/
|
|
nerd_icons.ttf`, 992 bytes, three Material Design glyphs), named in
|
|
`iris::icon` and drawn with `Family::Icons`. `mark` is deleted. That
|
|
serves every future icon rather than one triangle, and an icon is text,
|
|
so it takes the size, colour and baseline of the line it sits in for
|
|
free. The original entry, for the record: *the bundled fonts were
|
|
removed on 2026-09-07 in favour of the platform collection, so the mark
|
|
is a codepoint the phone's own faces may not have -- Iris's 2026-09-08
|
|
screenshot shows an empty box where it should be, and the desktop render
|
|
draws it as a small dot. UI_RULES: "don't rely on characters the
|
|
platform might not have."*
|
|
- [ ] **A tool card's text is not selectable.** `Selection` is keyed
|
|
`(RowKey, block index)` and a card has no markdown blocks, so nothing in
|
|
a card registers. Compose's `SelectionContainer` covers tool output,
|
|
which is the text people most want to copy. Needs a key for "the nth
|
|
text of this row" that a card can mint without colliding with a
|
|
message's blocks.
|
|
|
|
## Build (for the port)
|
|
|
|
Widgets `RUST.md`'s "The port, in order (decided 2026-09-05)" needs and
|
|
iris does not have yet, one entry per gap, named against the P-step that
|
|
first needs it. Move an entry up to "Fix" or tick it in place once built;
|
|
do not duplicate it there.
|
|
|
|
- [ ] **A history-paging cushion measured in on-screen viewports, not a
|
|
row count.** (**P1**.) `iris::widget::List` has no equivalent of the
|
|
Compose app's `HISTORY_SCREENS` — AGENTS.md's "Things that have
|
|
bitten" is explicit that a fixed row count under-fills a screen on a
|
|
tool-heavy transcript and over-fills one on a text-heavy one, so
|
|
whatever loads the next page has to ask the list how many viewports
|
|
are actually on screen, not assume a constant.
|
|
- [ ] **A scaled thumbnail/image widget for an in-transcript image.**
|
|
(**P1**.) `SessionImage.kt`'s bitmap decode-and-downscale has no iris
|
|
counterpart; iris's own image widget (used by `bench_images.rs`) draws
|
|
a loaded texture but does nothing about sourcing or scaling one from a
|
|
server-produced attachment.
|
|
- [ ] **A modal/dialog primitive.** (**P1**, reused by **P3** and
|
|
**P5**.) Needed for the session settings dialog, `UsageDialog`'s
|
|
equivalent, and the delete-with-`deleteForeign` confirmation with its
|
|
toggle switch. Build once, wherever it is first needed, rather than
|
|
once per screen that wants one.
|
|
- [ ] **A horizontal gauge/bar widget.** (**P1**.) For
|
|
`SessionUsageBar`'s equivalent — a bounded fill reflecting a fraction,
|
|
nothing fancier.
|
|
- [ ] **A `BusyItem` equivalent: a dimmed row carrying an operation
|
|
label that does not block its list's own scroll/drag.** (**P3**.) The
|
|
Compose version tried an overlay first and it swallowed the drag along
|
|
with the tap (AGENTS.md's "Shared appearance") — worth not repeating
|
|
that attempt in iris before building the row-level version directly.
|
|
- [ ] **A toggle switch.** (**P3**.) For the delete dialog's
|
|
`deleteForeign` control; iris has no switch/checkbox widget yet as far
|
|
as this pass found.
|
|
|
|
## Reconsider
|
|
|
|
- [ ] **`WidgetView`.** Iris is unsure of it: what she wants is an easy way
|
|
to compose a widget from others (a button is the main case). With
|
|
sizing folded into `draw`, composing may be easy enough that `View` is
|
|
redundant. Decide after the layout change lands, by writing a button
|
|
both ways and keeping the one that is shorter to explain; delete the
|
|
other rather than keeping two ways.
|
|
|
|
## Build (asked for by Iris, 2026-09-06): a density-independent length unit
|
|
|
|
- [x] **A third length kind beside relative and pixels, so display scales
|
|
"just work".** Done 2026-09-06 — `Len::dp`/`len_fns::dp`, resolved
|
|
against `UiRenderState`/`Painter::density()` at `apply_rest` time; text
|
|
additionally rasterises at the resolved (physical) size instead of
|
|
scaling a low-resolution bitmap afterward, which was making text blurry.
|
|
`Span::gap`/`Padding` moved from `f32` to `Len` so they take `dp(...)`
|
|
too; transcript-ui's row/composer padding and one example migrated.
|
|
`em` was not added — nothing in this pass needed a text-relative unit,
|
|
and `dp`'s own doc says why it and physical pixels are kept as separate
|
|
fields rather than one the caller pre-multiplies. Not yet verified on
|
|
Iris's own phone at two densities (this pass had no device) — see
|
|
docs/RUST.md's P0 box and docs/IRIS.md's 2026-09-06 entry for what to
|
|
check. Iris's words: "another length type similar to absolute &
|
|
relative, so instead there would be relative, pixels, and another unit
|
|
like em or whatever is standard. That way different display scales
|
|
should just work." Today a length is either a fraction of the parent
|
|
(`rest`/relative) or physical pixels, and the phone drew 16 px text at
|
|
roughly a third of its intended size until the P0 fixes applied the
|
|
display's scale factor globally. That global scale is a stopgap for the
|
|
benchmark; the real shape is a unit resolved against the display's
|
|
density at layout time — Android's `dp` / CSS's reference pixel is the
|
|
standard (1 unit = 1/160 in), with `em` as the text-relative option —
|
|
so a widget author writes `16.dp()` once and never sees the scale.
|
|
Done when: `Length` (or whatever the enum is called) has the third
|
|
variant; every place that resolves a length takes the density; the
|
|
examples and `transcript-ui` use the new unit for text sizes, padding
|
|
and control sizes; the emulator at two densities and the phone draw the
|
|
same layout at the same physical size. After the bench setup is
|
|
finished, before P1 draws any new screen.
|
|
|
|
## From the phone, bench v2 (2026-09-06): streaming re-lays out the whole message
|
|
|
|
- [x] **Streaming a delta into a long message costs a full text layout of
|
|
that message.** **Done 2026-09-06** -- a row is a column of one
|
|
`TextEdit` per markdown block (`client_core::markdown_blocks`,
|
|
`row::RowBlocks::apply_delta`), so a delta re-shapes the last block and
|
|
keeps every earlier block's layout. A block is the selection unit now
|
|
(`Selection`'s `SelKey`); selection across blocks and rows still works,
|
|
checked on the emulator with a real long-press drag. Pass condition met
|
|
in `a_delta_into_a_long_reply_redraws_the_same_widgets_as_a_short_one`:
|
|
a delta into a 100-paragraph reply redraws the same widget count as one
|
|
into a one-paragraph reply (30 either way). Emulator stream phase, same
|
|
AVD before and after: **p50 61.5 -> 54.5ms, p90 211.7 -> 113.1ms, p99
|
|
342.6 -> 137.4ms, worst 403.6 -> 143.0ms**, 202 -> 293 frames in the same
|
|
21 seconds. docs/RUST.md's Task B box has the detail and the two dead
|
|
ends. **The phone is the measurement that decides it** -- these are
|
|
emulator numbers and only the ratio transfers.
|
|
|
|
The original entry, for the record: Iris's phone report (`docs/bench/iris-phone-v2-2026-09-06.md`):
|
|
the stream phase is the one place iris is behind Compose (p50 18.2 ms vs
|
|
13.4 ms; p99 level at ~43 ms). `TranscriptScreen::apply` replaces only
|
|
the last row, but that row is the growing message, and replacing it
|
|
re-renders its markdown and re-shapes the entire paragraph run through
|
|
parley on every event. Compose pays a reparse (8.6 ms mean) for the
|
|
same event. What "done" looks like: a streamed delta re-lays out only
|
|
the block it lands in (the last paragraph or code block), with earlier
|
|
blocks' layouts kept -- which needs a row to be a column of per-block
|
|
`Text`s rather than one `TextEdit` for the whole message, or parley's
|
|
layout to be split at block boundaries; measured by the stream phase's
|
|
p50 dropping below Compose's on the phone. Do this after the four bench
|
|
v2 defects (stale primitives, finger fling, decay curve, IME show) are
|
|
closed, since they are what make the run unrepresentative today.
|
|
|
|
## From the phone, 2026-09-07 (build from ed04d4c)
|
|
|
|
- [x] **"Some transcript blocks will be hidden until I uncover enough of
|
|
them."** Two screenshots of the bench app's transcript at the top
|
|
edge, both wrong in opposite directions: in one, rows scrolled above
|
|
the viewport are still drawn and bleed *through* the header bar
|
|
(`version = "0.1.0"` and a paragraph visible behind "Run benchmark /
|
|
Copy report / Diagnostics"), so the list's mask is not clipping at
|
|
the header's bottom edge; in the other, scrolled a little further,
|
|
the row that straddles the top edge is not drawn at all -- black from
|
|
the header down to "You", where the previous shot showed a paragraph
|
|
-- so a row is culled as soon as its *top* leaves the viewport rather
|
|
than when its *bottom* does. Suspects: the list's visible-range test
|
|
(`iris/src/widget/list.rs`) comparing a row's top against the
|
|
viewport top; the mask region for the transcript set from the
|
|
window rather than from the area under the header; and the two-phase
|
|
provisional/real draw noted in `03c6be8`'s header-duplicate
|
|
investigation, which was never root-caused and has the same shape.
|
|
Reproduce at layer 1 of the test rig: a headless screen with a row
|
|
straddling the top edge must place that row, and a primitive above
|
|
the header's bottom must be masked. Fix both with one rule: a row is
|
|
drawn if any part of it intersects the viewport, and the viewport is
|
|
the list's own region.
|
|
|
|
**Done, e922b73 + d507ae4.** Three causes, and the rule above is what
|
|
they are all fixed with (`List::intersects_viewport`).
|
|
`iris/transcript-fixture/tests/top_edge.rs` is the layer-1
|
|
reproduction -- the real screen under a bench-app-shaped header --
|
|
and each test was confirmed to fail on its own subject and no other.
|
|
|
|
1. *Drawn over the header*: **nothing was clipping the list at all**,
|
|
and a row straddling an edge is drawn in full, so the part above
|
|
the list was on screen. It could not be `.masked()` before, either:
|
|
`Painter::set_mask` aborted when an ancestor already had a mask,
|
|
and the list's own rows use `.masked()` (a code fence, a tool
|
|
card's title). So masks nest now -- `Mask::parent`, walked in the
|
|
fragment stage, chained rather than intersected on the CPU because
|
|
each mask moves with its own widget. `the_list_is_clipped_to_its_
|
|
own_box`.
|
|
2. *Rows already scrolled past still drawn*: the layout walk runs from
|
|
the anchor, `scroll` moves the anchor's offset and nothing else, so
|
|
panning leaves the anchor's row further and further outside the
|
|
viewport and **every row between it and the viewport was drawn,
|
|
every frame** -- measured at 64 rows for a 2012px viewport after 8
|
|
scrolls of 3000px. `place` skips a row whose known box does not
|
|
overlap, and `rehome_anchor` puts the anchor back on a visible row
|
|
each frame without moving anything drawn.
|
|
`rows_that_have_left_the_viewport_are_not_drawn`.
|
|
3. *The blank band*: not a culling rule at all -- the list could rest
|
|
**past its own first row** (`fling_toward_the_start_stops_at_the_
|
|
first_row` was leaving it 1398px below a 600px viewport, a blank
|
|
screen, and that test's own assertion could not see it).
|
|
`clamp_to_content` gives the gap back. Both ends:
|
|
`scrolling_past_the_first_row_settles_on_it`,
|
|
`scrolling_past_the_last_row_settles_on_it`. This is also the first
|
|
item of the later report below.
|
|
|
|
What was suspected and is *not* what happened: the visible-range test
|
|
never compared a row's top against the viewport's top (there was no
|
|
culling test at all), and `03c6be8`'s header duplicate is untouched by
|
|
any of this -- it stays open. A row straddling the top edge is drawn
|
|
both before and after; the test that would catch that mistake
|
|
(`the_row_across_the_top_edge_is_drawn`) is in place, and fails if the
|
|
rule is written against the row's top instead of its bottom.
|
|
|
|
## From the phone, 2026-09-07, later (build from 4274b8b, ai-app-bench b47eb73)
|
|
|
|
- [x] **"You shouldn't be able to scroll below the bottom (or above
|
|
top)."** Done in e922b73, as `List::clamp_to_content` rather than as a
|
|
clamp inside the scroll setter: nothing at the moment of a `scroll`
|
|
call knows where the content ends (that is what walking the rows finds
|
|
out), so the correction is measured from the ends the layout walk
|
|
already placed and written to the anchor. In the app that lands in the
|
|
same frame -- a scrolled list is dirty, and `redraw_updates` drains
|
|
the mark the correction sets before the frame is submitted -- so
|
|
nothing displaced is displayed; only a full-tree redraw (a resize)
|
|
could show one frame of it. A fling that reaches an end already ends
|
|
there (`tick_fling`'s `hit_bound`), and now stops *on* the end rather
|
|
than wherever the spline's last step had put it. Layer-1 tests at both
|
|
ends, listed in the item above. The list's offset is not clamped to its content range while
|
|
dragging and/or flinging. Compose's `LazyColumn` never moves content
|
|
past its ends -- the overscroll *effect* on Android 12+ is a stretch
|
|
drawn over clamped content, not a displacement. Clamp the offset in
|
|
one place (`List`'s scroll setter, so drag, fling, page-in and
|
|
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.
|
|
- [x] **"Flinging now actually works but is slower than Compose's
|
|
immediately after releasing the flick (the slow down seems
|
|
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 **12250px/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
|
|
the code with timings so I can give you time reports through the
|
|
same button." **Built on the log ring, 2026-09-07** (docs/RUST.md's
|
|
own section): `iris::sense::log_input_event` (one line per platform
|
|
pointer sample -- Android's `MotionEvent`, historical samples inline;
|
|
winit's `WindowEvent`; the harness's `TouchScript` line) and
|
|
`iris::diagnostics::log_frame` (one line per frame: frame number,
|
|
the frame clock, time since the last input, layout/draw durations,
|
|
`redraw_all`/`redraw_updates`/neither, primitives on screen,
|
|
whether something is animating), both under
|
|
`iris::diagnostics::trace_enabled()`, off by default because the ring
|
|
is only 2000 lines / 256 KiB and both targets at 120Hz fill that in
|
|
seconds. `iris/benches/report_to_touch.py` turns a report's
|
|
`iris::input` lines back into a `.touch` file for layer 1/2 replay --
|
|
round-tripped in `iris/transcript-fixture/tests/
|
|
input_log_roundtrip.rs`. **Not wired to a button**: the Diagnostics
|
|
pane is `iris/android-app/src/bench_client.rs`, open under another
|
|
agent at the time this landed; `set_trace(bool)` is the whole surface
|
|
a control needs. `docs/REVIEW-2026-09-07.md`'s D1 (the ring already
|
|
drowned in per-frame `debug!` lines that predated this pass) is fixed
|
|
in the same change -- see RUST.md's section for which four call
|
|
sites.
|
|
|
|
## From the phone, 2026-09-07, night (build 92985ba, ai-app-bench bf2088b)
|
|
|
|
Iris pasted a full Copy report (Mali-G715 Vulkan, 2.55, 120Hz). What it
|
|
showed, beyond her words:
|
|
|
|
- [x] **"Sometimes when I try to catch it while it's still moving
|
|
(particularly if I drag) then it fails to stop & snap to where finger
|
|
is." (done 2026-09-07, b87f5a5.)** Built as described below.
|
|
`DragArbiter::press_start` takes a `PressState` -- what the target
|
|
looked like at the moment the press landed -- rather than asking the
|
|
list later, because by then the fling has already been cancelled and
|
|
the answer is no. The defect layer 1 found doing it: one touch-down
|
|
reaches every sensor under the finger, so a block and the tool row
|
|
containing it deliver the same `PressStart` twice, and re-reading the
|
|
state on the second delivery turned every catch back into an ordinary
|
|
slop-waiting press. Tests in
|
|
`iris/transcript-fixture/tests/catch_a_fling.rs`, with
|
|
`the_same_small_drag_on_a_settled_list_moves_nothing` as the half the
|
|
change had no reason to touch. **Not yet confirmed from the phone.**
|
|
The original reading follows. The report's release lines show catches ending as
|
|
`v=-41`/`v=-274` pans, so the gesture *does* reach `Panning`, but the
|
|
content under the finger does not follow it while the fling is still
|
|
running and the slop has not been crossed. Compose: a down while
|
|
`isScrollInProgress` stops the fling *at the down* and starts the
|
|
drag immediately with no touch slop (`scrollable`'s
|
|
`startDragImmediately = isScrollInProgress`); the content is pinned to
|
|
the finger from the first sample. Port that: `PressStart` on a list
|
|
with a live fling ends the fling on that sample and enters `Panning`
|
|
without waiting for `DRAG_SLOP`; a release with no movement is then a
|
|
`Released(None)`, not a tap (Compose does not deliver a click either).
|
|
Layer-1 test on a flick followed by a down + small drag 150 ms later:
|
|
offset tracks the finger sample-for-sample from the down.
|
|
- [x] **"The copy report button seemed impossible to hit until I hit the
|
|
diagnostics one." (done 2026-09-07, b8ea723).** Not hit-testing: the
|
|
button logged `iris bench report: nothing to copy -- run the benchmark
|
|
first` six times and did nothing on screen. A control that silently
|
|
declines is the UI_RULES failure "a failure is reported where it
|
|
happened": `copy_report` now always copies something -- the
|
|
diagnostics pane's own text (with a first line saying no benchmark has
|
|
run) when nothing has run yet, or the last report otherwise -- and
|
|
never depends on another button having been pressed first.
|
|
- [x] **"The logs seem way too big to send in this message box, causes a
|
|
lot of lag." (done 2026-09-07, 7485d78 + b8ea723).** Two causes. (1)
|
|
The ring was 1339 lines of `naga::front` / `wgpu_core` / `jni` DEBUG
|
|
output with 4050 dropped: the ring logger accepted every crate at
|
|
Debug, and the trace gate (992c472) only covered iris's own lines.
|
|
`client_core::log_ring::ring_accepts` is the one filter now, applied at
|
|
the ring rather than per callsite: Debug/Trace only from `iris`/
|
|
`client_core` targets when tracing is on, Info and above from
|
|
everything else. (2) Copy report appended the whole ring; it now
|
|
appends `LogRing::tail_text(COPY_REPORT_TAIL_LINES)` (150, named at the
|
|
constant) with a first line saying how many older lines were left out
|
|
-- the full ring is still what the devlog provider hands Dev Updater.
|
|
- [x] Keyboard: the report shows `ime_bottom=891 ime_visible=true` then
|
|
back to 0 on the phone, so the insets now arrive with a height; the
|
|
push-up was not reported broken this time.
|