diff --git a/device_native.go b/device_native.go index b7cca90..62d44f3 100644 --- a/device_native.go +++ b/device_native.go @@ -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 @@ -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 diff --git a/device_release_test.go b/device_release_test.go new file mode 100644 index 0000000..27c3771 --- /dev/null +++ b/device_release_test.go @@ -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) + } +}