Skip to content

Use DXGI flip-model swap chain (borderless windowed) for lower latency/power; revisit #617 keyboard focus #663

Description

@scottdraves

Proposal

Switch the D3D11 swap chain to the DXGI flip model (DXGI_SWAP_EFFECT_FLIP_DISCARD via IDXGIFactory2::CreateSwapChainForHwnd) and use borderless windowed fullscreen everywhere, instead of the current legacy BitBlt swap chain (DXGI_SWAP_EFFECT_DISCARD) + DXGI exclusive fullscreen.

Current state (DisplayDX11.cpp)

  • CreateDeviceAndSwapChain builds a DXGI_SWAP_CHAIN_DESC with the default swap effect → legacy BitBlt (DISCARD) model.
  • The app uses DXGI exclusive fullscreen (SetFullscreenState(TRUE)); only the screensaver opts out (SetDisableExclusiveFullscreen(m_isScreenSaverRun) in Player.cpp).
  • So the screensaver is already borderless, but still on the BitBlt model — it never gets the compositor-bypass either.

Why flip mode — basic performance wins

This is a video/visual player, so flip mode does not raise fps. The wins are latency, power, GPU headroom, and frame pacing. Magnitude depends on the baseline:

vs. old exclusive fullscreen (fullscreen case): ~parity. Exclusive already bypassed the DWM compositor; flip-model independent flip also bypasses it (direct scanout). So flip mode keeps the performance we had while dropping exclusive mode's multi-monitor input bug. No regression.

vs. BitBlt model (windowed mode + the screensaver): real net-new win. BitBlt-windowed goes through a full DWM composition pass every frame:

  • Latency: ~1 frame less on average (≈16 ms @ 60 Hz), up to ~2 frames worst case — the compositor hop is removed.
  • GPU time / bandwidth: eliminates one full-screen copy per frame (~33 MB/frame at 4K). Sub-ms on a dGPU; a few % of GPU utilization and ~0.2–0.5 ms/frame on an integrated laptop GPU.
  • Power (biggest practical win, esp. laptops): direct scanout lets the GPU down-clock and can engage a hardware overlay plane (MPO) so the display controller scans the video out directly — the classic fullscreen-video battery saver.
  • Smoothness: cleaner vsync, fewer compositor-induced dropped/duplicated frames — probably the most user-visible benefit for a "dream" visual.

Caveats

  • Independent flip only engages when the window truly covers the monitor with nothing on top (no notification popups/other topmost windows, compatible format). Plain windowed gets flip's modest benefits but not full independent flip.
  • The MPO/overlay path is driver/GPU-dependent.
  • Verify with PresentMon (reports Hardware: Independent Flip / Composed: Flip per frame) that the fast path actually engages.

Implementation requirements (already satisfied by this renderer)

  • BufferCount >= 2 (currently 2). ✔
  • No MSAA on the back buffer (SampleDesc.Count == 1). ✔
  • No GDI drawn on the back buffer (custom titlebar is a DX overlay). ✔
  • Renderer re-binds its render target every frame (CRendererDX11::BeginFrameOMSetRenderTargets), which the flip model requires after each Present. ✔
  • Use CreateSwapChainForHwnd + DXGI_SWAP_CHAIN_DESC1; add MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER) so DXGI can't bounce Alt+Enter back into exclusive mode.

Related: the keyboard-focus bug (#617) — current diagnosis

#617 ("full screen on external monitor breaks keyboard commands") is what motivated this. My current read:

Root cause is a Win32 foreground/keyboard-focus ownership problem on multi-monitor, made worse by exclusive fullscreen. Keys go to whatever window holds keyboard focus. When the app is fullscreen on a secondary monitor and the user has been interacting on the primary, our fullscreen window is not the foreground window, so WM_KEYDOWN never reaches it. With exclusive fullscreen on the secondary monitor, even clicking the surface didn't recover focus (reported). It "works on the laptop/primary screen" because activation tends to stay there.

Attempted fix (in the working tree, builds, but NOT confirmed working):

  1. Flip-model swap chain + borderless windowed everywhere (this issue).
  2. SetFullscreen(true) now claims foreground + focus and goes WS_EX_TOPMOST, matching the startup-fullscreen path.
  3. WM_MOUSEMOVE in fullscreen reclaims foreground/focus if another window holds it.

Why it likely still isn't enough / where to look tomorrow:

  • SetForegroundWindow foreground-lock restrictions. Windows silently no-ops SetForegroundWindow when the calling thread isn't allowed to steal foreground (foreground belongs to another process, no recent input directed at us). So the WM_MOUSEMOVE focus-reclaim may simply never take effect. The robust pattern is the AttachThreadInput dance (attach to the current foreground thread, BringWindowToTop + SetForegroundWindow + SetFocus, detach), or AllowSetForegroundWindow / a brief SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT) workaround.
  • Verify the plain click path first. In borderless mode a real mouse click on the window should activate it via normal Win32 rules (it didn't under exclusive). If clicking now works but mouse-move doesn't, that isolates the problem to the auto-reclaim, not input routing.
  • Decide whether focus-follows-mouse is even wanted. Auto-stealing foreground on hover can be surprising; an explicit click-to-focus may be the better UX.
  • Rule out multi-window event routing. Confirm there's a single top-level display window and that WM_KEYDOWNAppendKeyEventg_Player().Display() queue is the one the client actually drains.

Flip mode may help #617 (borderless makes normal activation work again) but is probably not sufficient on its own — the focus-reclaim robustness above is the likely missing piece.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions