diff --git a/CHANGELOG.md b/CHANGELOG.md index 92fbdaf..06e43b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/wgpu/abi_test.go b/wgpu/abi_test.go index 2ae4ea3..143859e 100644 --- a/wgpu/abi_test.go +++ b/wgpu/abi_test.go @@ -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)+ @@ -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 @@ -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) }) diff --git a/wgpu/instance.go b/wgpu/instance.go index b261d66..eb96f73 100644 --- a/wgpu/instance.go +++ b/wgpu/instance.go @@ -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. @@ -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)) } diff --git a/wgpu/instance_test.go b/wgpu/instance_test.go index cb11ba8..428ba11 100644 --- a/wgpu/instance_test.go +++ b/wgpu/instance_test.go @@ -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") + } +} diff --git a/wgpu/surface_swapchainpanel_windows_test.go b/wgpu/surface_swapchainpanel_windows_test.go new file mode 100644 index 0000000..94994a4 --- /dev/null +++ b/wgpu/surface_swapchainpanel_windows_test.go @@ -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") + } +} diff --git a/wgpu/surface_windows.go b/wgpu/surface_windows.go index 7086947..fefcef2 100644 --- a/wgpu/surface_windows.go +++ b/wgpu/surface_windows.go @@ -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 +}