Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ 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

### Added

- `InstanceDescriptor.NativeBackends`: chains `WGPUInstanceExtras` to
`wgpuCreateInstance` so callers can restrict the backends wgpu-native
enumerates (e.g. `InstanceBackendDX12`). Previously the descriptor was
ignored and there was no way to pick a backend.
- Windows: `Instance.CreateSurfaceFromSwapChainPanel` for
`WGPUSurfaceSourceSwapChainPanel` (DX12 composition swap chains handed to an
`ISwapChainPanelNative`), with v29 wire-layout tests.

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

### Changed
Expand Down
37 changes: 37 additions & 0 deletions wgpu/abi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ func TestABIStructSizes(t *testing.T) {
{"instanceDescriptorWire", unsafe.Sizeof(instanceDescriptorWire{}), 32},
// InstanceLimits: nextInChain(8)+timedWaitAnyMaxCount(8) = 16
{"InstanceLimits", unsafe.Sizeof(InstanceLimits{}), 16},
// nativeDisplayHandleWire: type(4)+pad(4)+union(16) = 24
{"nativeDisplayHandleWire", unsafe.Sizeof(nativeDisplayHandleWire{}), 24},
// instanceExtrasWire: chain(16)+backends(8)+flags(8)+3 enums(12)+pad(4)+
// dxcPath(16)+2 enums(8)+2 budgets(16)+displayHandle(24) = 112
{"instanceExtrasWire", unsafe.Sizeof(instanceExtrasWire{}), 112},

// Adapter-level structs
// requestAdapterOptionsWire: nextInChain(8)+featureLevel(4)+powerPreference(4)+
Expand Down Expand Up @@ -269,6 +274,37 @@ func TestABIStructFieldOffsets(t *testing.T) {
}
})

t.Run("instanceExtrasWire", func(t *testing.T) {
var e instanceExtrasWire
offsets := []struct {
name string
got uintptr
expected uintptr
}{
{"Chain", unsafe.Offsetof(e.Chain), 0},
{"Backends", unsafe.Offsetof(e.Backends), 16},
{"Flags", unsafe.Offsetof(e.Flags), 24},
{"Dx12ShaderCompiler", unsafe.Offsetof(e.Dx12ShaderCompiler), 32},
{"Gles3MinorVersion", unsafe.Offsetof(e.Gles3MinorVersion), 36},
{"GLFenceBehaviour", unsafe.Offsetof(e.GLFenceBehaviour), 40},
{"DxcPath", unsafe.Offsetof(e.DxcPath), 48},
{"DxcMaxShaderModel", unsafe.Offsetof(e.DxcMaxShaderModel), 64},
{"Dx12PresentationSystem", unsafe.Offsetof(e.Dx12PresentationSystem), 68},
{"BudgetForDeviceCreation", unsafe.Offsetof(e.BudgetForDeviceCreation), 72},
{"BudgetForDeviceLoss", unsafe.Offsetof(e.BudgetForDeviceLoss), 80},
{"DisplayHandle", unsafe.Offsetof(e.DisplayHandle), 88},
}
for _, o := range offsets {
o := o
t.Run(o.name, func(t *testing.T) {
if o.got != o.expected {
t.Errorf("offsetof(instanceExtrasWire.%s) = %d, want %d",
o.name, o.got, o.expected)
}
})
}
})

