iris: a device change re-uploads its textures, and a mark is one texture per shape

The bench APK panicked on frame 1 on the emulator:

    iris panic at iris/core/src/render/texture.rs:461:22:
    texture slot 89 is not a live standalone image: None

widget::mark called Textures::add per widget, so a folded card per tool
call meant a standalone image, a bind group and a draw call each --
hundreds of copies of three pictures. Textures::reset, which the Android
surface-rebuild path calls for a genuinely new renderer, then threw the
slot numbering away with the pixels, leaving every one of those live
handles naming a slot nothing recognised. Its doc had said the only
standalone image in the workspace was tabs-ui's, "confirmed by grep" --
true when written, false the moment mark existed.

Textures::reupload replaces reset: queue every slot for upload again in
slot order, empty slots included, so the new device gets the same slot
numbering and a handle a widget has been holding still names its own
texture. The glyph atlas is no longer cleared on that path either, so an
app switch stops re-rasterising every glyph on screen.

Textures::shared(key, make) is one texture per description, keyed by a
SharedTextureKey the caller packs exactly rather than hashes. mark keys on
direction and colour: three mark textures for the screen, not one a card.

And the devlog can finally show a panic. After a crash, Dev Updater's
query starts the app process for the provider alone, so no activity ran,
so set_crash_dir never replayed the panic hook's file -- the Runtime tab
held one line, the provider announcing itself. DevLogProvider.nativeReady
takes the files directory and does the replay from onCreate; the hook also
saves the dying run's last 80 lines beside the panic, read through a new
non-blocking LogRing::try_tail_text so a panic holding the ring's lock
cannot deadlock the hook.

Verified on this checkout's emulator: opens clean, survives 33 full-screen
scrolls back through the fixture, image_bind_group_creates_prev=1; a real
panic replays into the next launch, and a hand-written last-panic.txt
replays in a process started by a provider query with no activity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-08 14:27:09 -04:00
1 parent c8785b6091
commit 341b7a5922
9 files changed
+550 -57

No files matched your search

+92
View File
@@ -8064,3 +8064,95 @@ states the refresh it ran at.
**Not yet confirmed on the phone** -- that needs a build in her hands,
and the emulator cannot answer it (it is a 60Hz GLES rig, so the defect
is invisible there by construction).
## Iris's phone report, 2026-09-08 evening: "crashes as soon as I scroll up" and a devlog that showed one line
Her two sentences: the bench crashes shortly after she scrolls back from
where it opens, and Dev Updater's Runtime tab held only `iris devlog:
serving this app's log at content://dev.iris.android.demo.bench.devlog`.
### The devlog could only ever show the run that was still up
**Fixed.** After a crash, Dev Updater's query starts the app process **for
the provider alone**: no activity runs, so
`MainActivity.nativeSetFilesDir` never fired, so `app_log::set_crash_dir`
never replayed the line the panic hook had written to
`files/last-panic.txt`. The one line she saw is the provider announcing
itself in a process that had just been started to answer her.
`DevLogProvider.nativeReady` takes the files directory now and calls
`set_crash_dir` from `onCreate`; whichever of the provider and the
activity runs first does the replay, and the file is deleted before it is
replayed, so a replay that itself panicked cannot make a loop. The hook
also saves the dying run's last 80 log lines beside the panic (a new
non-blocking `LogRing::try_tail_text`, so a panic raised while the ring's
lock was held cannot deadlock the hook), replayed into the new ring ahead
of the panic line under the target `previous_run`.
Verified on this checkout's emulator both ways: a real panic replayed into
the next launch, and a hand-written `files/last-panic.txt` replayed by a
process started by `content query` alone with **no activity** (the query
is refused for the `shell` uid, which does not hold
`dev.updater.permission.READ_DEVLOG` -- the provider's `onCreate` still
runs, which is the half under test).
### The crash: one texture per mark, and a device change that forgot them
The bench APK **panicked on frame 1** on this checkout's emulator, which
is where this was found rather than by reasoning from her report:
iris panic at iris/core/src/render/texture.rs:461:22:
texture slot 89 is not a live standalone image: None
`widget::mark` (added the same day) called `Textures::add` **per widget**,
so a folded card per tool call meant a standalone image, a bind group and
a draw call each -- slot 89 was one of hundreds of copies of three
pictures. `Textures::reset`, which the Android surface-rebuild path calls
when it builds a genuinely new renderer, then threw the slot *numbering*
away along with the pixels, leaving every one of those live handles naming
a slot nothing recognised. Its own doc had said the only standalone image
in the workspace was `tabs-ui`'s, "confirmed by grep" -- true when it was
written, false the moment `mark` existed. **A rule stated on one member of
a set** (CODE_RULES): the grep was the invariant, and nothing re-ran it.
Two fixes, both in `iris-core`:
- **`Textures::reupload` replaces `reset`**: queue every slot for upload
again in slot order, empty slots included (they cross as `PushFree`, so
the indices after a hole still land where they were). This side holds
the images, so the new device gets the same slot numbering and every
handle a widget is holding still names its own texture. The glyph atlas
is no longer cleared on that path either -- its pages are slots here, so
they come back with everything else, and an app switch stops costing a
re-rasterisation of every glyph on screen.
- **`Textures::shared(key, make)`**: one texture per description. `mark`
keys on direction and colour, packed exactly into a
`SharedTextureKey { owner, id }` rather than hashed, so the screen has
three mark textures rather than one per card and the rasterising is paid
once. The map holds its own reference, so a shared slot is never freed
and never recycled under a widget still drawing it.
After the fix the emulator opens clean and survives 33 full-screen scroll
gestures back through the fixture, with
`image_bind_group_creates_prev=1`.
**Whether this is the crash on *her* phone is not yet confirmed** -- her
build is release, its startup does not take the GLES fallback that made
the emulator rebuild its renderer, and the panic she saw was never
recorded anywhere. It is the crash this code had; the devlog fix above is
what will say whether it was hers. Ask for the Runtime tab after the next
bench APK.
### Open: why `mark` exists at all (Iris asked, 2026-09-08)
Her question, mid-fix: "why does mark exist? The font should be working if
it's working for compose and nerd fonts are bundled." The Compose app
draws its chevron from **its own committed Nerd Fonts subset**
(`app/build-icon-font.sh`, `NerdIcons.kt`); iris bundles no font at all
since 2026-09-07, and `tool.rs` was using bare geometric codepoints
(U+25B8/25BE/25B4) out of whatever system face resolved -- which her phone
had none for. So the two apps were never doing the same thing, and the
2026-09-07 note that "iris had no equivalent icon font to keep" is what
left the gap. The alternative to `mark` is to bundle the same ~100-glyph
subset in iris and take icons from it, which would serve every future icon
rather than one triangle. Not done: it is hers to choose.