Skip to content

About

Offscreen WebGPU rendering and image interchange for Go

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-gpuimage

go-gpuimage is an offscreen WebGPU rendering for Golang, built on gogpu/wgpu. The Go package is gpuimage.

Render into a GPU texture with your own shaders, upload and sample Go images, and read pixels back as *image.NRGBA. There is no window and no event loop. The same API builds natively and for browser WebAssembly (GOOS=js GOARCH=wasm).

The library manages the render target, texture resources, GPU execution, and readback. Shaders, pipelines, buffers, and drawing use upstream WebGPU types directly.

The current release is v0.1.0. The API may change before v1.0. DESIGN.md explains the design and compares related GoGPU libraries. VALIDATION.md records what was tested and how.

Quickstart

Install the library:

go get github.com/NimbleMarkets/go-gpuimage@v0.1.0
import (
	"github.com/gogpu/gputypes"
	"github.com/gogpu/wgpu"

	gpuimage "github.com/NimbleMarkets/go-gpuimage"
)

r, err := gpuimage.New(ctx, gpuimage.Options{Width: 640, Height: 480, Depth: true})
if err != nil { /* errors.Is(err, gpuimage.ErrNoAdapter), or device creation failed */ }
defer r.Close()

// Create pipelines, shaders, and buffers on the renderer's GPU thread.
err = r.Do(func(dev *wgpu.Device) error { /* dev.CreateRenderPipeline(...) */ return nil })

img, stats, err := r.Render(ctx, func(f *gpuimage.Frame) error {
	// f.Device.Queue().WriteBuffer(...) updates uniforms for this frame.
	pass, err := f.Encoder.BeginRenderPass(&wgpu.RenderPassDescriptor{
		ColorAttachments: []wgpu.RenderPassColorAttachment{{
			View: f.Color, LoadOp: gputypes.LoadOpClear, StoreOp: gputypes.StoreOpStore,
			ClearValue: gputypes.Color{R: 0.1, G: 0.1, B: 0.1, A: 1},
		}},
		DepthStencilAttachment: &wgpu.RenderPassDepthStencilAttachment{
			View: f.Depth, DepthLoadOp: gputypes.LoadOpClear, DepthStoreOp: gputypes.StoreOpStore,
			DepthClearValue: 1, StencilReadOnly: true, // required in the browser
		},
	})
	if err != nil { return err }
	// pass.SetPipeline(...); pass.Draw(...)
	return pass.End()
})
// img is a new *image.NRGBA that the caller owns.

Run the examples:

go run ./examples/clear -out frame.png      # clear a target and read it back
go run ./examples/texture -out texture.png  # upload, sample, update, read back

API

Call Purpose
New(ctx, Options) Create the device and the first target.
Renderer.Do(func(*wgpu.Device) error) Run setup or teardown on the GPU thread.
Renderer.Render(ctx, draw) Draw, then read the frame back in the same submission.
Renderer.RenderToTexture(ctx, draw) Draw without reading back.
Renderer.ReadPixels(ctx) Read back the most recent frame.
Renderer.Resize(w, h), Size() Replace the target. A failed resize keeps the old one.
Renderer.AdapterInfo() Adapter name, backend, and device type.
Renderer.UploadTexture(image, TextureOptions) Create a sampled texture from a Go image.
Texture.Update, ReadPixels, Size, Close Replace pixels in place, copy them back, release.
Texture.BindGroup(), BindGroupLayout() Handles for the caller's pipeline.
Renderer.Close() Release everything, including textures still open.

Frame gives the draw callback Device, Encoder, Color, Depth, Width, Height, and Seq. Stats reports Draw, Submit, Map, Copy, and Total durations for a frame.

Pixels and alpha

  • Targets and textures are RGBA8Unorm. Colors are not converted between color spaces.
  • Readback returns the target's bytes as image.NRGBA. If a shader writes premultiplied color, un-premultiply before treating the image as straight alpha. Opaque output needs no conversion.
  • UploadTexture accepts any image.Image, including sub-images and padded strides, and stores straight alpha. Linear filtering of straight alpha darkens edges next to transparent texels; use an opaque image, nearest filtering, or a padded atlas where that matters.
  • GoGPU's gogpu and gg libraries use premultiplied alpha. Convert when exchanging images with them.

Sampled images

tile, err := r.UploadTexture(sourceImage, gpuimage.TextureOptions{})
if err != nil { /* handle error */ }
defer tile.Close()

// Include tile.BindGroupLayout() in your pipeline layout. In WGSL:
// @group(0) @binding(0) var tileTexture: texture_2d<f32>;
// @group(0) @binding(1) var tileSampler: sampler;
// Inside a draw callback, bind tile.BindGroup() at that group index.

err = tile.Update(nextImage)         // same dimensions; the bind group stays valid
pixels, err := tile.ReadPixels(ctx)  // optional CPU copy of the texture

TextureOptions{Filter: gputypes.FilterModeNearest} selects nearest filtering. The zero value selects linear. UVs clamp at the edges.

Threading and ownership

  • Each Renderer owns its instance, adapter, device, target, and textures, and releases them in Close. Calls that need the device return ErrClosed after it closes.
  • All GPU work runs on one goroutine pinned to an OS thread. Calls from any goroutine are serialized, with one frame in flight.
  • Do not call methods that queue GPU work from inside Do or a draw callback. They would wait for the thread they already occupy. Texture handle accessors such as BindGroup and BindGroupLayout are safe to use in callbacks.
  • Objects you create with the device are yours. Release them inside Do before Close.
  • Do not keep the device, encoder, or views from a callback for use on another goroutine.
  • Sharing a device owned by another library is not supported yet. DESIGN.md describes a possible API.

Cancellation

Render, RenderToTexture, and both ReadPixels methods take a context.

  • A call waiting for the GPU thread returns ctx.Err() when the context is cancelled or expires, without drawing.
  • Cancellation during the draw callback or while the GPU finishes the frame also returns ctx.Err(). A running draw callback is not interrupted.
  • After cancellation the GPU may still complete the abandoned frame. Later calls are unaffected.
  • On Metal, a cancellable context added about 0.1 ms per readback in testing, because the wait must poll. context.Background() uses a single blocking wait.
  • With a cancellable context, native RenderToTexture returns once the frame is submitted instead of waiting for the GPU.
  • Do, Resize, UploadTexture, Update, and Close take no context and wait their turn.

Browser

  • Requires a WebGPU-capable browser in a secure context (HTTPS or localhost).
  • Call the library from a Go goroutine, not from inside a JavaScript callback. Readback awaits a promise, which would deadlock the event loop.
  • While a readback is pending, the library requests animation frames so that the browser settles the map promptly.
  • Set StencilReadOnly: true when attaching Frame.Depth. The browser backend otherwise emits stencil operations, which WebGPU rejects for a depth-only format.
  • AdapterInfo cannot identify a software adapter in the browser. Upstream reports every browser adapter as "WebGPU Adapter".

Testing

With Task installed, task list shows every task.

Task Runs Needs
task Race tests and builds of examples, the browser runner, and WASM Nothing
task ci Everything CI runs: race tests, vet, WASM build, Node tests Node
task test-gpu The full suite on a native adapter A GPU
task bench Clear and readback benchmarks, three runs per case A GPU
task test-browser The full suite in headless Chrome Chrome with WebGPU
task validate ci, test-gpu, and test-browser All of the above
task test-vulkan The full suite on Vulkan under the validation layer Linux with Vulkan
task test-vulkan-remote HOST=name Copies the tree to name over ssh and runs test-vulkan there ssh access
task run-examples Both examples, writing PNG files to bin/ A GPU

The same commands without Task:

