diff --git a/hal/dx12/adapter.go b/hal/dx12/adapter.go index fb9a6791..f4bc0f42 100644 --- a/hal/dx12/adapter.go +++ b/hal/dx12/adapter.go @@ -380,6 +380,17 @@ func (a *Adapter) TextureFormatCapabilities(format gputypes.TextureFormat) hal.T // SurfaceCapabilities returns surface capabilities. func (a *Adapter) SurfaceCapabilities(surface hal.Surface) *hal.SurfaceCapabilities { + // Opaque is always supported via CreateSwapChainForHwnd. + // Premultiplied alpha requires DirectComposition (dcomp.dll, Windows 8+). + // Rust wgpu reports Premultiplied only for VisualFromWndHandle targets; + // we simplify by checking DComp DLL availability at runtime. + alphaModes := []hal.CompositeAlphaMode{ + hal.CompositeAlphaModeOpaque, + } + if dcompAvailable() { + alphaModes = append(alphaModes, hal.CompositeAlphaModePremultiplied) + } + // D3D12 supports these formats for swap chains return &hal.SurfaceCapabilities{ Formats: []gputypes.TextureFormat{ @@ -390,10 +401,7 @@ func (a *Adapter) SurfaceCapabilities(surface hal.Surface) *hal.SurfaceCapabilit gputypes.TextureFormatRGBA16Float, }, PresentModes: a.presentModes(), - AlphaModes: []hal.CompositeAlphaMode{ - hal.CompositeAlphaModeOpaque, - hal.CompositeAlphaModePremultiplied, - }, + AlphaModes: alphaModes, } } @@ -671,13 +679,19 @@ func (a *AdapterLegacy) TextureFormatCapabilities(format gputypes.TextureFormat) // SurfaceCapabilities returns surface capabilities. func (a *AdapterLegacy) SurfaceCapabilities(surface hal.Surface) *hal.SurfaceCapabilities { + // Opaque is always supported. Premultiplied requires DirectComposition. + alphaModes := []hal.CompositeAlphaMode{hal.CompositeAlphaModeOpaque} + if dcompAvailable() { + alphaModes = append(alphaModes, hal.CompositeAlphaModePremultiplied) + } + return &hal.SurfaceCapabilities{ Formats: []gputypes.TextureFormat{ gputypes.TextureFormatBGRA8Unorm, gputypes.TextureFormatRGBA8Unorm, }, PresentModes: []hal.PresentMode{hal.PresentModeFifo}, - AlphaModes: []hal.CompositeAlphaMode{hal.CompositeAlphaModeOpaque}, + AlphaModes: alphaModes, } } diff --git a/hal/dx12/dcomp.go b/hal/dx12/dcomp.go new file mode 100644 index 00000000..9887e6dc --- /dev/null +++ b/hal/dx12/dcomp.go @@ -0,0 +1,351 @@ +// Copyright 2026 The GoGPU Authors +// SPDX-License-Identifier: MIT + +//go:build windows && !(js && wasm) + +// DirectComposition COM bindings for per-pixel alpha transparency on DX12. +// +// DirectComposition (dcomp.dll) is required when a swap chain needs +// DXGI_ALPHA_MODE_PREMULTIPLIED — the standard CreateSwapChainForHwnd path +// does not support per-pixel alpha. Instead, we create the swap chain via +// CreateSwapChainForComposition and attach it to an IDCompositionVisual, +// which is rooted on an IDCompositionTarget bound to the HWND. +// +// Rust wgpu reference: wgpu-hal/src/dx12/dcomp.rs + +package dx12 + +import ( + "fmt" + "sync" + "syscall" + "unsafe" + + "github.com/gogpu/wgpu/hal/dx12/d3d12" + "github.com/gogpu/wgpu/hal/dx12/dxgi" +) + +// --------------------------------------------------------------------------- +// dcomp.dll lazy loading +// --------------------------------------------------------------------------- + +var ( + dcompOnce sync.Once + dcompDLL *syscall.LazyDLL + dcompCreateDevice2 *syscall.LazyProc + errDCompLoad error +) + +func loadDComp() error { + dcompOnce.Do(func() { + dcompDLL = syscall.NewLazyDLL("dcomp.dll") + dcompCreateDevice2 = dcompDLL.NewProc("DCompositionCreateDevice2") + errDCompLoad = dcompCreateDevice2.Find() + }) + return errDCompLoad +} + +// dcompAvailable reports whether dcomp.dll can be loaded on this system. +// Windows 8+ ships dcomp.dll; older systems or Server Core may not. +func dcompAvailable() bool { + return loadDComp() == nil +} + +// --------------------------------------------------------------------------- +// IID for IDCompositionDevice +// --------------------------------------------------------------------------- + +// iidIDCompositionDevice is the COM interface ID for IDCompositionDevice. +// {C37EA93A-E7AA-450D-B16F-9746CB0407F3} +var iidIDCompositionDevice = d3d12.GUID{ + Data1: 0xC37EA93A, + Data2: 0xE7AA, + Data3: 0x450D, + Data4: [8]byte{0xB1, 0x6F, 0x97, 0x46, 0xCB, 0x04, 0x07, 0xF3}, +} + +// --------------------------------------------------------------------------- +// COM interface vtbl structs +// --------------------------------------------------------------------------- + +// idcompositionDeviceVtbl is the vtable layout for IDCompositionDevice. +// Inherits IUnknown (QueryInterface, AddRef, Release). +type idcompositionDeviceVtbl struct { + // IUnknown + QueryInterface uintptr + AddRef uintptr + Release uintptr + + // IDCompositionDevice + Commit uintptr // vtbl index 3 + WaitForCommitCompletion uintptr // vtbl index 4 + GetFrameStatistics uintptr // vtbl index 5 + CreateTargetForHwnd uintptr // vtbl index 6 + CreateVisual uintptr // vtbl index 7 +} + +// idcompositionDevice wraps a raw COM pointer to IDCompositionDevice. +type idcompositionDevice struct { + vtbl *idcompositionDeviceVtbl +} + +// Release decrements the reference count. Safe to call on nil. +func (d *idcompositionDevice) Release() { + if d == nil { + return + } + //nolint:errcheck // COM Release returns remaining refcount, not an error. + syscall.SyscallN(d.vtbl.Release, uintptr(unsafe.Pointer(d))) +} + +// Commit commits all pending DirectComposition commands. +func (d *idcompositionDevice) Commit() error { + ret, _, _ := syscall.SyscallN(d.vtbl.Commit, uintptr(unsafe.Pointer(d))) + if ret != 0 { + return d3d12.HRESULTError(ret) + } + return nil +} + +// CreateTargetForHwnd creates a composition target for the specified window. +// topmost controls the z-order: false places the visual tree behind the HWND +// children, true places it in front. +func (d *idcompositionDevice) CreateTargetForHwnd(hwnd uintptr, topmost bool) (*idcompositionTarget, error) { + var target *idcompositionTarget + var topmostInt uintptr + if topmost { + topmostInt = 1 + } + + ret, _, _ := syscall.SyscallN( + d.vtbl.CreateTargetForHwnd, + uintptr(unsafe.Pointer(d)), + hwnd, + topmostInt, + uintptr(unsafe.Pointer(&target)), + ) + + if ret != 0 { + return nil, d3d12.HRESULTError(ret) + } + return target, nil +} + +// CreateVisual creates a new composition visual. +func (d *idcompositionDevice) CreateVisual() (*idcompositionVisual, error) { + var visual *idcompositionVisual + + ret, _, _ := syscall.SyscallN( + d.vtbl.CreateVisual, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&visual)), + ) + + if ret != 0 { + return nil, d3d12.HRESULTError(ret) + } + return visual, nil +} + +// --------------------------------------------------------------------------- +// IDCompositionTarget +// --------------------------------------------------------------------------- + +// idcompositionTargetVtbl is the vtable layout for IDCompositionTarget. +// Inherits IUnknown (QueryInterface, AddRef, Release). +type idcompositionTargetVtbl struct { + // IUnknown + QueryInterface uintptr + AddRef uintptr + Release uintptr + + // IDCompositionTarget + SetRoot uintptr // vtbl index 3 +} + +// idcompositionTarget wraps a raw COM pointer to IDCompositionTarget. +type idcompositionTarget struct { + vtbl *idcompositionTargetVtbl +} + +// Release decrements the reference count. Safe to call on nil. +func (t *idcompositionTarget) Release() { + if t == nil { + return + } + //nolint:errcheck // COM Release returns remaining refcount, not an error. + syscall.SyscallN(t.vtbl.Release, uintptr(unsafe.Pointer(t))) +} + +// SetRoot sets the root visual for this composition target. +func (t *idcompositionTarget) SetRoot(visual *idcompositionVisual) error { + ret, _, _ := syscall.SyscallN( + t.vtbl.SetRoot, + uintptr(unsafe.Pointer(t)), + uintptr(unsafe.Pointer(visual)), + ) + + if ret != 0 { + return d3d12.HRESULTError(ret) + } + return nil +} + +// --------------------------------------------------------------------------- +// IDCompositionVisual +// --------------------------------------------------------------------------- + +// idcompositionVisualVtbl is the vtable layout for IDCompositionVisual. +// Inherits IUnknown (QueryInterface, AddRef, Release). +// +// COM overloaded methods occupy separate vtbl slots. Field names use the +// overload suffix (e.g., SetOffsetXAnimation vs SetOffsetXFloat) rather than +// underscores to satisfy Go naming conventions. We only call SetContent (index 15); +// the remaining slots are reserved for correct vtbl layout. +type idcompositionVisualVtbl struct { + // IUnknown + QueryInterface uintptr + AddRef uintptr + Release uintptr + + // IDCompositionVisual + SetOffsetXAnimation uintptr // vtbl index 3 — SetOffsetX(IDCompositionAnimation*) + SetOffsetXFloat uintptr // vtbl index 4 — SetOffsetX(float) + SetOffsetYAnimation uintptr // vtbl index 5 — SetOffsetY(IDCompositionAnimation*) + SetOffsetYFloat uintptr // vtbl index 6 — SetOffsetY(float) + SetTransformTransform uintptr // vtbl index 7 — SetTransform(IDCompositionTransform*) + SetTransformMatrix uintptr // vtbl index 8 — SetTransform(D2D_MATRIX_3X2_F) + SetTransformParent uintptr // vtbl index 9 + SetEffect uintptr // vtbl index 10 + SetBitmapInterpolation uintptr // vtbl index 11 + SetBorderMode uintptr // vtbl index 12 + SetClipObject uintptr // vtbl index 13 — SetClip(IDCompositionClip*) + SetClipRect uintptr // vtbl index 14 — SetClip(D2D_RECT_F) + SetContent uintptr // vtbl index 15 + AddVisual uintptr // vtbl index 16 + RemoveVisual uintptr // vtbl index 17 + RemoveAllVisuals uintptr // vtbl index 18 + SetCompositeMode uintptr // vtbl index 19 +} + +// idcompositionVisual wraps a raw COM pointer to IDCompositionVisual. +type idcompositionVisual struct { + vtbl *idcompositionVisualVtbl +} + +// Release decrements the reference count. Safe to call on nil. +func (v *idcompositionVisual) Release() { + if v == nil { + return + } + //nolint:errcheck // COM Release returns remaining refcount, not an error. + syscall.SyscallN(v.vtbl.Release, uintptr(unsafe.Pointer(v))) +} + +// setContent sets the content (typically a swap chain) for this visual. +// The content parameter is an IUnknown pointer — in our case it will be +// the IDXGISwapChain1 from CreateSwapChainForComposition. +func (v *idcompositionVisual) setContent(content unsafe.Pointer) error { + ret, _, _ := syscall.SyscallN( + v.vtbl.SetContent, + uintptr(unsafe.Pointer(v)), + uintptr(content), + ) + + if ret != 0 { + return d3d12.HRESULTError(ret) + } + return nil +} + +// --------------------------------------------------------------------------- +// dcompState — per-Surface lifecycle +// --------------------------------------------------------------------------- + +// dcompState manages the DirectComposition visual tree for a single Surface. +// It owns three COM objects that must be released in reverse init order. +type dcompState struct { + device *idcompositionDevice + target *idcompositionTarget // must outlive visual + visual *idcompositionVisual +} + +// init creates the DComp device and visual tree for an HWND. +// Matches Rust wgpu InnerState::init: create device, create target, create +// visual, set root, ready for bindSwapChain + Commit. +func (s *dcompState) init(hwnd uintptr) error { + if err := loadDComp(); err != nil { + return fmt.Errorf("dcomp: %w", err) + } + + // DCompositionCreateDevice2(NULL, IID_IDCompositionDevice, &device) + // NULL renderingDevice = software composition device (sufficient for + // our use — we only need the visual tree, DX12 does the actual rendering). + var device *idcompositionDevice + ret, _, _ := dcompCreateDevice2.Call( + 0, // renderingDevice = NULL + uintptr(unsafe.Pointer(&iidIDCompositionDevice)), + uintptr(unsafe.Pointer(&device)), + ) + if ret != 0 { + return fmt.Errorf("DCompositionCreateDevice2: %w", d3d12.HRESULTError(ret)) + } + s.device = device + + // CreateTargetForHwnd — topmost=false places behind HWND children + // (matches Rust wgpu: topmost=false). + target, err := device.CreateTargetForHwnd(hwnd, false) + if err != nil { + s.release() + return fmt.Errorf("IDCompositionDevice::CreateTargetForHwnd: %w", err) + } + s.target = target + + visual, err := device.CreateVisual() + if err != nil { + s.release() + return fmt.Errorf("IDCompositionDevice::CreateVisual: %w", err) + } + s.visual = visual + + // Connect the visual to the target's root. + if err := target.SetRoot(visual); err != nil { + s.release() + return fmt.Errorf("IDCompositionTarget::SetRoot: %w", err) + } + + return nil +} + +// bindSwapChain associates a swap chain with the DComp visual and commits +// the composition. The swap chain must have been created with +// CreateSwapChainForComposition (not CreateSwapChainForHwnd). +func (s *dcompState) bindSwapChain(swapchain *dxgi.IDXGISwapChain1) error { + if s.visual == nil { + return fmt.Errorf("dcomp: visual tree not initialized") + } + + if err := s.visual.setContent(unsafe.Pointer(swapchain)); err != nil { + return fmt.Errorf("IDCompositionVisual::SetContent: %w", err) + } + + if err := s.device.Commit(); err != nil { + return fmt.Errorf("IDCompositionDevice::Commit: %w", err) + } + + return nil +} + +// release tears down DComp objects in reverse init order: visual, target, device. +// Safe to call multiple times; safe to call on a zero-value dcompState. +func (s *dcompState) release() { + // Reverse order: visual depends on target, target depends on device. + s.visual.Release() + s.visual = nil + + s.target.Release() + s.target = nil + + s.device.Release() + s.device = nil +} diff --git a/hal/dx12/dxgi/factory.go b/hal/dx12/dxgi/factory.go index 20e6bb66..d797832f 100644 --- a/hal/dx12/dxgi/factory.go +++ b/hal/dx12/dxgi/factory.go @@ -387,6 +387,37 @@ func (f *IDXGIFactory6) CreateSwapChainForHwnd( return swapChain, nil } +// CreateSwapChainForComposition creates a swap chain for use with DirectComposition +// or Windows.UI.Xaml. Unlike CreateSwapChainForHwnd, the swap chain is not associated +// with an HWND — it must be attached to a DirectComposition visual via +// IDCompositionVisual::SetContent. This is required for per-pixel alpha transparency +// (DXGI_ALPHA_MODE_PREMULTIPLIED) on DX12. +// +// restrictToOutput can be nil to allow output on any display. +func (f *IDXGIFactory6) CreateSwapChainForComposition( + device unsafe.Pointer, // ID3D12CommandQueue + desc *DXGI_SWAP_CHAIN_DESC1, + restrictToOutput *IDXGIOutput, +) (*IDXGISwapChain1, error) { + var swapChain *IDXGISwapChain1 + + ret, _, _ := syscall.Syscall6( + f.vtbl.CreateSwapChainForComposition, + 5, + uintptr(unsafe.Pointer(f)), + uintptr(device), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(restrictToOutput)), + uintptr(unsafe.Pointer(&swapChain)), + 0, + ) + + if ret != 0 { + return nil, d3d12.HRESULTError(ret) + } + return swapChain, nil +} + // CheckFeatureSupport checks for DXGI feature support. func (f *IDXGIFactory6) CheckFeatureSupport(feature DXGI_FEATURE, featureData unsafe.Pointer, featureDataSize uint32) error { ret, _, _ := syscall.Syscall6( diff --git a/hal/dx12/instance.go b/hal/dx12/instance.go index 6a52fa6d..0c5a1e99 100644 --- a/hal/dx12/instance.go +++ b/hal/dx12/instance.go @@ -361,6 +361,11 @@ type Surface struct { // DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL (instead of FLIP_DISCARD). // Only in this mode can Present1 with dirty rects be used. damagePresent bool + + // dcomp holds the DirectComposition visual tree state when using + // per-pixel alpha (CreateSwapChainForComposition path). nil for + // the standard HWND path. + dcomp *dcompState } // Configure configures the surface for presentation. @@ -375,9 +380,15 @@ func (s *Surface) Configure(device hal.Device, config *hal.SurfaceConfiguration) return fmt.Errorf("dx12: device is not a DX12 device") } - // If we already have a swapchain with the same device, resize it + // If we already have a swapchain with the same device, resize it — + // UNLESS the alpha mode changed (Opaque↔Premultiplied requires a + // different swap chain creation path: HWND vs DirectComposition). if s.swapchain != nil && s.device == dx12Device { - return s.resizeSwapchain(config) + needsDComp := config.AlphaMode == hal.CompositeAlphaModePremultiplied + hasDComp := s.dcomp != nil + if needsDComp == hasDComp { + return s.resizeSwapchain(config) + } } // Destroy old swapchain if switching devices @@ -409,6 +420,13 @@ func (s *Surface) Unconfigure(_ hal.Device) { s.swapchain = nil } + // Release DirectComposition state (must be after swapchain release — + // the visual holds a reference to the swap chain via SetContent). + if s.dcomp != nil { + s.dcomp.release() + s.dcomp = nil + } + s.device = nil s.width = 0 s.height = 0 diff --git a/hal/dx12/surface.go b/hal/dx12/surface.go index ea764f64..733185bd 100644 --- a/hal/dx12/surface.go +++ b/hal/dx12/surface.go @@ -7,6 +7,7 @@ package dx12 import ( "fmt" + "os" "unsafe" "github.com/gogpu/gputypes" @@ -81,22 +82,67 @@ func (s *Surface) createSwapchain(device *Device, config *hal.SurfaceConfigurati Flags: swapchainFlags, } - // Create swapchain using factory and command queue - swapchain1, err := s.instance.factory.CreateSwapChainForHwnd( - unsafe.Pointer(device.directQueue), - s.hwnd, - &desc, - nil, // fullscreen desc (windowed) - nil, // restrict to output - ) - if err != nil { - return fmt.Errorf("dx12: CreateSwapChainForHwnd failed: %w", err) + // Determine swap chain creation path. + // DirectComposition is required for per-pixel alpha (DXGI_ALPHA_MODE_PREMULTIPLIED) + // because CreateSwapChainForHwnd only supports DXGI_ALPHA_MODE_IGNORE. + // GOGPU_DX12_FORCE_HWND=1 overrides this for RenderDoc compatibility (RenderDoc + // cannot capture frames through DirectComposition). + useDComp := config.AlphaMode == hal.CompositeAlphaModePremultiplied && + os.Getenv("GOGPU_DX12_FORCE_HWND") != "1" + + var swapchain1 *dxgi.IDXGISwapChain1 + + if useDComp { + // DirectComposition path — create swap chain via CreateSwapChainForComposition + // and bind it to a DComp visual tree rooted on the HWND. + s.dcomp = &dcompState{} + if err := s.dcomp.init(s.hwnd); err != nil { + s.dcomp = nil + return fmt.Errorf("dx12: DirectComposition init failed: %w", err) + } + + sc, err := s.instance.factory.CreateSwapChainForComposition( + unsafe.Pointer(device.directQueue), + &desc, + nil, // restrict to output + ) + if err != nil { + s.dcomp.release() + s.dcomp = nil + return fmt.Errorf("dx12: CreateSwapChainForComposition failed: %w", err) + } + swapchain1 = sc + + // Bind swap chain to DComp visual and commit the composition. + if err := s.dcomp.bindSwapChain(swapchain1); err != nil { + swapchain1.Release() + s.dcomp.release() + s.dcomp = nil + return fmt.Errorf("dx12: DComp bindSwapChain failed: %w", err) + } + } else { + // Standard HWND path — swap chain is directly associated with the window. + sc, err := s.instance.factory.CreateSwapChainForHwnd( + unsafe.Pointer(device.directQueue), + s.hwnd, + &desc, + nil, // fullscreen desc (windowed) + nil, // restrict to output + ) + if err != nil { + return fmt.Errorf("dx12: CreateSwapChainForHwnd failed: %w", err) + } + swapchain1 = sc } // Query for IDXGISwapChain4 interface (required for GetCurrentBackBufferIndex) swapchain4, err := querySwapChain4(swapchain1) if err != nil { swapchain1.Release() + if s.dcomp != nil { + s.dcomp.release() + s.dcomp = nil + } return fmt.Errorf("dx12: failed to query IDXGISwapChain4: %w", err) } // Release the original swapchain1 reference (swapchain4 holds a reference) @@ -119,16 +165,25 @@ func (s *Surface) createSwapchain(device *Device, config *hal.SurfaceConfigurati // Without this wait, DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT is a no-op. s.frameLatencyWaitableObject = swapchain4.GetFrameLatencyWaitableObject() - // Disable Alt+Enter fullscreen toggle - if err := s.instance.factory.MakeWindowAssociation(s.hwnd, dxgi.DXGI_MWA_NO_ALT_ENTER); err != nil { - // Non-fatal, just continue - _ = err + // Disable Alt+Enter fullscreen toggle (HWND path only — DirectComposition + // swap chains are not associated with the HWND, so DXGI Alt+Enter + // interception does not apply). Matches Rust wgpu: MakeWindowAssociation + // is called only for SurfaceTarget::WndHandle. + if s.dcomp == nil { + if err := s.instance.factory.MakeWindowAssociation(s.hwnd, dxgi.DXGI_MWA_NO_ALT_ENTER); err != nil { + // Non-fatal, just continue + _ = err + } } // Create RTVs for back buffers if err := s.createBackBufferRTVs(); err != nil { swapchain4.Release() s.swapchain = nil + if s.dcomp != nil { + s.dcomp.release() + s.dcomp = nil + } return err } @@ -137,12 +192,23 @@ func (s *Surface) createSwapchain(device *Device, config *hal.SurfaceConfigurati return err } - hal.Logger().Info("dx12: surface configured", - "width", config.Width, - "height", config.Height, - "format", config.Format, - "presentMode", config.PresentMode, - ) + if useDComp { + hal.Logger().Info("dx12: surface configured (DirectComposition)", + "width", config.Width, + "height", config.Height, + "format", config.Format, + "presentMode", config.PresentMode, + "alphaMode", config.AlphaMode, + ) + } else { + hal.Logger().Info("dx12: surface configured", + "width", config.Width, + "height", config.Height, + "format", config.Format, + "presentMode", config.PresentMode, + "alphaMode", config.AlphaMode, + ) + } return nil }