Skip to content
Open
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
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,47 @@ 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).

## Unreleased

### Fixed

- **wgpu-native v29 ABI:** `bindGroupLayoutEntryWire` gains `bindingArraySize`
after `visibility`, and `vertexAttributeWire` / `renderPassDepthStencilAttachment`
gain the leading `nextInChain`, matching the `webgpu.h` shipped with
wgpu-native v29.0.0.0. Without them wgpu-native panicked with "invalid buffer
binding type for buffer binding layout at binding N" and "invalid vertex
format for vertex attribute: 0", and depth attachments were misread.
- `AdapterInfo` strings are copied out of wgpu-native memory instead of aliased
with `unsafe.String`; the adapter name read garbage once wgpu-native reused
the buffer.
- `Surface.GetCurrentTexture` returns the `SurfaceTexture` (status, and the
texture when wgpu acquired one) on error statuses so the caller can present
or release it. Dropping it left the swapchain image acquired and the next
call aborted with "Surface image is already acquired".
- `Surface.Present` reports a failed `wgpuSurfacePresent` instead of always
returning nil; releasing the texture after a failed present made wgpu-native
discard the acquisition twice and abort.
- `Proc.Call` carries `//go:uintptrescapes`. Callers pass Go structs as
`uintptr(unsafe.Pointer(&x))`; without the directive those locals could move
with the goroutine stack between the conversion and the native call, so
wgpu-native wrote its output to stale memory (seen as
`wgpuSurfaceGetCurrentTexture` returning a zeroed `WGPUSurfaceTexture` about
once every few program starts).
- Nested descriptors are pinned on the heap: every Go struct whose address
reaches wgpu-native only as a `uintptr` stored in another struct (render pass
attachments, dynamic offsets of `SetBindGroup`, color target `Blend`, surface
sources, ...) goes through `pin`, which forces heap allocation.
`//go:uintptrescapes` only covers pointers converted in the call's argument
list, so these could still move with the goroutine stack before the call.

### Changed

- **Breaking for custom loaders:** `Proc` is now a concrete struct wrapping the
platform implementation and `Library.NewProc` returns `*Proc`. The directive
above is only honored on direct calls, so it could not stay on an interface.
- `TestABIWireStructAlignment` asserts the v29 layouts (the migration gap
sub-tests are gone).

## v0.5.5 (2026-08-02)

### Changed
Expand Down
73 changes: 14 additions & 59 deletions wgpu/abi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -838,68 +838,27 @@ func TestABIWireStructAlignment(t *testing.T) {
})

