From 5dba40e74ba89bd99cdf89811428194e69dad904 Mon Sep 17 00:00:00 2001 From: omartelo Date: Sun, 8 Nov 2026 17:58:52 -0300 Subject: [PATCH] app,internal/d3d11: [Windows] present with the flip model The swap chain used the old blit model with one buffer, which adds a copy and a frame of queueing between a keystroke and the screen. Create a FLIP_DISCARD chain of two buffers instead and set the maximum frame latency to 1, falling back to the previous DISCARD chain where a flip chain cannot be created. Flip-model buffers cannot have an _SRGB format, so the buffers are UNORM and the render target view carries the sRGB format. ResizeBuffers now runs with the view unbound and its deferred destruction flushed, and Release clears and flushes the context, since a window holds one flip chain and a new one after a lost device would otherwise fail. The first frame of a new size presents with sync interval 0, replacing any queued frame so the window does not show an older one stretched. --- app/d3d11_windows.go | 28 +++++- internal/d3d11/d3d11_windows.go | 155 ++++++++++++++++++++++++------- internal/d3d11/swapchain.go | 34 +++++++ internal/d3d11/swapchain_test.go | 67 +++++++++++++ 4 files changed, 250 insertions(+), 34 deletions(-) create mode 100644 internal/d3d11/swapchain.go create mode 100644 internal/d3d11/swapchain_test.go diff --git a/app/d3d11_windows.go b/app/d3d11_windows.go index a7af4408..dc66dd8d 100644 --- a/app/d3d11_windows.go +++ b/app/d3d11_windows.go @@ -18,6 +18,8 @@ type d3d11Context struct { swchain *d3d11.IDXGISwapChain renderTarget *d3d11.RenderTargetView width, height int + // resized is set until the first Present of a new size. + resized bool } const debugDirectX = false @@ -60,7 +62,14 @@ func (c *d3d11Context) RenderTarget() (gpu.RenderTarget, error) { } func (c *d3d11Context) Present() error { - return wrapErr(c.swchain.Present(1, 0)) + syncInterval := 1 + if c.resized { + // Interval 0 replaces any frame still queued, so the new size shows + // at the next composition instead of the old frame stretched to it. + syncInterval = 0 + c.resized = false + } + return wrapErr(c.swchain.Present(syncInterval, 0)) } func wrapErr(err error) error { @@ -82,19 +91,29 @@ func (c *d3d11Context) Refresh() error { if c.renderTarget != nil && width == c.width && height == c.height { return nil } + // ResizeBuffers fails while the context binds a view of the back buffer + // or the destruction of a released view is still deferred. + c.ctx.OMSetRenderTargets(nil, nil) c.releaseFBO() + c.ctx.Flush() if err := c.swchain.ResizeBuffers(0, 0, 0, d3d11.DXGI_FORMAT_UNKNOWN, 0); err != nil { return wrapErr(err) } c.width = width c.height = height + c.resized = true backBuffer, err := c.swchain.GetBuffer(0, &d3d11.IID_Texture2D) if err != nil { return err } texture := (*d3d11.Resource)(unsafe.Pointer(backBuffer)) - renderTarget, err := c.dev.CreateRenderTargetView(texture) + // The view, not the buffer, is sRGB: flip-model buffers cannot have an + // _SRGB format. + renderTarget, err := c.dev.CreateRenderTargetViewTEX2D(texture, &d3d11.RENDER_TARGET_VIEW_DESC_TEX2D{ + Format: d3d11.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, + ViewDimension: d3d11.RTV_DIMENSION_TEXTURE2D, + }) d3d11.IUnknownRelease(unsafe.Pointer(backBuffer), backBuffer.Vtbl.Release) if err != nil { return err @@ -116,6 +135,11 @@ func (c *d3d11Context) Release() { d3d11.IUnknownRelease(unsafe.Pointer(c.swchain), c.swchain.Vtbl.Release) } if c.ctx != nil { + // A window can have one flip-model swap chain, and the destruction of + // the old one is deferred: a new context after a lost device would + // fail to create its own without this. + c.ctx.ClearState() + c.ctx.Flush() d3d11.IUnknownRelease(unsafe.Pointer(c.ctx), c.ctx.Vtbl.Release) } if c.dev != nil { diff --git a/internal/d3d11/d3d11_windows.go b/internal/d3d11/d3d11_windows.go index 728fc2b9..31449729 100644 --- a/internal/d3d11/d3d11_windows.go +++ b/internal/d3d11/d3d11_windows.go @@ -451,6 +451,16 @@ type TEX2D_DSV struct { MipSlice uint32 } +type RENDER_TARGET_VIEW_DESC_TEX2D struct { + Format uint32 + ViewDimension uint32 + Texture2D TEX2D_RTV +} + +type TEX2D_RTV struct { + MipSlice uint32 +} + type BLEND_DESC struct { AlphaToCoverageEnable uint32 IndependentBlendEnable uint32 @@ -529,6 +539,23 @@ type IDXGIDevice struct { } } +type IDXGIDevice1 struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + GetAdapter uintptr + CreateSurface uintptr + QueryResourceResidency uintptr + SetGPUThreadPriority uintptr + GetGPUThreadPriority uintptr + SetMaximumFrameLatency uintptr + GetMaximumFrameLatency uintptr + } +} + type IUnknown struct { Vtbl *struct { _IUnknownVTbl @@ -614,6 +641,7 @@ var ( IID_Texture2D = GUID{0x6f15aaf2, 0xd208, 0x4e89, 0x9a, 0xb4, 0x48, 0x95, 0x35, 0xd3, 0x4f, 0x9c} IID_IDXGIDebug = GUID{0x119E7452, 0xDE9E, 0x40fe, 0x88, 0x06, 0x88, 0xF9, 0x0C, 0x12, 0xB4, 0x41} IID_IDXGIDevice = GUID{0x54ec77fa, 0x1377, 0x44e6, 0x8c, 0x32, 0x88, 0xfd, 0x5f, 0x44, 0xc8, 0x4c} + IID_IDXGIDevice1 = GUID{0x77db970f, 0x6276, 0x48ba, 0xba, 0x28, 0x07, 0x01, 0x43, 0xb4, 0x39, 0x2c} IID_IDXGIFactory = GUID{0x7b7166ec, 0x21c7, 0x44ae, 0xb2, 0x1a, 0xc9, 0xae, 0x32, 0x1a, 0xe3, 0x69} IID_ID3D11Debug = GUID{0x79cf2233, 0x7536, 0x4948, 0x9d, 0x36, 0x1e, 0x46, 0x92, 0xdc, 0x57, 0x60} @@ -635,21 +663,19 @@ const ( SDK_VERSION = 7 DRIVER_TYPE_HARDWARE = 1 - DXGI_FORMAT_UNKNOWN = 0 - DXGI_FORMAT_R16_FLOAT = 54 - DXGI_FORMAT_R32_FLOAT = 41 - DXGI_FORMAT_R32_TYPELESS = 39 - DXGI_FORMAT_R32G32_FLOAT = 16 - DXGI_FORMAT_R32G32B32_FLOAT = 6 - DXGI_FORMAT_R32G32B32A32_FLOAT = 2 - DXGI_FORMAT_R8G8B8A8_UNORM = 28 - DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 - DXGI_FORMAT_R16_SINT = 59 - DXGI_FORMAT_R16G16_SINT = 38 - DXGI_FORMAT_R16_UINT = 57 - DXGI_FORMAT_D24_UNORM_S8_UINT = 45 - DXGI_FORMAT_R16G16_FLOAT = 34 - DXGI_FORMAT_R16G16B16A16_FLOAT = 10 + DXGI_FORMAT_UNKNOWN = 0 + DXGI_FORMAT_R16_FLOAT = 54 + DXGI_FORMAT_R32_FLOAT = 41 + DXGI_FORMAT_R32_TYPELESS = 39 + DXGI_FORMAT_R32G32_FLOAT = 16 + DXGI_FORMAT_R32G32B32_FLOAT = 6 + DXGI_FORMAT_R32G32B32A32_FLOAT = 2 + DXGI_FORMAT_R16_SINT = 59 + DXGI_FORMAT_R16G16_SINT = 38 + DXGI_FORMAT_R16_UINT = 57 + DXGI_FORMAT_D24_UNORM_S8_UINT = 45 + DXGI_FORMAT_R16G16_FLOAT = 34 + DXGI_FORMAT_R16G16B16A16_FLOAT = 10 DXGI_DEBUG_RLO_SUMMARY = 0x1 DXGI_DEBUG_RLO_DETAIL = 0x2 @@ -664,8 +690,6 @@ const ( MAP_READ = 1 - DXGI_SWAP_EFFECT_DISCARD = 0 - FEATURE_LEVEL_9_1 = 0x9100 FEATURE_LEVEL_9_3 = 0x9300 FEATURE_LEVEL_11_0 = 0xb000 @@ -714,6 +738,7 @@ const ( CLEAR_STENCIL = 0x2 DSV_DIMENSION_TEXTURE2D = 3 + RTV_DIMENSION_TEXTURE2D = 4 DEPTH_WRITE_MASK_ALL = 1 @@ -1055,6 +1080,23 @@ func (d *Device) CreateRenderTargetView(res *Resource) (*RenderTargetView, error return target, nil } +func (d *Device) CreateRenderTargetViewTEX2D(res *Resource, desc *RENDER_TARGET_VIEW_DESC_TEX2D) (*RenderTargetView, error) { + var target *RenderTargetView + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateRenderTargetView, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(res)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&target)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateRenderTargetView", Code: uint32(r)} + } + return target, nil +} + func (d *Device) CreateBlendState(desc *BLEND_DESC) (*BlendState, error) { var state *BlendState r, _, _ := syscall.Syscall( @@ -1486,6 +1528,24 @@ func (c *DeviceContext) OMSetRenderTargets(target *RenderTargetView, depthStenci ) } +func (c *DeviceContext) ClearState() { + syscall.Syscall( + c.Vtbl.ClearState, + 1, + uintptr(unsafe.Pointer(c)), + 0, 0, + ) +} + +func (c *DeviceContext) Flush() { + syscall.Syscall( + c.Vtbl.Flush, + 1, + uintptr(unsafe.Pointer(c)), + 0, 0, + ) +} + func (c *DeviceContext) Draw(count, start uint32) { syscall.Syscall( c.Vtbl.Draw, @@ -1574,6 +1634,20 @@ func (d *IDXGIFactory) CreateSwapChain(device *IUnknown, desc *DXGI_SWAP_CHAIN_D return swchain, nil } +func (d *IDXGIDevice1) SetMaximumFrameLatency(frames uint32) error { + r, _, _ := syscall.Syscall( + d.Vtbl.SetMaximumFrameLatency, + 2, + uintptr(unsafe.Pointer(d)), + uintptr(frames), + 0, + ) + if r != 0 { + return ErrorCode{Name: "IDXGIDevice1SetMaximumFrameLatency", Code: uint32(r)} + } + return nil +} + func (d *IDXGIDevice) GetAdapter() (*IDXGIAdapter, error) { var adapter *IDXGIAdapter r, _, _ := syscall.Syscall( @@ -1643,22 +1717,30 @@ func CreateSwapChain(dev *Device, hwnd windows.Handle) (*IDXGISwapChain, error) if err != nil { return nil, fmt.Errorf("NewContext: %v", err) } - swchain, err := (*IDXGIFactory)(unsafe.Pointer(dxgiFactory)).CreateSwapChain( - (*IUnknown)(unsafe.Pointer(dev)), - &DXGI_SWAP_CHAIN_DESC{ - BufferDesc: DXGI_MODE_DESC{ - Format: DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, - }, - SampleDesc: DXGI_SAMPLE_DESC{ - Count: 1, + // Best effort: the default latency of 3 frames only costs input lag. + _ = setMaximumFrameLatency(dev, maxFrameLatency) + factory := (*IDXGIFactory)(unsafe.Pointer(dxgiFactory)) + var swchain *IDXGISwapChain + err = firstSwapChain(func(cfg swapChainConfig) error { + var err error + swchain, err = factory.CreateSwapChain( + (*IUnknown)(unsafe.Pointer(dev)), + &DXGI_SWAP_CHAIN_DESC{ + BufferDesc: DXGI_MODE_DESC{ + Format: cfg.format, + }, + SampleDesc: DXGI_SAMPLE_DESC{ + Count: 1, + }, + BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, + BufferCount: cfg.bufferCount, + OutputWindow: hwnd, + Windowed: 1, + SwapEffect: cfg.effect, }, - BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, - BufferCount: 1, - OutputWindow: hwnd, - Windowed: 1, - SwapEffect: DXGI_SWAP_EFFECT_DISCARD, - }, - ) + ) + return err + }) IUnknownRelease(unsafe.Pointer(dxgiFactory), dxgiFactory.Vtbl.Release) if err != nil { return nil, fmt.Errorf("NewContext: %v", err) @@ -1666,6 +1748,15 @@ func CreateSwapChain(dev *Device, hwnd windows.Handle) (*IDXGISwapChain, error) return swchain, nil } +func setMaximumFrameLatency(dev *Device, frames uint32) error { + dxgiDev, err := IUnknownQueryInterface(unsafe.Pointer(dev), dev.Vtbl.QueryInterface, &IID_IDXGIDevice1) + if err != nil { + return err + } + defer IUnknownRelease(unsafe.Pointer(dxgiDev), dxgiDev.Vtbl.Release) + return (*IDXGIDevice1)(unsafe.Pointer(dxgiDev)).SetMaximumFrameLatency(frames) +} + func CreateDepthView(d *Device, width, height, depthBits int) (*DepthStencilView, error) { depthTex, err := d.CreateTexture2D(&TEXTURE2D_DESC{ Width: uint32(width), diff --git a/internal/d3d11/swapchain.go b/internal/d3d11/swapchain.go new file mode 100644 index 00000000..e54f1df1 --- /dev/null +++ b/internal/d3d11/swapchain.go @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package d3d11 + +const ( + DXGI_FORMAT_R8G8B8A8_UNORM = 28 + DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 + + DXGI_SWAP_EFFECT_DISCARD = 0 + DXGI_SWAP_EFFECT_FLIP_DISCARD = 4 + + maxFrameLatency = 1 +) + +type swapChainConfig struct { + effect, format, bufferCount uint32 +} + +// swapChainConfigs are tried in order. Windows before 10, and some VMs and +// drivers, cannot create a flip-model swap chain. +var swapChainConfigs = [...]swapChainConfig{ + {DXGI_SWAP_EFFECT_FLIP_DISCARD, DXGI_FORMAT_R8G8B8A8_UNORM, 2}, + {DXGI_SWAP_EFFECT_DISCARD, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, 1}, +} + +func firstSwapChain(create func(swapChainConfig) error) error { + var err error + for _, cfg := range swapChainConfigs { + if err = create(cfg); err == nil { + return nil + } + } + return err +} diff --git a/internal/d3d11/swapchain_test.go b/internal/d3d11/swapchain_test.go new file mode 100644 index 00000000..f9ec712a --- /dev/null +++ b/internal/d3d11/swapchain_test.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package d3d11 + +import ( + "errors" + "testing" +) + +func TestFlipConfigsAreValidForFlipModel(t *testing.T) { + flips := 0 + for _, cfg := range swapChainConfigs { + if cfg.effect != DXGI_SWAP_EFFECT_FLIP_DISCARD { + continue + } + flips++ + if cfg.bufferCount < 2 { + t.Errorf("flip model needs at least 2 buffers, got %d", cfg.bufferCount) + } + if cfg.format == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB { + t.Error("flip-model buffers cannot have an _SRGB format") + } + } + if flips == 0 { + t.Error("no flip-model config") + } +} + +func TestFirstSwapChainPrefersFlip(t *testing.T) { + var tried []uint32 + err := firstSwapChain(func(cfg swapChainConfig) error { + tried = append(tried, cfg.effect) + return nil + }) + if err != nil { + t.Fatal(err) + } + if len(tried) != 1 || tried[0] != DXGI_SWAP_EFFECT_FLIP_DISCARD { + t.Fatalf("tried %v, want only the flip model", tried) + } +} + +func TestFirstSwapChainFallsBackToDiscard(t *testing.T) { + var last swapChainConfig + err := firstSwapChain(func(cfg swapChainConfig) error { + last = cfg + if cfg.effect == DXGI_SWAP_EFFECT_FLIP_DISCARD { + return errors.New("no flip model here") + } + return nil + }) + if err != nil { + t.Fatal(err) + } + want := swapChainConfig{DXGI_SWAP_EFFECT_DISCARD, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, 1} + if last != want { + t.Fatalf("fell back to %+v, want %+v", last, want) + } +} + +func TestFirstSwapChainReportsLastError(t *testing.T) { + want := errors.New("no swap chain") + err := firstSwapChain(func(swapChainConfig) error { return want }) + if !errors.Is(err, want) { + t.Fatalf("got %v, want %v", err, want) + } +}