Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions device_native.go
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,14 @@ func (d *Device) WaitIdle() error {
if d.released.Load() {
return ErrReleased
}
return d.waitIdle()
}

// waitIdle drains the HAL and deferred resource queues without consulting the
// public released bit. Release marks the device unavailable before starting
// teardown, but the native device must remain alive while its last submission
// is drained.
func (d *Device) waitIdle() error {
halDevice := d.halDevice()
if halDevice == nil {
return ErrReleased
Expand Down Expand Up @@ -915,14 +923,19 @@ func (d *Device) Release() {
}
d.released.Store(true)

if d.queue != nil {
d.queue.release()
}

// Step 0: Wait for ALL GPU work to finish. This ensures PollCompleted()
// returns the final submission index, so Triage processes all submissions
// and deferred encoder recycling callbacks fire correctly.
_ = d.WaitIdle()
// Use the internal path because the public released guard is already set to
// reject new work during teardown.
_ = d.waitIdle()

// Pending writes may own staging buffers and encoders. Destroy them only
// after the HAL has reported idle so no in-flight submission can reference
// resources that are being torn down.
if d.queue != nil {
d.queue.release()
}

// Step 1: Flush deferred destructions. With GPU idle, Triage processes
// all submissions. Encoder recycling callbacks fire, returning encoders
Expand Down
62 changes: 62 additions & 0 deletions device_release_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//go:build !rust && !(js && wasm)

package wgpu

import (
"testing"

"github.com/gogpu/gputypes"
"github.com/gogpu/wgpu/core"
"github.com/gogpu/wgpu/hal"
"github.com/gogpu/wgpu/hal/noop"
)

// releaseTrackingDevice embeds a real HAL device and records only the two
// lifecycle calls whose ordering is required by Device.Release. Embedding keeps
// this test seam local without expanding the HAL or public wgpu interfaces.
type releaseTrackingDevice struct {
hal.Device
events *[]string
}

func (d *releaseTrackingDevice) WaitIdle() error {
*d.events = append(*d.events, "wait-idle")
return d.Device.WaitIdle()
}

func (d *releaseTrackingDevice) Destroy() {
*d.events = append(*d.events, "destroy")
d.Device.Destroy()
}

func TestDeviceReleaseWaitsBeforeHALDestroy(t *testing.T) {
instance, err := (noop.API{}).CreateInstance(nil)
if err != nil {
t.Fatalf("noop CreateInstance: %v", err)
}
defer instance.Destroy()

adapters := instance.EnumerateAdapters(nil)
if len(adapters) != 1 {
t.Fatalf("noop adapter count = %d, want 1", len(adapters))
}
limits := gputypes.DefaultLimits()
opened, err := adapters[0].Adapter.Open(0, limits)
if err != nil {
t.Fatalf("open noop adapter: %v", err)
}

events := make([]string, 0, 2)
tracked := &releaseTrackingDevice{Device: opened.Device, events: &events}
device := &Device{
core: core.NewDevice(tracked, nil, 0, limits, "release-order-test"),
queue: &Queue{hal: opened.Queue, halDevice: tracked},
}
device.queue.device = device

device.Release()

if len(events) != 2 || events[0] != "wait-idle" || events[1] != "destroy" {
t.Fatalf("HAL lifecycle events = %v, want [wait-idle destroy]", events)
}
}