Skip to content
Merged
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
46 changes: 46 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,52 @@ env:
CGO_ENABLED: 0

jobs:
# Integration against canonical goffi and the exact merged WebGPU Android
# source. The ephemeral go.work injects the change until it is released.
android-arm64-preview:
name: Android arm64 preview - Go ${{ matrix.go-version }}
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.25.12', '1.26.5']

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Checkout exact merged WebGPU Android surface source
uses: actions/checkout@v4
with:
repository: go-webgpu/webgpu
ref: a801aed7399042e5564ef76fc9f075da5cb70081
path: .ci/webgpu
persist-credentials: false

- name: Verify exact WebGPU merge
working-directory: .ci/webgpu
run: test "$(git rev-parse HEAD)" = a801aed7399042e5564ef76fc9f075da5cb70081

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true

- name: Set up Android SDK
uses: android-actions/setup-android@v3

- name: Install Android NDK r29
run: sdkmanager 'ndk;29.0.14206865'

- name: Check Android arm64 source, tests, and ELF dependencies
env:
WEBGPU_DIR: ${{ github.workspace }}/.ci/webgpu
WEBGPU_EXPECTED_HEAD: a801aed7399042e5564ef76fc9f075da5cb70081
WEBGPU_EXPECTED_PATCH: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
run: |
export ANDROID_NDK_HOME="$ANDROID_SDK_ROOT/ndk/29.0.14206865"
./scripts/check-android-arm64-preview.sh

# Build verification - Cross-platform
build:
name: Build - ${{ matrix.os }}
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ wgpu (public API — Device, Queue, Buffer, Texture, Pipeline...)

## Current Version

v0.30.22 | Go 1.25+ | Dependencies: naga v0.17.15, gpucontext v0.21.1, gputypes v0.5.1, goffi v0.6.0, webgpu v0.5.3
v0.30.22 | Go 1.25+ | Dependencies: naga v0.17.15, gpucontext v0.21.1, gputypes v0.5.1, goffi v0.6.1, webgpu v0.5.3

