Thank you for considering contributing to go-webgpu! This document outlines the development workflow and guidelines.
All changes to main branch must go through Pull Requests. The main branch is protected.
main # Protected. Production-ready code (tagged releases)
├─ feat/* # New features
├─ fix/* # Bug fixes
├─ deps/* # Dependency updates
├─ docs/* # Documentation
└─ hotfix/* # Critical fixes
- main is protected — no direct pushes allowed
- All changes require a Pull Request
- Admins can bypass protection for emergency fixes
# Create feature branch from main
git checkout main
git pull origin main
git checkout -b feat/my-new-feature
# Work on your feature...
git add .
git commit -m "feat: add my new feature"
# Push branch and create PR
git push -u origin feat/my-new-feature
gh pr create --title "feat: add my new feature" --body "Description..."
# After PR is merged, clean up
git checkout main
git pull origin main
git branch -d feat/my-new-feature# Create fix branch from main
git checkout main
git pull origin main
git checkout -b fix/issue-123
# Fix the bug...
git add .
git commit -m "fix: resolve issue #123"
# Push and create PR
git push -u origin fix/issue-123
gh pr create --title "fix: resolve issue #123" --body "Closes #123"# After PR is merged, create release from main
git checkout main
git pull origin main
# Create tag and release
git tag -a v0.2.0 -m "Release v0.2.0"
git push origin v0.2.0
gh release create v0.2.0 --title "v0.2.0" --notes "Release notes..."Follow Conventional Commits specification:
<type>(<scope>): <description>
[optional body]
[optional footer]
- feat: New feature
- fix: Bug fix
- docs: Documentation changes
- deps: Dependency updates
- style: Code style changes (formatting, etc.)
- refactor: Code refactoring
- test: Adding or updating tests
- chore: Maintenance tasks (build, CI, etc.)
- perf: Performance improvements
feat: add Texture3D support
fix: correct buffer mapping alignment
docs: update README with compute shader example
deps: update goffi v0.3.1 → v0.3.3
refactor: simplify device creation flow
test: add render pipeline tests
chore: update CI workflow
perf: optimize command encoder batch submission-
Format code:
go fmt ./...
-
Run linter:
golangci-lint run
-
Run tests:
go test ./wgpu/... -
All-in-one (use pre-release script):
bash scripts/pre-release-check.sh
- Code is formatted (
go fmt ./...) - Linter passes (
golangci-lint run- 0 issues) - All tests pass (
go test ./wgpu/...) - New code has tests (minimum 70% coverage)
- Documentation updated (if applicable)
- Commit messages follow conventions
- No sensitive data (credentials, tokens, etc.)
- Examples updated for new features
- Go 1.25 or later
- golangci-lint v2
- wgpu-native v29.0.0.0 shared libraries (download script provided)
| Platform | Requirements |
|---|---|
| Windows | Go + golangci-lint |
| Linux | Go + golangci-lint (CGO_ENABLED=0) |
| macOS | Go + golangci-lint (CGO_ENABLED=0, Intel x86_64 only) |
# Clone repository
git clone https://github.com/go-webgpu/webgpu.git
cd webgpu
# Download wgpu-native libraries
bash scripts/download-wgpu-native.sh
# Download dependencies
go mod download
# Install golangci-lint
# See: https://golangci-lint.run/welcome/install/# Run all tests
go test ./wgpu/...
# Run with coverage
go test -cover ./wgpu/...
# Run specific test
go test -v ./wgpu/... -run "TestDeviceCreation"
# Run benchmarks
go test -bench=. -benchmem ./wgpu/...Tests are organized into three tiers:
| Tier | Filter | GPU required | Runs in CI |
|---|---|---|---|
| ABI tests | -run "TestABI" |
No | Yes |
| Safe tests | -run "Mat4|Vec3|StructSizes|CheckInit|WGPUError|Fuzz|NullGuard" |
No | Yes |
| GPU tests | -run "TestAdapter|TestDevice|TestBuffer|TestSurface|TestLeak|TestErrorScope" |
Yes | No |
abi_test.go contains 271 assertions that verify Go wire struct sizes and field offsets match the C ABI defined in wgpu-native v29 webgpu.h. These tests catch silent ABI regressions (wrong struct size, missing padding, field reorder) that would cause memory corruption at runtime. Run them after any struct change:
go test -v -run "TestABI" ./wgpu/...# Run linter
golangci-lint run
# Run with verbose output
golangci-lint run -v
# Verify config
golangci-lint config verifywebgpu/
├── .github/ # GitHub workflows and templates
│ ├── CODEOWNERS # Code ownership
│ └── workflows/ # CI/CD pipelines (test.yml, release.yml)
├── wgpu/ # WebGPU bindings (PUBLIC API)
│ ├── doc.go # Package-level documentation (godoc)
│ ├── types.go # Core WebGPU handle types (Instance, Device, Buffer, ...)
│ ├── enums.go # WebGPU enum types and constants
│ ├── wgpu.go # Library initialization (Init, mustInit, checkInit)
│ ├── wgpu_errors.go # Typed error system (WGPUError, sentinel errors)
│ ├── convert.go # gputypes ↔ wgpu-native enum conversion
│ ├── loader.go # Cross-platform library loading abstraction
│ ├── loader_*.go # Platform-specific loaders (Windows/Unix)
│ ├── debug.go # Resource leak detection (SetDebugMode, ReportLeaks)
│ ├── math.go # 3D math helpers (Mat4, Vec3)
│ ├── instance.go # WebGPU instance
│ ├── adapter.go # GPU adapter (StringView, Limits, SupportedLimits)
│ ├── device.go # GPU device (RequestDevice, GetQueue, GetLimits)
│ ├── buffer.go # Buffer management (MapAsync, WriteBuffer)
│ ├── texture.go # Texture management (CreateTexture, WriteTexture)
│ ├── shader.go # Shader module (CreateShaderModuleWGSL)
│ ├── pipeline.go # Compute pipeline, pipeline layout
│ ├── render_pipeline.go # Render pipeline
│ ├── bindgroup.go # Bind groups and layouts
│ ├── command.go # Command encoder/buffer, compute pass
│ ├── render.go # Render pass encoder
│ ├── render_bundle.go # Render bundle encoder
│ ├── sampler.go # Texture sampler
│ ├── queryset.go # Query set (occlusion, timestamp)
│ ├── surface.go # Surface (Configure, GetCurrentTexture, Present)
│ ├── surface_*.go # Platform-specific surfaces (Windows/Linux/macOS)
│ ├── errors.go # Error scope API (PushErrorScope, PopErrorScope)
│ ├── fuzz_test.go # Fuzz tests for FFI boundary
│ └── *_test.go # Tests
├── examples/ # Usage examples
│ ├── triangle/ # Basic rendering
│ ├── compute/ # Compute shaders
│ ├── cube/ # 3D with depth buffer
│ └── ... # More examples
├── scripts/ # Development scripts
│ ├── download-wgpu-native.sh
│ └── pre-release-check.sh
├── CHANGELOG.md # Version history
├── STABILITY.md # API stability & deprecation policy
├── LICENSE # MIT License
└── README.md # Main documentation
When wrapping a new wgpu-native function, follow this pattern exactly:
Step 1: Add the procedure handle (wgpu/wgpu.go or the relevant *.go file)
var procDeviceNewFunction = loadProc("wgpuDeviceNewFunction")Step 2: Add the wire struct (if the function takes a descriptor)
The wire struct must match the C struct layout from webgpu.h exactly:
// wireNewFunctionDescriptor must match WGPUNewFunctionDescriptor in webgpu.h exactly.
type newFunctionDescriptorWire struct {
NextInChain uintptr // *WGPUChainedStruct — always 8 bytes
Label StringView // {Data uintptr, Length uintptr} — always 16 bytes
SomeField uint32 // matches C uint32_t
_ [4]byte // explicit padding to match C struct alignment
}Step 3: Add the public method on the appropriate receiver type
// NewFunction creates a new thing. Returns an error if the device is nil or
// wgpu-native reports a validation error.
func (d *Device) NewFunction(desc *NewFunctionDescriptor) (*NewFunction, error) {
if err := d.checkInit(); err != nil {
return nil, err
}
wire := newFunctionDescriptorWire{
Label: toStringView(desc.Label),
SomeField: toWGPUSomeEnum(desc.SomeField), // use converter if needed
}
handle := procDeviceNewFunction.callUintptr(d.handle, uintptr(unsafe.Pointer(&wire)))
if handle == 0 {
return nil, fmt.Errorf("wgpu: DeviceNewFunction failed")
}
f := &NewFunction{handle: handle}
trackResource(handle, "NewFunction")
return f, nil
}Step 4: Add Release method on the new type
// Release releases the GPU resource. Safe to call on a nil or already-released handle.
func (f *NewFunction) Release() {
if f == nil || f.handle == 0 {
return
}
untrackResource(f.handle)
procNewFunctionRelease.call(f.handle)
f.handle = 0
}Step 5: Update abi_test.go
Add size and offset assertions for the new wire struct:
{"newFunctionDescriptorWire", unsafe.Sizeof(newFunctionDescriptorWire{}), expectedSize},Run go test -run TestABI ./wgpu/... to verify before pushing.
Step 6: Add a test
Even a basic null-guard test is required:
func TestNullGuard_NewFunction(t *testing.T) {
var f *NewFunction
f.Release() // must not panic
}See docs/ARCHITECTURE.md for the full explanation of the FFI layer design, wire structs, and the enum conversion rules.
- Check if issue exists, if not create one
- Discuss approach in the issue
- Create feature branch from
main - Implement feature with tests (follow the FFI pattern above)
- Update
abi_test.gofor any new wire structs - Update documentation and examples
- Run quality checks (
bash scripts/pre-release-check.sh) - Create pull request to
main - Wait for code review
- Address feedback
- Merge when approved
- Follow Go conventions and idioms
- Write self-documenting code
- Add comments for complex FFI logic
- Keep functions small and focused
- Use meaningful variable names
- Public types/functions:
PascalCase(e.g.,CreateDevice,Buffer) - Private types/functions:
camelCase(e.g.,loadLibrary,callProc) - Constants:
PascalCasewith context prefix (e.g.,BufferUsageVertex,TextureFormatRGBA8Unorm) - Test functions:
Test*(e.g.,TestBufferCreation) - Benchmark functions:
Benchmark*(e.g.,BenchmarkTextureUpload)
// Always check errors from FFI calls where applicable
result := proc.Call(args...)
if result == 0 {
return nil, errors.New("FFI call failed")
}
// Use //nolint:errcheck for .Call() methods that don't return errors
proc.Call(ptr) //nolint:errcheck
// Document unsafe pointer usage
// SAFETY: ptr is valid for the lifetime of the buffer
unsafe.Pointer(ptr)
// Match C struct layouts exactly (no fieldalignment optimization)
type CStruct struct {
Field1 uint32
Field2 uint64 // 4-byte padding implied
}- Use
WGPUErrorfor all WebGPU errors (supportserrors.Is()/errors.As()) - Match against sentinel errors:
ErrValidation,ErrOutOfMemory,ErrInternal - Use
checkInit()(returns error) for functions that return errors - Use
mustInit()(panics) only for void functions - Validate inputs before FFI calls
Every Create*() function must:
- Call
trackResource(handle, "TypeName")after successful creation - Have a corresponding
Release()method that callsuntrackResource(handle)
Use SetDebugMode(true) + ReportLeaks() during development to catch leaks.
- Use table-driven tests when appropriate
- Test both success and error cases
- Test on all supported platforms
- Add examples for new features
- Compare with wgpu-native C examples for correctness
The FFI boundary is fuzz-tested via Go native fuzzing. When adding new conversion
functions, add corresponding fuzz targets in fuzz_test.go:
# Run fuzz tests on seed corpus (fast, part of normal test suite)
go test ./wgpu/... -run Fuzz
# Run actual fuzzing for a specific target
go test ./wgpu/ -fuzz=FuzzToWGPUTextureFormat -fuzztime=60sSee STABILITY.md for the API stability policy. When deprecating a function:
// Deprecated: Use NewFunction instead.
func OldFunction() { ... }For a deep dive into the internal design — wire structs vs public structs, the enum conversion layer, async callback pattern, and testing strategy — see docs/ARCHITECTURE.md.
- Uses
syscall.LazyDLLfor dynamic loading - Surface requires HWND from win32 window
- Uses goffi for pure-Go FFI (CGO_ENABLED=0)
- Supports X11 and Wayland surfaces
- Requires wgpu-native .so in LD_LIBRARY_PATH
- Uses goffi for pure-Go FFI (CGO_ENABLED=0)
- Supports both x86_64 and ARM64 (Apple Silicon)
- Surface requires Metal layer
- Check existing issues and discussions
- Read ROADMAP.md for project direction
- Ask questions in GitHub Issues
- Reference wgpu-native documentation
By contributing, you agree that your contributions will be licensed under the MIT License.
Thank you for contributing to go-webgpu!