t.Run("requestAdapterOptionsWire", func(t *testing.T) {
// nextInChain(0)+featureLevel(8)+powerPreference(12)+
// forceFallbackAdapter(16)+backendType(20)+compatibleSurface(24) = 32
Expand Down Expand Up @@ -600,6 +636,7 @@ func TestABIEnumValues(t *testing.T) {
{"InstanceExtras", uint32(STypeInstanceExtras), 0x00030006},
{"BindGroupEntryExtras", uint32(STypeBindGroupEntryExtras), 0x00030007},
{"BindGroupLayoutEntryExtras", uint32(STypeBindGroupLayoutEntryExtras), 0x00030008},
{"SurfaceSourceSwapChainPanel", uint32(STypeSurfaceSourceSwapChainPanel), 0x0003000B},
}
runEnumTests(t, tests)
})
Expand Down
46 changes: 45 additions & 1 deletion wgpu/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,45 @@ type InstanceDescriptor struct {
// Flags controls instance features like debug layers and validation.
// Use gputypes.InstanceFlagsDebug to enable GPU debug layer.
Flags gputypes.InstanceFlags
// NativeBackends, when non-zero, is chained to wgpuCreateInstance as
// WGPUInstanceExtras.backends and restricts the backends wgpu-native
// enumerates (for example InstanceBackendDX12 to pick Direct3D 12 on
// Windows, where the default adapter may otherwise be Vulkan).
// Zero keeps the wgpu-native default (all backends).
NativeBackends InstanceBackend
}

// nativeDisplayHandleWire matches WGPUNativeDisplayHandle in the wgpu-native
// v29 header: type(4)+pad(4)+union{xlib,xcb,wayland}(16) = 24 bytes.
type nativeDisplayHandleWire struct {
Type uint32
_ uint32
Data [2]uintptr
}

// instanceExtrasWire matches WGPUInstanceExtras in the wgpu-native v29 header
// (112 bytes on 64-bit targets).
type instanceExtrasWire struct {
Chain ChainedStruct // 0
Backends uint64 // 16: WGPUInstanceBackend
Flags uint64 // 24: WGPUInstanceFlag
Dx12ShaderCompiler uint32 // 32: WGPUDx12Compiler
Gles3MinorVersion uint32 // 36: WGPUGles3MinorVersion
GLFenceBehaviour uint32 // 40: WGPUGLFenceBehaviour
_ uint32 // 44: padding
DxcPath StringView // 48
DxcMaxShaderModel uint32 // 64: WGPUDxcMaxShaderModel
Dx12PresentationSystem uint32 // 68: WGPUDx12SwapchainKind
BudgetForDeviceCreation uintptr // 72: const uint8_t* (nullable)
BudgetForDeviceLoss uintptr // 80: const uint8_t* (nullable)
DisplayHandle nativeDisplayHandleWire // 88
}

// instanceExtrasSink keeps the last chained WGPUInstanceExtras on the heap.
// The descriptor refers to it only through a uintptr, so a stack-allocated
// value could be moved by stack growth before the native call reads it.
var instanceExtrasSink *instanceExtrasWire

// instanceDescriptorWire is the FFI-compatible C-layout struct for wgpuCreateInstance.
// v29 layout: nextInChain(8)+requiredFeatureCount(8)+requiredFeatures(8)+requiredLimits(8) = 32 bytes.
// The v27 InstanceCapabilities/Features field is removed in v29.
Expand Down Expand Up @@ -68,7 +105,14 @@ func CreateInstance(desc *InstanceDescriptor) (*Instance, error) {
// When desc is nil, pass null to wgpu-native for default behavior.
var wirePtr uintptr
if desc != nil {
wire := instanceDescriptorWire{} // zero = default, backends/flags handled by wgpu-native extensions
wire := instanceDescriptorWire{} // zero = default
if desc.NativeBackends != 0 {
extras := &instanceExtrasWire{}
instanceExtrasSink = extras
extras.Chain.SType = uint32(STypeInstanceExtras)
extras.Backends = uint64(desc.NativeBackends)
wire.NextInChain = uintptr(unsafe.Pointer(extras))
}
wirePtr = uintptr(unsafe.Pointer(&wire))
}

Expand Down
12 changes: 12 additions & 0 deletions wgpu/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,15 @@ func TestCreateInstanceReturnsErrLibraryNotLoaded(t *testing.T) {
}
t.Logf("ErrLibraryNotLoaded is defined: %v", ErrLibraryNotLoaded)
}

