2021-08-21窗口管理器杂谈

窗口管理器有好几种,stacking 、tiling 和 compositing

其中 瓦片式(tiling window manager )用的比较少,代表性的就是 awesome。不详细讨论,简单描述一下:所有窗口打开后不会有重叠,全部平铺放在屏幕上。整个屏幕没有其他空闲,被窗口全部填充,根据一些布局算法来排列窗口,一般分为 master 和 non-master 窗口。根据算法不同,master 的个数也不同,比如 n-master 算法中,master 就是列表中最近 focus 的那 n 个窗口,给它们分配的位置最大最显眼。下图就是一个 2-master 模式的 tiling 窗管布局示意图。

2021-08-14_11-34.png

本文主要介绍 marco 和 kwin,这两种窗口管理器和合成器在同一个程序里实现的。stacking + compositing,也被成为 dynamic window manager。

没有合成器的时候,窗口管理器收到客户端窗口的请求,只是为它创建一个 frame 窗口,reparent 过去。直接调用 XMapWindow,X server 就会直接把窗口显示出来。对于 XMapWindow,如果是客户端窗口调用的,窗口管理器会先收到客户端的 MapRequest,再收到 X 发过来的 MapNotify。窗口管理器在给客户端窗口加上 frame window 之后,会再调用一次 XMapWindow,这一次的调用就不会再收到 MapRequest了,因为 X 知道它是窗口管理器,不会再多此一举。

Ø 软件合成器

合成器的出现就是为了窗口的效果好看一点,上面那种方式,窗口直接就显示出来了,被覆盖的窗口或者最小化的窗口(没显示在屏幕上的所有窗口)内容都拿不到。因为只有一层屏幕上的图像,没办法做特效。比如说我们要做透明或者半透明的效果,也就是 alpha 混合,需要上层图像和它的下层图像两部分内容才能合成。所以软件合成器需要将所有窗口内容都重定向到离屏缓存。

Ø 利用硬件加速的3D合成器

其实也就是利用 OpenGL 库去渲染,待补充。

在 X window system 中,合成器都需要一个透明的事件穿透的 overlay 窗口,合成后的最终图像会拷贝到这个窗口上去显示。

Ø wayland 合成器

传统的 X11合成器的工作原理是将各个窗口的离屏缓存图片合成到一个overlay window 再渲染到显存中,不可避免的需要对各应用程序离屏缓存进行拷贝。不同于X11合成器,在 wayland 中,客户端和服务端共享显存 buffer,客户端可以通过 OpenGL 之类的渲染库或者 DRM机制等将窗口内容渲染到这块 buffer。当客户端自己渲染完成后,通知合成器发生变化的buffer,合成器拿到这块 buffer 的纹理数据去合成,不需要进行任何拷贝。由于 wayland 合成器避免了多余的窗口内容拷贝工作,让硬件去合成最终图像,从而渲染性能比 X11 要好一点。

从发展趋势来说,3D窗口管理器才是发展的方向,为什么经常听到反馈说 3D 卡顿, 2D 比 3D 要快?

原因一:这是因为多了一道工序,X 处理的对象是 pixmap,2D 的直接就是用这个 pixmap,而 3D 为了做出各种效果,它操作的对象是 texture。所以 3D 比 2D 多了一道”texture from pixmap” 的操作。XRender 不需要请求 TextureFromPixmap 这个操作,所以第一次显示窗口的时候比较快,打开应用很快。但是其他的各种视觉效果就没有了,比如 ease-in-out,渐进式动画,透明窗口,毛玻璃效果等。

原因二:3D窗口管理器在国产显卡上运行卡顿,很重要的原因是 kwin 默认启用了 openGL 后端的合成器,利用显卡的硬件能力去做大量矩阵运算实现窗口特效。尤其是毛玻璃效果,基于高斯模糊算法去实现,当硬件能力不支持时,很多特效无法施展只能关闭,有一些的简单的特效也只能靠 CPU 去做,CPU 处理许多矩阵运算,耗费时间从而导致运行卡顿。