t.Run("vertexAttributeWire_size", func(t *testing.T) {
// v29 STATUS: WGPUVertexAttribute in C v29 has nextInChain as first field (32 bytes).
// Our vertexAttributeWire does NOT have nextInChain (24 bytes).
//
// This is a KNOWN MIGRATION GAP:
// C v29 WGPUVertexAttribute:
// nextInChain(8)+format(4)+pad(4)+offset(8)+shaderLocation(4)+pad(4) = 32 bytes
// Go vertexAttributeWire (current):
// format(4)+pad(4)+offset(8)+shaderLocation(4)+pad(4) = 24 bytes [MISSING nextInChain]
//
// TODO(v29-migration): Add nextInChain to vertexAttributeWire when upgrading to wgpu-native v29.
// Tracked in: docs/dev/kanban/blocked/0010-webgpu-headers-upgrade.md
// wgpu-native v29: WGPUVertexAttribute starts with nextInChain:
// nextInChain(8)+format(4)+pad(4)+offset(8)+shaderLocation(4)+pad(4) = 32 bytes
const gotSize = unsafe.Sizeof(vertexAttributeWire{})
const expectedCurrent = uintptr(24) // current Go wire (no nextInChain)
const expectedV29C = uintptr(32) // C v29 target (has nextInChain)

if gotSize != expectedCurrent {
t.Errorf("sizeof(vertexAttributeWire) = %d, want %d (current Go layout)",
gotSize, expectedCurrent)
}
// Document the gap: once v29 migration is complete, this must be 32.
if gotSize == expectedV29C {
t.Log("vertexAttributeWire already matches C v29 size (32 bytes) — remove migration TODO")
} else {
t.Logf("MIGRATION GAP: vertexAttributeWire is %d bytes, C v29 target is %d bytes (missing nextInChain)",
const expectedV29C = uintptr(32)
if gotSize != expectedV29C {
t.Errorf("sizeof(vertexAttributeWire) = %d, want %d (C v29 layout with nextInChain)",
gotSize, expectedV29C)
}
})

t.Run("bindGroupLayoutEntryWire_knownGap", func(t *testing.T) {
// v29 STATUS: WGPUBindGroupLayoutEntry in C v29 has bindingArraySize (uint32)
// between visibility (uint64) and buffer (bufferBindingLayoutWire).
//
// This is a KNOWN MIGRATION GAP:
// C v29 layout after visibility:
// bindingArraySize(4)+pad(4)+buffer(...)+sampler(...)+...
// Go bindGroupLayoutEntryWire (current):
// NO bindingArraySize field between visibility and buffer
//
// Impact: buffer, sampler, texture, storageTexture offsets are all shifted
// by -8 relative to C v29. This will cause incorrect binding when binding arrays
// are used (NativeFeatureTextureBindingArray).
//
// TODO(v29-migration): Add bindingArraySize uint32 + padding after Visibility
// in bindGroupLayoutEntryWire when upgrading to wgpu-native v29.
// Tracked in: docs/dev/kanban/blocked/0010-webgpu-headers-upgrade.md

t.Run("bindGroupLayoutEntryWire_bindingArraySize", func(t *testing.T) {
// wgpu-native v29: WGPUBindGroupLayoutEntry has bindingArraySize (uint32 +
// padding) between visibility (uint64) and buffer: buffer sits at visibility+16.
var e bindGroupLayoutEntryWire
// Verify current layout is self-consistent (no accidental regressions)
visibilityOffset := unsafe.Offsetof(e.Visibility)
bufferOffset := uintptr(unsafe.Pointer(&e.Buffer)) - uintptr(unsafe.Pointer(&e))

// Current: visibility at some offset, buffer directly after (no bindingArraySize gap)
// In C v29: buffer should be at visibility+8+8 = visibility+16 (bindingArraySize+pad)
// Currently buffer is at visibility+8 (just uint64 visibility, no bindingArraySize)
expectedCurrentGap := uintptr(8) // sizeof(Visibility uint64) = 8, buffer follows directly
actualGap := bufferOffset - visibilityOffset
if actualGap != expectedCurrentGap {
t.Errorf("gap(Visibility→Buffer) = %d bytes, want %d (current layout without bindingArraySize)",
actualGap, expectedCurrentGap)
}
t.Logf("MIGRATION GAP: C v29 expects gap(Visibility→Buffer)=16 bytes (bindingArraySize+pad), current Go has %d bytes",
actualGap)
const expectedGap = uintptr(16) // visibility(8) + bindingArraySize(4) + pad(4)
if actualGap := bufferOffset - visibilityOffset; actualGap != expectedGap {
t.Errorf("gap(Visibility->Buffer) = %d bytes, want %d (C v29: bindingArraySize+pad)",
actualGap, expectedGap)
}
})

t.Run("colorTargetStateWire", func(t *testing.T) {
Expand Down Expand Up @@ -930,13 +889,9 @@ func TestABIWireStructAlignment(t *testing.T) {
t.Run("bindGroupLayoutEntryWire_visibility_uint64", func(t *testing.T) {
// CRITICAL: Visibility must be uint64 (WGPUShaderStageFlags = WGPUFlags = uint64 in wgpu-native).
// This is NOT uint32 as in the webgpu.h spec — wgpu-native uses WGPUFlags typedef.
// Verify the Visibility field size via its offset and the next field offset.
var e bindGroupLayoutEntryWire
visibilityOffset := unsafe.Offsetof(e.Visibility)
bufferOffset := uintptr(unsafe.Pointer(&e.Buffer)) - uintptr(unsafe.Pointer(&e))
visibilitySize := bufferOffset - visibilityOffset
const expectedVisibilitySize = uintptr(8) // must be uint64 = 8 bytes
if visibilitySize != expectedVisibilitySize {
if visibilitySize := unsafe.Sizeof(e.Visibility); visibilitySize != expectedVisibilitySize {
t.Errorf("sizeof(Visibility in bindGroupLayoutEntryWire) = %d, want %d (must be uint64)",
visibilitySize, expectedVisibilitySize)
}
Expand Down
19 changes: 11 additions & 8 deletions wgpu/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func (i *Instance) RequestAdapter(options *RequestAdapterOptions) (*Adapter, err
ForceFallbackAdapter: boolToWGPU(options.ForceFallbackAdapter),
CompatibleSurface: surfaceHandle,
}
optionsPtr = uintptr(unsafe.Pointer(&wire))
optionsPtr = uintptr(unsafe.Pointer(pin(&wire)))
}

// Prepare callback info
Expand All @@ -163,7 +163,7 @@ func (i *Instance) RequestAdapter(options *RequestAdapterOptions) (*Adapter, err
procInstanceRequestAdapter.Call( //nolint:errcheck
i.handle,
optionsPtr,
uintptr(unsafe.Pointer(&callbackInfo)),
uintptr(unsafe.Pointer(pin(&callbackInfo))),
)

// Process events until callback fires
Expand Down Expand Up @@ -196,7 +196,7 @@ func fetchAdapterLimits(handle uintptr) Limits {
var wire limitsWire
status, _, _ := procAdapterGetLimits.Call(
handle,
uintptr(unsafe.Pointer(&wire)),
uintptr(unsafe.Pointer(pin(&wire))),
)
if WGPUStatus(status) != WGPUStatusSuccess {
return Limits{}
Expand Down Expand Up @@ -428,7 +428,7 @@ func (a *Adapter) Features() []FeatureName {
var sf SupportedFeatures
procAdapterGetFeatures.Call( //nolint:errcheck
a.handle,
uintptr(unsafe.Pointer(&sf)),
uintptr(unsafe.Pointer(pin(&sf))),
)

if sf.FeatureCount == 0 || sf.Features == 0 {
Expand All @@ -445,7 +445,7 @@ func (a *Adapter) Features() []FeatureName {
}

// Free C-allocated memory
procSupportedFeaturesFreeMembers.Call(uintptr(unsafe.Pointer(&sf))) //nolint:errcheck
procSupportedFeaturesFreeMembers.Call(uintptr(unsafe.Pointer(pin(&sf)))) //nolint:errcheck

return features
}
Expand Down Expand Up @@ -486,7 +486,7 @@ func (a *Adapter) Info() (*AdapterInfoGo, error) {
var nativeInfo AdapterInfo
status, _, _ := procAdapterGetInfo.Call(
a.handle,
uintptr(unsafe.Pointer(&nativeInfo)),
uintptr(unsafe.Pointer(pin(&nativeInfo))),
)

if WGPUStatus(status) != WGPUStatusSuccess {
Expand Down Expand Up @@ -516,7 +516,7 @@ func (a *Adapter) Info() (*AdapterInfoGo, error) {
}

// Free C memory allocated by wgpu-native
procAdapterInfoFreeMembers.Call(uintptr(unsafe.Pointer(&nativeInfo))) //nolint:errcheck
procAdapterInfoFreeMembers.Call(uintptr(unsafe.Pointer(pin(&nativeInfo)))) //nolint:errcheck

return info, nil
}
Expand All @@ -531,5 +531,8 @@ func stringViewToString(sv StringView) string {
if sv.Length > 1<<20 { // 1MB max
return ""
}
return unsafe.String((*byte)(ptrFromUintptr(sv.Data)), int(sv.Length))
// Copy the bytes: the StringView memory belongs to wgpu-native and is
// freed or reused after the call. Aliasing it with unsafe.String returned
// garbage adapter names when the string was read later.
return string(unsafe.Slice((*byte)(ptrFromUintptr(sv.Data)), int(sv.Length)))
}
30 changes: 18 additions & 12 deletions wgpu/bindgroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,20 @@ type storageTextureBindingLayoutWire struct {
// bindGroupLayoutEntryWire is the FFI-compatible struct with converted enums.
// CRITICAL: Visibility is uint64 because wgpu-native defines WGPUShaderStageFlags as uint64!
type bindGroupLayoutEntryWire struct {
NextInChain uintptr
Binding uint32
_pad [4]byte // padding to align Visibility to 8 bytes
Visibility uint64 // WGPUShaderStageFlags = uint64 in wgpu-native!
Buffer bufferBindingLayoutWire
Sampler samplerBindingLayoutWire
Texture textureBindingLayoutWire
StorageTexture storageTextureBindingLayoutWire
NextInChain uintptr
Binding uint32
_pad [4]byte // padding to align Visibility to 8 bytes
Visibility uint64 // WGPUShaderStageFlags = uint64 in wgpu-native!
// BindingArraySize (uint32 + padding) sits between visibility and buffer
// in wgpu-native v29's webgpu.h. Without it buffer.type is read from the
// wrong offset and wgpu-native panics with "invalid buffer binding type
// for buffer binding layout at binding N".
BindingArraySize uint32
_pad2 [4]byte
Buffer bufferBindingLayoutWire
Sampler samplerBindingLayoutWire
Texture textureBindingLayoutWire
StorageTexture storageTextureBindingLayoutWire
}

// toWire converts a BindGroupLayoutEntry to its wire representation.
Expand Down Expand Up @@ -237,12 +243,12 @@ func (d *Device) CreateBindGroupLayout(desc *BindGroupLayoutDescriptor) (*BindGr
for i := range desc.Entries {
wireEntries[i] = desc.Entries[i].toWire()
}
wireDesc.Entries = uintptr(unsafe.Pointer(&wireEntries[0]))
wireDesc.Entries = uintptr(unsafe.Pointer(pin(&wireEntries[0])))
}

handle, _, _ := procDeviceCreateBindGroupLayout.Call(
d.handle,
uintptr(unsafe.Pointer(&wireDesc)),
uintptr(unsafe.Pointer(pin(&wireDesc))),
)
if handle == 0 {
return nil, &WGPUError{Op: "CreateBindGroupLayout", Message: "wgpu returned null handle"}
Expand Down Expand Up @@ -295,7 +301,7 @@ func (d *Device) CreateBindGroup(desc *BindGroupDescriptor) (*BindGroup, error)
for i := range desc.Entries {
wireEntries[i] = desc.Entries[i].toWire()
}
wireEntriesPtr = uintptr(unsafe.Pointer(&wireEntries[0]))
wireEntriesPtr = uintptr(unsafe.Pointer(pin(&wireEntries[0])))
}

wire := bindGroupDescriptorWire{
Expand All @@ -307,7 +313,7 @@ func (d *Device) CreateBindGroup(desc *BindGroupDescriptor) (*BindGroup, error)

handle, _, _ := procDeviceCreateBindGroup.Call(
d.handle,
uintptr(unsafe.Pointer(&wire)),
uintptr(unsafe.Pointer(pin(&wire))),
)
if handle == 0 {
return nil, &WGPUError{Op: "CreateBindGroup", Message: "wgpu returned null handle"}
Expand Down
4 changes: 2 additions & 2 deletions wgpu/buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func (d *Device) CreateBuffer(desc *BufferDescriptor) (*Buffer, error) {
}
handle, _, _ := procDeviceCreateBuffer.Call(
d.handle,
uintptr(unsafe.Pointer(&wire)),
uintptr(unsafe.Pointer(pin(&wire))),
)
if handle == 0 {
return nil, &WGPUError{Op: "CreateBuffer", Message: "wgpu returned null handle"}
Expand Down Expand Up @@ -222,7 +222,7 @@ func (q *Queue) WriteBuffer(buffer *Buffer, offset uint64, data []byte) error {
q.handle,
buffer.handle,
uintptr(offset),
uintptr(unsafe.Pointer(&data[0])),
uintptr(unsafe.Pointer(pin(&data[0]))),
uintptr(len(data)),
)
return nil
Expand Down
Loading