diff --git a/CHANGELOG.md b/CHANGELOG.md index 92fbdaf..f717233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/wgpu/abi_test.go b/wgpu/abi_test.go index 2ae4ea3..39130d2 100644 --- a/wgpu/abi_test.go +++ b/wgpu/abi_test.go @@ -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) { @@ -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) } diff --git a/wgpu/adapter.go b/wgpu/adapter.go index b2f69d5..5972cef 100644 --- a/wgpu/adapter.go +++ b/wgpu/adapter.go @@ -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 @@ -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 @@ -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{} @@ -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 { @@ -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 } @@ -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 { @@ -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 } @@ -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))) } diff --git a/wgpu/bindgroup.go b/wgpu/bindgroup.go index 1e8cd7d..4c2cd16 100644 --- a/wgpu/bindgroup.go +++ b/wgpu/bindgroup.go @@ -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. @@ -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"} @@ -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{ @@ -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"} diff --git a/wgpu/buffer.go b/wgpu/buffer.go index 7d16018..1491511 100644 --- a/wgpu/buffer.go +++ b/wgpu/buffer.go @@ -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"} @@ -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 diff --git a/wgpu/command.go b/wgpu/command.go index 1849f8e..7b0a837 100644 --- a/wgpu/command.go +++ b/wgpu/command.go @@ -56,7 +56,7 @@ func (d *Device) CreateCommandEncoder(desc *CommandEncoderDescriptor) (*CommandE wire := commandEncoderDescriptorWire{ Label: stringToStringView(desc.Label), } - descPtr = uintptr(unsafe.Pointer(&wire)) + descPtr = uintptr(unsafe.Pointer(pin(&wire))) } handle, _, _ := procDeviceCreateCommandEncoder.Call( d.handle, @@ -88,7 +88,7 @@ func (enc *CommandEncoder) BeginComputePass(desc *ComputePassDescriptor) (*Compu if desc.Label != "" { labelBytes := []byte(desc.Label) wireDesc.label = StringView{ - Data: uintptr(unsafe.Pointer(&labelBytes[0])), + Data: uintptr(unsafe.Pointer(pin(&labelBytes[0]))), Length: uintptr(len(labelBytes)), } } else { @@ -101,9 +101,9 @@ func (enc *CommandEncoder) BeginComputePass(desc *ComputePassDescriptor) (*Compu beginningOfPassWriteIndex: desc.TimestampWrites.BeginningOfPassWriteIndex, endOfPassWriteIndex: desc.TimestampWrites.EndOfPassWriteIndex, } - wireDesc.timestampWrites = uintptr(unsafe.Pointer(&wireTimestamp)) + wireDesc.timestampWrites = uintptr(unsafe.Pointer(pin(&wireTimestamp))) } - descPtr = uintptr(unsafe.Pointer(&wireDesc)) + descPtr = uintptr(unsafe.Pointer(pin(&wireDesc))) } handle, _, _ := procCommandEncoderBeginComputePass.Call( @@ -160,12 +160,12 @@ func (enc *CommandEncoder) InsertDebugMarker(markerLabel string) { return } label := StringView{ - Data: uintptr(unsafe.Pointer(&labelBytes[0])), + Data: uintptr(unsafe.Pointer(pin(&labelBytes[0]))), Length: uintptr(len(labelBytes)), } procCommandEncoderInsertDebugMarker.Call( //nolint:errcheck enc.handle, - uintptr(unsafe.Pointer(&label)), + uintptr(unsafe.Pointer(pin(&label))), ) } @@ -181,12 +181,12 @@ func (enc *CommandEncoder) PushDebugGroup(groupLabel string) { return } label := StringView{ - Data: uintptr(unsafe.Pointer(&labelBytes[0])), + Data: uintptr(unsafe.Pointer(pin(&labelBytes[0]))), Length: uintptr(len(labelBytes)), } procCommandEncoderPushDebugGroup.Call( //nolint:errcheck enc.handle, - uintptr(unsafe.Pointer(&label)), + uintptr(unsafe.Pointer(pin(&label))), ) } @@ -238,9 +238,9 @@ func (enc *CommandEncoder) CopyTextureToBuffer(src *Texture, dst *Buffer, region size := r.Size procCommandEncoderCopyTextureToBuffer.Call( //nolint:errcheck enc.handle, - uintptr(unsafe.Pointer(&srcWire)), - uintptr(unsafe.Pointer(&dstWire)), - uintptr(unsafe.Pointer(&size)), + uintptr(unsafe.Pointer(pin(&srcWire))), + uintptr(unsafe.Pointer(pin(&dstWire))), + uintptr(unsafe.Pointer(pin(&size))), ) } } @@ -276,9 +276,9 @@ func (enc *CommandEncoder) CopyTextureToTexture(src, dst *Texture, regions []Tex size := r.Size procCommandEncoderCopyTextureToTexture.Call( //nolint:errcheck enc.handle, - uintptr(unsafe.Pointer(&srcWire)), - uintptr(unsafe.Pointer(&dstWire)), - uintptr(unsafe.Pointer(&size)), + uintptr(unsafe.Pointer(pin(&srcWire))), + uintptr(unsafe.Pointer(pin(&dstWire))), + uintptr(unsafe.Pointer(pin(&size))), ) } } @@ -311,7 +311,7 @@ func (enc *CommandEncoder) Finish(desc ...*CommandBufferDescriptor) (*CommandBuf } var descPtr uintptr if len(desc) > 0 && desc[0] != nil { - descPtr = uintptr(unsafe.Pointer(desc[0])) + descPtr = uintptr(unsafe.Pointer(pin(desc[0]))) } handle, _, _ := procCommandEncoderFinish.Call( enc.handle, @@ -389,7 +389,7 @@ func (cpe *ComputePassEncoder) SetBindGroup(groupIndex uint32, group *BindGroup, var offsetsPtr uintptr offsetCount := uintptr(0) if len(dynamicOffsets) > 0 { - offsetsPtr = uintptr(unsafe.Pointer(&dynamicOffsets[0])) + offsetsPtr = uintptr(unsafe.Pointer(pin(&dynamicOffsets[0]))) offsetCount = uintptr(len(dynamicOffsets)) } procComputePassEncoderSetBindGroup.Call( //nolint:errcheck @@ -473,7 +473,7 @@ func (q *Queue) Submit(commands ...*CommandBuffer) (uint64, error) { submissionIndex, _, _ := procQueueSubmitForIndex.Call( q.handle, uintptr(len(handles)), - uintptr(unsafe.Pointer(&handles[0])), + uintptr(unsafe.Pointer(pin(&handles[0]))), ) return uint64(submissionIndex), nil } @@ -489,7 +489,7 @@ func (q *Queue) GetTimestampPeriod() float32 { return 0 } - proc, ok := procQueueGetTimestampPeriod.(float32Proc) + proc, ok := procQueueGetTimestampPeriod.impl.(float32Proc) if !ok { return 0 } diff --git a/wgpu/descriptors.go b/wgpu/descriptors.go index b27ccef..e52b39a 100644 --- a/wgpu/descriptors.go +++ b/wgpu/descriptors.go @@ -19,7 +19,7 @@ func stringToStringView(s string) StringView { } b := []byte(s) return StringView{ - Data: uintptr(unsafe.Pointer(&b[0])), + Data: uintptr(unsafe.Pointer(pin(&b[0]))), Length: uintptr(len(b)), } } diff --git a/wgpu/device.go b/wgpu/device.go index 87b3c4c..61b0f02 100644 --- a/wgpu/device.go +++ b/wgpu/device.go @@ -100,13 +100,13 @@ func (a *Adapter) RequestDevice(options *DeviceDescriptor) (*Device, error) { } if len(options.RequiredFeatures) > 0 { wire.RequiredFeatureCount = uintptr(len(options.RequiredFeatures)) - wire.RequiredFeatures = uintptr(unsafe.Pointer(&options.RequiredFeatures[0])) + wire.RequiredFeatures = uintptr(unsafe.Pointer(pin(&options.RequiredFeatures[0]))) } if options.RequiredLimits != nil { reqLimitsWire = limitsToWire(options.RequiredLimits) - wire.RequiredLimits = uintptr(unsafe.Pointer(&reqLimitsWire)) + wire.RequiredLimits = uintptr(unsafe.Pointer(pin(&reqLimitsWire))) } - optionsPtr = uintptr(unsafe.Pointer(&wire)) + optionsPtr = uintptr(unsafe.Pointer(pin(&wire))) } _ = reqLimitsWire // ensure not optimised away before the call below @@ -123,7 +123,7 @@ func (a *Adapter) RequestDevice(options *DeviceDescriptor) (*Device, error) { procAdapterRequestDevice.Call( //nolint:errcheck a.handle, optionsPtr, - uintptr(unsafe.Pointer(&callbackInfo)), + uintptr(unsafe.Pointer(pin(&callbackInfo))), ) // Process events until callback fires @@ -156,7 +156,7 @@ func fetchDeviceLimits(handle uintptr) Limits { var wire limitsWire status, _, _ := procDeviceGetLimits.Call( handle, - uintptr(unsafe.Pointer(&wire)), + uintptr(unsafe.Pointer(pin(&wire))), ) if WGPUStatus(status) != WGPUStatusSuccess { return Limits{} @@ -344,7 +344,7 @@ func (d *Device) Features() []FeatureName { var supported SupportedFeatures procDeviceGetFeatures.Call( //nolint:errcheck d.handle, - uintptr(unsafe.Pointer(&supported)), + uintptr(unsafe.Pointer(pin(&supported))), ) if supported.FeatureCount == 0 || supported.Features == 0 { @@ -360,7 +360,7 @@ func (d *Device) Features() []FeatureName { copy(result, features) // Free C-allocated memory (pass pointer to struct, not individual fields) - procSupportedFeaturesFreeMembers.Call(uintptr(unsafe.Pointer(&supported))) //nolint:errcheck + procSupportedFeaturesFreeMembers.Call(uintptr(unsafe.Pointer(pin(&supported)))) //nolint:errcheck return result } diff --git a/wgpu/errors.go b/wgpu/errors.go index 2d4cc83..4d3f76f 100644 --- a/wgpu/errors.go +++ b/wgpu/errors.go @@ -164,7 +164,7 @@ func (d *Device) PopErrorScopeAsync(instance *Instance) (ErrorType, string, erro // nolint:gosec // FFI requires unsafe.Pointer conversion for struct passing procDevicePopErrorScope.Call( d.handle, - uintptr(unsafe.Pointer(&callbackInfo)), + uintptr(unsafe.Pointer(pin(&callbackInfo))), ) // Process events until callback fires diff --git a/wgpu/instance.go b/wgpu/instance.go index b261d66..1d3ff4a 100644 --- a/wgpu/instance.go +++ b/wgpu/instance.go @@ -69,7 +69,7 @@ func CreateInstance(desc *InstanceDescriptor) (*Instance, error) { var wirePtr uintptr if desc != nil { wire := instanceDescriptorWire{} // zero = default, backends/flags handled by wgpu-native extensions - wirePtr = uintptr(unsafe.Pointer(&wire)) + wirePtr = uintptr(unsafe.Pointer(pin(&wire))) } handle, _, _ := procCreateInstance.Call(wirePtr) diff --git a/wgpu/loader.go b/wgpu/loader.go index fe66a20..5c2ae2f 100644 --- a/wgpu/loader.go +++ b/wgpu/loader.go @@ -6,19 +6,46 @@ package wgpu // Platform-specific implementations handle the actual loading mechanism. type Library interface { // NewProc retrieves a procedure (function) from the library. - // Returns a Proc interface that can be used to call the function. - NewProc(name string) Proc + // Returns a Proc that can be used to call the function. + NewProc(name string) *Proc } -// Proc represents a procedure (function pointer) from a dynamically loaded library. -// It abstracts platform-specific function calling mechanisms. -type Proc interface { +// procImpl is the platform-specific calling mechanism behind a Proc. +type procImpl interface { // Call invokes the procedure with the given arguments. // Returns the result value and error (if any). // Arguments are passed as uintptr to match C ABI. Call(args ...uintptr) (uintptr, uintptr, error) } +// Proc represents a procedure (function pointer) from a dynamically loaded +// library. It abstracts platform-specific function calling mechanisms. +// +// Proc is a concrete type (not an interface) so that Call can carry +// //go:uintptrescapes. Callers pass Go structs as +// uintptr(unsafe.Pointer(pin(&local))); without the directive those locals stay +// on the goroutine stack and can move (stack growth, GC shrink) between the +// conversion and the native call, so wgpu-native reads stale input or +// writes its output to the old stack. Seen as wgpuSurfaceGetCurrentTexture +// "returning" a zeroed WGPUSurfaceTexture (status 0) while the texture was +// really acquired, which then aborted the process on the next present or +// configure. The directive is only honored on direct calls, hence no +// interface. +type Proc struct { + impl procImpl +} + +func newProc(impl procImpl) *Proc { return &Proc{impl: impl} } + +// Call invokes the procedure with the given arguments. Pointer arguments +// converted to uintptr in the call expression are moved to the heap and kept +// alive for the duration of the call. +// +//go:uintptrescapes +func (p *Proc) Call(args ...uintptr) (uintptr, uintptr, error) { + return p.impl.Call(args...) +} + // float32Proc is implemented by platform loaders for procedures whose native // return type is float32. Proc.Call intentionally keeps the existing integer // return contract for the rest of the WebGPU API; this narrow interface lets diff --git a/wgpu/loader_unix.go b/wgpu/loader_unix.go index e5b3ce7..fd96e39 100644 --- a/wgpu/loader_unix.go +++ b/wgpu/loader_unix.go @@ -42,34 +42,34 @@ func loadLibrary(name string) (Library, error) { } // NewProc retrieves a procedure from the Unix shared library. -func (u *unixLibrary) NewProc(name string) Proc { +func (u *unixLibrary) NewProc(name string) *Proc { if u.handle == nil { // Return a proc that will fail on Call - return &unixProc{ + return newProc(&unixProc{ lib: u, name: name, fnPtr: nil, prepared: false, - } + }) } fnPtr, err := ffi.GetSymbol(u.handle, name) if err != nil { // Return a proc that will fail on Call - return &unixProc{ + return newProc(&unixProc{ lib: u, name: name, fnPtr: nil, prepared: false, - } + }) } - return &unixProc{ + return newProc(&unixProc{ lib: u, name: name, fnPtr: fnPtr, prepared: false, - } + }) } // Call invokes the Unix procedure with the given arguments. diff --git a/wgpu/loader_windows.go b/wgpu/loader_windows.go index 1c46497..6218ad1 100644 --- a/wgpu/loader_windows.go +++ b/wgpu/loader_windows.go @@ -31,10 +31,10 @@ func loadLibrary(name string) (Library, error) { } // NewProc retrieves a procedure from the Windows DLL. -func (w *windowsLibrary) NewProc(name string) Proc { - return &windowsProc{ +func (w *windowsLibrary) NewProc(name string) *Proc { + return newProc(&windowsProc{ proc: w.dll.NewProc(name), - } + }) } // Call invokes the Windows procedure with the given arguments. diff --git a/wgpu/map_pending.go b/wgpu/map_pending.go index 31d088e..2e5821b 100644 --- a/wgpu/map_pending.go +++ b/wgpu/map_pending.go @@ -109,7 +109,7 @@ func (b *Buffer) mapAsyncStart(mode MapMode, offset, size uint64) (*mapRequest, uintptr(mode), uintptr(offset), uintptr(size), - uintptr(unsafe.Pointer(&callbackInfo)), + uintptr(unsafe.Pointer(pin(&callbackInfo))), ) return req, nil diff --git a/wgpu/pin.go b/wgpu/pin.go new file mode 100644 index 0000000..a65dabd --- /dev/null +++ b/wgpu/pin.go @@ -0,0 +1,24 @@ +package wgpu + +// pinSink and pinSinkOn force the escape: the compiler cannot prove that +// pinSinkOn is always false, so anything passed through pin is heap-allocated. +var ( + pinSink any + pinSinkOn bool +) + +// pin returns p unchanged but forces the pointed-to object onto the heap. +// +// //go:uintptrescapes on Proc.Call only covers pointers converted to uintptr +// directly in the call's argument list. Descriptors that wgpu-native receives +// nested (a pointer stored as uintptr inside another struct, or converted +// before the call) are not covered: if they live on the goroutine stack, a +// stack growth or shrink between the conversion and the native call leaves +// the uintptr pointing at stale memory. The Go heap does not move objects, so +// the pointer stays valid. +func pin[T any](p *T) *T { + if pinSinkOn { + pinSink = p + } + return p +} diff --git a/wgpu/pipeline.go b/wgpu/pipeline.go index d9a0998..eb36bf4 100644 --- a/wgpu/pipeline.go +++ b/wgpu/pipeline.go @@ -94,7 +94,7 @@ func (d *Device) CreatePipelineLayout(desc *PipelineLayoutDescriptor) (*Pipeline handles[i] = l.handle } } - layoutsPtr = uintptr(unsafe.Pointer(&handles[0])) + layoutsPtr = uintptr(unsafe.Pointer(pin(&handles[0]))) } wire := pipelineLayoutDescriptorWire{ @@ -105,7 +105,7 @@ func (d *Device) CreatePipelineLayout(desc *PipelineLayoutDescriptor) (*Pipeline handle, _, _ := procDeviceCreatePipelineLayout.Call( d.handle, - uintptr(unsafe.Pointer(&wire)), + uintptr(unsafe.Pointer(pin(&wire))), ) if handle == 0 { return nil, &WGPUError{Op: "CreatePipelineLayout", Message: "wgpu returned null handle"} @@ -157,7 +157,7 @@ func (d *Device) CreateComputePipeline(desc *ComputePipelineDescriptor) (*Comput } if len(entryPointBytes) > 0 { compute.EntryPoint = StringView{ - Data: uintptr(unsafe.Pointer(&entryPointBytes[0])), + Data: uintptr(unsafe.Pointer(pin(&entryPointBytes[0]))), Length: uintptr(len(entryPointBytes)), } } else { @@ -177,7 +177,7 @@ func (d *Device) CreateComputePipeline(desc *ComputePipelineDescriptor) (*Comput handle, _, _ := procDeviceCreateComputePipeline.Call( d.handle, - uintptr(unsafe.Pointer(&wire)), + uintptr(unsafe.Pointer(pin(&wire))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateComputePipeline", Message: "wgpu returned null handle"} diff --git a/wgpu/queryset.go b/wgpu/queryset.go index 8938f94..7d39b48 100644 --- a/wgpu/queryset.go +++ b/wgpu/queryset.go @@ -38,7 +38,7 @@ func (d *Device) CreateQuerySet(desc *QuerySetDescriptor) (*QuerySet, error) { handle, _, _ := procDeviceCreateQuerySet.Call( d.handle, - uintptr(unsafe.Pointer(&nativeDesc)), + uintptr(unsafe.Pointer(pin(&nativeDesc))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateQuerySet", Message: "wgpu returned null handle"} diff --git a/wgpu/queue_timestamp_period_test.go b/wgpu/queue_timestamp_period_test.go index d8f6668..b1b0583 100644 --- a/wgpu/queue_timestamp_period_test.go +++ b/wgpu/queue_timestamp_period_test.go @@ -46,7 +46,7 @@ func (*integerOnlyTimestampPeriodProc) Call(args ...uintptr) (uintptr, uintptr, func TestABIQueueGetTimestampPeriodRequiresFloat32Proc(t *testing.T) { original := procQueueGetTimestampPeriod - procQueueGetTimestampPeriod = &integerOnlyTimestampPeriodProc{} + procQueueGetTimestampPeriod = newProc(&integerOnlyTimestampPeriodProc{}) defer func() { procQueueGetTimestampPeriod = original }() if got := (&Queue{handle: 0x1234}).GetTimestampPeriod(); got != 0 { @@ -67,7 +67,7 @@ func TestABIQueueGetTimestampPeriodUnavailable(t *testing.T) { func TestABIQueueGetTimestampPeriodUsesNativeFloat32(t *testing.T) { stub := ×tampPeriodProcStub{period: 0.125} original := procQueueGetTimestampPeriod - procQueueGetTimestampPeriod = stub + procQueueGetTimestampPeriod = newProc(stub) defer func() { procQueueGetTimestampPeriod = original }() got := (&Queue{handle: 0x1234}).GetTimestampPeriod() @@ -82,7 +82,7 @@ func TestABIQueueGetTimestampPeriodUsesNativeFloat32(t *testing.T) { func TestABIQueueGetTimestampPeriodCallError(t *testing.T) { stub := ×tampPeriodProcStub{period: 0.125, err: errors.New("call failed")} original := procQueueGetTimestampPeriod - procQueueGetTimestampPeriod = stub + procQueueGetTimestampPeriod = newProc(stub) defer func() { procQueueGetTimestampPeriod = original }() if got := (&Queue{handle: 0x1234}).GetTimestampPeriod(); got != 0 { @@ -100,7 +100,7 @@ func TestABIQueueGetTimestampPeriodDynamicLibrary(t *testing.T) { t.Fatal(err) } defer closeTimestampPeriodABILibrary(t, library) - proc, ok := library.NewProc("wgpuQueueGetTimestampPeriod").(float32Proc) + proc, ok := library.NewProc("wgpuQueueGetTimestampPeriod").impl.(float32Proc) if !ok { t.Fatal("platform loader does not implement float32 return calls") } diff --git a/wgpu/render.go b/wgpu/render.go index ad95bb8..b76309b 100644 --- a/wgpu/render.go +++ b/wgpu/render.go @@ -67,6 +67,8 @@ type RenderPassDepthStencilAttachment struct { // renderPassDepthStencilAttachment is the native structure (40 bytes). // Uses uint32 for LoadOp/StoreOp with wgpu-native converted values. type renderPassDepthStencilAttachment struct { + // nextInChain comes first in wgpu-native v29's webgpu.h. + nextInChain uintptr view uintptr depthLoadOp uint32 // wgpu-native converted value depthStoreOp uint32 // wgpu-native converted value @@ -172,7 +174,7 @@ func (enc *CommandEncoder) BeginRenderPass(desc *RenderPassDescriptor) (*RenderP stencilClearValue: desc.DepthStencilAttachment.StencilClearValue, stencilReadOnly: stencilRO, } - depthStencilPtr = uintptr(unsafe.Pointer(&nativeDepthStencil)) + depthStencilPtr = uintptr(unsafe.Pointer(pin(&nativeDepthStencil))) } // Build timestamp writes if present (v29: passTimestampWrites with nextInChain) @@ -185,14 +187,14 @@ func (enc *CommandEncoder) BeginRenderPass(desc *RenderPassDescriptor) (*RenderP beginningOfPassWriteIndex: desc.TimestampWrites.BeginningOfPassWriteIndex, endOfPassWriteIndex: desc.TimestampWrites.EndOfPassWriteIndex, } - timestampWritesPtr = uintptr(unsafe.Pointer(&nativeTimestampWrites)) + timestampWritesPtr = uintptr(unsafe.Pointer(pin(&nativeTimestampWrites))) } nativeDesc := renderPassDescriptor{ nextInChain: 0, label: stringToStringView(desc.Label), colorAttachmentCount: uintptr(len(nativeColorAttachments)), - colorAttachments: uintptr(unsafe.Pointer(&nativeColorAttachments[0])), + colorAttachments: uintptr(unsafe.Pointer(pin(&nativeColorAttachments[0]))), depthStencilAttachment: depthStencilPtr, occlusionQuerySet: 0, timestampWrites: timestampWritesPtr, @@ -200,7 +202,7 @@ func (enc *CommandEncoder) BeginRenderPass(desc *RenderPassDescriptor) (*RenderP handle, _, _ := procCommandEncoderBeginRenderPass.Call( enc.handle, - uintptr(unsafe.Pointer(&nativeDesc)), + uintptr(unsafe.Pointer(pin(&nativeDesc))), ) if handle == 0 { return nil, &WGPUError{Op: "BeginRenderPass", Message: "wgpu returned null handle"} @@ -228,7 +230,7 @@ func (rpe *RenderPassEncoder) SetBindGroup(groupIndex uint32, group *BindGroup, var offsetsPtr uintptr offsetCount := uintptr(0) if len(dynamicOffsets) > 0 { - offsetsPtr = uintptr(unsafe.Pointer(&dynamicOffsets[0])) + offsetsPtr = uintptr(unsafe.Pointer(pin(&dynamicOffsets[0]))) offsetCount = uintptr(len(dynamicOffsets)) } @@ -414,12 +416,12 @@ func (rpe *RenderPassEncoder) InsertDebugMarker(markerLabel string) { return } label := StringView{ - Data: uintptr(unsafe.Pointer(&labelBytes[0])), + Data: uintptr(unsafe.Pointer(pin(&labelBytes[0]))), Length: uintptr(len(labelBytes)), } procRenderPassEncoderInsertDebugMarker.Call( //nolint:errcheck rpe.handle, - uintptr(unsafe.Pointer(&label)), + uintptr(unsafe.Pointer(pin(&label))), ) } @@ -435,12 +437,12 @@ func (rpe *RenderPassEncoder) PushDebugGroup(groupLabel string) { return } label := StringView{ - Data: uintptr(unsafe.Pointer(&labelBytes[0])), + Data: uintptr(unsafe.Pointer(pin(&labelBytes[0]))), Length: uintptr(len(labelBytes)), } procRenderPassEncoderPushDebugGroup.Call( //nolint:errcheck rpe.handle, - uintptr(unsafe.Pointer(&label)), + uintptr(unsafe.Pointer(pin(&label))), ) } diff --git a/wgpu/render_bundle.go b/wgpu/render_bundle.go index ba7cd1a..5334b47 100644 --- a/wgpu/render_bundle.go +++ b/wgpu/render_bundle.go @@ -66,12 +66,12 @@ func (d *Device) CreateRenderBundleEncoder(desc *RenderBundleEncoderDescriptor) for i, f := range desc.ColorFormats { convertedFormats[i] = uint32(f) } - wire.colorFormats = uintptr(unsafe.Pointer(&convertedFormats[0])) + wire.colorFormats = uintptr(unsafe.Pointer(pin(&convertedFormats[0]))) } handle, _, _ := procDeviceCreateRenderBundleEncoder.Call( d.handle, - uintptr(unsafe.Pointer(&wire)), + uintptr(unsafe.Pointer(pin(&wire))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateRenderBundleEncoder", Message: "wgpu returned null handle"} @@ -107,7 +107,7 @@ func (rbe *RenderBundleEncoder) SetBindGroup(groupIndex uint32, group *BindGroup } var offsetsPtr uintptr if len(dynamicOffsets) > 0 { - offsetsPtr = uintptr(unsafe.Pointer(&dynamicOffsets[0])) + offsetsPtr = uintptr(unsafe.Pointer(pin(&dynamicOffsets[0]))) } procRenderBundleEncoderSetBindGroup.Call( //nolint:errcheck rbe.handle, @@ -215,7 +215,7 @@ func (rbe *RenderBundleEncoder) Finish(desc ...*RenderBundleDescriptor) *RenderB var descPtr uintptr if len(desc) > 0 && desc[0] != nil { - descPtr = uintptr(unsafe.Pointer(desc[0])) + descPtr = uintptr(unsafe.Pointer(pin(desc[0]))) } handle, _, _ := procRenderBundleEncoderFinish.Call(rbe.handle, descPtr) @@ -267,6 +267,6 @@ func (rpe *RenderPassEncoder) ExecuteBundles(bundles []*RenderBundle) { procRenderPassEncoderExecuteBundles.Call( //nolint:errcheck rpe.handle, uintptr(len(handles)), - uintptr(unsafe.Pointer(&handles[0])), + uintptr(unsafe.Pointer(pin(&handles[0]))), ) } diff --git a/wgpu/render_pipeline.go b/wgpu/render_pipeline.go index 5b3bded..302eacf 100644 --- a/wgpu/render_pipeline.go +++ b/wgpu/render_pipeline.go @@ -17,6 +17,10 @@ type VertexAttribute struct { // vertexAttributeWire is the FFI-compatible structure with converted Format. // Field order matches webgpu.h: format, offset, shaderLocation type vertexAttributeWire struct { + // NextInChain comes first in wgpu-native v29's webgpu.h. Without it the + // format was read from the offset field and wgpu-native panicked with + // "invalid vertex format for vertex attribute: 0". + NextInChain uintptr Format uint32 // converted from gputypes.VertexFormat _pad1 [4]byte Offset uint64 @@ -236,7 +240,7 @@ func (d *Device) CreateRenderPipeline(desc *RenderPipelineDescriptor) (*RenderPi if len(entryPointBytes) > 0 { nativeVertex.entryPoint = StringView{ - Data: uintptr(unsafe.Pointer(&entryPointBytes[0])), + Data: uintptr(unsafe.Pointer(pin(&entryPointBytes[0]))), Length: uintptr(len(entryPointBytes) - 1), } } else { @@ -262,7 +266,7 @@ func (d *Device) CreateRenderPipeline(desc *RenderPipelineDescriptor) (*RenderPi ShaderLocation: attr.ShaderLocation, } } - attrsPtr = uintptr(unsafe.Pointer(&allNativeAttrs[i][0])) + attrsPtr = uintptr(unsafe.Pointer(pin(&allNativeAttrs[i][0]))) } nativeBuffers[i] = vertexBufferLayoutWire{ NextInChain: 0, // v29: required first field @@ -272,7 +276,7 @@ func (d *Device) CreateRenderPipeline(desc *RenderPipelineDescriptor) (*RenderPi Attributes: attrsPtr, } } - nativeVertex.buffers = uintptr(unsafe.Pointer(&nativeBuffers[0])) + nativeVertex.buffers = uintptr(unsafe.Pointer(pin(&nativeBuffers[0]))) } // Build primitive state @@ -328,7 +332,7 @@ func (d *Device) CreateRenderPipeline(desc *RenderPipelineDescriptor) (*RenderPi depthBiasSlopeScale: desc.DepthStencil.DepthBiasSlopeScale, depthBiasClamp: desc.DepthStencil.DepthBiasClamp, } - depthStencilPtr = uintptr(unsafe.Pointer(&nativeDepthStencil)) + depthStencilPtr = uintptr(unsafe.Pointer(pin(&nativeDepthStencil))) } // Build fragment state if present @@ -352,7 +356,7 @@ func (d *Device) CreateRenderPipeline(desc *RenderPipelineDescriptor) (*RenderPi if len(fragEntryPointBytes) > 0 { nativeFragment.entryPoint = StringView{ - Data: uintptr(unsafe.Pointer(&fragEntryPointBytes[0])), + Data: uintptr(unsafe.Pointer(pin(&fragEntryPointBytes[0]))), Length: uintptr(len(fragEntryPointBytes) - 1), } } else { @@ -368,15 +372,15 @@ func (d *Device) CreateRenderPipeline(desc *RenderPipelineDescriptor) (*RenderPi writeMask: uint64(target.WriteMask), // widen to uint64 } if target.Blend != nil { - nativeTargets[i].blend = uintptr(unsafe.Pointer(target.Blend)) + nativeTargets[i].blend = uintptr(unsafe.Pointer(pin(target.Blend))) } } if len(nativeTargets) > 0 { - nativeFragment.targets = uintptr(unsafe.Pointer(&nativeTargets[0])) + nativeFragment.targets = uintptr(unsafe.Pointer(pin(&nativeTargets[0]))) } - fragmentPtr = uintptr(unsafe.Pointer(&nativeFragment)) + fragmentPtr = uintptr(unsafe.Pointer(pin(&nativeFragment))) } // Build pipeline layout @@ -399,7 +403,7 @@ func (d *Device) CreateRenderPipeline(desc *RenderPipelineDescriptor) (*RenderPi handle, _, _ := procDeviceCreateRenderPipeline.Call( d.handle, - uintptr(unsafe.Pointer(&nativeDesc)), + uintptr(unsafe.Pointer(pin(&nativeDesc))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateRenderPipeline", Message: "wgpu returned null handle"} diff --git a/wgpu/sampler.go b/wgpu/sampler.go index 16177af..69ff814 100644 --- a/wgpu/sampler.go +++ b/wgpu/sampler.go @@ -80,7 +80,7 @@ func (d *Device) CreateSampler(desc *SamplerDescriptor) (*Sampler, error) { handle, _, _ := procDeviceCreateSampler.Call( d.handle, - uintptr(unsafe.Pointer(&wire)), + uintptr(unsafe.Pointer(pin(&wire))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateSampler", Message: "wgpu returned null handle"} diff --git a/wgpu/shader.go b/wgpu/shader.go index 988fda4..ef5a45e 100644 --- a/wgpu/shader.go +++ b/wgpu/shader.go @@ -38,19 +38,19 @@ func (d *Device) CreateShaderModuleWGSL(code string) (*ShaderModule, error) { SType: uint32(STypeShaderSourceWGSL), }, Code: StringView{ - Data: uintptr(unsafe.Pointer(&codeBytes[0])), + Data: uintptr(unsafe.Pointer(pin(&codeBytes[0]))), Length: uintptr(len(codeBytes)), }, } desc := ShaderModuleDescriptor{ - NextInChain: uintptr(unsafe.Pointer(&wgslSource)), + NextInChain: uintptr(unsafe.Pointer(pin(&wgslSource))), Label: EmptyStringView(), } handle, _, _ := procDeviceCreateShaderModule.Call( d.handle, - uintptr(unsafe.Pointer(&desc)), + uintptr(unsafe.Pointer(pin(&desc))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateShaderModuleWGSL", Message: "wgpu returned null handle"} @@ -139,18 +139,18 @@ func (d *Device) CreateShaderModuleSPIRV(label string, spirv []uint32) (*ShaderM Next: 0, SType: uint32(STypeShaderSourceSPIRV), }, - Code: uintptr(unsafe.Pointer(&spirv[0])), + Code: uintptr(unsafe.Pointer(pin(&spirv[0]))), CodeSize: uint32(len(spirv)), } desc := ShaderModuleDescriptor{ - NextInChain: uintptr(unsafe.Pointer(&spirvSource)), + NextInChain: uintptr(unsafe.Pointer(pin(&spirvSource))), Label: stringToStringView(label), } handle, _, _ := procDeviceCreateShaderModule.Call( d.handle, - uintptr(unsafe.Pointer(&desc)), + uintptr(unsafe.Pointer(pin(&desc))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateShaderModuleSPIRV", Message: "wgpu returned null handle"} diff --git a/wgpu/surface.go b/wgpu/surface.go index 71e9223..2413149 100644 --- a/wgpu/surface.go +++ b/wgpu/surface.go @@ -134,7 +134,7 @@ func (s *Surface) Configure(device *Device, config *SurfaceConfiguration) error procSurfaceConfigure.Call( //nolint:errcheck s.handle, - uintptr(unsafe.Pointer(&nativeConfig)), + uintptr(unsafe.Pointer(pin(&nativeConfig))), ) return nil } @@ -169,14 +169,18 @@ func (s *Surface) GetCurrentTexture() (*SurfaceTexture, bool, error) { procSurfaceGetCurrentTexture.Call( //nolint:errcheck s.handle, - uintptr(unsafe.Pointer(&surfTex)), + uintptr(unsafe.Pointer(pin(&surfTex))), ) - result := &SurfaceTexture{ - Texture: &Texture{handle: surfTex.texture}, - Status: surfTex.status, + result := &SurfaceTexture{Status: surfTex.status} + if surfTex.texture != 0 { + result.Texture = &Texture{handle: surfTex.texture} } + // The result is returned on error statuses too (with Status, and with + // Texture only when wgpu handed one out) so the caller can present or + // release it. Dropping the handle leaked the acquired texture and the + // next GetCurrentTexture aborted with "Surface image is already acquired". switch surfTex.status { case SurfaceGetCurrentTextureStatusSuccessOptimal: return result, false, nil @@ -186,20 +190,23 @@ func (s *Surface) GetCurrentTexture() (*SurfaceTexture, bool, error) { case SurfaceGetCurrentTextureStatusOutdated: return result, false, ErrSurfaceNeedsReconfigure case SurfaceGetCurrentTextureStatusLost: - return nil, false, ErrSurfaceLost + return result, false, ErrSurfaceLost case SurfaceGetCurrentTextureStatusTimeout: - return nil, false, ErrSurfaceTimeout + return result, false, ErrSurfaceTimeout case NativeSurfaceGetCurrentTextureStatusOccluded: // wgpu-native v29: window is occluded/minimized (Metal backend only). // No texture is returned; caller should skip this frame and try again. - return nil, false, ErrSurfaceOccluded + return result, false, ErrSurfaceOccluded default: // v29: SurfaceGetCurrentTextureStatusError (0x06) covers all error cases // including former OutOfMemory (0x06) and DeviceLost (0x07). - return nil, false, &WGPUError{Op: "Surface.GetCurrentTexture", Message: "failed to get surface texture"} + return result, false, &WGPUError{Op: "Surface.GetCurrentTexture", Message: "failed to get surface texture"} } } +// wgpuStatusSuccess is WGPUStatus_Success from webgpu.h (WGPUStatus_Error = 2). +const wgpuStatusSuccess = 0x00000001 + // Present presents the current frame to the surface. // The texture argument is accepted for API compatibility with gogpu/wgpu but // is unused in the FFI implementation (wgpuSurfacePresent takes no texture arg). @@ -209,7 +216,15 @@ func (s *Surface) Present(texture ...*SurfaceTexture) error { if s == nil || s.handle == 0 { return nil } - procSurfacePresent.Call(s.handle) //nolint:errcheck + // wgpuSurfacePresent returns a WGPUStatus. On failure the acquired + // texture was not presented (wgpu already dropped the acquisition, or + // there was none) and the caller must not release it as presented: + // wgpu-native would discard it a second time and abort with "already + // acquired". + status, _, _ := procSurfacePresent.Call(s.handle) + if status != wgpuStatusSuccess { + return &WGPUError{Op: "Surface.Present", Message: "presentation failed"} + } return nil } @@ -245,7 +260,7 @@ func (s *Surface) GetCapabilities(adapter *Adapter) (*SurfaceCapabilities, error procSurfaceGetCapabilities.Call( //nolint:errcheck s.handle, adapter.handle, - uintptr(unsafe.Pointer(&wire)), + uintptr(unsafe.Pointer(pin(&wire))), ) // Convert wire struct to Go struct @@ -281,7 +296,7 @@ func (s *Surface) GetCapabilities(adapter *Adapter) (*SurfaceCapabilities, error } // Free C memory allocated by wgpu-native - procSurfaceCapabilitiesFreeMembers.Call(uintptr(unsafe.Pointer(&wire))) //nolint:errcheck + procSurfaceCapabilitiesFreeMembers.Call(uintptr(unsafe.Pointer(pin(&wire)))) //nolint:errcheck return caps, nil } diff --git a/wgpu/surface_android.go b/wgpu/surface_android.go index 550ad00..76471a4 100644 --- a/wgpu/surface_android.go +++ b/wgpu/surface_android.go @@ -22,13 +22,13 @@ func (inst *Instance) CreateSurfaceFromAndroidNativeWindow(window uintptr) (*Sur return nil, err } desc := surfaceDescriptor{ - nextInChain: uintptr(unsafe.Pointer(&source)), + nextInChain: uintptr(unsafe.Pointer(pin(&source))), label: EmptyStringView(), } handle, _, _ := procInstanceCreateSurface.Call( inst.handle, - uintptr(unsafe.Pointer(&desc)), + uintptr(unsafe.Pointer(pin(&desc))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateSurface", Message: "failed to create surface"} diff --git a/wgpu/surface_darwin.go b/wgpu/surface_darwin.go index da30664..2c0d1fd 100644 --- a/wgpu/surface_darwin.go +++ b/wgpu/surface_darwin.go @@ -33,13 +33,13 @@ func (inst *Instance) CreateSurfaceFromMetalLayer(layer uintptr) (*Surface, erro // Build WGPUSurfaceDescriptor with source chained desc := surfaceDescriptor{ - nextInChain: uintptr(unsafe.Pointer(&source)), + nextInChain: uintptr(unsafe.Pointer(pin(&source))), label: EmptyStringView(), } handle, _, _ := procInstanceCreateSurface.Call( inst.handle, - uintptr(unsafe.Pointer(&desc)), + uintptr(unsafe.Pointer(pin(&desc))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateSurface", Message: "failed to create surface"} diff --git a/wgpu/surface_linux.go b/wgpu/surface_linux.go index 8e0ed91..9f8de4f 100644 --- a/wgpu/surface_linux.go +++ b/wgpu/surface_linux.go @@ -43,13 +43,13 @@ func (inst *Instance) CreateSurfaceFromXlibWindow(display uintptr, window uint64 // Build WGPUSurfaceDescriptor with source chained desc := surfaceDescriptor{ - nextInChain: uintptr(unsafe.Pointer(&source)), + nextInChain: uintptr(unsafe.Pointer(pin(&source))), label: EmptyStringView(), } handle, _, _ := procInstanceCreateSurface.Call( inst.handle, - uintptr(unsafe.Pointer(&desc)), + uintptr(unsafe.Pointer(pin(&desc))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateSurface", Message: "failed to create surface"} @@ -82,13 +82,13 @@ func (inst *Instance) CreateSurfaceFromWaylandSurface(display, surface uintptr) // Build WGPUSurfaceDescriptor with source chained desc := surfaceDescriptor{ - nextInChain: uintptr(unsafe.Pointer(&source)), + nextInChain: uintptr(unsafe.Pointer(pin(&source))), label: EmptyStringView(), } handle, _, _ := procInstanceCreateSurface.Call( inst.handle, - uintptr(unsafe.Pointer(&desc)), + uintptr(unsafe.Pointer(pin(&desc))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateSurface", Message: "failed to create surface"} diff --git a/wgpu/surface_windows.go b/wgpu/surface_windows.go index 7086947..b82f3cc 100644 --- a/wgpu/surface_windows.go +++ b/wgpu/surface_windows.go @@ -36,13 +36,13 @@ func (inst *Instance) CreateSurfaceFromWindowsHWND(hinstance, hwnd uintptr) (*Su // Build WGPUSurfaceDescriptor with source chained desc := surfaceDescriptor{ - nextInChain: uintptr(unsafe.Pointer(&source)), + nextInChain: uintptr(unsafe.Pointer(pin(&source))), label: EmptyStringView(), } handle, _, _ := procInstanceCreateSurface.Call( inst.handle, - uintptr(unsafe.Pointer(&desc)), + uintptr(unsafe.Pointer(pin(&desc))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateSurface", Message: "failed to create surface"} diff --git a/wgpu/texture.go b/wgpu/texture.go index 7f3bdf1..b5f9a05 100644 --- a/wgpu/texture.go +++ b/wgpu/texture.go @@ -88,7 +88,7 @@ func (t *Texture) CreateView(desc *TextureViewDescriptor) (*TextureView, error) Aspect: desc.Aspect, Usage: uint64(desc.Usage), // bitflags, uint64 in wgpu-native } - descPtr = uintptr(unsafe.Pointer(&wireDesc)) + descPtr = uintptr(unsafe.Pointer(pin(&wireDesc))) } handle, _, _ := procTextureCreateView.Call( @@ -219,7 +219,7 @@ func (d *Device) CreateTexture(desc *TextureDescriptor) (*Texture, error) { wireFormats[i] = uint32(f) } viewFormatCount = uintptr(len(wireFormats)) - viewFormatsPtr = uintptr(unsafe.Pointer(&wireFormats[0])) + viewFormatsPtr = uintptr(unsafe.Pointer(pin(&wireFormats[0]))) } // Convert to wire format with wgpu-native enum values @@ -237,7 +237,7 @@ func (d *Device) CreateTexture(desc *TextureDescriptor) (*Texture, error) { handle, _, _ := procDeviceCreateTexture.Call( d.handle, - uintptr(unsafe.Pointer(&wireDesc)), + uintptr(unsafe.Pointer(pin(&wireDesc))), ) if handle == 0 { return nil, &WGPUError{Op: "CreateTexture", Message: "wgpu returned null handle"} @@ -322,10 +322,10 @@ func (q *Queue) WriteTexture(dest *ImageCopyTexture, data []byte, layout *ImageD } procQueueWriteTexture.Call( //nolint:errcheck q.handle, - uintptr(unsafe.Pointer(&wire)), - uintptr(unsafe.Pointer(&data[0])), + uintptr(unsafe.Pointer(pin(&wire))), + uintptr(unsafe.Pointer(pin(&data[0]))), uintptr(len(data)), - uintptr(unsafe.Pointer(&wireLayout)), + uintptr(unsafe.Pointer(pin(&wireLayout))), uintptr(unsafe.Pointer(size)), ) return nil @@ -341,7 +341,7 @@ func (q *Queue) WriteTextureRaw(dest *TexelCopyTextureInfo, data []byte, layout procQueueWriteTexture.Call( //nolint:errcheck q.handle, uintptr(unsafe.Pointer(dest)), - uintptr(unsafe.Pointer(&data[0])), + uintptr(unsafe.Pointer(pin(&data[0]))), uintptr(len(data)), uintptr(unsafe.Pointer(layout)), uintptr(unsafe.Pointer(size)), diff --git a/wgpu/wgpu.go b/wgpu/wgpu.go index 9963c2e..2a2f53e 100644 --- a/wgpu/wgpu.go +++ b/wgpu/wgpu.go @@ -15,182 +15,182 @@ var ( initErr error // Function pointers - Instance - procCreateInstance Proc - procInstanceRelease Proc - procInstanceProcessEvents Proc + procCreateInstance *Proc + procInstanceRelease *Proc + procInstanceProcessEvents *Proc // Function pointers - Adapter - procAdapterRelease Proc - procInstanceRequestAdapter Proc - procAdapterRequestDevice Proc - procAdapterGetLimits Proc - procAdapterGetFeatures Proc // v29: replaces EnumerateFeatures (single-call with SupportedFeatures) - procSupportedFeaturesFreeMembers Proc - procAdapterHasFeature Proc - procAdapterGetInfo Proc - procAdapterInfoFreeMembers Proc + procAdapterRelease *Proc + procInstanceRequestAdapter *Proc + procAdapterRequestDevice *Proc + procAdapterGetLimits *Proc + procAdapterGetFeatures *Proc // v29: replaces EnumerateFeatures (single-call with SupportedFeatures) + procSupportedFeaturesFreeMembers *Proc + procAdapterHasFeature *Proc + procAdapterGetInfo *Proc + procAdapterInfoFreeMembers *Proc // Function pointers - Device - procDeviceRelease Proc - procDeviceGetQueue Proc - procDeviceCreateBuffer Proc - procDevicePoll Proc // wgpu-native extension - procDevicePushErrorScope Proc - procDevicePopErrorScope Proc - procDeviceGetFeatures Proc - procDeviceHasFeature Proc - procDeviceGetLimits Proc + procDeviceRelease *Proc + procDeviceGetQueue *Proc + procDeviceCreateBuffer *Proc + procDevicePoll *Proc // wgpu-native extension + procDevicePushErrorScope *Proc + procDevicePopErrorScope *Proc + procDeviceGetFeatures *Proc + procDeviceHasFeature *Proc + procDeviceGetLimits *Proc // Function pointers - Queue - procQueueRelease Proc - procQueueWriteBuffer Proc - procQueueGetTimestampPeriod Proc + procQueueRelease *Proc + procQueueWriteBuffer *Proc + procQueueGetTimestampPeriod *Proc // Function pointers - Instance (global) - procGetInstanceFeatures Proc // v29: global instance feature query - procGetInstanceLimits Proc // v29: global instance limits query - procHasInstanceFeature Proc // v29: check a single instance feature + procGetInstanceFeatures *Proc // v29: global instance feature query + procGetInstanceLimits *Proc // v29: global instance limits query + procHasInstanceFeature *Proc // v29: check a single instance feature // Function pointers - Buffer - procBufferRelease Proc - procBufferDestroy Proc - procBufferGetMappedRange Proc - procBufferReadMappedRange Proc // v29: explicit read mapped range - procBufferWriteMappedRange Proc // v29: explicit write mapped range - procBufferUnmap Proc - procBufferGetSize Proc - procBufferMapAsync Proc - procBufferGetUsage Proc - procBufferGetMapState Proc + procBufferRelease *Proc + procBufferDestroy *Proc + procBufferGetMappedRange *Proc + procBufferReadMappedRange *Proc // v29: explicit read mapped range + procBufferWriteMappedRange *Proc // v29: explicit write mapped range + procBufferUnmap *Proc + procBufferGetSize *Proc + procBufferMapAsync *Proc + procBufferGetUsage *Proc + procBufferGetMapState *Proc // Function pointers - ShaderModule - procDeviceCreateShaderModule Proc - procShaderModuleRelease Proc + procDeviceCreateShaderModule *Proc + procShaderModuleRelease *Proc // Function pointers - BindGroupLayout - procDeviceCreateBindGroupLayout Proc - procBindGroupLayoutRelease Proc + procDeviceCreateBindGroupLayout *Proc + procBindGroupLayoutRelease *Proc // Function pointers - BindGroup - procDeviceCreateBindGroup Proc - procBindGroupRelease Proc + procDeviceCreateBindGroup *Proc + procBindGroupRelease *Proc // Function pointers - PipelineLayout - procDeviceCreatePipelineLayout Proc - procPipelineLayoutRelease Proc + procDeviceCreatePipelineLayout *Proc + procPipelineLayoutRelease *Proc // Function pointers - ComputePipeline - procDeviceCreateComputePipeline Proc - procComputePipelineGetBindGroupLayout Proc - procComputePipelineRelease Proc + procDeviceCreateComputePipeline *Proc + procComputePipelineGetBindGroupLayout *Proc + procComputePipelineRelease *Proc // Function pointers - CommandEncoder - procDeviceCreateCommandEncoder Proc - procCommandEncoderBeginComputePass Proc - procCommandEncoderCopyBufferToBuffer Proc - procCommandEncoderCopyBufferToTexture Proc - procCommandEncoderCopyTextureToBuffer Proc - procCommandEncoderCopyTextureToTexture Proc - procCommandEncoderClearBuffer Proc - procCommandEncoderInsertDebugMarker Proc - procCommandEncoderPushDebugGroup Proc - procCommandEncoderPopDebugGroup Proc - procCommandEncoderFinish Proc - procCommandEncoderRelease Proc + procDeviceCreateCommandEncoder *Proc + procCommandEncoderBeginComputePass *Proc + procCommandEncoderCopyBufferToBuffer *Proc + procCommandEncoderCopyBufferToTexture *Proc + procCommandEncoderCopyTextureToBuffer *Proc + procCommandEncoderCopyTextureToTexture *Proc + procCommandEncoderClearBuffer *Proc + procCommandEncoderInsertDebugMarker *Proc + procCommandEncoderPushDebugGroup *Proc + procCommandEncoderPopDebugGroup *Proc + procCommandEncoderFinish *Proc + procCommandEncoderRelease *Proc // Function pointers - ComputePassEncoder - procComputePassEncoderSetPipeline Proc - procComputePassEncoderSetBindGroup Proc - procComputePassEncoderDispatchWorkgroups Proc - procComputePassEncoderDispatchWorkgroupsIndirect Proc - procComputePassEncoderEnd Proc - procComputePassEncoderRelease Proc + procComputePassEncoderSetPipeline *Proc + procComputePassEncoderSetBindGroup *Proc + procComputePassEncoderDispatchWorkgroups *Proc + procComputePassEncoderDispatchWorkgroupsIndirect *Proc + procComputePassEncoderEnd *Proc + procComputePassEncoderRelease *Proc // Function pointers - CommandBuffer - procCommandBufferRelease Proc + procCommandBufferRelease *Proc // Function pointers - Queue (additional) - procQueueSubmit Proc - procQueueSubmitForIndex Proc // wgpu-native extension: returns WGPUSubmissionIndex (uint64) + procQueueSubmit *Proc + procQueueSubmitForIndex *Proc // wgpu-native extension: returns WGPUSubmissionIndex (uint64) // Function pointers - Surface - procInstanceCreateSurface Proc - procSurfaceRelease Proc - procSurfaceConfigure Proc - procSurfaceUnconfigure Proc - procSurfaceGetCapabilities Proc - procSurfaceCapabilitiesFreeMembers Proc - procSurfaceGetCurrentTexture Proc - procSurfacePresent Proc + procInstanceCreateSurface *Proc + procSurfaceRelease *Proc + procSurfaceConfigure *Proc + procSurfaceUnconfigure *Proc + procSurfaceGetCapabilities *Proc + procSurfaceCapabilitiesFreeMembers *Proc + procSurfaceGetCurrentTexture *Proc + procSurfacePresent *Proc // Function pointers - Texture - procDeviceCreateTexture Proc - procTextureRelease Proc - procTextureDestroy Proc - procTextureCreateView Proc - procTextureViewRelease Proc - procTextureGetWidth Proc - procTextureGetHeight Proc - procTextureGetDepthOrArrayLayers Proc - procTextureGetMipLevelCount Proc - procTextureGetFormat Proc - procTextureGetSampleCount Proc // v29: new getter - procTextureGetUsage Proc // v29: new getter - procTextureGetTextureBindingViewDimension Proc // v29: new getter + procDeviceCreateTexture *Proc + procTextureRelease *Proc + procTextureDestroy *Proc + procTextureCreateView *Proc + procTextureViewRelease *Proc + procTextureGetWidth *Proc + procTextureGetHeight *Proc + procTextureGetDepthOrArrayLayers *Proc + procTextureGetMipLevelCount *Proc + procTextureGetFormat *Proc + procTextureGetSampleCount *Proc // v29: new getter + procTextureGetUsage *Proc // v29: new getter + procTextureGetTextureBindingViewDimension *Proc // v29: new getter // Function pointers - Sampler - procDeviceCreateSampler Proc - procSamplerRelease Proc + procDeviceCreateSampler *Proc + procSamplerRelease *Proc // Function pointers - Queue (texture operations) - procQueueWriteTexture Proc + procQueueWriteTexture *Proc // Function pointers - RenderPass - procCommandEncoderBeginRenderPass Proc - procRenderPassEncoderSetPipeline Proc - procRenderPassEncoderSetBindGroup Proc - procRenderPassEncoderSetVertexBuffer Proc - procRenderPassEncoderSetIndexBuffer Proc - procRenderPassEncoderDraw Proc - procRenderPassEncoderDrawIndexed Proc - procRenderPassEncoderDrawIndirect Proc - procRenderPassEncoderDrawIndexedIndirect Proc - procRenderPassEncoderEnd Proc - procRenderPassEncoderRelease Proc - procRenderPassEncoderSetViewport Proc - procRenderPassEncoderSetScissorRect Proc - procRenderPassEncoderSetBlendConstant Proc - procRenderPassEncoderSetStencilReference Proc - procRenderPassEncoderInsertDebugMarker Proc - procRenderPassEncoderPushDebugGroup Proc - procRenderPassEncoderPopDebugGroup Proc + procCommandEncoderBeginRenderPass *Proc + procRenderPassEncoderSetPipeline *Proc + procRenderPassEncoderSetBindGroup *Proc + procRenderPassEncoderSetVertexBuffer *Proc + procRenderPassEncoderSetIndexBuffer *Proc + procRenderPassEncoderDraw *Proc + procRenderPassEncoderDrawIndexed *Proc + procRenderPassEncoderDrawIndirect *Proc + procRenderPassEncoderDrawIndexedIndirect *Proc + procRenderPassEncoderEnd *Proc + procRenderPassEncoderRelease *Proc + procRenderPassEncoderSetViewport *Proc + procRenderPassEncoderSetScissorRect *Proc + procRenderPassEncoderSetBlendConstant *Proc + procRenderPassEncoderSetStencilReference *Proc + procRenderPassEncoderInsertDebugMarker *Proc + procRenderPassEncoderPushDebugGroup *Proc + procRenderPassEncoderPopDebugGroup *Proc // Function pointers - RenderPipeline - procDeviceCreateRenderPipeline Proc - procRenderPipelineRelease Proc - procRenderPipelineGetBindGroupLayout Proc + procDeviceCreateRenderPipeline *Proc + procRenderPipelineRelease *Proc + procRenderPipelineGetBindGroupLayout *Proc // Function pointers - QuerySet - procDeviceCreateQuerySet Proc - procQuerySetDestroy Proc - procQuerySetRelease Proc - procCommandEncoderWriteTimestamp Proc - procCommandEncoderResolveQuerySet Proc + procDeviceCreateQuerySet *Proc + procQuerySetDestroy *Proc + procQuerySetRelease *Proc + procCommandEncoderWriteTimestamp *Proc + procCommandEncoderResolveQuerySet *Proc // Function pointers - RenderBundle - procDeviceCreateRenderBundleEncoder Proc - procRenderBundleEncoderSetPipeline Proc - procRenderBundleEncoderSetBindGroup Proc - procRenderBundleEncoderSetVertexBuffer Proc - procRenderBundleEncoderSetIndexBuffer Proc - procRenderBundleEncoderDraw Proc - procRenderBundleEncoderDrawIndexed Proc - procRenderBundleEncoderDrawIndirect Proc - procRenderBundleEncoderDrawIndexedIndirect Proc - procRenderBundleEncoderFinish Proc - procRenderBundleEncoderRelease Proc - procRenderBundleRelease Proc - procRenderPassEncoderExecuteBundles Proc + procDeviceCreateRenderBundleEncoder *Proc + procRenderBundleEncoderSetPipeline *Proc + procRenderBundleEncoderSetBindGroup *Proc + procRenderBundleEncoderSetVertexBuffer *Proc + procRenderBundleEncoderSetIndexBuffer *Proc + procRenderBundleEncoderDraw *Proc + procRenderBundleEncoderDrawIndexed *Proc + procRenderBundleEncoderDrawIndirect *Proc + procRenderBundleEncoderDrawIndexedIndirect *Proc + procRenderBundleEncoderFinish *Proc + procRenderBundleEncoderRelease *Proc + procRenderBundleRelease *Proc + procRenderPassEncoderExecuteBundles *Proc ) // Init initializes the wgpu library. Called automatically on first use.