合成式窗口管理器

窗口管理器跟合成器其实是两个概念,由于现在很多窗口管理器跟合成器开发在同一个程序中,所以一说窗口管理器就免不了合成器。但是也有单独的,比如为了分析代码方便,我下载了 xcompmgr,这就是一个独立的合成器,而不是窗口管理器。有些窗口管理器可以禁用掉合成器,比如 marco。mutter 不支持完全禁掉合成器功能,但是对于某些特别的窗口,它也提供了方法,让某些窗口可以不经过合成器合成,比较有代表性的就是在 Linux 上玩游戏,游戏窗口都是全屏的,为了提升游戏体验,可以在全屏窗口时关闭合成器功能。kwin 窗口管理器可以切换后端,从 openGL 切换到 xrender,从硬件加速合成器切换到软件合成器,或者也可以彻底关闭合成器。

合成器的工作前提是拿到所有窗口的内容,然后合成。比如常用的 XComposite 提供一种机制,通过它来请求 X 不要将某个窗口及其子孙的内容直接渲染到硬件上,而是渲染到 X 维护的一个特殊 buffer 中,这里的渲染是窗口整个渲染,不需要裁剪、重叠等计算。然后其他 X client 就可以使用这块 buffer 中的内容了。这也就是合成式窗口管理器所做的工作,它让 X 把所有顶层窗口都渲染到离屏的 in memory buffer,将它们合成到 overlay window。

OpenGL 为了实现同样的功能,将所有的离屏图像合成到一张图片显示到屏幕上,首先它需要把窗口的pixmap转为 texture 对象。一旦拿到了窗口对应的 texture 对象,就可以进行任何 gl 操作了。

将过程串起来,也就是说,3D窗口管理器将窗口重定向为一个 backing pixmap,通过 NameWindowPixmap 可以拿到这个 pixmap 对象,再将这个 pixmap 通过 TextureFromPixmap 拿到对应的 GL texture。

2D窗口管理器将窗口重定向为一个 backing pixmap,通过 NameWindowPixmap 可以拿到这个 pixmap 对象,通过 XRender 扩展合成。

marco 窗口管理器

以 Alt-Tab 应用程序切换为例,窗口管理器所做的工作有:

1、将所有顶层窗口和她们的子孙后代都渲染到一个离屏的 in memeory buffer,而不是直接渲染到显存。

2、将这些 buffer 变形,比如2D的常见变形只是等比例缩小,3D 的变形包括其他矩阵变化,用于动画切换显示视觉效果更佳。

3、将所有变形后的 buffer 合成为一个最终的 buffer,加上桌面背景、其他一些 UI 元素等一起做好准备。这个其他 UI 元素在不同的窗口管理器中表现形式也不一样。一般在 marco 中会创建一个比较小的 Gtk 窗口,占据屏幕中间一小部分,存放应用图标或者缩略图,不影响桌面上其他应用程序窗口图标的显示;大部分 3D 窗管为了视觉效果,都会新建一个跟屏幕相等大小窗口,覆盖整个屏幕,挡住所有的窗口们,让切换的应用更直观的展现给用户。

创建一个 overlay 窗口,大小等同于屏幕大小,这个 overlay 窗口处在所有普通窗口之上,锁屏窗口之下。将合成后的 final buffer 渲染到 overlay 窗口。

为了实现上面的功能:

1、我们需要拿到顶层窗口中显示的内容。

2、我们还得实时更新窗口内容的变化。

3、顶层窗口间还会遮盖,比如 A 盖住了 B 的一部分,但我们在应用切换的时候需要拿到全部内容。

需要 XComposite、XRender、XDamage、XFixes 等扩展来帮助我们实现上面的功能。

Marco 中的合成器

Marco 是软件合成器,代码相对来说比较简单,不像其他窗口有 opengl 后端,marco 只有 xrender 后端。

代码位置: src/compositor 目录下的 compositor.c 和 compositor-xrender.c

上面提到了 overlay window,这是一个顶层窗口,只用于输出,不接收任何输入事件。关于 overlay 窗口的创建:

xroot = meta_screen_get_xroot (screen);

output = XCompositeGetOverlayWindow (xdisplay, xroot);

XSelectInput (xdisplay, output, ExposureMask);

XCompositeGetOverlayWindow 返回 Composite Overlay Window 的 id. 这个 overlay window 在所有普通窗口之上,锁屏窗口之下,大小与屏幕相同。

overlay window作为一个特殊的窗口,QueryTree 查询窗口树的时候不会被列出来。同时它还具有 override redirect 属性,所以窗口管理器不会给它加 frame window。

XSelectInput() :因为 overlay 只用于输出的,所以这个函数请求 X server 只需要报告 Exposure 事件就可以。。

前面介绍过,客户端窗口的内容都被重定向到了离屏缓存,虽然重定向了窗口内容,但是没有重定向输入。因为如果重定向输入就太复杂了。 不重定向输入事件,而把它交给 X 处理。具体的实现方式就是通过 XFixes 扩展提供的 SetWindowShapeRegion API 将 OverlayWindow 的输入区域 ShapeInput 设为空区域,从而忽略对这个 OverlayWindow 的一切鼠标键盘事件。 这样一来对 OverlayWindow 的点击会透过 OverlayWindow 直接作用到底下的窗口上。下面这段代码在 marco 的 src/compositor/compositor-xrender.c 中:

 XserverRegion region;

 region = XFixesCreateRegion (xdisplay, NULL, 0);

 XFixesSetWindowShapeRegion (xdisplay, cow, ShapeBounding, 0, 0, 0);

 // 将输入区域设置为空区域,从而忽略这个 overlay 窗口的一切键盘鼠标事件

 XFixesSetWindowShapeRegion (xdisplay, cow, ShapeInput, 0, 0, region);

 XFixesDestroyRegion (xdisplay, region);

链接:https://www.x.org/releases/X11R7.7/doc/fixesproto/fixesproto.txt

第一步,窗口内容

渲染到离屏

XComposite 是个 X 扩展,有了它,我们就可以将所有顶层窗口都重定向到一块离屏的 buffer 中,

XCompositeRedirectSubwindows(dpy, RootWindow( dpy, i ), CompositeRedirectAutomatic)

对 root 窗口设置 Automatic 重定向,也就是在这之后创建的所有顶层窗口都会自动渲染到离屏 buffer(也叫做 backing pixmap)。既然窗口已经渲染到离屏 buffer 了,怎么拿到这块区域对它做处理呢?

拿到窗口属性

首先我们肯定需要有窗口 id,根据窗口 id,从 X 获取窗口的所有属性信息,比如窗口大小、坐标、窗口离屏 buffer 中每个像素点的格式、是否支持 alpha 通道等。

XWindowAttributes attr;

XGetWindowAttributes( dpy, wId, &attr );

XRenderPictFormat *format = XRenderFindVisualFormat( dpy, attr.visual );

bool hasAlpha = ( format->type == PictTypeDirect && format->direct.alphaMask );

int x = attr.x;

int y = attr.y;

int width = attr.width;

int height = attr.height;

有了这个 attr 之后,就可以想办法引用到窗口的内容了。

拿到窗口内容

到了这一步,需要为窗口创建对应的 XRender Picture。picture 是什么?就是给 drawable 对象再封装了一些额外的信息,比如 format,裁剪区域等。

已有 window 信息,接口 XRenderCreatePicture 用于创建一个 Xrender 的 picture。这是个异步接口,不会返回错误码,X server 处理到这个请求,如果失败就直接发 X error 给应用程序,这时候返回的 picture handle 无效,不可操作。

// 下面的代码用于创建一个 Render picture,有了它我们就能引用到 window contents.

// 将 subwindow 设为 IncludeInferiors, 要不然我们渲染窗口的时候,它子控件不会被渲染。

