BLumia
deepin
2025-12-26 10:25 KWin (怀旧服)是如何渲染的?
Reply Like 2 View the author
KWin (怀旧服)是如何渲染的?
被标题耽搁的帖子,消灭二楼回复。
不是总分总结构的文章都不是好文章,从你行文的风格上来看应该比较适合搞直播!😏
不是总分总结构的文章都不是好文章,从你行文的风格上来看应该比较适合搞直播!😏
太长时间没写了,生疏了
这个就叫专业
这个就叫专业

先mark再看~

Popular Events
More
好久不见,甚是想念,给大家来点稍微没那么湿的干货,有些地方讲的可能太跳跃了,希望大家可以结合实际代码走一遍。
照例给自己打个广告
我是 Deepin 的开发者竹子,现在负责维护 deepin fork 的 kwin,偶尔兼职 DDE 移植和 Treeland 新项目研发。
Github: https://github.com/justforlxz
Blog: https://blog.mkacg.com
本文尝试从源码调用链的角度,梳理一次 KWin 完整的渲染流程。
需要提前说明的是,文章并不会对出现的代码做逐行语义解释,也不会深入讨论具体的渲染算法、OpenGL 状态细节或特效本身的视觉实现。这里关注的核心问题只有一个:一次画面是如何在 KWin 内部被触发、调度并最终提交的。
在阅读 KWin 源码时,很容易被大量接口类、虚函数和跨子系统调用打断思路,例如 RenderLoop、Compositor、Scene、EffectsHandler、ItemRenderer 等模块彼此交织。如果不先把调用顺序和职责边界理清楚,单独看某个函数往往很难判断它处在渲染管线的哪个阶段。
因此,下面的分析会刻意保持在调用关系和阶段划分这一层级:
代码片段的选取仅用于定位关键路径,其它分支和边缘逻辑会被省略或忽略。
合成器执行
KWin 的渲染是以物理屏幕为单位执行的,所以首先在添加屏幕时,会添加一个 RenderLayer 作为渲染层,当需要为该屏幕更新画面时,调用对应的 RenderLayer 对象就可以了。
从上述代码可以看出,添加一个屏幕后,会为 RenderLayer 绑定信号槽,在槽函数中执行
composite。现在让我们关注一下
frameRequested是从哪里发出的。scheduleRepaint表示该输出存在未呈现的视觉变化,其来源可能是 Wayland surface damage、动画、特效、光标或屏幕输出状态变化。RenderLoop 并不关心变化来自哪里,而是根据 presentMode 决定何时通过 frameRequested 触发一次合成。
至此,我们已经知道了 KWin 是从何时产生一次渲染操作的,回到最开始的位置,开始执行一次 composite 时,里面又在做哪些事情。
KWin 将绘制画面的操作分为了四个阶段:
prePaintPass、preparePaintPass、paintPass、PostPaintPass。prePaintPass—— 状态预处理阶段职责
RenderLayerDelegate、Scene和Effect:一帧即将开始阶段语义
preparePaintPass—— 重绘区域汇总阶段职责
RenderLayer及其 delegate 上报的 repaint 区域阶段语义
paintPass—— 实际绘制阶段职责
阶段语义
postPaintPass—— 帧结束清理阶段职责
阶段语义
在四个函数中,都支持递归处理 sublayer,这些代码都可以先无视掉。
重点关注
paintPass中调用的layer->delegate()->paint(target, region);,在这里开始,KWin 才是真正开始着手处理渲染。在 KWin 中,所有的子系统都是由接口类定义的,这使得 KWin 可以随意更换子系统的实现,例如合成器可以换成 XRander、OpenGL,送显有窗口化和 DRM。
paint 函数一个接口函数,layer、delegate、scene 都是渲染子系统定义的多个接口类,在 scene 中定义了交互方式,之后就由不同的渲染后端提供对应的实现。
delegate 里最终调用的是
WorkspaceScene::paint,WorkspaceScene就是接口类Scene的实现。ScreenPaintData里存储的是绘制相关的数据,里面其实只有屏幕和矩阵的信息。在
WorkspaceScene::paint中,在beginFrame和endFrame之间,effects->paintScreen()开始一幅画面的处理了。从名字上也能看得出,这里是特效子系统绘制屏幕的函数,和渲染子系统一样,特效子系统也是有好几个接口类规定了交互方式。
本次我们只关注流程,函数是如何调用的,所以就对不重要的地方先略过了。
beginFrame和endFrame调用的都是m_renderer,根据上述的介绍,渲染器就要看此时配置使用的哪个,目前应该以OpenGL渲染方式为主。这两个函数的代码量不多,就是做些绘制前和绘制后的工作,让我们重点开始看特效的调用,特效子系统的实现是在
EffectsHandlerImpl类中,在该类中,有这些函数是我们需要关注的。从函数的名字上可以看出,KWin 将很多内容都拆分为三个阶段:
pre、paint和post,然后将具体的工作放在paint中,但是当我们真的打开paintScreen时,我们就会傻眼了,这都写的啥、啥、啥(自动播放王宝强语音)。从现在开始,会开始烧脑,特别是在编辑器里点一下函数发现是虚函数,然后又要跳回去重新找实现,然后发现又回到了原地时。
根据代码可以看出,这里是在检查当前的迭代器是不是激活特效的最后一个,如果不是,就调用下一个,然后再减回来。如果已经是最后一个,则会调用
finalPaintScreen。至少我们能先看懂这个最终绘制屏幕函数,让我们先看一下这个迭代器里有什么吧。
看起来这里就是所有激活的特效插件,那么
paintScreen里应该就是调用一个特效插件的paintScreen。来看一下代码。这个 effects 是
EffectsHandler,而EffectsHandlerImpl的基类就是EffectsHandler...噫,这什么鬼代码,怎么自己又调回来自己了。
这时候就要回想一些很重要的事情了,KWin 是 C++ 开发的,C++ 是面向对象的语言,而面向对象编程有三个特点:
封装继承多态。既然这里代码这么诡异,那就应该看一下paintScreen到底是个啥了!其实在上面我已经贴出来了重要信息,不知道各位有没有发现,
paintScreen竟然有一个override的小尾巴,那Effect::paintScreen也是虚函数吗?果然,这个函数也是虚的!
这也符合最开始的想法,迭代器里执行的函数确实是特效插件的函数,让我们打开一个特效插件,看一下里面的内容。
果然,在每个特效插件里,都主动调用了一次
effects->paintScreen,从而回到了最开始看到的奇怪迭代器调用。现在让我们梳理一遍流程:
既然已经执行到了 finalPaintScreen,那就看一下 finalPaintScreen 的实现吧。
无论是
paintGenericScreen还是paintSimpleScreen,基本结构都是类似的,遍历窗口列表,为每个窗口执行paint。有一个需要注意的是,在 paintSimpleScreen 中存在一个遮挡剔除的功能。
paintWindow就比较简答了,和 screen 一个套路。但是需要留意的是,在开发插件时,如果我们想要直接替换画面,那应该先调用 effects->paintWindow,将其他插件的画面都绘制上去,再绘制自己的。
举个例子,窗口模糊插件是先在窗口位置上绘制一块模糊背景,再调用
effectc->paintWindow绘制窗口本体画面,而水印插件则是先绘制窗口本体画面,再绘制水印内容。这是一个非常关键的设计约束:
在 effects 的 paintWindow 的最后一步里,又调用回了 scene 的 finalPaintWindow,然后调用 drawWindow,再经过同样的一套流程,最终执行到了 scene 的 finalDrawWindow 函数。
在这个函数里,调用到了渲染的 renderItem 函数,看一眼 OpenGL 渲染后端的代码。
代码略长,大概内容就是绘制窗口数据,设置一些参数。
renderItem()是一个很好的分界点:在此之前,所有数据仍然是 逻辑对象 + 渲染参数,从这里开始,才进入 顶点、纹理、Shader、Draw Call。
一个重要但容易忽略的事实是:
它能做的事情被严格限制在:
真正的几何拆解、VBO 填充、Shader 选择,全部发生在渲染后端内部。
到这里,其实已经可以给整个流程下一个阶段性结论了:
梳理一遍过程:
本次我们只进行代码的梳理,并未涉及底层的内容,窗口的画面是如何经过特效链加工的?怎么绘制到屏幕上的?WindowPaintData 又是什么?这些都没有涉及。
通过对代码调用链的梳理,可以看出 KWin 的渲染流程并不是一条线性的“绘制函数调用序列”,而是一套由时序驱动、阶段划分明确、子系统职责严格隔离的渲染管线。
从触发机制上看,渲染并非由窗口事件直接发起,而是由 RenderLoop 根据当前的呈现模式(VSync、Async、Adaptive)调度,在合适的时间点通过 frameRequested 信号进入合成器。这一设计将何时渲染与渲染什么彻底解耦。
在合成器内部,一次 composite() 被拆分为 prePaint、preparePaint、paint、postPaint 四个阶段。这种拆分并非形式上的分层,而是明确区分了:
进一步向下,Scene 作为渲染流程的中枢,将“屏幕级”与“窗口级”绘制分离处理。屏幕级流程负责背景、遍历顺序以及全局遮挡策略,而窗口级流程则以 WindowItem 为最小单位,逐一执行特效链和最终绘制。这种分离使得复杂特效只需关注自身作用的层级,而不必感知整个屏幕状态。
特效子系统的实现是整个流程中最具辨识度的部分。KWin 并未采用传统的“前后回调”或“事件分发”模型,而是通过一个显式维护的迭代器递归调用链,将所有激活的特效串联成一条严格有序的执行路径。特效插件必须主动调用 effects->paintScreen / paintWindow 才能将控制权传递下去,这一设计在机制上强制了绘制顺序的可控性,也为特效之间的叠加提供了明确的语义边界。
当特效链执行完毕后,流程才真正进入渲染后端。无论是 OpenGL 还是其他实现,后端只接收已经完全加工好的渲染数据:几何、纹理、变换矩阵、混合参数。特效系统不直接参与任何 API 级别的绘制调用,这保证了渲染后端的可替换性,同时也限制了特效的能力边界。
综合来看,KWin 的渲染架构体现出几个鲜明特征:
本文只完成了对整体流程和结构的梳理,并未深入到具体的数据结构和渲染实现细节。但在理解了这条主干之后,再回头分析某一个特效、某一次性能回退,或某个渲染异常,其定位成本都会显著降低。