go test -race ./...                   # helpers and executor, no GPU
GPUIMAGE_GPU_TEST=1 go test ./...     # real native adapter; never with -race

# Helper tests under Node, which has no WebGPU:
GOOS=js GOARCH=wasm go test -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec" .

# The full suite in headless Chrome with WebGPU:
go build -o /tmp/browsertest ./internal/browsertest
GPUIMAGE_GPU_TEST=1 GOOS=js GOARCH=wasm go test -exec=/tmp/browsertest -v .

GPU tests log the adapter they ran on. A run on a CPU or fallback adapter does not test hardware GPU behavior.

Benchmarks

BenchmarkRenderReadback measures one complete frame: clear an RGBA8 color target, submit it, wait for the GPU, and copy the pixels into a new image.NRGBA. It includes allocation and garbage collection during the loop. Device creation, ten warmup frames, context creation, and cleanup are outside the timer. It does not draw geometry, upload textures, or use a depth attachment.

Each size runs with context.Background() and with one reused cancellable context that is never cancelled. This measures the cost of checking for cancellation, not the cost of cancelling a frame.

task bench                            # choose the default native backend
task bench BACKEND=metal               # macOS
task bench BACKEND=vulkan              # Linux
task bench BENCHTIME=3s COUNT=5         # longer runs

# Without Task (set GPUIMAGE_BENCH_BACKEND=metal or vulkan to choose):
GPUIMAGE_GPU_TEST=1 go test -run='^$' -bench=BenchmarkRenderReadback \
  -benchmem -benchtime=1s -count=3 .

Run without the race detector or Vulkan validation layers. The benchmark logs the adapter and rejects software adapters. MB/s is the output image size divided by frame time; it is not a measurement of GPU bus bandwidth. Allocation counts cover Go memory, not GPU memory.

Results from 2026-09-27, using wgpu v0.34.5 and naga v0.19.0. Each value is the median of three runs with -benchtime=1s. Lower is better.

Frame size Context Apple M3 Max / Metal NVIDIA GB10 / Vulkan
640x400 Background 0.587 ms 0.480 ms
640x400 Cancellable 0.645 ms 0.684 ms
1280x720 Background 0.657 ms 0.768 ms
1280x720 Cancellable 0.758 ms 0.938 ms
1920x1080 Background 0.820 ms 1.077 ms
1920x1080 Cancellable 0.954 ms 1.199 ms

The Metal run used macOS 27.0 arm64 and Go 1.27.1. The Vulkan run used Ubuntu 24.04.5 arm64, Go 1.26.8, and NVIDIA driver 580.178.04. Vulkan used VK_DRIVER_FILES=/usr/share/vulkan/icd.d/nvidia_icd.json to select the NVIDIA driver. These numbers compare complete systems, including CPU, driver, and Go runtime costs.

Raw output, including throughput and allocations: Metal and Vulkan.

Known limitations

  • naga v0.19.0 generates invalid Metal source for arithmetic on a component of an indexed let or const array, such as positions[i].x * 0.5. List the values explicitly, as the examples do. TestDerivedUVShader reports when upstream fixes this.
  • The color target cannot be sampled by a later pass. It has no TextureBinding usage.
  • No mipmaps, MSAA, sRGB formats, or partial texture updates.
  • Metal, Vulkan on one NVIDIA adapter, and Chrome were run. DX12, GLES, and other browsers compile but were not tested.
  • ReadPixels fails until a frame has been drawn to the current target.

Open Collaboration

We welcome contributions and feedback. Please adhere to our Code of Conduct when engaging our community.

Acknowledgements

Thanks to the GoGPU organization for their dedication to making GPU processing available to Golang developers, especially in the form of the wgpu WebGPU library.

License

This project is released under the MIT License, see LICENSE.txt.

Copyright (c) 2026 Neomantra Corp.


Made with ❤️ and 🔥 by the team behind Nimble.Markets.

About

Offscreen WebGPU rendering and image interchange for Go

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages