Magpie icon indicating copy to clipboard operation
Magpie copied to clipboard

窗口化缩放

Open Blinue opened this issue 11 months ago • 52 comments

Close #135 Close #1052 Close #959

这个 PR 用于跟踪开发进度和收集反馈。已实现的功能:

  • 全屏模式缩放时禁用了源窗口的标题栏和大小调整。
  • 切换前台窗口不会中止缩放,且缩放窗口不再总是置顶。现在源窗口是缩放窗口的所有者,这确保了缩放窗口始终在源窗口之上。技术上缩放窗口不再需要置顶,但置顶有助于在旧设备上激活 DirectFlip,因此当源窗口位于前台,缩放窗口仍会置顶。
    • 因此不再需要“记忆缩放窗口”功能。
    • 已知问题:Win10 的文件资源管理器存在兼容性问题,置顶总是失败。
  • 如果源窗口不在前台,允许鼠标进入黑边。
  • 游戏内叠加层上添加开发者选项和调试信息,打开开发者模式后可以使用。
  • 窗口化缩放时缩放窗口和源窗口保持风格一致。
  • 缩放窗口支持调整大小,存在最小和最大尺寸限制。调整大小时长宽比保持不变。
  • 鼠标在源窗口的标题栏或叠加层工具栏上可以拖动缩放窗口。
  • 不再使用 imgui.ini,叠加层位置保存在配置文件中。叠加层支持贴靠在窗口边缘。
  • 工具栏提供截图功能,开启开发者模式后还可以导出任意通道的任意输出,这对调试着色器很有帮助。效果输出导出格式为 png,中间结果导出格式为 dds。

Blinue avatar Feb 11 '25 12:02 Blinue

窗口下使用的话,最小帧率给的太低了,强制看慢动作一样,至少要60fps 理想情况至少120fps(我的显示器是240Hz,两边并行使用不同的app,左边240fps在刷网页,右边30fps在刷程序,这个体感很微妙,特别是上下滚动内容的视觉体验特别黏腻)

hooke007 avatar Feb 15 '25 17:02 hooke007

窗口下使用的话,最小帧率给的太低了,强制看慢动作一样,至少要60fps 理想情况至少120fps(我的显示器是240Hz,两边并行使用不同的app,左边240fps在刷网页,右边30fps在刷程序,这个体感很微妙,特别是上下滚动内容的视觉体验特别黏腻)

最小帧率不影响流畅度,你限制最大帧率了吗

Blinue avatar Feb 16 '25 00:02 Blinue

没有限制最大帧率

hooke007 avatar Feb 16 '25 03:02 hooke007

https://github.com/user-attachments/assets/eea34685-6014-4d9c-9556-7e3a316d66bb

录了一个高糊的120fps视频,不知道能不能看出来,缩放前后滚动的手感有点发粘。

hooke007 avatar Feb 16 '25 06:02 hooke007

滚动时帧率多少

Blinue avatar Feb 16 '25 07:02 Blinue

大部分时间30-50左右,没超过60fps

hooke007 avatar Feb 16 '25 07:02 hooke007

是帧率低导致的,换个缩放模式或捕获试试

Blinue avatar Feb 16 '25 07:02 Blinue

是帧率的问题,但是最低帧稳不住,需要提高下限。 DD能慢慢接近120fps,这时候是流畅的,需要鼠标和画面一直保持大幅变化才能维持,一旦开始回落就开始卡手了 GDI和DWM不能使用

hooke007 avatar Feb 16 '25 07:02 hooke007

看起来是因为功耗不稳定,那么全屏化也有同样的问题

Blinue avatar Feb 16 '25 07:02 Blinue

是的全屏也是一样的。窗口化的使用放大了这个差异,因为和旁边的程序不在一个流畅度上。

hooke007 avatar Feb 16 '25 07:02 hooke007

