在 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

一开始很容易以为只要拦截普通 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

LD_PRELOAD 能拦截动态链接过程中的外部符号引用,但 WPS 自带 QtCore 内部也有大量直接调用:

kso_qt::QUnifiedTimer -> kso_qt::QElapsedTimer::elapsed()

这些调用发生在同一个共享库内部,不一定经过 PLT,也就不一定会被 LD_PRELOAD 替换。

这点很关键。PPT 动画调度很可能正好在 QtCore 的 QUnifiedTimer 里。只拦截 WPS 其他 .soQElapsedTimer 的调用,可能能看到 hook 生效,但真正影响动画调度的路径仍然没有被改到。

因此最终实现分成两层:

  1. 导出同名同版本的 QElapsedTimer 符号,处理外部动态符号引用。
  2. 在进程启动后对 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