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
29 changes: 23 additions & 6 deletions adapter_native.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,27 @@ func (a *Adapter) Limits() Limits { return a.limits }
// RequestDevice creates a logical device from this adapter.
// If desc is nil, default features and limits are used.
func (a *Adapter) RequestDevice(desc *DeviceDescriptor) (*Device, error) {
if a.released {
if a == nil || a.released || a.instance == nil || a.instance.isReleased() {
return nil, ErrReleased
}

var (
device *Device
err error
)
if a.core.HasHAL() {
return a.requestDeviceHAL(desc)
device, err = a.requestDeviceHAL(desc)
} else {
device, err = a.requestDeviceCore(desc)
}

return a.requestDeviceCore(desc)
if err != nil {
return nil, err
}
if err := a.instance.adoptDevice(device); err != nil {
device.Release()
return nil, err
}
return device, nil
}

func (a *Adapter) requestDeviceHAL(desc *DeviceDescriptor) (*Device, error) {
Expand Down Expand Up @@ -143,7 +155,8 @@ type SurfaceCapabilities struct {
// GetSurfaceCapabilities returns the capabilities of a surface for this adapter.
// Returns nil if the adapter has no HAL (core-only path) or the surface is nil.
func (a *Adapter) GetSurfaceCapabilities(surface *Surface) *SurfaceCapabilities {
if a.released || surface == nil {
if a == nil || a.released || a.instance == nil || a.instance.isReleased() ||
surface == nil || surface.released || surface.instance != a.instance {
return nil
}

Expand All @@ -154,7 +167,11 @@ func (a *Adapter) GetSurfaceCapabilities(surface *Surface) *SurfaceCapabilities
}
}

halCaps := a.core.HALAdapter().SurfaceCapabilities(surface.HAL())
halSurface := surface.HAL()
if halSurface == nil {
return nil
}
halCaps := a.core.HALAdapter().SurfaceCapabilities(halSurface)
if halCaps == nil {
return nil
}
Expand Down
18 changes: 18 additions & 0 deletions core/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -1564,6 +1564,12 @@ type Surface struct {
// acquiredTex is the currently acquired surface texture (nil when not acquired).
acquiredTex hal.SurfaceTexture

// acquisition identifies the currently borrowed surface texture. A zero
// value means no acquisition is valid; nextAcquisition prevents an older
// wrapper from becoming valid after a later acquire.
acquisition uint64
nextAcquisition uint64

// prepareFrame is an optional platform hook called before acquiring a texture.
prepareFrame PrepareFrameFunc

Expand Down Expand Up @@ -1594,6 +1600,11 @@ func NewSurface(

// RawSurface returns the underlying HAL surface.
func (s *Surface) RawSurface() hal.Surface {
if s == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
return s.raw
}

Expand All @@ -1602,5 +1613,12 @@ func (s *Surface) RawSurface() hal.Surface {
// instance (e.g., switching from Vulkan surface to software surface during
// Configure when the device's backend differs from the original surface).
func (s *Surface) SetRawSurface(raw hal.Surface) {
s.mu.Lock()
defer s.mu.Unlock()
s.raw = raw
s.acquiredTex = nil
s.invalidateAcquisitionLocked()
s.device = nil
s.config = nil
s.state = SurfaceStateUnconfigured
}
102 changes: 97 additions & 5 deletions core/surface.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"image"
"unsafe"

"github.com/gogpu/wgpu/hal"
)
Expand Down Expand Up @@ -77,6 +78,7 @@ func (s *Surface) Configure(device *Device, config *hal.SurfaceConfiguration) er
return err
}

s.invalidateAcquisitionLocked()
s.device = device
s.config = config
s.state = SurfaceStateConfigured
Expand All @@ -100,6 +102,7 @@ func (s *Surface) Unconfigure() {
s.raw.DiscardTexture(s.acquiredTex)
s.acquiredTex = nil
}
s.invalidateAcquisitionLocked()

halDevice := s.getHALDevice(s.device)
if halDevice != nil {
Expand All @@ -120,29 +123,53 @@ func (s *Surface) Unconfigure() {
// After a successful acquire, the surface enters the Acquired state.
// The caller must either Present or DiscardTexture before acquiring again.
func (s *Surface) AcquireTexture(fence hal.Fence) (*hal.AcquiredSurfaceTexture, error) {
result, _, err := s.AcquireTextureWithLease(fence)
return result, err
}

// AcquireTextureWithLease acquires a texture and returns its opaque lifetime
// lease. Call AcquisitionValid before converting a retained public wrapper to
// HAL; the lease expires on present, discard, unconfigure, or destruction.
func (s *Surface) AcquireTextureWithLease(fence hal.Fence) (*hal.AcquiredSurfaceTexture, uint64, error) {
s.mu.Lock()
defer s.mu.Unlock()

if s.state == SurfaceStateAcquired {
return nil, ErrSurfaceAlreadyAcquired
return nil, 0, ErrSurfaceAlreadyAcquired
}
if s.state != SurfaceStateConfigured {
return nil, ErrSurfaceNotConfigured
return nil, 0, ErrSurfaceNotConfigured
}

// Call PrepareFrame hook if registered
if err := s.applyPrepareFrame(); err != nil {
return nil, err
return nil, 0, err
}

result, err := s.raw.AcquireTexture(fence)
if err != nil {
return nil, err
return nil, 0, err
}

s.acquiredTex = result.Texture
s.nextAcquisition++
if s.nextAcquisition == 0 {
s.nextAcquisition++
}
s.acquisition = s.nextAcquisition
s.state = SurfaceStateAcquired
return result, nil
return result, s.acquisition, nil
}

// AcquisitionValid reports whether lease still identifies this surface's
// current acquired texture.
func (s *Surface) AcquisitionValid(lease uint64) bool {
if s == nil || lease == 0 {
return false
}
s.mu.Lock()
defer s.mu.Unlock()
return s.state == SurfaceStateAcquired && s.acquisition == lease
}

// Present presents the acquired surface texture to the screen.
Expand Down Expand Up @@ -173,6 +200,7 @@ func (s *Surface) PresentWithDamage(queue hal.Queue, damageRects []image.Rectang

err := queue.Present(s.raw, s.acquiredTex, damageRects)
s.acquiredTex = nil
s.invalidateAcquisitionLocked()
s.state = SurfaceStateConfigured
return err
}
Expand Down Expand Up @@ -212,6 +240,7 @@ func (s *Surface) PresentPixels(data []byte, width, height uint32, damageRects [
if s.state == SurfaceStateAcquired && s.acquiredTex != nil {
s.raw.DiscardTexture(s.acquiredTex)
s.acquiredTex = nil
s.invalidateAcquisitionLocked()
s.state = SurfaceStateConfigured
}

Expand All @@ -235,9 +264,72 @@ func (s *Surface) DiscardTexture() {
}

s.acquiredTex = nil
s.invalidateAcquisitionLocked()
s.state = SurfaceStateConfigured
}

func (s *Surface) invalidateAcquisitionLocked() {
s.acquisition = 0
}

// RetireDevice clears the logical configuration after its HAL device has been
// destroyed. The public API calls this after backend device teardown so a
// retained surface can be released or configured with another device without
// retaining a valid acquisition from the old device.
func (s *Surface) RetireDevice(device *Device) {
if s == nil || device == nil {
return
}
s.mu.Lock()
if s.device != device {
s.mu.Unlock()
return
}
s.acquiredTex = nil
s.invalidateAcquisitionLocked()
s.device = nil
s.config = nil
s.state = SurfaceStateUnconfigured
s.mu.Unlock()
}

// Destroy invalidates the active acquisition and releases the owned HAL
// surface. It deliberately does not call DiscardTexture: device-first instance
// teardown may already have retired the backend swapchain.
func (s *Surface) Destroy() {
if s == nil {
return
}
s.mu.Lock()
raw := s.raw
if raw == nil {
s.mu.Unlock()
return
}
acquiredTex := s.acquiredTex
halDevice := s.getHALDevice(s.device)
s.raw = nil
s.acquiredTex = nil
s.invalidateAcquisitionLocked()
s.device = nil
s.config = nil
s.state = SurfaceStateUnconfigured
s.mu.Unlock()

// When the configured device is still alive, retire the current acquisition
// and configuration before destroying the platform surface. Device-first
// teardown leaves no HAL device here, so the backend has already retired (or
// abandoned) its device-owned children and only the platform handle remains.
if acquiredTex != nil {
raw.DiscardTexture(acquiredTex)
}
if halDevice != nil {
raw.Unconfigure(halDevice)
}
raw.Destroy()
untrackResource(uintptr(unsafe.Pointer(s))) //nolint:gosec // debug tracking uses pointer as unique ID
}

// State returns the current lifecycle state of the surface.
func (s *Surface) State() SurfaceState {
s.mu.Lock()
Expand Down
84 changes: 84 additions & 0 deletions core/surface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,90 @@ func TestSurfaceAcquirePresent(t *testing.T) {
}
}

func TestSurfaceAcquisitionLeaseExpiresAtLifecycleBoundaries(t *testing.T) {
surface, device, queue := newTestSurface(t)
if err := surface.Configure(device, testSurfaceConfig()); err != nil {
t.Fatalf("Configure: %v", err)
}

_, first, err := surface.AcquireTextureWithLease(nil)
if err != nil {
t.Fatalf("first acquire: %v", err)
}
if !surface.AcquisitionValid(first) {
t.Fatal("first lease was not valid after acquire")
}
if err := surface.Present(queue); err != nil {
t.Fatalf("Present: %v", err)
}
if surface.AcquisitionValid(first) {
t.Fatal("presented lease remained valid")
}

_, second, err := surface.AcquireTextureWithLease(nil)
if err != nil {
t.Fatalf("second acquire: %v", err)
}
if second == first || !surface.AcquisitionValid(second) {
t.Fatalf("second lease = %d, first = %d, valid = %v", second, first, surface.AcquisitionValid(second))
}
surface.DiscardTexture()
if surface.AcquisitionValid(second) {
t.Fatal("discarded lease remained valid")
}

_, third, err := surface.AcquireTextureWithLease(nil)
if err != nil {
t.Fatalf("third acquire: %v", err)
}
surface.Unconfigure()
if surface.AcquisitionValid(third) {
t.Fatal("unconfigured lease remained valid")
}

if err := surface.Configure(device, testSurfaceConfig()); err != nil {
t.Fatalf("reconfigure: %v", err)
}
_, fourth, err := surface.AcquireTextureWithLease(nil)
if err != nil {
t.Fatalf("fourth acquire: %v", err)
}
surface.Destroy()
if surface.AcquisitionValid(fourth) {
t.Fatal("destroyed lease remained valid")
}
}

type surfaceDestroyObserver struct {
noop.Surface
discards int
unconfigures int
destroys int
}

func (s *surfaceDestroyObserver) DiscardTexture(hal.SurfaceTexture) { s.discards++ }
func (s *surfaceDestroyObserver) Unconfigure(hal.Device) { s.unconfigures++ }
func (s *surfaceDestroyObserver) Destroy() { s.destroys++ }

func TestSurfaceDestroyRetiresAcquisitionBeforePlatformSurface(t *testing.T) {
_, device, _ := newTestSurface(t)
raw := &surfaceDestroyObserver{}
surface := NewSurface(raw, "destroy-order-test")
if err := surface.Configure(device, testSurfaceConfig()); err != nil {
t.Fatalf("Configure: %v", err)
}
if _, _, err := surface.AcquireTextureWithLease(nil); err != nil {
t.Fatalf("AcquireTextureWithLease: %v", err)
}

surface.Destroy()
surface.Destroy()
if raw.discards != 1 || raw.unconfigures != 1 || raw.destroys != 1 {
t.Fatalf("destroy calls = discard:%d unconfigure:%d destroy:%d; want 1,1,1",
raw.discards, raw.unconfigures, raw.destroys)
}
}

func TestSurfaceDoubleAcquire(t *testing.T) {
surface, device, _ := newTestSurface(t)
config := testSurfaceConfig()
Expand Down
Loading