From 95991aca68e11fe559f73e303f2687d51cdee490 Mon Sep 17 00:00:00 2001 From: Fi3w0 Date: Wed, 28 Oct 2026 02:34:45 +0100 Subject: [PATCH] feat: sweep every GPU's released textures while idle Smithay frees dropped GL textures, images and buffers only after a rendered frame, and only on the renderer that drew it. Anything released after an idle desktop's last frame waited for the next one, and render-only GPU nodes holding cross-GPU imports were never swept. The udev maintenance timer now calls GpuManager::cleanup_texture_cache at most every 5 s, skipped while the DRM device is paused. Upstream only makes a context current when its destruction queue is non-empty, so an idle GPU is not woken from runtime suspend. --- CHANGELOG.md | 4 ++++ Cargo.lock | 2 +- Cargo.toml | 2 +- DEV_CHANGELOG.md | 6 ++++++ src/backend/udev.rs | 28 ++++++++++++++++++++++++++++ 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f85672e..958add6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ What changed in TideWM, in plain words, newest first. Releases are grouped by the stretch of work they came from. Every release in full technical detail lives in [DEV_CHANGELOG.md](DEV_CHANGELOG.md). +## 0.90.200 · 2026-10-28 — GPU memory freed while idle + +- Textures and copied buffers that TideWM no longer needs are now freed every few seconds, on every GPU, even when nothing on screen is changing. Before, they could wait for the next drawn frame, and on a second GPU they were never swept. GPUs with nothing to free are not woken up. + ## 0.90.199 · 2026-10-28 — Screen sharing survives a monitor switching off - Turning off or unplugging a monitor you are sharing no longer kills the share. OBS, Discord and other apps keep the last picture while it is gone, and the live picture comes back by itself when the monitor returns. If it comes back at a different resolution, the share ends as before and you pick it again. diff --git a/Cargo.lock b/Cargo.lock index 602e893..d75153d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2416,7 +2416,7 @@ dependencies = [ [[package]] name = "tidewm" -version = "0.90.199" +version = "0.90.200" dependencies = [ "accesskit", "accesskit_unix", diff --git a/Cargo.toml b/Cargo.toml index 8c13227..58f3549 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tidewm" -version = "0.90.199" +version = "0.90.200" edition = "2021" license = "GPL-3.0-or-later" # Verified floor: wayland-protocols 0.32.13 requires 1.86 (1.85 fails to diff --git a/DEV_CHANGELOG.md b/DEV_CHANGELOG.md index b9c9503..627cf13 100644 --- a/DEV_CHANGELOG.md +++ b/DEV_CHANGELOG.md @@ -2,6 +2,12 @@ Every TideWM release in full technical detail, newest first. For a short summary of what changed for users, see [CHANGELOG.md](CHANGELOG.md). Format loosely follows [Keep a Changelog](https://keepachangelog.com/). +## 0.90.200 — Idle texture-cache sweep on every GPU node + +- Smithay frees dropped GL textures, EGL images and buffers only in `GlesRenderer::cleanup`, which runs on `unbind` after each rendered frame and on no-damage/direct-scanout `render_frame` calls, and only for the renderer that drew. Two gaps followed. Anything released after an idle desktop's last frame (a hidden-workspace window closing, a backdrop or capture texture dropped outside a frame) stayed queued until the next frame. Render-only GPU nodes, which hold cross-GPU imports in `GpuManager`, were never swept, because `ImportBridge::cleanup_texture_cache` only reaches the primary renderer. +- The udev maintenance timer (≈1 s idle period, not tied to frames or outputs) now calls `GpuManager::cleanup_texture_cache` (upstream `347b2b30`) at most every 5 s (`TEXTURE_CACHE_CLEANUP_INTERVAL`), through `UdevGpu::cleanup_texture_caches`. It is skipped while the DRM device is paused (VT switched away) and uses `try_borrow_mut`, so it can never collide with a render. Upstream `79bbed5e` makes the context current only when its destruction queue is non-empty, so an idle node is not woken from runtime suspend (Smithay #2178/#2180). This is the call site plan §3 deferred to after the migration, and the basis of Ebb's `low` level. +- No dGPU/iGPU, vendor or output-specific logic; the winit backend is unchanged (single host-owned renderer). Native evidence owed: no unexpected GPU wake or frame stutter, and memory after closing windows on a hidden workspace. + ## 0.90.199 — Monitor screencasts outlive a disconnected output - Native report on `.198`: OBS capture of HDMI-A-2 broke after the monitor was powered off. Session log: `Output disconnected` at 01:59:29; OBS's stream went `streaming` → `paused` at the same second and stayed there, and its `tidewm-screencast-HDMI-A-2` node was gone. Cause (TideWM, not the migration): the compositor-side `FrameRequested` handler called `FrameTarget::close()` when the named output was missing, so the PipeWire worker stopped and removed its node, but the portal session was never told (no `impl.portal.Session` `Closed`), leaving the consumer on a dead stream that a returning monitor could not revive. diff --git a/src/backend/udev.rs b/src/backend/udev.rs index ea5a00c..70409fa 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -250,6 +250,21 @@ impl UdevGpu { imported } + /// Frees textures and imports already released on every GPU node. + /// + /// Each rendered frame cleans only the renderer it drew with, so the + /// render-only GPUs that hold cross-GPU imports, and anything dropped + /// after the last frame of an idle desktop, would otherwise wait. Smithay + /// makes a context current only when it has something to destroy, so a + /// node with nothing queued is not woken from runtime suspend. + fn cleanup_texture_caches(&self) { + // The timer never runs inside a render, but stay safe if a borrow + // is ever held across it. + if let Ok(mut manager) = self.manager.try_borrow_mut() { + let _ = manager.cleanup_texture_cache(); + } + } + pub(crate) fn prepare_surface(&self, surface: &WlSurface) { let mut manager = self.manager.borrow_mut(); if let Err(error) = manager.early_import(self.primary_render_node, surface) { @@ -1102,9 +1117,19 @@ pub fn init_udev( // Continuous animation derives its wake period from the fastest live mode; // scheduled caustics uses its configured deadline directly. let device_for_timer = Rc::clone(&device); + let mut last_texture_cleanup = Instant::now(); event_loop .handle() .insert_source(Timer::immediate(), move |_, _, state: &mut Smallvil| { + // Rate-limited, and never while VT-switched away: GL work on a + // paused device has nothing to free and nothing to gain. + if last_texture_cleanup.elapsed() >= TEXTURE_CACHE_CLEANUP_INTERVAL { + last_texture_cleanup = Instant::now(); + let device = device_for_timer.borrow(); + if device.drm.is_active() { + device.gpu.cleanup_texture_caches(); + } + } state.update_window_depths(); state.update_urgent_pulses(); state.update_float_physics_full(); @@ -1669,6 +1694,9 @@ fn handle_connector_change( /// bindable, so clients that raced a bind still find it (niri uses the same). const OUTPUT_GLOBAL_REMOVE_DELAY: Duration = Duration::from_secs(10); +/// How often the maintenance timer sweeps every GPU's released textures. +const TEXTURE_CACHE_CLEANUP_INTERVAL: Duration = Duration::from_secs(5); + /// Retracts a disconnected output's `wl_output` global. Must run after every /// surface has left the output: a client that sees `global_remove` destroys /// its proxy, and a later `wl_surface.leave` naming it reaches the client as