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:
@@ -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.
|
||||
|
||||
@@ -413,3 +413,56 @@ fn an_empty_chain_falls_through_to_the_ordinary_render() {
|
||||
assert_eq!(pass.detail_dispatches(), 0);
|
||||
assert_eq!(pass.detail_allocations(), 0);
|
||||
}
|
||||
|
||||
/// TRACES: FR-PLAT-AND-5
|
||||
#[test]
|
||||
fn eviction_gives_the_pools_back_without_changing_a_pixel() {
|
||||
// The half of memory-pressure eviction that cannot be checked by looking
|
||||
// at a counter. `release_caches` frees the detail pool, and slot 0 of that
|
||||
// pool is where the fused colour result lives between frames — so the
|
||||
// render after an eviction has to notice that the promise recorded in
|
||||
// `colour_key` no longer holds and run the colour chain again.
|
||||
//
|
||||
// Leave the key standing and this test does not error: it draws. It draws
|
||||
// whatever a freshly-allocated texture happens to contain, which is the
|
||||
// failure worth building a test around, because on a device it would
|
||||
// appear only under memory pressure and only as a wrong-looking photograph.
|
||||
let Some(ctx) = ctx() else { return };
|
||||
const SIZE: u32 = 48;
|
||||
let source = step_edge(&ctx, SIZE);
|
||||
let mut pass = AdjustPass::new(&ctx);
|
||||
let mut graph = EditGraph::with_detail_probe();
|
||||
graph.set_param(PROBE, RADIUS, 0.05);
|
||||
|
||||
let before = render(&ctx, &mut pass, &graph, &source, SIZE);
|
||||
assert!(
|
||||
pass.cached_pipelines() > 0,
|
||||
"the colour pass compiled something"
|
||||
);
|
||||
assert!(
|
||||
pass.cached_detail_pipelines() > 0,
|
||||
"so did the detail stage"
|
||||
);
|
||||
let allocations = pass.detail_allocations();
|
||||
assert!(allocations > 0, "and the pool holds textures");
|
||||
|
||||
pass.release_caches();
|
||||
assert_eq!(pass.cached_pipelines(), 0);
|
||||
assert_eq!(pass.cached_detail_pipelines(), 0);
|
||||
|
||||
// The same edit at the same size. Nothing about the picture changed, so
|
||||
// nothing about the pixels may change either — only what it cost.
|
||||
let after = render(&ctx, &mut pass, &graph, &source, SIZE);
|
||||
assert_eq!(before.len(), after.len());
|
||||
for (i, (a, b)) in before.iter().zip(&after).enumerate() {
|
||||
assert!(
|
||||
a.abs_diff(*b) <= 1,
|
||||
"byte {i}: {a} before eviction, {b} after — the colour chain did \
|
||||
not re-run, so this frame is reading an empty intermediate"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
pass.detail_allocations() > allocations,
|
||||
"the pool was rebuilt, which is the evidence it was really given back"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user