XRenderPictureAttributes pa;

pa.subwindow_mode = IncludeInferiors; // Don't clip child widgets

Picture picture = XRenderCreatePicture( dpy, wId, format, CPSubwindowMode, &pa );

第二步,窗口内容变化

Damage,也就是说窗口内容被破坏了,比如说一个窗口 A 原先被 B 盖住了一小半,拖动 B 就会暴露 A 窗口的其他内容,也就是 damage,这种情况下,肯定需要更新窗口的内容,所以窗口管理器需要这么一个 Damage 信号,也就是 XDamage 提供的能力。将窗口内容设置为“脏”标志,触发整个刷新。

int damage_event, damage_error;  // 这个 event base 很重要

XDamageQueryExtension( dpy, &damage_event, &damage_error );

如果有 XDamage 扩展,需要给每一个窗口(我们关心的)创建一个 damage handle。

new->damage = XDamageCreate (dpy, id, XDamageReportNonEmpty);

在事件循环中判断是否是 damage :

if (ev.type == damage_event + XDamageNotify)

X扩展的 base event

每个 X 事件都有唯一的数值与之对应,不过广大人民群众会主动开发 X 扩展,我们不知道会有多少 X 扩展,而每个 X 扩展都有可能添加自己的事件,所以这个扩展的事件的 base number 就很重要,这个 base number 的数值是多少由 X 决定,运行时从 X server 获取。

XEvent 的数据结构,事件类型(包含event number),实际使用时,做类型转换为对应的实际时间,比如 damage 事件、 xfixes 事件、 xshape 事件等。

bool x11EventFilter( XEvent *event )

{

    if ( event->type == damage_event + XDamageNotify ) {

        XDamageNotifyEvent *e = reinterpret_cast( event );

        // e->drawable is the window ID of the damaged window

        // e->geometry is the geometry of the damaged window

        // e->area is the bounding rect for the damaged area

        // e->damage is the damage handle returned by XDamageCreate()

        // Subtract all the damage, repairing the window.

        XDamageSubtract( dpy, e->damage, None, None );

    }

……

}

第三步,渲染窗口

Xrender 扩展用来渲染窗口内容

void XRenderComposite (Display *dpy,
**int op, // 操作码,重要**
Picture src,
Picture mask,
Picture dst,
int src_x,
int src_y,
int mask_x,
int mask_y,
int dst_x,
int dst_y,
unsigned int width,
unsigned int height);

渲染操作码 PictOpSrc 和 PictOpOver.

PictOpSrc 目标像素点会被 source 像素点替换,包括 alpha 值。(dst = src)

PictOpOver 则跟 Porter/Duff Over 操作符有关(参见 pdf),它告诉 Xrender 使用 source pixel 中的 aplpha 值跟 destination pixel 混合。(dst = src Over dst).

换句话说 PictOpSrc 不会 blend 窗口, PictOpOver 则会 blend 窗口。 PictOpSrc 更快,所以在窗口没有 alpha 通道的情况下,最好用 PictOpSrc,速度快!

合成器事件

窗后管理器最重要的核心代码在 src/core/display.c 中的 event_callback,这个函数中处理 X 各个事件,比如 MapNotify 等。当启用合成器时,每处理一个事件后,接着会再去执行 meta_compositor_process_event,找到对应合成器的 process_event。如果事件是 XDamageNotify,执行 process_damage,修复窗口内容。

kwin 窗口管理器

未命名文件.png

kwin 代码很多,刚分析了一点点,后面会继续补充。

我们从三大部分来分析 kwin 的框架。kwin core,kwin 窗口装饰和 kwin特效。什么是窗口装饰? 标题栏、边框、窗口阴影以及标题栏左右的按钮:最小化、最大化、关闭等,这些都属于窗口装饰。

