[DEV] GPU 硬件加速渲染下更新分层窗口的性能优化
使用 OpenGL, D3D12, Vulkan 图形接口进行离屏渲染,是否存在某种“捷径”可以减少在使用 UpdateLayeredWindow() 更新分层窗口时位图内存的复制次数?
背景
对于 GPU 硬件加速的分层窗口,可以直接在该窗口上创建 Swap Chain, 系统会把 GPU 绘制的内容按照不同的交换模型直接传输到各自的由 DWM 管理的 Surface 上。但该技术有一个限制:无法利用分层窗口的逐像素鼠标拾取检测特性。
因此 SAO Utils 2 在渲染桌面模式下的异形视图时 (View.sold = false), 会把 GPU 离屏渲染的纹理传送到 CPU 可以访问的内存,然后使用 UpdateLayeredWindow() 更新到对应的分层窗口。
这不可避免地会占用总线带宽和额外的缓冲区内存——更糟糕的是,由于 API 的设计 UpdateLayeredWindow() 必须通过 Memory DC 更新分层窗口,这又会引入另一个额外的的 从图形接口管理的内存缓冲区到 GDI Bitmap 内存的复制 。
参考
Direct3D 9
对于 D3D9 图形接口,可以通过 IDirect3DSurface9::GetDC() 获得一个 UpdateLayeredWindow() 直接用于更新分层窗口的 HDC.
Direct3D 11
虽然 D3D11 图形接口没有像 D3D9 那样直接给出 GetDC() 方法,但可以通过 ID3D11Texture2D::QueryInterface() 查询一个 IDXGISurface1 接口,最后通过 IDXGISurface1::GetDC() 获得一个和 D3D9 方案一样的可用 HDC.
以上捷径可能仍然无法避免 VRAM -> RAM 的传输,但至少这些内部实现应该可以减少一次额外的 GDI 位图转换或传输。
想法
其中一个突破口是,用于创建中转位图的 CreateDIBSection() 函数支持传入一个 CreateFileMapping() 所创建的 NT 句柄。
这使得 CreateDIBSection() 虽然不支持为创建的位图指定内存地址,但是至少可以指定一个共享内存对象。
OpenGL
对于 OpenGL 图形接口,应该可以通过 EXT_external_objects_win32 扩展减少额外的内存复制:
- 创建一个共享内存对象;
- 把该内存对象的句柄作为 Memory Object 导入到 OpenGL;
- 把该 Memory Object 绑定到 Pixel Buffer Object;
- 把渲染结果异步读取到该 PBO;
- 使用该内存对象的句柄创建 DIB 位图;
- 绑定该位图到 HDC 后更新分层窗口。
这个方案最大的挑战可能是 驱动的支持程度可能很低,大部分软硬件环境可能需要回退到内存对拷的实现。
Vulkan
相似地,Vulkan 存在 VK_KHR_external_memory_win32 扩展可以实现类似以上 OpenGL 的内存共享操作。
不同的是,Vulkan 对该扩展的支持度可能会比 OpenGL 不知道高到哪里去了。
Direct3D 12
D3D12 对其他图形接口的互操作似乎精简得只剩下 ID3D12Device::CreateSharedHandle() 了,然而该方法创建的 NT 句柄并非共享内存对象,不能用于 CreateDIBSection(), 纹理的 ID3D12Resource 接口也不提供 DXGI 互操作。