From 061ae17cc23880c9838819f814bdbb1baff855bc Mon Sep 17 00:00:00 2001 From: Ahmad Sayed Alsayed Ebrahim Date: Wed, 14 Oct 2026 17:06:11 +0300 Subject: [PATCH 01/21] Add MaliBridge as a submodule at external/MaliBridge MaliBridge (https://github.com/ASEbrahim/MaliBridge) is a Vulkan command bridge that lets the Linux side draw on a non-Adreno GPU (Mali) through the phone's own Vulkan driver. Pinned at v0.5.3.1. --- .gitmodules | 3 +++ external/MaliBridge | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 external/MaliBridge diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..299e3b068 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/MaliBridge"] + path = external/MaliBridge + url = https://github.com/ASEbrahim/MaliBridge.git diff --git a/external/MaliBridge b/external/MaliBridge new file mode 160000 index 000000000..669e668cf --- /dev/null +++ b/external/MaliBridge @@ -0,0 +1 @@ +Subproject commit 669e668cf4a380757dd532d991a35673307a70c3 From a69ce00b6a73678a37763a9f94c2b29e856ff596 Mon Sep 17 00:00:00 2001 From: Ahmad Sayed Alsayed Ebrahim Date: Wed, 14 Oct 2026 17:06:11 +0300 Subject: [PATCH 02/21] Build and package the Mali bridge from external/MaliBridge The server is built with the NDK into libvkbridge_server.so (a program the app execs from its native library directory); the Linux ICD, its manifest and the self-test go into the runtime assets with the other glibc preloads, locally and in CI. --- app/build.gradle | 15 +++++++++++++++ tools/build_local.sh | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 1c9b9bad8..08d3b958c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -325,6 +325,21 @@ def buildRustStores = tasks.register('buildRustStores', Exec) { if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir) tasks.named('preBuild').configure { dependsOn(buildRustStores) } +def maliBridgeDir = rootProject.file('external/MaliBridge') +def vkbridgeJniLibsDir = layout.buildDirectory.dir('vkbridgeJniLibs') +def buildVkBridgeServer = tasks.register('buildVkBridgeServer', Exec) { + description = 'Builds the Vulkan bridge server (libvkbridge_server.so) with the NDK.' + inputs.dir(new File(maliBridgeDir, 'common')) + inputs.dir(new File(maliBridgeDir, 'server')) + inputs.dir(new File(maliBridgeDir, 'third_party')) + inputs.file(new File(maliBridgeDir, 'scripts/build-server-android.sh')) + outputs.dir(vkbridgeJniLibsDir) + commandLine 'bash', new File(maliBridgeDir, 'scripts/build-server-android.sh').absolutePath, + android.ndkDirectory.absolutePath, vkbridgeJniLibsDir.get().asFile.absolutePath +} +android.sourceSets.main.jniLibs.srcDir(vkbridgeJniLibsDir) +tasks.named('preBuild').configure { dependsOn(buildVkBridgeServer) } + dependencies { implementation "org.opencv:opencv:${trackingPack.OPENCV_VERSION}" implementation 'org.khronos.openxr:openxr_loader_for_android:1.1.49' diff --git a/tools/build_local.sh b/tools/build_local.sh index a980d70a6..4a58f7721 100755 --- a/tools/build_local.sh +++ b/tools/build_local.sh @@ -40,6 +40,10 @@ if [[ ! -x "${java_dir}/bin/java" ]]; then echo "Java 17 not found at ${java_dir}; set JAVA_HOME." >&2 exit 1 fi +if [[ ! -f "${repo_root}/external/MaliBridge/scripts/build-client-linux.sh" ]]; then + echo "external/MaliBridge is empty; run: git submodule update --init" >&2 + exit 1 +fi if ! command -v docker >/dev/null 2>&1; then echo "Docker is required to cross-compile the glibc ARM64 preload libraries." >&2 exit 1 @@ -137,6 +141,7 @@ docker run --rm --platform linux/amd64 \ -o "$d/libssbs.so" tools/linuxfs/ssbs/ssbs_adapter.c -ldl aarch64-linux-gnu-strip --strip-unneeded "$d/libssbs.so" mkdir -p "$d/usr/local/bin" + external/MaliBridge/scripts/build-client-linux.sh "$d" aarch64-linux-gnu-gcc aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$d/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl aarch64-linux-gnu-strip --strip-unneeded "$d/usr/local/bin/droiddeck-clipboard" for script in tools/linuxfs/overlay/usr/local/bin/droiddeck-* tools/linuxfs/overlay/usr/local/bin/steam-compatibility; do From c883ce4439697ca7e1dc1d92b7d934287ffe7c1e Mon Sep 17 00:00:00 2001 From: maxjivi05 Date: Sun, 25 Oct 2026 19:47:41 -0400 Subject: [PATCH 03/21] CI builds the MaliBridge client as a component The components job checks out external/MaliBridge and builds the guest ICD, its manifest and the self-test as the vkbridge recipe; a submodule counts in a recipe's identity by its pinned commit. --- .github/workflows/build.yml | 15 +++++++++++++++ tools/components/prepare.py | 3 +++ tools/components/recipes.json | 15 +++++++++++++++ tools/components/recipes/vkbridge.sh | 4 ++++ tools/tests/test_component_prepare.py | 20 ++++++++++++++++++++ 5 files changed, 57 insertions(+) create mode 100644 tools/components/recipes/vkbridge.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d39651c0..75a28f793 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -149,6 +149,8 @@ jobs: run: test "$(git rev-parse 'HEAD^{tree}')" = "$SOURCE_TREE" - name: Resolve the pinned Android NDK run: bash tools/ci/resolve-ndk.sh + - name: Check out MaliBridge + run: git submodule update --init external/MaliBridge - name: Plan component identities id: plan @@ -212,6 +214,7 @@ jobs: ${{ steps.preloads.outputs.cache-hit != 'true' && 'runtime' || '' }} ${{ steps.component-preloads.outputs.arm64 != 'true' && 'arm64-preloads' || '' }} ${{ steps.comet.outputs.cache-hit != 'true' && 'comet' || '' }} + vkbridge run: python3 tools/components/prepare.py tools - name: Build changed ARM64 preloads @@ -222,6 +225,9 @@ jobs: if: steps.preloads.outputs.cache-hit != 'true' run: python3 tools/components/prepare.py runtime + - name: Build the Mali bridge's Linux side + run: python3 tools/components/prepare.py vkbridge + - name: Fetch the Debian image the preloads build in if: steps.component-preloads.outputs.x86 != 'true' env: @@ -253,6 +259,12 @@ jobs: run: | set -euo pipefail bash tools/linuxfs/check-preloads.sh app/src/main/assets/linuxfs + D=app/src/main/assets/linuxfs + VKB="$D/usr/local/lib/vkbridge/libvulkan_droidbridge.so" + test -f "$VKB" && test -f "$D/usr/local/share/vkbridge/droidbridge_icd.json" && test -f "$D/usr/local/bin/droiddeck-vkbridge-smoke" + VKB_NEED=$(readelf -d "$VKB" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p' | grep -vx 'libc.so.6' | grep -vx 'ld-linux-aarch64.so.1' || true) + [ -z "$VKB_NEED" ] || { echo "::error::the bridge ICD needs $VKB_NEED"; exit 1; } + [ "$(readelf -Ws "$VKB" | awk '$4 == "FUNC" && $5 == "GLOBAL" && $7 != "UND" {sub(/@.*/, "", $8); print $8}' | grep -c '^vk_icd')" = 3 ] || { echo "::error::the bridge ICD does not export the loader interface"; exit 1; } - name: Build GOG Galaxy companion if: steps.comet.outputs.cache-hit != 'true' @@ -504,6 +516,9 @@ jobs: echo "::warning::built proot pack $sha does not match tools/packs/proot/release.env ($PROOT_PACK_SHA256). This build publishes the pack it just built. Source builds keep downloading the pinned pack until release.env is updated." fi + - name: Check out MaliBridge + run: git submodule update --init external/MaliBridge + - name: Build id: build env: diff --git a/tools/components/prepare.py b/tools/components/prepare.py index 064b452be..fbcd27394 100644 --- a/tools/components/prepare.py +++ b/tools/components/prepare.py @@ -35,6 +35,9 @@ def identity(name, root=ROOT): for name_ in sorted(set(paths)): if '__pycache__' in name_ or name_.endswith(('.pyc', '.md')): continue + if (root / name_).is_dir(): + values[name_] = subprocess.check_output(['git', 'ls-files', '--stage', '--', name_], cwd=root, text=True).split()[1] + continue content = (root / name_).read_bytes() if b'\0' not in content: content = content.replace(b'\r\n', b'\n') diff --git a/tools/components/recipes.json b/tools/components/recipes.json index 602fb379e..993fd787a 100644 --- a/tools/components/recipes.json +++ b/tools/components/recipes.json @@ -222,5 +222,20 @@ "cmake", "xz-utils" ] + }, + "vkbridge": { + "script": "tools/components/recipes/vkbridge.sh", + "inputs": [ + "external/MaliBridge", + "tools/components/recipes/vkbridge.sh" + ], + "outputs": [ + "app/src/main/assets/linuxfs/usr/local/lib/vkbridge/libvulkan_droidbridge.so", + "app/src/main/assets/linuxfs/usr/local/share/vkbridge/droidbridge_icd.json", + "app/src/main/assets/linuxfs/usr/local/bin/droiddeck-vkbridge-smoke" + ], + "packages": [ + "gcc-aarch64-linux-gnu" + ] } } diff --git a/tools/components/recipes/vkbridge.sh b/tools/components/recipes/vkbridge.sh new file mode 100644 index 000000000..10b4b521c --- /dev/null +++ b/tools/components/recipes/vkbridge.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +test -f external/MaliBridge/scripts/build-client-linux.sh +bash external/MaliBridge/scripts/build-client-linux.sh app/src/main/assets/linuxfs aarch64-linux-gnu-gcc diff --git a/tools/tests/test_component_prepare.py b/tools/tests/test_component_prepare.py index b513c8fe7..d86fff9b3 100644 --- a/tools/tests/test_component_prepare.py +++ b/tools/tests/test_component_prepare.py @@ -57,6 +57,26 @@ class ComponentPrepareTest(unittest.TestCase): script.write_text(script.read_text() + '\nprintf changed\n') self.assertNotEqual(changed, components.identity('fake', self.root)) + def test_identity_follows_a_submodule_commit(self): + library = self.root / 'library' + subprocess.run(['git', 'init', '-q', str(library)], check=True) + def commit(text): + (library / 'source.c').write_text(text) + subprocess.run(['git', '-C', str(library), 'add', '.'], check=True) + subprocess.run(['git', '-C', str(library), '-c', 'user.name=Test Developer', '-c', 'user.email=test@example.test', + 'commit', '-qm', text], check=True) + return subprocess.check_output(['git', '-C', str(library), 'rev-parse', 'HEAD'], text=True).strip() + first = commit('int a;\n') + subprocess.run(['git', '-C', str(self.root), 'add', 'library'], check=True) + spec = dict(script='tools/components/recipes/fake.sh', inputs=['library'], outputs=[]) + (self.root / components.REGISTRY).write_text(json.dumps(dict(fake=spec))) + original = components.identity('fake', self.root) + commit('int b;\n') + self.assertEqual(original, components.identity('fake', self.root)) + subprocess.run(['git', '-C', str(self.root), 'add', 'library'], check=True) + self.assertNotEqual(original, components.identity('fake', self.root)) + self.assertNotEqual(first, subprocess.check_output(['git', '-C', str(library), 'rev-parse', 'HEAD'], text=True).strip()) + def test_tool_requirements_are_installed_in_one_union(self): definitions = dict(one=dict(packages=['gcc', 'cmake']), two=dict(packages=['gcc', 'qemu-user'])) with patch.object(components, 'registry', return_value=definitions), patch.object(components.subprocess, 'run') as run: From 3943794b0e78c593cc6e622d38efdba9cdd4a819 Mon Sep 17 00:00:00 2001 From: Ahmad Sayed Alsayed Ebrahim Date: Wed, 14 Oct 2026 17:06:11 +0300 Subject: [PATCH 04/21] waylandcomp: import layer pool buffers as AHardwareBuffers off Adreno A non-QTI gralloc handle (Arm/MediaTek gralloc on Mali) has no layout the compositor can read, so the pool import guessed a linear dma-buf and failed. On drivers other than Qualcomm's the compositor device enables VK_ANDROID_external_memory_android_hardware_buffer and VK_EXT_queue_family_foreign and imports such buffers as AHardwareBuffers; the linear dma-buf stays as the last resort. Adreno/Turnip keeps the same device and the dma-buf path. --- app/src/main/cpp/waylandcomp/src/sc_layer.c | 91 +++++++++++---- app/src/main/cpp/waylandcomp/src/vk_present.c | 104 +++++++++++++++++- app/src/main/cpp/waylandcomp/src/vk_present.h | 4 + 3 files changed, 178 insertions(+), 21 deletions(-) diff --git a/app/src/main/cpp/waylandcomp/src/sc_layer.c b/app/src/main/cpp/waylandcomp/src/sc_layer.c index f9d203ba1..299d2d04c 100644 --- a/app/src/main/cpp/waylandcomp/src/sc_layer.c +++ b/app/src/main/cpp/waylandcomp/src/sc_layer.c @@ -381,6 +381,26 @@ static int sniff_modifier(const struct droiddeck_native_handle *h, uint64_t *mod return 1; } +#define SC_MOD_GRALLOC (~0ULL - 1) + +static const char *pool_kind(uint64_t mod) { + return mod == MOD_QCOM_COMPRESSED ? "UBWC" : mod == SC_MOD_GRALLOC ? "gralloc-laid-out" : "linear"; +} + +static void describe_handle(const struct droiddeck_native_handle *h, char *out, size_t n) { + size_t o = 0; + out[0] = 0; + if (!h) { snprintf(out, n, "no handle"); return; } + for (int i = 0; i < h->numFds && i < 4 && o < n; i++) { + off_t sz = lseek(h->data[i], 0, SEEK_END); + if (sz >= 0) lseek(h->data[i], 0, SEEK_SET); + o += (size_t)snprintf(out + o, n - o, "%sfd%d %lld bytes", i ? ", " : "", i, (long long)sz); + } + if (o < n) o += (size_t)snprintf(out + o, n - o, "; ints"); + for (int i = 0; i < h->numInts && i < 8 && o < n; i++) + o += (size_t)snprintf(out + o, n - o, " %#x", (unsigned)h->data[h->numFds + i]); +} + static int g_alloc_failed; enum { TIER_UBWC, TIER_PLAIN, TIER_LINEAR }; @@ -416,22 +436,54 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm AHardwareBuffer_Desc got; AHardwareBuffer_describe(ahb, &got); const struct droiddeck_native_handle *nh = api.getNativeHandle(ahb); uint64_t mod = MOD_LINEAR; - if (!sniff_modifier(nh, &mod)) { - if (tier < TIER_LINEAR) { + struct vkp_image *img = NULL; + const char *path; + char info[160] = ""; + if (sniff_modifier(nh, &mod)) { + img = vkp_image_import_dmabuf(nh->data[0], fmt == AHB_RGB10A2 ? DRM_ABGR2101010 : DRM_ABGR8888, mod, w, h, + got.stride * 4, 0, 1); + path = "dma-buf (QTI handle)"; + } else { + if (tier == TIER_UBWC) { + char hd[256]; + describe_handle(nh, hd, sizeof(hd)); + droiddeck_log("layer", "%s: gralloc handle is not a QTI one (%d fds, %d ints: %s): no UBWC request; " + "pool buffers are imported %s", l->name, nh ? nh->numFds : -1, nh ? nh->numInts : -1, hd, + vkp_can_import_ahb() ? "as AHardwareBuffers (the driver takes gralloc's own layout)" + : "as linear dma-bufs (this driver has no AHardwareBuffer import)"); AHardwareBuffer_release(ahb); - l->alloc_tier = TIER_LINEAR; - droiddeck_log("layer", "%s: gralloc handle layout unknown (%d fds, %d ints): using linear pool buffers", - l->name, nh ? nh->numFds : -1, nh ? nh->numInts : -1); + l->alloc_tier = vkp_can_import_ahb() ? TIER_PLAIN : TIER_LINEAR; continue; } - mod = MOD_LINEAR; + mod = SC_MOD_GRALLOC; + path = "AHardwareBuffer"; + img = vkp_image_import_ahb(ahb, info, sizeof(info)); + if (!img && tier == TIER_LINEAR) { + const off_t need = (off_t)got.stride * 4 * h; + int fd = -1, which = -1; + for (int i = 0; nh && i < nh->numFds; i++) { + off_t sz = lseek(nh->data[i], 0, SEEK_END); + if (sz >= 0) lseek(nh->data[i], 0, SEEK_SET); + if (sz >= need) { fd = nh->data[i]; which = i; break; } + } + if (fd < 0 && nh && nh->numFds > 0) { fd = nh->data[0]; which = 0; } + mod = MOD_LINEAR; + path = "linear dma-buf"; + img = fd >= 0 ? vkp_image_import_dmabuf(fd, fmt == AHB_RGB10A2 ? DRM_ABGR2101010 : DRM_ABGR8888, mod, + w, h, got.stride * 4, 0, 1) : NULL; + snprintf(info, sizeof(info), "handle fd %d of %d", which, nh ? nh->numFds : 0); + if (vkp_can_import_ahb()) + droiddeck_log("layer", "%s: AHardwareBuffer import failed: the linear dma-buf in the handle's fd %d " + "(of %d) -> %s", l->name, which, nh ? nh->numFds : 0, img ? "imported" : "failed too"); + } } - int fd = (nh && nh->numFds > 0) ? nh->data[0] : -1; - struct vkp_image *img = fd >= 0 ? vkp_image_import_dmabuf(fd, fmt == AHB_RGB10A2 ? DRM_ABGR2101010 : DRM_ABGR8888, - mod, w, h, got.stride * 4, 0, 1) : NULL; if (!img) { - droiddeck_log("layer", "%s: import of a %s %dx%d pool buffer (stride %u px) into the compositor's Turnip failed", - l->name, mod == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", w, h, got.stride); + char hd[256]; + describe_handle(nh, hd, sizeof(hd)); + droiddeck_log("layer", "%s: import of a %s %dx%d pool buffer (stride %u px, %s, usage %#llx) into the " + "compositor's Turnip failed via %s; gralloc handle %d fds / %d ints: %s", l->name, + pool_kind(mod), w, h, got.stride, tier_name(tier), (unsigned long long)got.usage, path, + nh ? nh->numFds : -1, nh ? nh->numInts : -1, hd); AHardwareBuffer_release(ahb); if (tier < TIER_LINEAR) { l->alloc_tier = tier + 1; continue; } g_alloc_failed = 1; @@ -442,10 +494,11 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm if (l->logged_w != w || l->logged_h != h || l->logged_fmt != fmt || l->logged_mod != mod || l->logged_tier != tier) { l->logged_w = w; l->logged_h = h; l->logged_fmt = fmt; l->logged_mod = mod; l->logged_tier = tier; - droiddeck_log("layer", "%s: pool buffers %dx%d%s: %s (%s), stride %u px, up to %d buffers (gralloc handle %d fds / %d ints)", - l->name, w, h, fmt == AHB_RGB10A2 ? " 10-bit RGBA1010102" : "", - mod == MOD_QCOM_COMPRESSED ? "UBWC (QCOM_COMPRESSED)" : "linear", tier_name(tier), - got.stride, l->pool_n, nh ? nh->numFds : -1, nh ? nh->numInts : -1); + droiddeck_log("layer", "%s: pool buffers %dx%d%s: %s (%s), stride %u px, up to %d buffers (gralloc handle %d fds / " + "%d ints), imported as %s%s%s%s", l->name, w, h, fmt == AHB_RGB10A2 ? " 10-bit RGBA1010102" : "", + mod == MOD_QCOM_COMPRESSED ? "UBWC (QCOM_COMPRESSED)" : pool_kind(mod), tier_name(tier), + got.stride, l->pool_n, nh ? nh->numFds : -1, nh ? nh->numInts : -1, path, + info[0] ? " (" : "", info, info[0] ? ")" : ""); } return 0; } @@ -814,7 +867,7 @@ int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const stru l->first_logged = 1; droiddeck_log("layer", "presenting %dx%d game frames on their own SurfaceControl layer (%s pool, %d buffers); " "HUD and pointer stay Android views above it", sw, sh, - l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", l->pool_n); + pool_kind(l->pool_modifier), l->pool_n); } vkp_signal_first_frame(); return 0; @@ -836,7 +889,7 @@ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int l->first_logged = 1; droiddeck_log("layer", "presenting %dx%d frames on their own SurfaceControl layer (%s pool, %d buffers); " "HUD and pointer stay Android views above it", rw, rh, - l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", l->pool_n); + pool_kind(l->pool_modifier), l->pool_n); } vkp_signal_first_frame(); return 0; @@ -874,12 +927,12 @@ int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct if (tm) droiddeck_log("color", "tone-mapped picture on the game's display layer: %dx%d, %s 8-bit buffers, untagged " "(sRGB) - HDR output is switched off", rw, rh, - l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear"); + pool_kind(l->pool_modifier)); else { char cov[96]; coverage_text(l, cov, sizeof(cov)); droiddeck_log("color", "HDR picture on its own display layer: %dx%d, %s %s buffers, tagged %s%s%s", rw, rh, - l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", + pool_kind(l->pool_modifier), g_hdr_pool_fmt == AHB_RGB10A2 ? "10-bit" : "8-bit", color && color->dataspace == DROIDDECK_ADATASPACE_BT2020_PQ ? "BT2020_PQ" : "HDR", cov[0] ? ", " : "", cov); diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.c b/app/src/main/cpp/waylandcomp/src/vk_present.c index c917afbba..d76d84eda 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.c +++ b/app/src/main/cpp/waylandcomp/src/vk_present.c @@ -94,6 +94,9 @@ static int g_black_state; static PFN_vkImportSemaphoreFdKHR g_import_sem_fd; static VkSemaphore g_wait_sem; +#define VKP_VENDOR_QUALCOMM 0x5143 +static uint32_t g_vendor_id; + static struct vkp_perf g_perf; static int64_t perf_now(void) { struct timespec ts; @@ -454,6 +457,7 @@ static int dev_init(void) { { VkPhysicalDeviceProperties props; g_vk.GetPhysicalDeviceProperties(g_pd, &props); + g_vendor_id = props.vendorID; snprintf(g_gpu_name, sizeof(g_gpu_name), "%s", props.deviceName); droiddeck_log("gpu", "compositor renders on %s with %s", props.deviceName, g_library_name ? g_library_name : "the system Vulkan driver"); @@ -984,8 +988,9 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t .allocationSize = req.size, .memoryTypeIndex = (uint32_t)idx}; VkResult mr = g_vk.AllocateMemory(g_dev, &mai, NULL, &img->mem); if (mr != VK_SUCCESS) { - LOGE("%s: importing the %s dma-buf (%dx%d, %llu bytes) -> %s", as_blit_dst ? "layer" : "dmabuf", - vkp_modifier_name(modifier), w, h, (unsigned long long)req.size, vk_result_name(mr)); + LOGE("%s: importing the %s dma-buf (%dx%d, pitch %u, offset %u, %llu bytes, memory type %d of image types " + "%#x & fd types %#x) -> %s", as_blit_dst ? "layer" : "dmabuf", vkp_modifier_name(modifier), w, h, + stride, offset, (unsigned long long)req.size, idx, req.memoryTypeBits, allowed, vk_result_name(mr)); g_vk.DestroyImage(g_dev, img->image, NULL); close(dupfd); free(img); return NULL; } VkResult br = g_vk.BindImageMemory(g_dev, img->image, img->mem, 0); @@ -1052,6 +1057,101 @@ struct vkp_image *vkp_image_from_ahb(AHardwareBuffer *ahb) { return img; } +int vkp_can_import_ahb(void) { + return dev_init() == 0 && g_vendor_id != VKP_VENDOR_QUALCOMM && g_ahb_import && + g_vk.GetAndroidHardwareBufferPropertiesANDROID; +} + +struct vkp_image *vkp_image_import_ahb(struct AHardwareBuffer *ahb, char *info, size_t info_n) { + if (info && info_n) info[0] = 0; + if (!ahb || !vkp_can_import_ahb()) return NULL; + AHardwareBuffer_Desc d; + AHardwareBuffer_describe(ahb, &d); + VkAndroidHardwareBufferFormatPropertiesANDROID fp = { + .sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID}; + VkAndroidHardwareBufferPropertiesANDROID hp = { + .sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID, .pNext = &fp}; + VkResult pr = g_vk.GetAndroidHardwareBufferPropertiesANDROID(g_dev, ahb, &hp); + if (pr != VK_SUCCESS) { + LOGE("layer: vkGetAndroidHardwareBufferPropertiesANDROID(%ux%u, AHB format %#x, usage %#llx) -> %s", d.width, + d.height, d.format, (unsigned long long)d.usage, vk_result_name(pr)); + return NULL; + } + if (fp.format == VK_FORMAT_UNDEFINED) { + LOGE("layer: the driver reports the %ux%u AHB (format %#x) as external format %#llx only: no blit target", + d.width, d.height, d.format, (unsigned long long)fp.externalFormat); + return NULL; + } + const VkImageUsageFlags usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (g_vk.GetPhysicalDeviceImageFormatProperties2) { + VkPhysicalDeviceExternalImageFormatInfo ext = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO, + .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID}; + VkPhysicalDeviceImageFormatInfo2 qi = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2, .pNext = &ext, .format = fp.format, + .type = VK_IMAGE_TYPE_2D, .tiling = VK_IMAGE_TILING_OPTIMAL, .usage = usage}; + VkExternalImageFormatProperties extp = {.sType = VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES}; + VkImageFormatProperties2 ip = {.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2, .pNext = &extp}; + VkResult qr = g_vk.GetPhysicalDeviceImageFormatProperties2(g_pd, &qi, &ip); + if (qr != VK_SUCCESS || + !(extp.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT)) { + LOGE("layer: VkFormat %d as an imported AHB blit target is not supported (%s, external features %#x)", + (int)fp.format, vk_result_name(qr), extp.externalMemoryProperties.externalMemoryFeatures); + return NULL; + } + } + + struct vkp_image *img = calloc(1, sizeof(*img)); + if (!img) return NULL; + img->w = (int)d.width; img->h = (int)d.height; + img->dmabuf = 1; img->blit_dst = 1; + img->fmt = fp.format; + VkExternalMemoryImageCreateInfo extImg = { + .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO, + .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID}; + VkImageCreateInfo ici = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .pNext = &extImg, + .imageType = VK_IMAGE_TYPE_2D, .format = fp.format, .extent = {d.width, d.height, 1}, + .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = usage, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED}; + VkResult cr = g_vk.CreateImage(g_dev, &ici, NULL, &img->image); + if (cr != VK_SUCCESS) { + LOGE("layer: vkCreateImage(AHB %ux%u, VkFormat %d) -> %s", d.width, d.height, (int)fp.format, + vk_result_name(cr)); + free(img); return NULL; + } + int idx = -1; + for (int i = 0; i < 32; i++) if (hp.memoryTypeBits & (1u << i)) { idx = i; break; } + if (idx < 0) { + LOGE("layer: no memory type can import the %ux%u AHB (memoryTypeBits 0)", d.width, d.height); + g_vk.DestroyImage(g_dev, img->image, NULL); free(img); return NULL; + } + VkImportAndroidHardwareBufferInfoANDROID imp = { + .sType = VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID, .buffer = ahb}; + VkMemoryDedicatedAllocateInfo ded = {.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO, + .pNext = &imp, .image = img->image}; + VkMemoryAllocateInfo mai = {.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .pNext = &ded, + .allocationSize = hp.allocationSize, .memoryTypeIndex = (uint32_t)idx}; + VkResult mr = g_vk.AllocateMemory(g_dev, &mai, NULL, &img->mem); + if (mr != VK_SUCCESS) { + LOGE("layer: importing the %ux%u AHB (VkFormat %d, %llu bytes, memory type %d of memoryTypeBits %#x) -> %s", + d.width, d.height, (int)fp.format, (unsigned long long)hp.allocationSize, idx, hp.memoryTypeBits, + vk_result_name(mr)); + g_vk.DestroyImage(g_dev, img->image, NULL); free(img); return NULL; + } + VkResult br = g_vk.BindImageMemory(g_dev, img->image, img->mem, 0); + if (br != VK_SUCCESS) { + LOGE("layer: vkBindImageMemory(AHB) -> %s", vk_result_name(br)); + g_vk.FreeMemory(g_dev, img->mem, NULL); g_vk.DestroyImage(g_dev, img->image, NULL); + free(img); return NULL; + } + if (info && info_n) + snprintf(info, info_n, "VkFormat %d, %llu bytes, memory type %d of memoryTypeBits %#x", (int)fp.format, + (unsigned long long)hp.allocationSize, idx, hp.memoryTypeBits); + return img; +} + static int shm_sampled(void) { static int known; if (!known) { diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.h b/app/src/main/cpp/waylandcomp/src/vk_present.h index 7271f468b..0f8a03779 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.h +++ b/app/src/main/cpp/waylandcomp/src/vk_present.h @@ -1,6 +1,7 @@ #ifndef VK_PRESENT_H #define VK_PRESENT_H #include +#include #include void vk_present_set_driver(const char *driver_path, const char *library_name, const char *native_lib_dir); @@ -32,6 +33,9 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t int w, int h, uint32_t stride, uint32_t offset, int as_blit_dst); int vkp_image_is_dmabuf(const struct vkp_image *img); +struct AHardwareBuffer; +int vkp_can_import_ahb(void); +struct vkp_image *vkp_image_import_ahb(struct AHardwareBuffer *ahb, char *info, size_t info_n); struct vkp_image *vkp_image_create_shm(int w, int h); struct AHardwareBuffer; From dae36b0e679b8bd55b079614ad698ab6b112d06b Mon Sep 17 00:00:00 2001 From: Ahmad Sayed Alsayed Ebrahim Date: Wed, 14 Oct 2026 17:06:11 +0300 Subject: [PATCH 05/21] Mali bridge sessions: Setup switch, server, session wiring On a GPU without KGSL, Setup offers an experimental Mali bridge switch (off by default; ignored on Adreno). With it on, the session starts the bridge server, points the Vulkan loader at the bridge ICD, names Mali's device as the stand-in render node, runs the bridge self-test into the session log, and caps vkd3d-proton's feature level at 12_0. Flatpak sandboxes get the bridge ICD and socket. The DRM preload falls back to flock in a bridge session when kcmp is refused, and a clock/thermal log is kept beside the bridge logs. --- .../droiddeck/launcher/FrontEndActivity.kt | 9 +- .../droiddeck/launcher/core/DeviceSupport.kt | 5 + .../launcher/gpu/ClockLogComponent.kt | 108 ++++++++++++++++++ .../droiddeck/launcher/gpu/TurnipDriver.java | 4 + .../launcher/gpu/VkBridgeComponent.kt | 61 ++++++++++ .../droiddeck/launcher/gpu/VkBridgeNative.kt | 49 ++++++++ .../launcher/runtime/BwrapSpawner.kt | 23 ++++ .../launcher/runtime/LinuxRuntime.java | 31 ++++- .../launcher/session/SessionFiles.kt | 3 + .../launcher/session/SessionPrefs.kt | 7 ++ .../launcher/session/SessionService.kt | 23 +++- .../droiddeck/launcher/ui/FrontEndScreen.kt | 3 + .../droiddeck/launcher/ui/FrontEndSetup.kt | 9 +- app/src/main/res/values/strings.xml | 3 + .../overlay/usr/local/bin/droiddeck-game-env | 11 ++ .../overlay/usr/local/bin/droiddeck-session | 36 ++++++ tools/linuxfs/preload/drm.c | 40 +++++-- tools/tests/test_game_environment.py | 9 ++ 18 files changed, 419 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/droiddeck/launcher/gpu/ClockLogComponent.kt create mode 100644 app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt create mode 100644 app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeNative.kt diff --git a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt index a80e3c5d6..2d027f267 100644 --- a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt @@ -148,6 +148,8 @@ class FrontEndActivity : ComponentActivity() { private var gameCores by mutableStateOf>(emptySet()) private var tuSysmem by mutableStateOf(false) private var zinkLazy by mutableStateOf(false) + private var vkBridge by mutableStateOf(false) + private val adrenoGpu by lazy { com.droiddeck.launcher.core.DeviceSupport.adreno() } private var noXalia by mutableStateOf(true) private var fastSync by mutableStateOf(false) private var syncFallback by mutableStateOf(true) @@ -809,6 +811,7 @@ class FrontEndActivity : ComponentActivity() { FrontEndScreen( FrontEndState( installed = installed, ready = ready, available = available?.version, removalPending = removalPending, runtimeActionsBlocked = runtimeChangesBlocked(), + vkBridge = vkBridge, adreno = adrenoGpu, shortcutPicker = shortcutPicker, shortcutLibraryScanning = shortcutLibraryScanning, gameSyncFolder = gameSyncFolder, @@ -949,6 +952,7 @@ class FrontEndActivity : ComponentActivity() { onInstallPackage = { id -> installPackage(id) }, onRemovePackage = { id -> removePackage(id) }, onRuntime = { onRuntimeButton() }, + onVkBridge = { on -> SessionPrefs.setVkBridge(this, on); vkBridge = on }, onFrameGenPick = { mode -> FrameGen.set(this, mode) frameGenLabel = FrameGen.label(this) @@ -2072,6 +2076,7 @@ class FrontEndActivity : ComponentActivity() { gameCores = CpuCores.parse(SessionPrefs.gameCpus(this)).ifEmpty { CpuCores.all.toSet() } tuSysmem = SessionPrefs.tuSysmem(this) zinkLazy = SessionPrefs.zinkLazy(this) + vkBridge = SessionPrefs.vkBridge(this) noXalia = SessionPrefs.noXalia(this) fastSync = SessionPrefs.fastSync(this) syncFallback = SessionPrefs.syncFallback(this) @@ -2189,7 +2194,7 @@ class FrontEndActivity : ComponentActivity() { Thread({ checkCatalog() }, "catalog").start() return } - if (installed == null && !com.droiddeck.launcher.core.DeviceSupport.adreno()) { showNonAdreno = release; return } + if (installed == null && !com.droiddeck.launcher.core.DeviceSupport.canDraw(this)) { showNonAdreno = release; return } install(release) } @@ -2204,7 +2209,7 @@ class FrontEndActivity : ComponentActivity() { showPhantomGate = true return false } - val warn = installed == null && !com.droiddeck.launcher.core.DeviceSupport.adreno() + val warn = installed == null && !com.droiddeck.launcher.core.DeviceSupport.canDraw(this) if (warn && available != null) { showNonAdreno = available; return false } startActivity(intent) return true diff --git a/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt b/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt index 325ed7d43..1036f5c06 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt @@ -9,6 +9,11 @@ object DeviceSupport { fun adreno(): Boolean = File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists() + fun vkBridge(context: Context): Boolean = + !adreno() && com.droiddeck.launcher.session.SessionPrefs.vkBridge(context) + + fun canDraw(context: Context): Boolean = adreno() || vkBridge(context) + fun gpuName(context: Context): String { val soc = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it.isNotBlank() && it != Build.UNKNOWN } else null return soc?.let { "$it (${Build.HARDWARE})" } ?: Build.HARDWARE.ifBlank { context.getString(R.string.gpu_this_gpu) } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/ClockLogComponent.kt b/app/src/main/java/com/droiddeck/launcher/gpu/ClockLogComponent.kt new file mode 100644 index 000000000..8385deaaf --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/ClockLogComponent.kt @@ -0,0 +1,108 @@ +package com.droiddeck.launcher.gpu + +import android.content.Context +import android.os.Build +import android.os.PowerManager +import android.util.Log +import com.droiddeck.launcher.core.SessionPart +import java.io.File +import java.io.FileWriter +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class ClockLogComponent(private val logFile: File) : SessionPart() { + @Volatile private var running = false + private var thread: Thread? = null + + override fun start() { + running = true + thread = Thread({ run() }, "clock-log").apply { isDaemon = true; start() } + } + + override fun stop() { + running = false + thread?.interrupt() + thread = null + } + + private fun run() { + val policies = File("/sys/devices/system/cpu/cpufreq").listFiles { f -> f.name.startsWith("policy") } + ?.sortedBy { it.name.removePrefix("policy").toIntOrNull() ?: 0 } ?: emptyList() + val gpuCandidates = gpuSources() + val gpu = gpuCandidates.firstOrNull { !it.startsWith("/proc/") && firstNumber(it) != null } + val power = app().getSystemService(Context.POWER_SERVICE) as? PowerManager + val time = SimpleDateFormat("HH:mm:ss", Locale.US) + try { + FileWriter(logFile, true).use { w -> + w.write("== clocks: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.HARDWARE}), every ${PERIOD_MS / 1000}s\n") + w.write("cpu policies: ${policies.joinToString { p -> p.name + " " + (File(p, "related_cpus").readTextOrNull()?.trim() ?: "?") }}\n") + for (source in gpuCandidates) { + val f = File(source) + val state = when { + !f.exists() -> continue + firstNumber(source) != null -> "readable" + else -> "refused" + } + w.write("gpu source $source: $state\n") + } + w.write("gpu clock from: ${gpu ?: "nothing readable"}\n") + w.flush() + while (running && logFile.length() < MAX_BYTES) { + val line = StringBuilder(time.format(Date())) + for (p in policies) { + val cur = firstNumber(File(p, "scaling_cur_freq").path) + val max = firstNumber(File(p, "scaling_max_freq").path) + val hw = firstNumber(File(p, "cpuinfo_max_freq").path) + line.append(" ${p.name.removePrefix("policy").let { "p$it" }} ${mhz(cur)}/${mhz(max)}") + if (hw != null && max != null && max < hw) line.append("(of ${mhz(hw)})") + } + gpu?.let { line.append(" | gpu ${mhz(firstNumber(it))} MHz") } + if (power != null && Build.VERSION.SDK_INT >= 29) { + line.append(" | thermal ${power.currentThermalStatus}") + if (Build.VERSION.SDK_INT >= 30) { + val h = power.getThermalHeadroom(10) + if (!h.isNaN()) line.append(String.format(Locale.US, " headroom %.2f", h)) + } + } + w.write(line.append('\n').toString()) + w.flush() + try { Thread.sleep(PERIOD_MS) } catch (e: InterruptedException) { break } + } + } + } catch (e: Exception) { + Log.w(TAG, "clock log stopped: $e") + } + } + + companion object { + private const val TAG = "SessionService" + private const val PERIOD_MS = 2000L + private const val MAX_BYTES = 4L shl 20 + + private fun gpuSources(): List { + val list = mutableListOf( + "/sys/kernel/ged/hal/current_freqency", "/sys/kernel/ged/hal/current_frequency", + "/proc/gpufreqv2/gpufreq_status", "/proc/gpufreq/gpufreq_var_dump", + "/sys/kernel/gpu/gpu_clock", "/sys/class/misc/mali0/device/cur_freq", + "/sys/class/misc/mali0/device/clock", + ) + File("/sys/class/devfreq").list()?.sorted()?.forEach { node -> + if (node.contains("mali", true) || node.contains("gpu", true)) list.add("/sys/class/devfreq/$node/cur_freq") + } + return list.distinct() + } + + private fun File.readTextOrNull(): String? = runCatching { readText() }.getOrNull() + + private fun firstNumber(path: String): Long? = + runCatching { Regex("\\d+").find(File(path).readText())?.value?.toLong() }.getOrNull() + + private fun mhz(raw: Long?): String = when { + raw == null -> "?" + raw > 10_000_000 -> (raw / 1_000_000).toString() + raw > 10_000 -> (raw / 1_000).toString() + else -> raw.toString() + } + } +} diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java index 9978409c5..46b09e1e8 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java @@ -284,6 +284,10 @@ public final class TurnipDriver { } String quest = QuestExperimental.INSTANCE.defaultAndroidDriver(); if (quest != null) return quest; + if (com.droiddeck.launcher.core.DeviceSupport.INSTANCE.vkBridge(context)) { + Log.i(TAG, "Mali bridge: the compositor uses the system Vulkan driver"); + return null; + } int model = GpuInfo.Companion.detect().getModel(); Log.i(TAG, "gpu model: " + (model == 0 ? "unknown" : model)); return chooseByModel(model, android.os.Build.VERSION.SDK_INT); diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt b/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt new file mode 100644 index 000000000..599e5e5d8 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt @@ -0,0 +1,61 @@ +package com.droiddeck.launcher.gpu + +import android.os.Process +import android.util.Log +import com.droiddeck.launcher.core.HostProcess +import com.droiddeck.launcher.core.SessionPart +import java.io.File + +class VkBridgeComponent(private val socket: File, private val logFile: File) : SessionPart() { + @Volatile private var pid = -1 + @Volatile private var exited = false + + override fun start() { + stop() + val binary = File(app().applicationInfo.nativeLibraryDir, BINARY) + if (!binary.isFile) { + Log.e(TAG, "server missing at $binary; the session will have no Vulkan") + return + } + socket.parentFile?.mkdirs() + val cacheDir = File(app().cacheDir, "vkbridge-pipelines").apply { mkdirs() } + if (VkBridgeNative.start(app(), socket, logFile, cacheDir)) { + Log.i(TAG, "server ready in the app's process at $socket") + return + } + socket.delete() + exited = false + val cmd = listOf(binary.absolutePath, "--socket", socket.absolutePath, "--log", logFile.absolutePath, + "--cache-dir", cacheDir.absolutePath) + .joinToString(" ") { it.replace("\\", "\\\\").replace(" ", "\\ ") } + val env = arrayOf("HOME=" + app().filesDir) + pid = HostProcess.start(cmd, env, app().filesDir, { status -> + exited = true + Log.w(TAG, "server exited with status $status") + runCatching { logFile.appendText("== vkbridge server exited with status $status\n") } + }, { line -> Log.i(TAG, line) }) + val deadline = System.currentTimeMillis() + 15_000L + while (!socket.exists() && !exited && System.currentTimeMillis() < deadline) { + try { Thread.sleep(50L) } catch (e: InterruptedException) { Thread.currentThread().interrupt(); break } + } + Log.i(TAG, if (socket.exists()) "server ready (pid $pid) at $socket" else "server NOT ready (pid $pid, exited=$exited); see $logFile") + if (!socket.exists() && VkBridgeNative.start(app(), socket, logFile, cacheDir)) Log.i(TAG, "server ready in the app's process at $socket") + } + + override fun stop() { + if (pid > 0) { + Process.killProcess(pid) + pid = -1 + } + if (!VkBridgeNative.serving) socket.delete() + } + + override fun suspendPid(): Int = pid + + companion object { + private const val TAG = "VkBridge" + const val BINARY = "libvkbridge_server.so" + const val GUEST_ICD = "/usr/local/share/vkbridge/droidbridge_icd.json" + const val GUEST_LIB = "/usr/local/lib/vkbridge/libvulkan_droidbridge.so" + } +} diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeNative.kt b/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeNative.kt new file mode 100644 index 000000000..97efa7bed --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeNative.kt @@ -0,0 +1,49 @@ +package com.droiddeck.launcher.gpu + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.util.Log +import java.io.File + +object VkBridgeNative { + private const val TAG = "VkBridge" + + @Volatile private var loaded: Boolean? = null + + private fun load(context: Context): Boolean { + loaded?.let { return it } + val ok = try { + val dev = File(context.filesDir, "vkbridge/dev/libvkbridge_jni.so") + val debuggable = context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 + if (debuggable && dev.isFile) { + Log.w(TAG, "in-app server from the development override $dev") + System.load(dev.absolutePath) + } else { + System.loadLibrary("vkbridge_jni") + } + true + } catch (e: Throwable) { + Log.w(TAG, "in-app server unavailable: $e") + false + } + loaded = ok + return ok + } + + @Volatile var serving = false + private set + + fun start(context: Context, socket: File, log: File, cacheDir: File): Boolean { + if (!load(context)) return false + val ok = try { + nativeStart(socket.absolutePath, log.absolutePath, cacheDir.absolutePath) == 1 + } catch (e: Throwable) { + Log.w(TAG, "in-app server failed to start: $e") + false + } + if (ok) serving = true + return ok + } + + @JvmStatic private external fun nativeStart(socket: String, log: String, cacheDir: String): Int +} 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 81fabd7f7..c6e1d802b 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt @@ -189,6 +189,7 @@ object BwrapSpawner { private const val GPU_DIR = "/run/droiddeck-gpu" private fun gpu(rootfs: String, root: File, env: MutableMap): List? { + env["VKBRIDGE_SOCKET"]?.let { sock -> if (File(sock).exists()) return bridgeGpu(rootfs, root, env, sock) } val lib = File(rootfs, "usr/lib") val driver = File(lib, "libvulkan_freedreno.so") if (!driver.isFile || !File("/dev/kgsl-3d0").exists()) return null @@ -213,6 +214,28 @@ object BwrapSpawner { return binds } + private fun bridgeGpu(rootfs: String, root: File, env: MutableMap, socket: String): List? { + val driver = File(rootfs, "usr/local/lib/vkbridge/libvulkan_droidbridge.so") + if (!driver.isFile) { Log.w(TAG, "gpu: vkbridge session but no bridge ICD in the runtime"); return null } + val binds = ArrayList() + binds.add(Bind(driver.canonicalPath, "$GPU_DIR/libvulkan_droidbridge.so")) + val sockDir = File(socket).parent ?: return null + binds.add(Bind(sockDir, sockDir)) + val icd = File(root.parentFile, "vkbridge_icd.json") + val api = runCatching { + JSONObject(File(rootfs, "usr/local/share/vkbridge/droidbridge_icd.json").readText()).getJSONObject("ICD").getString("api_version") + }.getOrNull() ?: run { Log.w(TAG, "gpu: vkbridge session but the runtime's bridge manifest is unreadable"); return null } + icd.writeText("{\"file_format_version\": \"1.0.0\", \"ICD\": {\"library_path\": \"$GPU_DIR/libvulkan_droidbridge.so\", \"api_version\": \"$api\"}}\n") + binds.add(Bind(icd.path, "$GPU_DIR/vkbridge_icd.json")) + env["VK_DRIVER_FILES"] = "$GPU_DIR/vkbridge_icd.json" + env["VK_ICD_FILENAMES"] = "$GPU_DIR/vkbridge_icd.json" + if (!env.containsKey("MESA_LOADER_DRIVER_OVERRIDE") && !env.containsKey("GALLIUM_DRIVER")) { + env["MESA_LOADER_DRIVER_OVERRIDE"] = "zink" + env["GALLIUM_DRIVER"] = "zink" + } + return binds + } + 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() diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java index d02fdf0c2..49c4724ee 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java @@ -280,10 +280,32 @@ public final class LinuxRuntime { return false; } + private static final String MALI_DEVICE = "/dev/mali0"; + + public static String gpuStandIn(Context context) { + if (new File(KGSL_DEVICE).exists()) return KGSL_DEVICE; + if (com.droiddeck.launcher.core.DeviceSupport.INSTANCE.vkBridge(context) && new File(MALI_DEVICE).exists()) return MALI_DEVICE; + return null; + } + + public static String deviceNumbers(String path) { + try { + long dev = Os.stat(path).st_rdev; + long major = ((dev >> 8) & 0xfff) | ((dev >> 32) & ~0xfffL); + long minor = (dev & 0xff) | ((dev >> 12) & ~0xffL); + return major + ":" + minor; + } catch (ErrnoException e) { + return null; + } + } + private static void bindGpuNode(Context context, List cmd) { + String standIn = gpuStandIn(context); + if (standIn == null) return; + boolean kgsl = standIn.equals(KGSL_DEVICE); StructStat st; try { - st = Os.stat(KGSL_DEVICE); + st = Os.stat(standIn); } catch (ErrnoException e) { return; } @@ -303,13 +325,14 @@ public final class LinuxRuntime { Files.write(new File(drm, "dev").toPath(), (major + ":" + minor + "\n").getBytes(StandardCharsets.UTF_8)); Files.write(new File(device, "uevent").toPath(), - "DRIVER=kgsl-3d0\nMODALIAS=platform:kgsl-3d0\n".getBytes(StandardCharsets.UTF_8)); + (kgsl ? "DRIVER=kgsl-3d0\nMODALIAS=platform:kgsl-3d0\n" : "DRIVER=mali\nMODALIAS=platform:mali\n") + .getBytes(StandardCharsets.UTF_8)); File subsystem = new File(device, "subsystem"); if (!Files.isSymbolicLink(subsystem.toPath())) { Os.symlink("/sys/bus/platform", subsystem.getPath()); } File driver = new File(device, "driver"); - if (!Files.isSymbolicLink(driver.toPath())) { + if (kgsl && !Files.isSymbolicLink(driver.toPath())) { Os.symlink("/sys/bus/platform/drivers/msm_drm", driver.getPath()); } } catch (IOException | ErrnoException e) { @@ -317,7 +340,7 @@ public final class LinuxRuntime { } bind(cmd, new File(base, "sys").getPath() + ":/sys/dev/char"); bind(cmd, dri.getPath() + ":/dev/dri"); - bind(cmd, KGSL_DEVICE + ":/dev/dri/" + node); + bind(cmd, standIn + ":/dev/dri/" + node); bindDrmClass(base, cmd, node, major + ":" + minor); } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt index d9c59b1b8..6979ff3b3 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt @@ -154,6 +154,9 @@ object SessionFiles { "usr/local/bin/gamescope" to "usr/local/bin/gamescope", // The gamescope pack carries its own WSI layer (patch 0117); it replaces the runtime's stock copy. "usr/lib/libVkLayer_FROG_gamescope_wsi_aarch64.so" to "usr/lib/libVkLayer_FROG_gamescope_wsi_aarch64.so", + "usr/local/lib/vkbridge/libvulkan_droidbridge.so" to "usr/local/lib/vkbridge/libvulkan_droidbridge.so", + "usr/local/share/vkbridge/droidbridge_icd.json" to "usr/local/share/vkbridge/droidbridge_icd.json", + "usr/local/bin/droiddeck-vkbridge-smoke" to "usr/local/bin/droiddeck-vkbridge-smoke", "usr/local/lib/droiddeck/uruntime" to "usr/local/lib/droiddeck/uruntime", "usr/local/lib/droiddeck-pipewire.tzst" to "usr/local/lib/droiddeck-pipewire.tzst", "usr/local/share/licenses/uruntime/LICENSE" to "usr/local/share/licenses/uruntime/LICENSE", diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index 8ffaacd56..104c3572d 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -596,6 +596,13 @@ object SessionPrefs { fun zinkLazy(context: Context): Boolean = prefs(context).getBoolean("zinkLazy", true) + @JvmStatic + fun vkBridge(context: Context): Boolean = prefs(context).getBoolean("vkBridge", false) + + fun setVkBridge(context: Context, on: Boolean) { + prefs(context).edit().putBoolean("vkBridge", on).apply() + } + fun setZinkLazy(context: Context, on: Boolean) { prefs(context).edit().putBoolean("zinkLazy", on).apply() } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index be9795042..7dcf5a361 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -496,6 +496,7 @@ class SessionService : Service() { components.add(encoder) guest.add("BL_VENC_SOCKET=" + encoder.socket.absolutePath) } + startVkBridge(guest, sessionDir) guest.add("BL_WIDTH=" + size.first) guest.add("BL_HEIGHT=" + size.second) @@ -760,7 +761,7 @@ class SessionService : Service() { ?: SessionPrefs.androidDriver(this), linuxDriverId, ) - LinuxVulkanDriver.resolveIcdPath(this, linuxDriverId) + if (!com.droiddeck.launcher.core.DeviceSupport.vkBridge(this)) LinuxVulkanDriver.resolveIcdPath(this, linuxDriverId) ?.let { guest.add(LinuxVulkanDriver.ENV + "=" + it) } val tuOverride = tuDebugOverride() val tuDriver = sysmemDriver(linuxDriverId) @@ -814,6 +815,26 @@ class SessionService : Service() { private fun noSteamGame(): Boolean = SessionState.mode == MODE_RUN && SessionState.program in setOf(NoSteamLaunch.PROTON_RUN, NoSteamLaunch.EPIC, NoSteamLaunch.BATTLENET) + private fun startVkBridge(guest: MutableList, sessionDir: File) { + if (!com.droiddeck.launcher.core.DeviceSupport.vkBridge(this)) return + val socket = File(filesDir, "vkbridge/server.sock") + val bridge = com.droiddeck.launcher.gpu.VkBridgeComponent(socket, File(sessionDir, "vkbridge-server.log")) + bridge.attach(this) + components.add(bridge) + com.droiddeck.launcher.gpu.ClockLogComponent(File(sessionDir, "vkbridge-clocks.log")).let { + it.attach(this) + components.add(it) + } + guest.add("BL_VKBRIDGE=1") + guest.add("VKBRIDGE_SOCKET=" + socket.path) + guest.add("VKBRIDGE_CLIENT_LOG=" + File(sessionDir, "vkbridge-client.log").path) + LinuxRuntime.gpuStandIn(this)?.let { node -> + guest.add("BL_DRM_STANDIN=$node") + LinuxRuntime.deviceNumbers(node)?.let { guest.add("VKBRIDGE_DRM_RENDER=$it") } + } + Log.i(TAG, "vkbridge: on (${com.droiddeck.launcher.core.DeviceSupport.gpuName(this)}), socket $socket") + } + private fun startAudio(guest: MutableList, sessionDir: File, directAudioPack: Boolean): PulseAudioComponent { val wantsDirectAudio = directAudioPack && (SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP || noSteamGame()) && diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt index 4df261e8d..2391044fa 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -137,6 +137,8 @@ class FrontEndState( val oscMode: String = SessionPrefs.OSC_AUTO, val controller: com.droiddeck.launcher.input.ControllerPrefs.Settings? = null, val phantomProcessStatus: PhantomProcessStatus = PhantomProcessStatus.NOT_APPLICABLE, + val adreno: Boolean = true, + val vkBridge: Boolean = false, val showPhantomGate: Boolean = false, val orientation: Int = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, val launcherOrientation: Int = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER, @@ -193,6 +195,7 @@ class FrontEndActions( val onInstallPackage: (String) -> Unit, val onRemovePackage: (String) -> Unit, val onRuntime: () -> Unit, + val onVkBridge: (Boolean) -> Unit = {}, val onFrameGenPick: (FrameGen.Mode) -> Unit, val onImportLossless: () -> Unit, val onProtons: () -> Unit, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt index 43926aa6b..5db7900c6 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -235,9 +235,16 @@ internal fun SetupPanel( when (gpu.support) { com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED -> stringResource(R.string.setup_gpu_detail, gpu.displayName(ctx), gpuName) com.droiddeck.launcher.gpu.GpuInfo.Support.UNTESTED -> stringResource(R.string.setup_gpu_untested_detail, gpu.displayName(ctx), gpu.supportText(ctx).replaceFirstChar { it.lowercase() }) - else -> stringResource(R.string.setup_gpu_unsupported_detail, gpuName) + else -> if (s.vkBridge) stringResource(R.string.setup_gpu_bridge_detail, gpuName) + else stringResource(R.string.setup_gpu_unsupported_detail, gpuName) }, divider = divider, + action = if (s.adreno) null else { { + SecondaryButton( + stringResource(if (s.vkBridge) R.string.setup_gpu_bridge_on else R.string.setup_gpu_bridge_off), + enabled = !s.sessionRunning, compact = true, onClick = { a.onVkBridge(!s.vkBridge) }, + ) + } }, ) } val runtimeCheck: @Composable (Boolean) -> Unit = { divider -> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 840e26ebb..ebf2ff2bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3226,4 +3226,7 @@ Cloud settings Xbox Cloud Gaming Game Pass + Mali bridge: off + Mali bridge: on + %1$s is not an Adreno. The experimental Vulkan bridge is on: the session draws through the phone\'s own driver. Expect bugs; send the session logs. diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env index a854bb1d7..0bab42b21 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env @@ -371,6 +371,14 @@ def apply_config(env, config, app_id, fixes=None): return result +def bridge_feature_level(env): + if env.get("BL_VKBRIDGE") != "1" or env.get("VKD3D_FEATURE_LEVEL") not in ("12_1", "12_2"): + return None + was = env["VKD3D_FEATURE_LEVEL"] + env["VKD3D_FEATURE_LEVEL"] = "12_0" + return "Mali bridge: VKD3D_FEATURE_LEVEL %s capped at 12_0 (no ray tracing or mesh shaders)" % was + + def sync_config(config, app_id): games = config.get("games", {}) return { @@ -613,6 +621,9 @@ def main(): if mic_enabled == "0": env["BANNER_AUDIO_DIRECT_MIC"] = "0" env = vram_cap(env) + note = bridge_feature_level(env) + if note: + print("droiddeck-game-env: " + note, file=sys.stderr) env = force_ssbs(command, env) game_cores(env) try: diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index 0f99643ee..37eef3b4d 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -114,6 +114,32 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then fi unset bl_icd_lib fi +if [ "${BL_VKBRIDGE:-0}" = 1 ]; then + bl_vkb_icd=/usr/local/share/vkbridge/droidbridge_icd.json + if [ -r "$bl_vkb_icd" ] && [ -r /usr/local/lib/vkbridge/libvulkan_droidbridge.so ] && [ -S "${VKBRIDGE_SOCKET:-}" ]; then + export VK_DRIVER_FILES=$bl_vkb_icd VK_ICD_FILENAMES=$bl_vkb_icd + export VKBRIDGE_SOCKET VKBRIDGE_CLIENT_LOG + if [ -r /root/.vkbridge-env ]; then + while IFS='=' read -r _k _v; do + case "$_k" in VKBRIDGE_*|BL_VKBRIDGE_*) export "$_k=$_v" ;; esac + done < /root/.vkbridge-env + [ -z "${BL_INSIDE:-}" ] && echo "== vkbridge: development settings from /root/.vkbridge-env: $(cut -d= -f1 /root/.vkbridge-env | tr '\n' ' ')" + fi + export VKD3D_FEATURE_LEVEL=${VKD3D_FEATURE_LEVEL:-12_0} + unset TU_DEBUG + if [ -z "${BL_INSIDE:-}" ]; then + step "vkbridge self-test" + timeout 60 /usr/local/bin/droiddeck-vkbridge-smoke 2>&1 | sed 's/^/== vkbridge: /' + if command -v vulkaninfo >/dev/null 2>&1; then + timeout 30 vulkaninfo --summary 2>&1 | sed -n '/^Devices:/,$p' | head -40 | sed 's/^/== vulkaninfo: /' + fi + fi + echo "== vulkan driver: Mali bridge ($bl_vkb_icd, server at $VKBRIDGE_SOCKET)" + else + echo "== vulkan driver: Mali bridge requested but NOT usable (icd $bl_vkb_icd, socket ${VKBRIDGE_SOCKET:-unset}); this GPU will show a black screen" + fi + unset bl_vkb_icd +fi export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0} start_agent() { @@ -955,6 +981,16 @@ BUSCONF _bl_cef=(-cef-disable-gpu) _bl_ui=(-nobigpicture) fi + if [ "${BL_VKBRIDGE:-0}" = 1 ]; then + if [ "${BL_VKBRIDGE_CEF_GPU:-1}" = 0 ]; then + _bl_cef=(-cef-disable-gpu -cef-disable-gpu-compositing) + echo "== steam: browser UI in software (Mali bridge)" + elif [ "${_bl_cef[0]}" = -cef-force-gpu ]; then + _bl_cef=(-cef-force-gpu -cef-use-gl=angle -cef-use-angle="${BL_VKBRIDGE_CEF_ANGLE:-vulkan}") + fi + [ "${BL_VKBRIDGE_CEF_LOG:-0}" = 1 ] && _bl_cef+=(-cef-enable-logging=stderr -cef-v=1) + [ -n "${BL_VKBRIDGE_CEF_EXTRA:-}" ] && read -r -a _bl_cef_extra <<<"$BL_VKBRIDGE_CEF_EXTRA" && _bl_cef+=("${_bl_cef_extra[@]}") + fi if [ "${BL_STEAM_UI:-}" != desktop ]; then if [ "$steam_x64" = 0 ] || [ "${BL_X64_DECK:-1}" != 0 ]; then _bl_ui+=(-steamdeck -steamos3) diff --git a/tools/linuxfs/preload/drm.c b/tools/linuxfs/preload/drm.c index 101512387..37784c3d9 100644 --- a/tools/linuxfs/preload/drm.c +++ b/tools/linuxfs/preload/drm.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -34,7 +35,8 @@ static int is_kgsl(int fd) { struct stat st; pthread_mutex_lock(&lock); if (kgsl_state == 0) { - kgsl_state = stat("/dev/kgsl-3d0", &st) == 0 ? 1 : -1; + const char *standin = getenv("BL_DRM_STANDIN"); + kgsl_state = stat("/dev/kgsl-3d0", &st) == 0 || (standin && *standin && stat(standin, &st) == 0) ? 1 : -1; kgsl_dev = st.st_rdev; } pthread_mutex_unlock(&lock); @@ -49,18 +51,42 @@ static void *real(const char *name) { return fn; } +static int same_by_flock(int a, int b) { + struct stat sa, sb; + if (fstat(a, &sa) || fstat(b, &sb) || sa.st_dev != sb.st_dev || sa.st_ino != sb.st_ino || + flock(a, LOCK_EX | LOCK_NB)) { + return 0; + } + int same = flock(b, LOCK_EX | LOCK_NB) == 0; + flock(a, LOCK_UN); + return same; +} + static int same_file_description(int a, int b) { if (a == b) { return 1; } -#ifdef SYS_kcmp int saved_errno = errno; - long ret = syscall(SYS_kcmp, getpid(), getpid(), 0 , a, b); - errno = saved_errno; - return ret == 0; -#else - return 0; + int ret = -1; +#ifdef SYS_kcmp + static int kcmp_missing; + if (!kcmp_missing) { + long k = syscall(SYS_kcmp, getpid(), getpid(), 0, a, b); + if (k >= 0) { + ret = k == 0; + } else if (errno == ENOSYS || errno == EPERM) { + kcmp_missing = 1; + } else { + ret = 0; + } + } #endif + if (ret < 0) { + const char *standin = getenv("BL_DRM_STANDIN"); + ret = standin && *standin ? same_by_flock(a, b) : 0; + } + errno = saved_errno; + return ret; } static int find_handle_locked(int fd, uint32_t handle) { diff --git a/tools/tests/test_game_environment.py b/tools/tests/test_game_environment.py index b4bc05aad..c6aadd0cd 100644 --- a/tools/tests/test_game_environment.py +++ b/tools/tests/test_game_environment.py @@ -192,6 +192,15 @@ class GameEnvironmentTest(unittest.TestCase): self.assertEqual(evaluate(), (0, 2)) self.assertEqual(evaluate("changed"), (0, 3)) + def test_the_mali_bridge_caps_the_vkd3d_feature_level(self): + bridge = {"BL_VKBRIDGE": "1", "VKD3D_FEATURE_LEVEL": "12_2"} + self.assertIn("capped", MODULE["bridge_feature_level"](bridge)) + self.assertEqual(bridge["VKD3D_FEATURE_LEVEL"], "12_0") + for env in ({"BL_VKBRIDGE": "1", "VKD3D_FEATURE_LEVEL": "11_1"}, {"VKD3D_FEATURE_LEVEL": "12_2"}, {"BL_VKBRIDGE": "1"}): + before = dict(env) + self.assertIsNone(MODULE["bridge_feature_level"](env)) + self.assertEqual(env, before) + def test_profile_precedence_and_unset(self): env = {"KEEP": "inherited", "REMOVE": "inherited", "CUSTOM": "launch option"} config = {"version": 1, "shared": {"CUSTOM": "shared"}, "games": {"42": {"REMOVE": None, "CUSTOM": "game", "EMPTY": ""}}} From b3c5570bdddc21ca228e380aa0224f089b1cb2bf Mon Sep 17 00:00:00 2001 From: Ahmad Sayed Alsayed Ebrahim Date: Wed, 14 Oct 2026 17:54:17 +0300 Subject: [PATCH 06/21] Mali bridge sessions: keep Steam's web helper GPU process out of process Steam's web helper now runs its GPU work inside the browser process (--in-process-gpu --no-zygote, #566). On the Mali bridge that browser stops answering after its first window, Steam restarts it every 10 s and then quits before the login page. Bridge sessions keep the stock process layout (BL_WEBHELPER_ARGS=stock); BL_VKBRIDGE_WEBHELPER_ARGS overrides it for tests. Adreno sessions are unchanged. --- tools/linuxfs/overlay/usr/local/bin/droiddeck-session | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index 37eef3b4d..98f105365 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -990,6 +990,8 @@ BUSCONF fi [ "${BL_VKBRIDGE_CEF_LOG:-0}" = 1 ] && _bl_cef+=(-cef-enable-logging=stderr -cef-v=1) [ -n "${BL_VKBRIDGE_CEF_EXTRA:-}" ] && read -r -a _bl_cef_extra <<<"$BL_VKBRIDGE_CEF_EXTRA" && _bl_cef+=("${_bl_cef_extra[@]}") + export BL_WEBHELPER_ARGS="${BL_VKBRIDGE_WEBHELPER_ARGS:-stock}" + echo "== steam: web helper process layout: $BL_WEBHELPER_ARGS (Mali bridge)" fi if [ "${BL_STEAM_UI:-}" != desktop ]; then if [ "$steam_x64" = 0 ] || [ "${BL_X64_DECK:-1}" != 0 ]; then From e04eb840909ae4dbe59c1fa50305de8c6a9fc785 Mon Sep 17 00:00:00 2001 From: Ahmad Sayed Alsayed Ebrahim Date: Thu, 15 Oct 2026 05:09:39 +0300 Subject: [PATCH 07/21] Mali bridge: keep the server's cache directory in the app's files, not its cache Android empties an app's cacheDir when storage runs low; it took the bridge's pipeline caches, the probe profile, stuck-pipelines and the env file from under a running server. The directory is now files/vkbridge-pipelines (--cache-dir and the in-app start alike), and an existing cache/vkbridge-pipelines is moved there once. Nothing trims filesDir, so the server's own limits bound it (64 MB per pipeline cache file, a total limit in newer servers). --- .../launcher/gpu/VkBridgeComponent.kt | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt b/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt index 599e5e5d8..8033d8eff 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt @@ -1,5 +1,6 @@ package com.droiddeck.launcher.gpu +import android.content.Context import android.os.Process import android.util.Log import com.droiddeck.launcher.core.HostProcess @@ -18,7 +19,7 @@ class VkBridgeComponent(private val socket: File, private val logFile: File) : S return } socket.parentFile?.mkdirs() - val cacheDir = File(app().cacheDir, "vkbridge-pipelines").apply { mkdirs() } + val cacheDir = cacheDir(app()) if (VkBridgeNative.start(app(), socket, logFile, cacheDir)) { Log.i(TAG, "server ready in the app's process at $socket") return @@ -54,6 +55,30 @@ class VkBridgeComponent(private val socket: File, private val logFile: File) : S companion object { private const val TAG = "VkBridge" + private const val CACHE_DIR = "vkbridge-pipelines" + + fun cacheDir(context: Context): File { + val dir = File(context.filesDir, CACHE_DIR) + val old = File(context.cacheDir, CACHE_DIR) + if (old.isDirectory) migrate(old, dir) + dir.mkdirs() + return dir + } + + private fun migrate(old: File, dir: File) { + val moved = if (!dir.exists()) old.renameTo(dir) else false + if (!moved) { + dir.mkdirs() + old.listFiles()?.forEach { f -> + val to = File(dir, f.name) + if (to.exists()) return@forEach + if (!f.renameTo(to)) runCatching { f.copyRecursively(to, overwrite = false) } + } + old.deleteRecursively() + } + Log.i(TAG, "bridge cache moved from $old to $dir") + } + const val BINARY = "libvkbridge_server.so" const val GUEST_ICD = "/usr/local/share/vkbridge/droidbridge_icd.json" const val GUEST_LIB = "/usr/local/lib/vkbridge/libvulkan_droidbridge.so" From 5b35ace098bfb76a30184d5ca0474dc53a25b6a5 Mon Sep 17 00:00:00 2001 From: Ahmad Sayed Alsayed Ebrahim Date: Thu, 15 Oct 2026 06:16:25 +0300 Subject: [PATCH 08/21] Mali bridge: Setup's "Safe mode" switch, on by default A second row under the GPU check, shown only when the bridge is on: "Safe mode" keeps the bridge's proven workarounds wherever its startup probe has not verified this GPU and driver; off is performance mode, native paths wherever the probe passed. It reaches the server as /mode ("safe" or "performance", written atomically before the server starts or is reused, so the in-app server's JNI start stays as it is) and the guest as VKBRIDGE_MODE, which /root/.vkbridge-env can still override. A server without modes ignores the file. --- .../com/droiddeck/launcher/FrontEndActivity.kt | 5 ++++- .../droiddeck/launcher/gpu/VkBridgeComponent.kt | 13 +++++++++++++ .../com/droiddeck/launcher/session/SessionPrefs.kt | 10 ++++++++++ .../droiddeck/launcher/session/SessionService.kt | 3 ++- .../com/droiddeck/launcher/ui/FrontEndScreen.kt | 2 ++ .../com/droiddeck/launcher/ui/FrontEndSetup.kt | 14 +++++++++++++- app/src/main/res/values/strings.xml | 5 +++++ 7 files changed, 49 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt index 2d027f267..3fcf7dbd2 100644 --- a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt @@ -149,6 +149,7 @@ class FrontEndActivity : ComponentActivity() { private var tuSysmem by mutableStateOf(false) private var zinkLazy by mutableStateOf(false) private var vkBridge by mutableStateOf(false) + private var vkBridgeSafe by mutableStateOf(true) private val adrenoGpu by lazy { com.droiddeck.launcher.core.DeviceSupport.adreno() } private var noXalia by mutableStateOf(true) private var fastSync by mutableStateOf(false) @@ -811,7 +812,7 @@ class FrontEndActivity : ComponentActivity() { FrontEndScreen( FrontEndState( installed = installed, ready = ready, available = available?.version, removalPending = removalPending, runtimeActionsBlocked = runtimeChangesBlocked(), - vkBridge = vkBridge, adreno = adrenoGpu, + vkBridge = vkBridge, vkBridgeSafe = vkBridgeSafe, adreno = adrenoGpu, shortcutPicker = shortcutPicker, shortcutLibraryScanning = shortcutLibraryScanning, gameSyncFolder = gameSyncFolder, @@ -953,6 +954,7 @@ class FrontEndActivity : ComponentActivity() { onRemovePackage = { id -> removePackage(id) }, onRuntime = { onRuntimeButton() }, onVkBridge = { on -> SessionPrefs.setVkBridge(this, on); vkBridge = on }, + onVkBridgeSafe = { on -> SessionPrefs.setVkBridgeSafe(this, on); vkBridgeSafe = on }, onFrameGenPick = { mode -> FrameGen.set(this, mode) frameGenLabel = FrameGen.label(this) @@ -2077,6 +2079,7 @@ class FrontEndActivity : ComponentActivity() { tuSysmem = SessionPrefs.tuSysmem(this) zinkLazy = SessionPrefs.zinkLazy(this) vkBridge = SessionPrefs.vkBridge(this) + vkBridgeSafe = SessionPrefs.vkBridgeSafe(this) noXalia = SessionPrefs.noXalia(this) fastSync = SessionPrefs.fastSync(this) syncFallback = SessionPrefs.syncFallback(this) diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt b/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt index 8033d8eff..d606348ff 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt @@ -20,6 +20,7 @@ class VkBridgeComponent(private val socket: File, private val logFile: File) : S } socket.parentFile?.mkdirs() val cacheDir = cacheDir(app()) + writeMode(cacheDir, com.droiddeck.launcher.session.SessionPrefs.vkBridgeMode(app())) if (VkBridgeNative.start(app(), socket, logFile, cacheDir)) { Log.i(TAG, "server ready in the app's process at $socket") return @@ -56,6 +57,7 @@ class VkBridgeComponent(private val socket: File, private val logFile: File) : S companion object { private const val TAG = "VkBridge" private const val CACHE_DIR = "vkbridge-pipelines" + private const val MODE_FILE = "mode" fun cacheDir(context: Context): File { val dir = File(context.filesDir, CACHE_DIR) @@ -65,6 +67,17 @@ class VkBridgeComponent(private val socket: File, private val logFile: File) : S return dir } + fun writeMode(dir: File, mode: String) { + val file = File(dir, MODE_FILE) + if (runCatching { file.readText().trim() }.getOrNull() == mode) return + val tmp = File(dir, "$MODE_FILE.tmp") + runCatching { + tmp.writeText(mode + "\n") + if (!tmp.renameTo(file)) error("rename failed") + Log.i(TAG, "bridge mode: $mode") + }.onFailure { Log.w(TAG, "could not write $file: $it"); tmp.delete() } + } + private fun migrate(old: File, dir: File) { val moved = if (!dir.exists()) old.renameTo(dir) else false if (!moved) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index 104c3572d..34ae95149 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -603,6 +603,16 @@ object SessionPrefs { prefs(context).edit().putBoolean("vkBridge", on).apply() } + @JvmStatic + fun vkBridgeSafe(context: Context): Boolean = prefs(context).getBoolean("vkBridgeSafe", true) + + fun setVkBridgeSafe(context: Context, on: Boolean) { + prefs(context).edit().putBoolean("vkBridgeSafe", on).apply() + } + + @JvmStatic + fun vkBridgeMode(context: Context): String = if (vkBridgeSafe(context)) "safe" else "performance" + fun setZinkLazy(context: Context, on: Boolean) { prefs(context).edit().putBoolean("zinkLazy", on).apply() } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index 7dcf5a361..4dfdca431 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -828,11 +828,12 @@ class SessionService : Service() { guest.add("BL_VKBRIDGE=1") guest.add("VKBRIDGE_SOCKET=" + socket.path) guest.add("VKBRIDGE_CLIENT_LOG=" + File(sessionDir, "vkbridge-client.log").path) + guest.add("VKBRIDGE_MODE=" + SessionPrefs.vkBridgeMode(this)) LinuxRuntime.gpuStandIn(this)?.let { node -> guest.add("BL_DRM_STANDIN=$node") LinuxRuntime.deviceNumbers(node)?.let { guest.add("VKBRIDGE_DRM_RENDER=$it") } } - Log.i(TAG, "vkbridge: on (${com.droiddeck.launcher.core.DeviceSupport.gpuName(this)}), socket $socket") + Log.i(TAG, "vkbridge: on (${com.droiddeck.launcher.core.DeviceSupport.gpuName(this)}, ${SessionPrefs.vkBridgeMode(this)} mode), socket $socket") } private fun startAudio(guest: MutableList, sessionDir: File, directAudioPack: Boolean): PulseAudioComponent { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt index 2391044fa..d9ee66632 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -139,6 +139,7 @@ class FrontEndState( val phantomProcessStatus: PhantomProcessStatus = PhantomProcessStatus.NOT_APPLICABLE, val adreno: Boolean = true, val vkBridge: Boolean = false, + val vkBridgeSafe: Boolean = true, val showPhantomGate: Boolean = false, val orientation: Int = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, val launcherOrientation: Int = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER, @@ -196,6 +197,7 @@ class FrontEndActions( val onRemovePackage: (String) -> Unit, val onRuntime: () -> Unit, val onVkBridge: (Boolean) -> Unit = {}, + val onVkBridgeSafe: (Boolean) -> Unit = {}, val onFrameGenPick: (FrameGen.Mode) -> Unit, val onImportLossless: () -> Unit, val onProtons: () -> Unit, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt index 5db7900c6..e93832a7e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -238,7 +238,7 @@ internal fun SetupPanel( else -> if (s.vkBridge) stringResource(R.string.setup_gpu_bridge_detail, gpuName) else stringResource(R.string.setup_gpu_unsupported_detail, gpuName) }, - divider = divider, + divider = divider || (!s.adreno && s.vkBridge), action = if (s.adreno) null else { { SecondaryButton( stringResource(if (s.vkBridge) R.string.setup_gpu_bridge_on else R.string.setup_gpu_bridge_off), @@ -246,6 +246,18 @@ internal fun SetupPanel( ) } }, ) + if (!s.adreno && s.vkBridge) CheckRow( + CheckState.OK, + stringResource(R.string.setup_bridge_safe), + stringResource(if (s.vkBridgeSafe) R.string.setup_bridge_safe_on_detail else R.string.setup_bridge_safe_off_detail), + divider = divider, + action = { + SecondaryButton( + stringResource(if (s.vkBridgeSafe) R.string.setup_bridge_safe_on else R.string.setup_bridge_safe_off), + enabled = !s.sessionRunning, compact = true, onClick = { a.onVkBridgeSafe(!s.vkBridgeSafe) }, + ) + }, + ) } val runtimeCheck: @Composable (Boolean) -> Unit = { divider -> CheckRow( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ebf2ff2bf..7995209fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3228,5 +3228,10 @@ Game Pass Mali bridge: off Mali bridge: on + Safe mode + Safe mode: on + Safe mode: off + Most compatible. Recommended for a new device. + Faster where this GPU passed its checks. %1$s is not an Adreno. The experimental Vulkan bridge is on: the session draws through the phone\'s own driver. Expect bugs; send the session logs. From 9c28b41d49052bf80318ee0fc38cf4490f447cd5 Mon Sep 17 00:00:00 2001 From: Ahmad Sayed Alsayed Ebrahim Date: Sat, 17 Oct 2026 23:48:39 +0300 Subject: [PATCH 09/21] droiddeck-session: wait up to 10 s for the Mali bridge socket before falling back The bridge check tested the server's socket once. It now polls every 50 ms (BL_VKBRIDGE_WAIT_MS, default 10000) on the first pass, logs how long the socket took, stops waiting when the session is told to stop, and names what is missing (icd, library, socket) when it falls back. --- .../overlay/usr/local/bin/droiddeck-session | 47 ++++++- tools/tests/test_session_vkbridge_wait.py | 127 ++++++++++++++++++ 2 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 tools/tests/test_session_vkbridge_wait.py diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index 98f105365..9d9c6d331 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -97,7 +97,7 @@ collect_steam_logs() { cp "$fex_state/status.json" "$fex_state/config/Config.json" "$BL_DEBUG_DIR/droiddeck-fex/" 2>/dev/null fi } -trap 'collect_steam_logs 2>/dev/null || true' TERM INT HUP +trap 'bl_stopping=1; collect_steam_logs 2>/dev/null || true' TERM INT HUP if [ -n "${BL_VK_DRIVER:-}" ]; then bl_icd_lib=$(sed -n 's/.*"library_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$BL_VK_DRIVER" 2>/dev/null | head -1) bl_icd_lib=$(printf '%s' "$bl_icd_lib" | sed 's|\\/|/|g') @@ -114,9 +114,48 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then fi unset bl_icd_lib fi +bl_now_ms() { + if [ -n "${EPOCHREALTIME:-}" ]; then + local t=${EPOCHREALTIME/[.,]/} + echo $((10#$t / 1000)) + else + date +%s%3N + fi +} +vkbridge_wait_socket() { + local sock=$1 limit=${2:-10000} start now waited + [ -n "$sock" ] || return 1 + if [ -S "$sock" ]; then + echo "== vkbridge: server socket ready (waited 0 ms)" + return 0 + fi + start=$(bl_now_ms) + while :; do + now=$(bl_now_ms) + waited=$((now - start)) + if [ -S "$sock" ]; then + echo "== vkbridge: server socket ready (waited $waited ms; the server listens after its startup probe)" + return 0 + fi + if [ -n "${bl_stopping:-}" ]; then + echo "== vkbridge: no server socket after $waited ms; the session is stopping" + return 1 + fi + if [ "$waited" -ge "$limit" ]; then + echo "== vkbridge: no server socket after $waited ms (gave up at $limit ms)" + return 1 + fi + sleep 0.05 + done +} if [ "${BL_VKBRIDGE:-0}" = 1 ]; then bl_vkb_icd=/usr/local/share/vkbridge/droidbridge_icd.json - if [ -r "$bl_vkb_icd" ] && [ -r /usr/local/lib/vkbridge/libvulkan_droidbridge.so ] && [ -S "${VKBRIDGE_SOCKET:-}" ]; then + [ -z "${BL_INSIDE:-}" ] && vkbridge_wait_socket "${VKBRIDGE_SOCKET:-}" "${BL_VKBRIDGE_WAIT_MS:-10000}" + bl_vkb_missing= + [ -r "$bl_vkb_icd" ] || bl_vkb_missing="$bl_vkb_missing icd" + [ -r /usr/local/lib/vkbridge/libvulkan_droidbridge.so ] || bl_vkb_missing="$bl_vkb_missing library" + [ -S "${VKBRIDGE_SOCKET:-}" ] || bl_vkb_missing="$bl_vkb_missing socket" + if [ -z "$bl_vkb_missing" ]; then export VK_DRIVER_FILES=$bl_vkb_icd VK_ICD_FILENAMES=$bl_vkb_icd export VKBRIDGE_SOCKET VKBRIDGE_CLIENT_LOG if [ -r /root/.vkbridge-env ]; then @@ -136,9 +175,9 @@ if [ "${BL_VKBRIDGE:-0}" = 1 ]; then fi echo "== vulkan driver: Mali bridge ($bl_vkb_icd, server at $VKBRIDGE_SOCKET)" else - echo "== vulkan driver: Mali bridge requested but NOT usable (icd $bl_vkb_icd, socket ${VKBRIDGE_SOCKET:-unset}); this GPU will show a black screen" + echo "== vulkan driver: Mali bridge requested but NOT usable (icd $bl_vkb_icd, socket ${VKBRIDGE_SOCKET:-unset}; missing:$bl_vkb_missing); this GPU will show a black screen" fi - unset bl_vkb_icd + unset bl_vkb_icd bl_vkb_missing fi export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0} diff --git a/tools/tests/test_session_vkbridge_wait.py b/tools/tests/test_session_vkbridge_wait.py new file mode 100644 index 000000000..bbd4fdb26 --- /dev/null +++ b/tools/tests/test_session_vkbridge_wait.py @@ -0,0 +1,127 @@ +import os +import re +import shutil +import socket +import subprocess +import tempfile +import threading +import time +import unittest +from pathlib import Path + +SESSION = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/droiddeck-session" + + +def wait_functions(): + text = SESSION.read_text() + found = [] + for name in ("bl_now_ms", "vkbridge_wait_socket"): + match = re.search(r"^%s\(\) \{\n.*?^\}\n" % name, text, re.M | re.S) + if match: + found.append(match.group(0)) + return found + + +class VkBridgeSocketWaitTest(unittest.TestCase): + def setUp(self): + self.root = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.root, True) + self.sock = self.root / "vkbridge" / "server.sock" + self.sock.parent.mkdir() + self.listeners = [] + self.addCleanup(lambda: [s.close() for s in self.listeners]) + + def listen(self): + listener = socket.socket(socket.AF_UNIX) + listener.bind(str(self.sock)) + listener.listen(1) + self.listeners.append(listener) + + def listen_after(self, seconds): + timer = threading.Timer(seconds, self.listen) + timer.start() + self.addCleanup(timer.cancel) + + def run_wait(self, limit_ms, prelude=""): + functions = wait_functions() + self.assertEqual(2, len(functions), "droiddeck-session lacks bl_now_ms() or vkbridge_wait_socket()") + script = "set -u\n" + "".join(functions) + prelude + script += 'if vkbridge_wait_socket "$1" "$2"; then echo USABLE; else echo FALLBACK; fi\n' + started = time.monotonic() + out = subprocess.run(["bash", "-c", script, "session", str(self.sock), str(limit_ms)], + capture_output=True, text=True, timeout=30, check=True).stdout + return out, time.monotonic() - started + + def waited_ms(self, out): + match = re.search(r"waited (\d+) ms|after (\d+) ms", out) + self.assertIsNotNone(match, out) + return int(match.group(1) or match.group(2)) + + def test_a_socket_already_listening_is_used_at_once(self): + self.listen() + out, elapsed = self.run_wait(10000) + self.assertTrue(out.endswith("USABLE\n"), out) + self.assertIn("== vkbridge: server socket ready (waited 0 ms)", out) + self.assertLess(elapsed, 2) + + def test_a_server_still_probing_is_waited_for(self): + self.listen_after(0.4) + out, elapsed = self.run_wait(10000) + self.assertTrue(out.endswith("USABLE\n"), out) + self.assertIn("== vkbridge: server socket ready (waited ", out) + waited = self.waited_ms(out) + self.assertGreaterEqual(waited, 300, out) + self.assertLess(waited, 5000, out) + self.assertLess(elapsed, 6) + + def test_no_server_falls_back_after_the_limit(self): + out, elapsed = self.run_wait(300) + self.assertTrue(out.endswith("FALLBACK\n"), out) + self.assertIn("== vkbridge: no server socket after ", out) + self.assertIn("(gave up at 300 ms)", out) + self.assertGreaterEqual(self.waited_ms(out), 300) + self.assertLess(elapsed, 5) + + def test_a_plain_file_is_not_a_socket(self): + self.sock.write_text("stale") + out, _ = self.run_wait(200) + self.assertTrue(out.endswith("FALLBACK\n"), out) + + def test_a_stopping_session_does_not_wait(self): + out, elapsed = self.run_wait(10000, prelude="bl_stopping=1\n") + self.assertTrue(out.endswith("FALLBACK\n"), out) + self.assertIn("the session is stopping", out) + self.assertLess(elapsed, 2) + + def test_no_socket_path_is_a_fallback_without_waiting(self): + functions = "".join(wait_functions()) + out = subprocess.run(["bash", "-c", "set -u\n" + functions + + 'if vkbridge_wait_socket "" 10000; then echo USABLE; else echo FALLBACK; fi\n'], + capture_output=True, text=True, timeout=10, check=True).stdout + self.assertEqual("FALLBACK\n", out) + + +class VkBridgeSessionCheckTest(unittest.TestCase): + + def setUp(self): + self.text = SESSION.read_text() + match = re.search(r'^if \[ "\$\{BL_VKBRIDGE:-0\}" = 1 \]; then\n.*?^fi\n', self.text, re.M | re.S) + self.assertIsNotNone(match) + self.block = match.group(0) + + def test_the_first_pass_waits_before_the_usability_check(self): + wait = self.block.index("vkbridge_wait_socket") + check = self.block.index('[ -S "${VKBRIDGE_SOCKET:-}" ]') + self.assertLess(wait, check) + self.assertIn('[ -z "${BL_INSIDE:-}" ] && vkbridge_wait_socket', self.block) + + def test_the_fallback_message_names_what_is_missing(self): + self.assertIn("Mali bridge requested but NOT usable", self.block) + self.assertIn("missing:$bl_vkb_missing", self.block) + + def test_a_stop_ends_the_wait(self): + self.assertRegex(self.text, r"trap '[^']*bl_stopping=1[^']*' TERM INT HUP") + + +if __name__ == "__main__": + unittest.main() From 30a70a0f7c60a832c1d65f588234d75ddce3009e Mon Sep 17 00:00:00 2001 From: Ahmad Sayed Alsayed Ebrahim Date: Sat, 17 Oct 2026 23:55:23 +0300 Subject: [PATCH 10/21] Setup: show the stored Mali bridge switch in a new process; log the server's start time FrontEndActivity read vkBridge and vkBridgeSafe only when the Performance page opened, so after an app restart (every APK update) Setup showed "Mali bridge: off" while sessions used the bridge. refresh() now reads them. VkBridgeComponent logs how long the server took to listen. --- .../main/java/com/droiddeck/launcher/FrontEndActivity.kt | 2 ++ .../java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt index 3fcf7dbd2..a592b0c5e 100644 --- a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt @@ -2113,6 +2113,8 @@ class FrontEndActivity : ComponentActivity() { offline = OfflineMode.enabled(this) installed = LinuxRuntimeInstaller.installedVersion(this) ready = LinuxRuntime.isInstalled(this) + vkBridge = SessionPrefs.vkBridge(this) + vkBridgeSafe = SessionPrefs.vkBridgeSafe(this) removalPending = LinuxRuntimeInstaller.hasRemovalPending(this) frameGenLabel = FrameGen.label(this) romsDir = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt b/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt index d606348ff..8e1c4419e 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt @@ -36,11 +36,14 @@ class VkBridgeComponent(private val socket: File, private val logFile: File) : S Log.w(TAG, "server exited with status $status") runCatching { logFile.appendText("== vkbridge server exited with status $status\n") } }, { line -> Log.i(TAG, line) }) - val deadline = System.currentTimeMillis() + 15_000L + val began = System.currentTimeMillis() + val deadline = began + 15_000L while (!socket.exists() && !exited && System.currentTimeMillis() < deadline) { try { Thread.sleep(50L) } catch (e: InterruptedException) { Thread.currentThread().interrupt(); break } } - Log.i(TAG, if (socket.exists()) "server ready (pid $pid) at $socket" else "server NOT ready (pid $pid, exited=$exited); see $logFile") + val waited = System.currentTimeMillis() - began + Log.i(TAG, if (socket.exists()) "server ready (pid $pid) at $socket after $waited ms" + else "server NOT ready (pid $pid, exited=$exited) after $waited ms; see $logFile") if (!socket.exists() && VkBridgeNative.start(app(), socket, logFile, cacheDir)) Log.i(TAG, "server ready in the app's process at $socket") } From e527d4a9f39dd35df57926ef0c2d7df608d6ca66 Mon Sep 17 00:00:00 2001 From: Ahmad Sayed Alsayed Ebrahim Date: Sun, 18 Oct 2026 21:45:41 +0300 Subject: [PATCH 11/21] fix: droiddeck-session: BL_VKBRIDGE_CEF_ANGLE reaches Chromium through the web helper preload Steam drops -cef-use-angle: on the bridge the CEF GPU process reports ANGLE_OPENGL (ANGLE on Zink) with -cef-use-angle=vulkan on the command line. An explicit BL_VKBRIDGE_CEF_ANGLE now also becomes --use-gl=angle --use-angle= on steamwebhelper's own argv (stock process layout kept); unset, nothing changes. --- tools/linuxfs/overlay/usr/local/bin/droiddeck-session | 5 ++++- tools/tests/test_webhelper_preload.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index 9d9c6d331..cf3f46365 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -1029,7 +1029,10 @@ BUSCONF fi [ "${BL_VKBRIDGE_CEF_LOG:-0}" = 1 ] && _bl_cef+=(-cef-enable-logging=stderr -cef-v=1) [ -n "${BL_VKBRIDGE_CEF_EXTRA:-}" ] && read -r -a _bl_cef_extra <<<"$BL_VKBRIDGE_CEF_EXTRA" && _bl_cef+=("${_bl_cef_extra[@]}") - export BL_WEBHELPER_ARGS="${BL_VKBRIDGE_WEBHELPER_ARGS:-stock}" + _bl_wh_args=stock + [ -n "${BL_VKBRIDGE_CEF_ANGLE:-}" ] && _bl_wh_args="--use-gl=angle --use-angle=$BL_VKBRIDGE_CEF_ANGLE" + export BL_WEBHELPER_ARGS="${BL_VKBRIDGE_WEBHELPER_ARGS:-$_bl_wh_args}" + unset _bl_wh_args echo "== steam: web helper process layout: $BL_WEBHELPER_ARGS (Mali bridge)" fi if [ "${BL_STEAM_UI:-}" != desktop ]; then diff --git a/tools/tests/test_webhelper_preload.py b/tools/tests/test_webhelper_preload.py index 30a41fc7b..def4aa67a 100644 --- a/tools/tests/test_webhelper_preload.py +++ b/tools/tests/test_webhelper_preload.py @@ -61,6 +61,12 @@ class WebhelperPreloadTest(unittest.TestCase): args = [WEBHELPER, "--enable-features=A"] self.assertEqual(self.argv(*args, spec="stock"), args) + def test_angle_switches_are_appended_with_the_gpu_process_kept_out_of_process(self): + out = self.argv(WEBHELPER, "-lang=en_US", "--enable-features=A", spec="--use-gl=angle --use-angle=vulkan") + self.assertEqual(out, [WEBHELPER, "-lang=en_US", "--use-gl=angle", "--use-angle=vulkan", + "--enable-features=A"]) + self.assertNotIn("--in-process-gpu", out) + def test_an_empty_setting_means_the_default(self): self.assertIn("--no-zygote", self.argv(WEBHELPER, spec="")) From e63fad1daab57499e454b89143e0e46efcf27fcb Mon Sep 17 00:00:00 2001 From: Ahmad Sayed Alsayed Ebrahim Date: Sun, 18 Oct 2026 23:35:51 +0300 Subject: [PATCH 12/21] fix: droiddeck-session: ANGLE on Vulkan brings Chromium's Vulkan compositing With BL_VKBRIDGE_CEF_ANGLE=vulkan the CEF GPU process stopped crashing but Chromium still composited in software (gpu_compositing: disabled_software, Steam "Disabling GPU acceleration: Disabled/RuntimeDetect", VizCompositorThread at a full core). The GPU report shows why: "eglCreateWindowSurface failed with error EGL_BAD_CONFIG". ANGLE's Vulkan backend on X11 gives every EGL config the root window's visual; Chromium's X11 surface asks for a config whose EGL_NATIVE_VISUAL_ID is the ARGB visual whenever a compositing manager owns _NET_WM_CM_S0 (gamescope does), finds none and passes a null config. bl_webhelper_layout() now adds +Vulkan +VulkanFromANGLE +DefaultANGLEVulkan for ANGLE on Vulkan, so Skia composites through an xcb swapchain on ANGLE's Vulkan device and no EGL window surface is needed. BL_VKBRIDGE_CEF_VULKAN=0 leaves it off; unset BL_VKBRIDGE_CEF_ANGLE still means Steam's stock layout. --- .../overlay/usr/local/bin/droiddeck-session | 17 +++++-- tools/tests/test_session_webhelper_layout.py | 48 +++++++++++++++++++ tools/tests/test_webhelper_preload.py | 7 +++ 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 tools/tests/test_session_webhelper_layout.py diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index cf3f46365..b91057000 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -114,6 +114,17 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then fi unset bl_icd_lib fi +bl_webhelper_layout() { + if [ -n "${BL_VKBRIDGE_WEBHELPER_ARGS:-}" ]; then + printf '%s\n' "$BL_VKBRIDGE_WEBHELPER_ARGS" + elif [ -z "${BL_VKBRIDGE_CEF_ANGLE:-}" ]; then + echo stock + elif [ "$BL_VKBRIDGE_CEF_ANGLE" = vulkan ] && [ "${BL_VKBRIDGE_CEF_VULKAN:-1}" != 0 ]; then + echo "--use-gl=angle --use-angle=vulkan +Vulkan +VulkanFromANGLE +DefaultANGLEVulkan" + else + echo "--use-gl=angle --use-angle=$BL_VKBRIDGE_CEF_ANGLE" + fi +} bl_now_ms() { if [ -n "${EPOCHREALTIME:-}" ]; then local t=${EPOCHREALTIME/[.,]/} @@ -1029,10 +1040,8 @@ BUSCONF fi [ "${BL_VKBRIDGE_CEF_LOG:-0}" = 1 ] && _bl_cef+=(-cef-enable-logging=stderr -cef-v=1) [ -n "${BL_VKBRIDGE_CEF_EXTRA:-}" ] && read -r -a _bl_cef_extra <<<"$BL_VKBRIDGE_CEF_EXTRA" && _bl_cef+=("${_bl_cef_extra[@]}") - _bl_wh_args=stock - [ -n "${BL_VKBRIDGE_CEF_ANGLE:-}" ] && _bl_wh_args="--use-gl=angle --use-angle=$BL_VKBRIDGE_CEF_ANGLE" - export BL_WEBHELPER_ARGS="${BL_VKBRIDGE_WEBHELPER_ARGS:-$_bl_wh_args}" - unset _bl_wh_args + BL_WEBHELPER_ARGS=$(bl_webhelper_layout) + export BL_WEBHELPER_ARGS echo "== steam: web helper process layout: $BL_WEBHELPER_ARGS (Mali bridge)" fi if [ "${BL_STEAM_UI:-}" != desktop ]; then diff --git a/tools/tests/test_session_webhelper_layout.py b/tools/tests/test_session_webhelper_layout.py new file mode 100644 index 000000000..f14afd61d --- /dev/null +++ b/tools/tests/test_session_webhelper_layout.py @@ -0,0 +1,48 @@ +import os +import re +import subprocess +import unittest +from pathlib import Path + +SESSION = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/droiddeck-session" +KEYS = ("BL_VKBRIDGE_WEBHELPER_ARGS", "BL_VKBRIDGE_CEF_ANGLE", "BL_VKBRIDGE_CEF_VULKAN") + + +def layout_function(): + match = re.search(r"^bl_webhelper_layout\(\) \{\n.*?^\}\n", SESSION.read_text(), re.M | re.S) + return match.group(0) if match else None + + +class WebhelperLayoutTest(unittest.TestCase): + def layout(self, **settings): + function = layout_function() + self.assertIsNotNone(function, "droiddeck-session lacks bl_webhelper_layout()") + env = {k: v for k, v in os.environ.items() if k not in KEYS} + env.update(settings) + return subprocess.run(["bash", "-c", "set -u\n" + function + "bl_webhelper_layout\n"], env=env, + capture_output=True, text=True, check=True).stdout.strip() + + def test_unset_keeps_steams_own_layout(self): + self.assertEqual(self.layout(), "stock") + + def test_angle_on_vulkan_brings_chromiums_vulkan_compositing(self): + self.assertEqual(self.layout(BL_VKBRIDGE_CEF_ANGLE="vulkan"), + "--use-gl=angle --use-angle=vulkan +Vulkan +VulkanFromANGLE +DefaultANGLEVulkan") + + def test_vulkan_compositing_can_be_left_off(self): + self.assertEqual(self.layout(BL_VKBRIDGE_CEF_ANGLE="vulkan", BL_VKBRIDGE_CEF_VULKAN="0"), + "--use-gl=angle --use-angle=vulkan") + + def test_other_angle_backends_get_only_the_angle_switches(self): + self.assertEqual(self.layout(BL_VKBRIDGE_CEF_ANGLE="gl"), "--use-gl=angle --use-angle=gl") + + def test_explicit_layout_wins(self): + self.assertEqual(self.layout(BL_VKBRIDGE_CEF_ANGLE="vulkan", BL_VKBRIDGE_WEBHELPER_ARGS="--x +Y"), + "--x +Y") + + def test_the_steam_block_uses_it(self): + self.assertIn("BL_WEBHELPER_ARGS=$(bl_webhelper_layout)", SESSION.read_text()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_webhelper_preload.py b/tools/tests/test_webhelper_preload.py index def4aa67a..7c49e1d76 100644 --- a/tools/tests/test_webhelper_preload.py +++ b/tools/tests/test_webhelper_preload.py @@ -67,6 +67,13 @@ class WebhelperPreloadTest(unittest.TestCase): "--enable-features=A"]) self.assertNotIn("--in-process-gpu", out) + def test_vulkan_compositing_features_join_steams_feature_list(self): + out = self.argv(WEBHELPER, "--enable-features=PlatformHEVCDecoderSupport,V4L2VideoDecode", + spec="--use-gl=angle --use-angle=vulkan +Vulkan +VulkanFromANGLE +DefaultANGLEVulkan") + self.assertEqual(out, [WEBHELPER, "--use-gl=angle", "--use-angle=vulkan", + "--enable-features=PlatformHEVCDecoderSupport,V4L2VideoDecode," + "Vulkan,VulkanFromANGLE,DefaultANGLEVulkan"]) + def test_an_empty_setting_means_the_default(self): self.assertIn("--no-zygote", self.argv(WEBHELPER, spec="")) From 9040f828e988b4032586af4f8330036eb0e09fe5 Mon Sep 17 00:00:00 2001 From: ASEbrahim <264076441+ASEbrahim@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:13:51 +0300 Subject: [PATCH 13/21] Mali bridge: DXVK 1.10.3 on phones whose Vulkan stops below 1.3 Proton's DXVK 3 refuses a device below Vulkan 1.3, without shaderInt64 or with less than 256 bytes of push constants, so on a Mali with a 1.1 driver (OnePlus Ace 2V, Mali-G710, r32p1) every D3D9-11 game exits. On a bridge session the forced-package step now picks the newest DXVK that starts on what games see: 1.10.3 below Vulkan 1.3, 2.6.2 on 1.3 without shaderInt64 or 256 bytes of push constants, Proton's own otherwise. What games see comes from /apps-vulkan (a bridge with Vulkan promotion writes it after its self-test) or else from the phone's driver (vkinfo now reports shader_int64 and max_push_constants). These choices are marked automatic: a later session with another answer swaps or restores them, and the player's own choice in Components is never touched. --- app/src/main/cpp/deviceinfo/vkinfo.c | 2 + .../com/droiddeck/launcher/gpu/AppVulkan.kt | 68 +++++++ .../launcher/session/ComponentsManager.kt | 71 +++++-- .../launcher/session/BridgeDxvkTest.kt | 179 ++++++++++++++++++ 4 files changed, 308 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/droiddeck/launcher/gpu/AppVulkan.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt diff --git a/app/src/main/cpp/deviceinfo/vkinfo.c b/app/src/main/cpp/deviceinfo/vkinfo.c index 81ccc6b0d..5846e055d 100644 --- a/app/src/main/cpp/deviceinfo/vkinfo.c +++ b/app/src/main/cpp/deviceinfo/vkinfo.c @@ -62,6 +62,7 @@ Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz LINE("driver_version=%u.%u.%u\n", p.driverVersion >> 22, (p.driverVersion >> 12) & 0x3ff, p.driverVersion & 0xfff); LINE("vendor_id=0x%04x\ndevice_id=0x%08x\n", p.vendorID, p.deviceID); LINE("max_image_2d=%u\n", p.limits.maxImageDimension2D); + LINE("max_push_constants=%u\n", p.limits.maxPushConstantsSize); uint32_t extCount = 0; if (vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, NULL); @@ -91,6 +92,7 @@ Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz vkGetPhysicalDeviceFeatures(device, &f); LINE("geometry_shader=%s\ntessellation_shader=%s\ntexture_bc=%s\n", f.geometryShader ? "yes" : "no", f.tessellationShader ? "yes" : "no", f.textureCompressionBC ? "yes" : "no"); + LINE("shader_int64=%s\n", f.shaderInt64 ? "yes" : "no"); } if (vkGetPhysicalDeviceMemoryProperties) { VkPhysicalDeviceMemoryProperties m; diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/AppVulkan.kt b/app/src/main/java/com/droiddeck/launcher/gpu/AppVulkan.kt new file mode 100644 index 000000000..437f232f8 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/AppVulkan.kt @@ -0,0 +1,68 @@ +package com.droiddeck.launcher.gpu + +import android.content.Context +import android.util.Log +import com.droiddeck.launcher.core.DeviceSupport +import com.droiddeck.launcher.core.FileUtils +import java.io.File + +data class AppVulkan( + val major: Int, + val minor: Int, + val shaderInt64: Boolean?, + val maxPushConstants: Int?, + val source: String, +) { + fun atLeast(major: Int, minor: Int): Boolean = this.major > major || (this.major == major && this.minor >= minor) + + val label: String + get() = "Vulkan $major.$minor, shaderInt64 ${shaderInt64?.let { if (it) "yes" else "no" } ?: "?"}, " + + "push constants ${maxPushConstants ?: "?"} ($source)" + + companion object { + private const val TAG = "AppVulkan" + const val BRIDGE_FILE = "apps-vulkan" + + fun detect(context: Context): AppVulkan? { + if (!DeviceSupport.vkBridge(context)) return null + val native = VulkanInfo.query() + val file = File(VkBridgeComponent.cacheDir(context), BRIDGE_FILE) + val fromBridge = FileUtils.readString(file)?.let { fromBridgeFile(it, native["device"]) } + val found = fromBridge ?: fromDriver(native) + Log.i(TAG, found?.label ?: "unknown (driver query: ${native["error"] ?: "no api line"})") + return found + } + + internal fun fromBridgeFile(text: String, driverDevice: String?): AppVulkan? { + val map = keyValues(text) + val device = map["device"] + if (device != null && driverDevice != null && device != driverDevice) return null + val (major, minor) = version(map["api"]) ?: return null + return AppVulkan(major, minor, bool(map["shader_int64"]), map["max_push_constants"]?.toIntOrNull(), "bridge") + } + + internal fun fromDriver(map: Map): AppVulkan? { + val (major, minor) = version(map["api"]) ?: return null + return AppVulkan(major, minor, bool(map["shader_int64"]), map["max_push_constants"]?.toIntOrNull(), "driver") + } + + private fun keyValues(text: String): Map = text.lineSequence().mapNotNull { line -> + val eq = line.indexOf('=') + if (eq <= 0) null else line.substring(0, eq).trim() to line.substring(eq + 1).trim() + }.toMap() + + private fun version(v: String?): Pair? { + val parts = v?.trim()?.split('.') ?: return null + if (parts.size < 2) return null + val major = parts[0].toIntOrNull() ?: return null + val minor = parts[1].toIntOrNull() ?: return null + return major to minor + } + + private fun bool(v: String?): Boolean? = when (v?.trim()?.lowercase()) { + "1", "yes", "true" -> true + "0", "no", "false" -> false + else -> null + } + } +} diff --git a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt index f10ab1244..ca5afd557 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt @@ -7,6 +7,7 @@ import android.util.Log import com.droiddeck.launcher.core.Downloader import com.droiddeck.launcher.core.FileUtils import com.droiddeck.launcher.core.GameEnvironment +import com.droiddeck.launcher.gpu.AppVulkan import com.droiddeck.launcher.gpu.GpuInfo import com.droiddeck.launcher.runtime.LinuxRuntime import com.github.luben.zstd.ZstdInputStream @@ -629,29 +630,31 @@ object ComponentsManager { } - fun swap(context: Context, protonId: String, file: String): String = synchronized(lock) { + fun swap(context: Context, protonId: String, file: String, auto: Boolean = false): String = synchronized(lock) { val p = proton(context, protonId) val wcp = File(packagesDir(context), safeName(file)) val info = packageInfo(context, wcp) val state = loadState(context) if (inUse(context, p)) { - state.sub("queued").sub(p.id).put(info.comp, JSONObject().put("kind", "package").put("file", wcp.name).put("label", info.version)) + state.sub("queued").sub(p.id).put(info.comp, JSONObject().put("kind", "package").put("file", wcp.name).put("label", info.version) + .apply { if (auto) put("auto", true) }) saveState(context, state) return context.getString(R.string.cmgr_swap_queued, LABEL[info.comp], info.version, p.name) } - applyPackage(context, p, info, wcp, state) + applyPackage(context, p, info, wcp, state, auto) state.sub("queued").optJSONObject(p.id)?.remove(info.comp) saveState(context, state) context.getString(R.string.cmgr_swap_done, LABEL[info.comp], info.version, p.name) } - private fun applyPackage(context: Context, p: Proton, info: Package, wcp: File, state: JSONObject) { + private fun applyPackage(context: Context, p: Proton, info: Package, wcp: File, state: JSONObject, auto: Boolean = false) { ensureOriginal(context, p, info.comp, state) val hashes = installFiles(context, p.dir, info.comp, wcp) state.sub("active").sub(p.id).put(info.comp, JSONObject() .put("file", wcp.name).put("label", info.version).put("protonVersion", p.version) .put("files", JSONObject(hashes.filterKeys { owned(info.comp, it) } as Map<*, *>)) - .put("at", System.currentTimeMillis() / 1000)) + .put("at", System.currentTimeMillis() / 1000) + .apply { if (auto) put("auto", true) }) Log.i(TAG, "swapped ${info.comp} ${info.version} into ${p.name} ${p.version}") } @@ -699,7 +702,7 @@ object ComponentsManager { applyOriginal(context, p, comp, v, File(originalsDir(context), "$pid/${safeName(v)}/$comp.wcp"), state) } else { val wcp = File(packagesDir(context), q.optString("file")) - applyPackage(context, p, packageInfo(context, wcp), wcp, state) + applyPackage(context, p, packageInfo(context, wcp), wcp, state, q.optBoolean("auto")) } done += "${LABEL[comp]} ${q.optString("label")} → ${p.name}" }.onFailure { Log.w(TAG, "queued $comp for ${p.name}", it) } @@ -1005,6 +1008,21 @@ object ComponentsManager { internal fun forcedPackageWanted(activeFile: String?, protonVersionMatches: Boolean): Boolean = activeFile.isNullOrEmpty() || !protonVersionMatches + internal fun bridgeDxvk(vk: AppVulkan?): ForcedPackage? = when { + vk == null -> null + !vk.atLeast(1, 1) -> null + !vk.atLeast(1, 3) -> ForcedPackage(emptySet(), "dxvk", DXVK_FOR_VULKAN_1_1, "Dxvk-Linux") + vk.shaderInt64 == false || (vk.maxPushConstants ?: 256) < 256 -> + ForcedPackage(emptySet(), "dxvk", DXVK_FOR_VULKAN_1_3_NO_INT64, "Dxvk-Linux") + else -> null + } + + internal const val DXVK_FOR_VULKAN_1_1 = "dxvk-1.10.3-linux.wcp" + internal const val DXVK_FOR_VULKAN_1_3_NO_INT64 = "dxvk-2.6.2-linux.wcp" + + internal fun autoChoiceStale(entry: JSONObject?, wantedFile: String?): Boolean = + entry != null && entry.optBoolean("auto") && entry.optString("file") != wantedFile + internal fun catalogMatch(items: List, file: String, release: String): CatalogItem? = items.firstOrNull { it.file == file && it.release == release } @@ -1013,24 +1031,52 @@ object ComponentsManager { { file, release -> catalogItem(context, file, release) }, { download(context, it) {} }, { id, file -> swap(context, id, file) }, + vulkan = AppVulkan.detect(context), + bridgeSession = com.droiddeck.launcher.core.DeviceSupport.vkBridge(context), + applyAuto = { id, file -> swap(context, id, file, auto = true) }, + undoAuto = { id, comp -> undoAutoChoice(context, id, comp) }, ) + internal fun undoAutoChoice(context: Context, protonId: String, comp: String): String = synchronized(lock) { + val p = proton(context, protonId) + val state = loadState(context) + val entry = state.sub("active").optJSONObject(p.id)?.optJSONObject(comp) + if (entry?.optString("protonVersion") == p.version && originalFile(context, p, comp).isFile) + return restore(context, protonId, comp, safeName(p.version)) + state.sub("active").optJSONObject(p.id)?.remove(comp) + saveState(context, state) + "${LABEL[comp]}: ${p.name} keeps its own copy" + } + internal fun ensureForcedPackages( context: Context, gpu: GpuInfo, lookup: (file: String, release: String) -> CatalogItem?, fetch: (CatalogItem) -> Unit, apply: (protonId: String, file: String) -> String, + vulkan: AppVulkan? = null, + bridgeSession: Boolean = false, + applyAuto: (protonId: String, file: String) -> String = apply, + undoAuto: (protonId: String, comp: String) -> String = { _, _ -> "" }, ): String? { - val rules = FORCED_PACKAGES.filter { it.matches(gpu) } - if (rules.isEmpty()) return null + val bridgeRule = if (bridgeSession) bridgeDxvk(vulkan) else null + val rules = FORCED_PACKAGES.filter { it.matches(gpu) }.map { it to false } + listOfNotNull(bridgeRule?.let { it to true }) + if (rules.isEmpty() && !bridgeSession) return null val installed = protons(context) val lines = mutableListOf() - for (rule in rules) { + if (bridgeSession) { + val state = loadState(context) + for (p in installed) { + val entry = state.optJSONObject("active")?.optJSONObject(p.id)?.optJSONObject("dxvk") + if (bridgeRule == null && autoChoiceStale(entry, null)) lines.add(undoAuto(p.id, "dxvk")) + } + } + for ((rule, auto) in rules) { val state = loadState(context) val targets = installed.filter { p -> val active = state.optJSONObject("active")?.optJSONObject(p.id)?.optJSONObject(rule.comp) - forcedPackageWanted(active?.optString("file"), active?.optString("protonVersion") == p.version) + forcedPackageWanted(active?.optString("file"), active?.optString("protonVersion") == p.version) || + (auto && autoChoiceStale(active, rule.file)) } if (targets.isEmpty()) continue val missing = missingPackage(context, rule, lookup, fetch) @@ -1038,9 +1084,10 @@ object ComponentsManager { lines.add(missing) continue } - lines.add(targets.joinToString("; ") { apply(it.id, rule.file) }) + if (auto) lines.add("${vulkan?.label}: DXVK ${rule.file}") + lines.add(targets.joinToString("; ") { if (auto) applyAuto(it.id, rule.file) else apply(it.id, rule.file) }) } - return lines.joinToString("; ").ifEmpty { null } + return lines.filter { it.isNotEmpty() }.joinToString("; ").ifEmpty { null } } private fun missingPackage( diff --git a/app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt b/app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt new file mode 100644 index 000000000..9bcc7b682 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt @@ -0,0 +1,179 @@ +package com.droiddeck.launcher.session + +import android.content.Context +import com.droiddeck.launcher.gpu.AppVulkan +import com.droiddeck.launcher.gpu.GpuInfo +import com.droiddeck.launcher.runtime.LinuxRuntime +import com.github.luben.zstd.ZstdOutputStream +import org.apache.commons.compress.archivers.tar.TarArchiveEntry +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream +import org.json.JSONObject +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import java.io.File + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28], manifest = Config.NONE) +class BridgeDxvkTest { + private val old = ComponentsManager.DXVK_FOR_VULKAN_1_1 + private val mid = ComponentsManager.DXVK_FOR_VULKAN_1_3_NO_INT64 + private lateinit var context: Context + + private val g710 = AppVulkan(1, 1, shaderInt64 = false, maxPushConstants = 128, source = "driver") + private val promoted = g710.copy(minor = 2, source = "bridge") + private val full = AppVulkan(1, 3, shaderInt64 = true, maxPushConstants = 256, source = "driver") + private val mali = GpuInfo("Mali-G710", 0, GpuInfo.Family.NOT_ADRENO, "", false) + + @Before fun setUp() { + context = RuntimeEnvironment.getApplication() + File(context.filesDir, "components").deleteRecursively() + LinuxRuntime.rootDir(context).deleteRecursively() + } + + @Test fun theNewestDxvkThatStartsIsChosen() { + assertEquals(old, ComponentsManager.bridgeDxvk(g710)?.file) + assertEquals(old, ComponentsManager.bridgeDxvk(promoted)?.file) + assertEquals(mid, ComponentsManager.bridgeDxvk(full.copy(shaderInt64 = false))?.file) + assertEquals(mid, ComponentsManager.bridgeDxvk(full.copy(maxPushConstants = 128))?.file) + assertNull(ComponentsManager.bridgeDxvk(full)) + assertNull(ComponentsManager.bridgeDxvk(full.copy(shaderInt64 = null, maxPushConstants = null))) + assertNull(ComponentsManager.bridgeDxvk(AppVulkan(1, 0, null, null, "driver"))) + assertNull(ComponentsManager.bridgeDxvk(null)) + assertEquals("Dxvk-Linux", ComponentsManager.bridgeDxvk(g710)?.release) + assertEquals("dxvk", ComponentsManager.bridgeDxvk(g710)?.comp) + } + + @Test fun theDriversLinesAreRead() { + val vk = AppVulkan.fromDriver(mapOf("device" to "Mali-G710 MC10", "api" to "1.1.177", "shader_int64" to "no", "max_push_constants" to "128")) + assertEquals(g710, vk) + assertNull(AppVulkan.fromDriver(mapOf("error" to "no Vulkan device"))) + assertNull(AppVulkan.fromDriver(mapOf("api" to "garbage"))) + val older = AppVulkan.fromDriver(mapOf("api" to "1.3.250"))!! + assertNull(older.shaderInt64) + assertNull(older.maxPushConstants) + } + + @Test fun theBridgesFileWinsForItsOwnGpuOnly() { + val text = "device=Mali-G710 MC10\napi=1.2.177\ndriver_api=1.1.177\nshader_int64=0\nmax_push_constants=128\n" + assertEquals(promoted, AppVulkan.fromBridgeFile(text, "Mali-G710 MC10")) + assertEquals(promoted, AppVulkan.fromBridgeFile(text, null)) + assertNull(AppVulkan.fromBridgeFile(text, "Mali-G720 Immortalis MC12")) + assertNull(AppVulkan.fromBridgeFile("device=x\n", null)) + } + + @Test fun anAdrenoSessionIsNotTouched() { + proton("proton-exp") + assertNull(run(g710, bridge = false)) + } + + @Test fun aShippedDxvk3IsReplacedOnAVulkan11Phone() { + proton("proton-exp") + proton("ge") + val applied = mutableListOf() + val result = run(g710, applyAuto = { id, file -> applied += "$id $file"; "swapped $id" }) + assertEquals(listOf("ge $old", "proton-exp $old"), applied.sorted()) + assertTrue(result!!.startsWith("Vulkan 1.1, shaderInt64 no, push constants 128 (driver): DXVK $old")) + } + + @Test fun thePlayersOwnChoiceIsKept() { + proton("proton-exp") + writeState("proton-exp", JSONObject().put("file", "dxvk-3.1-linux.wcp").put("protonVersion", "experimental-11")) + assertNull(run(g710)) + assertNull(run(full)) + } + + @Test fun anAutomaticChoiceFollowsTheDevice() { + proton("proton-exp") + writeState("proton-exp", JSONObject().put("file", mid).put("protonVersion", "experimental-11").put("auto", true)) + val applied = mutableListOf() + run(g710, applyAuto = { id, file -> applied += "$id $file"; "swapped" }) + assertEquals(listOf("proton-exp $old"), applied) + + writeState("proton-exp", JSONObject().put("file", old).put("protonVersion", "experimental-11").put("auto", true)) + assertNull(run(g710)) + val undone = mutableListOf() + run(full, undoAuto = { id, comp -> undone += "$id $comp"; "undone" }) + assertEquals(listOf("proton-exp dxvk"), undone) + } + + @Test fun theSwapAndItsUndoReachTheProton() { + proton("proton-exp") + val d3d11 = File(protonDir("proton-exp"), "files/lib/wine/dxvk/aarch64-windows/d3d11.dll") + writePackage(old, "DXVK 1.10.3") + val auto: (String, String) -> String = { id, file -> ComponentsManager.swap(context, id, file, auto = true) } + val undo: (String, String) -> String = { id, comp -> ComponentsManager.undoAutoChoice(context, id, comp) } + + assertNotNull(run(g710, applyAuto = auto, undoAuto = undo)) + assertEquals("DXVK 1.10.3", d3d11.readText()) + val entry = state().getJSONObject("active").getJSONObject("proton-exp").getJSONObject("dxvk") + assertEquals(old, entry.getString("file")) + assertTrue(entry.getBoolean("auto")) + assertNull(run(g710, applyAuto = auto, undoAuto = undo)) + + run(full, applyAuto = auto, undoAuto = undo) + assertEquals("bundled DXVK experimental-11", d3d11.readText()) + assertNull(state().getJSONObject("active").optJSONObject("proton-exp")?.optJSONObject("dxvk")) + } + + @Test fun aProtonUpdateWithNoRuleLeftJustForgetsTheChoice() { + proton("proton-exp", "experimental-12") + writeState("proton-exp", JSONObject().put("file", old).put("protonVersion", "experimental-11").put("auto", true)) + run(full, undoAuto = { id, comp -> ComponentsManager.undoAutoChoice(context, id, comp) }) + assertNull(state().getJSONObject("active").optJSONObject("proton-exp")?.optJSONObject("dxvk")) + assertEquals("bundled DXVK experimental-12", File(protonDir("proton-exp"), "files/lib/wine/dxvk/aarch64-windows/d3d11.dll").readText()) + } + + private fun run( + vk: AppVulkan?, + bridge: Boolean = true, + applyAuto: (String, String) -> String = { id, file -> error("apply $id $file") }, + undoAuto: (String, String) -> String = { id, comp -> error("undo $id $comp") }, + ) = ComponentsManager.ensureForcedPackages( + context, mali, + lookup = { file, release -> ComponentsManager.CatalogItem(file, "dxvk", release, "https://example.invalid/$file", 4, "sha256:" + "ab".repeat(32)) }, + fetch = {}, + apply = { id, file -> error("non-automatic apply $id $file") }, + vulkan = vk, bridgeSession = bridge, applyAuto = applyAuto, undoAuto = undoAuto, + ) + + private fun protonDir(name: String) = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps/common/$name") + + private fun proton(name: String, version: String = "experimental-11") { + val dir = protonDir(name) + File(dir, "files/lib/wine").mkdirs() + File(dir, "proton").writeText("#!/bin/sh\n") + File(dir, "version").writeText("1 $version\n") + File(dir, "files/lib/wine/dxvk/aarch64-windows/d3d11.dll").apply { + parentFile!!.mkdirs() + writeText("bundled DXVK $version") + } + } + + private fun state() = JSONObject(File(context.filesDir, "components/state.json").readText()) + + private fun writeState(protonId: String, dxvk: JSONObject) { + val f = File(context.filesDir, "components/state.json") + f.parentFile!!.mkdirs() + f.writeText(JSONObject().put("active", JSONObject().put(protonId, JSONObject().put("dxvk", dxvk))) + .put("queued", JSONObject()).toString()) + } + + private fun writePackage(file: String, d3d11: String) { + val dest = File(context.filesDir, "components/packages/$file") + dest.parentFile!!.mkdirs() + TarArchiveOutputStream(ZstdOutputStream(dest.outputStream())).use { tar -> + fun put(name: String, bytes: ByteArray) { + tar.putArchiveEntry(TarArchiveEntry(name).apply { size = bytes.size.toLong() }) + tar.write(bytes) + tar.closeArchiveEntry() + } + put("./profile.json", JSONObject().put("type", "DXVK-Linux").put("versionName", file.removeSuffix(".wcp")).toString().toByteArray()) + put("./files/lib/wine/dxvk/aarch64-windows/d3d11.dll", d3d11.toByteArray()) + } + } +} From 678e16af4d3901f488f1133398307bf153b44b92 Mon Sep 17 00:00:00 2001 From: ASEbrahim <264076441+ASEbrahim@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:23:59 +0300 Subject: [PATCH 14/21] Mali old DXVK: a failed detection keeps the last automatic choice, one Proton failing does not stop the others, the bridge file's driver_api counts unless promotion=on When nothing could be read (no apps-vulkan file and VulkanInfo failing), every automatic choice was undone: a 1.1 phone went back to a DXVK that cannot start, and the next session flipped it again. Now an automatic choice is undone only when the Vulkan is known and wants none. Each Proton's swap or undo is on its own: a failure (a missing saved copy, an I/O error) is said in the result and the remaining Protons still get theirs. When the Proton is the same but its saved copy is gone, the choice stays and is said, instead of being forgotten with DXVK 1.10.3 still installed. apps-vulkan: the version counts only with promotion=on; otherwise, and in files from servers before the field (which wrote a promoted version even where it was not trusted), driver_api does. A file whose driver_api is not the driver's own version (a driver update, a server rolled back) is left out. --- .../com/droiddeck/launcher/gpu/AppVulkan.kt | 13 ++++-- .../launcher/session/ComponentsManager.kt | 15 +++++-- .../launcher/session/BridgeDxvkTest.kt | 41 +++++++++++++++++-- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/AppVulkan.kt b/app/src/main/java/com/droiddeck/launcher/gpu/AppVulkan.kt index 437f232f8..59818f263 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/AppVulkan.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/AppVulkan.kt @@ -27,18 +27,23 @@ data class AppVulkan( if (!DeviceSupport.vkBridge(context)) return null val native = VulkanInfo.query() val file = File(VkBridgeComponent.cacheDir(context), BRIDGE_FILE) - val fromBridge = FileUtils.readString(file)?.let { fromBridgeFile(it, native["device"]) } + val fromBridge = FileUtils.readString(file)?.let { fromBridgeFile(it, native["device"], native["api"]) } val found = fromBridge ?: fromDriver(native) Log.i(TAG, found?.label ?: "unknown (driver query: ${native["error"] ?: "no api line"})") return found } - internal fun fromBridgeFile(text: String, driverDevice: String?): AppVulkan? { + internal fun fromBridgeFile(text: String, driverDevice: String?, driverApi: String? = null): AppVulkan? { val map = keyValues(text) val device = map["device"] if (device != null && driverDevice != null && device != driverDevice) return null - val (major, minor) = version(map["api"]) ?: return null - return AppVulkan(major, minor, bool(map["shader_int64"]), map["max_push_constants"]?.toIntOrNull(), "bridge") + val fileDriver = version(map["driver_api"]) + val native = version(driverApi) + if (fileDriver != null && native != null && fileDriver != native) return null + val promoted = map["promotion"]?.trim()?.lowercase() == "on" + val (major, minor) = (if (promoted) version(map["api"]) else fileDriver ?: version(map["api"])) ?: return null + return AppVulkan(major, minor, bool(map["shader_int64"]), map["max_push_constants"]?.toIntOrNull(), + if (promoted) "bridge, promoted" else "bridge") } internal fun fromDriver(map: Map): AppVulkan? { diff --git a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt index ca5afd557..17f3d93da 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt @@ -1041,8 +1041,13 @@ object ComponentsManager { val p = proton(context, protonId) val state = loadState(context) val entry = state.sub("active").optJSONObject(p.id)?.optJSONObject(comp) - if (entry?.optString("protonVersion") == p.version && originalFile(context, p, comp).isFile) + val sameProton = entry?.optString("protonVersion") == p.version + if (sameProton && originalFile(context, p, comp).isFile) return restore(context, protonId, comp, safeName(p.version)) + if (sameProton) { + Log.w(TAG, "${p.name}: its own ${LABEL[comp]} copy is gone; keeping ${entry?.optString("label")}") + return "${LABEL[comp]}: ${p.name} keeps ${entry?.optString("label")} (its own copy is gone)" + } state.sub("active").optJSONObject(p.id)?.remove(comp) saveState(context, state) "${LABEL[comp]}: ${p.name} keeps its own copy" @@ -1064,11 +1069,13 @@ object ComponentsManager { if (rules.isEmpty() && !bridgeSession) return null val installed = protons(context) val lines = mutableListOf() - if (bridgeSession) { + fun each(p: Proton, what: () -> String): String = + runCatching(what).getOrElse { e -> Log.w(TAG, "${p.name}: ${e.message}", e); "${p.name}: ${e.message ?: e.javaClass.simpleName}" } + if (bridgeSession && vulkan != null) { val state = loadState(context) for (p in installed) { val entry = state.optJSONObject("active")?.optJSONObject(p.id)?.optJSONObject("dxvk") - if (bridgeRule == null && autoChoiceStale(entry, null)) lines.add(undoAuto(p.id, "dxvk")) + if (bridgeRule == null && autoChoiceStale(entry, null)) lines.add(each(p) { undoAuto(p.id, "dxvk") }) } } for ((rule, auto) in rules) { @@ -1085,7 +1092,7 @@ object ComponentsManager { continue } if (auto) lines.add("${vulkan?.label}: DXVK ${rule.file}") - lines.add(targets.joinToString("; ") { if (auto) applyAuto(it.id, rule.file) else apply(it.id, rule.file) }) + lines.add(targets.joinToString("; ") { p -> if (auto) each(p) { applyAuto(p.id, rule.file) } else apply(p.id, rule.file) }) } return lines.filter { it.isNotEmpty() }.joinToString("; ").ifEmpty { null } } diff --git a/app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt b/app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt index 9bcc7b682..5f621a23a 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt @@ -59,13 +59,24 @@ class BridgeDxvkTest { } @Test fun theBridgesFileWinsForItsOwnGpuOnly() { - val text = "device=Mali-G710 MC10\napi=1.2.177\ndriver_api=1.1.177\nshader_int64=0\nmax_push_constants=128\n" - assertEquals(promoted, AppVulkan.fromBridgeFile(text, "Mali-G710 MC10")) - assertEquals(promoted, AppVulkan.fromBridgeFile(text, null)) + val text = "device=Mali-G710 MC10\napi=1.2.177\ndriver_api=1.1.177\npromotion=on\nshader_int64=0\nmax_push_constants=128\n" + val on = promoted.copy(source = "bridge, promoted") + assertEquals(on, AppVulkan.fromBridgeFile(text, "Mali-G710 MC10")) + assertEquals(on, AppVulkan.fromBridgeFile(text, null)) + assertEquals(on, AppVulkan.fromBridgeFile(text, "Mali-G710 MC10", "1.1.177")) assertNull(AppVulkan.fromBridgeFile(text, "Mali-G720 Immortalis MC12")) assertNull(AppVulkan.fromBridgeFile("device=x\n", null)) } + @Test fun withoutPromotionOnTheDriversVersionCounts() { + val g710File = g710.copy(source = "bridge") + val off = "device=Mali-G710 MC10\napi=1.1.177\ndriver_api=1.1.177\npromotion=off\nshader_int64=0\nmax_push_constants=128\n" + assertEquals(g710File, AppVulkan.fromBridgeFile(off, "Mali-G710 MC10", "1.1.177")) + val old = "device=Mali-G710 MC10\napi=1.3.177\ndriver_api=1.1.177\nshader_int64=0\nmax_push_constants=128\n" + assertEquals(g710File, AppVulkan.fromBridgeFile(old, "Mali-G710 MC10")) + assertNull(AppVulkan.fromBridgeFile(off, "Mali-G710 MC10", "1.3.212")) + } + @Test fun anAdrenoSessionIsNotTouched() { proton("proton-exp") assertNull(run(g710, bridge = false)) @@ -120,6 +131,30 @@ class BridgeDxvkTest { assertNull(state().getJSONObject("active").optJSONObject("proton-exp")?.optJSONObject("dxvk")) } + @Test fun aFailedDetectionKeepsTheLastAutomaticChoice() { + proton("proton-exp") + writeState("proton-exp", JSONObject().put("file", old).put("protonVersion", "experimental-11").put("auto", true)) + assertNull(run(null)) + assertEquals(old, state().getJSONObject("active").getJSONObject("proton-exp").getJSONObject("dxvk").getString("file")) + } + + @Test fun oneProtonFailingDoesNotStopTheOthers() { + proton("ge") + proton("proton-exp") + val applied = mutableListOf() + val result = run(g710, applyAuto = { id, file -> if (id == "ge") error("disk full"); applied += "$id $file"; "swapped $id" }) + assertEquals(listOf("proton-exp $old"), applied) + assertTrue(result!!, result.contains("disk full") && result.contains("swapped proton-exp")) + } + + @Test fun aChoiceWhoseSavedCopyIsGoneIsKept() { + proton("proton-exp") + writeState("proton-exp", JSONObject().put("file", old).put("label", "DXVK 1.10.3").put("protonVersion", "experimental-11").put("auto", true)) + val result = run(full, undoAuto = { id, comp -> ComponentsManager.undoAutoChoice(context, id, comp) }) + assertTrue(result!!, result.contains("its own copy is gone")) + assertEquals(old, state().getJSONObject("active").getJSONObject("proton-exp").getJSONObject("dxvk").getString("file")) + } + @Test fun aProtonUpdateWithNoRuleLeftJustForgetsTheChoice() { proton("proton-exp", "experimental-12") writeState("proton-exp", JSONObject().put("file", old).put("protonVersion", "experimental-11").put("auto", true)) From 7aeb194d6e369fddabb6f6efde6172c2dd8822e7 Mon Sep 17 00:00:00 2001 From: ASEbrahim <264076441+ASEbrahim@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:13:52 +0300 Subject: [PATCH 15/21] Mali old DXVK: a setting to keep Proton's own, and Proton's own back when the bridge is off Setup has a "DXVK" row under the bridge's safe mode: automatic (the default, as before) or Proton's own (SessionPrefs.bridgeProtonDxvk). With Proton's own, nothing is chosen and an earlier automatic choice goes back to the Proton's copy. Automatic choices also go back when the session is not a bridge session (the bridge turned off), even when nothing is known of the Vulkan; a queued automatic swap is dropped with them, the player's own queued swaps stay. Turning the bridge off, or choosing Proton's own, does it right away in the background instead of at the next session. A failed detection in a bridge session still keeps the last choice. --- .../droiddeck/launcher/FrontEndActivity.kt | 14 ++++- .../launcher/session/ComponentsManager.kt | 42 ++++++++++++-- .../launcher/session/SessionPrefs.kt | 6 ++ .../droiddeck/launcher/ui/FrontEndScreen.kt | 2 + .../droiddeck/launcher/ui/FrontEndSetup.kt | 12 ++++ app/src/main/res/values-zh-rCN/strings.xml | 5 ++ app/src/main/res/values-zh-rHK/strings.xml | 5 ++ app/src/main/res/values-zh-rTW/strings.xml | 5 ++ app/src/main/res/values/strings.xml | 5 ++ .../launcher/session/BridgeDxvkTest.kt | 55 ++++++++++++++++++- 10 files changed, 142 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt index a592b0c5e..9b16a4886 100644 --- a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt @@ -150,6 +150,7 @@ class FrontEndActivity : ComponentActivity() { private var zinkLazy by mutableStateOf(false) private var vkBridge by mutableStateOf(false) private var vkBridgeSafe by mutableStateOf(true) + private var bridgeProtonDxvk by mutableStateOf(false) private val adrenoGpu by lazy { com.droiddeck.launcher.core.DeviceSupport.adreno() } private var noXalia by mutableStateOf(true) private var fastSync by mutableStateOf(false) @@ -812,7 +813,7 @@ class FrontEndActivity : ComponentActivity() { FrontEndScreen( FrontEndState( installed = installed, ready = ready, available = available?.version, removalPending = removalPending, runtimeActionsBlocked = runtimeChangesBlocked(), - vkBridge = vkBridge, vkBridgeSafe = vkBridgeSafe, adreno = adrenoGpu, + vkBridge = vkBridge, vkBridgeSafe = vkBridgeSafe, bridgeProtonDxvk = bridgeProtonDxvk, adreno = adrenoGpu, shortcutPicker = shortcutPicker, shortcutLibraryScanning = shortcutLibraryScanning, gameSyncFolder = gameSyncFolder, @@ -953,8 +954,15 @@ class FrontEndActivity : ComponentActivity() { onInstallPackage = { id -> installPackage(id) }, onRemovePackage = { id -> removePackage(id) }, onRuntime = { onRuntimeButton() }, - onVkBridge = { on -> SessionPrefs.setVkBridge(this, on); vkBridge = on }, + onVkBridge = { on -> + SessionPrefs.setVkBridge(this, on); vkBridge = on + if (!on) com.droiddeck.launcher.session.ComponentsManager.settleAutoDxvk(this) + }, onVkBridgeSafe = { on -> SessionPrefs.setVkBridgeSafe(this, on); vkBridgeSafe = on }, + onBridgeProtonDxvk = { on -> + SessionPrefs.setBridgeProtonDxvk(this, on); bridgeProtonDxvk = on + if (on) com.droiddeck.launcher.session.ComponentsManager.settleAutoDxvk(this) + }, onFrameGenPick = { mode -> FrameGen.set(this, mode) frameGenLabel = FrameGen.label(this) @@ -2080,6 +2088,7 @@ class FrontEndActivity : ComponentActivity() { zinkLazy = SessionPrefs.zinkLazy(this) vkBridge = SessionPrefs.vkBridge(this) vkBridgeSafe = SessionPrefs.vkBridgeSafe(this) + bridgeProtonDxvk = SessionPrefs.bridgeProtonDxvk(this) noXalia = SessionPrefs.noXalia(this) fastSync = SessionPrefs.fastSync(this) syncFallback = SessionPrefs.syncFallback(this) @@ -2115,6 +2124,7 @@ class FrontEndActivity : ComponentActivity() { ready = LinuxRuntime.isInstalled(this) vkBridge = SessionPrefs.vkBridge(this) vkBridgeSafe = SessionPrefs.vkBridgeSafe(this) + bridgeProtonDxvk = SessionPrefs.bridgeProtonDxvk(this) removalPending = LinuxRuntimeInstaller.hasRemovalPending(this) frameGenLabel = FrameGen.label(this) romsDir = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() } diff --git a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt index 17f3d93da..0505bcea1 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt @@ -1020,13 +1020,28 @@ object ComponentsManager { internal const val DXVK_FOR_VULKAN_1_1 = "dxvk-1.10.3-linux.wcp" internal const val DXVK_FOR_VULKAN_1_3_NO_INT64 = "dxvk-2.6.2-linux.wcp" + internal fun autoDxvkUndoWanted(bridgeSession: Boolean, keepProtonDxvk: Boolean, vulkan: AppVulkan?, bridgeRule: ForcedPackage?): Boolean = + !bridgeSession || keepProtonDxvk || (vulkan != null && bridgeRule == null) + + private fun dropQueuedAuto(context: Context, installed: List) = synchronized(lock) { + val state = loadState(context) + var changed = false + for (p in installed) { + val q = state.optJSONObject("queued")?.optJSONObject(p.id) ?: continue + if (q.optJSONObject("dxvk")?.optBoolean("auto") == true) { q.remove("dxvk"); changed = true } + } + if (changed) saveState(context, state) + } + internal fun autoChoiceStale(entry: JSONObject?, wantedFile: String?): Boolean = entry != null && entry.optBoolean("auto") && entry.optString("file") != wantedFile internal fun catalogMatch(items: List, file: String, release: String): CatalogItem? = items.firstOrNull { it.file == file && it.release == release } - fun ensureForcedPackages(context: Context): String? = ensureForcedPackages( + private val forcedLock = Any() + + fun ensureForcedPackages(context: Context): String? = synchronized(forcedLock) { ensureForcedPackages( context, GpuInfo.detect(), { file, release -> catalogItem(context, file, release) }, { download(context, it) {} }, @@ -1035,7 +1050,18 @@ object ComponentsManager { bridgeSession = com.droiddeck.launcher.core.DeviceSupport.vkBridge(context), applyAuto = { id, file -> swap(context, id, file, auto = true) }, undoAuto = { id, comp -> undoAutoChoice(context, id, comp) }, - ) + keepProtonDxvk = SessionPrefs.bridgeProtonDxvk(context), + autoPossible = !com.droiddeck.launcher.core.DeviceSupport.adreno(), + ) } + + fun settleAutoDxvk(context: Context) { + val app = context.applicationContext + Thread({ + runCatching { ensureForcedPackages(app) } + .onSuccess { if (it != null) Log.i(TAG, it) } + .onFailure { Log.w(TAG, "automatic DXVK", it) } + }, "auto-dxvk").start() + } internal fun undoAutoChoice(context: Context, protonId: String, comp: String): String = synchronized(lock) { val p = proton(context, protonId) @@ -1063,19 +1089,23 @@ object ComponentsManager { bridgeSession: Boolean = false, applyAuto: (protonId: String, file: String) -> String = apply, undoAuto: (protonId: String, comp: String) -> String = { _, _ -> "" }, + keepProtonDxvk: Boolean = false, + autoPossible: Boolean = true, ): String? { - val bridgeRule = if (bridgeSession) bridgeDxvk(vulkan) else null + val chooseDxvk = bridgeSession && !keepProtonDxvk + val bridgeRule = if (chooseDxvk) bridgeDxvk(vulkan) else null val rules = FORCED_PACKAGES.filter { it.matches(gpu) }.map { it to false } + listOfNotNull(bridgeRule?.let { it to true }) - if (rules.isEmpty() && !bridgeSession) return null + if (rules.isEmpty() && !autoPossible) return null val installed = protons(context) val lines = mutableListOf() fun each(p: Proton, what: () -> String): String = runCatching(what).getOrElse { e -> Log.w(TAG, "${p.name}: ${e.message}", e); "${p.name}: ${e.message ?: e.javaClass.simpleName}" } - if (bridgeSession && vulkan != null) { + if (autoPossible && autoDxvkUndoWanted(bridgeSession, keepProtonDxvk, vulkan, bridgeRule)) { + dropQueuedAuto(context, installed) val state = loadState(context) for (p in installed) { val entry = state.optJSONObject("active")?.optJSONObject(p.id)?.optJSONObject("dxvk") - if (bridgeRule == null && autoChoiceStale(entry, null)) lines.add(each(p) { undoAuto(p.id, "dxvk") }) + if (autoChoiceStale(entry, null)) lines.add(each(p) { undoAuto(p.id, "dxvk") }) } } for ((rule, auto) in rules) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index 34ae95149..45f746721 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -604,6 +604,12 @@ object SessionPrefs { } @JvmStatic + fun bridgeProtonDxvk(context: Context): Boolean = prefs(context).getBoolean("bridgeProtonDxvk", false) + + fun setBridgeProtonDxvk(context: Context, on: Boolean) { + prefs(context).edit().putBoolean("bridgeProtonDxvk", on).apply() + } + fun vkBridgeSafe(context: Context): Boolean = prefs(context).getBoolean("vkBridgeSafe", true) fun setVkBridgeSafe(context: Context, on: Boolean) { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt index d9ee66632..f15dbe3cc 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -140,6 +140,7 @@ class FrontEndState( val adreno: Boolean = true, val vkBridge: Boolean = false, val vkBridgeSafe: Boolean = true, + val bridgeProtonDxvk: Boolean = false, val showPhantomGate: Boolean = false, val orientation: Int = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, val launcherOrientation: Int = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER, @@ -198,6 +199,7 @@ class FrontEndActions( val onRuntime: () -> Unit, val onVkBridge: (Boolean) -> Unit = {}, val onVkBridgeSafe: (Boolean) -> Unit = {}, + val onBridgeProtonDxvk: (Boolean) -> Unit = {}, val onFrameGenPick: (FrameGen.Mode) -> Unit, val onImportLossless: () -> Unit, val onProtons: () -> Unit, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt index e93832a7e..297f3534e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -258,6 +258,18 @@ internal fun SetupPanel( ) }, ) + if (!s.adreno && s.vkBridge) CheckRow( + CheckState.OK, + stringResource(R.string.setup_bridge_dxvk), + stringResource(if (s.bridgeProtonDxvk) R.string.setup_bridge_dxvk_proton_detail else R.string.setup_bridge_dxvk_auto_detail), + divider = divider, + action = { + SecondaryButton( + stringResource(if (s.bridgeProtonDxvk) R.string.setup_bridge_dxvk_proton else R.string.setup_bridge_dxvk_auto), + enabled = !s.sessionRunning, compact = true, onClick = { a.onBridgeProtonDxvk(!s.bridgeProtonDxvk) }, + ) + }, + ) } val runtimeCheck: @Composable (Boolean) -> Unit = { divider -> CheckRow( diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2b5f4083a..3b54cb480 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -3064,4 +3064,9 @@ 显示 云 云设置 + DXVK + DXVK:自动 + DXVK:Proton 自带 + 此 GPU 的 Vulkan 版本对 Proton 自带的 DXVK 来说太旧时,改用较旧的 DXVK。 + 每个 Proton 都使用自带的 DXVK,即使此 GPU 的 Vulkan 版本可能太旧。 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 54058fe19..3d8b8eff9 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -3063,4 +3063,9 @@ 顯示 雲端 雲端設定 + DXVK + DXVK:自動 + DXVK:Proton 內建 + 此 GPU 的 Vulkan 版本對 Proton 內建的 DXVK 來說太舊時,改用較舊的 DXVK。 + 每個 Proton 都使用內建的 DXVK,即使此 GPU 的 Vulkan 版本可能太舊。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 88adc508f..31dd74463 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -3063,4 +3063,9 @@ 顯示 雲端 雲端設定 + DXVK + DXVK:自動 + DXVK:Proton 內建 + 此 GPU 的 Vulkan 版本對 Proton 內建的 DXVK 來說太舊時,改用較舊的 DXVK。 + 每個 Proton 都使用內建的 DXVK,即使此 GPU 的 Vulkan 版本可能太舊。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7995209fe..545bf2abb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3233,5 +3233,10 @@ Safe mode: off Most compatible. Recommended for a new device. Faster where this GPU passed its checks. + DXVK + DXVK: automatic + DXVK: Proton\'s own + Uses an older DXVK where this GPU\'s Vulkan is too old for Proton\'s. + Every Proton keeps its own DXVK, even where this GPU\'s Vulkan may be too old for it. %1$s is not an Adreno. The experimental Vulkan bridge is on: the session draws through the phone\'s own driver. Expect bugs; send the session logs. diff --git a/app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt b/app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt index 5f621a23a..45afb7785 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt @@ -163,9 +163,62 @@ class BridgeDxvkTest { assertEquals("bundled DXVK experimental-12", File(protonDir("proton-exp"), "files/lib/wine/dxvk/aarch64-windows/d3d11.dll").readText()) } + @Test fun thePlayerCanKeepProtonsOwnDxvk() { + proton("proton-exp") + assertNull("nothing chosen when declined", run(g710, keep = true)) + writeState("proton-exp", JSONObject().put("file", old).put("protonVersion", "experimental-11").put("auto", true)) + val undone = mutableListOf() + run(g710, keep = true, undoAuto = { id, comp -> undone += "$id $comp"; "undone" }) + assertEquals("an earlier automatic choice goes back", listOf("proton-exp dxvk"), undone) + } + + @Test fun turningTheBridgeOffRestoresProtonsOwn() { + proton("proton-exp") + val d3d11 = File(protonDir("proton-exp"), "files/lib/wine/dxvk/aarch64-windows/d3d11.dll") + writePackage(old, "DXVK 1.10.3") + val auto: (String, String) -> String = { id, file -> ComponentsManager.swap(context, id, file, auto = true) } + val undo: (String, String) -> String = { id, comp -> ComponentsManager.undoAutoChoice(context, id, comp) } + run(g710, applyAuto = auto, undoAuto = undo) + assertEquals("DXVK 1.10.3", d3d11.readText()) + run(null, bridge = false, applyAuto = auto, undoAuto = undo) + assertEquals("bundled DXVK experimental-11", d3d11.readText()) + assertNull(state().getJSONObject("active").optJSONObject("proton-exp")?.optJSONObject("dxvk")) + } + + @Test fun turningTheBridgeOffKeepsThePlayersOwnChoice() { + proton("proton-exp") + writeState("proton-exp", JSONObject().put("file", "dxvk-3.1-linux.wcp").put("protonVersion", "experimental-11")) + assertNull(run(g710, bridge = false)) + assertNull(run(g710, keep = true)) + } + + @Test fun aQueuedAutomaticSwapIsDroppedWhenTheBridgeIsOff() { + proton("proton-exp") + val f = File(context.filesDir, "components/state.json") + f.parentFile!!.mkdirs() + f.writeText(JSONObject().put("active", JSONObject()) + .put("queued", JSONObject().put("proton-exp", JSONObject() + .put("dxvk", JSONObject().put("kind", "package").put("file", old).put("auto", true)) + .put("vkd3d", JSONObject().put("kind", "package").put("file", "vkd3d-x.wcp")))).toString()) + assertNull(run(null, bridge = false)) + val q = state().getJSONObject("queued").getJSONObject("proton-exp") + assertFalse(q.has("dxvk")) + assertTrue("the player's own queued swap stays", q.has("vkd3d")) + } + + @Test fun whenAutomaticChoicesAreUndone() { + val rule = ComponentsManager.bridgeDxvk(g710) + assertTrue(ComponentsManager.autoDxvkUndoWanted(false, false, null, null)) + assertTrue(ComponentsManager.autoDxvkUndoWanted(true, true, g710, null)) + assertTrue(ComponentsManager.autoDxvkUndoWanted(true, false, full, null)) + assertFalse("failed detection keeps it", ComponentsManager.autoDxvkUndoWanted(true, false, null, null)) + assertFalse(ComponentsManager.autoDxvkUndoWanted(true, false, g710, rule)) + } + private fun run( vk: AppVulkan?, bridge: Boolean = true, + keep: Boolean = false, applyAuto: (String, String) -> String = { id, file -> error("apply $id $file") }, undoAuto: (String, String) -> String = { id, comp -> error("undo $id $comp") }, ) = ComponentsManager.ensureForcedPackages( @@ -173,7 +226,7 @@ class BridgeDxvkTest { lookup = { file, release -> ComponentsManager.CatalogItem(file, "dxvk", release, "https://example.invalid/$file", 4, "sha256:" + "ab".repeat(32)) }, fetch = {}, apply = { id, file -> error("non-automatic apply $id $file") }, - vulkan = vk, bridgeSession = bridge, applyAuto = applyAuto, undoAuto = undoAuto, + vulkan = vk, bridgeSession = bridge, applyAuto = applyAuto, undoAuto = undoAuto, keepProtonDxvk = keep, ) private fun protonDir(name: String) = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps/common/$name") From 9c8d2acacc2cf4fb2410051d5c133f48e9cd04f5 Mon Sep 17 00:00:00 2001 From: maxjivi05 Date: Sun, 25 Oct 2026 19:59:26 -0400 Subject: [PATCH 16/21] Turn the Mali bridge on by default on a Mali GPU A device with /dev/mali0 and no KGSL starts with the bridge on, so it draws without a trip to Setup. An Adreno keeps Turnip and its automatic driver route whatever the setting says, other GPUs keep the switch off until it is turned on, and a choice made in Setup always wins. The GPU checks are read once per process. --- .../java/com/droiddeck/launcher/core/DeviceSupport.kt | 9 ++++++++- .../java/com/droiddeck/launcher/session/SessionPrefs.kt | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt b/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt index 1036f5c06..ee214fedf 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt @@ -6,8 +6,15 @@ import com.droiddeck.launcher.R import java.io.File object DeviceSupport { - fun adreno(): Boolean = + private val adrenoGpu by lazy { File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists() + } + + private val maliGpu by lazy { !adrenoGpu && File("/dev/mali0").exists() } + + fun adreno(): Boolean = adrenoGpu + + fun mali(): Boolean = maliGpu fun vkBridge(context: Context): Boolean = !adreno() && com.droiddeck.launcher.session.SessionPrefs.vkBridge(context) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index 45f746721..a7555bff0 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -597,7 +597,7 @@ object SessionPrefs { fun zinkLazy(context: Context): Boolean = prefs(context).getBoolean("zinkLazy", true) @JvmStatic - fun vkBridge(context: Context): Boolean = prefs(context).getBoolean("vkBridge", false) + fun vkBridge(context: Context): Boolean = prefs(context).getBoolean("vkBridge", com.droiddeck.launcher.core.DeviceSupport.mali()) fun setVkBridge(context: Context, on: Boolean) { prefs(context).edit().putBoolean("vkBridge", on).apply() From 3639196c83f74f06da431bc6bede37f76a0d2c07 Mon Sep 17 00:00:00 2001 From: maxjivi05 Date: Sun, 25 Oct 2026 20:01:08 -0400 Subject: [PATCH 17/21] Translate the Mali bridge settings --- app/src/main/res/values-ar/strings.xml | 13 +++++++++++++ app/src/main/res/values-b+es+419/strings.xml | 13 +++++++++++++ app/src/main/res/values-bg/strings.xml | 13 +++++++++++++ app/src/main/res/values-cs/strings.xml | 13 +++++++++++++ app/src/main/res/values-da/strings.xml | 13 +++++++++++++ app/src/main/res/values-de/strings.xml | 13 +++++++++++++ app/src/main/res/values-el/strings.xml | 13 +++++++++++++ app/src/main/res/values-es/strings.xml | 13 +++++++++++++ app/src/main/res/values-fi/strings.xml | 13 +++++++++++++ app/src/main/res/values-fr/strings.xml | 13 +++++++++++++ app/src/main/res/values-hi/strings.xml | 13 +++++++++++++ app/src/main/res/values-hu/strings.xml | 13 +++++++++++++ app/src/main/res/values-in/strings.xml | 13 +++++++++++++ app/src/main/res/values-it/strings.xml | 13 +++++++++++++ app/src/main/res/values-ja/strings.xml | 13 +++++++++++++ app/src/main/res/values-ko/strings.xml | 13 +++++++++++++ app/src/main/res/values-ms/strings.xml | 13 +++++++++++++ app/src/main/res/values-nb/strings.xml | 13 +++++++++++++ app/src/main/res/values-nl/strings.xml | 13 +++++++++++++ app/src/main/res/values-pl/strings.xml | 13 +++++++++++++ app/src/main/res/values-pt-rPT/strings.xml | 13 +++++++++++++ app/src/main/res/values-pt/strings.xml | 13 +++++++++++++ app/src/main/res/values-ro/strings.xml | 13 +++++++++++++ app/src/main/res/values-ru/strings.xml | 13 +++++++++++++ app/src/main/res/values-sv/strings.xml | 13 +++++++++++++ app/src/main/res/values-th/strings.xml | 13 +++++++++++++ app/src/main/res/values-tr/strings.xml | 13 +++++++++++++ app/src/main/res/values-uk/strings.xml | 13 +++++++++++++ app/src/main/res/values-vi/strings.xml | 13 +++++++++++++ app/src/main/res/values-zh-rCN/strings.xml | 8 ++++++++ app/src/main/res/values-zh-rHK/strings.xml | 8 ++++++++ app/src/main/res/values-zh-rTW/strings.xml | 8 ++++++++ 32 files changed, 401 insertions(+) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index fe7c0e138..c327f8de5 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -3221,4 +3221,17 @@ عرض سحابية إعدادات السحابة + جسر Mali: متوقف + جسر Mali: يعمل + %1$s ليس Adreno. جسر Vulkan التجريبي يعمل: تعرض الجلسة الرسومات عبر برنامج تشغيل الهاتف نفسه. توقّع وجود أخطاء؛ أرسل سجلات الجلسة. + الوضع الآمن + الوضع الآمن: يعمل + الوضع الآمن: متوقف + الأكثر توافقًا. يوصى به لجهاز جديد. + أسرع حيث اجتاز معالج الرسومات هذا فحوصاته. + DXVK + DXVK: تلقائي + DXVK: الخاص بـ Proton + يستخدم إصدار DXVK أقدم عندما يكون Vulkan في معالج الرسومات هذا أقدم من أن يدعم DXVK الخاص بـ Proton. + يحتفظ كل Proton بـ DXVK الخاص به، حتى عندما قد يكون Vulkan في معالج الرسومات هذا أقدم من اللازم. diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 1609361f2..1522a1780 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -3071,4 +3071,17 @@ Mostrar Nube Configuración de nube + Puente Mali: desactivado + Puente Mali: activado + %1$s no es una Adreno. El puente Vulkan experimental está activado: la sesión dibuja a través del propio controlador del teléfono. Espera errores; envía los registros de la sesión. + Modo seguro + Modo seguro: activado + Modo seguro: desactivado + El más compatible. Recomendado para un dispositivo nuevo. + Más rápido donde esta GPU pasó sus pruebas. + DXVK + DXVK: automático + DXVK: el de Proton + Usa un DXVK más antiguo cuando el Vulkan de esta GPU es demasiado antiguo para el de Proton. + Cada Proton conserva su propio DXVK, aunque el Vulkan de esta GPU pueda ser demasiado antiguo para él. diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index a40f3e38d..026901aec 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -3085,4 +3085,17 @@ Показване Облак Настройки на облака + Mali мост: изкл. + Mali мост: вкл. + %1$s не е Adreno. Експерименталният Vulkan мост е включен: сесията рисува през собствения драйвер на телефона. Очаквайте грешки; изпратете логовете на сесията. + Безопасен режим + Безопасен режим: вкл. + Безопасен режим: изкл. + Най-съвместим. Препоръчва се за ново устройство. + По-бързо там, където този GPU е минал проверките си. + DXVK + DXVK: автоматично + DXVK: на Proton + Използва по-стар DXVK, когато Vulkan на този GPU е твърде стар за този на Proton. + Всеки Proton запазва своя DXVK, дори когато Vulkan на този GPU може да е твърде стар за него. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4d493645a..7efe217ac 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -3153,4 +3153,17 @@ Zobrazit Cloud Nastavení cloudu + Most Mali: vypnuto + Most Mali: zapnuto + %1$s není Adreno. Experimentální most Vulkan je zapnutý: relace vykresluje přes vlastní ovladač telefonu. Počítejte s chybami; pošlete protokoly relace. + Bezpečný režim + Bezpečný režim: zapnuto + Bezpečný režim: vypnuto + Nejvyšší kompatibilita. Doporučeno pro nové zařízení. + Rychlejší tam, kde tento GPU prošel kontrolami. + DXVK + DXVK: automaticky + DXVK: vlastní z Protonu + Použije starší DXVK, pokud je Vulkan tohoto GPU pro DXVK z Protonu příliš starý. + Každý Proton si ponechá vlastní DXVK, i když pro něj může být Vulkan tohoto GPU příliš starý. diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index f737e6bd7..3b1099428 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -3085,4 +3085,17 @@ Vis Sky Skyindstillinger + Mali-bro: fra + Mali-bro: til + %1$s er ikke en Adreno. Den eksperimentelle Vulkan-bro er slået til: sessionen tegner gennem telefonens egen driver. Forvent fejl; send sessionslogfilerne. + Sikker tilstand + Sikker tilstand: til + Sikker tilstand: fra + Mest kompatibel. Anbefales til en ny enhed. + Hurtigere, hvor denne GPU har bestået sine tjek. + DXVK + DXVK: automatisk + DXVK: Protons egen + Bruger en ældre DXVK, hvor denne GPU\'s Vulkan er for gammel til Protons. + Hver Proton beholder sin egen DXVK, også hvor denne GPU\'s Vulkan kan være for gammel til den. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8e9b65332..87033e585 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -3085,4 +3085,17 @@ Anzeigen Cloud Cloud-Einstellungen + Mali-Brücke: aus + Mali-Brücke: an + %1$s ist kein Adreno. Die experimentelle Vulkan-Brücke ist an: Die Sitzung zeichnet über den eigenen Treiber des Telefons. Rechne mit Fehlern und sende die Sitzungsprotokolle. + Sicherer Modus + Sicherer Modus: an + Sicherer Modus: aus + Am kompatibelsten. Empfohlen für ein neues Gerät. + Schneller, wo diese GPU ihre Prüfungen bestanden hat. + DXVK + DXVK: automatisch + DXVK: von Proton + Nutzt ein älteres DXVK, wenn das Vulkan dieser GPU für das von Proton zu alt ist. + Jedes Proton behält sein eigenes DXVK, auch wenn das Vulkan dieser GPU dafür zu alt sein könnte. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 2da1c1b2a..6a7985cfd 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -3085,4 +3085,17 @@ Εμφάνιση Cloud Ρυθμίσεις cloud + Γέφυρα Mali: ανενεργή + Γέφυρα Mali: ενεργή + Η %1$s δεν είναι Adreno. Η πειραματική γέφυρα Vulkan είναι ενεργή: η συνεδρία σχεδιάζει μέσω του προγράμματος οδήγησης του ίδιου του τηλεφώνου. Αναμένετε σφάλματα· στείλτε τα αρχεία καταγραφής της συνεδρίας. + Ασφαλής λειτουργία + Ασφαλής λειτουργία: ενεργή + Ασφαλής λειτουργία: ανενεργή + Μέγιστη συμβατότητα. Συνιστάται για νέα συσκευή. + Ταχύτερη όπου αυτή η GPU πέρασε τους ελέγχους της. + DXVK + DXVK: αυτόματο + DXVK: του Proton + Χρησιμοποιεί παλαιότερο DXVK όταν το Vulkan αυτής της GPU είναι πολύ παλιό για αυτό του Proton. + Κάθε Proton κρατά το δικό του DXVK, ακόμη κι αν το Vulkan αυτής της GPU ίσως είναι πολύ παλιό για αυτό. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 45f4cfc67..a14d2cc8f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -3121,4 +3121,17 @@ Mostrar Nube Ajustes de nube + Puente Mali: desactivado + Puente Mali: activado + %1$s no es una Adreno. El puente Vulkan experimental está activado: la sesión dibuja a través del propio controlador del teléfono. Espera errores; envía los registros de la sesión. + Modo seguro + Modo seguro: activado + Modo seguro: desactivado + El más compatible. Recomendado para un dispositivo nuevo. + Más rápido donde esta GPU superó sus comprobaciones. + DXVK + DXVK: automático + DXVK: el de Proton + Usa un DXVK más antiguo cuando el Vulkan de esta GPU es demasiado antiguo para el de Proton. + Cada Proton conserva su propio DXVK, aunque el Vulkan de esta GPU pueda ser demasiado antiguo para él. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index ddf4d4605..66ff8b4d0 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -3085,4 +3085,17 @@ Näytä Pilvi Pilviasetukset + Mali-silta: pois + Mali-silta: päällä + %1$s ei ole Adreno. Kokeellinen Vulkan-silta on päällä: istunto piirtää puhelimen oman ajurin kautta. Odota virheitä; lähetä istunnon lokit. + Turvatila + Turvatila: päällä + Turvatila: pois + Yhteensopivin. Suositellaan uudelle laitteelle. + Nopeampi siellä, missä tämä GPU läpäisi tarkistuksensa. + DXVK + DXVK: automaattinen + DXVK: Protonin oma + Käyttää vanhempaa DXVK:ta, kun tämän GPU:n Vulkan on liian vanha Protonin omalle. + Jokainen Proton pitää oman DXVK:nsa, vaikka tämän GPU:n Vulkan voi olla sille liian vanha. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 535cbf487..a290a7e97 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -3121,4 +3121,17 @@ Afficher Cloud Paramètres cloud + Pont Mali : désactivé + Pont Mali : activé + %1$s n\'est pas un Adreno. Le pont Vulkan expérimental est activé : la session dessine via le pilote du téléphone lui-même. Attendez-vous à des bugs ; envoyez les journaux de session. + Mode sans échec + Mode sans échec : activé + Mode sans échec : désactivé + Le plus compatible. Recommandé pour un nouvel appareil. + Plus rapide là où ce GPU a réussi ses vérifications. + DXVK + DXVK : automatique + DXVK : celui de Proton + Utilise un DXVK plus ancien quand le Vulkan de ce GPU est trop ancien pour celui de Proton. + Chaque Proton garde son propre DXVK, même quand le Vulkan de ce GPU risque d\'être trop ancien pour lui. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index be2c1b803..63246c462 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -3085,4 +3085,17 @@ दिखाएँ क्लाउड क्लाउड सेटिंग्स + Mali ब्रिज: बंद + Mali ब्रिज: चालू + %1$s Adreno नहीं है। प्रायोगिक Vulkan ब्रिज चालू है: सत्र फ़ोन के अपने ड्राइवर से ड्रॉ करता है। बग की उम्मीद रखें; सत्र लॉग भेजें। + सुरक्षित मोड + सुरक्षित मोड: चालू + सुरक्षित मोड: बंद + सबसे अधिक संगत। नए डिवाइस के लिए अनुशंसित। + जहाँ इस GPU ने अपनी जाँचें पास कीं, वहाँ तेज़। + DXVK + DXVK: स्वचालित + DXVK: Proton का अपना + जहाँ इस GPU का Vulkan Proton के DXVK के लिए बहुत पुराना है, वहाँ पुराना DXVK इस्तेमाल करता है। + हर Proton अपना DXVK रखता है, भले ही इस GPU का Vulkan उसके लिए बहुत पुराना हो। diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 6902c2cc3..4bf813672 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -3085,4 +3085,17 @@ Megjelenítés Felhő Felhőbeállítások + Mali híd: ki + Mali híd: be + A(z) %1$s nem Adreno. A kísérleti Vulkan híd be van kapcsolva: a munkamenet a telefon saját illesztőprogramján keresztül rajzol. Hibákra számíts; küldd el a munkamenet naplóit. + Biztonságos mód + Biztonságos mód: be + Biztonságos mód: ki + A legjobb kompatibilitás. Új eszközhöz ajánlott. + Gyorsabb ott, ahol ez a GPU átment az ellenőrzéseken. + DXVK + DXVK: automatikus + DXVK: a Protoné + Régebbi DXVK-t használ, ha ennek a GPU-nak a Vulkanja túl régi a Proton sajátjához. + Minden Proton megtartja a saját DXVK-ját, akkor is, ha ennek a GPU-nak a Vulkanja túl régi lehet hozzá. diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 24ce23f45..e59e92edf 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -3075,4 +3075,17 @@ Tampilkan Cloud Pengaturan cloud + Jembatan Mali: mati + Jembatan Mali: nyala + %1$s bukan Adreno. Jembatan Vulkan eksperimental menyala: sesi menggambar melalui driver ponsel itu sendiri. Bersiaplah menemui bug; kirim log sesi. + Mode aman + Mode aman: nyala + Mode aman: mati + Paling kompatibel. Disarankan untuk perangkat baru. + Lebih cepat di bagian yang lolos pemeriksaan GPU ini. + DXVK + DXVK: otomatis + DXVK: bawaan Proton + Memakai DXVK lebih lama jika Vulkan GPU ini terlalu lama untuk DXVK bawaan Proton. + Setiap Proton tetap memakai DXVK bawaannya, meski Vulkan GPU ini mungkin terlalu lama untuknya. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 524eba544..c356086ce 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -3119,4 +3119,17 @@ Mostra Cloud Impostazioni cloud + Ponte Mali: disattivato + Ponte Mali: attivo + %1$s non è una Adreno. Il ponte Vulkan sperimentale è attivo: la sessione disegna tramite il driver del telefono. Aspettati bug; invia i log della sessione. + Modalità sicura + Modalità sicura: attiva + Modalità sicura: disattivata + La più compatibile. Consigliata per un nuovo dispositivo. + Più veloce dove questa GPU ha superato i controlli. + DXVK + DXVK: automatico + DXVK: quello di Proton + Usa un DXVK più vecchio se il Vulkan di questa GPU è troppo vecchio per quello di Proton. + Ogni Proton mantiene il proprio DXVK, anche se il Vulkan di questa GPU potrebbe essere troppo vecchio. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 4d605fa6b..dbd8d5b64 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -3062,4 +3062,17 @@ 表示 クラウド クラウド設定 + Mali ブリッジ: オフ + Mali ブリッジ: オン + %1$s は Adreno ではありません。試験的な Vulkan ブリッジがオンです。セッションはスマートフォン本体のドライバーで描画します。不具合が出る可能性があります。セッションログを送ってください。 + セーフモード + セーフモード: オン + セーフモード: オフ + 最も互換性が高い設定です。新しいデバイスにおすすめです。 + この GPU がチェックに合格した部分で高速になります。 + DXVK + DXVK: 自動 + DXVK: Proton 付属 + この GPU の Vulkan が Proton 付属の DXVK には古すぎる場合、古い DXVK を使います。 + この GPU の Vulkan が古すぎる場合でも、各 Proton は付属の DXVK を使い続けます。 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 9420ce605..46c77c2da 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -3062,4 +3062,17 @@ 표시 클라우드 클라우드 설정 + Mali 브리지: 끔 + Mali 브리지: 켬 + %1$s은(는) Adreno가 아닙니다. 실험적 Vulkan 브리지가 켜져 있어 세션이 휴대폰 자체 드라이버로 그립니다. 버그가 있을 수 있으니 세션 로그를 보내 주세요. + 안전 모드 + 안전 모드: 켬 + 안전 모드: 끔 + 호환성이 가장 높습니다. 새 기기에 권장합니다. + 이 GPU가 검사를 통과한 부분에서 더 빠릅니다. + DXVK + DXVK: 자동 + DXVK: Proton 기본 + 이 GPU의 Vulkan이 Proton 기본 DXVK에 너무 오래된 경우 이전 DXVK를 사용합니다. + 이 GPU의 Vulkan이 너무 오래되었을 수 있어도 각 Proton은 자체 DXVK를 유지합니다. diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 218fc9a81..e8d85ffc8 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -3060,4 +3060,17 @@ Tunjukkan Awan Tetapan awan + Jambatan Mali: mati + Jambatan Mali: hidup + %1$s bukan Adreno. Jambatan Vulkan eksperimen dihidupkan: sesi melukis melalui pemacu telefon itu sendiri. Jangkakan pepijat; hantar log sesi. + Mod selamat + Mod selamat: hidup + Mod selamat: mati + Paling serasi. Disyorkan untuk peranti baharu. + Lebih pantas di bahagian yang lulus semakan GPU ini. + DXVK + DXVK: automatik + DXVK: milik Proton + Menggunakan DXVK yang lebih lama jika Vulkan GPU ini terlalu lama untuk DXVK Proton. + Setiap Proton mengekalkan DXVK sendiri, walaupun Vulkan GPU ini mungkin terlalu lama untuknya. diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index bbb6c8a24..0e27e1a58 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -3085,4 +3085,17 @@ Vis Sky Skyinnstillinger + Mali-bro: av + Mali-bro: på + %1$s er ikke en Adreno. Den eksperimentelle Vulkan-broen er på: økten tegner gjennom telefonens egen driver. Forvent feil; send øktloggene. + Sikker modus + Sikker modus: på + Sikker modus: av + Mest kompatibel. Anbefales for en ny enhet. + Raskere der denne GPU-en besto kontrollene sine. + DXVK + DXVK: automatisk + DXVK: Protons egen + Bruker en eldre DXVK der denne GPU-ens Vulkan er for gammel for Protons. + Hver Proton beholder sin egen DXVK, selv der denne GPU-ens Vulkan kan være for gammel for den. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index e8c394cf1..753869f71 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -3085,4 +3085,17 @@ Weergeven Cloud Cloudinstellingen + Mali-brug: uit + Mali-brug: aan + %1$s is geen Adreno. De experimentele Vulkan-brug staat aan: de sessie tekent via het eigen stuurprogramma van de telefoon. Verwacht fouten; stuur de sessielogboeken. + Veilige modus + Veilige modus: aan + Veilige modus: uit + Meest compatibel. Aanbevolen voor een nieuw apparaat. + Sneller waar deze GPU zijn controles heeft doorstaan. + DXVK + DXVK: automatisch + DXVK: die van Proton + Gebruikt een oudere DXVK als de Vulkan van deze GPU te oud is voor die van Proton. + Elke Proton houdt zijn eigen DXVK, ook als de Vulkan van deze GPU daarvoor te oud kan zijn. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 8d1736c64..3ebc340c5 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -3156,4 +3156,17 @@ Pokaż Chmura Ustawienia chmury + Most Mali: wył. + Most Mali: wł. + %1$s to nie Adreno. Eksperymentalny most Vulkan jest włączony: sesja rysuje przez własny sterownik telefonu. Spodziewaj się błędów; wyślij logi sesji. + Tryb bezpieczny + Tryb bezpieczny: wł. + Tryb bezpieczny: wył. + Największa zgodność. Zalecany dla nowego urządzenia. + Szybciej tam, gdzie ten GPU przeszedł testy. + DXVK + DXVK: automatycznie + DXVK: z Protona + Używa starszego DXVK, gdy Vulkan tego GPU jest za stary dla DXVK z Protona. + Każdy Proton zachowuje własny DXVK, nawet jeśli Vulkan tego GPU może być dla niego za stary. diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index f2c4bf67c..99a7c8a54 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -3119,4 +3119,17 @@ Mostrar Nuvem Definições da nuvem + Ponte Mali: desligada + Ponte Mali: ligada + %1$s não é uma Adreno. A ponte Vulkan experimental está ligada: a sessão desenha através do próprio controlador do telemóvel. Conte com erros; envie os registos da sessão. + Modo seguro + Modo seguro: ligado + Modo seguro: desligado + O mais compatível. Recomendado para um dispositivo novo. + Mais rápido onde esta GPU passou nas verificações. + DXVK + DXVK: automático + DXVK: o do Proton + Usa um DXVK mais antigo quando o Vulkan desta GPU é demasiado antigo para o do Proton. + Cada Proton mantém o seu próprio DXVK, mesmo quando o Vulkan desta GPU pode ser demasiado antigo para ele. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 3ade72aee..caaf99844 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -3119,4 +3119,17 @@ Mostrar Nuvem Configurações da nuvem + Ponte Mali: desligada + Ponte Mali: ligada + %1$s não é uma Adreno. A ponte Vulkan experimental está ligada: a sessão desenha pelo próprio driver do celular. Espere bugs; envie os logs da sessão. + Modo seguro + Modo seguro: ligado + Modo seguro: desligado + O mais compatível. Recomendado para um dispositivo novo. + Mais rápido onde esta GPU passou nas verificações. + DXVK + DXVK: automático + DXVK: o do Proton + Usa um DXVK mais antigo quando o Vulkan desta GPU é antigo demais para o do Proton. + Cada Proton mantém o próprio DXVK, mesmo quando o Vulkan desta GPU pode ser antigo demais para ele. diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 2305d87ab..244a029fe 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -3119,4 +3119,17 @@ Afișează Cloud Setări cloud + Punte Mali: oprită + Punte Mali: pornită + %1$s nu este Adreno. Puntea Vulkan experimentală este pornită: sesiunea desenează prin driverul propriu al telefonului. Așteaptă-te la erori; trimite jurnalele sesiunii. + Mod sigur + Mod sigur: pornit + Mod sigur: oprit + Cel mai compatibil. Recomandat pentru un dispozitiv nou. + Mai rapid acolo unde acest GPU a trecut verificările. + DXVK + DXVK: automat + DXVK: al Proton + Folosește un DXVK mai vechi când Vulkan-ul acestui GPU este prea vechi pentru cel din Proton. + Fiecare Proton își păstrează propriul DXVK, chiar dacă Vulkan-ul acestui GPU poate fi prea vechi pentru el. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 9a7447b3d..e871da9ca 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -3155,4 +3155,17 @@ Показать Облако Настройки облака + Мост Mali: выкл. + Мост Mali: вкл. + %1$s — не Adreno. Включён экспериментальный мост Vulkan: сеанс рисует через собственный драйвер телефона. Возможны ошибки; отправьте журналы сеанса. + Безопасный режим + Безопасный режим: вкл. + Безопасный режим: выкл. + Максимальная совместимость. Рекомендуется для нового устройства. + Быстрее там, где этот GPU прошёл проверки. + DXVK + DXVK: автоматически + DXVK: из Proton + Использует более старый DXVK, если Vulkan этого GPU слишком стар для DXVK из Proton. + Каждый Proton оставляет свой DXVK, даже если Vulkan этого GPU может быть для него слишком старым. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index f50d90edf..5004c2a48 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -3085,4 +3085,17 @@ Visa Moln Molninställningar + Mali-brygga: av + Mali-brygga: på + %1$s är inte en Adreno. Den experimentella Vulkan-bryggan är på: sessionen ritar via telefonens egen drivrutin. Räkna med buggar; skicka sessionsloggarna. + Säkert läge + Säkert läge: på + Säkert läge: av + Mest kompatibelt. Rekommenderas för en ny enhet. + Snabbare där den här GPU:n klarade sina kontroller. + DXVK + DXVK: automatiskt + DXVK: Protons egen + Använder en äldre DXVK när den här GPU:ns Vulkan är för gammal för Protons. + Varje Proton behåller sin egen DXVK, även där den här GPU:ns Vulkan kan vara för gammal för den. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index ef164e408..f77e19e5c 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -3060,4 +3060,17 @@ แสดง คลาวด์ การตั้งค่าคลาวด์ + บริดจ์ Mali: ปิด + บริดจ์ Mali: เปิด + %1$s ไม่ใช่ Adreno บริดจ์ Vulkan รุ่นทดลองเปิดอยู่: เซสชันวาดผ่านไดรเวอร์ของโทรศัพท์เอง อาจพบบั๊ก โปรดส่งบันทึกของเซสชัน + โหมดปลอดภัย + โหมดปลอดภัย: เปิด + โหมดปลอดภัย: ปิด + เข้ากันได้มากที่สุด แนะนำสำหรับอุปกรณ์ใหม่ + เร็วขึ้นในส่วนที่ GPU นี้ผ่านการตรวจสอบ + DXVK + DXVK: อัตโนมัติ + DXVK: ของ Proton + ใช้ DXVK รุ่นเก่ากว่าเมื่อ Vulkan ของ GPU นี้เก่าเกินไปสำหรับ DXVK ของ Proton + Proton แต่ละตัวใช้ DXVK ของตัวเอง แม้ Vulkan ของ GPU นี้อาจเก่าเกินไปสำหรับมัน diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 379511296..99d05d10c 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -3087,4 +3087,17 @@ Göster Bulut Bulut ayarları + Mali köprüsü: kapalı + Mali köprüsü: açık + %1$s bir Adreno değil. Deneysel Vulkan köprüsü açık: oturum, telefonun kendi sürücüsü üzerinden çizer. Hatalar olabilir; oturum günlüklerini gönderin. + Güvenli mod + Güvenli mod: açık + Güvenli mod: kapalı + En uyumlu. Yeni bir cihaz için önerilir. + Bu GPU\'nun kontrollerini geçtiği yerlerde daha hızlı. + DXVK + DXVK: otomatik + DXVK: Proton\'unki + Bu GPU\'nun Vulkan\'ı Proton\'un DXVK\'sı için çok eskiyse daha eski bir DXVK kullanır. + Bu GPU\'nun Vulkan\'ı onun için çok eski olsa bile her Proton kendi DXVK\'sını korur. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 78eef7de8..b4380e6a8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -3153,4 +3153,17 @@ Показати Хмара Налаштування хмари + Міст Mali: вимк. + Міст Mali: увімк. + %1$s — не Adreno. Увімкнено експериментальний міст Vulkan: сеанс малює через власний драйвер телефона. Можливі помилки; надішліть журнали сеансу. + Безпечний режим + Безпечний режим: увімк. + Безпечний режим: вимк. + Найкраща сумісність. Рекомендовано для нового пристрою. + Швидше там, де цей GPU пройшов перевірки. + DXVK + DXVK: автоматично + DXVK: з Proton + Використовує старіший DXVK, якщо Vulkan цього GPU застарий для DXVK з Proton. + Кожен Proton залишає свій DXVK, навіть якщо Vulkan цього GPU може бути для нього застарим. diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 475bef045..4a5d3a134 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -3060,4 +3060,17 @@ Hiển thị Đám mây Cài đặt đám mây + Cầu Mali: tắt + Cầu Mali: bật + %1$s không phải Adreno. Cầu Vulkan thử nghiệm đang bật: phiên vẽ qua trình điều khiển của chính điện thoại. Có thể gặp lỗi; hãy gửi nhật ký phiên. + Chế độ an toàn + Chế độ an toàn: bật + Chế độ an toàn: tắt + Tương thích nhất. Nên dùng cho thiết bị mới. + Nhanh hơn ở những chỗ GPU này đã vượt qua kiểm tra. + DXVK + DXVK: tự động + DXVK: của Proton + Dùng DXVK cũ hơn khi Vulkan của GPU này quá cũ cho DXVK của Proton. + Mỗi Proton giữ DXVK riêng, kể cả khi Vulkan của GPU này có thể quá cũ cho nó. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3b54cb480..f314826a2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -3069,4 +3069,12 @@ DXVK:Proton 自带 此 GPU 的 Vulkan 版本对 Proton 自带的 DXVK 来说太旧时,改用较旧的 DXVK。 每个 Proton 都使用自带的 DXVK,即使此 GPU 的 Vulkan 版本可能太旧。 + Mali 桥接:关 + Mali 桥接:开 + %1$s 不是 Adreno。实验性 Vulkan 桥接已开启:会话通过手机自带的驱动绘制。可能会有问题,请发送会话日志。 + 安全模式 + 安全模式:开 + 安全模式:关 + 兼容性最好。推荐新设备使用。 + 在此 GPU 通过检查的地方更快。 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 3d8b8eff9..66ccf8982 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -3068,4 +3068,12 @@ DXVK:Proton 內建 此 GPU 的 Vulkan 版本對 Proton 內建的 DXVK 來說太舊時,改用較舊的 DXVK。 每個 Proton 都使用內建的 DXVK,即使此 GPU 的 Vulkan 版本可能太舊。 + Mali 橋接:關 + Mali 橋接:開 + %1$s 不是 Adreno。實驗性 Vulkan 橋接已開啟:工作階段透過手機內建的驅動程式繪製。可能會有問題,請傳送工作階段記錄。 + 安全模式 + 安全模式:開 + 安全模式:關 + 相容性最好。建議新裝置使用。 + 在此 GPU 通過檢查的地方較快。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 31dd74463..0fd53316c 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -3068,4 +3068,12 @@ DXVK:Proton 內建 此 GPU 的 Vulkan 版本對 Proton 內建的 DXVK 來說太舊時,改用較舊的 DXVK。 每個 Proton 都使用內建的 DXVK,即使此 GPU 的 Vulkan 版本可能太舊。 + Mali 橋接:關 + Mali 橋接:開 + %1$s 不是 Adreno。實驗性 Vulkan 橋接已開啟:工作階段透過手機內建的驅動程式繪製。可能會有問題,請傳送工作階段記錄。 + 安全模式 + 安全模式:開 + 安全模式:關 + 相容性最好。建議新裝置使用。 + 在此 GPU 通過檢查的地方較快。 From fdc4ff607d281933c74eef48e0d682f937393996 Mon Sep 17 00:00:00 2001 From: maxjivi05 Date: Sun, 25 Oct 2026 20:01:43 -0400 Subject: [PATCH 18/21] Credit MaliBridge by ASEbrahim --- README.md | 2 +- docs/third-party/malibridge/NOTICE | 21 ++++++++++++++++++ docs/third-party/malibridge/README.md | 32 +++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 docs/third-party/malibridge/NOTICE create mode 100644 docs/third-party/malibridge/README.md diff --git a/README.md b/README.md index 80958dcda..0876bac79 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Compatibility and performance vary by device; hardware validation is limited. De ## Credits and licence -GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. Battle.net support is [@kayakyakr](https://github.com/kayakyakr)'s: it runs Blizzard's own Battle.net client, downloaded from Blizzard and not included, in Proton, and shows the key art from each game's [Battle.net Shop](https://shop.battle.net) page; without the Steam client its games see DroidDeck's pad as an Xbox 360 controller through `PROTON_SPOOF_STEAMINPUT_VIDPID` in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/winebus.sys`. Its games install from Stores through Blizzard's own Agent and the local HTTP API it serves, driven the way [@barncastle](https://github.com/barncastle)'s [Battle.Net-Installer](https://github.com/barncastle/Battle.Net-Installer) drives it, with the requests described on [wowdev.wiki](https://wowdev.wiki/Agent); how other launchers find, install and start Blizzard games was read in [@mubaraknumann](https://github.com/mubaraknumann)'s [unifideck](https://github.com/mubaraknumann/unifideck), [@djmittens](https://github.com/djmittens)'s [neo-mittens](https://github.com/djmittens/neo-mittens) `bnetswitch`, [@BCD1210](https://github.com/BCD1210)'s [soju](https://github.com/BCD1210/soju) and [Lutris](https://github.com/lutris/lutris)'s `product.db` reader, without taking their code. The registry entries a GOG game's install script (`goggame-.script`) sets are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher)'s GOG setup (GPL-3.0) reads them. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence, which also starts the arm64 emulator AppImages whose own runtime cannot run its extractor on Android. x86 AppImages packed with [sharun](https://github.com/VHSgunzo/sharun) by VHSgunzo (MIT) start because DroidDeck's own [FEX](https://github.com/FEX-Emu/FEX) build (MIT) puts the platform name under `AT_PLATFORM`, which sharun's [userland-execve](https://crates.io/crates/userland-execve) loader (MIT) requires; FEX filed it under `AT_BASE_PLATFORM`. The Steam client's audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). x86 programs that link OpenSSL 3 under FEX use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt) platform. Hooks written with `WriteProcessMemory` into code [FEX](https://github.com/FEX-Emu/FEX) has translated go through `/proc//mem` again when the kernel refuses `process_vm_writev`, as [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` wrote them before Wine 11. x86 Linux games under FEX get the same `/etc/asound.conf` that [pressure-vessel](https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) writes into the Steam Linux Runtime, so ALSA plays through PulseAudio. Games render with [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's own per-render-pass choice between tile memory and system memory, while the Steam client keeps `TU_DEBUG=sysmem`; [FEX](https://github.com/FEX-Emu/FEX)'s code disk cache was checked in its source and is already on through Proton. The proot fast path picks the guest name of a host file bound under several names in the order [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later) sorts its bindings, so links and `/proc/self/fd` read back as they do through proot. The session's [PipeWire](https://gitlab.freedesktop.org/pipewire/pipewire) (MIT) opens its sockets with the per-socket access settings documented in its `module-access.c`, and [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) (MIT) keeps those clients at full permissions through its `access.rules`. Wine write watches use the userfaultfd support in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/ntdll/unix/virtual.c` (LGPL-2.1-or-later): libblsession gives it the arm64 syscall number and answers its `PAGEMAP_SCAN` from `/proc/self/pagemap` where Android refuses the ioctl. Wine on arm64 sets a thread's full context by raising `SIGUSR2` from `syscall_dispatcher_return_slowpath` in [Wine](https://gitlab.winehq.org/wine/wine)'s `dlls/ntdll/unix/signal_arm64.c` (LGPL-2.1-or-later); libblsession runs that handler in place on a frame laid out as the [Linux](https://github.com/torvalds/linux) kernel's `arch/arm64/kernel/signal.c` builds it and returns through `rt_sigreturn`, so proot does not stop the thread for it. Wine's process list reads each process's memory counters from `/proc//status` in `fill_vm_counters` ([Wine](https://gitlab.winehq.org/wine/wine) `dlls/ntdll/unix/process.c`, LGPL-2.1-or-later), so libblsession gives those reads the kernel's file instead of a copy with `TracerPid` cleared. droiddeck-ntsync is checked against the object, wait and error semantics documented for the [Linux](https://github.com/torvalds/linux) ntsync driver in `Documentation/userspace-api/ntsync.rst` (GPL-2.0). Xbox games load the Windows modules of [@domsvoboda](https://github.com/domsvoboda)'s [xodus-nexus](https://github.com/domsvoboda/xodus-nexus) runtime (LGPL-2.1-or-later), including its `wintypes.dll`, whose `ApiInformation` answer Minecraft for Windows needs to start; how Minecraft installs GameInput under Wine was checked against [@Wyze3306](https://github.com/Wyze3306)'s [BedrockOnLinux](https://github.com/Wyze3306/BedrockOnLinux) (MIT). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve. +GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. Battle.net support is [@kayakyakr](https://github.com/kayakyakr)'s: it runs Blizzard's own Battle.net client, downloaded from Blizzard and not included, in Proton, and shows the key art from each game's [Battle.net Shop](https://shop.battle.net) page; without the Steam client its games see DroidDeck's pad as an Xbox 360 controller through `PROTON_SPOOF_STEAMINPUT_VIDPID` in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/winebus.sys`. Its games install from Stores through Blizzard's own Agent and the local HTTP API it serves, driven the way [@barncastle](https://github.com/barncastle)'s [Battle.Net-Installer](https://github.com/barncastle/Battle.Net-Installer) drives it, with the requests described on [wowdev.wiki](https://wowdev.wiki/Agent); how other launchers find, install and start Blizzard games was read in [@mubaraknumann](https://github.com/mubaraknumann)'s [unifideck](https://github.com/mubaraknumann/unifideck), [@djmittens](https://github.com/djmittens)'s [neo-mittens](https://github.com/djmittens/neo-mittens) `bnetswitch`, [@BCD1210](https://github.com/BCD1210)'s [soju](https://github.com/BCD1210/soju) and [Lutris](https://github.com/lutris/lutris)'s `product.db` reader, without taking their code. The registry entries a GOG game's install script (`goggame-.script`) sets are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher)'s GOG setup (GPL-3.0) reads them. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence, which also starts the arm64 emulator AppImages whose own runtime cannot run its extractor on Android. x86 AppImages packed with [sharun](https://github.com/VHSgunzo/sharun) by VHSgunzo (MIT) start because DroidDeck's own [FEX](https://github.com/FEX-Emu/FEX) build (MIT) puts the platform name under `AT_PLATFORM`, which sharun's [userland-execve](https://crates.io/crates/userland-execve) loader (MIT) requires; FEX filed it under `AT_BASE_PLATFORM`. The Steam client's audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). x86 programs that link OpenSSL 3 under FEX use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt) platform. Hooks written with `WriteProcessMemory` into code [FEX](https://github.com/FEX-Emu/FEX) has translated go through `/proc//mem` again when the kernel refuses `process_vm_writev`, as [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` wrote them before Wine 11. x86 Linux games under FEX get the same `/etc/asound.conf` that [pressure-vessel](https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) writes into the Steam Linux Runtime, so ALSA plays through PulseAudio. Games render with [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's own per-render-pass choice between tile memory and system memory, while the Steam client keeps `TU_DEBUG=sysmem`; [FEX](https://github.com/FEX-Emu/FEX)'s code disk cache was checked in its source and is already on through Proton. The proot fast path picks the guest name of a host file bound under several names in the order [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later) sorts its bindings, so links and `/proc/self/fd` read back as they do through proot. The session's [PipeWire](https://gitlab.freedesktop.org/pipewire/pipewire) (MIT) opens its sockets with the per-socket access settings documented in its `module-access.c`, and [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) (MIT) keeps those clients at full permissions through its `access.rules`. Wine write watches use the userfaultfd support in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/ntdll/unix/virtual.c` (LGPL-2.1-or-later): libblsession gives it the arm64 syscall number and answers its `PAGEMAP_SCAN` from `/proc/self/pagemap` where Android refuses the ioctl. Wine on arm64 sets a thread's full context by raising `SIGUSR2` from `syscall_dispatcher_return_slowpath` in [Wine](https://gitlab.winehq.org/wine/wine)'s `dlls/ntdll/unix/signal_arm64.c` (LGPL-2.1-or-later); libblsession runs that handler in place on a frame laid out as the [Linux](https://github.com/torvalds/linux) kernel's `arch/arm64/kernel/signal.c` builds it and returns through `rt_sigreturn`, so proot does not stop the thread for it. Wine's process list reads each process's memory counters from `/proc//status` in `fill_vm_counters` ([Wine](https://gitlab.winehq.org/wine/wine) `dlls/ntdll/unix/process.c`, LGPL-2.1-or-later), so libblsession gives those reads the kernel's file instead of a copy with `TracerPid` cleared. droiddeck-ntsync is checked against the object, wait and error semantics documented for the [Linux](https://github.com/torvalds/linux) ntsync driver in `Documentation/userspace-api/ntsync.rst` (GPL-2.0). Xbox games load the Windows modules of [@domsvoboda](https://github.com/domsvoboda)'s [xodus-nexus](https://github.com/domsvoboda/xodus-nexus) runtime (LGPL-2.1-or-later), including its `wintypes.dll`, whose `ApiInformation` answer Minecraft for Windows needs to start; how Minecraft installs GameInput under Wine was checked against [@Wyze3306](https://github.com/Wyze3306)'s [BedrockOnLinux](https://github.com/Wyze3306/BedrockOnLinux) (MIT). GPUs other than Adreno draw through [MaliBridge by ASEbrahim](https://github.com/ASEbrahim/MaliBridge) (GPL-3.0-or-later, with the attribution terms in its NOTICE), built from the `external/MaliBridge` submodule; its DroidDeck integration is [@ASEbrahim](https://github.com/ASEbrahim)'s work from their [DroidDeck fork](https://github.com/ASEbrahim/DroidDeck), and it credits Vulkan-Headers, bcdec, Lee Gao's etc2_encode and Mali research, David Cimo, The412Banner and others; see [docs/third-party/malibridge](docs/third-party/malibridge/README.md). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve. XR glasses support builds on WinNative's VITURE glasses work. VITURE protocol details were checked against [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) by [@wheaney](https://github.com/wheaney), including the VITURE protocol header it ships, [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) by [@badicsalex](https://github.com/badicsalex) and the [go-macos/iokit VITURE package](https://pkg.go.dev/github.com/go-macos/iokit/viture); the optional VITURE SDK belongs to VITURE and is not included. RayNeo support adapts [@informalTechCode](https://github.com/informalTechCode)'s [RayNeo-Air-Series-3DoF-Scaffold](https://github.com/informalTechCode/RayNeo-Air-Series-3DoF-Scaffold) (Apache-2.0) and references [@verncat](https://github.com/verncat)'s [RayNeo-Air-3S-Pro-OpenVR](https://github.com/verncat/RayNeo-Air-3S-Pro-OpenVR) (MIT) and [Monado](https://gitlab.freedesktop.org/monado/monado) (BSL-1.0); see [docs/third-party/rayneo](docs/third-party/rayneo/README.md). XREAL 1S support references [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) and [xrealOneDeviceKit](https://github.com/wheaney/xrealOneDeviceKit) by @wheaney, [xreal_one_driver](https://github.com/rohitsangwan01/xreal_one_driver) by [@rohitsangwan01](https://github.com/rohitsangwan01) (MIT) and [xr-tools](https://github.com/0xcaff/xr-tools) by [@0xcaff](https://github.com/0xcaff). Rokid support follows the Rokid protocol notes in [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) and @badicsalex's [Void Computing write-ups](https://voidcomputing.hu/blog/worse-better-prettier/), and [Monado](https://gitlab.freedesktop.org/monado/monado)'s Rokid driver. The REDMAGIC glasses-display findings draw on [@mekhontsev](https://github.com/mekhontsev)'s [MagicDesk investigation](https://github.com/mekhontsev/magicdesk/blob/main/docs/xr-resolution-fix.md) and [@Coding-BR](https://github.com/Coding-BR)'s [decompiled Nubia kernel](https://github.com/Coding-BR/android_kernel_nubia_sm8850_qwjujube). VITURE, RayNeo, XREAL, Rokid and REDMAGIC are trademarks of their owners; this project is not affiliated with them. diff --git a/docs/third-party/malibridge/NOTICE b/docs/third-party/malibridge/NOTICE new file mode 100644 index 000000000..6f8ab48c2 --- /dev/null +++ b/docs/third-party/malibridge/NOTICE @@ -0,0 +1,21 @@ +MaliBridge +Copyright (C) 2026 ASEbrahim and MaliBridge contributors +https://github.com/ASEbrahim/MaliBridge + +MaliBridge is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. See LICENSE. + +Additional terms under section 7 of the GNU GPL version 3: + +1. Attribution (section 7(b)). Anyone who conveys MaliBridge, or a work based on it, in source or + object form must keep the notice "MaliBridge by ASEbrahim" with a link to + https://github.com/ASEbrahim/MaliBridge in the conveyed work's documentation, and, where the + work has a user interface that shows credits, licences or an "about" screen, in that screen. + +2. Names (section 7(e)). This licence grants no right to use the name "MaliBridge" or the + author's name to name or promote a modified version. A modified version must be marked as + changed and must not be presented as MaliBridge or as the author's work. + +Third-party components keep their own licences and notices (third_party/ and the files that name +them). diff --git a/docs/third-party/malibridge/README.md b/docs/third-party/malibridge/README.md new file mode 100644 index 000000000..f9cf4117c --- /dev/null +++ b/docs/third-party/malibridge/README.md @@ -0,0 +1,32 @@ +# MaliBridge by ASEbrahim + +MaliBridge by ASEbrahim: https://github.com/ASEbrahim/MaliBridge +(GPL-3.0-or-later with the additional terms in NOTICE here, copied unchanged from the project). + +DroidDeck builds MaliBridge from the `external/MaliBridge` submodule, pinned at 669e668 +(v0.5.3.1) and not modified. Its Vulkan ICD runs inside the Linux runtime and forwards each call +over a Unix socket to its server, which runs in the app on the phone's own Vulkan driver. +DroidDeck uses it only on a GPU without Adreno's KGSL; an Adreno keeps Turnip. + +The DroidDeck side of the bridge (the Setup switches, the server's start and session wiring, the +Mali render node stand-in, the compositor's AHardwareBuffer import off Adreno, the Steam web helper +layout, the socket wait and the DXVK choice for older Vulkan drivers) is Ahmad Sayed Alsayed +Ebrahim's (ASEbrahim) work from https://github.com/ASEbrahim/DroidDeck, kept under their name in +this repository's history. + +MaliBridge's own credits, in its README, include: + +- Vulkan-Headers 1.4.341 (Khronos Group), Apache-2.0 OR MIT: https://github.com/KhronosGroup/Vulkan-Headers +- bcdec by Sergii Kudlai, MIT or Unlicense: https://github.com/iOrange/bcdec +- etc2_encode by Lee Gao, MIT: https://github.com/leegao/etc2_encode +- Lee Gao's Mali findings and SPIRV-Tools MaliOptimizationBarrierPass (Apache-2.0): + https://github.com/leegao/SPIRV-Tools, and compat_layer (MIT): https://github.com/leegao/compat_layer +- Julius Pinsker's bionic-vulkan-wrapper PR #228 (MIT) +- David Cimo's droiddeck-mali (GPL-3.0): https://github.com/DavidCimo/droiddeck-mali +- The412Banner's Mali compatibility layer and testing on the DroidDeck branch `test/non-adreno-all` +- taowen's libhybris `arlinux` branch (Apache-2.0): https://github.com/taowen/libhybris/tree/arlinux +- GameNative (GPL-3.0), wallentx's placed-memory layer for DroidDeck (MIT), Arm's published errata, + ANGLE's Mali workarounds, and design notes from Winlator's Vortek + +Each keeps its own licence and notice in the submodule (`third_party/` and the files that name +them). From c300342a292fe0e5e7a35513c4457a9cde7bbd44 Mon Sep 17 00:00:00 2001 From: maxjivi05 Date: Mon, 26 Oct 2026 14:08:24 -0400 Subject: [PATCH 19/21] Update MaliBridge to v0.5.4 v0.5.4 backs the exported memory with AHardwareBuffers on phones without a dma-buf heap (older Mali such as the G68, G77 and G78) and fixes green and red textures on the Dimensity 9000 series. --- README.md | 2 +- docs/third-party/malibridge/README.md | 5 +++-- external/MaliBridge | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0876bac79..897d78c21 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Compatibility and performance vary by device; hardware validation is limited. De ## Credits and licence -GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. Battle.net support is [@kayakyakr](https://github.com/kayakyakr)'s: it runs Blizzard's own Battle.net client, downloaded from Blizzard and not included, in Proton, and shows the key art from each game's [Battle.net Shop](https://shop.battle.net) page; without the Steam client its games see DroidDeck's pad as an Xbox 360 controller through `PROTON_SPOOF_STEAMINPUT_VIDPID` in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/winebus.sys`. Its games install from Stores through Blizzard's own Agent and the local HTTP API it serves, driven the way [@barncastle](https://github.com/barncastle)'s [Battle.Net-Installer](https://github.com/barncastle/Battle.Net-Installer) drives it, with the requests described on [wowdev.wiki](https://wowdev.wiki/Agent); how other launchers find, install and start Blizzard games was read in [@mubaraknumann](https://github.com/mubaraknumann)'s [unifideck](https://github.com/mubaraknumann/unifideck), [@djmittens](https://github.com/djmittens)'s [neo-mittens](https://github.com/djmittens/neo-mittens) `bnetswitch`, [@BCD1210](https://github.com/BCD1210)'s [soju](https://github.com/BCD1210/soju) and [Lutris](https://github.com/lutris/lutris)'s `product.db` reader, without taking their code. The registry entries a GOG game's install script (`goggame-.script`) sets are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher)'s GOG setup (GPL-3.0) reads them. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence, which also starts the arm64 emulator AppImages whose own runtime cannot run its extractor on Android. x86 AppImages packed with [sharun](https://github.com/VHSgunzo/sharun) by VHSgunzo (MIT) start because DroidDeck's own [FEX](https://github.com/FEX-Emu/FEX) build (MIT) puts the platform name under `AT_PLATFORM`, which sharun's [userland-execve](https://crates.io/crates/userland-execve) loader (MIT) requires; FEX filed it under `AT_BASE_PLATFORM`. The Steam client's audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). x86 programs that link OpenSSL 3 under FEX use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt) platform. Hooks written with `WriteProcessMemory` into code [FEX](https://github.com/FEX-Emu/FEX) has translated go through `/proc//mem` again when the kernel refuses `process_vm_writev`, as [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` wrote them before Wine 11. x86 Linux games under FEX get the same `/etc/asound.conf` that [pressure-vessel](https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) writes into the Steam Linux Runtime, so ALSA plays through PulseAudio. Games render with [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's own per-render-pass choice between tile memory and system memory, while the Steam client keeps `TU_DEBUG=sysmem`; [FEX](https://github.com/FEX-Emu/FEX)'s code disk cache was checked in its source and is already on through Proton. The proot fast path picks the guest name of a host file bound under several names in the order [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later) sorts its bindings, so links and `/proc/self/fd` read back as they do through proot. The session's [PipeWire](https://gitlab.freedesktop.org/pipewire/pipewire) (MIT) opens its sockets with the per-socket access settings documented in its `module-access.c`, and [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) (MIT) keeps those clients at full permissions through its `access.rules`. Wine write watches use the userfaultfd support in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/ntdll/unix/virtual.c` (LGPL-2.1-or-later): libblsession gives it the arm64 syscall number and answers its `PAGEMAP_SCAN` from `/proc/self/pagemap` where Android refuses the ioctl. Wine on arm64 sets a thread's full context by raising `SIGUSR2` from `syscall_dispatcher_return_slowpath` in [Wine](https://gitlab.winehq.org/wine/wine)'s `dlls/ntdll/unix/signal_arm64.c` (LGPL-2.1-or-later); libblsession runs that handler in place on a frame laid out as the [Linux](https://github.com/torvalds/linux) kernel's `arch/arm64/kernel/signal.c` builds it and returns through `rt_sigreturn`, so proot does not stop the thread for it. Wine's process list reads each process's memory counters from `/proc//status` in `fill_vm_counters` ([Wine](https://gitlab.winehq.org/wine/wine) `dlls/ntdll/unix/process.c`, LGPL-2.1-or-later), so libblsession gives those reads the kernel's file instead of a copy with `TracerPid` cleared. droiddeck-ntsync is checked against the object, wait and error semantics documented for the [Linux](https://github.com/torvalds/linux) ntsync driver in `Documentation/userspace-api/ntsync.rst` (GPL-2.0). Xbox games load the Windows modules of [@domsvoboda](https://github.com/domsvoboda)'s [xodus-nexus](https://github.com/domsvoboda/xodus-nexus) runtime (LGPL-2.1-or-later), including its `wintypes.dll`, whose `ApiInformation` answer Minecraft for Windows needs to start; how Minecraft installs GameInput under Wine was checked against [@Wyze3306](https://github.com/Wyze3306)'s [BedrockOnLinux](https://github.com/Wyze3306/BedrockOnLinux) (MIT). GPUs other than Adreno draw through [MaliBridge by ASEbrahim](https://github.com/ASEbrahim/MaliBridge) (GPL-3.0-or-later, with the attribution terms in its NOTICE), built from the `external/MaliBridge` submodule; its DroidDeck integration is [@ASEbrahim](https://github.com/ASEbrahim)'s work from their [DroidDeck fork](https://github.com/ASEbrahim/DroidDeck), and it credits Vulkan-Headers, bcdec, Lee Gao's etc2_encode and Mali research, David Cimo, The412Banner and others; see [docs/third-party/malibridge](docs/third-party/malibridge/README.md). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve. +GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. Battle.net support is [@kayakyakr](https://github.com/kayakyakr)'s: it runs Blizzard's own Battle.net client, downloaded from Blizzard and not included, in Proton, and shows the key art from each game's [Battle.net Shop](https://shop.battle.net) page; without the Steam client its games see DroidDeck's pad as an Xbox 360 controller through `PROTON_SPOOF_STEAMINPUT_VIDPID` in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/winebus.sys`. Its games install from Stores through Blizzard's own Agent and the local HTTP API it serves, driven the way [@barncastle](https://github.com/barncastle)'s [Battle.Net-Installer](https://github.com/barncastle/Battle.Net-Installer) drives it, with the requests described on [wowdev.wiki](https://wowdev.wiki/Agent); how other launchers find, install and start Blizzard games was read in [@mubaraknumann](https://github.com/mubaraknumann)'s [unifideck](https://github.com/mubaraknumann/unifideck), [@djmittens](https://github.com/djmittens)'s [neo-mittens](https://github.com/djmittens/neo-mittens) `bnetswitch`, [@BCD1210](https://github.com/BCD1210)'s [soju](https://github.com/BCD1210/soju) and [Lutris](https://github.com/lutris/lutris)'s `product.db` reader, without taking their code. The registry entries a GOG game's install script (`goggame-.script`) sets are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher)'s GOG setup (GPL-3.0) reads them. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence, which also starts the arm64 emulator AppImages whose own runtime cannot run its extractor on Android. x86 AppImages packed with [sharun](https://github.com/VHSgunzo/sharun) by VHSgunzo (MIT) start because DroidDeck's own [FEX](https://github.com/FEX-Emu/FEX) build (MIT) puts the platform name under `AT_PLATFORM`, which sharun's [userland-execve](https://crates.io/crates/userland-execve) loader (MIT) requires; FEX filed it under `AT_BASE_PLATFORM`. The Steam client's audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). x86 programs that link OpenSSL 3 under FEX use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt) platform. Hooks written with `WriteProcessMemory` into code [FEX](https://github.com/FEX-Emu/FEX) has translated go through `/proc//mem` again when the kernel refuses `process_vm_writev`, as [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` wrote them before Wine 11. x86 Linux games under FEX get the same `/etc/asound.conf` that [pressure-vessel](https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) writes into the Steam Linux Runtime, so ALSA plays through PulseAudio. Games render with [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's own per-render-pass choice between tile memory and system memory, while the Steam client keeps `TU_DEBUG=sysmem`; [FEX](https://github.com/FEX-Emu/FEX)'s code disk cache was checked in its source and is already on through Proton. The proot fast path picks the guest name of a host file bound under several names in the order [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later) sorts its bindings, so links and `/proc/self/fd` read back as they do through proot. The session's [PipeWire](https://gitlab.freedesktop.org/pipewire/pipewire) (MIT) opens its sockets with the per-socket access settings documented in its `module-access.c`, and [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) (MIT) keeps those clients at full permissions through its `access.rules`. Wine write watches use the userfaultfd support in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/ntdll/unix/virtual.c` (LGPL-2.1-or-later): libblsession gives it the arm64 syscall number and answers its `PAGEMAP_SCAN` from `/proc/self/pagemap` where Android refuses the ioctl. Wine on arm64 sets a thread's full context by raising `SIGUSR2` from `syscall_dispatcher_return_slowpath` in [Wine](https://gitlab.winehq.org/wine/wine)'s `dlls/ntdll/unix/signal_arm64.c` (LGPL-2.1-or-later); libblsession runs that handler in place on a frame laid out as the [Linux](https://github.com/torvalds/linux) kernel's `arch/arm64/kernel/signal.c` builds it and returns through `rt_sigreturn`, so proot does not stop the thread for it. Wine's process list reads each process's memory counters from `/proc//status` in `fill_vm_counters` ([Wine](https://gitlab.winehq.org/wine/wine) `dlls/ntdll/unix/process.c`, LGPL-2.1-or-later), so libblsession gives those reads the kernel's file instead of a copy with `TracerPid` cleared. droiddeck-ntsync is checked against the object, wait and error semantics documented for the [Linux](https://github.com/torvalds/linux) ntsync driver in `Documentation/userspace-api/ntsync.rst` (GPL-2.0). Xbox games load the Windows modules of [@domsvoboda](https://github.com/domsvoboda)'s [xodus-nexus](https://github.com/domsvoboda/xodus-nexus) runtime (LGPL-2.1-or-later), including its `wintypes.dll`, whose `ApiInformation` answer Minecraft for Windows needs to start; how Minecraft installs GameInput under Wine was checked against [@Wyze3306](https://github.com/Wyze3306)'s [BedrockOnLinux](https://github.com/Wyze3306/BedrockOnLinux) (MIT). GPUs other than Adreno draw through [MaliBridge by ASEbrahim](https://github.com/ASEbrahim/MaliBridge) (GPL-3.0-or-later, with the attribution terms in its NOTICE), built from the `external/MaliBridge` submodule; its DroidDeck integration is [@ASEbrahim](https://github.com/ASEbrahim)'s work from their [DroidDeck fork](https://github.com/ASEbrahim/DroidDeck), and it credits Vulkan-Headers, Arm's HWCPipe, bcdec, Lee Gao's etc2_encode and Mali research, David Cimo, The412Banner and others; see [docs/third-party/malibridge](docs/third-party/malibridge/README.md). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve. XR glasses support builds on WinNative's VITURE glasses work. VITURE protocol details were checked against [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) by [@wheaney](https://github.com/wheaney), including the VITURE protocol header it ships, [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) by [@badicsalex](https://github.com/badicsalex) and the [go-macos/iokit VITURE package](https://pkg.go.dev/github.com/go-macos/iokit/viture); the optional VITURE SDK belongs to VITURE and is not included. RayNeo support adapts [@informalTechCode](https://github.com/informalTechCode)'s [RayNeo-Air-Series-3DoF-Scaffold](https://github.com/informalTechCode/RayNeo-Air-Series-3DoF-Scaffold) (Apache-2.0) and references [@verncat](https://github.com/verncat)'s [RayNeo-Air-3S-Pro-OpenVR](https://github.com/verncat/RayNeo-Air-3S-Pro-OpenVR) (MIT) and [Monado](https://gitlab.freedesktop.org/monado/monado) (BSL-1.0); see [docs/third-party/rayneo](docs/third-party/rayneo/README.md). XREAL 1S support references [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) and [xrealOneDeviceKit](https://github.com/wheaney/xrealOneDeviceKit) by @wheaney, [xreal_one_driver](https://github.com/rohitsangwan01/xreal_one_driver) by [@rohitsangwan01](https://github.com/rohitsangwan01) (MIT) and [xr-tools](https://github.com/0xcaff/xr-tools) by [@0xcaff](https://github.com/0xcaff). Rokid support follows the Rokid protocol notes in [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) and @badicsalex's [Void Computing write-ups](https://voidcomputing.hu/blog/worse-better-prettier/), and [Monado](https://gitlab.freedesktop.org/monado/monado)'s Rokid driver. The REDMAGIC glasses-display findings draw on [@mekhontsev](https://github.com/mekhontsev)'s [MagicDesk investigation](https://github.com/mekhontsev/magicdesk/blob/main/docs/xr-resolution-fix.md) and [@Coding-BR](https://github.com/Coding-BR)'s [decompiled Nubia kernel](https://github.com/Coding-BR/android_kernel_nubia_sm8850_qwjujube). VITURE, RayNeo, XREAL, Rokid and REDMAGIC are trademarks of their owners; this project is not affiliated with them. diff --git a/docs/third-party/malibridge/README.md b/docs/third-party/malibridge/README.md index f9cf4117c..97882e8a6 100644 --- a/docs/third-party/malibridge/README.md +++ b/docs/third-party/malibridge/README.md @@ -3,8 +3,8 @@ MaliBridge by ASEbrahim: https://github.com/ASEbrahim/MaliBridge (GPL-3.0-or-later with the additional terms in NOTICE here, copied unchanged from the project). -DroidDeck builds MaliBridge from the `external/MaliBridge` submodule, pinned at 669e668 -(v0.5.3.1) and not modified. Its Vulkan ICD runs inside the Linux runtime and forwards each call +DroidDeck builds MaliBridge from the `external/MaliBridge` submodule, pinned at 48477fe +(v0.5.4) and not modified. Its Vulkan ICD runs inside the Linux runtime and forwards each call over a Unix socket to its server, which runs in the app on the phone's own Vulkan driver. DroidDeck uses it only on a GPU without Adreno's KGSL; an Adreno keeps Turnip. @@ -17,6 +17,7 @@ this repository's history. MaliBridge's own credits, in its README, include: - Vulkan-Headers 1.4.341 (Khronos Group), Apache-2.0 OR MIT: https://github.com/KhronosGroup/Vulkan-Headers +- Arm's HWCPipe (MIT), whose counter layouts its GPU counter reader follows: https://github.com/ARM-software/HWCPipe - bcdec by Sergii Kudlai, MIT or Unlicense: https://github.com/iOrange/bcdec - etc2_encode by Lee Gao, MIT: https://github.com/leegao/etc2_encode - Lee Gao's Mali findings and SPIRV-Tools MaliOptimizationBarrierPass (Apache-2.0): diff --git a/external/MaliBridge b/external/MaliBridge index 669e668cf..48477fe6c 160000 --- a/external/MaliBridge +++ b/external/MaliBridge @@ -1 +1 @@ -Subproject commit 669e668cf4a380757dd532d991a35673307a70c3 +Subproject commit 48477fe6c4eb550dccc08ade257240abedabd848 From 018d0e1db933af4757a85a836a79c9b171f1da45 Mon Sep 17 00:00:00 2001 From: K11MCH1 <81425424+K11MCH1@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:20:06 +0900 Subject: [PATCH 20/21] Steam UI on the Mali bridge: gamescope composites every frame Through the Mali bridge the Steam UI flickered: frames alternated with and without its footer while gamescope handed its layers to the phone's compositor as separate planes. Turning gamescope's overlay planes off nearly stopped it (2 flips), and a composited image has none (Mali-G76 MC4 r26p0 at rest: 145 footer flips in 6 s, 0 composited). Where in the plane path it goes wrong is not known. Steam sessions on the bridge set gamescope_composite_force=1 unless it is already set. --- tools/linuxfs/overlay/usr/local/bin/droiddeck-session | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index b91057000..0038778d4 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -307,6 +307,13 @@ if [ -z "${BL_INSIDE:-}" ]; then if [ "$mode" = steam ] && [ "${BL_CURSOR_COMPOSITE:-0}" = 1 ]; then export gamescope_cursor_composite=2 fi + # Through the Mali bridge, Steam's UI flickered while gamescope handed its layers to the phone's + # compositor as separate planes, for a reason not yet found; one composited image does not + # (Mali-G76 MC4: 145 footer flips in 6 s at rest, 0 composited). + if [ "$mode" = steam ] && [ "${BL_VKBRIDGE:-0}" = 1 ] && [ -z "${gamescope_composite_force:-}" ]; then + export gamescope_composite_force=1 + echo "== gamescope: composites every frame (Mali bridge: the Steam UI flickers on separate planes)" + fi args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$nested" -h "$nested_h") if [ "$nested" != "$width" ] || [ "$nested_h" != "$height" ]; then args+=(-S stretch) From e450765eb6419a7dc2efbe3c619d6ab2de218d0d Mon Sep 17 00:00:00 2001 From: K11MCH1 <81425424+K11MCH1@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:20:06 +0900 Subject: [PATCH 21/21] Mali bridge with under 4 GB of RAM: Steam's browser UI in software On a 2.9 GB Mali-G76 MC4 phone the GPU-drawn Steam UI took about 400 MB, and Android's low-memory killer ended DroidDeck itself while The Binding of Isaac loaded; with the UI in software Isaac reached its title screen. Below 4 GB of RAM, bridge sessions now default BL_VKBRIDGE_CEF_GPU to 0; a value already set is kept. The software UI takes about a core while it animates and idles while a game has focus. --- tools/linuxfs/overlay/usr/local/bin/droiddeck-session | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index 0038778d4..1e67b14d6 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -1039,6 +1039,15 @@ BUSCONF _bl_ui=(-nobigpicture) fi if [ "${BL_VKBRIDGE:-0}" = 1 ]; then + # On the GPU the Steam UI held about 400 MB through the bridge (Mali-G76 MC4, 3 GB of RAM); with + # a game loading beside it, Android's low-memory killer took the whole session down. + if [ -z "${BL_VKBRIDGE_CEF_GPU:-}" ]; then + _bl_mem_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null) + if [ "${_bl_mem_kb:-0}" -gt 0 ] && [ "$_bl_mem_kb" -lt 4194304 ]; then + BL_VKBRIDGE_CEF_GPU=0 + echo "== steam: $((_bl_mem_kb / 1024)) MB of RAM: the browser UI draws in software to leave games the memory" + fi + fi if [ "${BL_VKBRIDGE_CEF_GPU:-1}" = 0 ]; then _bl_cef=(-cef-disable-gpu -cef-disable-gpu-compositing) echo "== steam: browser UI in software (Mali bridge)"