• 首页
  • 加入
  • RSS
  • Wednesday, May 14, 2025

    起因

    之前已经写过一篇使用 distrobox 加速开发的文章,主要动机就是在面对不同版本的系统时,准备多份开发环境将变得十分痛苦,最终就演变成了基于 VSCode 的 Remote 开发搭配容器环境。

    这样只需要准备一份 base 环境,每次只需要设置环境版本就可以安装对应的开发依赖,并且不需要更换开发工具,甚至可以在环境变量中设置好 DISPLAY 等,还能直接让容器内的程序显示在宿主环境上。

    本篇文章是基于 podman 的技术方案执行的。

    环境准备

    安装 podman

    sudo pacman -Sy podman

    确认配置

    cat /etc/subuid

    内容应该是 lxz:100000:65536

    VSCode

    打开 VScode 的设置,搜索 container,将 docker 的二进制改成 podman。

    .devcontainer.json

    VSCode 会识别打开目录下的 .devcontainer 目录和 .devcontainer.json 文件,本文会先介绍 json 格式的配置,之后会介绍使用 Dockerfile 构建运行环境的方式。

    {
    "name": "UOS Dev Container",
    "containerUser": "root",
    "image": "uos:base",
    "customizations": {
    "vscode": {
    "extensions": [
    "llvm-vs-code-extensions.vscode-clangd",
    "donjayamanne.git-extension-pack",
    "donjayamanne.githistory",
    "theqtcompany.qt-cpp-pack",
    "eamodio.gitlens",
    "ms-vscode.cmake-tools",
    "go2sh.cmake-integration-vscode",
    "josetr.cmake-language-support-vscode",
    "VisualStudioExptTeam.vscodeintellicode",
    "VisualStudioExptTeam.intellicode-api-usage-examples",
    "esbenp.prettier-vscode",
    "ms-vscode.cpptools",
    "twxs.cmake"
    ]
    }
    },
    "runArgs": [
    "--rm",
    "--network=host",
    "--privileged",
    "--userns=host"
    ],
    "remoteEnv": {
    },
    "onCreateCommand": "apt update && apt install -y clangd-13 ninja-build curl jq && update-alternatives --install /usr/bin/clangd clangd /usr/bin/clangd-13 13 && echo > /etc/apt/sources.list",
    "postCreateCommand": "echo \"请输入系统版本:\" && read version && sh -c \"curl -s 'https://xxx/api/shuttle/repo/info?type=deb&reponame=eagle-$version' | jq -r '.data.repobase.archives[].address' | tee /etc/apt/sources.list\" && echo 'deb [trusted=yes] https://xxx/pkg/eagle-1073/release-candidate/MDQyNeeql-euoeS4u-e6v-aPkOa1izIwMjUtMDQtMjUgMTQ6NTk6MDE/ unstable main' >> /etc/apt/sources.list && apt update && apt build-dep -y . && apt install -y qt5-default qtwayland5-private-dev"
    }

    在 json 中,需要指定一个运行时环境,在这里使用的是本地手动准备的镜像,为了确保开发和服务器构建环境的一致,我直接使用了服务器提供的构建环境,并使用 podman image import 命令导入。

    runArgs 中 --userns=host 的作用是使用主机的 user namespace,而 containerUser 必须指定成 root,因为容器内没有别的用户。

    为了防止泄露内网信息,postCreateCommand 内已将域名屏蔽。

    在 UOS 中,clangd 的版本有点低,需要手动创建符号,否则 VSCode 的 clangd 插件无法正常工作。这个事情就放在 onCreateCommand 中执行了,postCreateCommand 则是配置仓库和安装项目的构建依赖。

    当打开项目后,VSCode 会提醒在容器内重新打开。

    容器内打开容器内打开容器内打开

    之后等待 VSCode 完成 server 的下载,就会开始执行 onCreateCommandpostCreateCommand

    启动完毕后就会开始安装插件,是一个正常的工作环境了。

    容器内打开容器内打开

    打开终端,可以看到是 root 用户,但 podman 做了 uid 映射,实际目录的权限会是用户自身,不必担心。

    容器内打开容器内打开

    Dockerfile

    通常情况下应当使用 image,这样能确保所有人的环境是一致的,但仍然有时候想在 Dockerfile 里提前准备好一些事情,而不是在容器启动后进行配置,只需要把 image 字段改成 build。

    "build": { "dockerfile": "Dockerfile" },

    这种情况更推荐放在 .devcontainer 目录。在这个目录下,可以放置 Dockerfile 和其他配置文件,以便在构建容器时使用。

    FROM --platform=$TARGETPLATFORM hub.deepin.com/kwin-devcontainer/linuxdeepin/eagle:base

    ARG USERNAME=user
    ARG UID=1000
    ARG GID=1000
    ARG UOS_VERSION=1073

    RUN groupadd --gid $GID $USERNAME && \
    useradd --uid $UID --gid $GID -m $USERNAME

    RUN apt-get update && \
    apt-get install -y \
    curl \
    jq \
    && \
    apt-get dist-upgrade -y && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

    RUN apt-get update && \
    apt-get install -y \
    sudo \
    clangd-13 \
    apt-utils \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release \
    && \
    update-alternatives --install /usr/bin/clangd clangd /usr/bin/clangd-13 13 && \
    echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers && \
    apt-get dist-upgrade -y && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

    USER $USERNAME

    json 也稍微修改了一下,使用 Dockerfile,并且使用参数控制一些构建过程,并且增加了本地用户。

    {
    "name": "UOS Dev Container",
    "remoteUser": "lxz",
    "build": {
    "dockerfile": "Dockerfile",
    "args": {
    "UOS_VERSION": "1073",
    "USERNAME": "lxz"
    }
    },
    "customizations": {
    "vscode": {
    "extensions": [
    "llvm-vs-code-extensions.vscode-clangd",
    "donjayamanne.git-extension-pack",
    "donjayamanne.githistory",
    "theqtcompany.qt-cpp-pack",
    "eamodio.gitlens",
    "ms-vscode.cmake-tools",
    "go2sh.cmake-integration-vscode",
    "josetr.cmake-language-support-vscode",
    "VisualStudioExptTeam.vscodeintellicode",
    "VisualStudioExptTeam.intellicode-api-usage-examples",
    "esbenp.prettier-vscode",
    "saoudrizwan.claude-dev"
    ]
    }
    },
    "runArgs": [
    "--rm",
    "--network=host",
    "--privileged",
    "--env",
    "XDG_RUNTIME_DIR=${env:XDG_RUNTIME_DIR}",
    "-v",
    "${env:SSH_AUTH_SOCK}:/tmp/ssh-agent.socket",
    "-e",
    "SSH_AUTH_SOCK=/tmp/ssh-agent.socket"
    ],
    "remoteEnv": {
    "XDG_RUNTIME_DIR": "${env:XDG_RUNTIME_DIR}",
    "PODMAN_USERNS": "host"
    },
    "postCreateCommand": "sudo apt update && sudo apt build-dep -y . && sudo apt install -y qt5-default qtwayland5-private-dev"
    }

    https://code.visualstudio.com/docs/devcontainers/create-dev-container

    Tuesday, April 22, 2025

    最近遇到了两起 X11 下客户端报 Maximum number of clients reached 的错误,并且无法连接到 Xorg。

    搜到了一个排查方法,执行

    sudo ss -x src "*/tmp/.X11-unix/*" | grep -Eo "[0-9]+\s*$" | while read port
    do
    sudo ss -p -x | grep -w $port | grep -v X11-unix
    done | grep -Eo '".+"' | sort | uniq -c | sort -rn

    这将列出每个进程及其打开的 X 连接数,按连接数降序排列。

    13 "dde-session-dae"
    5 "fcitx"
    3 "kwin_x11"
    3 "kglobalaccel5"
    2 "cpis-panel-serv"
    1 "uos-activator"
    1 "startdde"

    https://snapoverflow.com/ubuntu/question/4499/how-can-i-diagnose-debug-maximum-number-of-clients-reached-x-errors/

    Wednesday, April 16, 2025

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

    deepin 23.1 现已发布。为了方便各个其它发行版的包维护者可以更方便的移植 DDE 到对应的发行版,这里提供一篇简要的移植指南,用以描述常见的移植问题和解决方案。

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

    概览

    对于 DDE 本次更新并未包含大规模的结构调整,而是比较存催的缺陷修复为主的更新,对于比较值得注意的事项将会列在下方。对于 deepin 23 的注意事项,可参见 deepin 23 正式版发布时的移植指南文章所给出的说明。

    由于 DDE 涉及到的各个组件项目的版本间互相影响,我们强烈建议移植人员参照 deepin 23.1 正式版所使用的包版本进行打包(也务必遵循依赖顺序打包)。下面会对主要的部分进行详细说明。

    下面给出的版本号信息供打包移植时参考。若您需要获取 ISO 镜像中使用的确切软件版本列表,请挂载 ISO 后参阅 LIVE/FILESYSTEM.MANIFEST (也可能是 LIVE/FILESYS0.MAN)路径对应的文件的内容。

    主要组件

    DTK 与 DTK6

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

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

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

    deepin-kwin wayland 功能已经废弃,未来将由 treeland 替代。目前 dwayland 包已经不再使用,依赖此包的应用比如 qt5platform-plugins,不应该继续编译依赖 dwayland 的功能,可参照 linuxdeepin/developer-center#7217 打对应的 patch 规避。

    目前,使用 dtk6 的正式组件有 dde-application-manager,dde-launchpad 与 dde-shell。需要注意,deepin 23 环境中 dde-shell 的托盘组件 dde-tray-loader 仍然需要使用 qt5。

    DDE 主要组件

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

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

    packageversion
    deepin-osconfig2024.08.06
    dde-app-services1.0.27
    dde-session1.2.13
    dde-application-manager1.2.27
    dde-tray-loader1.0.10
    dde-shell1.0.10
    dde-launchpad1.0.11
    dde-application-wizard0.1.10
    deepin-wayland-protocols1.10.0.28
    deepin-kwin5.27.2.213
    dde-launcher被 dde-launchpad 取代,不再使用
    dde-dock被 dde-shell 取代,不再使用

    dde-application-manager

    此组件现已使用主干分支最新版本(当前为 1.2.27)。请注意,较早的主干版本(例如 1.2.26)在 deepin 23 环境存在一些已知行为问题,故移植最新的 deepin 23 DDE 时,请至少使用 1.2.27 版本。

    dde-session-shell

    尽管此组件不存在架构性质层面的较大调整,但涉及到打包移植相关的注意事项。此组件由于主干分支的研发需求,对仓库进行过迁移到。当前 GitHub 上的 linuxdeepin/dde-session-shell 仓库历史已与之前不同。故如果你需要适用于 deepin 23 的此仓库的完整历史,请转到 dde-session-shell-snipe。所有原始仓库的提交历史以及 tag 均可在这个仓库中找到(实质是仓库重命名后新建了与原名的同名仓库)。

    (注:相关请参见此邮件列表存档

    Qt 6.9 编译问题

    如果遇到 qmlsc 崩溃问题,见 QTBUG-135885QTBUG-135885,需要为 qtdeclarative 增加以下 patch:

    技术预览组件

    原本涉及的技术预览组件在 23 至 23.1 的这个阶段均无较大进展,故不再于此罗列。

    获取移植帮助

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

    Friday, March 21, 2025

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

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

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

    概览

    相对于 deepin 25 preview,在 deepin 25 alpha 中并不存在较大幅的架构调整,而是以缺陷修复以及完善之前尚未完善但计划涵盖在最终版本的组件(例如 QML 版控制中心)作为研发的重心。同时,我们也对 Qt、DTK 进行了更多完善,以供 DDE 组件以及 Treeland 能够更好的运行。

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

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

    主要组件

    DTK 与 DTK6

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

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

    除 dtklog 以及 dtk6 的 core 于 declarative 外,本次 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.11
    dde-application-manager1.2.26
    dde-shell1.99.28
    dde-launchpad1.99.9
    dde-tray-loader1.99.19
    dde-application-wizard0.1.13
    dde-clipboard6.1.6
    dde-launcher被 dde-launchpad 取代,不再使用
    dde-dock被 dde-shell 取代,不再使用

    dde-application-manager

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

    dde-shell

    dde-shell 旨在将 DDE 桌面环境插件化与模块化,降低开发难度,使各个组件的替换变得更加容易,并且提供更好的桌面环境集成支持。alpha 阶段相比 preview 阶段集中在缺陷的修复上,并未涵盖太多的结构调整和新特性。对于 alpha 以及更早版本的变化,请阅读之前的博客文章。

    为保障 dde-shell 在 Qt 6.8.0 或 6.8.1 的环境可以正常运行(即使是X11环境下),若 ,则 必须 给 qtwayland 打下面的 patch:

    另外,dde-shell 在 alpha 中为修正一个特定问题所包含的一个变更依赖另一个 Qt Wayland 的 patch:

    若你所移植的目标发行版不接受此补丁,则可考虑对 dde-shell 项目 revert 于此相关的对应 commit:

    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.13
    vioken/qwlroots0.5.3
    treeland0.5.20
    ddm0.1.10

    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.2。

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

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

    另外,如果你的发行版所附的 Qt 6.8 版本并未更新至 Qt 6.8.2,则可能需要打三个额外的补丁,可参见 DDE Qt 6.8 适配说明(针对 Qt 6.8.0) 以及 deepin 25 preview DDE 移植简要指南(针对 Qt 6.8.1)

    获取移植帮助

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

    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