目前开源kwin 版本到了 5.22,看了一个版本 fork 的是 5.18 的版本。代码主要在 src 目录下,kwin 支持 X11 和 wayland,使用 xcb 接口而不是 Xlib。xcb 接口的优势在于它的所有请求都是异步的,Xlib 有异步也有同步,总的来说 xcb性能比Xlib稍微好一点,但是文档比较少,有时候需要对着 xlib 文档看才行。

本文先简单介绍一下 kwin 的合成器,然后再主要分析窗口特效这一部分,以 scene.cpp 为出发点。

合成器

composite.*

这个代码将窗口内容重定向的离屏缓存并且跟踪 damage 信号, xcb_composite_redirect_subwindows.

在开启合成器的情况下,XComposite扩展用来将window重定向到 pixmaps,XDamage 用来发生变化的窗口内容。这些都是在这个 composite.cpp 中实现的。

何时开始 composite 的动作?在fork kwin 5.18的代码中,由定时器执行Compositor::performCompositing() 开启一个 paint 通路。通过 paint screen,从而 paint 每个窗口来实现一次全屏渲染。每个特效 (effects) 都可以影响到 Painting 的效果,而且是链式的影响。开源新版本中加入了 renderloop.* renderjounal.*,不再是简单的定一个时间由定时器来触发渲染,而是有一定的 scheduleRepaint方式。根据刷新率,得到 vblankInterval,看看是立即开始渲染还是 scheduling 下一次的渲染。

a、如果客户端程序的渲染请求比刷新率要慢,就立即显示 compositeTimer.start(0);

b、如果客户端的渲染请求比刷新率还快,就得按计划来渲染,计算得到一个 waitInternal 的时机,compositeTimer.start (waitInternal)

compositeTimer 是什么?就是一个 QTimer 计时器,收到超时信号就执行 timeout 回调函数 dispatch(),在这个回调中,发出 frameRequested 信号。

合成器代码 composite.cpp 监听这个信号,在对应的回调函数 handleFrameRequested中调用 composite(renderLoop)。

在 x11 合成器调用的是 m_scene->paint(screenId, repaints, windows, renderLoop);这个 paint 函数的具体实现在哪儿需要我们先看看 m_scene。先确定它对应的是哪个渲染后端?然后再去找它的 paint 实现函数。如果后端是opengl 那么实现位置: plugins/scenes/opengl/scene_opengl.cpp。也就是说 scene.cpp 中定义了 paint 的虚函数,SceneOpenGL::paint 去实现它。到这里旧版新版都一样,链式调用 paintScreen。

可以这么理解链式效果,比如说 paintScreen 会先执行第一个 effect 的 paintScreen,在第一个特效的paintScreen 中很有可能再调用下一个 effect 的 paintScreen,这样依次执行从而产生连锁反应,直到 Scene::finalPaintScreen 被调用才算完成。

合成时借用了 QRegion对象,它的 united、xored、subtracted 等可以很容易地进行区域的合并或者裁剪。kwin 旧版和新版的代码有点差异【5.18 vs 5.22】,新版代码在 Scene 对象中维护了一个 QVector 类型的 m_repaints 变量,存储 QRegion。数组大小应该是跟屏幕个数相关【没验证】,每个屏幕上窗口内容发生了变化,那块区域就会被 addRepaints 更新到这个数组中:

m_repaints[screenId] += dirtyRegion;

composite() 函数获取 repaints 区域,执行 m_scene->paint(screenId, repaints, windows, renderLoop);

scene.*

合成器的基类,定义了一些通用的接口。每一次 paint 都有三个阶段,从而形成一个 paint 通路(paint pass)。

1、pre paint: pre阶段定义这一次的 paint 要怎么做,比如应用或者不应用哪些特效 effects。如果屏幕上只有一小部分需要更新,不需要特效不需要矩阵变换,我们就能使用优化后的 paint function。至于 paint 具体是怎么做的,这就要看 mask 参数了,scene.h 文件中定义PAINT_WINDOW_* 和 PAINT_SCREEN_* 的各个掩码。

比如说有个窗口需要透明效果,在 prePaintWindow 中就需要加上 PAINT_WINDOW_TRANSLUCENT 掩码。

