Saturday, November 9, 2024
名词概念
Qt
Qt(/ˈkjuːt/,发音同“cute”)是一个跨平台的C++应用程序开发框架。广泛用于开发GUI程序,这种情况下又被称为部件工具箱。也可用于开发非GUI程序,例如控制台工具和服务器。
wlroots
用于构建 Wayland 合成器的模块化工具集,简化了约 60,000 行代码的开发工作。
- 提供抽象底层显示和输入的后端,支持 KMS/DRM、libinput、Wayland、X11 等,可动态创建和销毁。
- 实现多种 Wayland 接口,支持协议扩展,促进合成器标准化。
- 提供通用合成器组件,如物理空间输出管理。
- 集成 Xwayland 抽象,简化 X11 窗口管理。
- 提供渲染器抽象,支持简单和自定义渲染需求。
seat
由分配给特定工作场景的所有硬件设备组成。它至少包含一个图形设备,通常还有键盘和鼠标。此外,它可能包括摄像头、声卡等设备。座位由座位名称标识,这是一个短字符串(不超过64个字符),以”seat”四个字符开头,后跟至少一个a-zA-Z0-9范围内的字符,或”_”和”-“。这种命名方式适合用于文件名。座位名称可能是稳定的,也可能不稳定,如果座位再次可用,其名称可以重复使用。
RHI
RHI 是 Renderer Hardware Interface(渲染硬件接口)的缩写,是一套对硬件的抽象,在上层只需要设置参数,底层具体使用的是 OpenGL、Vulkan、DX12 还是 Metal 哪套接口,我们是不必关心的。
Qt6 提供了 QRHI,为 Qt 程序提供了底层的硬件抽象,这样上层的 QtQuick 组件在执行 GPU 渲染时,就可以自动调用对应的驱动接口。
QPA
Qt 平台抽象(QPA)是 Qt 中的核心平台抽象层。
QPA 的 API 可通过类前缀”QPlatform*”识别,用于实现 Qt GUI 中的高级类。例如,QPlatformWindow 用于窗口系统集成,而 QPlatformTheme 和 QStyleHint 则用于深层次的平台主题和集成。
基本工作流程
Treeland 使用 QQuickWindow 作为渲染的根,这样在 Treeland 里开发时,就如同开发一个普通 Qt 程序一样,先创建一个 Window,在 Window 内创建 Qt 控件,使用 QEvent 处理各种事件。
那么 Treeland 是如何实现这件事的呢?
QQuickWindow 的私有类提供了自定义 QQuickGraphicsDevice 对象的接口,而 QQuickGraphicsDevice 可以使用 fromOpenGLContext 和 fromPhyicalDevice 创建新的对象,那么 Treeland 只需要继承 QQuickWindow,并从 wlroots 获取 OpenGL context 和 phyical device,就可以将 Qt QuickWindow 的渲染,嫁接到 wlroots 上。
通过将 wlroots 的渲染上下文与 Qt 的渲染上下文进行结合,可以将 wlroots 渲染的图形结果嵌入到 Qt 应用程序的渲染流程中,可以直接使用 wlroots 提供的图形资源和设备对象,如物理设备(phdev)、逻辑设备(dev)和队列族(queue_family),以减少不必要的上下文切换和资源拷贝。这样,Qt 就可以利用 wlroots 提供的渲染能力,同时能够继续使用 Qt 的渲染框架和 API。
之后在 Qt QPA 中将屏幕信息,以及输入信息转换成 Qt 内部对象,从而利用 Qt 自身的事件循环等机制继续处理。
Qt QPA
QPA 为 Qt 提供了跨平台的接口抽象能力,我们可以提供自己的 QPA 插件来为 Qt 程序提供新的能力,例如将 wlroots 的输入事件转换成 Qt 内部事件。
输入事件处理

