ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

为什么Denial丝滑不撕裂:Volition原子DRM/KMS呈现与多输出帧同步完全指南

为什么Denial丝滑不撕裂:Volition原子DRM/KMS呈现与多输出帧同步完全指南

【免费下载链接】denial

A smooth, customizable Wayland compositor with glass effects, three window layouts, and settings you can actually click. We got a little carried away.

项目地址:https://gitcode.com/gh_mirrors/de/denial
点击查看免费下载

Denial 是一款 Flutter 原生的 Wayland 合成器,它通过内置的 Volition 原子 DRM/KMS 呈现引擎实现零撕裂的帧同步:无论单屏还是多屏混刷,每一帧画面都会在显示器垂直消隐(vblank)边界被原子地整体替换。本文将用通俗的方式解释 Denial 为什么"丝滑不撕裂",以及它的多输出帧同步机制是如何工作的。

1. 先搞懂"撕裂":画面为什么会裂开?

显示器是逐行"扫描"刷新的:电子枪(或扫描电路)从屏幕顶部一行一行画到底部,画完一整帧后进入垂直消隐(vblank),然后开始下一帧。

传统做法存在一个风险:如果合成器在显示器画到一半时往显存里塞新画面,屏幕上半部分显示旧帧、下半部分显示新帧——这就是"撕裂"。

DRM/KMS(Linux 内核管理显示硬件的子系统)对此的解法是原子提交(atomic commit)+ 页面翻转(page flip):

  • 合成器一次性提交一整组新的显示状态(哪块 framebuffer 上屏、裁剪区域、旋转等);
  • 内核保证这些属性要么在某个 vblank 边界全部生效,要么完全不生效;
  • 绝不会出现"半新半旧"的中间状态。

Denial 的呈现路径正是建立在原子提交之上,而真正决定"提交时机是否精确"的,是它自研的 Volition 模块。

2. Volition:永不阻塞的 KMS 呈现预调度器

Volition 是 Denial 源码树内的显示同步库,位于 compositor/src/volition/mod.rs。它的模块注释把它定义为"有序原子 KMS 呈现前瞻引擎(ordered atomic-KMS presentation lookahead)"。

职责划分非常清晰(这一点对理解整个架构很关键):

角色负责什么
合成器(deniald)决定"呈现什么":选帧、持有 buffer 直到翻页完成、处理 Flutter/Wayland/DPMS/截屏策略
Volition决定"何时提交":持有 DRM 文件描述符、复用原子 plane 请求、在截止时刻精确提交

它的核心设计可以用三个数字概括:

  1. 提前 2 毫秒提交(LOOKAHEAD_SUBMIT_LEAD):许多 DRM 驱动的实际"锁存窗口"早于物理 vblank,所以 Volition 会提前一点把原子状态交给内核,让驱动在目标边界成功锁存。
  2. 100 微秒重试一次(LOOKAHEAD_RETRY_INTERVAL):提交若被内核短暂拒绝(EBUSY/EAGAIN等),不原地死等,而是重新入队、快速重试。
  3. 最多等 100 毫秒(LOOKAHEAD_MAX_WAIT):重试窗口有界。窗口内仍失败则上报Stalled,由合成器重建 KMS 状态,而不是让整个图形会话崩溃。

更妙的一点是:Volition 的所有 ioctl 都是非阻塞的(NONBLOCK标志 +PAGE_FLIP_EVENT)。它在一条名为volition-kms的独立线程上运行一个基于二叉堆的"截止时刻调度器":每个提交任务携带一个not_before时刻,线程只负责"逼近边界",绝不把主合成器线程卡在内核里等待。这带来两个直接收益:

  • 关闭会话、热插拔显示器时,主循环不会被一次卡住的 ioctl 拖死;
  • 每个输出流最多保留1 个在途提交(MAX_IN_FLIGHT_COMMITS_PER_STREAM),队列满时返回Backpressured(背压),上游帧管线据此自然降速,而不需要任何忙等。

提交请求的入口是Volition::submit_for_target():合成器把"渲染已完成的这一代画面 + 目标显示时刻"交给 Volition,由它负责逼近边界并提交。相关的调用方实现在 compositor/src/bin/deniald/output_scheduler.rs。

3. 多输出帧同步:让 144Hz 与 60Hz 各自丝滑