2、paint: 这个阶段会基于上一个阶段收集到的信息去做真正的 painting。通过链式的执行 effects 的 paintScreen【paintGenericScreen 或者优化的paintSimpleScreen】,那些对窗口执行的 paintWindow,也是链式的,直到 finalPaintWindow。这个 final 调用 performPaint 去做的就是真正的 paint 了。

3、post paint:这个阶段会做些 cleanup,还会schedule 下一次的 repaint。如果特效想要重绘某些区域,可以在 post paint 阶段主动去 damage 这块区域,这样下一个 paint pass 就会重绘这些区域了。

这个基类中没有定义 paint()的实现,在各个后端分别去做。

// Repaints the given screen areas, windows provides the stacking order.
// The entry point for the main part of the painting pass.
// returns the time since the last vblank signal - if there's one
// ie. "what of this frame is lost to painting"
virtual void paint(int screenId, const QRegion &damage, const QList &windows, RenderLoop *renderLoop) = 0;

由于“合成”这一概念的出现早于 wayland,所以一开始 kwin 的 scene 设计是非常简单的,就是栈序排列的窗口链表,因为在X11中,所谓合成器,只不过就是拿到所有窗口的 buffers 在合成到一张大图片而已。

但是,出现了 wayland,不断更新的新渲染技术与传统简单 scene 的设计模式之间出现了矛盾。比如 wayland 中的 wl_surface 是一个很普遍的概念,它可以是窗口的内容,可以是光标,甚至可以是 DND 的 icon 等等。但是旧有版本的 scene 是从 window 的角度来看待 screen 的组成,所以为了能 cover 所有的 wl_surface 的场景,可能窗管中需要编写很多定制的 code path。 比如说应用程序调用 OpenGL 或者 Vulkan 的接口渲染鼠标,旧的 kwin 无法显示这样的 cursors,因为它渲染鼠标的代码路径中没有处理client buffer 的硬件加速。除此之外,还有另一个问题,就是没办法很有效地跟踪每个 wl_surface 的 damage 信号,为了更新某个小小的 wl_surface 甚至有可能需要重绘整个屏幕。

上面说的两个问题的根本原因是scene 的设计模式中将 screen 上所有的内容都当成一个个窗口了,如果我们不再把窗口视为单独的对象,而是把窗口拆分开,当成若干个其他 item 组成的对象就可以解决这些问题了。比如 WindowItem 由这几部分组成:窗口内容是SurfaceItem,窗口装饰是 server-side DecorationItem,还有 drop 时的 ShadowItem等。在这种设计模式下,Scene 下面不再是 window window window 而是 WindowItem,SurfaceItem(DND icon),SurfaceItem(software cursor)等。下面是两种设计模式的结构图:

未命名文件(1).png
未命名文件(2).png

scene 的具体实现在 plugins/scene 目录,根据不同后端实现了 scene,有三个 xrender、opengl 和 qpainer :

scene_opengl.* - compositing backed using OpenGL;
scene_xrender.* - compositing backend using XRender.
scene_qpainter.* - compositing backend using QPainter

以 xrender 为例,介绍一下 repaints 区域的接口调用。composite() 函数从 Scene 对象中拿到了 damaged region,调用 m_scene 的 paint 来重绘这些区域。在 scene_xrender.cpp 中执行:

QRegion updateRegion, validRegion;

paintScreen(&mask, damage, QRegion(), &updateRegion, &validRegion, renderLoop);

m_backend->showOverlay();

最新的kwin开源代码中在 paintScreen 前后加上了 renderLoop-> beginFrame(); 和 renderLoop->endFrame(); 由于显示器的刷新一般是逐行进行的,因此为了防止交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,交换动作(swap buffer)一般会等待显示器刷新完成的信号,在显示器两次刷新的间隔中进行交换,这个信号就被称为垂直同步信号,这个技术被称为垂直同步。