func TestCreateInstanceWithNativeBackends(t *testing.T) {
// Chains WGPUInstanceExtras; Primary keeps the native backend of every platform.
inst, err := CreateInstance(&InstanceDescriptor{NativeBackends: InstanceBackendPrimary})
if err != nil {
t.Fatalf("CreateInstance(NativeBackends) failed: %v", err)
}
defer inst.Release()
if inst.Handle() == 0 {
t.Fatal("Instance handle is zero")
}
}
29 changes: 29 additions & 0 deletions wgpu/surface_swapchainpanel_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//go:build windows

package wgpu

import (
"testing"
"unsafe"
)

func TestSurfaceSourceSwapChainPanelLayout(t *testing.T) {
var s surfaceSourceSwapChainPanel
if got := unsafe.Sizeof(s); got != 24 {
t.Errorf("sizeof(surfaceSourceSwapChainPanel) = %d, want 24", got)
}
if got := unsafe.Offsetof(s.panelNative); got != 16 {
t.Errorf("offsetof(surfaceSourceSwapChainPanel.panelNative) = %d, want 16", got)
}
}

func TestCreateSurfaceFromSwapChainPanelRejectsNil(t *testing.T) {
inst, err := CreateInstance(nil)
if err != nil {
t.Skipf("CreateInstance failed: %v", err)
}
defer inst.Release()
if _, err := inst.CreateSurfaceFromSwapChainPanel(0); err == nil {
t.Fatal("expected an error for a nil swap chain panel")
}
}
60 changes: 60 additions & 0 deletions wgpu/surface_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,63 @@ func (inst *Instance) CreateSurfaceFromWindowsHWND(hinstance, hwnd uintptr) (*Su
trackResource(handle, "Surface")
return &Surface{handle: handle}, nil
}

// surfaceSourceSwapChainPanel matches WGPUSurfaceSourceSwapChainPanel in the
// wgpu-native v29 header - 24 bytes.
type surfaceSourceSwapChainPanel struct {
chain ChainedStruct // 16 bytes
panelNative uintptr // 8 bytes - ISwapChainPanelNative*
}

// swapChainPanelSourceSink keeps the last chained source on the heap: the
// descriptor refers to it only through a uintptr.
var swapChainPanelSourceSink *surfaceSourceSwapChainPanel

// CreateSurfaceFromSwapChainPanel creates a surface from an
// ISwapChainPanelNative pointer (WGPUSurfaceSourceSwapChainPanel, DX12 only).
//
// wgpu-native creates the swap chain with CreateSwapChainForComposition and
// hands it to ISwapChainPanelNative::SetSwapChain. Besides a WinUI
// SwapChainPanel, the pointer can be any object implementing that interface,
// which lets the caller place the swap chain in its own DirectComposition
// tree (IDCompositionVisual::SetContent), e.g. to keep other DComp content
// such as a WebView2 composition controller visible above the swap chain in
// fullscreen, where an HWND swap chain would be promoted to independent flip.
// panelNative must stay valid while the surface lives: wgpu-native keeps a
// reference to it.
func (inst *Instance) CreateSurfaceFromSwapChainPanel(panelNative uintptr) (*Surface, error) {
if err := checkInit(); err != nil {
return nil, err
}
if inst == nil || inst.handle == 0 {
return nil, &WGPUError{Op: "CreateSurface", Message: "instance is nil or released"}
}
if panelNative == 0 {
return nil, &WGPUError{Op: "CreateSurface", Message: "swap chain panel is nil"}
}

source := &surfaceSourceSwapChainPanel{
chain: ChainedStruct{
Next: 0,
SType: uint32(STypeSurfaceSourceSwapChainPanel),
},
panelNative: panelNative,
}
swapChainPanelSourceSink = source

desc := surfaceDescriptor{
nextInChain: uintptr(unsafe.Pointer(source)),
label: EmptyStringView(),
}

handle, _, _ := procInstanceCreateSurface.Call(
inst.handle,
uintptr(unsafe.Pointer(&desc)),
)
if handle == 0 {
return nil, &WGPUError{Op: "CreateSurface", Message: "failed to create surface"}
}

trackResource(handle, "Surface")
return &Surface{handle: handle}, nil
}