From e4a7e52f811601390ca40a98a432051c48d71c38 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:10:16 -0400 Subject: [PATCH] Fix nested KWin rendering on Android (#1330) Select the explicitly configured Android render node for nested KWin 6.8, apply OpenGL settings before Plasma starts, and pin the original signed Mesa 26.2.4 package that passed Thor rendering. Remove build-directory runtime search paths and fail closed on ELF inspection errors. Native ARM64 build passed. Thor validated a visible nested Wayland terminal, active OpenGL ES/Zink on Turnip, 1280x720 output, and keyboard input. Mesa pin regression tests, launcher and ELF failure controls, shell syntax, and human authorship checks passed. Published desktop pin remains r2; full latest-head CI is pending and maintainer authorized this merge after device validation. --- tools/desktop-kde/README.md | 16 ++++- tools/desktop-kde/build-kwin.sh | 20 ++++-- tools/desktop-kde/build.sh | 7 +- tools/desktop-kde/closure.py | 10 ++- tools/desktop-kde/mesa.env | 2 + .../usr/share/droiddeck/kwin-compositor | 1 + ...2-select-explicit-nested-render-node.patch | 23 +++++++ tools/desktop-kde/pins/mesa/desc | 38 +++++++++++ tools/linuxfs/desktop/droiddeck-desktop | 7 ++ tools/tests/test_desktop_mesa_pin.py | 68 +++++++++++++++++++ 10 files changed, 180 insertions(+), 12 deletions(-) create mode 100644 tools/desktop-kde/mesa.env create mode 100644 tools/desktop-kde/overlay/usr/share/droiddeck/kwin-compositor create mode 100644 tools/desktop-kde/patches/0002-select-explicit-nested-render-node.patch create mode 100644 tools/desktop-kde/pins/mesa/desc create mode 100644 tools/tests/test_desktop_mesa_pin.py diff --git a/tools/desktop-kde/README.md b/tools/desktop-kde/README.md index b76457f60..f2303b26f 100644 --- a/tools/desktop-kde/README.md +++ b/tools/desktop-kde/README.md @@ -18,9 +18,11 @@ is a new tag and a `release.env` update. At run time the desktop is started by `tools/linuxfs/desktop/droiddeck-desktop` (the app stages it at every session): Plasma's own `startplasma-wayland`, with KWin nested in the app's compositor -through `kwin_wayland_wrapper`. KWin composites in software (QPainter) - the Adreno node is not a -DRM device - so games and emulators from the menu go through `droiddeck-gpu`, and Steam goes back -to the app as a Steam session. +through `kwin_wayland_wrapper`. The package's `kwin-compositor` marker selects OpenGL through +Zink for KWin 6.8, which removed QPainter. Older packages retain their software compositor. +On Android the launcher explicitly selects the KGSL render node; the nested backend can use it +when the host's dmabuf feedback cannot name a DRM device. Games and emulators from the menu go +through `droiddeck-gpu`, and Steam goes back to the app as a Steam session. `kwin.env` pins the KWin version shared by the build and packaging jobs. `build-kwin.sh` builds that version's Wayland executable and library on a native ARM64 runner, using a disposable @@ -30,6 +32,14 @@ by SHA-256 from KDE's official source inventory; `patches/0001-respect-nested-de lets Display Settings control the inner desktop scale independently of the outer window size. It keeps layer sizes, pointer/touch coordinates and the cursor in the same physical pixels. The behavior is enabled only with `DROIDDECK_NESTED_SCALE`, which the desktop script supplies. +`0002-select-explicit-nested-render-node.patch` lets that nested backend use one explicitly +selected `KWIN_RENDER_NODES` device when host feedback cannot identify it. It does not guess +between GPUs or change normal KWin device selection. + +`pins/mesa/desc` keeps Mesa 26.2.4 and its actual package dependencies ahead of the rolling +repository metadata. Mesa 26.2.5 crashes in Zink shader compilation on Thor with the selected +Turnip driver. `mesa.env` points at immutable copies of the original 26.2.4 package and signature; +both build jobs verify the Arch Linux ARM signature and pinned SHA-256 as for every other package. The desktop workflow replaces the same-version `kwin_wayland` executable and `libkwin.so`; the rest comes from the signature-checked Arch packages. `desktop-kde.kwin.txt` records the source, patch, diff --git a/tools/desktop-kde/build-kwin.sh b/tools/desktop-kde/build-kwin.sh index c740d484a..cff8d075f 100644 --- a/tools/desktop-kde/build-kwin.sh +++ b/tools/desktop-kde/build-kwin.sh @@ -4,6 +4,7 @@ out=$(realpath -m "${1:?out dir}") here=$(cd "$(dirname "$0")" && pwd) [ "$(uname -m)" = aarch64 ] || { echo 'KWin needs a native aarch64 runner' >&2; exit 1; } source "$here/kwin.env" +source "$here/mesa.env" version=$DESKTOP_KWIN_VERSION source_sha=4bfaf31e29a8f9fdf231dc3bbb9b09b0ce07cae9fee7a01cfc86425ece796258 mirror=https://fl.us.mirror.archlinuxarm.org/aarch64 @@ -24,11 +25,12 @@ case $kwin_package in "extra/kwin-$version"-[0-9]*-aarch64.pkg.tar.*) ;; *) echo "KWin package $kwin_package does not match source KWin $version" >&2; exit 1 ;; esac -export work mirror builder here +export work mirror builder here DESKTOP_MESA_PACKAGE DESKTOP_MESA_URL download() { - local entry=$1 sum=$2 file=${1#*/} - fetch "$work/pkgs/$file" "$mirror/$entry" - fetch "$work/pkgs/$file.sig" "$mirror/$entry.sig" + local entry=$1 sum=$2 file=${1#*/} url=$mirror/$1 + [ "$file" != "$DESKTOP_MESA_PACKAGE" ] || url=$DESKTOP_MESA_URL + fetch "$work/pkgs/$file" "$url" + fetch "$work/pkgs/$file.sig" "$url.sig" gpgv --status-fd 1 --keyring "$here/archlinuxarm-builder.gpg" "$work/pkgs/$file.sig" "$work/pkgs/$file" 2>/dev/null \ | grep -Eq "^\[GNUPG:\] VALIDSIG .* $builder\$" echo "$sum $work/pkgs/$file" | sha256sum -c - >/dev/null @@ -49,19 +51,27 @@ sudo mknod -m 666 "$work/root/dev/urandom" c 1 9 sudo mount -t proc proc "$work/root/proc" sudo tar -xf "$work/kwin.tar.xz" -C "$work/root/build" sudo patch -d "$work/root/build/kwin-$version" -p1 < "$here/patches/0001-respect-nested-desktop-scale.patch" +sudo patch -d "$work/root/build/kwin-$version" -p1 < "$here/patches/0002-select-explicit-nested-render-node.patch" sudo chroot "$work/root" /usr/bin/env PATH=/usr/bin:/bin HOME=/root LC_ALL=C.UTF-8 \ /usr/bin/cmake -S "/build/kwin-$version" -B /build/out -G Ninja \ - -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBEXECDIR=lib -DBUILD_TESTING=OFF + -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBEXECDIR=lib \ + -DCMAKE_SKIP_RPATH=ON -DBUILD_TESTING=OFF sudo chroot "$work/root" /usr/bin/env PATH=/usr/bin:/bin HOME=/root LC_ALL=C.UTF-8 \ /usr/bin/cmake --build /build/out --target kwin_wayland --parallel 4 sudo cp "$work/root/build/out/bin/kwin_wayland" "$out/kwin_wayland" sudo cp "$work/root/build/out/bin/libkwin.so.$version" "$out/libkwin.so.$version" sudo chown "$(id -u):$(id -g)" "$out/kwin_wayland" "$out/libkwin.so.$version" strip --strip-unneeded "$out/kwin_wayland" "$out/libkwin.so.$version" +readelf -d "$out/kwin_wayland" "$out/libkwin.so.$version" > "$work/dynamic-tags.txt" +if grep -Eq '\((RPATH|RUNPATH)\)' "$work/dynamic-tags.txt"; then + echo 'KWin artifacts must not contain build-directory runtime search paths' >&2 + exit 1 +fi grep -aq DROIDDECK_NESTED_SCALE "$out/libkwin.so.$version" { echo "KWin $version source sha256 $source_sha" echo "Patch sha256 $(sha256sum "$here/patches/0001-respect-nested-desktop-scale.patch" | cut -d' ' -f1)" + echo "Render node patch sha256 $(sha256sum "$here/patches/0002-select-explicit-nested-render-node.patch" | cut -d' ' -f1)" echo "Executable sha256 $(sha256sum "$out/kwin_wayland" | cut -d' ' -f1)" echo "Library sha256 $(sha256sum "$out/libkwin.so.$version" | cut -d' ' -f1)" echo 'Build packages (Arch Linux ARM signatures verified):' diff --git a/tools/desktop-kde/build.sh b/tools/desktop-kde/build.sh index b434e81bc..b8d8a0efc 100755 --- a/tools/desktop-kde/build.sh +++ b/tools/desktop-kde/build.sh @@ -3,6 +3,7 @@ set -euo pipefail out=${1:?out dir} here=$(cd "$(dirname "$0")" && pwd) source "$here/kwin.env" +source "$here/mesa.env" mirror=${ALARM_MIRROR:-https://fl.us.mirror.archlinuxarm.org/aarch64} case $mirror in https://*) ;; *) echo "ALARM_MIRROR must be https" >&2; exit 1 ;; esac builder=68B3537F39A313B3E574D06777193F152BDBE6A6 @@ -21,8 +22,10 @@ echo "kde: $(wc -l < "$work/pkglist.txt") packages beyond the runtime's own" while read -r entry sum; do file=${entry#*/} - [ -s "$pkgs/$file" ] || fetch "$pkgs/$file" "$mirror/$entry" - fetch "$work/pkg.sig" "$mirror/$entry.sig" + url=$mirror/$entry + [ "$file" != "$DESKTOP_MESA_PACKAGE" ] || url=$DESKTOP_MESA_URL + [ -s "$pkgs/$file" ] || fetch "$pkgs/$file" "$url" + fetch "$work/pkg.sig" "$url.sig" if ! gpgv --status-fd 1 --keyring "$here/archlinuxarm-builder.gpg" "$work/pkg.sig" "$pkgs/$file" 2>/dev/null \ | grep -Eq "^\[GNUPG:\] VALIDSIG .* $builder\$"; then echo "$file: no valid Arch Linux ARM Build System signature" >&2; rm -f "$pkgs/$file"; exit 1 diff --git a/tools/desktop-kde/closure.py b/tools/desktop-kde/closure.py index f078a5068..2c74e299f 100755 --- a/tools/desktop-kde/closure.py +++ b/tools/desktop-kde/closure.py @@ -72,8 +72,12 @@ def main(): seeds = [l.strip() for l in open(seeds_file) if l.strip() and not l.lstrip().startswith("#")] pkgs, providers = {}, collections.defaultdict(list) - for repo in REPOS: - root = os.path.join(dbdir, "x_" + repo) + pins = os.path.join(os.path.dirname(__file__), "pins") + sources = [("extra", pins)] + sources.extend((repo, os.path.join(dbdir, "x_" + repo)) for repo in REPOS) + for repo, root in sources: + if not os.path.isdir(root): + continue for entry in sorted(os.listdir(root)): fields, key = {}, None for fname in ("desc", "depends"): @@ -92,6 +96,8 @@ def main(): name = fields.get("NAME", [None])[0] if not name or name in pkgs: continue + if root == pins: + runtime.pop(name, None) provides = [parse_dep(p) for p in fields.get("PROVIDES", [])] pkgs[name] = {"repo": repo, "file": fields["FILENAME"][0], "version": fields["VERSION"][0], "sha256": fields.get("SHA256SUM", [""])[0], diff --git a/tools/desktop-kde/mesa.env b/tools/desktop-kde/mesa.env new file mode 100644 index 000000000..e300d0d43 --- /dev/null +++ b/tools/desktop-kde/mesa.env @@ -0,0 +1,2 @@ +DESKTOP_MESA_PACKAGE='mesa-1:26.2.4-1-aarch64.pkg.tar.xz' +DESKTOP_MESA_URL='https://github.com/Droid-Deck/DroidDeck-Components/releases/download/build-dependency-mesa-26.2.4-r1/mesa-26.2.4-1-aarch64.pkg.tar.xz' diff --git a/tools/desktop-kde/overlay/usr/share/droiddeck/kwin-compositor b/tools/desktop-kde/overlay/usr/share/droiddeck/kwin-compositor new file mode 100644 index 000000000..a6797bf62 --- /dev/null +++ b/tools/desktop-kde/overlay/usr/share/droiddeck/kwin-compositor @@ -0,0 +1 @@ +opengl diff --git a/tools/desktop-kde/patches/0002-select-explicit-nested-render-node.patch b/tools/desktop-kde/patches/0002-select-explicit-nested-render-node.patch new file mode 100644 index 000000000..369ce1a56 --- /dev/null +++ b/tools/desktop-kde/patches/0002-select-explicit-nested-render-node.patch @@ -0,0 +1,23 @@ +--- a/src/backends/wayland/wayland_backend.cpp ++++ b/src/backends/wayland/wayland_backend.cpp +@@ -455,6 +455,20 @@ + + if (WaylandClient::LinuxDmabufV1 *dmabuf = m_display->linuxDmabuf()) { + m_renderDevice = GpuManager::self()->compatibleRenderDevice(dmabuf->mainDevice()); ++ if (!m_renderDevice && qEnvironmentVariableIsSet("DROIDDECK_NESTED_SCALE")) { ++ const auto nodes = GpuManager::splitPathList(qEnvironmentVariable("KWIN_RENDER_NODES")); ++ const auto &devices = GpuManager::self()->renderDevices(); ++ // Android's KGSL host cannot name a DRM device in its dmabuf feedback. ++ if (nodes.size() == 1) { ++ for (const auto &device : devices) { ++ if (device->path() == nodes.constFirst()) { ++ m_renderDevice = device.get(); ++ qCWarning(KWIN_WAYLAND_BACKEND) << "Using explicitly selected nested render node" << m_renderDevice->path(); ++ break; ++ } ++ } ++ } ++ } + } + + createOutputs(); diff --git a/tools/desktop-kde/pins/mesa/desc b/tools/desktop-kde/pins/mesa/desc new file mode 100644 index 000000000..34c38603a --- /dev/null +++ b/tools/desktop-kde/pins/mesa/desc @@ -0,0 +1,38 @@ +%NAME% +mesa + +%VERSION% +1:26.2.4-1 + +%FILENAME% +mesa-1:26.2.4-1-aarch64.pkg.tar.xz + +%SHA256SUM% +b03b498ba04ef5217299a81a1ef4c1c3582a250fab74848a443b85ba4a352fbd + +%DEPENDS% +expat +glibc +libdrm +libelf +libgcc +libglvnd +libstdc++ +libx11 +libxcb +libxext +libxshmfence +libxxf86vm +llvm-libs +lm_sensors +spirv-tools +wayland +zlib +zstd + +%PROVIDES% +libva-mesa-driver=1:26.2.4-1 +mesa-libgl=1:26.2.4-1 +libva-driver +opengl-driver + diff --git a/tools/linuxfs/desktop/droiddeck-desktop b/tools/linuxfs/desktop/droiddeck-desktop index 5078fbd9b..71377fed6 100644 --- a/tools/linuxfs/desktop/droiddeck-desktop +++ b/tools/linuxfs/desktop/droiddeck-desktop @@ -73,6 +73,13 @@ if [ -f "$HOME/.droiddeck-remote-run" ]; then done ) & fi +if [ "$(cat /usr/share/droiddeck/kwin-compositor 2>/dev/null)" = opengl ]; then + unset LIBGL_ALWAYS_SOFTWARE QT_QUICK_BACKEND + export KWIN_COMPOSE=O2 MESA_LOADER_DRIVER_OVERRIDE=zink GALLIUM_DRIVER=zink LIBGL_KOPPER_DRI2=true + if [ -z "${KWIN_RENDER_NODES:-}" ] && [ -c /dev/dri/renderD0 ]; then + export KWIN_RENDER_NODES=/dev/dri/renderD0 + fi +fi echo "== droiddeck-desktop: Plasma on $WAYLAND_DISPLAY" dbus-run-session -- startplasma-wayland rc=$? diff --git a/tools/tests/test_desktop_mesa_pin.py b/tools/tests/test_desktop_mesa_pin.py new file mode 100644 index 000000000..1928a2b60 --- /dev/null +++ b/tools/tests/test_desktop_mesa_pin.py @@ -0,0 +1,68 @@ +import pathlib +import subprocess +import sys +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[2] +BUILDER = ROOT / "tools/desktop-kde" + + +class MesaPinTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = pathlib.Path(self.temp.name) + for repo in ("core", "extra", "alarm"): + (self.root / ("x_" + repo)).mkdir() + rolling = self.root / "x_extra/mesa-26.2.5" + rolling.mkdir() + (rolling / "desc").write_text( + "%NAME%\nmesa\n\n%VERSION%\n1:26.2.5-1\n\n" + "%FILENAME%\nrolling-mesa.pkg.tar.xz\n\n%SHA256SUM%\nrolling\n\n" + "%DEPENDS%\nrolling-only-dependency\n\n%PROVIDES%\nopengl-driver\n" + ) + fields = {} + for block in (BUILDER / "pins/mesa/desc").read_text().strip().split("\n\n"): + key, *values = block.splitlines() + fields[key.strip("%")] = values + self.pin = fields + self.runtime = self.root / "runtime.txt" + self.runtime.write_text("".join(name + " 999\n" for name in fields["DEPENDS"])) + self.seeds = self.root / "seeds.txt" + + def resolve(self, seed): + self.seeds.write_text(seed + "\n") + return subprocess.run( + [sys.executable, str(BUILDER / "closure.py"), str(self.root), + str(self.runtime), str(self.seeds)], + text=True, capture_output=True, + ) + + def assertPinned(self, seed): + result = self.resolve(seed) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + result.stdout.strip(), + "extra/" + self.pin["FILENAME"][0] + " " + self.pin["SHA256SUM"][0], + ) + + def test_pinned_metadata_overrides_rolling_dependencies(self): + self.assertPinned("mesa") + + def test_virtual_provider_uses_pinned_package(self): + self.assertPinned("opengl-driver") + + def test_newer_runtime_cannot_bypass_tested_mesa(self): + with self.runtime.open("a") as stream: + stream.write("mesa 1:26.2.5-1\n") + self.assertPinned("mesa") + + def test_requirement_above_pin_fails_instead_of_using_rolling_mesa(self): + result = self.resolve("mesa>=1:26.2.5") + self.assertNotEqual(result.returncode, 0) + + +if __name__ == "__main__": + unittest.main()