引入最低帧率就是为了解决这个问题,最高可以设置成 30FPS,这也不够吗

Blinue avatar Feb 16 '25 07:02 Blinue

30FPS 实在太低了。。。我显卡够你放240我都没问题

hooke007 avatar Feb 16 '25 07:02 hooke007

主流显示器还是60Hz,而且最低帧率的目的是维持显卡的能耗等级,和显示器刷新率无关。这个问题和窗口化无关就新开议题讨论吧,增加选项很容易,但需要你测试一下。

Blinue avatar Feb 16 '25 08:02 Blinue

窗口化缩放有两个大原则:

  1. 缩放窗口风格尽量贴近源窗口,比如边框和圆角风格保持一致。一个例外是阴影,阴影可以提高窗口边缘的对比度,也有助于区分前台和非前台窗口,因此缩放窗口始终有阴影。
  2. 缩放窗口支持在边框外调整尺寸,避免对操作源窗口产生干扰。Windows 窗口上边缘都是在边框内调整尺寸,因此至少需要一个辅助窗口来拦截上边框外的鼠标操作。

下面是窗口分类,我见过的窗口都可以归类到其中一种

  1. 原生 原生

    • 典型窗口:命令提示符
    • 窗口样式:需要 WS_CAPTION
    • 特征:系统标题栏和边框,有阴影,左右下三边在窗口外调整大小,Win11 中有圆角
    • 处理:缩放窗口应保持风格一致,而且上边框应在窗口外调整大小,因此需要一个辅助窗口。如果没有裁剪且捕获标题栏,可以考虑只捕获客户区并添加原生标题栏。
  2. 无标题栏 无标题栏

    • 典型窗口:UWP
    • 实现方式:使用 WM_NCCALCSIZE 移除系统标题栏,然后自绘上边框(极少数窗口实现了系统原生上边框,如 Windows Terminal 和 Magpie),Win11 中上边框由 OS 绘制到客户区内
    • 窗口样式:需要 WS_CAPTION
    • 特征:无标题栏,系统边框(上边框可能自绘),有阴影,左右下三边在窗口外调整大小,Win11 中有圆角
    • 处理:同 1
  3. 无边框 无边框_Win10 无边框_Win11

    • 典型窗口:VSCode
    • 实现方式:使用 WM_NCCALCSIZE 移除边框,然后使用 DwmExtendFrameIntoClientArea 恢复阴影
    • 窗口样式:需要 WS_CAPTION 或 WS_THICKFRAME
    • 特征:无标题栏,是否有边框取决于 OS 版本(Win10 中不存在边框,Win11 中边框被绘制到客户区内),有阴影,在窗口内调整大小,Win11 中有圆角
    • 处理:缩放窗口应保持风格一致,这意味着 Win10 和 Win11 上缩放窗口应使用不同的风格。Win10 中如果要移除边框但保留阴影,必须使用和源窗口相同的方式,另外我们需要在窗口外调整大小,因此需要 4 个辅助窗口。Win11 中这类窗口有着特殊的边框,因此和 Win10 的处理方式相同,不同的地方在于客户区内存在边框,捕获时应把边框裁剪掉,缩放窗口也应在四周为边框保留空间。
    • 备注:正常情况下位于前台时窗口阴影会变深,但某些窗口的阴影始终不变(大部分基于 electron),这可以通过WM_NCACTIVATE 实现,不清楚是 bug 还是故意为之。这种行为破坏了视觉效果的一致性,因此缩放后不会遵守这一点
  4. 无边框和阴影 无边框和阴影

    • 典型窗口:微信
    • 实现方式:通过 WM_NCCALCSIZE 移除边框,不使用 DwmExtendFrameIntoClientArea
    • 窗口样式:任意
    • 特征:无标题栏、边框和阴影,在窗口内调整大小,Win11 中无圆角
    • 处理:同 3。原因在于无法确定窗口是否使用了 DwmExtendFrameIntoClientArea,因此没办法获知它是否有阴影。由于这个限制,我们假设所有使用 WM_NCCALCSIZE 移除边框的窗口都有阴影,一方面有阴影的情况更多,比如基于 electron 的窗口,另一方面如果假设没有阴影会使得 Win11 中不能正确裁剪边框导致黑边,而如果假设有阴影,猜错的后果相对较轻。
  5. 无边框和阴影2

    • 实现方式:特殊窗口样式
    • 窗口样式:无 WS_CAPTION、WS_BORDER 和 WS_THICKFRAME
    • 特征:无标题栏,边框和阴影,在窗口内调整大小,Win11 中无圆角
    • 处理:这类窗口外观和 4 相同,但我们可以精确判断。源窗口太朴素,甚至没办法区分是否有焦点,缩放时有必要添加阴影。Win10 中处理方式同 3,Win11 中则同 2 并禁用边框和圆角,优点是只需一个辅助窗口。

