在 Linux 上绕过 WPS 演示的 FPS 限制
由于没有硬件加速,WPS Office 的 Linux 版本在播放 PPT 时有一个很明显的问题:演示动画经常被锁在接近 24 FPS,在使用平滑切换时效果不佳。
社区里有人提出过一个思路:先让 QElapsedTimer::elapsed() 和 QElapsedTimer::restart() 返回更小的时间值。这样 WPS 仍然按照原来的逻辑判断动画进度,但它看到的时间流逝变慢了。然后再用 libfaketime 把进程时间加速回来。两个操作叠加后,目标是让 WPS 的帧触发更频繁,同时尽量保持动画速度不变。
问题模型
如果 WPS 内部把演示动画调度在大约 40 ms 一帧,那么实际帧率就是:
1000 ms / 40 ms = 25 FPS
如果能让 WPS 认为只过了 20 ms,它就不会那么快把动画推进到下一段状态;但如果同时把进程时间加速 2 倍,那么现实世界中的 20 ms 对应用来说又像 40 ms。这样调度器可能更频繁醒来,而动画时间轴的推进速度仍接近原样。
默认参数是:
WPS_FAKE_SPEED=2.0
WPS_QELAPSED_SCALE=0.5
也就是:
libfaketime让时间加速 2 倍;QElapsedTimer返回值乘以 0.5;- 理论目标是接近 48 FPS,同时维持原动画速度。
如果想尝试接近 60 FPS,可以用:
WPS_FAKE_SPEED=2.5
WPS_QELAPSED_SCALE=0.4
确认并导出 WPS 自带 Qt 的计时器符号
一开始很容易以为只要拦截普通 Qt5 的符号就可以:
QElapsedTimer::elapsed()
QElapsedTimer::restart()
但当前机器上的 WPS 并不是直接使用系统 Qt,而是自带了一套 Qt 库:
/usr/lib/office6/libQt5CoreKso.so.5.12.12
查符号后发现,WPS 的 Qt 类被放进了 kso_qt 命名空间:
kso_qt::QElapsedTimer::elapsed() const@@Qt_5
kso_qt::QElapsedTimer::restart()@@Qt_5
kso_qt::QElapsedTimer::nsecsElapsed() const@@Qt_5
所以 preload 库必须导出这些带 kso_qt 命名空间、并且带 Qt_5 symbol version 的符号,否则 WPS 根本不会调用到我们的实现。
对应的 mangled names 是:
_ZNK6kso_qt13QElapsedTimer7elapsedEv
_ZN6kso_qt13QElapsedTimer7restartEv
_ZNK6kso_qt13QElapsedTimer12nsecsElapsedEv
覆盖 QtCore 内部的直接计时器调用
LD_PRELOAD 能拦截动态链接过程中的外部符号引用,但 WPS 自带 QtCore 内部也有大量直接调用:
kso_qt::QUnifiedTimer -> kso_qt::QElapsedTimer::elapsed()
这些调用发生在同一个共享库内部,不一定经过 PLT,也就不一定会被 LD_PRELOAD 替换。
这点很关键。PPT 动画调度很可能正好在 QtCore 的 QUnifiedTimer 里。只拦截 WPS 其他 .so 对 QElapsedTimer 的调用,可能能看到 hook 生效,但真正影响动画调度的路径仍然没有被改到。
因此最终实现分成两层:
- 导出同名同版本的
QElapsedTimer符号,处理外部动态符号引用。 - 在进程启动后对 WPS QtCore 内部的
elapsed()、restart()、nsecsElapsed()做 inline patch,让库内部直接调用也跳到我们的缩放逻辑。
inline patch 的基本方式是:
- 找到 WPS QtCore 中原始函数地址;
- 校验函数开头的机器码前缀,避免 WPS 更新后错误 patch;
- 生成 trampoline 保存原始函数前缀,并跳回原函数剩余部分;
- 把原函数入口改成跳转到我们的 hook 函数;
- hook 函数调用 trampoline 得到原始时间,再乘以缩放系数返回。
这比单纯 LD_PRELOAD 更激进,但对这个场景更直接。
启动脚本
最终启动脚本做了几件事:
export WPS_QELAPSED_SCALE=0.5
export FAKETIME="+0 x2.0"
export LD_PRELOAD="/path/to/libwps_qelapsed_scale.so:/usr/lib/faketime/libfaketimeMT.so.1"
exec /usr/lib/office6/wpp "$@"
实际使用时可以这样启动:
bin/wpp-fps-unlock demo.pptx
也可以调参:
WPS_FAKE_SPEED=2.5 bin/wpp-fps-unlock demo.pptx
如果 WPS 更新后 inline patch 失效,可以先禁用它排查:
WPS_QELAPSED_INLINE=0 bin/wpp-fps-unlock demo.pptx
验证方式
有两个小探针来验证实现是否真的生效。
第一个探针在运行时 dlopen() WPS QtCore,然后调用 QElapsedTimer:
make verify-runtime
验证结果显示,正常 120 ms 的计时会被缩放成 60 ms:
elapsed_default_ms=120
elapsed_direct_ms=120
elapsed_default_ms=60
elapsed_direct_ms=60
第二个探针把 WPS QtCore 作为启动时依赖加载,模拟 WPS 进程启动时 QtCore 已经存在的情况:
make verify-startup-patch
结果同样显示,直接调用 QtCore 自己地址上的 elapsed() 也会被 inline patch:
startup_direct_ms=120
startup_direct_ms=60