Skip to content

fix(core): release tracker index on Buffer.Destroy - #361

Open
arturonaredo wants to merge 1 commit into
gogpu:mainfrom
arturonaredo:fix/core-buffer-destroy-release-tracker-index
Open

arturonaredo wants to merge 1 commit into
gogpu:mainfrom
arturonaredo:fix/core-buffer-destroy-release-tracker-index

Conversation

@arturonaredo

Copy link
Copy Markdown

Summary

Buffer.Destroy() frees the HAL buffer but never releases its tracker spine index
(buffer.trackingData.Release()), so every destroyed buffer permanently consumes a
TrackerIndex. Downstream, BufferUsageScope.SetUsage sizes its states slice to
maxIndex+1, so total-slice capacity grows with the cumulative number of buffers
allocated 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 arrays
of ~4–10 MB each, growing as command buffers accumulate across frames).

Reproduction (library-only, backend-agnostic, noop backend works):

for i := 0; i < n; i++ {
    buf, _ := device.CreateBuffer(&wgpu.BufferDescriptor{Size: 16, Usage: wgpu.BufferUsageVertex})
    _ = buf.TrackingData() // index allocated via track.NewTrackingData
    buf.Release()          // refcount hits 0 -> Buffer.Destroy (no Release!)
}

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.Destroy is reached through three paths:

  1. the ResourceRef onZero callback installed at CreateBuffer
    (device_native.go) — fires only after DestroyQueue.Triage drops the
    in-flight clones for the owning submission, i.e. GPU completion;
  2. the GC safety net (runtime.AddCleanup), which also goes through
    Ref.Drop() → same completion rule;
  3. the legacy no-Ref path (legacy tests/direct construction).

By contract, every in-flight submission holds its own Clone(), so at refcount
zero 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.Destroy already 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; -race tests 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 buffer
    reuses the freed index (bounded growth).
  • TestBufferDestroyInFlightRetained — with a cloned (simulated in-flight)
    reference outstanding, Release() does not release the index; after the
    clone 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.4 without the fix and pass with it. Verified with
go test ./core/..., the module-root suite, and go test -race. I have not
run the device Mit pipeline for other backends locally for this change.

Notes

  • While auditing the readback path I noticed encodeSubmitReadback (in gg)
    submits a command buffer without tracking it or freeing the HAL buffer —
    reported separately in the gg repo; unrelated to this change.
  • Happy to iterate on reviewer feedback (e.g. alternative placement of the
    drain, or extending the same hygiene to other tracked resources if desired).

@arturonaredo
arturonaredo marked this pull request as ready for review September 29, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant