From eab805e7050d4f6f349a08b50885ef10cbf66de0 Mon Sep 17 00:00:00 2001 From: pax Date: Wed, 15 Apr 2026 22:21:32 -0500 Subject: [PATCH] video_player: free GL render context on stop to release idle VRAM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit behavior change: stop() now calls _gl_widget.release_render_context() after dropping hwdec, which frees the MpvRenderContext's internal textures and FBOs. Previously the render context stayed alive for the widget lifetime — its GPU allocations accumulated across video-to-image switches in the stacked widget even though no video was playing. The context is recreated lazily on the next play_file() via the existing ensure_gl_init() path (~5ms, invisible behind network fetch). After release, paintGL is a no-op (_ctx is None guard) and mpv won't fire frame-ready callbacks, so the hidden QOpenGLWidget is inert. cleanup() now delegates to release_render_context() + terminate() instead of duplicating the ctx.free() logic. --- CHANGELOG.md | 1 + booru_viewer/gui/media/mpv_gl.py | 19 ++++++++++++++++++- booru_viewer/gui/media/video_player.py | 5 +++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ceb4a6..1a6088d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Bookmark→library save and bookmark Save As now plumb the active site's `CategoryFetcher` through to the filename template, so `%artist%`/`%character%` tokens render correctly instead of silently dropping out when saving a post that wasn't previewed first - Info panel no longer silently drops tags that failed to land in a cached category — any tag from `post.tag_list` not rendered under a known category section now appears in an "Other" bucket, so partial cache coverage can't make individual tags invisible - `BooruClient._request` retries now cover `httpx.RemoteProtocolError` and `httpx.ReadError` in addition to the existing timeout/connect/network set — an overloaded booru that drops the TCP connection mid-response no longer fails the whole search on the first try +- VRAM retained when no video is playing — `stop()` now frees the GL render context (textures + FBOs) instead of just dropping the hwdec surface pool. Context is recreated lazily on next `play_file()` via `ensure_gl_init()` (~5ms, invisible behind network fetch) ### Refactored - `category_fetcher` batch tag-API params are now built by a shared `_build_tag_api_params` helper instead of duplicated across `fetch_via_tag_api` and `_probe_batch_api` diff --git a/booru_viewer/gui/media/mpv_gl.py b/booru_viewer/gui/media/mpv_gl.py index 9927325..c50b2dc 100644 --- a/booru_viewer/gui/media/mpv_gl.py +++ b/booru_viewer/gui/media/mpv_gl.py @@ -111,7 +111,20 @@ class _MpvGLWidget(QWidget): self._gl.makeCurrent() self._init_gl() - def cleanup(self) -> None: + def release_render_context(self) -> None: + """Free the GL render context without terminating mpv. + + Releases all GPU-side textures and FBOs that the render context + holds. The next ``ensure_gl_init()`` call (from ``play_file``) + recreates the context cheaply (~5ms). This is the difference + between "mpv is idle but holding VRAM" and "mpv is idle and + clean." + + Safe to call when mpv has no active file (after + ``mpv.command('stop')``). After this, ``_paint_gl`` is a no-op + (``_ctx is None`` guard) and mpv won't fire frame-ready + callbacks because there's no render context to trigger them. + """ if self._ctx: # GL context must be current so mpv can release its textures # and FBOs on the correct context. Without this, drivers that @@ -123,6 +136,10 @@ class _MpvGLWidget(QWidget): finally: self._gl.doneCurrent() self._ctx = None + self._gl_inited = False + + def cleanup(self) -> None: + self.release_render_context() if self._mpv: self._mpv.terminate() self._mpv = None diff --git a/booru_viewer/gui/media/video_player.py b/booru_viewer/gui/media/video_player.py index d4c0c51..cc7efa4 100644 --- a/booru_viewer/gui/media/video_player.py +++ b/booru_viewer/gui/media/video_player.py @@ -491,6 +491,11 @@ class VideoPlayer(QWidget): # teardown and rejects the write, GL context destruction # still drops the surface pool eventually. pass + # Free the GL render context so its internal textures and FBOs + # release VRAM while no video is playing. The next play_file() + # call recreates the context via ensure_gl_init() (~5ms cost, + # swamped by the network fetch for uncached videos). + self._gl_widget.release_render_context() self._time_label.setText("0:00") self._duration_label.setText("0:00") self._seek_slider.setRange(0, 0)