Renderer: dispose live frames before re-alloc on source switch#704
Open
lrohrmann wants to merge 1 commit into
Open
Renderer: dispose live frames before re-alloc on source switch#704lrohrmann wants to merge 1 commit into
lrohrmann wants to merge 1 commit into
Conversation
Re-opening a source without a full Stop()/Renderer reset left the previous source's decoded frames alive. Frames that escape the VideoCache (switch/seek/screamer hand-off) were freed only by non-deterministic COM finalizers, keeping the D3D11 device referenced so the driver never reclaimed the native surface memory -> committed memory grew unbounded per switch (observed on Intel iGPU). Track every frame the renderer returns and dispose survivors deterministically in OpenInternal (before the new decoder allocates) and on teardown, then Flush so D3D11 destroys them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
Re-opening a source on a
Player(i.e. switching streams viaOpen/OpenAsyncwithout a full
Stop()/renderer reset) leaks native GPU memory. The process'scommitted memory grows on every switch and is only released when the device is
destroyed (Stop/dispose). On integrated Intel GPUs (where "VRAM" is system RAM) this
ratchets up until the machine runs out of commit charge.
Root cause
A
VideoFramecan leave theVideoCacheand never beDispose()d — e.g. the frameheld by the renderer/screamer at the moment of a source switch, seek, or hand-off.
VideoFramehas no finalizer, so the native objects it owns — the D3D11Texture/ShaderResourceView/VideoProcessorInputViewand the pinnedAVFrame—are released only when the COM wrappers are finalized by the GC.
Because the managed heap stays tiny during playback, Gen2 collections almost never
run, so those finalizers almost never run. Each switch therefore leaves the previous
source's frames (and their surfaces) committed while the new decoder allocates its
own → the concurrent peak is
old + new, and committed memory never recovers.A heap dump of a leaking process shows thousands of
VideoFrameandID3D11VideoProcessorInputViewinstances that are unrooted (no GC root) but whosenative resources were never freed, while only a handful of
ID3D11Texture2Dremain(HW frames share one texture array) — i.e. the managed wrappers are dead garbage and
the native memory leaked.
Reproduction (current
master)Player(e.g. theFlyleafPlayersample).Stop()between switches — e.g. alternate
player.OpenAsync(A)/player.OpenAsync(B)on a ~1s timer, or loop a playlist of a few items.
or VMMap "Private Committed"). Do not rely on the Working Set — it is trimmed
by the OS and hides the leak.
never drops while the player keeps running.
several megapixels) and on Intel integrated GPUs; hardware-decoded streams leak
more slowly but still ratchet up.
Stop()(full renderer reset) before eachOpenreleases the memory —confirming the leak is specific to the in-place re-open path.
Fix
Track every frame the renderer hands out through the single
FillPlaneschoke point,and dispose any survivors deterministically:
Player.OpenInternal, before the new decoder allocates (so the freedsurfaces are reused instead of accumulating), and
DisposeLocal).A
Flush()after disposal lets D3D11 actually destroy the released resources (thereis no
Presenton a paused/just-switched player to trigger deferred destruction).Changes
Renderer.Device.cs: per-renderer live-frame registry (TrackFrame/UntrackFrame/DisposeLiveFrames) +FlushContext();DisposeLiveFrames()is also called fromDisposeLocal.Renderer.VP.cs:FillPlanesbecomes a small wrapper around the (now private)fillPlanesdelegate that records each returned frame.VideoFrame: gains anOwnerback-reference and untracks itself onDispose().Player.Open.cs: dispose cached + escaped frames and flush beforedecoder.Open(...).No public API change (the affected members are
internal). The per-frame tracking isa
HashSetadd/remove guarded by a lock — negligible overhead on the frame path.Testing
Verified on an Intel integrated GPU (committed-memory growth is most pronounced there)
with a setup of several concurrent
Players, repeatedly switching one player's sourcebetween an H.264 RTSP stream (hardware-decoded) and large multi-megapixel MJPEG stills
(software-decoded).
Working Set): before the fix it rose by roughly one source's worth of surfaces on
every switch and never recovered (~tens of MB per switch with the large SW frames,
unbounded over ~30 switches). After the fix it stays bounded — it plateaus at roughly
a single source's peak instead of ratcheting up.
!dumpheap -stat -type VideoFrameand-type ID3D11VideoProcessorInputViewno longer show thousands ofaccumulating instances after repeated switches (they stay at a small steady count),
and
!gcrooton a sampled survivor confirms old frames are actually disposed ratherthan left as unrooted objects whose native resources were never released.
paths; the regression was reproducible and is resolved on both.
behave as before; no black frames or flicker introduced by the deterministic disposal