Merge: answer Android's memory warnings, and stop reporting a lost root as an empty library

FR-PLAT-AND-5 in full, FR-PLAT-AND-2 in part -- the recovery is built and
live for Nextcloud roots, the SAF cause it names does not exist yet.

FR-PLAT-AND-4 and FR-PLAT-AND-6 are not here, both blocked behind the
same gap: assemble-apk.sh compiles no Java, so the APK cannot carry a
Service or a FileProvider. The container has JDK 17 and build-tools 36;
the build step is what is missing.

Verified: fmt, clippy --workspace --all-targets -D warnings, and 1043
tests across dr-catalog, dr-sync, dr-sync-folder, dr-sync-nextcloud,
dr-plat and dr-ui. The aarch64 target was checked before the branch was
finished but not after; no device was available.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-29 22:19:46 +02:00
co-authored by Claude Opus 5
16 changed files with 1002 additions and 39 deletions
+43
View File
@@ -39,6 +39,7 @@ mod library_ui;
#[cfg(live_style)]
mod live_style;
mod masks_ui;
pub mod memory;
mod net_runtime;
mod peaking;
mod preset_store;
@@ -1012,6 +1013,22 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
// faces are ticked for a split.
let identity = std::rc::Rc::new(identity_ui::IdentityController::new());
// TRACES: FR-PLAT-AND-5
// The thumbnail tier. Registered here, beside the thing it frees, so that
// a controller which grows another cache is one line from offering it up.
//
// Weak, not strong: `run` returns when the window closes, and a registry
// holding the last reference to a controller would keep it — and every
// decoded portrait in it — alive past the interface it belonged to.
{
let identity = std::rc::Rc::downgrade(&identity);
memory::evict_at(memory::Tier::Thumbnails, move || {
if let Some(ctl) = identity.upgrade() {
ctl.clear_covers();
}
});
}
// Launch screen: shown when there is nothing to display — no local paths
// and no configured library. A user who has already signed in and chosen
// a folder goes straight to their images (FR-NC-1).
@@ -1434,6 +1451,32 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
// The current develop session, if the file yielded sensor data.
let session: Rc<RefCell<Option<DevelopSession>>> = Rc::new(RefCell::new(None));
// TRACES: FR-PLAT-AND-5
// The GPU tier — the first thing given back under memory pressure, and on
// Android the only thing given back merely for going into the background.
//
// `try_borrow_mut` rather than `borrow_mut`, and the miss is not an error
// worth reporting. A memory warning can land in the middle of a render, at
// which point the slot is already borrowed and freeing its textures under
// the code drawing with them is not something to do politely — skipping is
// correct, because the pass that is running will have finished by the time
// the platform asks again, and a warning that has not been acted on is
// always followed by another one.
{
let session = Rc::downgrade(&session);
memory::evict_at(memory::Tier::Gpu, move || {
let Some(session) = session.upgrade() else {
return;
};
let Ok(mut slot) = session.try_borrow_mut() else {
return;
};
if let Some(open) = slot.as_mut() {
open.release_gpu_caches();
}
});
}
// TRACES: FR-DEV-6 | FR-CAT-8
// The settings clipboard, and where the open image's edit is stored.
//