新版 kwin 有个 RenderJoural 类评估渲染一帧需要多长时间以及离渲染下一帧大概还有多久?通过 beginFrame 和 endFrame 的 elapsedtime 来估算。也就是在 SceneOpenGL::paint 函数调用 renderLoop 的beginFrame 和 endFrame 打时间点来评估。

窗口特效

kwin 的窗口特效极其丰富,如下图四种:关闭窗口、拖动窗口、最小化窗口、切换工作区等。

cap_Peek 2021-08-21 13-24_00:00:05_01.jpg

图1. 关闭窗口的碎片化效果

cap_Peek 2021-08-21 13-24_00:00:09_02.jpg

图2. 拖动窗口的坐标标示与晃动效果

cap_Peek 2021-08-21 13-29_00:00:20_01.jpg

图3. 切换工作区的立方体效果

cap_Peek 2021-08-21 13-34_00:01:03_01.jpg

图4. 窗口最小化的神灯效果

effectloader.*

从名字来理解的话,这个应该是加载我们开发的 effects 插件的类。需要加载的特效插件分为三种:built-in 的,scripts 的,二进制插件的。

为了简便,每一种插件都单独对应了一个加载器。在此之上提炼了一个 AbstractEffectLoader 抽象加载器,定义了一些基本接口。极其重要的接口是 loadEffect,这是一个同步加载的接口

loadEffect:首先会检查该特效是否已经加载过了,为了实现这个check,加载器需要能跟踪所有已加载或者已销毁的特效。当加载成功,发出 effectLoaded 信号。

readConfig 接口,每个特效都需要Enabled这样的 key:

const QString key = effectName + QStringLiteral("Enabled");

也就是配置文件 配置文件 /home/xunli/.config/kwinrc 和 /etc/xdg/kwinrc 中的定义。

对于内建特效插件,可以阅读源码文件 effect_builtins.*,EffectData 结构定义了插件的 name、displayname、createFunction 等等。

struct EffectData {
    QString name;
    QString displayName;
    QString comment;
    QString category;
    QString exclusiveCategory;
    QUrl video;
    bool enabled;
    bool internal;
    std::function createFunction;
    std::function supportedFunction;
    std::function enabledFunction;
    QString configModule;
};

哪些特效属于 builtin 在头文件 effect_builtin.h 中定义。

插件加载器在加载内建插件后发出 effectLoaded 信号,BuiltInEffectLoader:: loadEffect :

Effect *e = BuiltInEffects::create(effect);

m_loadedEffects.insert(effect, e);

emit effectLoaded(e, name);

create函数中去执行 createHelper 也就是新建特效。

libkwineffects 目录

./libkwineffects/kwineffects.cpp 定义 EffectsHandler。如果合成器为 none,compositing_type == NoCompositing 那么 EffectsHandler 为空;如果不为空,所有特效都会 connect 这个EffectsHandler对象。libkwineffects 库,是 kwin core 和 effects 的桥梁。所有 effects 目录下的特效渲染的实现,调用的是 libkwineffects 目录下的接口。

1、定义 Effect,这是所有 kwin effects 的基类,除了可以自定义如何 painting,还要对状态变更做出响应,比如窗口关闭了,所以还需要关联 EffectsHandler 的信号槽函数.

这个类中的大部分接口都是链式调用的,比如特效 A 和 B 都处于 active 状态,那么首先调用 A::paintWindow 接着在这个函数中又会去调用 B::paintWindow。为了达到这样的链式效果,需要借助于 EffectsHandler :effects->postPaintScreen();

简单介绍一下这个基类提供的重要接口

a、prePaintScreen、paintScreen、postPaintScreen

pre 阶段: 设置windows或整个屏幕是否需要变形、改变需要被重绘的区域、housekeeping 之类的任务,比如为下一步painting pass 或者更新动画做准备,初始化 effects 的变量
paint 阶段:在所有窗口之上再画一点东西【需要在 effects->paintScreen() 之后画】、画多个桌面或者同一个桌面的多个拷贝【多次调用 effects->paintScreen()】
post 阶段:所有paint动作完成后,schedule 下一次重绘【比如动画效果】