Treeland 处理底层事件与上层事件的流程
bool WOutputRenderWindow::event(QEvent *event) |
在 WOutputRenderWindow 的事件处理中,会额外调用下 seat 的事件过滤器,确保合成器可以拦截掉一部分事件,例如将一部分按键拦截下来,不发送给客户端。
bool QWlrootsRenderWindow::beforeDisposeEventFilter(QEvent *event) |
这段代码展示了转换输入设备的功能,判断输入设备的类型,创建对应的 QInputDevice 对象。
QPointer<QInputDevice> QWlrootsIntegration::addInputDevice(WInputDevice *device, const QString &seatName) |
客户端事件
在 Treeland 还有一种事件需要处理,当用户点击一个窗口,合成器需要告知客户端哪个坐标点击了。或者使用键盘进行输入时,需要告知客户端输入的内容。
首先,Treeland 会标记一个窗口成为激活窗口,设置给 seat,这样 wlroots 就知道哪个窗口此时拥有焦点。
之后当键盘发生输入事件时,Treeland 没有过滤掉按键事件,或者是放行某些按键,这些剩余的输入事件就会在 wseat 的 sendEvent 中,发送给激活的客户端。
// for keyboard event |
屏幕信息
在 QPA 中还对 WOutput 进行了封装 QWlrootsScreen。
QWlrootsScreen *QWlrootsIntegration::addScreen(WOutput *output) |
QWlrootsScreen 继承自 QPlatformScreen,做的事情是将部分参数进行转换,例如physicalSize、devicePixelRatio、DPI等,之后通过 QWindowSystemInterface::handleScreenAdded 将创建好的 QWlrootsScreen 添加进 Qt 内。
Qt RHI
摘抄一段来自 waylib 中初始化 Qt RHI 的代码
bool WOutputRenderWindowPrivate::initRCWithRhi() |
先获取 QSGRhiSupport 及相关控制对象。
判断 RHI backend 的类型,需要适配 vulkan、gles等。
从 wlroots 获取物理设备等参数,使用 QQuickGraphicsDevice::fromDeviceObjects 创建 Qt 的 QQuickGraphicsDevice。
render window的私有类是继承自 QQuickWindowPrivate,只需要将获取到的 QQuickGraphicsDevice 设置给 QQuickWindowPrivate::setGraphicsDevice 即可。
之后创建一个离屏渲染表面,用于 RHI 的初始化。
Qt Viewport
在 Qt 中,想要查看或者渲染一个组件,需要使用 Viewport 组件,俗称照相机。
视口(Viewport)是一个可观察的多边形区域,只有 Viewport 范围内的画面才能显示到屏幕上。
wlroots 中的 Viewport 是一个与 Wayland 显示协议相关的概念,主要用于定义渲染输出在屏幕上的显示区域。它允许在渲染时对显示内容进行缩放、裁剪或平移,以适应不同的分辨率和显示需求。
Treeland 使用 WOutputViewport 提供 Viewport 功能,使用 wlroots 的 wlr_output 中的屏幕信息,对画面进行矩阵变换,这里会涉及到屏幕的缩放、DPI等参数。
QMatrix4x4 WOutputViewport::renderMatrix() const |
WOutputViewport 提供了 Viewport 所需的所有参数,变换矩阵、源几何大小、目标几何大小等信息。
在 WOutputRenderWindow 的事件中,判断如果是渲染的事件,就执行渲染。
bool WOutputRenderWindow::event(QEvent *event) |
在 doRender 中,遍历所有的 Output,执行 beginRender,然后执行 Output 的渲染。
void WOutputRenderWindowPrivate::doRender(const QList<OutputHelper *> &outputs, |
qw_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePixelRatio, |
QVector<std::pair<OutputHelper*, WBufferRenderer*>> |
void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, |
处理完画面以后,如果需要上屏画面,就调用 commit 把画面送到屏幕上。
bool OutputHelper::commit(WBufferRenderer *buffer) |
还会判断是否有硬件加速(GPU),会优先使用硬件来加速计算过程。
} else { |
Surface 渲染
在 Treeland 中,为 Surface 创建了 WSurfaceItem,用于表示一个窗口,并创建了 WSurfaceContent 作为 WSurfaceItem 的 delegate。
void WSurfaceItemPrivate::initForDelegate() |
之后当 WSurfaceItem 需要更新画面时,就能调用 updatePaintNode 更新渲染。
QSGNode *WSurfaceItemContent::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) |
而使用 delegate 的目的是为了能让多个 WSurfaceItem 使用相同的窗口画面,例如某些场景需要临时创建一个窗口的分身,窗口切换列表、多任务视图等。
QSGTextureProvider *WSurfaceItemContent::textureProvider() const |
Treeland 使用 WQuickTextureProxy 创建窗口的代理显示,而其中就是获取 WSurfaceItem 的 textureProvider。
QSGTextureProvider *WQuickTextureProxy::textureProvider() const |
这样多个 proxy 就可以显示同一个窗口的内容,比 QML 的 ShaderEffectSource 效率更高。
结尾
上述仅仅是 Treeland 实现 Qt 和 wlroots 缝合的一部分流程,实际上对事件的处理就十分复杂,不止键盘输入,还需要处理光标、触控、触摸等其他设备。还有光标的绘制也需要区分硬光标和软光标,渲染画面时的硬件加速及软件实现等。
后续准备写一下光标相关的处理,以及还没介绍 Treeland 的画面是怎么绘制的。


