下面是不包含 WS_CAPTION 样式的窗口类型,它们很罕见,方便的话可以支持。WS_OVERLAPPED 和 WS_POPUP 唯一的区别在于前者会自动添加 WS_CAPTION 样式,如果手动移除 WS_CAPTION 样式,两者没有区别。

  1. WS_THICKFRAME WS_THICKFRAME

    • 窗口样式:无 WS_CAPTION(即 WS_BORDER | WS_DLGFRAME),存在 WS_THICKFRAME
    • 特征:无标题栏,系统边框但上边框较粗,有阴影,左右下三边在窗口外调整大小,Win11 中有圆角
    • 处理:技术上这类窗口和 1 唯一的区别在于标题栏很窄只能容纳调整窗口大小的区域。处理方式和 1 类似,但即使捕获标题栏也应把上边框裁剪掉。
  2. WS_BORDER WS_BORDER

    • 窗口样式:无 WS_THICKFRAME 和 WS_DLGFRAME,存在 WS_BORDER
    • 特征:无标题栏和阴影,深色边框(包括上边框)位于客户区外,边框宽度和窗口是否被 DPI 虚拟化有关,Win11 中无圆角
    • 处理:和其他类型差别很大,视同 5
  3. WS_DLGFRAME WS_DLGFRAME

    • 窗口样式:无 WS_THICKFRAME 和 WS_BORDER,存在 WS_DLGFRAME
    • 特征:无标题栏和阴影,3D 样式的边框位于客户区内,Win11 中无圆角
    • 处理:和其他类型差别很大,视同 5

Blinue avatar Feb 19 '25 11:02 Blinue

ImGui 的一个更改导致字体缓存和以前不兼容了。https://github.com/ocornut/imgui/commit/8a9de84cd0130110ce6d1411558a8d59d7169ae3

IM_DRAWLIST_TEX_LINES_WIDTH_MAX 用于定义数组长度,对它的修改破坏了 yas 的假设,导致读取旧缓存时报错。这个问题很好解决,更新缓存版本号使旧缓存失效即可,不过 imgui 在修正更新里修改头文件宏定义确实不谨慎。

Blinue avatar Mar 07 '25 06:03 Blinue

我尝试了各种组合,结果想在混合架构上实现流畅调整窗口大小几乎不可能。noflicker_directx_window 项目效果不错,可惜我们需要最小化延迟,不能用这个架构。

Blinue avatar Mar 09 '25 08:03 Blinue

从我用过的软件来看,基于 GDI 和非 Flip 交换链模型的 Direct3D 程序(如 Qt、WPF、Flutter)几乎都能做到流畅调整大小(即边框不会闪烁),代价是额外的拷贝。noflicker_directx_window 这个项目其实也有问题,我测试下来,在核显渲染压力很大导致 DWM 无法保持 60 帧的情况下,也是会一定程度地闪烁的。不过以那些 D3D 程序为灵感,我认为可以在一般情况下按原样调用 Present,如果检测到大小改变的话就转而渲染到一个 XAML/Composition 图片上,这样便能借助 UWP 原生的流畅调整大小的特性,用一时的延迟换流畅度。

