在 Linux 上绕过 WPS 演示的 FPS 限制
记录一次在 Arch Linux 上通过缩放 QElapsedTimer 与 libfaketime 加速,改善 WPS Office PPT 播放帧率。
目前在Linux上,由于没有硬件加速,WPS Office 的 Presentation 播放时动画会被锁在 24 FPS 左右,在使用平滑切换时效果尤其不佳。WPS 官方社区在2024年说会完善 Linux 的硬件加速,至今(2026)也没有下文。由于个人比较常用平滑切换,再加上这也是一种不错的视频动画制作方式,因此不得不临时想一个workaround
社区里有人提出过一个思路:先让 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()
但是 AUR 安装的 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 其他 .so 对 QElapsedTimer 的调用,可能能看到 hook 生效,但是并不能真正影响动画调度。
因此最终实现分成两层:
- 导出同名同版本的
QElapsedTimer符号,处理外部动态符号引用。 - 在进程启动后对 WPS QtCore 内部的
elapsed()、restart()、nsecsElapsed()做 inline patch,让库内部直接调用也跳到我们的缩放逻辑。
inline patch 的基本方式是:
- 找到 WPS QtCore 中原始函数地址;
- 校验函数开头的机器码前缀,避免 WPS 更新后错误 patch;
- 生成 trampoline 保存原始函数前缀,并跳回原函数剩余部分;
- 把原函数入口改成跳转到我们的 hook 函数;
- hook 函数调用 trampoline 得到原始时间,再乘以缩放系数返回。
启动脚本
最终启动脚本做了几件事:
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
验证方式
有两个probe来验证实现是否真的生效。
第一个在运行时 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