## Build & Test

Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
immutable. Iterates all queue families — ahead of Rust wgpu which hardcodes
`queue_family_index = 0`. Contributor: @besmpl (#267).

- **Typed surface targets** — add retained-provider
`CreateSurfaceFromTarget` and explicit raw-handle `CreateSurfaceUnsafe`
paths modeled on Rust `wgpu` v29. The original two-`uintptr` method remains a
compatibility adapter across native, Rust, and browser implementations. The
native path creates one surface per successful enabled backend and qualifies
adapters against their matching backend surface, as Rust `wgpu` does.

- **Android raw surface target** — route `ANativeWindow*` explicitly through
public, HAL, Vulkan, and Rust-tag surface creation without cgo or Activity/JNI
policy in WGPU.

### Changed

- **Counted indirect draws** — added `RenderPassEncoder.MultiDrawIndirect` and
Expand Down
33 changes: 21 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
| Category | Capabilities |
|----------|--------------|
| **Backends** | Vulkan, Metal, DirectX 12, OpenGL ES, Software, **Browser WebGPU**, **Rust FFI** |
| **Platforms** | Windows, Linux, macOS, iOS, **Browser (WASM)** |
| **Platforms** | Windows, Linux, macOS, iOS, **Browser (WASM)**, **Android/arm64 (preview)** |
| **API** | WebGPU-compliant (W3C specification) |
| **Shaders** | WGSL via gogpu/naga compiler (SPIR-V, HLSL, MSL, GLSL, DXIL) |
| **Compute** | Full compute shader support, GPU→CPU readback |
Expand All @@ -56,6 +56,14 @@ CGO_ENABLED=0 go build

> **Note:** wgpu uses Pure Go FFI via [goffi](https://github.com/go-webgpu/goffi). Both `CGO_ENABLED=0` (default, zero C compiler dependency) and `CGO_ENABLED=1` (for race detector or coexistence with CGO libraries) are supported.

The unreleased Android/arm64 Vulkan implementation is documented separately in
[Android Vulkan preview](docs/ANDROID.md). It requires the exact canonical
goffi candidate named there and is not yet a released support claim.

New surface integrations should use the explicit safe or unsafe target API
described in [Surface targets](docs/SURFACE-TARGETS.md). The original
two-`uintptr` method remains available as a compatibility adapter.

**Rust FFI backend** (optional, battle-tested wgpu-native drivers):
```bash
go build -tags rust
Expand Down Expand Up @@ -217,7 +225,7 @@ wgpu/
│ ├── noop/ # No-op backend (testing)
│ ├── software/ # CPU software rasterizer (~14K LOC)
│ ├── gles/ # OpenGL ES 3.0+ (~12K LOC)
│ ├── vulkan/ # Vulkan 1.3 (~42K LOC)
│ ├── vulkan/ # Pure Go Vulkan backend (~42K LOC)
│ ├── metal/ # Metal (~7K LOC)
│ └── dx12/ # DirectX 12 (~17K LOC)
├── examples/
Expand Down Expand Up @@ -252,7 +260,8 @@ import _ "github.com/gogpu/wgpu/hal/allbackends"
// Platform-specific backends auto-registered:
// - Windows: Vulkan, DX12, GLES, Software
// - Linux: Vulkan, GLES, Software
// - macOS: Metal, Software
// - macOS: Metal, Vulkan, Software
// - Android/arm64 preview: Vulkan only
```

---
Expand All @@ -261,19 +270,19 @@ import _ "github.com/gogpu/wgpu/hal/allbackends"

### Platform Support

| Backend | Windows | Linux | macOS | iOS | Notes |
|---------|:-------:|:-----:|:-----:|:---:|-------|
| **Vulkan** | Yes | Yes | Yes | - | MoltenVK on macOS |
| **Metal** | - | - | Yes | Yes | Native Apple GPU |
| **DX12** | Yes | - | - | - | Windows 10+ |
| **GLES** | Yes | Yes | - | - | OpenGL ES 3.0+ |
| **Software** | Yes | Yes | Yes | Yes | CPU fallback |
| Backend | Windows | Linux | macOS | iOS | Android/arm64 | Notes |
|---------|:-------:|:-----:|:-----:|:---:|:-------------:|-------|
| **Vulkan** | Yes | Yes | Yes | - | Preview | MoltenVK on macOS; [Android contract](docs/ANDROID.md) |
| **Metal** | - | - | Yes | Yes | - | Native Apple GPU |
| **DX12** | Yes | - | - | - | - | Windows 10+ |
| **GLES** | Yes | Yes | - | - | - | OpenGL ES 3.0+ |
| **Software** | Yes | Yes | Yes | Yes | - | CPU fallback |

**Architectures:** amd64, arm64 (including Windows ARM64 / Snapdragon X)

### Vulkan Backend

Full Vulkan 1.3 implementation with:
Pure Go Vulkan backend with:

- Auto-generated bindings from official `vk.xml`
- Buddy allocator for GPU memory (O(log n), minimal fragmentation)
Expand All @@ -282,7 +291,7 @@ Full Vulkan 1.3 implementation with:
- wgpu-style swapchain synchronization
- MSAA render pass with automatic resolve
- Complete resource management (Buffer, Texture, Pipeline, BindGroup)
- Surface creation: Win32, X11, Wayland, Metal (MoltenVK)
- Surface creation: Win32, X11, Wayland, Metal (MoltenVK), and Android `ANativeWindow` (preview)
- Debug messenger for validation layer error capture (`VK_EXT_debug_utils`)
- Structured diagnostic logging via `log/slog`

Expand Down
2 changes: 1 addition & 1 deletion adapter_native.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func (a *Adapter) GetSurfaceCapabilities(surface *Surface) *SurfaceCapabilities
}
}

halSurface := surface.HAL()
halSurface := surface.halSurfaceForBackend(a.info.Backend)
if halSurface == nil {
return nil
}
Expand Down
6 changes: 5 additions & 1 deletion cmd/gles-test/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"
"unsafe"

"github.com/gogpu/wgpu/hal"
"github.com/gogpu/wgpu/hal/gles"
"github.com/gogpu/wgpu/hal/gles/gl"
"github.com/gogpu/wgpu/hal/gles/wgl"
Expand Down Expand Up @@ -302,7 +303,10 @@ func testGLESBackend() error {

// Test 3: Create surface
fmt.Print(" Creating surface... ")
surface, err := instance.CreateSurface(0, hwnd)
surface, err := instance.CreateSurface(hal.SurfaceTarget{
Kind: hal.SurfaceTargetWindowsHWND,
WindowHandle: hwnd,
})
if err != nil {
return fmt.Errorf("CreateSurface: %w", err)
}
Expand Down
6 changes: 5 additions & 1 deletion cmd/vk-test/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"syscall"
"unsafe"

"github.com/gogpu/wgpu/hal"
"github.com/gogpu/wgpu/hal/vulkan"
"github.com/gogpu/wgpu/hal/vulkan/vk"
)
Expand Down Expand Up @@ -162,7 +163,10 @@ func testVulkanBackend() error {

// Test 3: Create surface
fmt.Print(" Creating surface... ")
surface, err := instance.CreateSurface(0, hwnd)
surface, err := instance.CreateSurface(hal.SurfaceTarget{
Kind: hal.SurfaceTargetWindowsHWND,
WindowHandle: hwnd,
})
if err != nil {
return fmt.Errorf("CreateSurface: %w", err)
}
Expand Down
5 changes: 4 additions & 1 deletion cmd/vulkan-triangle/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,10 @@ func initGPU(window *Window) (*gpuResources, error) {

// Create surface
fmt.Print("5. Creating surface... ")
surface, err := instance.CreateSurface(0, window.Handle())
surface, err := instance.CreateSurface(hal.SurfaceTarget{
Kind: hal.SurfaceTargetWindowsHWND,
WindowHandle: window.Handle(),
})
if err != nil {
return nil, fmt.Errorf("creating surface: %w", err)
}
Expand Down
4 changes: 3 additions & 1 deletion core/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,9 @@ func (b *testHALBackend) CreateInstance(_ *hal.InstanceDescriptor) (hal.Instance

type testHALInstance struct{}

func (i *testHALInstance) CreateSurface(_, _ uintptr) (hal.Surface, error) { return nil, nil } //nolint:nilnil
func (i *testHALInstance) CreateSurface(_ hal.SurfaceTarget) (hal.Surface, error) {
return nil, hal.ErrUnsupportedSurfaceTarget
}
func (i *testHALInstance) EnumerateAdapters(_ hal.Surface) []hal.ExposedAdapter {
return nil
}
Expand Down
55 changes: 54 additions & 1 deletion core/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ type Instance struct {
// halInstances tracks HAL instances created for each backend.
// These are destroyed when the Instance is destroyed.
halInstances []hal.Instance
// halInstanceEntries preserves backend priority alongside each HAL instance.
// Surface creation uses this ordered view to mirror Rust wgpu's attempt to
// create a raw surface for every enabled backend.
halInstanceEntries []HALInstanceEntry

// halInstanceMap maps backend type to its HAL instance for backend-specific
// surface creation. When a device uses a different backend than the initially
Expand All @@ -61,6 +65,13 @@ type Instance struct {
useMock bool
}

// HALInstanceEntry associates an enabled backend with its HAL instance.
// Entries are ordered by the same backend priority used during discovery.
type HALInstanceEntry struct {
Backend gputypes.Backend
Instance hal.Instance
}

// 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
Expand Down Expand Up @@ -169,13 +180,21 @@ func (i *Instance) enumerateRealAdapters(desc *gputypes.InstanceDescriptor) {
// Lock() call creates the GL context on the render thread via sync.Once.
if provider.Variant() == gputypes.BackendGL {
i.halInstances = append(i.halInstances, halInstance)
i.halInstanceEntries = append(i.halInstanceEntries, HALInstanceEntry{
Backend: provider.Variant(),
Instance: halInstance,
})
i.halInstanceMap[provider.Variant()] = halInstance
i.deferredGLES = append(i.deferredGLES, halInstance)
continue
}

// Track HAL instance for cleanup
i.halInstances = append(i.halInstances, halInstance)
i.halInstanceEntries = append(i.halInstanceEntries, HALInstanceEntry{
Backend: provider.Variant(),
Instance: halInstance,
})
i.halInstanceMap[provider.Variant()] = halInstance

// Enumerate adapters from this backend
Expand Down Expand Up @@ -372,6 +391,24 @@ func (i *Instance) RequestAdapterWithSurface(options *gputypes.RequestAdapterOpt
if surfaceHint == nil {
return i.RequestAdapter(options)
}
return i.requestAdapterWithSurfaceResolver(options, func(gputypes.Backend) hal.Surface {
return surfaceHint
})
}

// RequestAdapterWithSurfaces requests an adapter using the surface created for
// each adapter's own backend. This mirrors Rust wgpu's per-backend surface map
// while preserving RequestAdapterWithSurface for callers that own one HAL.
func (i *Instance) RequestAdapterWithSurfaces(options *gputypes.RequestAdapterOptions, surfaces map[gputypes.Backend]hal.Surface) (AdapterID, error) {
if len(surfaces) == 0 {
return i.RequestAdapter(options)
}
return i.requestAdapterWithSurfaceResolver(options, func(backend gputypes.Backend) hal.Surface {
return surfaces[backend]
})
}

func (i *Instance) requestAdapterWithSurfaceResolver(options *gputypes.RequestAdapterOptions, surfaceForBackend func(gputypes.Backend) hal.Surface) (AdapterID, error) {
i.surfaceRequestMu.Lock()
defer i.surfaceRequestMu.Unlock()

Expand All @@ -388,7 +425,9 @@ func (i *Instance) RequestAdapterWithSurface(options *gputypes.RequestAdapterOpt

// 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)
if glesSurface := surfaceForBackend(gputypes.BackendGL); glesSurface != nil {
i.enumerateDeferredGLES(glesSurface)
}

i.mu.RLock()
allAdapterIDs := append([]AdapterID(nil), i.adapters...)
Expand All @@ -405,6 +444,10 @@ func (i *Instance) RequestAdapterWithSurface(options *gputypes.RequestAdapterOpt
if err != nil {
continue
}
surfaceHint := surfaceForBackend(adapter.Backend)
if surfaceHint == nil {
continue
}

// A newly enumerated adapter was produced with the surface hint. Keep
// that backend-owned adapter as-is; this preserves existing GLES
Expand Down Expand Up @@ -598,6 +641,15 @@ func (i *Instance) HALInstance() hal.Instance {
return nil
}

// HALInstanceEntries returns enabled HAL instances in backend-priority order.
// The returned slice is a snapshot and may be inspected without holding the
// Instance lock.
func (i *Instance) HALInstanceEntries() []HALInstanceEntry {
i.mu.RLock()
defer i.mu.RUnlock()
return append([]HALInstanceEntry(nil), i.halInstanceEntries...)
}

// HALInstanceForBackend returns the HAL instance for a specific backend type.
// Returns nil if no instance exists for the given backend.
// Used to re-create surfaces when the device's backend differs from the
Expand Down Expand Up @@ -666,5 +718,6 @@ func (i *Instance) Destroy() {
halInstance.Destroy()
}
i.halInstances = nil
i.halInstanceEntries = nil
i.deferredGLES = nil
}
25 changes: 23 additions & 2 deletions core/instance_gles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type trackingGLESInstance struct {
halAdapter hal.Adapter // non-nil ⇒ returns a real ExposedAdapter
}

func (i *trackingGLESInstance) CreateSurface(_, _ uintptr) (hal.Surface, error) {
func (i *trackingGLESInstance) CreateSurface(_ hal.SurfaceTarget) (hal.Surface, error) {
return nil, nil //nolint:nilnil
}
func (i *trackingGLESInstance) EnumerateAdapters(hint hal.Surface) []hal.ExposedAdapter {
Expand All @@ -68,7 +68,7 @@ func (i *trackingGLESInstance) Destroy() {}

type countingGLESInstance struct{ callCount int }

func (i *countingGLESInstance) CreateSurface(_, _ uintptr) (hal.Surface, error) {
func (i *countingGLESInstance) CreateSurface(_ hal.SurfaceTarget) (hal.Surface, error) {
return nil, nil //nolint:nilnil
}
func (i *countingGLESInstance) EnumerateAdapters(_ hal.Surface) []hal.ExposedAdapter {
Expand Down Expand Up @@ -97,6 +97,27 @@ func TestRequestAdapterWithSurface_PassesHintToEnumerateAdapters(t *testing.T) {
}
}

func TestRequestAdapterWithSurfacesPassesMatchingBackendHint(t *testing.T) {
GetGlobal().Clear()

tracker := &trackingGLESInstance{halAdapter: &stubHALAdapter{}}
instance := &Instance{
backends: gputypes.BackendsAll,
deferredGLES: []hal.Instance{tracker},
}
vulkanHint := hal.Surface(&stubHALSurface{id: 1})
glHint := hal.Surface(&stubHALSurface{id: 2})

_, _ = instance.RequestAdapterWithSurfaces(nil, map[gputypes.Backend]hal.Surface{
gputypes.BackendVulkan: vulkanHint,
gputypes.BackendGL: glHint,
})

if tracker.receivedHint != glHint {
t.Errorf("GLES EnumerateAdapters received hint %v, want matching GL hint %v", tracker.receivedHint, glHint)
}
}

// TestRequestAdapterWithSurface_SelectsGLESAdapter verifies that the GLES
// adapter exposed by EnumerateAdapters(hint) is the one RequestAdapter returns.
func TestRequestAdapterWithSurface_SelectsGLESAdapter(t *testing.T) {
Expand Down
Loading
Loading