apkipa avatar Mar 09 '25 12:03 apkipa

我这里 Qt、wpf 和 flutter 都做不到流畅,独显直连时表现很好,混合架构只有 GDI 和 UWP 不会闪烁🤔

wpf 的表现

https://github.com/user-attachments/assets/351e3191-33b3-4bd2-9d20-b3a9d76d07a7

noflicker_directx_window 这个项目其实也有问题,我测试下来,在核显渲染压力很大导致 DWM 无法保持 60 帧的情况下,也是会一定程度地闪烁的。

readme 里写了已知问题,混合架构下可以消除 99% 的闪烁,不是完美的,我测试也是如此。

不过以那些 D3D 程序为灵感,我认为可以在一般情况下按原样调用 Present,如果检测到大小改变的话就转而渲染到一个 XAML/Composition 图片上,这样便能借助 UWP 原生的流畅调整大小的特性,用一时的延迟换流畅度。

  • 首先 XAML Islands 做不到流畅调整大小。
  • 使用 CreateSwapChainForCoreWindow 把 D3D 内容嵌入 UWP 应用做不到流畅调整大小。
  • 使用 UWP 原生框架我怀疑是否可行,这既不是官方支持的用法,也有初始化太慢、可定制程度低等问题。更棘手的是缩放时需要为边框保留空间,因此要渲染到子窗口里。

Blinue avatar Mar 09 '25 14:03 Blinue

我这里 Qt、wpf 和 flutter 都做不到流畅,独显直连时表现很好,混合架构只有 GDI 和 UWP 不会闪烁🤔

这可能和魔改过的窗口逻辑有关,我的机器是 i+n,不支持独显直连,不过无论是指定在独显还是核显上运行,WPF 默认项目模板的窗口以及这个 Flutter 项目 https://github.com/Ferry-200/coriander_player 都没有闪烁问题。(严格来说,包括 GDI 在内的这些程序还是会非常偶尔地闪烁,不如 UWP,但比 noflicker 要好上十几倍。)

首先 XAML Islands 做不到流畅调整大小。

其实 v1(系统 XAML)是可以的,但需要未公开 API 来诱导窗口同步,我之前在 noflicker 的 issue 里描述了部分工作机制;v2(WinUI 3)由于用了自己的 Compositor,可能反而不行。我所指的 UWP 也包括 v1 这个场景。

使用 CreateSwapChainForComposition 把 D3D 内容嵌入 UWP 应用做不到流畅调整大小。

确实如此,所以必须要用额外的 XAML 元素去挂载表面而不是交换链,因此就像非 Flip 交换链那样需要一次额外拷贝。不过我突然想到,拷到 GDI 重定向表面的效果应该差不多,不需要依赖 XAML 了。

GitHub
Windows端本地音乐播放器,使用Material You配色。Dart (Flutter) + Rust (lofty, windows-rs) + C (bass lib) 多语言项目。绝赞开发中。 - Ferry-200/coriander_player

apkipa avatar Mar 10 '25 01:03 apkipa

其实 v1(系统 XAML)是可以的,但需要未公开 API 来诱导窗口同步,我之前在 noflicker 的 issue 里描述了部分工作机制;v2(WinUI 3)由于用了自己的 Compositor,可能反而不行。我所指的 UWP 也包括 v1 这个场景。

我看到了 https://github.com/bigfatbrowncat/noflicker_directx_window/issues/1 ,谢谢挖掘!XAML Islands 有没有办法拿到 IDCompositionDevice?

不过我突然想到,拷到 GDI 重定向表面的效果应该差不多,不需要依赖 XAML 了。

很有意思,我会试试。

