From 5fa2b14475ab65e799ffcc2b6a46048622181226 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 15 Jul 2026 17:48:40 +0300 Subject: [PATCH 1/3] fix(core): require explicit mock adapter construction --- core/adapter_test.go | 16 ++--- core/backend.go | 7 +-- core/backend_test.go | 3 +- core/hal_integration_test.go | 9 +-- core/instance.go | 34 ++++------- core/instance_test.go | 109 ++++++++++++++++++++++++++++++++--- hal/allbackends/doc.go | 6 +- hal/allbackends/register.go | 17 ------ integration_test.go | 6 +- wgpu_test.go | 13 ++--- 10 files changed, 139 insertions(+), 81 deletions(-) delete mode 100644 hal/allbackends/register.go diff --git a/core/adapter_test.go b/core/adapter_test.go index e5906379..bdac0fb0 100644 --- a/core/adapter_test.go +++ b/core/adapter_test.go @@ -11,7 +11,7 @@ import ( func TestGetAdapterInfo(t *testing.T) { GetGlobal().Clear() - instance := NewInstance(nil) + instance := NewInstanceWithMock(nil) adapters := instance.EnumerateAdapters() if len(adapters) == 0 { t.Fatal("no adapters available") @@ -49,7 +49,7 @@ func TestGetAdapterInfoInvalid(t *testing.T) { func TestGetAdapterFeatures(t *testing.T) { GetGlobal().Clear() - instance := NewInstance(nil) + instance := NewInstanceWithMock(nil) adapters := instance.EnumerateAdapters() if len(adapters) == 0 { t.Fatal("no adapters available") @@ -80,7 +80,7 @@ func TestGetAdapterFeaturesInvalid(t *testing.T) { func TestGetAdapterLimits(t *testing.T) { GetGlobal().Clear() - instance := NewInstance(nil) + instance := NewInstanceWithMock(nil) adapters := instance.EnumerateAdapters() if len(adapters) == 0 { t.Fatal("no adapters available") @@ -154,7 +154,7 @@ func TestRequestDevice(t *testing.T) { t.Run(tt.name, func(t *testing.T) { GetGlobal().Clear() - instance := NewInstance(nil) + instance := NewInstanceWithMock(nil) adapters := instance.EnumerateAdapters() if len(adapters) == 0 { t.Fatal("no adapters available") @@ -202,7 +202,7 @@ func TestRequestDeviceInvalidAdapter(t *testing.T) { func TestAdapterDrop(t *testing.T) { GetGlobal().Clear() - instance := NewInstance(nil) + instance := NewInstanceWithMock(nil) adapters := instance.EnumerateAdapters() if len(adapters) == 0 { t.Fatal("no adapters available") @@ -244,7 +244,7 @@ func TestAdapterLifecycle(t *testing.T) { GetGlobal().Clear() // 1. Create instance - instance := NewInstance(nil) + instance := NewInstanceWithMock(nil) // 2. Request adapter adapterID, err := instance.RequestAdapter(nil) @@ -298,7 +298,7 @@ func TestAdapterLifecycle(t *testing.T) { func TestAdapterConcurrentAccess(t *testing.T) { GetGlobal().Clear() - instance := NewInstance(nil) + instance := NewInstanceWithMock(nil) adapterID, err := instance.RequestAdapter(nil) if err != nil { t.Fatalf("RequestAdapter() error: %v", err) @@ -324,7 +324,7 @@ func TestAdapterConcurrentAccess(t *testing.T) { func TestRequestDeviceFeatureValidation(t *testing.T) { GetGlobal().Clear() - instance := NewInstance(nil) + instance := NewInstanceWithMock(nil) adapterID, err := instance.RequestAdapter(nil) if err != nil { t.Fatalf("RequestAdapter() error: %v", err) diff --git a/core/backend.go b/core/backend.go index 46ebe5f6..ac171b52 100644 --- a/core/backend.go +++ b/core/backend.go @@ -66,7 +66,7 @@ var ( gputypes.BackendMetal, gputypes.BackendDX12, gputypes.BackendGL, - gputypes.BackendEmpty, // noop/software fallback + gputypes.BackendEmpty, // explicitly registered software/noop provider } ) @@ -182,9 +182,8 @@ func FilterBackendsByMask(mask gputypes.Backends) []BackendProvider { result = append(result, p) } case gputypes.BackendEmpty: - // Software/noop backend included as fallback for all masks. - // Adapter selection (RequestAdapter) prefers GPU adapters over CPU; - // software only wins if ForceFallbackAdapter is set or no GPU available. + // The software/noop provider is selectable when explicitly registered; + // NewInstance never fabricates an adapter when it is absent. result = append(result, p) default: // Unknown backend types pass through if Primary is set diff --git a/core/backend_test.go b/core/backend_test.go index 9bd5b27c..4b5a7d35 100644 --- a/core/backend_test.go +++ b/core/backend_test.go @@ -13,11 +13,12 @@ import ( type testProvider struct { variant gputypes.Backend available bool + instance hal.Instance } func (p *testProvider) Variant() gputypes.Backend { return p.variant } func (p *testProvider) CreateInstance(_ *hal.InstanceDescriptor) (hal.Instance, error) { - return nil, nil //nolint:nilnil + return p.instance, nil } func (p *testProvider) IsAvailable() bool { return p.available } diff --git a/core/hal_integration_test.go b/core/hal_integration_test.go index dee2d52f..529f042d 100644 --- a/core/hal_integration_test.go +++ b/core/hal_integration_test.go @@ -44,12 +44,9 @@ func TestCoreHALIntegration(t *testing.T) { } defer instance.Destroy() - // Check if we're using real adapters or mock - if instance.IsMock() { - t.Log("Instance is using mock adapters (no GPU available)") - } else { - t.Log("Instance is using real HAL adapters") - } + // NewInstance never fabricates mock adapters. Any adapters returned here + // therefore came from an explicitly registered HAL provider. + t.Log("Instance is using registered HAL adapters") // Enumerate adapters adapterIDs := instance.EnumerateAdapters() diff --git a/core/instance.go b/core/instance.go index 3e65c3b9..ebf635df 100644 --- a/core/instance.go +++ b/core/instance.go @@ -46,7 +46,8 @@ type Instance struct { // glesEnumerated tracks whether deferred GLES adapters have been enumerated. glesEnumerated bool - // useMock indicates whether to use mock adapters (for testing or when no HAL available). + // useMock indicates whether this instance was explicitly created with mock + // adapters through NewInstanceWithMock. useMock bool } @@ -54,9 +55,9 @@ type Instance struct { // If desc is nil, default settings are used. // // The instance will enumerate available GPU adapters based on the enabled -// backends specified in the descriptor. If HAL backends are available, -// real GPU adapters will be enumerated. Otherwise, a mock adapter is created -// for testing purposes. +// backends specified in the descriptor. If no provider is available, the +// instance remains empty and RequestAdapter reports the failure. Tests that +// need a deterministic adapter must opt in through NewInstanceWithMock. func NewInstance(desc *gputypes.InstanceDescriptor) *Instance { if desc == nil { defaultDesc := gputypes.DefaultInstanceDescriptor() @@ -73,13 +74,7 @@ func NewInstance(desc *gputypes.InstanceDescriptor) *Instance { } // Try to enumerate real adapters via HAL backends - realAdaptersFound := i.enumerateRealAdapters(desc) - - // Fall back to mock adapter if no real adapters were found - if !realAdaptersFound { - i.useMock = true - i.createMockAdapter() - } + i.enumerateRealAdapters(desc) trackResource(uintptr(unsafe.Pointer(i)), "Instance") //nolint:gosec // debug tracking uses pointer as unique ID return i @@ -107,19 +102,15 @@ func NewInstanceWithMock(desc *gputypes.InstanceDescriptor) *Instance { return i } -// enumerateRealAdapters attempts to enumerate real GPU adapters via HAL backends. -// Returns true if at least one real adapter was found. -func (i *Instance) enumerateRealAdapters(desc *gputypes.InstanceDescriptor) bool { +// enumerateRealAdapters attempts to enumerate real GPU adapters via HAL +// backends. If none are available, the instance remains empty. +func (i *Instance) enumerateRealAdapters(desc *gputypes.InstanceDescriptor) { // First, ensure HAL backends are registered RegisterHALBackends() // Get backend providers filtered by the enabled backends mask providers := FilterBackendsByMask(desc.Backends) - if len(providers) == 0 { - return false - } - foundAdapters := false hub := GetGlobal().Hub() // Create HAL descriptor @@ -186,11 +177,8 @@ func (i *Instance) enumerateRealAdapters(desc *gputypes.InstanceDescriptor) bool // Register in the hub adapterID := hub.RegisterAdapter(adapter) i.adapters = append(i.adapters, adapterID) - foundAdapters = true } } - - return foundAdapters } // createMockAdapter creates a mock adapter for testing purposes. @@ -473,8 +461,8 @@ func (i *Instance) Flags() gputypes.InstanceFlags { } // IsMock returns true if the instance is using mock adapters. -// Mock adapters are used when no HAL backends are available or -// when the instance was explicitly created with NewInstanceWithMock. +// Mock adapters are used only when the instance was explicitly created with +// NewInstanceWithMock. func (i *Instance) IsMock() bool { i.mu.RLock() defer i.mu.RUnlock() diff --git a/core/instance_test.go b/core/instance_test.go index dd0d3419..8852f44a 100644 --- a/core/instance_test.go +++ b/core/instance_test.go @@ -3,11 +3,49 @@ package core import ( + "errors" "testing" "github.com/gogpu/gputypes" + "github.com/gogpu/wgpu/hal" ) +type providerBackedTestInstance struct{} + +func (*providerBackedTestInstance) CreateSurface(_, _ uintptr) (hal.Surface, error) { + return nil, errors.New("test instance does not create surfaces") +} + +func (*providerBackedTestInstance) EnumerateAdapters(hal.Surface) []hal.ExposedAdapter { + return []hal.ExposedAdapter{{ + Adapter: &providerBackedTestAdapter{}, + Info: gputypes.AdapterInfo{ + Name: "provider-backed test adapter", + DeviceType: gputypes.DeviceTypeCPU, + Backend: gputypes.BackendVulkan, + }, + Capabilities: hal.Capabilities{Limits: gputypes.DefaultLimits()}, + }} +} + +func (*providerBackedTestInstance) Destroy() {} + +type providerBackedTestAdapter struct{} + +func (*providerBackedTestAdapter) Open(gputypes.Features, gputypes.Limits) (hal.OpenDevice, error) { + return hal.OpenDevice{}, nil +} + +func (*providerBackedTestAdapter) TextureFormatCapabilities(gputypes.TextureFormat) hal.TextureFormatCapabilities { + return hal.TextureFormatCapabilities{} +} + +func (*providerBackedTestAdapter) SurfaceCapabilities(hal.Surface) *hal.SurfaceCapabilities { + return nil +} + +func (*providerBackedTestAdapter) Destroy() {} + func TestNewInstance(t *testing.T) { tests := []struct { name string @@ -52,11 +90,9 @@ func TestNewInstance(t *testing.T) { t.Errorf("Backends() = %v, want %v", got, tt.want) } - // Verify mock adapter was created - adapters := instance.EnumerateAdapters() - if len(adapters) == 0 { - t.Error("Expected at least one mock adapter") - } + // Adapter discovery is provider-dependent. A missing provider must not + // fabricate a mock adapter; deterministic mock coverage uses the + // explicit NewInstanceWithMock constructor. }) } } @@ -102,10 +138,65 @@ func TestInstanceFlags(t *testing.T) { } } +func TestNewInstanceDoesNotFabricateAdapterWithoutProvider(t *testing.T) { + GetGlobal().Clear() + + instance := NewInstance(&gputypes.InstanceDescriptor{}) + if adapters := instance.EnumerateAdapters(); len(adapters) != 0 { + t.Fatalf("NewInstance fabricated %d adapter(s) without an enabled provider", len(adapters)) + } + if instance.IsMock() { + t.Fatal("NewInstance unexpectedly enabled mock mode") + } + if _, err := instance.RequestAdapter(nil); err == nil { + t.Fatal("RequestAdapter succeeded without a provider") + } +} + +func TestNewInstanceUsesRegisteredProviderWithoutEnablingMock(t *testing.T) { + providersMu.Lock() + savedProviders := providers + providers = map[gputypes.Backend]BackendProvider{ + gputypes.BackendVulkan: &testProvider{ + variant: gputypes.BackendVulkan, + available: true, + instance: &providerBackedTestInstance{}, + }, + } + providersMu.Unlock() + t.Cleanup(func() { + providersMu.Lock() + providers = savedProviders + providersMu.Unlock() + }) + GetGlobal().Clear() + + instance := NewInstance(&gputypes.InstanceDescriptor{Backends: gputypes.BackendsVulkan}) + t.Cleanup(instance.Destroy) + if instance.IsMock() { + t.Fatal("provider-backed NewInstance unexpectedly enabled mock mode") + } + if adapters := instance.EnumerateAdapters(); len(adapters) != 1 { + t.Fatalf("provider-backed NewInstance returned %d adapters, want 1", len(adapters)) + } +} + +func TestNewInstanceWithMockIsExplicit(t *testing.T) { + GetGlobal().Clear() + + instance := NewInstanceWithMock(nil) + if !instance.IsMock() { + t.Fatal("NewInstanceWithMock did not enable mock mode") + } + if adapters := instance.EnumerateAdapters(); len(adapters) != 1 { + t.Fatalf("NewInstanceWithMock returned %d adapters, want 1", len(adapters)) + } +} + func TestEnumerateAdapters(t *testing.T) { GetGlobal().Clear() - instance := NewInstance(nil) + instance := NewInstanceWithMock(nil) adapters := instance.EnumerateAdapters() if len(adapters) == 0 { @@ -173,7 +264,7 @@ func TestRequestAdapter(t *testing.T) { t.Run(tt.name, func(t *testing.T) { GetGlobal().Clear() - instance := NewInstance(nil) + instance := NewInstanceWithMock(nil) adapterID, err := instance.RequestAdapter(tt.options) if tt.wantErr { @@ -205,7 +296,7 @@ func TestRequestAdapter(t *testing.T) { func TestRequestAdapterNoAdapters(t *testing.T) { GetGlobal().Clear() - // Create instance but remove mock adapter + // An instance with no registered adapters must fail explicitly. instance := &Instance{ backends: gputypes.BackendsPrimary, flags: 0, @@ -277,7 +368,7 @@ func TestMatchesPowerPreference(t *testing.T) { func TestInstanceConcurrentAccess(t *testing.T) { GetGlobal().Clear() - instance := NewInstance(nil) + instance := NewInstanceWithMock(nil) // Test concurrent reads done := make(chan bool, 10) diff --git a/hal/allbackends/doc.go b/hal/allbackends/doc.go index 17260e25..a6ff8be5 100644 --- a/hal/allbackends/doc.go +++ b/hal/allbackends/doc.go @@ -16,14 +16,16 @@ // - Metal backend (macOS, iOS) // - DX12 backend (Windows) // - OpenGL ES backend (Windows, Linux) -// - No-op backend (all platforms, for testing) // // After importing, use hal.GetBackend or hal.SelectBestBackend to access backends. // // Build tags control which backends are available: // - Default: All backends for the current platform // - "!android": Excludes Android-specific Vulkan loader -// - "software": Includes software rasterizer backend +// +// The software and no-op providers are not registered by this package. Import +// github.com/gogpu/wgpu/hal/software or hal/noop explicitly when those +// providers are required. // // Example usage: // diff --git a/hal/allbackends/register.go b/hal/allbackends/register.go deleted file mode 100644 index 51880a99..00000000 --- a/hal/allbackends/register.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build !(js && wasm) - -// Copyright 2025 The GoGPU Authors -// SPDX-License-Identifier: MIT - -package allbackends - -import ( - // Import all HAL backends for side-effect registration. - // Each backend's init() function registers it with hal.RegisterBackend(). - - // Software backend - CPU-based renderer, always available as fallback. - // Note: noop backend is NOT included here — it's for testing only and - // should be imported explicitly when needed. Both noop and software - // register as BackendEmpty, so only one can be active at a time. - _ "github.com/gogpu/wgpu/hal/software" -) diff --git a/integration_test.go b/integration_test.go index 296abe0e..cc1b41d3 100644 --- a/integration_test.go +++ b/integration_test.go @@ -46,13 +46,13 @@ func createTestDevice(t *testing.T) (*wgpu.Instance, *wgpu.Adapter, *wgpu.Device t.Skipf("cannot request device: %v", err) } - // Check that the device has actual HAL integration (not a mock adapter). - // Mock adapters have no queue and cannot create GPU resources. + // Check that the device has actual HAL integration. A provider-less + // instance fails RequestAdapter rather than manufacturing a mock device. if device.Queue() == nil { device.Release() adapter.Release() instance.Release() - t.Skip("skipping: device has no HAL integration (mock adapter; no GPU backend available)") + t.Skip("skipping: device has no HAL integration (no GPU backend available)") } return instance, adapter, device diff --git a/wgpu_test.go b/wgpu_test.go index 8ca789f0..0fb5cbca 100644 --- a/wgpu_test.go +++ b/wgpu_test.go @@ -9,10 +9,9 @@ import ( "github.com/gogpu/gputypes" "github.com/gogpu/wgpu" - // Import noop backend. Note: the noop backend (BackendEmpty) is skipped by - // core.Instance during real adapter enumeration. A mock adapter is created - // instead. Tests that require HAL integration (CreateBuffer, CreateTexture, - // CreateShaderModule, etc.) are skipped when running on mock devices. + // Import noop backend. The noop backend is intentionally ignored during real + // adapter enumeration; tests that require HAL integration use an explicitly + // registered real backend when one is available. _ "github.com/gogpu/wgpu/hal/noop" ) @@ -59,13 +58,11 @@ func newDevice(t *testing.T) (*wgpu.Instance, *wgpu.Adapter, *wgpu.Device) { return inst, adapter, device } -// requireHAL skips the test if the device was created via the mock adapter path -// (no HAL integration). The mock path is used when no real GPU backends are -// available, which is common in CI and headless environments. +// requireHAL skips the test when no real HAL provider supplied the device. func requireHAL(t *testing.T, device *wgpu.Device) { t.Helper() if device.Queue() == nil { - t.Skip("skipping: device has no HAL integration (mock adapter; no real GPU backend available)") + t.Skip("skipping: device has no HAL integration (no real GPU backend available)") } } From e65bec3c74c56cd07265c055ce6a7ec7fbedc680 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 15 Jul 2026 19:44:03 +0300 Subject: [PATCH 2/3] fix(core): preserve explicit software backend registration --- hal/allbackends/register.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 hal/allbackends/register.go diff --git a/hal/allbackends/register.go b/hal/allbackends/register.go new file mode 100644 index 00000000..51880a99 --- /dev/null +++ b/hal/allbackends/register.go @@ -0,0 +1,17 @@ +//go:build !(js && wasm) + +// Copyright 2025 The GoGPU Authors +// SPDX-License-Identifier: MIT + +package allbackends + +import ( + // Import all HAL backends for side-effect registration. + // Each backend's init() function registers it with hal.RegisterBackend(). + + // Software backend - CPU-based renderer, always available as fallback. + // Note: noop backend is NOT included here — it's for testing only and + // should be imported explicitly when needed. Both noop and software + // register as BackendEmpty, so only one can be active at a time. + _ "github.com/gogpu/wgpu/hal/software" +) From 3e6c836891c794cf2c92a962f3ff87f2b81171fe Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 22 Jul 2026 09:19:37 +0300 Subject: [PATCH 3/3] fix(core): align backend docs and mock cleanup --- core/instance.go | 41 ----------------------------------------- hal/allbackends/doc.go | 6 +++--- 2 files changed, 3 insertions(+), 44 deletions(-) diff --git a/core/instance.go b/core/instance.go index ebf635df..6efafa87 100644 --- a/core/instance.go +++ b/core/instance.go @@ -389,47 +389,6 @@ func (i *Instance) enumerateDeferredGLES(surfaceHint hal.Surface) { // Clear deferred list -- enumeration is done. i.deferredGLES = nil - - // If we were in mock mode and now have real adapters from GLES, - // remove mock adapters so real ones are selected first. - if i.useMock && i.hasRealAdaptersLocked(hub) { - i.useMock = false - i.removeMockAdaptersLocked(hub) - } -} - -// hasRealAdaptersLocked checks if any adapter has a non-nil HAL adapter. -// Caller must hold i.mu. -func (i *Instance) hasRealAdaptersLocked(hub *Hub) bool { - for _, adapterID := range i.adapters { - adapter, err := hub.GetAdapter(adapterID) - if err != nil { - continue - } - if adapter.halAdapter != nil { - return true - } - } - return false -} - -// removeMockAdaptersLocked filters out mock adapters (halAdapter == nil) from -// the adapter list and unregisters them from the hub. -// Caller must hold i.mu. -func (i *Instance) removeMockAdaptersLocked(hub *Hub) { - filtered := make([]AdapterID, 0, len(i.adapters)) - for _, adapterID := range i.adapters { - adapter, err := hub.GetAdapter(adapterID) - if err != nil { - continue - } - if adapter.halAdapter != nil { - filtered = append(filtered, adapterID) - } else { - _, _ = hub.UnregisterAdapter(adapterID) - } - } - i.adapters = filtered } // matchesPowerPreference checks if a device type matches the power preference. diff --git a/hal/allbackends/doc.go b/hal/allbackends/doc.go index a6ff8be5..9d03b707 100644 --- a/hal/allbackends/doc.go +++ b/hal/allbackends/doc.go @@ -16,6 +16,7 @@ // - Metal backend (macOS, iOS) // - DX12 backend (Windows) // - OpenGL ES backend (Windows, Linux) +// - Software backend (all supported native platforms) // // After importing, use hal.GetBackend or hal.SelectBestBackend to access backends. // @@ -23,9 +24,8 @@ // - Default: All backends for the current platform // - "!android": Excludes Android-specific Vulkan loader // -// The software and no-op providers are not registered by this package. Import -// github.com/gogpu/wgpu/hal/software or hal/noop explicitly when those -// providers are required. +// The no-op provider is not registered by this package. Import +// github.com/gogpu/wgpu/hal/noop explicitly when it is required for tests. // // Example usage: //