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

+52
View File
@@ -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.