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:
1 parent
c8785b6091
commit
341b7a5922
9 files changed
+550
-57
No files matched your search
@@ -1370,3 +1370,55 @@ worth knowing because it changes what a build of the Android app *is*.
|
||||
both the desktop and the phone -- only the directory differs.
|
||||
`desktop-app --ca` is now the override for a link that carried no CA
|
||||
rather than a required flag.
|
||||
|
||||
## 2026-09-08: a new GPU device re-uploads its textures instead of forgetting them, and a mark is one texture per shape
|
||||
|
||||
Two defects with one cause: **`widget::mark` built a texture per widget**,
|
||||
so a transcript screen had one 48x48 standalone image, one bind group and
|
||||
one draw call *per folded card* rather than one per picture -- and the
|
||||
Android surface-rebuild path assumed no long-lived widget held a texture
|
||||
handle at all.
|
||||
|
||||
- **`Textures::reset` is gone; `Textures::reupload` replaces it.** A new
|
||||
GPU device holds none of the old one's textures, but this side still
|
||||
holds their pixels, so the answer is to queue every slot for upload
|
||||
again in slot order (empty slots included, as `PushFree`, so the
|
||||
indices after a hole still land where they were) rather than to throw
|
||||
the slot numbering away. Resetting left every live `TextureHandle`
|
||||
naming a slot nothing recognised: the first frame after the emulator's
|
||||
Vulkan-to-GLES fallback panicked with *"texture slot 89 is not a live
|
||||
standalone image: None"*, before anything had been touched.
|
||||
- **The glyph atlas is no longer cleared on that path either**, which
|
||||
falls out of the same change: its pages are slots here and their pixels
|
||||
are on this side, so re-uploading restores exactly the atlas that was
|
||||
there. An app switch no longer re-rasterises every glyph on screen.
|
||||
- **`Textures::shared(key, make)`** (new): the one texture for a
|
||||
description, built on the first ask and handed out again after, keyed
|
||||
by a `SharedTextureKey { owner, id }` the caller packs *exactly* rather
|
||||
than hashes. The map holds its own reference, so a shared slot is never
|
||||
freed and never recycled under a widget still drawing it. `mark()` is
|
||||
its first caller: three marks now exist for the whole transcript screen
|
||||
(open, closed, collapse) instead of one per card, and the rasterising is
|
||||
paid once.
|
||||
|
||||
## 2026-09-08: an app's own log survives the process that wrote it
|
||||
|
||||
`devlog`'s provider could only ever show the run that was still up. After
|
||||
a crash, Dev Updater's query starts the app process **for the provider
|
||||
alone** -- no activity runs, so `MainActivity.nativeSetFilesDir` never
|
||||
fired and the panic hook's file was never replayed. The Runtime tab
|
||||
therefore showed one line, `iris devlog: serving this app's log at ...`,
|
||||
which is exactly the run nobody needs.
|
||||
|
||||
- **`DevLogProvider.nativeReady` now takes the files directory too**, and
|
||||
`app_log::set_crash_dir` is called from whichever of the provider and
|
||||
the activity runs first (it deletes the file, so the second says
|
||||
nothing).
|
||||
- **The panic hook saves context, not just the panic**: the dying run's
|
||||
last 80 log lines go into the file with it, and are replayed into the
|
||||
new run's ring ahead of the panic line, so the Runtime tab reads
|
||||
chronologically -- what the app was doing, then what killed it, then
|
||||
this run. They are read with a new non-blocking
|
||||
`LogRing::try_tail_text`, because a panic raised while the ring's own
|
||||
lock was held would otherwise deadlock the hook and hang the process
|
||||
instead of aborting it.
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user