Give memory back in the order the user will miss it least
FR-PLAT-AND-5. Android asks for memory back through onTrimMemory and kills the process if it is not given; until now nothing listened, so the answer was always "no". A tiered registry answers instead: GPU caches first, then proxies, then thumbnails, driven from android_main on MainEvent::LowMemory and MainEvent::Stop. The order is the argument. A backgrounded app has no window to draw and therefore no use for a render pipeline, while its thumbnails are exactly what the user will be looking at half a second after they come back -- so going into the background frees only the GPU tier, and only being measured against death frees everything. Sinks register beside the cache they free and hold weak handles, so the registry cannot keep a controller -- and every decoded portrait in it -- alive past the interface it belonged to. `try_borrow_mut` and skip: a warning can land mid-render, freeing textures under the code drawing with them is worse than missing one, and a warning not acted on is always followed by another. The GPU test is the one that matters: an eviction must change no pixel. A freed intermediate pool whose `colour_key` promise still stands renders an empty texture, and nothing else would have caught it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1026,6 +1026,44 @@ impl AdjustPass {
|
||||
h
|
||||
}
|
||||
|
||||
/// TRACES: FR-PLAT-AND-5 | NFR-RES-1
|
||||
/// Give back every allocation this pass is holding only to be fast again.
|
||||
///
|
||||
/// What goes, and why each is safe to lose:
|
||||
///
|
||||
/// - **The compiled pipelines**, here and in the detail stage. A pure
|
||||
/// lookup keyed by structure hash with a compile-on-miss behind it, and
|
||||
/// unbounded until now — nothing ever removed an entry, so a session
|
||||
/// that visited enough distinct edit structures accumulated shader
|
||||
/// objects for the life of the process.
|
||||
/// - **The detail intermediates**, which are viewport-sized `Rgba16Float`
|
||||
/// and, as `detail.rs` says of them, grow but never shrink.
|
||||
/// - **The two output textures.** Dropping these does not take the picture
|
||||
/// off the screen: whatever was handed to the compositor holds its own
|
||||
/// reference to the `wgpu::Texture`, so releasing ours only means the
|
||||
/// *next* render allocates rather than reuses. `ensure_target` already
|
||||
/// treats an empty slot as "allocate", because that is the state it
|
||||
/// starts in.
|
||||
///
|
||||
/// **`colour_key` must be cleared with them, and this is the part that
|
||||
/// would bite.** The key is the promise that slot 0 of the detail pool
|
||||
/// still holds the fused colour result, and it is what lets a sharpening
|
||||
/// slider skip the colour chain (FR-DEV-3d). Freeing the pool while the
|
||||
/// promise stood would make the next detail-only render sample a
|
||||
/// just-allocated texture with nothing in it — a silently wrong frame, not
|
||||
/// a failure, and one that would only appear on a device under memory
|
||||
/// pressure.
|
||||
///
|
||||
/// What deliberately stays: the demosaiced source is not this pass's to
|
||||
/// drop, the film tables are set once by a caller that will not be asked
|
||||
/// again, and the bind group layouts are bytes rather than megabytes.
|
||||
pub fn release_caches(&mut self) {
|
||||
self.cache.clear();
|
||||
self.detail.release_caches();
|
||||
self.targets = [None, None];
|
||||
self.colour_key = None;
|
||||
}
|
||||
|
||||
/// How many distinct pipelines are compiled. Exposed for tests asserting
|
||||
/// that slider movement does not recompile.
|
||||
pub fn cached_pipelines(&self) -> usize {
|
||||
|
||||
@@ -157,6 +157,24 @@ impl Intermediates {
|
||||
self.allocations += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-PLAT-AND-5
|
||||
/// Drop the pool, leaving it as [`Intermediates::new`] left it.
|
||||
///
|
||||
/// The size is reset along with the slots, not merely because it is tidy:
|
||||
/// [`Self::ensure`] only refills when the count is short *or* the size
|
||||
/// differs, so a pool cleared while still claiming its old dimensions is
|
||||
/// indistinguishable from one that never held anything — which is fine
|
||||
/// here, and would stop being fine the moment `ensure` grew a fast path
|
||||
/// that trusted the stored size. `allocations` deliberately keeps
|
||||
/// counting: it exists so a test can see textures being made, and a
|
||||
/// counter reset on eviction would hide a reallocation storm rather than
|
||||
/// report one.
|
||||
fn release(&mut self) {
|
||||
self.slots.clear();
|
||||
self.width = 0;
|
||||
self.height = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the detail stage.
|
||||
@@ -526,6 +544,19 @@ impl DetailRunner {
|
||||
self.cache.len()
|
||||
}
|
||||
|
||||
/// TRACES: FR-PLAT-AND-5
|
||||
/// Give back everything this stage is only holding to be fast.
|
||||
///
|
||||
/// Both pools and the pipeline cache. Nothing here is state: a pool slot
|
||||
/// is re-created by the next [`Intermediates::ensure`] and a pipeline by
|
||||
/// the next compile-on-miss, so the only cost of this call is the work of
|
||||
/// doing both again.
|
||||
pub(crate) fn release_caches(&mut self) {
|
||||
self.cache.clear();
|
||||
self.pool.release();
|
||||
self.reduced.release();
|
||||
}
|
||||
|
||||
/// How many intermediate textures have been allocated since this pass was
|
||||
/// created. For tests — see [`crate::MaskPass::allocations`] for the
|
||||
/// regression this shape of counter exists to catch.
|
||||
|
||||
Reference in New Issue
Block a user