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()