• 首页
  • 加入
  • RSS
  • Wednesday, March 5, 2025

    由于 deepin-ports SIG 维护的 deepin (23/25) RISC-V 版本的版本发布周期和软件包维护状况与主线 deepin 有所不同,本文将为用户和开发者提供一些常见问题的解答。

    1. deepin RISC-V 版本的发布周期是多久?

    与主线发布的 iso 镜像不同,RISC-V 版本需要针对不同的硬件平台进行适配,其中不仅涉及到 deepin 主线软件仓库的更新,来自硬件厂商的驱动和内核也会有各自的更新周期,且适配过程中可能存在一些 bug,这也需要通过发布新版本的方式来修复。

    因此,我们有如下发布计划:

    • 对于主线 deepin 的 大版本发布(不同仓库) ,我们会在不晚于 1 个月的时间内发布对应的 RISC-V 镜像。
    • 对于主线 deepin 的 小版本发布或日常更新(同一仓库) ,我们不保证会同步发布对应的 RISC-V 镜像,但用户可以通过更新软件包的方式获得最新的软件。
    • 对于硬件厂商的 SDK 和驱动更新,我们会在收到更新后尽快发布对应的 RISC-V 镜像。
    • 对于 已知的 bug,我们不保证会在短时间内发布新镜像,但是我们每次发布新镜像时都会尽量修复所有已知的 bug。
    • 对于 内核的大更新(改变版本号),我们会在收到更新后尽快构建并推送至仓库。
    • 对于 内核的小更新(不改变版本号),我们会在有大量更新或者有重大 bug 修复时构建并推送至仓库。

    2. deepin RISC-V 版本的软件包是如何维护的?

    deepin-ports 发布的镜像通常包含以下全部或部分仓库的组合:

    • deepin 主线仓库 (23 或 25),由 deepin CI 统一构建
    • deepin-ports 仓库, 包含了一些 deepin 主线仓库中未包含的软件包
      • ports-apps 仓库, 包含一些针对 RISC-V 平台适配的应用软件包
      • ports-kernel 仓库, 包含了所有版本的非主线的 RISC-V 内核
      • ports-profiles 仓库,包含了针对 RISC-V 各种设备适配的配置文件和包组
      • ports-imggpu 仓库,包含了各版本的 Imagination GPU 驱动,通常由固件和特定版本的 glvnd 化 mesa 组成
      • ports-board-* 仓库,包含了特定硬件平台的驱动,这些驱动通常由硬件厂商提供,尚未上游化

    例如,JH7110 镜像(适用于 VisionFive 2、MilkV Mars 等设备)的 /etc/apt/sources.list 将包括以下内容:

    # deepin 25 stable + testing
    deb https://ci.deepin.com/repo/deepin/deepin-community/stable/ crimson main community commercial
    deb https://ci.deepin.com/repo/deepin/deepin-community/testing/ unstable/25 main community commercial
    # deepin-ports apps + kernel + imggpu + profiles + boards-jh7110
    deb [ trusted=yes ] https://ci.deepin.com/repo/deepin/deepin-ports/repo/ crimson ports-apps ports-kernel ports-imggpu ports-profiles ports-board-jh7110
    

    以上仓库均在 Open Build Service (OBS) 上构建,构建结果会被发布到对应仓库中。自 2024 年以来,我们已经通过此方式适配了十余款设备,该维护方式经验证有效。

    3. deepin RISC-V 版本的内核是如何维护的?

    对于一些厂商提供的内核,deepin-ports 提供了一套工作流实现自动化构建,产物将遵循 deepin-ports 的 RISC-V 发布周期推送至仓库,该工作流同样构建 u-boot。具体包含以下流程:

    • git clone 远程分支,安装内核构建依赖
    • 应用特定一组 deepin 下游 patch
    • 执行 defconfig,应用特定一组 deepin 下游 config
    • 构建内核并打包 deb

    该工作流在 GitHub Action 上使用交叉编译,在 deepin OBS (Open Build Service) 上使用 riscv64 构建机编译。(注意:仅后者打出的 linux-headers 可用于 DKMS)

    Thursday, January 16, 2025

    编辑的话请把自己的名字加到作者名单里

    即将发布(你阅读到这个文章的时候可能已经发布了)的 deepin 25 preview 将会包含对应的新版 DDE。为了方便各个其它发行版的包维护者可以更方便的移植 DDE 到对应的发行版,这里提供一篇简要的移植指南,用以描述常见的移植问题和解决方案。

    下面对项目名称的称呼均以 GitHub 对应的原始仓库名为准。 {.note}

    概览

    相对于 deepin 23,在 deepin 25 中,包括桌面、通知中心在内的大部分旧的 DDE 桌面组件已转化为 dde-shell 插件形式,以供更好的跨显示环境兼容性。包括控制中心在内的组件也已开始提供全新设计以及基于 QML 的全新界面。同时,我们也对 Qt、DTK 进行了更多完善,以供 DDE 组件以及 Treeland 能够更好的运行。

    由于这些项目的版本间互相影响,我们建议移植人员参照 deepin 25 preview 所使用的包版本进行打包,下面会对主要的部分进行详细说明。

    需要注意的是,由于此文章编写时间早于版本发布时间,故最终版本镜像中使用的版本可能高于下面列出的版本。我们尽可能确保此文章的准确性,但若您需要获取 ISO 镜像中使用的确切软件版本列表,请挂载 ISO 后参阅 LIVE/FILESYS{0,1}.MAN/live/filesystem.manifest 路径对应的文件的内容。

    主要组件

    DTK 与 DTK6

    DTK 是 DDE 组件与应用的基础依赖,适用于 RC 的版本参照如下:

    packageversion
    dtkcommon5.7.7
    dtklog0.0.2
    dtkcore5.7.7
    dtkgui5.7.7
    dtkwidget5.7.7
    dtkdeclarative5.7.7
    qt5integration5.7.7
    qt5platform-plugins5.7.7
    dtk6core6.0.27
    dtk6gui6.0.27
    dtk6widget6.0.27
    dtk6declarative6.0.27
    qt6integration6.0.27
    qt6platform-plugins6.0.27

    除 dtklog 外,本次 DTK 版本号以及相对应的平台插件等版本号均已对齐,可直接参照打包。

    关于 qt5platform-plugins,现有的 dwayland 插件可能对非 DDE 环境(例如 KDE)的 wayland 用户存在影响,可参照 linuxdeepin/developer-center#7217 打对应的 patch 规避影响。

    DDE 主要组件

    下面仅涉及变化较大或影响较广的组件。其余未涉及的组件可正常参照最新 tag 进行打包与移植。

    由于 deepin 25 preview 仍在持续开发过程中,故较多组件采取了 x.99.z 的版本号策略。此外,一般情况下,此类 tag 并不会实际以 git tag 的形式存在,而只会体现在 debian/changlog 文件中。下面涉及到的此类版本号将会在版本发布前后补充对应的 git tag。

    下面涉及到的组件的版本参照如下:

    packageversion
    dde-session1.99.7
    dde-application-manager1.2.23
    dde-shell1.99.19
    dde-launchpad1.99.5
    dde-tray-loader1.99.12
    dde-application-wizard0.1.11
    dde-clipboard6.1.4
    dde-launcher被 dde-launchpad 取代,不再使用
    dde-dock被 dde-shell 取代,不再使用

    dde-application-manager

    由于涉及到诸多关于应用识别的改善,故建议总是使用最新版本。

    dde-shell

    dde-shell 旨在将 DDE 桌面环境插件化与模块化,降低开发难度,使各个组件的替换变得更加容易,并且提供更好的桌面环境集成支持。preview 阶段,dde-shell 已经可以满足原计划的部分目标。现 DDE 环境下,dde-shell 已取代 dde-dock 来负责管理整个 dock 区域、 dde-launchpad 提供了对应的 dde-shell 插件用以展示启动器相关的界面、原 dde-session-ui 中的通知中心部分也转到了 dde-shell 中,且转用了新的界面设计。

    关于 shell 的服务启动方面,为了方便故障排查,dde-shell 从原本的单进程转为了两个进程(分别提供桌面和任务栏两个部分)。另外,shell项目的任务栏部分在此阶段也配合 dde-application-manager 对应用识别的准确度进行了诸多完善。若仍有发现应用错误识别和错误分组的问题,欢迎及时反馈。

    为保障dde-shell在Qt6.8之后的环境可以正常运行(即使是X11环境下),必须给qtwayland打下面的patch:

    https://codereview.qt-project.org/c/qt/qtwayland/+/603556

    dde-launchpad

    dde-launchpad 现仅支持以 dde-shell 插件的形式被最终用户使用。因而,打包 dde-launchpad 现需要先打包 dde-shell,并确保用户最终使用的是 dde-shell。

    dde-session

    需要注意的是,我们已在 deepin 23 beta3 起放弃了对 deepin-kwin wayland 的支持,DDE 后续所有 wayland 相关的支持均由 treeland 提供。请参见后续的 Treeland 段落。

    下面涉及到的组件的版本参照如下。对于位于非 linuxdeepin 组织的软件包,此处一并给出了组织名:

    packageversion
    vioken/waylib0.6.10
    vioken/qwlroots0.5.2
    treeland0.5.17
    ddm0.1.9

    Treeland 环境

    Treeland 环境相较于 deepin 23 阶段有了较多的提升,不过由于 Treeland 迭代开发过程中我们对 Qt 以及 wlroots 进行了诸多完善,故 Treeland 对 Qt 以及 wlroots 等组件有较高的版本要求,以及可能需要应用一些额外的 patch。

    DDM

    尽管 DDM 目前是基本功能可用状态,DDM 目前仍相对而言不够稳定。对于打包移植而言,建议采用其他DM来启动用户级的treeland。

    对于其它 DM,只需要打包时安装 usr/share/wayland-sessions/treeland-user.desktop 即可。

    Qt 补丁

    下述假定您的发行版使用的 Qt 版本为 Qt 6.8.1。

    为保障 dde-shell 在 Treeland 上可以正常运行,需要打下面的 patch,否则可能会出现 dde-shell 崩溃的情况。

    https://codereview.qt-project.org/c/qt/qtbase/+/607654

    如果你在 Treeland 下遇到小 launchpad 无法输入中文的问题,可以打下面的 patch,但是该 patch 目前尚未进行完整测试,可能存在一些问题。

    https://codereview.qt-project.org/c/qt/qtbase/+/611940

    另外,如果你的发行版所附的 Qt 6.8 版本并未更新至 Qt 6.8.1,则可能需要打两个额外的补丁,可参见 DDE Qt 6.8 适配说明

    获取移植帮助

    如果您希望得到移植相关的帮助,请考虑加入我们 DDE 移植小组的在线交流群(下列房间有桥接,任选其一即可),一起展开相关的交流:

    Thursday, January 2, 2025

    UOS 这仓库有多鬼畜,就不用我多说了,开发的时候每次都要重装一个不同版本的系统也挺麻烦,比如 1060、1063、1070、1070u1。。。

    所以我就想到,我可以用构建用的 rootfs 加上仓库,整出来一个基础环境,然后通过 distrobox 来启动,这样随时都可以快速准备一个环境。

    平时我用的开发工具就 neovim 和 vscode 居多,所以再配合上 nix 把 neovim 和一些 UOS 缺少的工具都装上,想想都是美滋滋。

    说干就干!

    构建 Docker 镜像

    由于 UOS 有多个不同架构的支持,肯定不能绑死到一个平台上,我使用 docker buildx 来构建多架构镜像。rootfs 我就不公开了,直接上 Dockerfile。

    FROM --platform=$TARGETPLATFORM scratch

    ARG TARGETARCH
    ARG UOS_VERSION

    ADD buster-${TARGETARCH}-1050update4.tgz /

    RUN echo "deb [trusted=yes] http://pools.uniontech.com/desktop-professional ${UOS_VERSION:-eagle/1070} main contrib non-free" > /etc/apt/sources.list

    RUN apt-get update && apt-get install -y \
    apt-utils \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release \
    && apt-get dist-upgrade -y \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

    CMD [ "bash" ]

    这里为了方便我构建,我将仓库的名称使用变量控制了。

    docker buildx build --platform=linux/amd64 -t linuxdeepin/1063:base --build-arg UOS_VERSION=eagle/1063 . --load

    只需要控制 label 和 仓库名称,就能快速的创建出一个可用的 docker 镜像。使用 --load 参数构建完以后直接导入镜像,可以在 docker images 中看到。

    创建 Distrobox 环境

    现在有个可用的 docker 镜像,就可以使用 distrobox 创建环境了。

    distrobox create --image linuxdeepin/1063:base --name 1063 --volume /nix:/nix:rw --additional-flags "--pids-limit -1"

    安装 nix 的过程请参考上一篇文章,这里是把 nix 挂载到环境里了。

    运行以后等待一会儿,就看到创建成功了。

    Creating '1063' using image linuxdeepin/1063:base        [ OK ]
    Distrobox '1063' successfully created.
    To enter, run:

    distrobox enter 1063

    Successfully copied 2.05kB to /tmp/1063.os-release

    提醒我们用 distrobox enter 1063 进入环境。

    运行

    运行的话没啥问题,只是需要设置一下用户密码,不知道是不是 distrobox 改变策略了,以前应该是首次进入的时候就提醒设置密码,现在必须手动先运行一下 passwd,不然 sudo 之类的命令是不能用的。

    ❯ distrobox enter 1063
    Starting container... [ OK ]
    Installing basic packages... [ OK ]
    Setting up devpts mounts... [ OK ]
    Setting up read-only mounts... [ OK ]
    Setting up read-write mounts... [ OK ]
    Setting up host's sockets integration... [ OK ]
    Integrating host's themes, icons, fonts... [ OK ]
    Setting up distrobox profile... [ OK ]
    Setting up sudo... [ OK ]
    Setting up user groups... [ OK ]
    Setting up user's group list... [ OK ]
    Adding user... [ OK ]
    Ensuring user's access... [ OK ]

    Container Setup Complete!

    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 用于窗口系统集成,而 QPlatformThemeQStyleHint 则用于深层次的平台主题和集成。

    基本工作流程

    Treeland 使用 QQuickWindow 作为渲染的根,这样在 Treeland 里开发时,就如同开发一个普通 Qt 程序一样,先创建一个 Window,在 Window 内创建 Qt 控件,使用 QEvent 处理各种事件。

    那么 Treeland 是如何实现这件事的呢?

    QQuickWindow 的私有类提供了自定义 QQuickGraphicsDevice 对象的接口,而 QQuickGraphicsDevice 可以使用 fromOpenGLContextfromPhyicalDevice 创建新的对象,那么 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 处理底层事件与上层事件的流程

    Treeland 处理底层事件与上层事件的流程

    bool WOutputRenderWindow::event(QEvent *event)
    {
    Q_D(WOutputRenderWindow);

    if (event->type() == doRenderEventType) {
    QCoreApplication::removePostedEvents(this, doRenderEventType);
    d_func()->doRender();
    return true;
    }

    if (QW::RenderWindow::beforeDisposeEventFilter(this, event)) {
    event->accept();
    QW::RenderWindow::afterDisposeEventFilter(this, event);
    return true;
    }

    bool isAccepted = QQuickWindow::event(event);
    if (QW::RenderWindow::afterDisposeEventFilter(this, event))
    return true;

    return isAccepted;
    }

    WOutputRenderWindow 的事件处理中,会额外调用下 seat 的事件过滤器,确保合成器可以拦截掉一部分事件,例如将一部分按键拦截下来,不发送给客户端。

    bool QWlrootsRenderWindow::beforeDisposeEventFilter(QEvent *event)
    {
    if (event->isInputEvent()) {
    auto ie = static_cast<QInputEvent*>(event);
    auto device = WInputDevice::from(ie->device());
    Q_ASSERT(device);
    Q_ASSERT(device->seat());
    lastActiveCursor = device->seat()->cursor();
    return device->seat()->filterEventBeforeDisposeStage(window(), ie);
    }

    return false;
    }

    这段代码展示了转换输入设备的功能,判断输入设备的类型,创建对应的 QInputDevice 对象。

    QPointer<QInputDevice> QWlrootsIntegration::addInputDevice(WInputDevice *device, const QString &seatName)
    {
    QPointer<QInputDevice> qtdev;
    auto qwDevice = device->handle();
    const QString name = QString::fromUtf8(qwDevice->handle()->name);
    qint64 systemId = reinterpret_cast<qint64>(device);

    switch (qwDevice->handle()->type) {
    case WLR_INPUT_DEVICE_KEYBOARD: {
    qtdev = new QInputDevice(name, systemId, QInputDevice::DeviceType::Keyboard, seatName);
    break;
    }
    case WLR_INPUT_DEVICE_POINTER: {
    qtdev = new QPointingDevice(name, systemId, QInputDevice::DeviceType::TouchPad, QPointingDevice::PointerType::Generic,
    QInputDevice::Capability::Position | QInputDevice::Capability::Hover
    | QInputDevice::Capability::Scroll | QInputDevice::Capability::MouseEmulation,
    10, 32, seatName, QPointingDeviceUniqueId());
    break;
    }
    case WLR_INPUT_DEVICE_TOUCH: {
    qtdev = new QPointingDevice(name, systemId, QInputDevice::DeviceType::TouchScreen, QPointingDevice::PointerType::Finger,
    QInputDevice::Capability::Position | QInputDevice::Capability::Area | QInputDevice::Capability::MouseEmulation,
    10, 32, seatName, QPointingDeviceUniqueId());
    break;
    }
    case WLR_INPUT_DEVICE_TABLET_TOOL: {
    qtdev = new QPointingDevice(name, systemId, QInputDevice::DeviceType::Stylus, QPointingDevice::PointerType::Pen,
    QInputDevice::Capability::XTilt | QInputDevice::Capability::YTilt | QInputDevice::Capability::Pressure,
    1, 32, seatName, QPointingDeviceUniqueId());
    break;
    }
    case WLR_INPUT_DEVICE_TABLET_PAD: {
    auto pad = wlr_tablet_pad_from_input_device(qwDevice->handle());
    qtdev = new QPointingDevice(name, systemId, QInputDevice::DeviceType::TouchPad, QPointingDevice::PointerType::Pen,
    QInputDevice::Capability::Position | QInputDevice::Capability::Hover | QInputDevice::Capability::Pressure,
    1, pad->button_count, seatName, QPointingDeviceUniqueId());
    break;
    }
    case WLR_INPUT_DEVICE_SWITCH: {
    qtdev = new QInputDevice(name, systemId, QInputDevice::DeviceType::Keyboard, seatName);
    break;
    }
    }

    if (qtdev) {
    device->setQtDevice(qtdev);
    QWindowSystemInterface::registerInputDevice(qtdev);

    if (qtdev->type() == QInputDevice::DeviceType::Mouse || qtdev->type() == QInputDevice::DeviceType::TouchPad) {
    auto primaryQtDevice = QPointingDevice::primaryPointingDevice();
    if (!WInputDevice::from(primaryQtDevice)) {
    // Ensure the primary pointing device is the WInputDevice
    auto pd = const_cast<QPointingDevice*>(primaryQtDevice);
    pd->setParent(nullptr);
    delete pd;
    }
    Q_ASSERT(WInputDevice::from(QPointingDevice::primaryPointingDevice()));
    } else if (qtdev->type() == QInputDevice::DeviceType::Keyboard) {
    auto primaryQtDevice = QInputDevice::primaryKeyboard();
    if (!WInputDevice::from(primaryQtDevice)) {
    // Ensure the primary keyboard device is the WInputDevice
    auto pd = const_cast<QInputDevice*>(primaryQtDevice);
    pd->setParent(nullptr);
    delete pd;
    }
    Q_ASSERT(WInputDevice::from(QInputDevice::primaryKeyboard()));
    }
    }

    return qtdev;
    }

    客户端事件

    在 Treeland 还有一种事件需要处理,当用户点击一个窗口,合成器需要告知客户端哪个坐标点击了。或者使用键盘进行输入时,需要告知客户端输入的内容。

    首先,Treeland 会标记一个窗口成为激活窗口,设置给 seat,这样 wlroots 就知道哪个窗口此时拥有焦点。

    之后当键盘发生输入事件时,Treeland 没有过滤掉按键事件,或者是放行某些按键,这些剩余的输入事件就会在 wseat 的 sendEvent 中,发送给激活的客户端。

    // for keyboard event
    inline bool doNotifyKey(WInputDevice *device, uint32_t keycode, uint32_t state, uint32_t timestamp) {
    if (!keyboardFocusSurface())
    return false;

    q_func()->setKeyboard(device);
    /* Send modifiers to the client. */
    this->handle()->keyboard_notify_key(timestamp, keycode, state);
    return true;
    }

    屏幕信息

    在 QPA 中还对 WOutput 进行了封装 QWlrootsScreen

    QWlrootsScreen *QWlrootsIntegration::addScreen(WOutput *output)
    {
    m_screens << new QWlrootsScreen(output);

    if (isMaster()) {
    QWindowSystemInterface::handleScreenAdded(m_screens.last());

    if (m_placeholderScreen) {
    QWindowSystemInterface::handleScreenRemoved(m_placeholderScreen.release());
    }
    } else {
    Q_UNUSED(new QScreen(m_screens.last()))
    }

    m_screens.last()->initialize();
    output->setScreen(m_screens.last());

    return m_screens.last();
    }

    QWlrootsScreen 继承自 QPlatformScreen,做的事情是将部分参数进行转换,例如physicalSize、devicePixelRatio、DPI等,之后通过 QWindowSystemInterface::handleScreenAdded 将创建好的 QWlrootsScreen 添加进 Qt 内。

    Qt RHI

    摘抄一段来自 waylib 中初始化 Qt RHI 的代码

    bool WOutputRenderWindowPrivate::initRCWithRhi()
    {
    W_Q(WOutputRenderWindow);

    QQuickRenderControlPrivate *rcd = QQuickRenderControlPrivate::get(rc());
    QSGRhiSupport *rhiSupport = QSGRhiSupport::instance();

    // sanity check for Vulkan
    #ifdef ENABLE_VULKAN_RENDER
    if (rhiSupport->rhiBackend() == QRhi::Vulkan) {
    vkInstance.reset(new QVulkanInstance());

    auto phdev = wlr_vk_renderer_get_physical_device(m_renderer->handle());
    auto dev = wlr_vk_renderer_get_device(m_renderer->handle());
    auto queue_family = wlr_vk_renderer_get_queue_family(m_renderer->handle());

    #if QT_VERSION > QT_VERSION_CHECK(6, 6, 0)
    auto instance = wlr_vk_renderer_get_instance(m_renderer->handle());
    vkInstance->setVkInstance(instance);
    #endif
    // vkInstance->setExtensions(fromCStyleList(vkRendererAttribs.extension_count, vkRendererAttribs.extensions));
    // vkInstance->setLayers(fromCStyleList(vkRendererAttribs.layer_count, vkRendererAttribs.layers));
    vkInstance->setApiVersion({1, 1, 0});
    vkInstance->create();
    q->setVulkanInstance(vkInstance.data());

    auto gd = QQuickGraphicsDevice::fromDeviceObjects(phdev, dev, queue_family);
    q->setGraphicsDevice(gd);
    } else
    #endif
    if (rhiSupport->rhiBackend() == QRhi::OpenGLES2) {
    Q_ASSERT(wlr_renderer_is_gles2(m_renderer->handle()));
    auto egl = wlr_gles2_renderer_get_egl(m_renderer->handle());
    auto display = wlr_egl_get_display(egl);
    auto context = wlr_egl_get_context(egl);

    this->glContext = new QW::OpenGLContext(display, context, rc());
    bool ok = this->glContext->create();
    if (!ok)
    return false;

    q->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(this->glContext));
    } else {
    return false;
    }

    QOffscreenSurface *offscreenSurface = new QW::OffscreenSurface(nullptr, q);
    offscreenSurface->create();

    QSGRhiSupport::RhiCreateResult result = rhiSupport->createRhi(q, offscreenSurface);
    if (!result.rhi) {
    qWarning("WOutput::initRhi: Failed to initialize QRhi");
    return false;
    }

    rcd->rhi = result.rhi;
    // Ensure the QQuickRenderControl don't reinit the RHI
    rcd->ownRhi = true;
    if (!rc()->initialize())
    return false;
    rcd->ownRhi = result.own;
    Q_ASSERT(rcd->rhi == result.rhi);
    Q_ASSERT(!swapchain);

    return true;
    }

    先获取 QSGRhiSupport 及相关控制对象。

    判断 RHI backend 的类型,需要适配 vulkangles等。

    wlroots 获取物理设备等参数,使用 QQuickGraphicsDevice::fromDeviceObjects 创建 Qt 的 QQuickGraphicsDevice

    render window的私有类是继承自 QQuickWindowPrivate,只需要将获取到的 QQuickGraphicsDevice 设置给 QQuickWindowPrivate::setGraphicsDevice 即可。

    之后创建一个离屏渲染表面,用于 RHI 的初始化。

    Qt Viewport

    在 Qt 中,想要查看或者渲染一个组件,需要使用 Viewport 组件,俗称照相机。

    视口(Viewport)是一个可观察的多边形区域,只有 Viewport 范围内的画面才能显示到屏幕上。

    wlroots 中的 Viewport 是一个与 Wayland 显示协议相关的概念,主要用于定义渲染输出在屏幕上的显示区域。它允许在渲染时对显示内容进行缩放、裁剪或平移,以适应不同的分辨率和显示需求。

    Treeland 使用 WOutputViewport 提供 Viewport 功能,使用 wlrootswlr_output 中的屏幕信息,对画面进行矩阵变换,这里会涉及到屏幕的缩放、DPI等参数。

    QMatrix4x4 WOutputViewport::renderMatrix() const
    {
    QMatrix4x4 renderMatrix;

    if (auto customTransform = viewportTransform()) {
    customTransform->applyTo(&renderMatrix);
    } else if (parentItem() && !ignoreViewport() && input() != this) {
    auto d = QQuickItemPrivate::get(const_cast<WOutputViewport*>(this));
    auto viewportMatrix = d->itemNode()->matrix().inverted();
    if (auto inputItem = input()) {
    QMatrix4x4 matrix = QQuickItemPrivate::get(parentItem())->itemToWindowTransform();
    matrix *= QQuickItemPrivate::get(inputItem)->windowToItemTransform();
    renderMatrix = viewportMatrix * matrix.inverted();
    } else { // the input item is window's contentItem
    auto pd = QQuickItemPrivate::get(parentItem());
    QMatrix4x4 parentMatrix = pd->itemToWindowTransform().inverted();
    renderMatrix = viewportMatrix * parentMatrix;
    }
    }

    return renderMatrix;
    }

    WOutputViewport 提供了 Viewport 所需的所有参数,变换矩阵、源几何大小、目标几何大小等信息。

    WOutputRenderWindow 的事件中,判断如果是渲染的事件,就执行渲染。

    bool WOutputRenderWindow::event(QEvent *event)
    {
    Q_D(WOutputRenderWindow);

    if (event->type() == doRenderEventType) {
    QCoreApplication::removePostedEvents(this, doRenderEventType);
    d_func()->doRender();
    return true;
    }

    if (QW::RenderWindow::beforeDisposeEventFilter(this, event)) {
    event->accept();
    QW::RenderWindow::afterDisposeEventFilter(this, event);
    return true;
    }

    bool isAccepted = QQuickWindow::event(event);
    if (QW::RenderWindow::afterDisposeEventFilter(this, event))
    return true;

    return isAccepted;
    }

    在 doRender 中,遍历所有的 Output,执行 beginRender,然后执行 Output 的渲染。

    void WOutputRenderWindowPrivate::doRender(const QList<OutputHelper *> &outputs,
    bool forceRender, bool doCommit)
    {
    Q_ASSERT(rendererList.isEmpty());
    Q_ASSERT(!inRendering);
    inRendering = true;

    W_Q(WOutputRenderWindow);
    for (OutputLayer *layer : std::as_const(layers)) {
    layer->beforeRender(q);
    }

    rc()->polishItems();

    if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi()))
    rc()->beginFrame();
    rc()->sync();

    QQuickAnimatorController_advance(animationController.get());
    Q_EMIT q->beforeRendering();
    runAndClearJobs(&beforeRenderingJobs);

    auto needsCommit = doRenderOutputs(outputs, forceRender);

    Q_EMIT q->afterRendering();
    runAndClearJobs(&afterRenderingJobs);

    if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi()))
    rc()->endFrame();

    if (doCommit) {
    for (auto i : std::as_const(needsCommit)) {
    bool ok = i.first->commit(i.second);

    if (i.second->currentBuffer()) {
    i.second->endRender();
    }

    i.first->resetState(ok);
    }
    }

    resetGlState();

    // On Intel&Nvidia multi-GPU environment, wlroots using Intel card do render for all
    // outputs, and blit nvidia's output buffer in drm_connector_state_update_primary_fb,
    // the 'blit' behavior will make EGL context to Nvidia renderer. So must done current
    // OpenGL context here in order to ensure QtQuick always make EGL context to Intel
    // renderer before next frame.
    if (glContext)
    glContext->doneCurrent();

    inRendering = false;
    Q_EMIT q->renderEnd();
    }
    qw_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePixelRatio,
    uint32_t format, RenderFlags flags)
    {
    Q_ASSERT(!state.buffer);
    Q_ASSERT(m_output);

    if (pixelSize.isEmpty())
    return nullptr;

    Q_EMIT beforeRendering();

    m_damageRing.set_bounds(pixelSize.width(), pixelSize.height());

    // configure swapchain
    if (flags.testFlag(RenderFlag::DontConfigureSwapchain)) {
    auto renderFormat = pickFormat(m_output->renderer(), format);
    if (!renderFormat) {
    qWarning("wlr_renderer doesn't support format 0x%s", drmGetFormatName(format));
    return nullptr;
    }

    if (!m_swapchain || QSize(m_swapchain->handle()->width, m_swapchain->handle()->height) != pixelSize
    || m_swapchain->handle()->format.format != renderFormat->format) {
    if (m_swapchain)
    delete m_swapchain;
    m_swapchain = qw_swapchain::create(m_output->allocator()->handle(), pixelSize.width(), pixelSize.height(), renderFormat);
    }
    } else if (flags.testFlag(RenderFlag::UseCursorFormats)) {
    bool ok = m_output->configureCursorSwapchain(pixelSize, format, &m_swapchain);
    if (!ok)
    return nullptr;
    } else {
    bool ok = m_output->configurePrimarySwapchain(pixelSize, format, &m_swapchain,
    !flags.testFlag(DontTestSwapchain));
    if (!ok)
    return nullptr;
    }

    // TODO: Support scanout buffer of wlr_surface(from WSurfaceItem)
    int bufferAge;
    auto wbuffer = m_swapchain->acquire(&bufferAge);
    if (!wbuffer)
    return nullptr;
    auto buffer = qw_buffer::from(wbuffer);

    if (!m_renderHelper)
    m_renderHelper = new WRenderHelper(m_output->renderer());
    m_renderHelper->setSize(pixelSize);

    auto wd = QQuickWindowPrivate::get(window());
    Q_ASSERT(wd->renderControl);
    auto lastRT = m_renderHelper->lastRenderTarget();
    auto rt = m_renderHelper->acquireRenderTarget(wd->renderControl, buffer);
    if (rt.isNull()) {
    buffer->unlock();
    return nullptr;
    }

    auto rtd = QQuickRenderTargetPrivate::get(&rt);
    QSGRenderTarget sgRT;

    if (rtd->type == QQuickRenderTargetPrivate::Type::PaintDevice) {
    sgRT.paintDevice = rtd->u.paintDevice;
    } else {
    Q_ASSERT(rtd->type == QQuickRenderTargetPrivate::Type::RhiRenderTarget);
    sgRT.rt = rtd->u.rhiRt;
    sgRT.cb = wd->redirect.commandBuffer;
    Q_ASSERT(sgRT.cb);
    sgRT.rpDesc = rtd->u.rhiRt->renderPassDescriptor();

    #ifndef QT_NO_OPENGL
    if (wd->rhi->backend() == QRhi::OpenGLES2) {
    auto glRT = QRHI_RES(QGles2TextureRenderTarget, rtd->u.rhiRt);
    Q_ASSERT(glRT->framebuffer >= 0);
    auto glContext = QOpenGLContext::currentContext();
    Q_ASSERT(glContext);
    QOpenGLContextPrivate::get(glContext)->defaultFboRedirect = glRT->framebuffer;
    }
    #endif
    }

    state.flags = flags;
    state.context = wd->context;
    state.pixelSize = pixelSize;
    state.devicePixelRatio = devicePixelRatio;
    state.bufferAge = bufferAge;
    state.lastRT = lastRT;
    state.buffer = buffer;
    state.renderTarget = rt;
    state.sgRenderTarget = sgRT;

    return buffer;
    }
    QVector<std::pair<OutputHelper*, WBufferRenderer*>>
    WOutputRenderWindowPrivate::doRenderOutputs(const QList<OutputHelper*> &outputs, bool forceRender)
    {
    QVector<OutputHelper*> renderResults;
    renderResults.reserve(outputs.size());
    for (OutputHelper *helper : std::as_const(outputs)) {
    if (Q_LIKELY(!forceRender)) {
    if (!helper->renderable()
    || Q_UNLIKELY(!WOutputViewportPrivate::get(helper->output())->renderable())
    || !helper->output()->output()->isEnabled())
    continue;

    if (!helper->contentIsDirty()) {
    if (helper->needsFrame())
    renderResults.append(helper);
    continue;
    }
    }

    Q_ASSERT(helper->output()->output()->scale() <= helper->output()->devicePixelRatio());

    const auto &format = helper->qwoutput()->handle()->render_format;
    const auto renderMatrix = helper->output()->renderMatrix();

    // maybe using the other WOutputViewport's QSGTextureProvider
    if (!helper->output()->depends().isEmpty())
    updateDirtyNodes();

    qw_buffer *buffer = helper->beginRender(helper->bufferRenderer(), helper->output()->output()->size(), format,
    WBufferRenderer::RedirectOpenGLContextDefaultFrameBufferObject);
    Q_ASSERT(buffer == helper->bufferRenderer()->currentBuffer());
    if (buffer) {
    helper->render(helper->bufferRenderer(), 0, renderMatrix,
    helper->output()->effectiveSourceRect(),
    helper->output()->targetRect(),
    helper->output()->preserveColorContents());
    }
    renderResults.append(helper);
    }

    QVector<std::pair<OutputHelper*, WBufferRenderer*>> needsCommit;
    needsCommit.reserve(renderResults.size());
    for (auto helper : std::as_const(renderResults)) {
    auto bufferRenderer = helper->afterRender();
    if (bufferRenderer)
    needsCommit.append({helper, bufferRenderer});
    }

    rendererList.clear();

    return needsCommit;
    }
    void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix,
    const QRectF &sourceRect, const QRectF &targetRect,
    bool preserveColorContents)
    {
    Q_ASSERT(state.buffer);

    const auto &source = m_sourceList.at(sourceIndex);
    QSGRenderer *renderer = ensureRenderer(sourceIndex, state.context);
    auto wd = QQuickWindowPrivate::get(window());

    const qreal devicePixelRatio = state.devicePixelRatio;
    state.renderer = renderer;
    state.worldTransform = renderMatrix;
    renderer->setDevicePixelRatio(devicePixelRatio);
    renderer->setDeviceRect(QRect(QPoint(0, 0), state.pixelSize));
    renderer->setRenderTarget(state.sgRenderTarget);
    const auto viewportRect = scaleToRect(targetRect, devicePixelRatio);

    auto softwareRenderer = dynamic_cast<QSGSoftwareRenderer*>(renderer);
    { // before render
    if (softwareRenderer) {
    // because software renderer don't supports viewportRect,
    // so use transform to simulation.
    const auto mapTransform = inputMapToOutput(sourceRect, targetRect,
    state.pixelSize, state.devicePixelRatio);
    if (!mapTransform.isIdentity())
    state.worldTransform = mapTransform * state.worldTransform;
    state.worldTransform.optimize();
    auto image = getImageFrom(state.renderTarget);
    image->setDevicePixelRatio(devicePixelRatio);

    // TODO: Should set to QSGSoftwareRenderer, but it's not support specify matrix.
    // If transform is changed, it will full repaint.
    if (isRootItem(source.source)) {
    auto rootTransform = QQuickItemPrivate::get(wd->contentItem)->itemNode();
    if (rootTransform->matrix() != state.worldTransform)
    rootTransform->setMatrix(state.worldTransform);
    } else {
    auto t = state.worldTransform.toTransform();
    if (t.type() > QTransform::TxTranslate) {
    (image->operator QImage &()).fill(renderer->clearColor());
    softwareRenderer->markDirty();
    }

    applyTransform(softwareRenderer, t);
    }
    } else {
    state.worldTransform.optimize();

    bool flipY = wd->rhi ? !wd->rhi->isYUpInNDC() : false;
    if (state.renderTarget.mirrorVertically())
    flipY = !flipY;

    if (viewportRect.isValid()) {
    QRect vr = viewportRect;
    if (flipY)
    vr.moveTop(-vr.y() + state.pixelSize.height() - vr.height());
    renderer->setViewportRect(vr);
    } else {
    renderer->setViewportRect(QRect(QPoint(0, 0), state.pixelSize));
    }

    QRectF rect = sourceRect;
    if (!rect.isValid())
    rect = QRectF(QPointF(0, 0), QSizeF(state.pixelSize) / devicePixelRatio);

    const float left = rect.x();
    const float right = rect.x() + rect.width();
    float bottom = rect.y() + rect.height();
    float top = rect.y();

    if (flipY)
    std::swap(top, bottom);

    QMatrix4x4 matrix;
    matrix.ortho(left, right, bottom, top, 1, -1);

    QMatrix4x4 projectionMatrix, projectionMatrixWithNativeNDC;
    projectionMatrix = matrix * state.worldTransform;

    if (wd->rhi && !wd->rhi->isYUpInNDC()) {
    std::swap(top, bottom);

    matrix.setToIdentity();
    matrix.ortho(left, right, bottom, top, 1, -1);
    }
    projectionMatrixWithNativeNDC = matrix * state.worldTransform;

    renderer->setProjectionMatrix(projectionMatrix);
    renderer->setProjectionMatrixWithNativeNDC(projectionMatrixWithNativeNDC);

    auto textureRT = static_cast<QRhiTextureRenderTarget*>(state.sgRenderTarget.rt);
    if (preserveColorContents) {
    textureRT->setFlags(textureRT->flags() | QRhiTextureRenderTarget::PreserveColorContents);
    } else {
    textureRT->setFlags(textureRT->flags() & ~QRhiTextureRenderTarget::PreserveColorContents);
    }
    }
    }

    state.context->renderNextFrame(renderer);

    { // after render
    if (!softwareRenderer) {
    // TODO: get damage area from QRhi renderer
    m_damageRing.add_whole();
    // ###: maybe Qt bug? Before executing QRhi::endOffscreenFrame, we may
    // use the same QSGRenderer for multiple drawings. This can lead to
    // rendering the same content for different QSGRhiRenderTarget instances
    // when using the RhiGles backend. Additionally, considering that the
    // result of the current drawing may be needed when drawing the next
    // sourceIndex, we should let the RHI (Rendering Hardware Interface)
    // complete the results of this drawing here to ensure the current
    // drawing result is available for use.
    wd->rhi->finish();
    } else {
    auto currentImage = getImageFrom(state.renderTarget);
    Q_ASSERT(currentImage && currentImage == softwareRenderer->m_rt.paintDevice);
    currentImage->setDevicePixelRatio(1.0);
    const auto scaleTF = QTransform::fromScale(devicePixelRatio, devicePixelRatio);
    const auto scaledFlushRegion = scaleTF.map(softwareRenderer->flushRegion());
    PixmanRegion scaledFlushDamage;
    bool ok = WTools::toPixmanRegion(scaledFlushRegion, scaledFlushDamage);
    Q_ASSERT(ok);

    {
    PixmanRegion damage;
    m_damageRing.get_buffer_damage(state.bufferAge, damage);

    if (viewportRect.isValid()) {
    QRect imageRect = (currentImage->operator const QImage &()).rect();
    QRegion invalidRegion(imageRect);
    invalidRegion -= viewportRect;
    if (!scaledFlushRegion.isEmpty())
    invalidRegion &= scaledFlushRegion;

    if (!invalidRegion.isEmpty()) {
    QPainter pa(currentImage);
    for (const auto r : std::as_const(invalidRegion))
    pa.fillRect(r, softwareRenderer->clearColor());
    }
    }

    if (!damage.isEmpty() && state.lastRT.first != state.buffer && !state.lastRT.second.isNull()) {
    auto image = getImageFrom(state.lastRT.second);
    Q_ASSERT(image);
    Q_ASSERT(image->size() == state.pixelSize);

    // TODO: Don't use the previous render target, we can get the damage region of QtQuick
    // before QQuickRenderControl::render for qw_damage_ring, and add dirty region to
    // QSGAbstractSoftwareRenderer to force repaint the damage region of current render target.
    QPainter pa(currentImage);

    PixmanRegion remainderDamage;
    ok = pixman_region32_subtract(remainderDamage, damage, scaledFlushDamage);
    Q_ASSERT(ok);

    int count = 0;
    auto rects = pixman_region32_rectangles(remainderDamage, &count);
    for (int i = 0; i < count; ++i) {
    auto r = rects[i];
    pa.drawImage(r.x1, r.y1, *image, r.x1, r.y1, r.x2 - r.x1, r.y2 - r.y1);
    }
    }
    }

    if (!isRootItem(source.source))
    applyTransform(softwareRenderer, state.worldTransform.inverted().toTransform());
    m_damageRing.add(scaledFlushDamage);
    }
    }

    if (auto dr = qobject_cast<QSGDefaultRenderContext*>(state.context)) {
    QRhiResourceUpdateBatch *resourceUpdates = wd->rhi->nextResourceUpdateBatch();
    dr->currentFrameCommandBuffer()->resourceUpdate(resourceUpdates);
    }

    if (shouldCacheBuffer())
    wTextureProvider()->setBuffer(state.buffer);
    }

    处理完画面以后,如果需要上屏画面,就调用 commit 把画面送到屏幕上。

    bool OutputHelper::commit(WBufferRenderer *buffer)
    {
    if (output()->offscreen())
    return true;

    if (!buffer || !buffer->currentBuffer()) {
    Q_ASSERT(!this->buffer());
    return WOutputHelper::commit();
    }

    setBuffer(buffer->currentBuffer());

    if (m_lastCommitBuffer == buffer) {
    if (pixman_region32_not_empty(&buffer->damageRing()->handle()->current))
    setDamage(&buffer->damageRing()->handle()->current);
    }

    m_lastCommitBuffer = buffer;

    return WOutputHelper::commit();
    }

    还会判断是否有硬件加速(GPU),会优先使用硬件来加速计算过程。

    } else {
    state.worldTransform.optimize();

    bool flipY = wd->rhi ? !wd->rhi->isYUpInNDC() : false;
    if (state.renderTarget.mirrorVertically())
    flipY = !flipY;

    if (viewportRect.isValid()) {
    QRect vr = viewportRect;
    if (flipY)
    vr.moveTop(-vr.y() + state.pixelSize.height() - vr.height());
    renderer->setViewportRect(vr);
    } else {
    renderer->setViewportRect(QRect(QPoint(0, 0), state.pixelSize));
    }

    QRectF rect = sourceRect;
    if (!rect.isValid())
    rect = QRectF(QPointF(0, 0), QSizeF(state.pixelSize) / devicePixelRatio);

    const float left = rect.x();
    const float right = rect.x() + rect.width();
    float bottom = rect.y() + rect.height();
    float top = rect.y();

    if (flipY)
    std::swap(top, bottom);

    QMatrix4x4 matrix;
    matrix.ortho(left, right, bottom, top, 1, -1);

    QMatrix4x4 projectionMatrix, projectionMatrixWithNativeNDC;
    projectionMatrix = matrix * state.worldTransform;

    if (wd->rhi && !wd->rhi->isYUpInNDC()) {
    std::swap(top, bottom);

    matrix.setToIdentity();
    matrix.ortho(left, right, bottom, top, 1, -1);
    }
    projectionMatrixWithNativeNDC = matrix * state.worldTransform;

    renderer->setProjectionMatrix(projectionMatrix);
    renderer->setProjectionMatrixWithNativeNDC(projectionMatrixWithNativeNDC);

    auto textureRT = static_cast<QRhiTextureRenderTarget*>(state.sgRenderTarget.rt);
    if (preserveColorContents) {
    textureRT->setFlags(textureRT->flags() | QRhiTextureRenderTarget::PreserveColorContents);
    } else {
    textureRT->setFlags(textureRT->flags() & ~QRhiTextureRenderTarget::PreserveColorContents);
    }
    }

    Surface 渲染

    在 Treeland 中,为 Surface 创建了 WSurfaceItem,用于表示一个窗口,并创建了 WSurfaceContent 作为 WSurfaceItem 的 delegate。

    void WSurfaceItemPrivate::initForDelegate()
    {
    Q_Q(WSurfaceItem);

    std::unique_ptr<QQuickItem> newContentContainer;

    if (!delegate) {
    if (getItemContent()) {
    Q_ASSERT(!delegateIsDirty);
    return;
    }

    delegateIsDirty = false;
    auto contentItem = new WSurfaceItemContent(q);
    if (surface)
    contentItem->setSurface(surface);
    contentItem->setCacheLastBuffer(!surfaceFlags.testFlag(WSurfaceItem::DontCacheLastBuffer));
    contentItem->setSmooth(q->smooth());
    contentItem->setLive(!q->flags().testFlag(WSurfaceItem::NonLive));
    QObject::connect(q, &WSurfaceItem::smoothChanged, contentItem, &WSurfaceItemContent::setSmooth);
    newContentContainer.reset(contentItem);
    } else if (delegateIsDirty) {
    auto obj = delegate->createWithInitialProperties({{"surface", QVariant::fromValue(q)}}, qmlContext(q));
    if (!obj) {
    qWarning() << "Failed on create surface item from delegate, error mssage:"
    << delegate->errorString();
    return;
    }

    delegateIsDirty = false;
    auto contentItem = qobject_cast<QQuickItem*>(obj);
    if (!contentItem)
    qFatal() << "SurfaceItem's delegate must is Item";

    newContentContainer.reset(new QQuickItem(q));
    QQmlEngine::setObjectOwnership(contentItem, QQmlEngine::CppOwnership);
    contentItem->setParent(newContentContainer.get());
    contentItem->setParentItem(newContentContainer.get());
    }

    if (!newContentContainer)
    return;

    newContentContainer->setZ(qreal(WSurfaceItem::ZOrder::ContentItem));

    if (contentContainer) {
    newContentContainer->setPosition(contentContainer->position());
    newContentContainer->setSize(contentContainer->size());
    newContentContainer->setTransformOrigin(contentContainer->transformOrigin());
    newContentContainer->setScale(contentContainer->scale());

    contentContainer->disconnect(q);
    contentContainer->deleteLater();
    }
    contentContainer = newContentContainer.release();
    updateEventItem(false);
    updateBoundingRect();
    if (eventItem)
    updateEventItemGeometry();

    Q_EMIT q->contentItemChanged();
    }

    之后当 WSurfaceItem 需要更新画面时,就能调用 updatePaintNode 更新渲染。

    QSGNode *WSurfaceItemContent::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
    {
    W_D(WSurfaceItemContent);

    auto tp = wTextureProvider();
    if (d->live || !tp->texture()) {
    auto texture = d->surface ? d->surface->handle()->get_texture() : nullptr;
    if (texture) {
    tp->setTexture(qw_texture::from(texture), d->buffer.get());
    } else {
    tp->setBuffer(d->buffer.get());
    }
    }

    if (!tp->texture() || width() <= 0 || height() <= 0) {
    delete oldNode;
    return nullptr;
    }

    auto node = static_cast<QSGImageNode*>(oldNode);
    if (Q_UNLIKELY(!node)) {
    node = window()->createImageNode();
    node->setOwnsTexture(false);
    QSGNode *fpnode = new WSGRenderFootprintNode(this);
    node->appendChildNode(fpnode);
    }

    auto texture = tp->texture();
    node->setTexture(texture);
    const QRectF textureGeometry = d->bufferSourceBox;
    node->setSourceRect(textureGeometry);
    const QRectF targetGeometry(d->ignoreBufferOffset ? QPointF() : d->bufferOffset, size());
    node->setRect(targetGeometry);
    node->setFiltering(smooth() ? QSGTexture::Linear : QSGTexture::Nearest);

    return node;
    }

    而使用 delegate 的目的是为了能让多个 WSurfaceItem 使用相同的窗口画面,例如某些场景需要临时创建一个窗口的分身,窗口切换列表、多任务视图等。

    QSGTextureProvider *WSurfaceItemContent::textureProvider() const
    {
    if (QQuickItem::isTextureProvider())
    return QQuickItem::textureProvider();

    return wTextureProvider();
    }

    WSGTextureProvider *WSurfaceItemContent::wTextureProvider() const
    {
    W_DC(WSurfaceItemContent);

    auto w = qobject_cast<WOutputRenderWindow*>(d->window);
    if (!w || !d->sceneGraphRenderContext() || QThread::currentThread() != d->sceneGraphRenderContext()->thread()) {
    qWarning("WQuickCursor::textureProvider: can only be queried on the rendering thread of an WOutputRenderWindow");
    return nullptr;
    }

    if (!d->textureProvider) {
    d->textureProvider = new WSGTextureProvider(w);
    if (d->surface) {
    if (auto texture = d->surface->handle()->get_texture()) {
    d->textureProvider->setTexture(qw_texture::from(texture), d->buffer.get());
    } else {
    d->textureProvider->setBuffer(d->buffer.get());
    }
    }
    }
    return d->textureProvider;
    }

    Treeland 使用 WQuickTextureProxy 创建窗口的代理显示,而其中就是获取 WSurfaceItem 的 textureProvider。

    QSGTextureProvider *WQuickTextureProxy::textureProvider() const
    {
    if (QQuickItem::isTextureProvider())
    return QQuickItem::textureProvider();

    W_DC(WQuickTextureProxy);
    if (!d->sourceItem)
    return nullptr;

    return d->sourceItem->textureProvider();
    }

    这样多个 proxy 就可以显示同一个窗口的内容,比 QML 的 ShaderEffectSource 效率更高。

    结尾

    上述仅仅是 Treeland 实现 Qt 和 wlroots 缝合的一部分流程,实际上对事件的处理就十分复杂,不止键盘输入,还需要处理光标、触控、触摸等其他设备。还有光标的绘制也需要区分硬光标和软光标,渲染画面时的硬件加速及软件实现等。

    后续准备写一下光标相关的处理,以及还没介绍 Treeland 的画面是怎么绘制的。

    相关文档

    Friday, November 1, 2024

    回顾

    DDE 在 v15 时期,使用 Mutter 作为带合成器的窗管,以及 Metacity 这种不带合成器的窗管,一个是在高性能设备上使用,一个是为低性能设备上使用。

    在 V20 时期,DDE 更换 KWin 当窗口管理器,由于 KWin 自带有关闭合成器的模式,所以 DDE 也放弃了 Metacity 作为备用窗管的选项。

    Mutter

    Mutter 是 GNOME 开发的带有合成器功能的窗口管理器。

    Mutter

    Mutter

    Metacity

    一个不带有合成器功能的窗口管理器。

    Metacity

    Metacity

    KWin

    KWin 是 KDE 开发的,具有动态切换渲染后端,动态开关合成器功能的窗口管理器。

    KWin

    KWin

    需求和指标

    在一个面向用户的产品中,拥有友好的界面是一个非常重要的事情,所以设计师给了一大堆非常好看的界面交互设计。

    但更多的动画效果,以及更多的组件交互,通常就要求使用更多的资源,更多的内存,更高的功耗。

    设计师说要动画华丽,要动画流畅,要优秀的用户体验。

    架构师说要低资源占用,低内存,不能卡。

    虽说一切目标都是为了用户体验,但两个大指标在实现上竟然是冲突的,那么如何平衡二者,就需要研发献祭一些头发了。

    对比

    在Linux下目前有两个技术栈,一个是历史悠久的X11,另一个是较新的 Wayland。

    接下来,跟随我一起来对比两个技术。

    X11

    架构

    X11采用了比较老的客户端-服务器架构。应用程序通过X服务器与硬件设备进行通信,这种架构非常灵活,支持网络传输,使得远程显示变得可能。然而,X11协议复杂且过时,在很多方面不再符合现代的图形需求。

    性能

    由于其设计较老,X11在一些现代场景下的性能表现不佳。它需要依赖很多扩展和补丁才能实现现代图形效果,例如合成管理器(Compositor)等。这些叠加的复杂性导致了额外的性能开销。

    安全性

    X11的安全性问题比较明显,因为所有应用程序都可以访问彼此的窗口信息。这意味着一个恶意程序可以读取或干扰其他程序的输入输出。虽然可以通过一些扩展和工具加强安全性,但这并不是X11的设计初衷。

    可扩展性和兼容性

    由于存在了几十年,X11有非常广泛的应用支持和兼容性。许多老的应用程序和桌面环境仍然依赖于X11协议,尤其是在远程桌面和某些专业领域。

    输入设备支持

    由于其历史悠久,X11对各种输入设备的支持相当成熟。但随着新硬件和新输入技术的出现,X11的设计显得有些不灵活,特别是在多点触控和手势支持方面。

    远程桌面和网络透明性

    X11的一个优势是其天然的网络透明性。它支持通过网络将显示内容远程传输,这是很多专业领域(如科学计算、服务器管理)中非常有用的功能。

    Wayland

    架构

    Wayland采用的是更简单的设计,去掉了X服务器的复杂层次。消除了很多中间环节,提高了效率和响应速度。Wayland专注于本地显示,不像X11那样直接支持远程显示。

    性能

    Wayland的设计更加现代化,它简化了渲染过程,减少了中间层,性能提升显著。特别是在动画和窗口操作方面,Wayland通常比X11更为流畅。

    安全性

    Wayland在设计时考虑了安全性。应用程序之间的隔离更强,应用程序无法访问其他程序的图像或输入输出,这显著提高了系统的整体安全性。

    可扩展性和兼容性

    尽管Wayland逐渐被主流桌面环境(如GNOME、KDE等)支持,但其应用生态仍然不如X11广泛。在某些老旧或专业的应用场景中,可能需要通过兼容层(如XWayland)来运行基于X11的应用程序。

    输入设备支持

    Wayland针对现代硬件进行了优化,尤其是在多点触控、触摸板手势等方面有更好的支持。此外,它对显示器的DPI缩放、刷新率等也有更灵活的处理。

    远程桌面和网络透明性

    Wayland默认没有X11那样的网络透明性功能。这意味着原生的远程桌面功能比较有限,尽管可以通过一些第三方工具或协议(如VNC、RDP)实现远程桌面,但这不是Wayland的核心功能。

    总结

    总结下来,X11已经进入维护阶段,不再进行大幅更新。随着时间的推移,开发者和社区的注意力逐渐转移到Wayland上,X11可能会逐步淡出主流桌面环境。Wayland正在逐步成为Linux桌面的标准。随着越来越多的应用程序和桌面环境转向支持Wayland,其生态系统正在不断成熟和扩展。

    架构设计

    从技术层面上,我们认为是时候更新技术方案了,曾经的X11+窗口管理器+合成器的模式,灵活但不满足需求,Wayland从底层就将三者融合在了一起,并且更新画面是以每幅完整的画面作为基础的,这确保了画面不会因为不同窗口更新界面的时机不一致导致画面撕裂。

    更重要的一点,自研的窗口管理器,它是以实现DDE的需求为目的的,这是第三方窗口管理器不能比拟的。通常使用第三方的项目时,都要进行大量破坏性的调整,导致fork后的项目无法和上游同步,不能及时获取更新和修复,并且第三方的项目已经发展了很长时间,内部有许多DDE用不上、甚至冲突的功能,都需要进行大量调整,更加剧了维护成本。

    所以 DDE 决定开发一个新的窗口管理器——Treeland。

    Treeland在底层使用 wlroots 作为 Wayland 的基础库,不修改 wlroots 的代码,也就意味着可以随时同步上游进度,获得新的功能与修复。上层使用 Qt,可以充分利用公司内大量的 Qt 开发者,不再需要一直有专人负责特定项目,让DDE的技术栈更加统一。

    imageTreeland 结构图

    Treeland 结构图

    简单介绍一些 Treeland 里涉及的重要项目。

    • QWlroots

      wlroots 的 Qt 绑定,将 wayland 信号转换成 Qt 风格的信号。

    • Waylib

      将 wlroots 中的组件封装成 QtQuick 对象,使用 QPA 为 Treeland 提供事件转换与分发。

    • DtkDeclarative

      DTK 的 QtQuick 组件,封装了大部分 DTK style 的控件。

    在 Waylib中,会使用到 Qt 的 QPA 功能,将 wlroots 作为一个新的平台来处理一部分功能。

    QPA 在 Treeland 中有着举足轻重的地位,来自系统底层的事件会先进入到 Waylib中,在 Waylib 里将事件转换成 Qt 内部事件,发送给上层。这样 Treeland 就可以在 QtQuick 中确定用户的点击位置、按键事件等行为。当用户点击的是窗口时,Treeland 还会通过 Waylib 生成一个事件,通过 seat 的接口发送给客户端,完成界面交互。

    Treeland 处理底层事件与上层事件的流程

    Treeland 处理底层事件与上层事件的流程

    界面效果与优化

    Treeland 作为一个窗口管理器,最重要的功能还是对窗口的管理及显示效果的控制,Treeland 所有的窗口都带有圆角和阴影,以及一些窗口模糊效果。

    圆角

    DDE及deepin社区应用都大量采用了窗口圆角的设计,由合成器提供圆角裁剪可以带来更加统一的界面设计。

    红色为QtQuick圆角/黄色为Treeland圆角

    红色为QtQuick圆角/黄色为Treeland圆角

    QtQuick 圆角是由 Rectangle 组件提供的,它只能同时对四个角进行操作(红色块)。但 DTK 程序具备异形窗口的能力,所以 Treeland 提供了自己的裁圆角控件(黄色块)。
    新的造型算法、几何顶点数量比 QtQuick 原生的 Rectangle 减少50%,GPU顶点渲染和三角细分性能提升100%。
    采用新的抗锯齿算法,提高了GPU片元着色器性能,相比于普通 4xMSAA 抗锯齿算法,计算量减少1/4。

    模糊

    QtQuick 模糊控件

    QtQuick 模糊控件

    QtQuick 的模糊组件仅支持对控件自身进行模糊,这并不符合 Treeland 的需求。

    Treeland 模糊控件

    Treeland 模糊控件

    Treeland 重新实现了模糊组件,能从显存里获取组件下方图像数据,再使用融合的模糊算法优化性能。

    阴影

    image

    Qt Quick 有一个 BorderImage 组件,它能以九宫格的方式,四个角保持不变,四边和中间的部分拉伸,来达到在组件底部充当装饰的效果。并通过 ImageProvider 的机制,手动控制图片资源的创建。

    Treeland 使用 BorderImage 作为窗口阴影贴图,通过 ImageProvider 手动创建贴图材质,在相同大小下可复用同一份材质。

    动画

    Treeland 直接使用 QtQuick 提供的动画组件,来为界面提供动画效果。

    使用 State 和 Transition 为组件定义属性变化,例如窗口最大化和还原,两个 State 切换会触发不同的 Transition 执行属性变化,在属性变化时,使用 QtQuick 的动画组件完成动画播放。

    多用户

    Treeland 作为解决方案的一部分,目的之一就是多用户共用合成器。在经典模式下,不同用户的切换需要在 tty 层面前端程序转移控制权,每个用户独占一个 tty 进行画面上屏。但切换tty所需的工作量不小,这导致切换时屏幕的缓冲区被不同的程序覆盖,给人的观感就是屏幕闪烁了一下,甚至是黑屏一会儿。

    DDM

    imageimage

    通过上图可以看出,LightDM模式下,每个用户拥有完整的一套进程组,都需要运行窗管、任务栏、文件管理器等。不同的用户会单独占用一个 tty,那么用户切换时,必然伴随着底层 DRM 以及显卡驱动等操作的切换,带来的结果就是会看到闪黑屏,而且两个用户都要跑一个锁屏界面来维持”假装是同一个界面“,也带来了跨用户进程的信息同步难题。

    而 DDM 和 Treeland 重新设计了工作流程,将 Treeland 单独抽离出来,每个用户都通过相同的一套机制将窗口画面发送给 Treeland,而 Treeland 负责最终的画面合成以及上屏。

    带来的好处显而易见,内存方面节省了窗管、锁屏等进程,切换用户也不会有黑屏闪烁,状态也不用想办法同步了。

    一个简化的 DDM 与 Treeland 的多用户登录流程

    一个简化的 DDM 与 Treeland 的多用户登录流程

    与 systemd 的集成

    DDE 的每一个用户会话,都已切换至 systemd 服务,而非所有进程都挂载到会话入口的服务上。这样做有很多好处,包括快速重启桌面环境,而非注销再登录。远程桌面的铺垫,会话的启动不再局限于本地会话。

    在 Treeland 模式下,DDE 会加载一个单独的服务,用于为用户会话注入显示环境变量。该服务使用 systemd 提供的 socket 机制实行懒加载,当 DDE 需要显示窗口时,即时向 Treeland 注册,完成用户显示服务的初始化。当 Treeland 崩溃重启时,该服务也会等待 Treeland 启动完成,并再次连接回 Treeland,确保用户侧的窗口能正常显示。

    多用户登录时的基本流程

    多用户登录时的基本流程

    总结

    本文介绍了深度操作系统(DDE)在窗口管理器方面的演进,从早期使用Mutter和Metacity,到后来采用KWin,最终决定开发自己的窗口管理器Treeland。Treeland基于wlroots和Qt技术栈,实现了更好的性能和更统一的技术框架。

    本文概述了 Treeland 的技术架构、界面效果优化(如圆角、模糊、阴影等),以及其在多用户场景下的优势。此外,还介绍了Treeland与systemd的集成,展示了DDE在系统架构和用户体验方面的持续创新和改进。

    Friday, October 25, 2024

    DDE Qt 6.8 适配说明

    Qt 6.8 发布已经有一段时间了,各个发行版尝试移植 DDE 时发现包括 dde-shell 在内的几个组件存在比较明显的问题,DDE 小组进行了相关的紧急修复。由于 DDE 部分项目也在分叉维护的状态,为了方便各位移植人员有效进行移植,故在此罗列相关注意事项。

    注:笔者所测试的环境为 Arch Linux,下述为 2024/10/25 testing 仓库状态下的测试结论。若未另行说明,则下述涉及到的项目名称仍然使用了与 DDE 对应项目原始仓库的名称,而非各个发行版下的包名。

    [!NOTE] 2024/11/06更新:对于 dde-launchpad、dde-tray-loader、dde-shell 目前均有新的维护分支版本,部分版本中已包含了下述中涉及到的一些 patch 的修复。本博客目前只更新了实机验证可用的新 tag 版本,但你也可以尝试未验证但位于维护分支的新 tag。

    分支与 tag 说明

    因维护需要,对于部分 DDE 组件(dde-shell、dde-launchpad、dde-tray-loader),我们对 deepin 23 所使用的分支创建了名为 release/beige 的维护分支。也会在维护分支上打对应的维护更新用的 tag。

    由于 deepin 现阶段的提测流程需要对提测版本打 tag,故我们对主干(master)分支也会打 tag。为了在不与现行规范冲突的情况下尽可能表示区分,我们使用格式为 x.99.z 的 tag 标记此版本是尚在开发中的版本。开发中的 tag 版本事实上在满足一定条件下也可供外部使用,但我们不保证 x.99.z 中 z 位更新时的兼容性,故仍然建议优先使用 release/beige 上的 tag 版本。

    Qt 6 Wayland

    由于 dde-shell 的托盘加载部分(dde-tray-loader)使用了 Wayland(即便是 x11 环境也如此)实现应用的嵌入,故对 Qt 6 的 wayland 组件存在依赖。有下述两个 Patch 需要应用到 Qt 6 Wayland 组件之上:

    dde-shell

    Patch 说明

    升级至 Qt 6.8 后,dde-shell 可能存在面板无任何内容的情况,就于此问题,需要应用这个 patch:

    https://github.com/linuxdeepin/dde-shell/commit/46871c83cf8ecfcf83bf2fb49e1f09af997eca96

    版本建议

    • 若目标发行版原本在使用 1.0.0 版本,则建议至少更新到 1.0.2
    • 1.0.3 以上版本依赖 treeland-protocols 项目,进行打包即可,建议对齐打包后至少更新 dde-shell 至 1.0.4
    • dde-shell 主干分支存在 1.99.1,但包括此版本在内的主干分支已不再在任务栏提供启动器图标,故需要配合启动器主干分支使用(启动器暂无 1.99.z 版本)

    tl;dr:建议打包 treeland-protocols 后更新至至少 1.0.4

    treeland-protocols

    被 dde-shell 1.0.3 以上版本所依赖。

    版本建议

    建议打包/更新至 0.4.1

    dde-tray-loader

    Patch 说明

    任务栏托盘区域的弹出面板(例如点击时间组件后的面板)早期版本有位置不正确的问题,需要应用这个 patch: https://github.com/linuxdeepin/dde-tray-loader/commit/664b093b6a913764fedbac9110927f26978aa8c9 。最新版本(1.0.5 起)中已经修正相关问题。

    版本建议

    建议更新至 1.0.5

    dde-launchpad

    Patch 说明

    启动器的维护分支版本应该可以在无任何修改的情况下正常工作,尽管启动器小窗口模式的面板位置可能不对,但位置问题暂不计划在维护分支解决。

    启动器主干分支不存在上述问题,但主干分支暂无 1.99.z tag。

    版本建议

    在之前的移植过程中发现小窗口搜索结果界面可能存在显示错位问题,此问题已在 1.0.6 修复,故建议更新至 1.0.6

    dde-application-manager

    Patch 说明

    不需要 patch。

    版本建议

    一个 deepin 23 的所谓“特性”即,父进程启动的子进程一般会被识别归属为父进程,会导致例如在终端启动 vscode,打开的 vscode 窗口会和终端共用相同图标的问题。此问题已经在最新维护版本得到解决。直接更新dde-shell (>= 1.0.4) dde-application-manager(>=1.2.16)版本即刻解决。

    建议更新至 1.2.16

    Wednesday, August 14, 2024

    deepin v23 将于 2024 年 8 月 15 日发布,这里为大家简要描述本次更新中,DDE 所涉及的变更,以及我们的进一步计划。

    需注意,本文章是站在 DDE SIG 角度的,倾向于对 DDE 项目整体的技术内容进行描述,面向 DDE 开发者和对 DDE 开发感兴趣的读者,并非面向最终用户的特性概览文章。若您需要大众化的发布概览,请参阅 deepin 公众号、官方网站等提供的介绍文章。另外,如果你对 DDE 的移植感兴趣,请参阅另一篇侧重于移植相关事项的文章

    不同于 beta3 与 RC 所提供的发布说明,此次将整体介绍 DDE v23 相对 v20 的变化内容。

    变化较大的默认组件

    dde-dockdde-shell

    虽然观感上对用户的差异是任务栏整体的变化,但 dde-shell 项目所要承载的责任要远超于 dde-dockdde-shell 旨在提供一个桌面环境级的外壳程序,使编写 DDE 桌面组件变得更轻松。例如它提供了允许你指定组件的层级关系、确定放置的屏幕位置等功能,并确保相应功能在 x11 与 Wayland 环境的表象均一致。这将使得桌面组件不必针对应用编写重复的代码来实现与桌面环境强相关的功能,并使得后续 x11 向 Wayland 切换变得更方便。当前,dde-shell 面向用户呈现的唯一主要组件即任务栏,而预计在后续,OSD、通知中心、剪切板等组件也都会逐渐进行迁移,由 dde-shell 统一管理。

    另外,dde-shell 整体也从传统的 QtWidgets 项目变成了 QML 项目,这使得 GPU 加速可以被有效的利用,使得相应的界面交互、动效等更加流畅。关于“任务栏”这个 dde-shell 组件,也存在了较大变化。为了避免插件崩溃时连带整个任务栏组件一起崩溃的问题发生,任务栏区域采用了内嵌 Wayland 合成器的解决方案实现了相关逻辑。

    dde-launcherdde-launchpad

    dde-launchpad 事实上是第一个试水 QML 的 DDE 桌面组件,由于 dde-launcher 存在大量的内部 model 状态维护不正确的问题以及界面问题,dde-launchpad 则对其进行了整体重构并将整个界面改用 QML 技术进行构建。如 dde-shell 一节中所述,这将使得整体的流畅度得以提升。

    此外,由于 DDE 计划对应用程序进行相关的权限管控,dde-launchpad 也将应用程序列表的获取和管理从原本的 GIO 切换到了新的 dde-application-manager

    dde-application-manager

    dde-application-manager 是一个后台服务,为需要获取应用列表以及启动应用程序的组件(文管、启动器、任务栏等)提供与管理相关数据,为启动的应用与组件设置恰当的 cgroup、环境变量等信息。尽管当下而言 dde-application-manager 并无特殊之处,但其为后续实现应用程序权限管控的计划提供了空间。

    技术预览组件

    我们在 v23 的开发过程中引入了技术预览组件的概念,而原本位于技术预览组件的两个主要项目(启动器与 shell)均已离开了技术预览阶段,现以正式版的形式面向社区发布。而目前仍然位于技术预览阶段的项目即 TreeLand 与 deepin-im 了。

    为了使得 v23 顺利发布、尽早发布,我们在 RC 阶段即将精力完全投入在了现有组件的缺陷修复上,TreeLand 与 deepin-im 项目均暂无显著成果可供分享,后续我们会在恰当的时候为大家详细介绍这两个项目。

    后续计划

    如之前计划所安排,dde-shell 现已走出技术预览并承载了任务栏的显示职责。dde-shell 的初衷之一是使桌面环境更加模块化,在后续,会有更多桌面组件成为 dde-shell 的一部分。我们也欢迎社区开发者开始尝试使用 dde-shell 编写一些自己觉得有趣的东西,并与我们讨论对 shell 相关设计与接口的体验,一同完善 dde-shell。如果你对这相关的话题感兴趣,欢迎加入 DDE SIG 的 Matrix 群聊 (#dde:matrix.org) 之中来。

    接下来,wayland 会话支持也会变成主要目标,treeland 将会逐步继续完善,并在恰当的时机提供给大家。

    此外,为了方便非 deepin 发行版的用户和开发者使用 DDE,我们也仍然和上次一样提供了一篇移植注意事项博客。笔者也是 DDE 移植 SIG 的成员,对应的文章现已发布到 DDE 移植小组。如果您感兴趣,可在此阅读。如果你本身在参与 DDE 的移植工作,那么也欢迎你加入 DDE 移植小组(#dde-port:deepin.orghttps://t.me/ddeport)。

    最后,感谢你读到这里。如有任何问题,欢迎在我们的开发者群(#deepin-community:deepin.org)进行讨论。

    Wednesday, May 15, 2024

    deepin v23 RC 将于 2024 年 5 月 15 日发布,这里为大家简要描述本次更新中,DDE 所涉及的变更,以及我们的进一步计划。

    需注意,本文章倾向于对 DDE 项目整体的技术内容进行描述,面向 DDE 开发者和对 DDE 开发感兴趣的读者,并非面向最终用户的特性概览文章。另外,如果你对 DDE 的移植感兴趣,请参阅另一篇侧重于移植相关事项的文章

    变化较大的默认组件

    相比 deepin v23 beta3 而言,在于 deepin v23 RC 发布的 DDE 中,有一个项目得到了较大规模的重构,并随 RC 默认提供给各位用户。即:

    • dde-shell

    dde-shell 旨在提供一个桌面环境级的外壳程序,使编写 DDE 桌面组件变得更轻松。这个项目在 beta3 阶段为技术预览状态,而现阶段,dde-shell 则走出了技术预览,并取代了原本的 dde-dock 项目来展示任务栏组件。

    此外,需要注意的是,dde-launchpad 项目也转而默认提供 dde-shell 插件的形式来提供启动器组件。

    技术预览组件

    非技术用户请慎重启用技术预览功能

    deepin v23 beta2 时,我们提供了一个需要用户手动安装的 dcc-insider-plugin 插件,称为技术预览插件。这个组件旨在帮助用户方便的测试 deepin 未来版本中计划提供但仍不稳定的系统组件。需要注意的是,为了加快版本迭代速度,使 v23 首个稳定版可以更快面向用户发布,故这个组件在 RC 阶段并未进行过较多测试,因而可能不会按预期行为工作,故我们目前不建议您使用此插件。

    • treeland / ddm
    • deepin-im

    treeland 与 deepin-im 这两个组件在 beta3 到 RC 的阶段中并无太大显著变化。其中,treeland 项目原本提供了合成器与 DM 两个功能,而现在,treeland 与 DM 也进行了拆分,后者被拆分到了 ddm 仓库之中。

    后续计划

    如之前计划所安排,dde-shell 现已走出技术预览,但 dde-shell 仍有很多需要进一步完善的地方。一些用户会注意到 dde-shell 版的任务栏缺失了一些原本 dde-dock 项目所提供的功能,此类特性会后续逐步补充上来。另外 dde-shell 也会继续向原本的目标迈进,我们会进一步将其他桌面组件转换为 dde-shell 插件的形式进行维护。使得桌面环境模块化之余,也为 wayland 支持作出更好的准备。

    另外,由于 dde-shell 的初衷之一是使桌面环境更加模块化,我们也欢迎社区开发者开始尝试使用 dde-shell 编写一些自己觉得有趣的东西,并与我们讨论对 shell 相关设计与接口的体验,一同完善 dde-shell。如果你对这相关的话题感兴趣,欢迎加入 DDE SIG 的 Matrix 群聊 (#dde:matrix.org) 之中来。

    接下来,wayland 会话支持也会变成主要目标(尽管 v23 首个正式版仍然可能不会提供 wayland 会话支持),treeland 与 ddm 将会逐步继续完善,并在恰当的时机提供给大家。

    此外,由于 DDE 在 beta3 与 RC 的变化仍然较大,我们也仍然和上次一样提供了一篇移植注意事项博客。笔者也是 DDE 移植 SIG 的成员,对应的文章现已发布到 DDE 移植小组。如果您感兴趣,可在此阅读。如果你本身在参与 DDE 的移植工作,那么也欢迎你加入 DDE 移植小组(#dde-port:deepin.orghttps://t.me/ddeport)。

    最后,感谢你读到这里。如有任何问题,欢迎在我们的开发者群(#deepin-community:deepin.org)进行讨论。

    Monday, March 18, 2024

    技术背景

    顾名思义,内存压缩就是压缩内存,节省内存空间。相信对于搞技术的人来说,压缩这个词并不陌生,一想到这个词,我们首先想到的是压缩可以降低占用空间,使同样的空间可以存放更多的东西。 内存无论多大,总是会有不够用的时候,或者说我们总是想在尽量低的成本控制下,达到系统最优,这个时候还是有必要引入诸如内存压缩的功能来优化系统内存占用。当系统内存紧张的时候,会将文件页丢弃或回写回磁盘(如果是脏页),还可能会触发LMK杀进程进行内存回收。这些被回收的内存如果再次使用都需要重新从磁盘读取,而这个过程涉及到较多的IO操作。 就目前的技术而言,IO的速度远远慢于这RAM操作速度。因此,如果频繁地做IO操作,不仅影响flash使用寿命,还严重影响系统性能。内存压缩是一种让IO过程平滑过渡的做法, 即尽量减少由于内存紧张导致的IO,提升性能。

    image

    图:内存管理大体框架(内存压缩技术处于内存回收memory reclaim部分中)

    主流内存压缩技术

    目前linux内核主流的内存压缩技术主要有3种:zSwap, zRAM, zCache。下面我们依次对几种方式进行一个简要的说明

    zSwap

    zSwap是在memory与flash之间的一层“cache”,当内存需要swap出去磁盘的时候,先通过压缩放到zSwap中去,zSwap空间按需增长。达到一定程度后则会按照LRU的顺序(前提是使用的内存分配方法需要支持LRU)将就最旧的page解压写入磁盘swap device,之后将当前的page压缩写入zSwap。

    zswap本身存在一些缺陷或问题:

    1. 如果开启当zswap满交换出backing store的功能, 由于需要将zswap里的内存按LRU顺序解压再swap out, 这就要求内存分配器支持LRU功能。
    2. 如果不开启当zswap满交换出backing store的功能, 和zRam是类似的。

    zRram

    zRram即压缩的内存, 使用内存模拟block device的做法。实际不会写到块设备中去,只会压缩后写到模拟的块设备中,其实也就是还是在RAM中,只是通过压缩了。由于压缩和解压缩的速度远比读写IO好,因此在移动终端设备广泛被应用。zRam是基于RAM的block device, 一般swap priority会比较高。只有当其满,系统才会考虑其他的swap devices。当然这个优先级用户可以配置。

    zRram本身存在一些缺陷或问题:

    1. zRam大小是可灵活配置的, 那是不是配置越大越好呢? 如果不是,配置多大是最合适的呢?
    2. 使用zRam可能会在低内存场景由于频繁的内存压缩导致kswapd进程占CPU高, 怎样改善?
    3. 增大了zRam配置,对系统内存碎片是否有影响?

    要利用好zRam功能, 并不是简单地配置了就OK了, 还需要对各种场景和问题都做好处理, 才能发挥最优的效果。

    zCache

    zCache是oracle提出的一种实现文件页压缩技术,也是memory与block dev之间的一层“cache”,与zswap比较接近,但zcache目前压缩的是文件页,而zSwap和zRAM压缩是匿名页。

    zcache本身存在一些缺陷或问题:

    1. 有些文件页可能本身是压缩的内容, 这时可能无法再进行压缩了;
    2. zCache目前无法使用zsmalloc, 如果使用zbud,压缩率较低;
    3. 使用的zbud/z3fold分配的内存是不可移动的, 需要关注内存碎片问题;

    内存压缩主流的内存分配器

    Zsmalloc

    zsmalloc是为ZRAM设计的一种内存分配器。内核已经有slub了,为什么还需要zsmalloc内存分配器?这是由内存压缩的场景和特点决定的。zsmalloc内存分配器期望在低内存的场景也能很好地工作,事实上,当需要压缩内存进行zsmalloc内存分配时,内存一般都比较紧张且内存碎片都比较严重了。如果使用slub分配, 很可能由于高阶内存分配不到而失败。另外,slub也可能导致内存碎片浪费比较严重,最坏情况下,当对象大小略大于PAGE_SIZE/2时,每个内存页接近一半的内存将被浪费。

    实测发现,anon pages的平均压缩比大约在1:3左右,所以compressed anon page size很多在1.2K左右。如果是Slub,为了分配大量1.2K的内存,可能内存浪费严重。zsmalloc分配器尝试将多个相同大小的对象存放在组合页(称为zspage)中,这个组合页不要求物理连续,从而提高内存的使用率。

    image

    需要注意的是, 当前zsmalloc不支持LRU功能, 旧版本内核分配的不可移动的页, 对内存碎片影响严重, 但最新版本内核已经是支持分配可移动类型内存了。

    Zbud

    zbud是一个专门为存储压缩page而设计的内存分配器。用于将2个objects存到1个单独的page中。zbud是可以支持LRU的, 但分配的内存是不可移动的。

    Z3fold

    z3fold是一个较新的内存分配器, 与zbud不同的是, 将3个objects存到1个单独的page中,也就是zbud内存利用率极限是1:2, z3fold极限是1:3。同样z3fold是可以支持LRU的, 但分配的内存是不可移动的。

    内存压缩技术与内存分配器组合

    结合上面zSwap / zRam /zCache的介绍, 与zsmalloc/zbud/z3fold分别怎样组合最合适呢? 下面总结了一下, 具体原因可以看上面介绍的时候各类型的特点。

    对比项zsmalloczbudz3fold
    zSwap(有实际swap device)×(不可用)√(可用)√(最佳)
    zSwap(无实际swap device)√(最佳)√(可用)√(可用)
    zRam√(最佳)√(可用)√(可用)
    zCache×(不可用)√(可用)√(最佳)

    zRAM技术原理

    zRam内存压缩技术是目前移动终端广泛使用的内存压缩技术。

    软件框架

    下图展示了内存管理大体的框架, 内存压缩技术处于内存回收memory reclaim部分中。

    image

    再具体到zRam, 它的软件架构可以分为3部分:数据流操作,内存压缩算法 ,zram驱动。 image

    实现原理

    Zram内存压缩技术本质上就是以时间换空间。通过CPU压缩、解压缩的开销换取更大的可用内存空间。

    我们主要描述清楚下面这2个问题: 1.什么时候会进行内存压缩? 2.进行内存压缩/解压缩的流程是怎样的?

    进行内存压缩的时机:

    1. Kswapd场景:kswapd是内核内存回收线程, 当内存watermark低于low水线时会被唤醒工作, 其到内存watermark不小于high水线。
    2. Direct reclaim场景:内存分配过程进入slowpath, 进行直接行内存回收。 image

    下面是基于4.4内核理出的内存压缩、解压缩流程。 内存回收过程路径进行内存压缩。会将非活跃链表的页进行shrink, 如果是匿名页会进行pageout, 由此进行内存压缩存放到ZRAM中, 调用路径如下:

    image

    在匿名页换出到swap设备后, 访问页时, 产生页访问错误, 当发现“页表项不为空, 但页不在内存中”, 该页就是已换到swap区中,由此会开始将该页从swap区中重新读取, 如果是ZRAM, 则是解压缩的过程。调用路径如下: image

    内存压缩算法

    目前比较主流的内存算法主要为LZ0, LZ4, ZSTD等。下面截取了几种算法在x86机器上的表现。各算法有各自特点, 有以压缩率高的, 有压缩/解压快的等, 具体要结合需求场景选择使用。

    image

    zRAM技术应用

    本节描述一下在使用ZRAM常遇到的一些使用或配置,调试的方法。

    如何配置开启zRAM

    配置内存压缩算法

    下面例子配置压缩算法为lz4

    echo lz4 > /sys/block/zram0/comp_algorithm
    

    配置ZRAM大小

    下面例子配置zram大小为2GB

    echo 2147483648 > /sys/block/zram0/disksize
    

    使能zram

    mkswap /dev/zram0

    swapon /dev/zram0

    zRAM块设备个数设定

    如果是编译为内核模块,那么可以在内核模块加载的时候,添加参数:insmod zram.ko num_devices=4

    也可直接修改内核源代码,代码地址为: /drivers/block/zram/zram_drv.c

    /* Module params (documentation at end) */
    static unsigned int num_devices = 1;
    

    修改num_devices为你想要的zram个数即可

    压缩流的最大个数设定

    这个是 3.15 版本及以后的 kernel 新加入的功能,3.15 版本之前的 zram 压缩都是使用一个压缩流(缓存 buffer 和算法私有部分)实现,每个写(压缩)操作都会独享压缩流,但是单压缩流如果出现数据奔溃或者卡住的现象,所有的写(压缩)操作将一直处于等待状态,这样效率非常低;而多压缩流的架构会让写(压缩)操作可以并行去执行,大大提高了压缩的效率和稳定性。

    查看压缩流个数:默认是1,可以直接向proc文件写入,也可以直接更改代码方式来改变默认压缩流个数

    cat /sys/block/zram0/max_comp_streams
    

    设定压缩流个数:

    echo 3 > /sys/block/zram0/max_comp_streams
    

    其他参数

    NameAccessDescription
    disksizeRW显示和设置该块设备的内存大小
    initstateRO显示设备的初始化状态
    resetWO重置设备
    num_readsRO读数据的个数
    failed_readsRO读数据失败的个数
    num_writeRO写数据的个数
    failed_writesRO写数据失败的个数
    invalid_ioRO非页面大小对齐的I/O请求的个数
    max_comp_streamsRW最大可能同时执行压缩操作的个数
    comp_algorithmRW显示和设置压缩算法
    notify_freeRO空闲内存的通知个数
    zero_pagesRO写入该块设备的全为的页面的个数
    orig_data_sizeRO保存在该块设备中没有被压缩的数据的大小
    compr_data_sizeRO保存在该块设备中已被压缩的数据的大小
    mem_used_totalRO分配给该块设备的总内存大小
    mem_used_maxRW该块设备已用的内存大小,可以写 1 重置这个计数参数到当前真实的统计值
    mem_limitRWzram 可以用来保存压缩数据的最大内存
    pages_compactedRO在压缩过程中可用的空闲页面的个数
    compactWO触发内存压缩

    swappiness含义简述

    swappiness参数是内核倾向于回收匿名页到swap(使用的ZRAM就是swap设备)的积极程度, 原生内核范围是0~100, 参数值越大, 表示回收匿名页到swap的比例就越大。如果配置为0, 表示仅回收文件页,不回收匿名页。默认值为60。可以通过节点“/proc/sys/vm/swappiness”配置。

    zRam相关的技术指标

    zRAM大小及剩余空间

    Proc/meminfo 中可以查看相关信息 SwapTotal:swap 总大小, 如果配置为ZRAM, 这里就是ZRAM总大小 SwapFree:swap 剩余大小, 如果配置为ZRAM, 这里就是ZRAM剩余大小

    当然, 节点 /sys/block/zram0/disksize 是最直接的。

    zRAM压缩率

    /sys/block/zram/mm_stat 中有压缩前后的大小数据, 由此可以计算出实际的压缩率 orig_data_size:压缩前数据大小, 单位为bytes compr_data_size :压缩后数据大小, 单位为bytes

    换出/换入swap区的总量

    proc/vmstat 中中有相关信息 pswpin:换入总量, 单位为page pswout:换出总量, 单位为page

    zRam相关优化

    上面提到zRam的一些缺陷, 怎么去改善呢?

    1. zRam大小是可灵活配置的, 那是不是配置越大越好呢? 如果不是配置多大是最合适的呢? zRam大小的配置比较灵活, 如果zRam配置过大, 后台缓存了应用过多, 这也是有可能会影响前台应用使用的流畅度。另外, zRam配置越大, 也需要关注系统的内存碎片化情。因此zRam并不是配置越大越好,具体的大小需要根据内存总大小及系统负载情况考虑及实测而定。

    2. 使用zRam,可能会存在低内存场景由于频繁的内存压缩导致kswapd进程占CPU高, 怎样改善? zRam本质就是以时间换空间, 在低内存的情况下, 肯定会比较频繁地回收内存, 这时kswapd进程是比较活跃的, 再加上通过压缩内存, 会更加消耗CPU资源。改善这种情况方法也比较多, 比如, 可以使用更优的压缩算法, 区别使用场景, 后台不影响用户使用的场景异步进行深度内存压缩, 与用户体验相关的场景同步适当减少内存压缩, 通过增加文件页的回收比例加快内存回收等等。

    3. 增大了zRam配置,对系统内存碎片是否有影响? 使用zRam是有可能导致系统内存碎片变得更严重的, 特别是zsmalloc分配不支持可移动内存类型的时候。新版的内核zsmalloc已经支持可移动类型分配的, 但由于增大了zRam,结合android手机的使用特点, 仍然会有可能导致系统内存碎片较严重的情况,因些内存碎片问题也是需要重点关注的。解决系统内存碎片的方法也比较多, 可以结合具体的原因及场景进行优化。

    参考资料

    Linux内存压缩浅析之原理

    zRAM内存压缩技术原理与应用

    Sunday, January 28, 2024

    deepin v23 beta3 将于 2024 年 1 月 31 日发布(注:因存在部分临时设计变动,延期发布,仍计划在当周发布),这里为大家简要描述本次更新中,DDE 所涉及的变更,以及我们的进一步计划。另需注意,本文章倾向于对 DDE 项目整体的技术内容进行描述,面向 DDE 开发者和对 DDE 开发感兴趣的读者,并非面向最终用户的特性概览文章。

    变化较大的默认组件

    相比 deepin v23 beta2 而言,在于 deepin v23 beta3 发布的 DDE 中,有两个项目得到了较大规模的重构,并随 beta3 默认提供给各位用户。这两个项目分别是:

    • dde-application-manager (>= 1.2)
    • dde-launchpad (取代 dde-launcher)

    dde-application-manager 承载了 DDE 下应用程序的启动与管理等职责,在早期版本中存在诸多架构不合理以及实现问题,影响到了 dde-launcher、dde-dock、dde-grand-search 等项目启动应用与管理应用功能的正常使用。于是我们对 dde-application-manager 进行了大规模的重构,也重新设计了此项目的对外 D-Bus 接口。在 beta3 中,你将不再会遇到类如应用频繁集体闪退的问题,对应用 desktop 文件的支持变得更加完整。在未来,也会对实时调整应用缩放等功能预留支持的空间。

    dde-launchpad 则是对 dde-launcher 的完整重写。一方面,dde-launcher 对旧的 dde-application-manager 具有很高的偶合度,另一方面,旧的 dde-launcher 架构使得维护此组件变得非常困难。dde-launchpad 是目前 DDE 的首个基于 QML 的组件,且基于最新的 Qt 6。得益于 QML 技术,launchpad 将会有效利用 GPU 绘制界面,使交互体验更加流畅。不过由于 dde-launchpad 是完全重写版本,故尚有部分功能未在当前的 dde-launchpad 中提供。

    技术预览组件

    非技术用户请慎重启用技术预览功能

    deepin v23 beta2 时,我们提供了一个需要用户手动安装的 dcc-insider-plugin 插件,称为技术预览插件。这个组件旨在帮助用户方便的测试 deepin 未来版本中计划提供但仍不稳定的系统组件。在 beta2 时,技术预览组件仅包含了 dde-launchpad 一项。而在 beta3 中,dde-launchpad 走出了技术预览阶段,而有更多的组件进入了技术预览阶段:

    • treeland
    • dde-shell
    • deepin-im

    treeland 是 deepin 的下一代 wayland 窗口合成器,且可以同时提供会话管理功能。由于 treeland 会替换 deepin-kwin 并同样会接管 lightdm,故切换 treeland 时需要格外留意。

    切换到 treeland 后,将会进入的 DDE 会话也会与原本有差异。treeland 会话中会使用 dde-shell 而非原本的 dde-dock、dde-widgets 等组件。尽管 dde-shell 可以独立使用,但目前 dde-shell 是作为与 treeland 共同配合进入技术预览阶段的桌面组件。dde-shell 提供了与 wayland 会话下的桌面环境的相关功能集成,今后的 dock 等组件也均会作为 dde-shell 的插件存在,以便提供更深度的集成。

    deepin-im 是新的输入法组件,作为目前各种底层输入法框架的抽象层存在,以便用户更方便的管理输入法而无需关注底层的配置细节。deepin-im 并不与 treeland 绑定,可以单独启用。

    后续计划

    在未来,首要计划即使 dde-shell 可以走出技术预览阶段并配合 treeland 达成真正稳定可用的 wayland 会话,这意味着 treeland 与 dde-shell 相关的项目还有比较多的相关工作需要持续进行。也是我们的主要努力方向。另外,尽管 dde-shell 相关的 API 可能仍会存在一些变动,但我们计划在随后提供一些必要的文档来帮助开发者更好的了解 dde-shell 的目的与作用,以便帮助开发者参与到项目之中来。如果你对这相关的话题感兴趣,欢迎加入 DDE SIG 的 Matrix 群聊 (#dde:matrix.org) 之中来。

    另外,由于 DDE 在 beta2 与 beta3 的变化较大,我们也计划提供一篇移植注意事项博客。笔者也是 DDE 移植 SIG 的成员,于是对应的文章计划会发布到 DDE 移植小组。如果您感兴趣,请考虑订阅 <planet.deepin.org> 的 RSS 更新。如果你本身在参与 DDE 的移植工作,那么也欢迎你加入 DDE 移植小组(#dde-port:deepin.orghttps://t.me/ddeport)。

    最后,感谢你读到这里。如有任何问题,欢迎在我们的开发者群(#deepin-community:deepin.org)进行讨论。