From 4fef38e87ee2e4661b47514d4653541f2cebda09 Mon Sep 17 00:00:00 2001 From: xXJsonDeruloXx Date: Tue, 29 Sep 2026 08:18:26 -0400 Subject: [PATCH] feat: give Flatpak apps the KGSL GPU, the controllers and a CPU LLVM can target --- .../launcher/runtime/BwrapSpawner.kt | 61 +++++++++++++++++++ .../overlay/usr/local/bin/bannerlator-flatpak | 15 +++-- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt b/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt index b6579bc2a..89ca5826f 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt @@ -211,9 +211,70 @@ object BwrapSpawner { val argv = request.getJSONArray("argv").let { a -> (0 until a.length()).map { a.getString(it) } } val env = LinkedHashMap() request.optJSONObject("env")?.let { e -> e.keys().forEach { k -> env[k] = e.getString(k) } } + if (binds.any { it.guest == "/dev" }) gpu(rootfs, root, env)?.forEach { addBind(it) } + preload(session, rootfs, env).forEach { addBind(it) } return Plan(root, binds, request.optString("cwd", "/").ifEmpty { "/" }, argv, env) } + /** Where a sandbox finds the runtime's own GPU driver. */ + private const val GPU_DIR = "/run/droiddeck-gpu" + + /** + * The GPU, for an app that is given /dev. Flathub's Mesa has Turnip, but built for the DRM + * kernel driver: Adreno on Android is KGSL, so apps fell back to drawing on the CPU. The + * runtime's own Turnip has the KGSL backend and needs nothing the Freedesktop runtime lacks + * but three libraries, which come with it; Mesa's GL then runs on it through Zink. An app's + * own choice of driver (VK_ICD_FILENAMES, MESA_LOADER_DRIVER_OVERRIDE) is left alone. + */ + private fun gpu(rootfs: String, root: File, env: MutableMap): List? { + val lib = File(rootfs, "usr/lib") + val driver = File(lib, "libvulkan_freedreno.so") + if (!driver.isFile || !File("/dev/kgsl-3d0").exists()) return null + if (env.containsKey("VK_ICD_FILENAMES") || env.containsKey("VK_DRIVER_FILES")) return null + val binds = ArrayList() + for (name in listOf("libvulkan_freedreno.so", "libdisplay-info.so.3", "libSPIRV-Tools.so", "libSPIRV-Tools-opt.so")) { + val f = File(lib, name) + if (!f.exists()) { Log.w(TAG, "gpu: the runtime has no $name; apps draw on the CPU"); return null } + binds.add(Bind(f.canonicalPath, "$GPU_DIR/$name")) + } + // Beside the sandbox root, in the sandbox's own directory: removed with it. + val icd = File(root.parentFile, "freedreno_icd.json") + icd.writeText("{\"file_format_version\": \"1.0.0\", \"ICD\": {\"library_path\": \"$GPU_DIR/libvulkan_freedreno.so\", \"api_version\": \"1.4.0\"}}\n") + binds.add(Bind(icd.path, "$GPU_DIR/freedreno_icd.json")) + env["VK_DRIVER_FILES"] = "$GPU_DIR/freedreno_icd.json" + env["VK_ICD_FILENAMES"] = "$GPU_DIR/freedreno_icd.json" + if (!env.containsKey("MESA_LOADER_DRIVER_OVERRIDE") && !env.containsKey("GALLIUM_DRIVER")) { + env["MESA_LOADER_DRIVER_OVERRIDE"] = "zink" + env["GALLIUM_DRIVER"] = "zink" + } + // The driver's own libraries, found beside it; nothing else is in that directory. + env["LD_LIBRARY_PATH"] = listOf(env["LD_LIBRARY_PATH"], GPU_DIR).filter { !it.isNullOrEmpty() }.joinToString(":") + return binds + } + + /** + * What every program in the session preloads (SessionFiles writes /etc/ld.so.preload): the + * session shim - SysV shared memory for X11, the network calls Android refuses - and the + * controller reader. A sandbox's /etc is the runtime's, so the same libraries go in through + * LD_PRELOAD instead, with the directory the controllers' rings live in; without them an app + * saw no gamepad. Both are built against an older glibc than any Flathub runtime has. + */ + private fun preload(session: List, rootfs: String, env: MutableMap): List { + val libs = FileUtils.readString(File(rootfs, "etc/ld.so.preload"))?.lines() + ?.map { it.trim() }?.filter { it.startsWith("/") && File(rootfs + it).isFile } ?: return emptyList() + if (libs.isEmpty()) return emptyList() + val binds = ArrayList() + val paths = libs.map { lib -> "$PRELOAD_DIR/${lib.substringAfterLast('/')}".also { binds.add(Bind(rootfs + lib, it)) } } + env["LD_PRELOAD"] = (paths + listOfNotNull(env["LD_PRELOAD"]?.takeIf { it.isNotBlank() })).joinToString(":") + // The controllers: FAKE_EVDEV_DIR is /dev/input, the rings beside it in /dev. + env["FAKE_EVDEV_DIR"]?.let { File(it).parent }?.let { dev -> + binds.addAll(bindsFor(session, rootfs, dev, dev)) + } + return binds + } + + private const val PRELOAD_DIR = "/run/droiddeck-preload" + /** * /proc/cpuinfo without the cores' names. Snapdragon's ARMv9 cores (Cortex-A510/A715/X3) have * SVE2 by architecture, and LLVM - llvmpipe's JIT in Flathub's Mesa, which is what apps draw diff --git a/tools/linuxfs/overlay/usr/local/bin/bannerlator-flatpak b/tools/linuxfs/overlay/usr/local/bin/bannerlator-flatpak index 956d33f9e..f206d1e42 100644 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-flatpak +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-flatpak @@ -98,11 +98,16 @@ def install(app_id): if app_ref(inst, app_id) is not None: emit(e="done", note="already installed") return 0 - try: - remote_ref = inst.fetch_remote_ref_sync(REMOTE, Flatpak.RefKind.APP, app_id, arch, None, None) - branch = remote_ref.get_branch() - except GLib.Error: - emit(e="error", message="%s is not on Flathub for %s" % (app_id, arch)) + # Flathub publishes apps on "stable"; libflatpak's default branch is "master". + branch, last = None, None + for candidate in ("stable", "master"): + try: + branch = inst.fetch_remote_ref_sync(REMOTE, Flatpak.RefKind.APP, app_id, arch, candidate, None).get_branch() + break + except GLib.Error as err: + last = err + if branch is None: + emit(e="error", message="%s is not on Flathub for %s (%s)" % (app_id, arch, last.message if last else "no branch")) return 1 t, state = transaction(inst) t.add_install(REMOTE, "app/%s/%s/%s" % (app_id, arch, branch), None)