evict layer backdrop textures on resize and destroy to prevent VRAM leak - #87
Open
sophietheopossum wants to merge 2 commits into
Open
evict layer backdrop textures on resize and destroy to prevent VRAM leak#87sophietheopossum wants to merge 2 commits into
sophietheopossum wants to merge 2 commits into
Conversation
…RAM leak
`layer_backdrop_cache` was insert-only, so every `CachedBackdropTexture` and
the full-size `GlesTexture` it holds survived for the whole session. Worse,
the key ends in the effect rect's `{width}x{height}`, so a layer that resizes
mints a new key and a new texture for every distinct size it has ever had:
a dock whose width tracks window titles grows the cache without bound and is
never destroyed. Add `evict_stale_backdrop_sizes` at each insert site to drop
the entries a layer left behind at its previous sizes, and
`purge_backdrop_cache_for_layer` from `layer_destroyed` to clear a departed
layer across all outputs, mirroring the window-side fix in 1ac0829.
Three sites also built the key from a bare `protocol_id()`, which is unique
only within a client, rather than `layer_runtime_id()`; two clients whose
surfaces shared a protocol id would collide on one key, sharing its texture,
its render-element `Id` and its commit counter. The signature check gates the
actual texture reuse, so the realistic effect is cache thrash rather than a
visibly wrong backdrop; found by inspection, not reproduced. In `configured_background_effect_elements_for_layer` the
correct id was already in scope and had been shadowed.
`purge_backdrop_cache_for_layer` is driven by `layer_destroyed`, which does
not fire for every departure. On an abrupt client exit `wl_surface().client()`
is already `None`, so `layer_runtime_id` degrades to `unknown-client:<id>` and
cannot match the keys written while the client was alive, leaving those
entries orphaned. Sweeping the cache against the live layer set instead needs
no event to fire, so it also covers a close the compositor missed.
this fix is called from both `refresh_layer_effects_for_output` paths, beside the existing
`layer_effect_cache` sweep, so the two caches are pruned by the same rule at
the same point.
It cannot reuse `retain_effect_texture_cache_for_live_ids`: that helper is
generic over the value type and so would compile if handed this map, but it
tests `{id}@` as a key *prefix*, while these keys carry the id between
underscores after the output name. It would match nothing and empty the cache
on every refresh while looking like correct reuse.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The layer-surface counterpart to 1ac0829 (#59), which fixed this leak class on the window side; both new helpers land beneath its
purge_shared_effect_pipeline_caches_for_windowand are named to match.layer_backdrop_cacheis insert-only: 42 references acrosstty.rs,winit.rsandstate.rs, not one of them aremove,retain,clearordrain, and because the key ends in the effect rect's size (__layer_background_effect_DP-1_<layer_id>_0_1520x471), a resize mints a fresh full-size texture for every size a layer has ever had. The texture is allocated in physical pixels while the key carries the logical rect, so that key costs 2.7 MiB at scale 1.Scope: only configs that put a backdrop effect on a layer surface, a per-layer
behind:effect, orbackground_effectand a client declaring a blur region throughext-background-effect-v1. A stock config never writes an entry, and a plain backdrop blur on a Top/Overlay layer takes the framebuffer path instead, so in practice it'sbehind:effects and Background/Bottom layers.Nothing reclaims those entries today.
layer_destroyeddoesn't touch the map; the hot-reload reset atstate.rs:2481clears five sibling effect caches and skips this one;refresh_layer_effects_for_outputsweepsconfigured_layer_effectsandlayer_effect_cachefor dead layer ids and skips this one; and the 128-entry pipeline LRU frees the intermediates while the final texture stays pinned by the clone stored here.What changed
evict_stale_backdrop_sizes(cache, current_key)at the four insert sites (two intty.rs, two inwinit.rs): splits the key at its last_and keeps only the current size for that layer and variant. The current key is exempt so the insert can still read back its elementIdand commit counter. It runs on the insert path only: the cache-hit branch returns before it, though anAlways-invalidation effect takes that path every frame; the retain is a prefix compare per entry over a map bounded by outputs × layers-with-backdrop × variants, inside a function already doing a full-size capture and N blur passes.purge_backdrop_cache_for_layerfromlayer_destroyed, clearing a departed layer across every output. On an abrupt client exitwl_surface().client()is alreadyNone, solayer_runtime_iddegrades tounknown-client:<id>and the purge matches nothing.retain_backdrop_cache_for_live_layerscloses that gap, called from bothrefresh_layer_effects_for_outputpaths beside the existinglayer_effect_cachesweep: it drops any entry whose layer is not in the live set, so it needs no event to fire and also covers a close the compositor missed. It cannot reuseretain_effect_texture_cache_for_live_ids: that tests{id}@as a key prefix, while these keys carry the id between underscores after the output name, so handing it this map would match nothing and empty the cache on every refresh.wl_surface().id().protocol_id(), unique only within a client, rather thanlayer_runtime_id()({client_id}:{protocol_id}); the purge searches for_{layer_runtime_id}_, so it cannot match a key built from a bare protocol id. The correct binding was already in scope at all three:tty.rs's top-layer andwinit.rs's lower-layer sites each ignored one earlier in the same function, andwinit.rs's top-layer site shadowed one, so that fix is a deleted line, which also re-keys that function's three sub-element rect keys from the protocol id to the runtime id (reads and writes move together, so it's inert, but it's why one deletion touches more thanstable_key). On its own the collision reads as cache thrash plus two render elements sharing a damage-trackerIdand commit counter, since the signature check, which hashes the effect rect and capture geometry, gates the actual texture reuse. Found by inspection, not reproduced.Testing. Both new paths fire in a real session here (Intel xe iGPU):
cargo check -p shoji_wm --binsis clean againstmainat 0d4eab1 itself: I built this branch in a fresh worktree on that base, zero warnings.cargo test -p shoji_wm --binsI have only run on my own tree: 186 passed, 4 failed, allssd::evaluator::tests::workspace_*kinetic-scroll assertions that fail the same way with this patch stashed, so they are not caused by the change; my tree diverges frommaininsidessd/, so I can't say whether they also fail for you. The session logs above were captured on my tree too, though every region this patch touches is identical in both, and the branch is one commit on0d4eab1, so it applies as a fast-forward.Two commits: the eviction and the keying fix, then the liveness sweep on top. The keying fix rides with the first because it is the same expression built wrong, happy to lift it into its own commit if you'd prefer that.