Blinue avatar Mar 10 '25 02:03 Blinue

我偶然发现 CreateSwapChainForHwnd 和 EnableResizeLayoutSynchronization 的组合完全没有闪烁,在多个设备上测试都没问题。源代码在下面 @apkipa

D3D11WithoutFlicker.zip

不过我突然想到,拷到 GDI 重定向表面的效果应该差不多,不需要依赖 XAML 了。

我测试了,这个办法完全可行,闪烁很偶尔会出现,属于 GDI 正常现象,有趣的是甚至不需要创建交换链了。(在混合架构下测试发现 GDI 也有明显闪烁🤔只有 UWP 始终完美)

Blinue avatar Mar 10 '25 12:03 Blinue

XAML Islands 有没有办法拿到 IDCompositionDevice?

虽然不完全是,但 Window.Compositor 返回的对象可以看作是一个 DComp 设备,对其 QI 被 cloaked 的接口即可。不过拿到也没意义,因为没法手动控制 Commit 的时机。我找到的不会闪烁的方法是在 WM_SIZE 时调用 IFrameworkApplicationPrivate::SetSynchronizationWindow(但说实话,我很怀疑这种操作的正确性与未来可靠性;UWP 的调用逻辑并非如此,因此我更倾向于这是在靠 bug 运行)。

D3D11WithoutFlicker.zip

给交换链指定 DXGI_SCALING_STRETCH 的话我认为就没意义了,画面还是会扭曲的,没法实现真正的流畅调整窗口大小。不过也许恰好适合 Magpie 的窗口化缩放?

apkipa avatar Mar 10 '25 13:03 apkipa

只要 back buffer 大小和输出大小相同就不会拉伸,而且它是默认值 0,不需要特别指定。noflicker_directx_window 也是使用这个值。

Blinue avatar Mar 10 '25 14:03 Blinue

只要 back buffer 大小和输出大小相同就不会拉伸,而且它是默认值 0,不需要特别指定。noflicker_directx_window 也是使用这个值。

我在交换链内画了一个(0,0,200,300)的定长矩形进行测试,指定 DXGI_SCALING_STRETCH 时边框部分没问题,但在核显高压情况下调整大小会导致矩形明显变形,类似 noflicker 的情况(录屏体现不出来)。DXGI_SCALING_NONE 的话就是边框闪烁了。

apkipa avatar Mar 10 '25 14:03 apkipa

我在 Magpie 中集成后也发现了一些问题

  • 拉伸问题确实存在,一般不明显,但开启叠加层后可以看到窗口抖动
  • 如果窗口没有非客户区,EnableResizeLayoutSynchronization 无效
  • 多次创建 DesktopWindowXamlSource 会出错,要修复需要更多 hack

这个方案效果很好,管理起来却很麻烦。

我测试了,这个办法完全可行,闪烁很偶尔会出现,属于 GDI 正常现象,有趣的是甚至不需要创建交换链了。(在混合架构下测试发现 GDI 也有明显闪烁🤔只有 UWP 始终完美)

GDI 的方案也有一些问题,首先是混合架构下闪烁比较明显,这是所有 GDI 的共同现象,其次有性能问题,尤其是窗口尺寸很大时调整大小很卡,显存占用也会大幅增加。GDI 是 CPU 世界的东西,和高性能不搭边。

case WM_PAINT:
{
	Render(true);

	PAINTSTRUCT ps;
	HDC hdc = BeginPaint(hWnd, &ps);

	winrt::com_ptr<ID3D11Texture2D> backBuffer;
	g_pSwapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer));
	winrt::com_ptr<IDXGISurface1> dxgiSurface = backBuffer.as<IDXGISurface1>();

	HDC hdcSrc;
	dxgiSurface->GetDC(FALSE, &hdcSrc);

	RECT clientRect;
	GetClientRect(g_hWnd, &clientRect);
	BitBlt(hdc, 0, 0, clientRect.right, clientRect.bottom, hdcSrc, 0, 0, SRCCOPY);

	dxgiSurface->ReleaseDC(nullptr);

	EndPaint(hWnd, &ps);

	DwmFlush();
	break;
}

