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
5 changes: 4 additions & 1 deletion adapter_native.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,11 @@ func (a *Adapter) GetSurfaceCapabilities(surface *Surface) *SurfaceCapabilities

// Release releases the adapter.
func (a *Adapter) Release() {
if a.released {
if a == nil || a.released {
return
}
a.released = true
if a.instance != nil && a.instance.core != nil {
a.instance.core.ReleaseSurfaceAdapter(a.id)
}
}
197 changes: 181 additions & 16 deletions core/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,22 @@ import (
// Thread-safe for concurrent use.
type Instance struct {
mu sync.RWMutex
// surfaceRequestMu serializes surface-aware adapter requests. Deferred GLES
// enumeration is intentionally one-shot, so the snapshot that distinguishes
// adapters created for the current surface must be taken atomically with that
// enumeration.
surfaceRequestMu sync.Mutex
backends gputypes.Backends
flags gputypes.InstanceFlags

// adapters contains the registered adapter IDs.
adapters []AdapterID

// surfaceAdapters contains request-local adapters created for a compatible
// surface. They are intentionally kept out of adapters so ordinary adapter
// enumeration and selection never retain surface-bound queue state.
surfaceAdapters []AdapterID

// halInstances tracks HAL instances created for each backend.
// These are destroyed when the Instance is destroyed.
halInstances []hal.Instance
Expand All @@ -50,6 +60,14 @@ type Instance struct {
useMock bool
}

// surfaceAdapterQualifier is an optional backend capability. It intentionally
// stays private to core so the stable HAL Adapter interface does not grow a
// surface-specific method. Vulkan implements the method on its concrete
// adapter and returns a request-local wrapper with queue/surface state.
type surfaceAdapterQualifier interface {
QualifySurface(surface hal.Surface) (hal.Adapter, error)
}

// NewInstance creates a new WebGPU instance with the given descriptor.
// If desc is nil, default settings are used.
//
Expand Down Expand Up @@ -257,16 +275,26 @@ func (i *Instance) RequestAdapter(options *gputypes.RequestAdapterOptions) (Adap
i.enumerateDeferredGLES(nil)

i.mu.RLock()
defer i.mu.RUnlock()
adapterIDs := append([]AdapterID(nil), i.adapters...)
i.mu.RUnlock()

return selectAdapterIDs(options, adapterIDs)
}

if len(i.adapters) == 0 {
// selectAdapterIDs applies the public adapter selection policy to an explicit
// candidate list. Keeping the policy independent from Instance state lets a
// surface request select request-local adapters without exposing unqualified
// cached adapters to the selection pass.
func selectAdapterIDs(options *gputypes.RequestAdapterOptions, adapterIDs []AdapterID) (AdapterID, error) { //nolint:gocognit // adapter selection with GPU preference logic
if len(adapterIDs) == 0 {
return AdapterID{}, fmt.Errorf("no adapters available")
}

hub := GetGlobal().Hub()

// If no options specified, prefer non-CPU adapters (GPU > Software fallback).
if options == nil {
hub := GetGlobal().Hub()
for _, adapterID := range i.adapters {
for _, adapterID := range adapterIDs {
adapter, err := hub.GetAdapter(adapterID)
if err != nil {
continue
Expand All @@ -275,14 +303,12 @@ func (i *Instance) RequestAdapter(options *gputypes.RequestAdapterOptions) (Adap
return adapterID, nil
}
}
return i.adapters[0], nil // fallback to first (Software)
return adapterIDs[0], nil // fallback to first (Software)
}

hub := GetGlobal().Hub()

// ForceFallbackAdapter: return first CPU adapter directly
// ForceFallbackAdapter: return first CPU adapter directly.
if options.ForceFallbackAdapter {
for _, adapterID := range i.adapters {
for _, adapterID := range adapterIDs {
adapter, err := hub.GetAdapter(adapterID)
if err != nil {
continue
Expand All @@ -304,7 +330,7 @@ func (i *Instance) RequestAdapter(options *gputypes.RequestAdapterOptions) (Adap
hasGPUFallback := false

// Pass 1: find preferred adapter + track fallbacks.
for _, adapterID := range i.adapters {
for _, adapterID := range adapterIDs {
adapter, err := hub.GetAdapter(adapterID)
if err != nil {
continue
Expand All @@ -318,13 +344,13 @@ func (i *Instance) RequestAdapter(options *gputypes.RequestAdapterOptions) (Adap
continue
}

// Track first non-CPU adapter as GPU fallback
// Track first non-CPU adapter as GPU fallback.
if !hasGPUFallback {
gpuFallback = adapterID
hasGPUFallback = true
}

// Check power preference — return immediately on exact match
// Check power preference — return immediately on exact match.
if options.PowerPreference != gputypes.PowerPreferenceNone {
if matchesPowerPreference(adapter.Info.DeviceType, options.PowerPreference) {
return adapterID, nil
Expand Down Expand Up @@ -355,11 +381,133 @@ func (i *Instance) RequestAdapter(options *gputypes.RequestAdapterOptions) (Adap
// a GL context, which is required before EnumerateAdapters can return a real
// adapter. Calling without a surface yields a zero-value Adapter (nil glCtx).
func (i *Instance) RequestAdapterWithSurface(options *gputypes.RequestAdapterOptions, surfaceHint hal.Surface) (AdapterID, error) {
// Run deferred GLES enumeration with the real surface hint BEFORE RequestAdapter
// calls enumerateDeferredGLES(nil). Once glesEnumerated is true the nil call
// inside RequestAdapter becomes a no-op, so the surface-backed adapters win.
if surfaceHint == nil {
return i.RequestAdapter(options)
}
i.surfaceRequestMu.Lock()
defer i.surfaceRequestMu.Unlock()

// Remember which adapters existed before deferred enumeration. Adapters
// created by EnumerateAdapters(surfaceHint) are already surface-qualified by
// their backend and can be used directly. Cached adapters are qualified via
// the optional HAL SurfaceAdapter interface below.
i.mu.RLock()
before := make(map[AdapterID]struct{}, len(i.adapters))
for _, adapterID := range i.adapters {
before[adapterID] = struct{}{}
}
i.mu.RUnlock()

// Run deferred GLES enumeration with the real surface hint before collecting
// candidates. Once glesEnumerated is true this is a no-op on later calls.
i.enumerateDeferredGLES(surfaceHint)
return i.RequestAdapter(options)

i.mu.RLock()
allAdapterIDs := append([]AdapterID(nil), i.adapters...)
i.mu.RUnlock()

hub := GetGlobal().Hub()
i.mu.RLock()
usingMock := i.useMock
i.mu.RUnlock()
candidates := make([]AdapterID, 0, len(allAdapterIDs))
qualifiedIDs := make([]AdapterID, 0, len(allAdapterIDs))
for _, adapterID := range allAdapterIDs {
adapter, err := hub.GetAdapter(adapterID)
if err != nil {
continue
}

// A newly enumerated adapter was produced with the surface hint. Keep
// that backend-owned adapter as-is; this preserves existing GLES
// enumeration behavior and avoids a second context query.
if _, isNew := before[adapterID]; !isNew {
candidates = append(candidates, adapterID)
continue
}

if adapter.halAdapter == nil {
if usingMock {
// Explicit mock instances have no native surface query. Preserve
// their established behavior instead of turning a test adapter
// request into an unexpected compatibility error.
candidates = append(candidates, adapterID)
}
continue
}

if qualifier, ok := adapter.halAdapter.(surfaceAdapterQualifier); ok {
qualified, err := qualifier.QualifySurface(surfaceHint)
if err != nil || qualified == nil {
continue
}

// Register a request-local core adapter. The cached adapter and its
// HAL object are left untouched; only the wrapper carries the queue
// family and checked surface snapshot into Open().
qualifiedCore := adapter
qualifiedCore.halAdapter = qualified
qualifiedID := hub.RegisterAdapter(&qualifiedCore)
i.mu.Lock()
i.surfaceAdapters = append(i.surfaceAdapters, qualifiedID)
i.mu.Unlock()
candidates = append(candidates, qualifiedID)
qualifiedIDs = append(qualifiedIDs, qualifiedID)
continue
}

// Backends without a request-local qualifier may still expose a
// checked surface capability query. Do not treat a nil result as
// compatible, and never fall back to an unqualified Vulkan adapter.
if adapter.halAdapter.SurfaceCapabilities(surfaceHint) != nil {
candidates = append(candidates, adapterID)
}
}

if len(candidates) == 0 {
return AdapterID{}, fmt.Errorf("no adapters compatible with surface")
}
selectedID, err := selectAdapterIDs(options, candidates)
for _, qualifiedID := range qualifiedIDs {
if err != nil || qualifiedID != selectedID {
i.ReleaseSurfaceAdapter(qualifiedID)
}
}
return selectedID, err
}

// ReleaseSurfaceAdapter releases a request-local adapter created by
// RequestAdapterWithSurface. Ordinary cached adapter IDs are instance-owned and
// intentionally ignored, so public Adapter.Release can use this method without
// special-casing the selected backend.
func (i *Instance) ReleaseSurfaceAdapter(id AdapterID) {
if i == nil {
return
}
i.mu.Lock()
defer i.mu.Unlock()

index := -1
for candidateIndex, candidateID := range i.surfaceAdapters {
if candidateID == id {
index = candidateIndex
break
}
}
if index < 0 {
return
}

hub := GetGlobal().Hub()
if adapter, err := hub.GetAdapter(id); err == nil {
if adapter.halAdapter != nil {
adapter.halAdapter.Destroy()
}
_, _ = hub.UnregisterAdapter(id)
}
copy(i.surfaceAdapters[index:], i.surfaceAdapters[index+1:])
i.surfaceAdapters[len(i.surfaceAdapters)-1] = AdapterID{}
i.surfaceAdapters = i.surfaceAdapters[:len(i.surfaceAdapters)-1]
}

// enumerateDeferredGLES enumerates adapters for deferred GLES HAL instances.
Expand Down Expand Up @@ -521,6 +669,9 @@ func (i *Instance) HALInstanceMap() map[gputypes.Backend]hal.Instance {
// This includes unregistering all adapters and destroying HAL instances.
// After calling Destroy, the instance should not be used.
func (i *Instance) Destroy() {
i.surfaceRequestMu.Lock()
defer i.surfaceRequestMu.Unlock()

i.mu.Lock()
defer i.mu.Unlock()

Expand All @@ -545,6 +696,20 @@ func (i *Instance) Destroy() {
}
i.adapters = nil

// Request-local surface adapters are separate from the ordinary adapter
// list, but still belong to this instance and must be released here.
for _, adapterID := range i.surfaceAdapters {
adapter, err := hub.GetAdapter(adapterID)
if err != nil {
continue
}
if adapter.halAdapter != nil {
adapter.halAdapter.Destroy()
}
_, _ = hub.UnregisterAdapter(adapterID)
}
i.surfaceAdapters = nil

// Destroy all HAL instances (includes deferred GLES instances).
for _, halInstance := range i.halInstances {
halInstance.Destroy()
Expand Down
Loading