目标:开发一个极其简单的 kwin 插件,当鼠标在屏幕上移动时,右上角悬浮一个小窗口,实时显示当前鼠标的坐标值。
下图简单画出了特效渲染的流程,可以比较清楚看出一条 pain t通路需要进行的工作。
如果需要加载特效,一定需要开启合成器,因为 paint pass 是从合成器开始的:performCompositing
prePaintScreen->paintScreen ->finalPaintScreen ->prePaintWindow->paintWindow->postPaintWindow->postPaintScreen
在下文中会尝试做简单的介绍。
插件加载的组织结构
libkwineffects 目录
在libkwineffects/kwineffects.cpp 定义 EffectsHandler。如果合成器为 none,compositing_type == NoCompositing 那么 EffectsHandler 为空;如果不为空,所有特效都会 connect 这个对象。可以关联的信号特别多,几乎所有窗口操作相关的信号在这里都对外提供了。开发一个 Effect,可以通过这个对象与 workspace 建立联系,包括但不限于激活窗口、拖动窗口、移动到其他桌面,以及键鼠的动作等等。可以实现的功能很多。
给特效渲染用的比较重要的几个接口:
virtual void prePaintScreen(ScreenPrePaintData& data, int time) = 0;
virtual void paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) = 0;
virtual void postPaintScreen() = 0;
virtual void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) = 0;
virtual void paintWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data) = 0;
virtual void postPaintWindow(EffectWindow* w) = 0;
窗口操作常用的几个信号:
// 用户开始拖动或者 resize 窗口时发出
void windowStartUserMovedResized(KWin::EffectWindow *w);
// 正在 move 和 resize 窗口中发出的信号
void windowStepUserMovedResized(KWin::EffectWindow *w, const QRect &geometry);
// 完成 move 和 resize 操作时发出的信号
void windowFinishUserMovedResized(KWin::EffectWindow *w);
// 以及其他各个窗口最小化、最大化等信号
鼠标相关的信号:
// 如果特效需要更新鼠标坐标,最好先调用 startMousePolling
void mouseChanged(const QPoint& pos, const QPoint& oldpos,
Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons,
Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers);
// 鼠标形状发生变化时候的信号
void cursorShapeChanged();
显示器相关的信号:
// 比如插拔显示器时
void screenGeometryChanged(const QSize &size);
libkwineffects 库,是 core 和 effects 之间沟通的桥梁。因此可以看出 effects 目录下的特效渲染的实现,很大程度上依赖于 libkwineffects 目录下的接口。
Effect 类,在libkwineffects/kwineffects.h中定义。这是所有 kwin effects 的基类,除了可以自定义如何渲染,通过关联 EffectsHandler 的各个信号,还能对窗口等状态变更做出响应。比如窗口关闭、移动、最小化时候的各种特效等,都可以这样实现。
文件 effectloader.cpp
代码中有个全局变量 effects = findAllEffects(),这个全局变量在 libkwineffects/kwineffects.h中定义
extern KWINEFFECTS_EXPORT EffectsHandler* effects;
从名字来理解的话,这个应该是加载我们开发的 effects 插件的类。需要加载的特效插件分为三种:built-in 的,scripts 的,二进制插件的。
为了简便,每一种插件都单独对应了一个加载器。在此之上提炼了一个 AbstractEffectLoader 抽象加载器,定义了一些基本接口。极其重要的接口是 loadEffect,这是一个同步加载的接口
loadEffect:首先会检查该特效是否已经加载过了,为了实现这个check,加载器需要能跟踪所有已加载或者已销毁的特效。当加载成功,发出 effectLoaded 信号
readConfig 接口,每个特效都需要<effectName>Enabled这样的 key,一般来说配置文件都保存在家目录下或者 /etc/xdg:
const QString key = effectName + QStringLiteral("Enabled");
配置文件内容示例:
autocomposerEnabled=true
blurEnabled=false
colorpickerEnabled=true
coverswitchEnabled=true
cubeEnabled=true
fallapartEnabled=true
kwin4_effect_maximizeEnabled=false
kwin4_effect_translucencyEnabled=false
magiclampEnabled=true
nocontentmoveEnabled=true
showmouseposEnabled=true
windowgeometryEnabled=true
wobblywindowsEnabled=true
zoomEnabled=true
effect_builtins.cpp 文件
内建特效插件的加载与创建,EffectData 结构定义了插件的 name、displayname、createFunction 等等。
create函数中去执行 createHelper 也就是新建特效:
template <class T>
inline Effect *createHelper()
{
return new T();
}
比如我们新增一个特效 showmousepos,需要在 s_effectData 新增这样一段:
{
QStringLiteral("showmousepos"),
i18ndc("kwin_effects", "Name of a KWin Effect", "ShowMousePos"),
i18ndc("kwin_effects", "Comment describing the KWin Effect", "Show cursor's current coordinate on screen"),
QStringLiteral("Tools"),
QString(),
QUrl(),
#if !defined(QT_NO_DEBUG)
true,
#else
false,
#endif
false,
#ifdef EFFECT_BUILTINS
&createHelper<ShowMousePosEffect>,
&ShowMousePosEffect::supported,
&ShowMousePosEffect::enabledByDefault
#endif
EFFECT_FALLBACK
},
新建特效,其实就是 new 一个 ShowMousePosEffect 对象。
effects.cpp 文件
上面提及的功能强大的 EffectsHandler 在这儿实现 —— EffectsHandlerImpl,这是一个处理特效的工具类,接受合成器和 scene 作为参数,实现了 EffectsHandler 类:
- 维护一个特效表,用于跟踪特效的状态。EffectsHandlerImpl 在收到 effectLoader 的 effectLoaded 信号时,将 EffectPair <name, effect> 对加入到 loaded_effects。
2.对外提供一个 dbus 接口 /Effect,可以命令行查看特效状态,loadEffect 等,比如 showfps
3.监听工作区、会话管理器等等各种信号并处理
eg:信号Workspace::internalClientAdded, 为这个新增的 client 去 setupClientConnections,转发各种信号damaged ,windowShown, windowHidden 等等
特效实现的时候关联这些信号,比如 windowGeometry 这个特效,它就会监听 windowStepUserMovedResized,更新坐标值数据,更新 EffectFrame 的位置,重新渲染。
链式调用也是在这里实现的, EffectsHandlerImpl 继承了 EffectsHandler 的 paintScreen 等各个渲染函数,遍历 m_activeEffects,挨个调用每个 effect 的 paintScreen,从而实现链式调用。
特效开发
每个特效都是 Effect 的子类,在 kwineffect.h 中定义,实现各个虚函数。窗管的一切这儿都有,所以特效可以在窗管执行一切操作时做出响应。比如说移动窗口时候加坐标的特效,我们就需要实现 windowUserMovedResized 的行为,每当用户移动或者 resize 某个窗口的时候,这个函数都会被调用。
特特特特特特别重要的是各种 paint 函数,假设我们resize 窗口时的特效,就需要在 resizing 的时候 paint the changed geometry。因此我们需要实现 custom painting。
paint pass 的三个阶段
一个 paint pass 包含三个阶段,形成一个 paint 通路。详细信息见 scene.cpp 的注释。
pre paint 阶段
prePaintScreen 和 prePaintWindow
收集渲染信息的阶段,知道这一次 paint pass 要怎么做。如果屏幕上只有一小部分需要更新,不需要特效不需要矩阵变换,我们就能使用优化后的 paint function。至于 paint 具体是怎么做的,这就要看 mask 参数了,scene.h 文件中定义PAINT_WINDOW_* 和 PAINT_SCREEN_* 等等。
参数为 ScreenPrePaintData 、WindowPrePaintData 类型的数据,如果屏幕或者窗口需要变形,比如我们拖动窗口时只想拖边线,不拖动内容,需要变形的是窗口,给它加上掩码:
data.mask |= PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS;
加上这个标志,表明effect激活的时候 paint screen 需要对窗口变形。同时在 prePaintWindow 中给 window 数据也加上掩码:
data.mask |= PAINT_WINDOW_TRANSFORMED;
再比如 zoom 特效,它不需要针对窗口有任何操作,因此它只需要在prePaintScreen 给 ScreenPrePaintData 加上掩码:
data.mask |= PAINT_SCREEN_TRANSFORMED;
相比较向GPU请求完整的重绘来说,对于屏幕上需要手动重绘的部分的追踪,更加较耗CPU。所以可能有些时候还不如请求整个重绘。
effects->prePaintScreen、effects->prePaintWindow 会链式调用到下一个特效,effects 前面提到过,是一个全局变量。
paint 阶段
这个阶段会基于上一个阶段收集到的信息去做真正的 painting。通过链式的执行 effects 的 paintScreen【paintGenericScreen 或者优化的paintSimpleScreen】,那些对窗口执行的 paintWindow,也是链式的,直到 finalPaintWindow,这个 final 调用 performPaint 去做的就是真正的 paint 了。
- paintWindow 第一步先链式调用 effects->paintWindow(w, mask, region, data); 准没错儿。
- 然后检查是否在我们自己的模式下,比如 resize 相关的特效,检查是否是 resizing mode,当前正在paint 的窗口是否是我们要重绘的。这个判断依据是:参数 w 表示正在paint的窗口,我们打算重绘的窗口,在 slotWindowStartUserMovedResized 中被设置。
- 计算我们需要绘制的 region,比如移动窗口时:先求 orig矩形和新窗口坐标之间的交集,然后新窗口减去这个交集,也就是 outer;然后得到 orig 矩形和这个 outer 的并集,就是我们需要绘制的区域。
- 如果窗口仅仅需要变形,基本上不需要做啥 OpenGL的操作。translate, scale and rotate 窗口等等都有现成的 API 来做。对单个四边形的变形也很简单,无需了解 OpenGL。
根据打印的信息来看,特效在 paintWindow 中拿到当前屏幕上的所有窗口,所有的 EffectWindow,包括输入法窗口。
假设我们计算得到了 paintRegion, 往 vbo 中填充所有 rect 数据,六个点坐标确定一个矩形,分别是右上、左上、左下确定了一个三角形,左下、右下、右上再确定一个三角形。填充数据后再请求 render 就好了。可以参考 effects 目录下各个特效的实现来看。
当链式调用 paintWindow 走到最后一个特效被执行了,就开始真正的 m_scene->finalPaintWindow。根据窗口栈序,自底向上执行每个窗口的 prePaintWindow 和 paintWindow。
post paint阶段
这个阶段,还会触发重绘。如果特效想要重绘某些区域,可以在 post paint 阶段主动去 damage 这块区域,这样下一个 paint pass 就会重绘这些区域了。 具体的重绘方式,得看自己实现的特效需要怎么样去安排。
这三个阶段对整个屏幕执行一次,对每个窗口也会执行一次。所有特效都是链式连接,每个特效中的stage都会接着调用下一个特效的相应stage。
如何开发
参考链接:
https://blog.martin-graesslin.com/blog/2009/07/how-to-write-a-kwin-effect/
右上角的悬浮窗口可以借助 EffectFrame 来实现,它在 scene.cpp 中定义一个空壳,是一个 overlay 的窗口,可以为特效做渲染文字和小图标,悬浮在所有窗口之上。最重要的接口函数 render()
使用场景:启用 windowGeometry 特效插件后,拖动窗口在左上角、右下角和窗口中间会显示坐标值,这个数值就是 EffectFrame 显示的
myMeasure[i] = effects->effectFrame(EffectFrameUnstyled, false);
所以我要实现的这个特效,在创建的时候也需要这么一个对象:
m_mousePosition(effects->effectFrame(EffectFrameUnstyled, false))
接着设置简单的样式:
QFont fnt;
fnt.setBold(true);
fnt.setPointSize(16);
m_mousePosition->setAlignment(Qt::AlignTop | Qt::AlignRight);
m_mousePosition->setFont (fnt);
监听鼠标移动的信号,来触发重绘操作:
connect (effects, &EffectsHandler::mouseChanged, this, &ShowMousePosEffect::slotMouseChanged);
void ShowMousePosEffect::slotMouseChanged (const QPoint& pos, const QPoint& old,
Qt::MouseButtons, Qt::MouseButtons,
Qt::KeyboardModifiers, Qt::KeyboardModifiers)
{
// 整个重刷,防止出现画面异常
if (pos != old)
effects->addRepaintFull();
}
更新坐标的主要工作在 paintScreen 中:
void ShowMousePosEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data)
{
effects->paintScreen(mask, region, data);
QPoint p = QCursor::pos();
m_mousePosition->setText(QStringLiteral("当前坐标是 (%1, %2)")
.arg(p.x())
.arg(p.y()));
m_mousePosition->render(infiniteRegion(), 1.0, alpha);
}
我不懂 openGL,接下来还需要学习简单的 openGL,特别是着色器的用法。想办法弄明白kwin 里面的 ShaderManager 在做什么?希望不久以后能知道一点点。