fix(core): release tracker index on Buffer.Destroy - #361
Open
arturonaredo wants to merge 1 commit into
Open
arturonaredo wants to merge 1 commit into
arturonaredo wants to merge 1 commit into
Conversation
arturonaredo
marked this pull request as ready for review
September 29, 2026 07:42
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
Buffer.Destroy()frees the HAL buffer but never releases its tracker spine index(
buffer.trackingData.Release()), so every destroyed buffer permanently consumes aTrackerIndex. Downstream,BufferUsageScope.SetUsagesizes itsstatesslice tomaxIndex+1, so total-slice capacity grows with the cumulative number of buffersallocated over the process lifetime instead of the peak concurrent population.
Observed in practice: a long-lived desktop application that creates and destroys
thousands of short-lived buffers per frame reached multiple GiB of live heap,
with Go CPU profiles attributing 97–98% of in-use space to
gogpu/wgpu/core/track.(*BufferUsageScope).SetUsage(dozens of backing arraysof ~4–10 MB each, growing as command buffers accumulate across frames).
Reproduction (library-only, backend-agnostic,
noopbackend works):With the current code, after N iterations
device.Tracker().Buffers().Size() == N;with this change it stays at the number of live buffers.
Why releasing here is safe
Buffer.Destroyis reached through three paths:ResourceRefonZero callback installed atCreateBuffer(
device_native.go) — fires only afterDestroyQueue.Triagedrops thein-flight clones for the owning submission, i.e. GPU completion;
runtime.AddCleanup), which also goes throughRef.Drop()→ same completion rule;Refpath (legacy tests/direct construction).By contract, every in-flight submission holds its own
Clone(), so at refcountzero the GPU has finished with the resource. Releasing the tracker index at that
point cannot free anything still in use — the same invariant the existing
Texture.Destroyalready exercises.The device tracker slot for the index is drained via
tracker.Buffers().Remove(idx)before the index is returned, so a recycled index starts untracked and its
first post-recycle submit inserts fresh state instead of synthesizing a transition
against the previous buffer's last usage. Allocator vs tracker operations are
separately mutex-guarded;
-racetests included.Testing
New
buffer_destroy_tracker_lifecycle_test.go(exercise the public lifecycle —prior tests called
td.Release()manually, which masked this omission):TestBufferDestroyReleasesTrackerIndex— create → use →Release()(onZero→ Destroy) →
TrackingData().IsReleased()== true; a newly allocated bufferreuses the freed index (bounded growth).
TestBufferDestroyInFlightRetained— with a cloned (simulated in-flight)reference outstanding,
Release()does not release the index; after theclone is dropped (Triage-equivalent at completion) the index is released. Pins
both directions: no premature free while in flight, no retention after
completion.
Both fail on
v0.31.4without the fix and pass with it. Verified withgo test ./core/..., the module-root suite, andgo test -race. I have notrun the device Mit pipeline for other backends locally for this change.
Notes
encodeSubmitReadback(ingg)submits a command buffer without tracking it or freeing the HAL buffer —
reported separately in the gg repo; unrelated to this change.
drain, or extending the same hygiene to other tracked resources if desired).