Blinue avatar Mar 10 '25 14:03 Blinue

@apkipa 我根据你的挖掘成功复现 UWP 调整大小逻辑,也集成进 Magpie 进行测试,各种配置下都很丝滑。下面是一个demo

D3D11WithoutFlicker.zip

Win10 不支持 IDCompositionDesktopDevicePartner6,有替代的接口吗?

原文链接

In the end there still doesn't exist a perfect solution that really solves the resize flicker. But the UWP applications also use D3D/DComp however they don't flicker at all no matter how you resize the window, I'm so curious how MS achieve this.

I'm afraid that there really isn't a way for D3D-based applications to properly synchronize with compositor (DWM), although it works much better if you use the old BitBlt model instead of flip model. And yes, while UWP (only XAML ones; Direct3D ones still flicker) never flickers, the techniques behind cannot be applied here because UWP never calls IDXGISwapChain::Present(); it just calls IDCompositionDevice::Commit() to commit the entire visual tree to DComp. The final presentation is handled by DWM atomically. Then, UWP solves the flickering by:

  • Marking the window as eligible for resize synchronization. This will let OS create a DComp synchronization handle during WM_SIZE, which can be retrieved by calling GetResizeDCompositionSynchronizationObject().
  • Making changes to the DComp visual tree, then calling the undocumented API SynchronizedCommit(resizeHandle) on the DComp device. Calling this (instead of Commit()) is necessary, or the changes will be visible before the window frame redraws (flickers in a reverse way).
  • Calling Windows::UI::Core::CoreWindowResizeManager::GetForCurrentView().NotifyLayoutCompleted(). This tells DWM to finally start composing a new frame for the window.

It cannot be done for D3D since AFAIK it doesn't have the SynchronizedCommit() equivalent.

Blinue avatar Mar 12 '25 15:03 Blinue

有替代的接口吗?

我在 gist 里进行了追加。很可惜,Win10 上这个接口并不可靠:我曾经测试过 1903 和 22H2,结果发现不同系统上的 IDCompositionDesktopDevicePartner6 违反了 COM 约定,他们在修改接口 ABI 的时候没有修改接口的 IID。虽然对于一个内部 API 来说这是可以接受的,但这就意味着我们用户没有可靠的办法去检查 QI 到的接口是否真实可用,强行用的话会在不兼容的系统版本直接崩溃。我没兴趣去一个个抓包,所以只能给出一个 22H2 的接口。虽然 Win10 到 Win11 似乎按照规范正确地改了接口,但不好说未来版本是否会重演这种情况。

apkipa avatar Mar 13 '25 01:03 apkipa

经过几天的研究,我的结论是如果没有 dwm 协助,想摆脱闪烁是不可能的。本质上我们希望 dwm 绘制新的窗口框架时刚好合成新帧,但其中变数太多,最严重的是混合架构下需要在显卡间传输帧数据,无法预测 Present/Commit 后多久 dwm 能收到。我找到了一个尽可能为 dwm 合成新帧预留时间的方法,这可能是极限了。

Blinue avatar Mar 13 '25 09:03 Blinue

能问下为啥不支持隐藏光标了么? 个人在玩adv的时候,喜欢截图,但是缩放后进行截图,光标会被截取到,所以我都隐藏光标玩的,挺希望能有这个功能的。

bluelaze avatar Mar 26 '25 10:03 bluelaze

能问下为啥不支持隐藏光标了么? 个人在玩adv的时候,喜欢截图,但是缩放后进行截图,光标会被截取到,所以我都隐藏光标玩的,挺希望能有这个功能的。

#1104

Blinue avatar Mar 26 '25 10:03 Blinue