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
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## v0.5.0 (Unreleased)
## v0.5.1 (2026-05-27)

### Added

- **Auto-setup tool** — `go run github.com/go-webgpu/webgpu/cmd/setup@latest` downloads correct wgpu-native binary for your platform
- **setup package** — public API: `setup.Install(dir)`, `setup.FindLibrary()`, `setup.Version`
- **internal/nativelib** — platform detection, download, zip extraction with tests

### Changed

- **deps:** goffi v0.5.0 → v0.5.2

## v0.5.0 (2026-05-26)

### Breaking Changes
- **wgpu-native v29.0.0.0**: Migrated from v27.0.4.0 to v29.0.0.0 with stable webgpu-headers
Expand Down
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,34 @@ Pure Go WebGPU bindings using [goffi](https://github.com/go-webgpu/goffi) + [wgp
go get github.com/go-webgpu/webgpu
```

Download wgpu-native and place `wgpu_native.dll` (Windows) or `libwgpu_native.so` (Linux) in your project directory or system PATH.
### wgpu-native Setup (auto)

To use a custom library location:
```bash
export WGPU_NATIVE_PATH=/path/to/libwgpu_native.so
go run github.com/go-webgpu/webgpu/cmd/setup@latest
```

This downloads the correct wgpu-native v29 binary for your platform (Windows/macOS/Linux, amd64/arm64) into `./lib/`.

### wgpu-native Setup (manual)

Download from [gfx-rs/wgpu-native releases](https://github.com/gfx-rs/wgpu-native/releases/tag/v29.0.0.0) and place in your project directory or system PATH.

Custom library location:
```bash
export WGPU_NATIVE_PATH=/path/to/libwgpu_native.so # Linux/macOS
set WGPU_NATIVE_PATH=C:\path\to\wgpu_native.dll # Windows
```

## gogpu/wgpu Integration

This library is the **Rust FFI backend** for [gogpu/wgpu](https://github.com/gogpu/wgpu) — the unified Go WebGPU package. Build with `-tags rust` to use wgpu-native instead of the Pure Go implementation:

```bash
go build -tags rust ./myapp
```

Same API, same types, same user code — build tag selects the implementation.

## Type System

WebGPU types from [gputypes](https://github.com/gogpu/gputypes) are re-exported as type aliases in the `wgpu` package. A single import is sufficient:
Expand Down
26 changes: 26 additions & 0 deletions cmd/setup/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Command setup downloads and installs the wgpu-native library.
//
// Usage:
//
// go run github.com/go-webgpu/webgpu/cmd/setup@latest
// go run github.com/go-webgpu/webgpu/cmd/setup@latest ./path/to/lib
package main

import (
"fmt"
"os"

"github.com/go-webgpu/webgpu/setup"
)

func main() {
destDir := "lib"
if len(os.Args) > 1 {
destDir = os.Args[1]
}

if _, err := setup.Install(destDir); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ module github.com/go-webgpu/webgpu

go 1.25.0

require github.com/go-webgpu/goffi v0.5.0
require github.com/go-webgpu/goffi v0.5.2

require golang.org/x/sys v0.42.0

require github.com/gogpu/gputypes v0.3.0
require github.com/gogpu/gputypes v0.5.0
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
github.com/go-webgpu/goffi v0.5.0 h1:EuvVRiRn9qAfCkYYXbHs9gz8NY+zv2/OA1N7gi56UVE=
github.com/go-webgpu/goffi v0.5.0/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM=
github.com/gogpu/gputypes v0.3.0 h1:gcwxsBrcoCX19GqqqiV55wLv2iFwaybiOluKCb0hVrs=
github.com/gogpu/gputypes v0.3.0/go.mod h1:cnXrDMwTpWTvJLW1Vreop3PcT6a2YP/i3s91rPaOavw=
github.com/go-webgpu/goffi v0.5.2 h1:Kq2llPlA6IhkkmAffkM5CtsgaXuBQC4mBI0BOREwV04=
github.com/go-webgpu/goffi v0.5.2/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM=
github.com/gogpu/gputypes v0.5.0 h1:i2ED/9w6m6yLxf8XJT69/NIMSNTLO2y5F1LqvugCKIE=
github.com/gogpu/gputypes v0.5.0/go.mod h1:cnXrDMwTpWTvJLW1Vreop3PcT6a2YP/i3s91rPaOavw=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
71 changes: 71 additions & 0 deletions internal/nativelib/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package nativelib

import (
"archive/zip"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
)

func Download(url string) (string, error) {
resp, err := http.Get(url) //nolint:gosec // G107: URL constructed from constants
if err != nil {
return "", fmt.Errorf("download failed: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("download failed: HTTP %d from %s", resp.StatusCode, url)
}

tmpFile, err := os.CreateTemp("", "wgpu-native-*.zip")
if err != nil {
return "", fmt.Errorf("create temp file: %w", err)
}
defer tmpFile.Close()

if _, err := io.Copy(tmpFile, resp.Body); err != nil {
os.Remove(tmpFile.Name())
return "", fmt.Errorf("download write: %w", err)
}

return tmpFile.Name(), nil
}

func ExtractLibrary(zipPath, destDir, libName string) (string, error) {
r, err := zip.OpenReader(zipPath)
if err != nil {
return "", fmt.Errorf("open zip: %w", err)
}
defer r.Close()

for _, f := range r.File {
name := filepath.Base(f.Name)
if name != libName {
continue
}

src, err := f.Open()
if err != nil {
return "", fmt.Errorf("open %s in zip: %w", f.Name, err)
}
defer src.Close()

destPath := filepath.Join(destDir, libName)
dst, err := os.Create(destPath)
if err != nil {
return "", fmt.Errorf("create %s: %w", destPath, err)
}
defer dst.Close()

if _, err := io.Copy(dst, src); err != nil {
return "", fmt.Errorf("extract %s: %w", libName, err)
}

return destPath, nil
}

return "", fmt.Errorf("%s not found in archive", libName)
}
106 changes: 106 additions & 0 deletions internal/nativelib/download_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package nativelib

import (
"archive/zip"
"os"
"path/filepath"
"testing"
)

func TestExtractLibrary(t *testing.T) {
zipPath := createTestZip(t, "test.dll", []byte("fake dll content"))
destDir := t.TempDir()

path, err := ExtractLibrary(zipPath, destDir, "test.dll")
if err != nil {
t.Fatalf("ExtractLibrary() error: %v", err)
}

if filepath.Base(path) != "test.dll" {
t.Errorf("extracted path = %q, want test.dll basename", path)
}

data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read extracted file: %v", err)
}
if string(data) != "fake dll content" {
t.Errorf("content = %q, want %q", string(data), "fake dll content")
}
}

func TestExtractLibraryNestedPath(t *testing.T) {
zipPath := createTestZipNested(t, "lib/wgpu_native.dll", []byte("nested content"))
destDir := t.TempDir()

path, err := ExtractLibrary(zipPath, destDir, "wgpu_native.dll")
if err != nil {
t.Fatalf("ExtractLibrary() error: %v", err)
}

data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read: %v", err)
}
if string(data) != "nested content" {
t.Errorf("content = %q, want %q", string(data), "nested content")
}
}

func TestExtractLibraryNotFound(t *testing.T) {
zipPath := createTestZip(t, "other.dll", []byte("data"))
destDir := t.TempDir()

_, err := ExtractLibrary(zipPath, destDir, "missing.dll")
if err == nil {
t.Fatal("expected error for missing file in archive")
}
}

func TestExtractLibraryInvalidZip(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "bad.zip")
os.WriteFile(tmpFile, []byte("not a zip"), 0o644)

_, err := ExtractLibrary(tmpFile, t.TempDir(), "test.dll")
if err == nil {
t.Fatal("expected error for invalid zip")
}
}

func createTestZip(t *testing.T, name string, content []byte) string {
t.Helper()
path := filepath.Join(t.TempDir(), "test.zip")
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()

w := zip.NewWriter(f)
entry, err := w.Create(name)
if err != nil {
t.Fatal(err)
}
entry.Write(content)
w.Close()
return path
}

func createTestZipNested(t *testing.T, name string, content []byte) string {
t.Helper()
path := filepath.Join(t.TempDir(), "test.zip")
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()

w := zip.NewWriter(f)
entry, err := w.Create(name)
if err != nil {
t.Fatal(err)
}
entry.Write(content)
w.Close()
return path
}
111 changes: 111 additions & 0 deletions internal/nativelib/platform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package nativelib

import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)

const (
libWindows = "wgpu_native.dll"
libDarwin = "libwgpu_native.dylib"
libLinux = "libwgpu_native.so"
)

type Platform struct {
OS string
Arch string
LibName string
}

func DetectPlatform() (*Platform, error) {
p := &Platform{OS: runtime.GOOS, Arch: runtime.GOARCH}

switch p.OS {
case "windows":
p.LibName = libWindows
case "darwin":
p.LibName = libDarwin
case "linux":
p.LibName = libLinux
default:
return nil, fmt.Errorf("unsupported OS: %s", p.OS)
}

if p.Arch != "amd64" && p.Arch != "arm64" {
return nil, fmt.Errorf("unsupported architecture: %s", p.Arch)
}

return p, nil
}

func (p *Platform) archName() string {
if p.Arch == "amd64" {
return "x86_64"
}
return "aarch64"
}

func (p *Platform) ZipName() string {
arch := p.archName()
switch p.OS {
case "windows":
return fmt.Sprintf("wgpu-%s-%s-msvc-release.zip", p.OS, arch)
case "darwin":
return fmt.Sprintf("wgpu-macos-%s-release.zip", arch)
case "linux":
return fmt.Sprintf("wgpu-%s-%s-release.zip", p.OS, arch)
}
return ""
}

func (p *Platform) DownloadURL(version string) string {
return fmt.Sprintf("https://github.com/gfx-rs/wgpu-native/releases/download/%s/%s", version, p.ZipName())
}

func LibraryName() string {
switch runtime.GOOS {
case "windows":
return libWindows
case "darwin":
return libDarwin
default:
return libLinux
}
}

func FindLibrary() string {
if p := os.Getenv("WGPU_NATIVE_PATH"); p != "" {
if _, err := os.Stat(p); err == nil {
return p
}
}

libName := LibraryName()
searchPaths := []string{
filepath.Join(".", libName),
filepath.Join("lib", libName),
}

if exe, err := os.Executable(); err == nil {
searchPaths = append(searchPaths, filepath.Join(filepath.Dir(exe), libName))
}

for _, p := range searchPaths {
if _, err := os.Stat(p); err == nil {
abs, _ := filepath.Abs(p)
return abs
}
}

for _, dir := range strings.Split(os.Getenv("PATH"), string(os.PathListSeparator)) {
p := filepath.Join(dir, libName)
if _, err := os.Stat(p); err == nil {
return p
}
}

return ""
}
Loading
Loading