b、prePaintWindow、paintWindow、postPaintWindow

pre阶段:在实际的 paint pass 之前对每个窗口去做的事情,包括:启动或者禁用窗口的绘制、设置窗口的透明绘制、设置窗口的变形、请求将窗口切分为多个 parts
paint 阶段:绘制窗口的主函数,包括:窗口变形、改变窗口的透明度、改变亮度或者颜色饱和度
post 阶段:假如窗口有动画效果的话,也是在这里 schedule 对各个窗口的下一次重绘

c、paintEffectFrame

这个函数在paint EffectFrame 之前调用的,比如想给 frame 加个 shader。在这里补充介绍一下 EffectFrame。这个类在 scene.cpp 中定义一个空壳。它是一个 overlay 的窗口,可以为特效做渲染文字和小图标,悬浮在所有窗口之上,比如图示2,拖动窗口时那三个坐标就是利用这个对象画出来的。最重要的接口函数是 render()

启用 windowGeometry 特效插件后,拖动窗口时就会在被拖动窗口的左上角、右下角和窗口中间会显示坐标值,这个数值就是 EffectFrame 控件显示的。

myMeasure[i] = effects->effectFrame(EffectFrameUnstyled, false);

跟 scene 中的其他部分一样,这个类的具体实现也在各个后端去做,plugins/scenes/opengl/scene_opengl.cpp,来看一看这里的 render 是怎么写的?都是 gl 操作【看的头晕,不看了】。需要学习 opengl 才能更好地理解。

text icon 等都需要从 QPixmap 转为纹理数据,kwin 中的 GLTexture 在 ./libkwineffects/kwingltexture.cpp 定义 最最后发现渲染还是 opengl 的绘画接口,比如 GLDrawArray:

GLVertexBuffer::render -> GLVertexBuffer::draw -> glDrawArrays 等。

effects.*

这个文件中定义了各个特效的基类, 其中render 函数调用:

effects->paintEffectFrame -> m_sceneFrame->render(region, opacity, frameOpacity);

对应到不同的 scene 后端,执行相应的 render()函数

startPaint 函数中表明 m_activeEffects 来源于 loaded_effects,EffectsHandlerImpl 在收到 effectLoader 的 effectLoaded 信号时,将 EffectPair :name effect 对加入到 loaded_effects。实际的特效也就是 iterator->second。

如何启用特效?配置文件 /home/xunli/.config/kwinrc 和 /etc/xdg/kwinrc

[Plugins]

blurEnabled=true

cubeEnabled=true   // Ctrl + F11 切换工作区的 cube 特效

kwin4_effect_translucencyEnabled=true // 拖动窗口时候的透明特效

wobblywindowsEnabled=true // 晃动窗口时候的不稳定效果

介绍一下 EffectsHandlerImpl,这是一个处理特效的工具类,接受合成器和 scene 作为参数,实现了 EffectsHandler 类:

1、维护一个特效表,用于跟踪特效的状态。

2、对外提供一个 dbus 接口 /Effect,可以命令行查看特效状态,loadEffect 等,比如 showfps

3、监听工作区、会话管理器等等各种信号并处理

eg:信号Workspace::internalClientAdded, 为这个新增的 client 去 setupClientConnections,转发各种信号damaged ,windowShown, windowHidden 等等。在具体去实现某个特效的时候,connect该特效关心的各种信号,比如 windowGeometry 这个特效,它就会监听 windowStepUserMovedResized,更新坐标值数据,更新 EffectFrame 的位置,重新渲染。

链式调用也是在这里实现的 EffectsHandlerImpl 继承了 EffectsHandler:

paintScreen 等函数,遍历 m_activeEffects,挨个调用每个 effect 的 paintScreen,从而实现链式调用。

你可能感兴趣的:(2021-08-21窗口管理器杂谈)