From 3013111cd8bba283bbeecf1ed758a0c436235854 Mon Sep 17 00:00:00 2001 From: Javier Podavini Date: Tue, 22 Sep 2026 20:17:48 -0300 Subject: [PATCH 1/5] fix(wgpu): match wgpu-native v29 wire layouts Three wire structs were missing fields that the webgpu.h shipped with wgpu-native v29.0.0.0 has, so every field after them was read from the wrong offset: - WGPUBindGroupLayoutEntry: bindingArraySize (uint32 + padding) between visibility and buffer. wgpu-native panicked with "invalid buffer binding type for buffer binding layout at binding N". - WGPUVertexAttribute: the leading nextInChain. wgpu-native read the format from the offset field and panicked with "invalid vertex format for vertex attribute: 0". - WGPURenderPassDepthStencilAttachment: the leading nextInChain. TestABIWireStructAlignment now asserts the v29 layouts instead of the old "migration gap" expectations. The binding's own test suite passes against the official v29.0.0.0 DLL with these changes. --- wgpu/abi_test.go | 73 ++++++++--------------------------------- wgpu/bindgroup.go | 22 ++++++++----- wgpu/render.go | 2 ++ wgpu/render_pipeline.go | 4 +++ 4 files changed, 34 insertions(+), 67 deletions(-) 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/bindgroup.go b/wgpu/bindgroup.go index 1e8cd7d..2a6df8e 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. diff --git a/wgpu/render.go b/wgpu/render.go index ad95bb8..001b3a5 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 diff --git a/wgpu/render_pipeline.go b/wgpu/render_pipeline.go index 5b3bded..9ef6e4c 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 From aeef556481041488427eff2970bb5d3f1f38c807 Mon Sep 17 00:00:00 2001 From: Javier Podavini Date: Tue, 22 Sep 2026 20:17:48 -0300 Subject: [PATCH 2/5] fix(wgpu): copy adapter strings, keep surface textures on error, report failed present - stringViewToString aliased wgpu-native memory with unsafe.String; the adapter name turned into garbage once wgpu-native reused the buffer. Copy the bytes instead. - Surface.GetCurrentTexture returned nil on Lost/Timeout/Occluded/Error even when wgpu had handed out a texture. The caller could not present or release it, the swapchain image stayed acquired and the next call aborted the process with "Surface image is already acquired". Return the SurfaceTexture (status, and the texture only when the handle is non-zero) together with the error. - Surface.Present ignored the WGPUStatus of wgpuSurfacePresent. When the present fails wgpu-native has already dropped the acquisition, and releasing the texture afterwards discards it a second time (fatal "already acquired" in wgpuTextureRelease). Return an error so callers can skip the release. --- wgpu/adapter.go | 5 ++++- wgpu/surface.go | 31 +++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/wgpu/adapter.go b/wgpu/adapter.go index b2f69d5..da9f5b4 100644 --- a/wgpu/adapter.go +++ b/wgpu/adapter.go @@ -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/surface.go b/wgpu/surface.go index 71e9223..dd47bb4 100644 --- a/wgpu/surface.go +++ b/wgpu/surface.go @@ -172,11 +172,15 @@ func (s *Surface) GetCurrentTexture() (*SurfaceTexture, bool, error) { uintptr(unsafe.Pointer(&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 } From d8b2a124aee5bb06685bf3afee993a16986d5c79 Mon Sep 17 00:00:00 2001 From: Javier Podavini Date: Tue, 22 Sep 2026 20:17:48 -0300 Subject: [PATCH 3/5] fix(wgpu): keep uintptr-converted Go pointers alive and pinned during Proc.Call Every binding method passes Go structs to wgpu-native as uintptr(unsafe.Pointer(&local)). Proc was an interface, so the conversion happened in a call that lacked //go:uintptrescapes: the local stayed on the goroutine stack and could move (stack growth, GC shrink) between the conversion and the syscall. wgpu-native then read stale input or wrote its output to the old stack. In practice wgpuSurfaceGetCurrentTexture "returned" a zeroed WGPUSurfaceTexture (status 0, texture 0) while the texture had really been acquired, and the process aborted on the next present or configure ("Surface image is already acquired" / "SurfaceOutput must be dropped before a new Surface is made"), about once every few starts. Proc is now a concrete struct wrapping the platform implementation and Proc.Call carries //go:uintptrescapes, which the compiler only honors on direct calls. Library.NewProc returns *Proc; the platform loaders wrap their implementation with newProc. This is a breaking change only for code implementing its own Library/Proc. --- wgpu/command.go | 2 +- wgpu/loader.go | 37 +++- wgpu/loader_unix.go | 14 +- wgpu/loader_windows.go | 6 +- wgpu/queue_timestamp_period_test.go | 8 +- wgpu/wgpu.go | 264 ++++++++++++++-------------- 6 files changed, 179 insertions(+), 152 deletions(-) diff --git a/wgpu/command.go b/wgpu/command.go index 1849f8e..3c7b493 100644 --- a/wgpu/command.go +++ b/wgpu/command.go @@ -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/loader.go b/wgpu/loader.go index fe66a20..1293aec 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(&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/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/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. From bd31e2c756df5313b485beb745a7093829cafc07 Mon Sep 17 00:00:00 2001 From: Javier Podavini Date: Tue, 22 Sep 2026 20:17:49 -0300 Subject: [PATCH 4/5] docs: changelog entries for the v29 ABI, surface and Proc.Call fixes --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92fbdaf..191372a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,41 @@ 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). + +### 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 From 61ba8a6096c5ed58bd6cc4907c9ce2dd5ef2bc79 Mon Sep 17 00:00:00 2001 From: Javier Podavini Date: Wed, 23 Sep 2026 01:41:22 -0300 Subject: [PATCH 5/5] fix(wgpu): pin nested descriptors on the heap //go:uintptrescapes on Proc.Call only protects pointers converted to uintptr directly in the call's argument list. Many descriptors reach wgpu-native nested: their address is stored as uintptr inside another struct (render pass color/depth attachments, SetBindGroup dynamic offsets, color target Blend, surface sources, command-buffer arrays) or converted before the call. When those values live on the goroutine stack, a stack growth or shrink between the conversion and the native call leaves wgpu-native reading stale memory. pin[T](p *T) *T returns p unchanged but forces the object onto the heap (an escape through a package-level sink the compiler cannot rule out); the Go heap does not move objects. Every uintptr(unsafe.Pointer(&x)) in the package now goes through pin, plus the two already-taken pointers stored in other descriptors. Cost: those structs are heap-allocated. Tested: go vet on windows/linux/darwin; go test ./wgpu passes with the wgpu-native v29.0.0.0 DLL on Windows (D3D12/Vulkan adapter). Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 6 ++++++ wgpu/adapter.go | 14 +++++++------- wgpu/bindgroup.go | 8 ++++---- wgpu/buffer.go | 4 ++-- wgpu/command.go | 34 +++++++++++++++++----------------- wgpu/descriptors.go | 2 +- wgpu/device.go | 14 +++++++------- wgpu/errors.go | 2 +- wgpu/instance.go | 2 +- wgpu/loader.go | 2 +- wgpu/map_pending.go | 2 +- wgpu/pin.go | 24 ++++++++++++++++++++++++ wgpu/pipeline.go | 8 ++++---- wgpu/queryset.go | 2 +- wgpu/render.go | 18 +++++++++--------- wgpu/render_bundle.go | 10 +++++----- wgpu/render_pipeline.go | 18 +++++++++--------- wgpu/sampler.go | 2 +- wgpu/shader.go | 12 ++++++------ wgpu/surface.go | 8 ++++---- wgpu/surface_android.go | 4 ++-- wgpu/surface_darwin.go | 4 ++-- wgpu/surface_linux.go | 8 ++++---- wgpu/surface_windows.go | 4 ++-- wgpu/texture.go | 14 +++++++------- 25 files changed, 128 insertions(+), 98 deletions(-) create mode 100644 wgpu/pin.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 191372a..f717233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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 diff --git a/wgpu/adapter.go b/wgpu/adapter.go index da9f5b4..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 } diff --git a/wgpu/bindgroup.go b/wgpu/bindgroup.go index 2a6df8e..4c2cd16 100644 --- a/wgpu/bindgroup.go +++ b/wgpu/bindgroup.go @@ -243,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"} @@ -301,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{ @@ -313,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 3c7b493..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 } 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 1293aec..5c2ae2f 100644 --- a/wgpu/loader.go +++ b/wgpu/loader.go @@ -23,7 +23,7 @@ type procImpl interface { // // Proc is a concrete type (not an interface) so that Call can carry // //go:uintptrescapes. Callers pass Go structs as -// uintptr(unsafe.Pointer(&local)); without the directive those locals stay +// 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 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/render.go b/wgpu/render.go index 001b3a5..b76309b 100644 --- a/wgpu/render.go +++ b/wgpu/render.go @@ -174,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) @@ -187,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, @@ -202,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"} @@ -230,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)) } @@ -416,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))), ) } @@ -437,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 9ef6e4c..302eacf 100644 --- a/wgpu/render_pipeline.go +++ b/wgpu/render_pipeline.go @@ -240,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 { @@ -266,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 @@ -276,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 @@ -332,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 @@ -356,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 { @@ -372,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 @@ -403,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 dd47bb4..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,7 +169,7 @@ func (s *Surface) GetCurrentTexture() (*SurfaceTexture, bool, error) { procSurfaceGetCurrentTexture.Call( //nolint:errcheck s.handle, - uintptr(unsafe.Pointer(&surfTex)), + uintptr(unsafe.Pointer(pin(&surfTex))), ) result := &SurfaceTexture{Status: surfTex.status} @@ -260,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 @@ -296,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)),