多显示器场景有个经典难题:两块屏幕刷新率不同(比如 240Hz 主屏 + 60Hz 副屏),帧"节拍"各不相同。Denial 的答案在 compositor/src/bin/deniald/frame_scheduler.rs,它的模块注释称之为"混刷帧管线(mixed-refresh frame pipeline)":

  • 每个通电输出都拥有自己独立的时间线,各自计算渲染截止时刻与呈现目标(presentation target);
  • 最快的输出同时是整个 Dart 桌面的"唯一时钟":Flutter 只按这个主时钟渲染一次;
  • 较慢的输出直接重放最近一次保留的画面投影,不额外触发渲染;
  • 物理呈现从不授权渲染:屏幕翻页事件只用于对"同一个输出时间线的未来边界"做有界的相位校准,不会打乱节拍。

配合"一块桌面级 GBM 大画布(atlas)"的设计——每个 CRTC(扫描输出控制器)从同一块 framebuffer 里扫描各自不同的源矩形——多屏不再意味着逐屏拷贝,屏数增加也不引入合成开销。这正是 compositor/README.md 里描述的"one desktop-wide GBM atlas"。

每块屏幕的输出调度器(output_scheduler.rs)还内置了两道保险:

  • 独立邮件箱(mailbox):每个 CRTC 各自的提交队列互不阻塞,一个屏忙不会拖住另一个屏;
  • 2 秒呈现看门狗(PRESENTATION_STALL_TIMEOUT):被接受的提交若迟迟不翻页,说明状态可能卡住,看门狗会触发恢复流程,而不是让画面"冻结"下去。

4. 出错时怎么办:重试、恢复、绝不黑屏

Volition 把内核错误分成了三类,每类都有明确归宿(见 compositor/src/volition/mod.rs 中的lookahead_failure_disposition):

错误类型例子处理方式
可重试EBUSY、EAGAIN、EINTR(内核忙)100µs 后重新入队重试,且允许其他输出的提交插队
可恢复EACCES、EINVAL、ENODEV等(链路重训、DRM 对象被替换、主权限交接)上报Stalled,合成器重建 KMS 状态——"重建显示状态是正确的边界,终止图形会话不是"
致命其余错误上报Failed,交由会话层处置

注意重试的一个细节:重试是"重新入队"而不是"原地重试"。这样当某个输出正忙时,另一块屏已经到点的提交可以先走,一块屏的拥塞不会让其他屏掉帧——这就是多输出"互不拖累"的代码级保证。

实测数据来自官方 README:在 DP-4/DP-5 双输出环境下,稳态循环跑在约180 Hz(跟随较慢的输出),平均渲染时间约0.32 ms;2400 帧的 Vulkan 客户端压测以 179.96 Hz 完成。

5. 相关源码与延伸阅读

想深入阅读或跟进开发,以下是关键入口:

  • Volition 呈现引擎:compositor/src/volition/mod.rs
  • 输出调度与多屏邮件箱:compositor/src/bin/deniald/output_scheduler.rs
  • 混刷帧时间线:compositor/src/bin/deniald/frame_scheduler.rs
  • 总体架构(含 Volition 定位说明):docs/architecture.md
  • 多输出与 VRR 配置(outputs.conf):compositor/README.md
  • 未来的硬件平面直扫方案(direct scanout):docs/direct_scanout.md
  • Rust 测试策略(KMS/热插拔/回滚契约):compositor/TESTING.md

合成器的快速自检命令(来自官方 README):

cargo test --manifest-path compositor/Cargo.toml --lib --bins --tests --features flutter

6. 常见问题

Q:这套机制需要手动开启吗?不需要。Volition 是deniald的核心组成部分,所有 KMS 呈现默认都经过它,用户只需正常安装并启动 Denial 会话。

Q:VRR(可变刷新率)下还会撕裂吗?Denial 支持按输出开启vrr=。VRR 改变的只是"边界何时到来",而 Volition 依旧在每条边界做原子锁存,同步保证不变。

Q:为什么 240Hz 游戏不会拖慢 60Hz 副屏?因为混刷管线让慢屏重放保留投影、每屏独立邮件箱;direct scanout 路线图更进一步:直扫时由硬件平面直接显示游戏画面,Flutter 只在自身有改动时才渲染,进一步降低跨屏耦合。

一句话总结:Denial 的丝滑来自"时机精确"(2ms 前瞻逼近 vblank)与"永不阻塞"(非阻塞 ioctl + 有界重试 + 背压)的组合,而多输出帧同步则来自每屏独立时间线 + 独立邮件箱 + 共享大画布的架构。这套机制不追求炫技,只追求一件事:每一帧,都在正确的边界,完整地上屏。

【免费下载链接】denial

A smooth, customizable Wayland compositor with glass effects, three window layouts, and settings you can actually click. We got a little carried away.

项目地址:https://gitcode.com/gh_mirrors/de/denial
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进