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) + } +}