diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7f78c0ab4..7418c5bfb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -169,6 +169,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 @@ -232,6 +234,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 @@ -242,6 +245,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: @@ -273,6 +279,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' @@ -559,6 +571,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/.gitmodules b/.gitmodules index 50f7a038b..de75c26af 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ +[submodule "external/MaliBridge"] + path = external/MaliBridge + url = https://github.com/ASEbrahim/MaliBridge.git [submodule "tools/syncthing/syncthing-android"] path = tools/syncthing/syncthing-android url = https://github.com/researchxxl/syncthing-android diff --git a/.signing.env.example b/.signing.env.example new file mode 100644 index 000000000..7b497c6a7 --- /dev/null +++ b/.signing.env.example @@ -0,0 +1,4 @@ +# Copy to .signing.env, fill in, chmod 600 .signing.env +RELEASE_KEYSTORE=/path/to/release.p12 +RELEASE_STORE_PASSWORD=change-me +RELEASE_KEY_ALIAS=droiddeck diff --git a/app/build.gradle b/app/build.gradle index 91fd2ad94..736d728ba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -345,6 +345,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/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/cpp/waylandcomp/src/sc_layer.c b/app/src/main/cpp/waylandcomp/src/sc_layer.c index ff90d37dc..c760d91c2 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 df1c0d817..895e1f6b4 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.c +++ b/app/src/main/cpp/waylandcomp/src/vk_present.c @@ -93,6 +93,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; @@ -460,6 +463,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"); @@ -989,8 +993,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); @@ -1057,6 +1062,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 5cc06ddb3..ac975c93d 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); @@ -33,6 +34,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; diff --git a/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt b/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt index 344aa7452..200deb55a 100644 --- a/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt +++ b/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt @@ -65,7 +65,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl } fun state() = GpuDriversState( - gpuName = gpu.displayName(activity), gpuFamily = gpu.family.label(activity), soc = gpu.soc, supportText = gpu.supportText(activity), + gpuName = gpu.displayName(activity), gpuFamily = gpu.family.label(activity), soc = gpu.soc, supportText = gpu.supportText(activity, com.droiddeck.launcher.core.DeviceSupport.vkBridge(activity)), supported = gpu.support == GpuInfo.Support.TESTED, unsupported = gpu.support == GpuInfo.Support.UNSUPPORTED, auto = mode == SessionPrefs.GPU_DRIVERS_AUTO, pairs = pairRows, busy = pairBusy, percent = pairPercent, autoStatus = autoStatus, releaseStatus = releaseStatus.ifEmpty { activity.getString(R.string.drivers_not_checked) }, checking = releaseChecking, @@ -264,7 +264,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl return } if (DriverPairs.recommendedKey(gpu, emptyList()) == null) { - autoStatus = activity.getString(R.string.drivers_none_to_set, gpu.supportText(activity).lowercase()) + autoStatus = activity.getString(R.string.drivers_none_to_set, gpu.supportText(activity, com.droiddeck.launcher.core.DeviceSupport.vkBridge(activity)).lowercase()) return } releaseChecking = true diff --git a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt index 9d9e30667..03a1ad669 100644 --- a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt @@ -163,6 +163,10 @@ 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 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) private var syncFallback by mutableStateOf(true) @@ -1076,6 +1080,7 @@ class FrontEndActivity : ComponentActivity() { FrontEndScreen( FrontEndState( installed = installed, ready = ready, available = available?.version, removalPending = removalPending, runtimeActionsBlocked = runtimeChangesBlocked(), + vkBridge = vkBridge, vkBridgeSafe = vkBridgeSafe, bridgeProtonDxvk = bridgeProtonDxvk, adreno = adrenoGpu, shortcutPicker = shortcutPicker, shortcutLibraryScanning = shortcutLibraryScanning, gameSyncFolder = gameSyncFolder, @@ -1224,6 +1229,15 @@ class FrontEndActivity : ComponentActivity() { onInstallPackage = { id -> installPackage(id) }, onRemovePackage = { id -> removePackage(id) }, onRuntime = { onRuntimeButton() }, + 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) @@ -2534,6 +2548,9 @@ 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) + vkBridgeSafe = SessionPrefs.vkBridgeSafe(this) + bridgeProtonDxvk = SessionPrefs.bridgeProtonDxvk(this) noXalia = SessionPrefs.noXalia(this) fastSync = SessionPrefs.fastSync(this) syncFallback = SessionPrefs.syncFallback(this) @@ -2570,6 +2587,9 @@ class FrontEndActivity : ComponentActivity() { offline = OfflineMode.enabled(this) installed = LinuxRuntimeInstaller.installedVersion(this) ready = LinuxRuntime.isInstalled(this) + vkBridge = SessionPrefs.vkBridge(this) + vkBridgeSafe = SessionPrefs.vkBridgeSafe(this) + bridgeProtonDxvk = SessionPrefs.bridgeProtonDxvk(this) removalPending = LinuxRuntimeInstaller.hasRemovalPending(this) ui.post { autoUpdateRuntime() } frameGenLabel = FrameGen.label(this) @@ -2674,7 +2694,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) } @@ -2693,7 +2713,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/DeviceReport.kt b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt index 3a40300dc..309716d3f 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt @@ -208,7 +208,7 @@ object DeviceReport { h("Drivers") val gpu = com.droiddeck.launcher.gpu.GpuInfo.detect() - k("GPU", "${gpu.name} · ${gpu.family.label} · ${gpu.supportText}" + if (gpu.oneUi8Gen2) " · One UI 8 Gen 2" else "") + k("GPU", "${gpu.name} · ${gpu.family.label} · ${gpu.supportText(SessionPrefs.vkBridge(context))}" + if (gpu.oneUi8Gen2) " · One UI 8 Gen 2" else "") if (gpu.model == 740) { val hint = com.droiddeck.launcher.gpu.TpUbwcHint k("TP UBWC hint", hint.forThisDevice(context) ?: "driver default") 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..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,20 @@ 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) + + 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 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..59818f263 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/AppVulkan.kt @@ -0,0 +1,73 @@ +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"], 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?, driverApi: String? = null): AppVulkan? { + val map = keyValues(text) + val device = map["device"] + if (device != null && driverDevice != null && device != driverDevice) return null + 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? { + 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/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/DeviceInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt index 9a5fb1182..2d9aab902 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt @@ -16,7 +16,7 @@ object DeviceInfo { val vk = VulkanInfo.query() val gpu = GpuInfo.detect() return listOf( - chip(), gpu(gpu), cpu(), memory(context), system(), vulkan(context, vk), drivers(context, gpu), + chip(), gpu(context, gpu), cpu(), memory(context), system(), vulkan(context, vk), drivers(context, gpu), ).filter { it.rows.isNotEmpty() } } @@ -51,7 +51,7 @@ object DeviceInfo { add("Family", read("/sys/devices/soc0/family")) }) - private fun gpu(gpu: GpuInfo) = Section("GPU", buildList> { + private fun gpu(context: Context, gpu: GpuInfo) = Section("GPU", buildList> { add("GPU", gpu.name) add("Family", gpu.family.label) add("Model found from", when (gpu.modelSource) { @@ -66,7 +66,7 @@ object DeviceInfo { add("Clock range", gpuRange()) add("Load", read("$kgsl/gpu_busy_percentage")?.let { if (it.endsWith("%")) it else "$it %" }) add("Temperature", read("$kgsl/temp")?.toLongOrNull()?.let { if (it > 1000) "${it / 1000} °C" else "$it °C" }) - add("Support", gpu.supportText) + add("Support", gpu.supportText(SessionPrefs.vkBridge(context))) }) private fun cpu() = Section("CPU", buildList> { diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt index c137e666f..9b0a1897e 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt @@ -53,6 +53,18 @@ data class GpuInfo( Support.UNSUPPORTED -> "Not supported: DroidDeck needs an Adreno (Snapdragon) GPU" } + fun supportText(bridge: Boolean): String = when { + support != Support.UNSUPPORTED -> supportText + bridge -> "Runs through the Mali bridge, on the phone's own Vulkan driver" + else -> "Not supported with the Mali bridge off: Turnip needs an Adreno (Snapdragon) GPU" + } + + fun supportText(context: Context, bridge: Boolean): String = when { + support != Support.UNSUPPORTED -> supportText(context) + bridge -> context.getString(R.string.gpuinfo_bridge) + else -> context.getString(R.string.gpuinfo_bridge_off) + } + fun supportText(context: Context): String = context.getString(when (support) { Support.TESTED -> R.string.gpuinfo_supported Support.UNTESTED -> when { 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 9bae92e42..ba96c3eca 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java @@ -291,6 +291,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; + } GpuInfo gpu = gpuDetector.get(); if (gpu.getFamily() == GpuInfo.Family.NOT_ADRENO) { Log.i(TAG, "not an Adreno GPU; leaving the display driver to the system"); 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..8e1c4419e --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/VkBridgeComponent.kt @@ -0,0 +1,102 @@ +package com.droiddeck.launcher.gpu + +import android.content.Context +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 = 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 + } + 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 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 } + } + 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") + } + + 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" + 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) + val old = File(context.cacheDir, CACHE_DIR) + if (old.isDirectory) migrate(old, dir) + dir.mkdirs() + 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) { + 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" + } +} 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/hud/DroidDeckHudHost.kt b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudHost.kt index 46a56ff0a..f612477f3 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudHost.kt +++ b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudHost.kt @@ -14,6 +14,8 @@ import com.droiddeck.launcher.gpu.FrameGen import com.droiddeck.launcher.gpu.GpuInfo import com.droiddeck.launcher.runtime.LinuxRuntime import com.droiddeck.launcher.session.SessionPrefs +import com.droiddeck.launcher.session.SessionService +import com.droiddeck.launcher.session.SessionState import com.droiddeck.launcher.ui.Motion import com.droiddeck.launcher.wayland.WaylandCompositor import java.io.File @@ -56,7 +58,10 @@ class DroidDeckHudHost(context: Context) { if (!gpuName.isNullOrBlank()) handler.post { this@DroidDeckHudHost.gpuName = cleanGpuName(gpuName) } if (window != null && window != lastWindow) { lastWindow = window - handler.post { onPresentingWindowChanged?.invoke() } + handler.post { + onPresentingWindowChanged?.invoke() + applyShown() + } } } @@ -174,8 +179,9 @@ class DroidDeckHudHost(context: Context) { private fun applyShown() { if (!running) return val show = !hidden && !backgrounded - view?.visibility = if (show) View.VISIBLE else View.GONE - showing = show + val visible = show && SessionState.mode != SessionService.MODE_DESKTOP + view?.visibility = if (visible) View.VISIBLE else View.GONE + showing = visible if (show != polling) { polling = show if (show) Choreographer.getInstance().postFrameCallback(frameCallback) @@ -208,14 +214,20 @@ class DroidDeckHudHost(context: Context) { file }.getOrNull() + private fun sessionResolution(): String { + val (width, height) = SessionState.outputSize + return "${width}x${height}" + } + private fun refreshLabels(hud: DroidDeckHudView) { if (feed.engine.isNotBlank() && !feed.steamFocused) engine = feed.engine hud.setEngineLabel(engine) if (gpuName.isNotBlank()) hud.setGpuModel(gpuName) - hud.setResolution(if (feed.outputWidth > 0 && feed.outputHeight > 0) buildString { + val feedResolution = if (feed.outputWidth > 0 && feed.outputHeight > 0) buildString { append(feed.outputWidth).append('x').append(feed.outputHeight) if (feed.refreshMilliHz > 0) append(" @ ").append((feed.refreshMilliHz + 499) / 1000).append("Hz") - } else "") + } else "" + hud.setResolution(feedResolution.ifBlank { sessionResolution() }) val pid = feed.pid if (pid > 0 && pid != protonPid) { protonPid = pid diff --git a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudView.kt b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudView.kt index 7aa76e00d..e78249a1c 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudView.kt +++ b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudView.kt @@ -698,6 +698,9 @@ class DroidDeckHudView(context: Context) : View(context) { contentW = max(contentW, pad + w + pad) } + private fun hasGpuStats(s: HudMetrics.Snapshot): Boolean = + s.gpuPercent != null || s.gpuTempC != null || s.gpuClockMhz != null + private fun buildFull(s: HudMetrics.Snapshot) { val rowPx = sp(12f); val unitPx = rowPx * 0.62f val pad = sp(10f); val lineGap = sp(4f); val lvGap = sp(8f) @@ -709,7 +712,7 @@ class DroidDeckHudView(context: Context) : View(context) { rows.add(HudRow(Span("DISP", colDisp, rowPx), listOf(Span(displayServer, colValue, rowPx)))) if (hdrState != DroidDeckHudHdr.NONE) rows.add(HudRow(Span("HDR", colHdr, rowPx), hdrValue(rowPx))) - if (showGPU) { + if (showGPU && hasGpuStats(s)) { val v = ArrayList() v += numUnit(s.gpuPercent?.toString(), "%", rowPx, unitPx) if (showGpuTemp) tempSpans(s.gpuTempC, HudMetrics.TempSensor.GPU, rowPx, unitPx) @@ -720,9 +723,9 @@ class DroidDeckHudView(context: Context) : View(context) { if (showCPU) { val v = ArrayList() v += numUnit(s.cpuPercent?.toString(), "%", rowPx, unitPx) + v += gap(unitPx); v += numUnit(s.cpuClockMhz?.toString(), "MHz", rowPx, unitPx) if (showCpuTemp) tempSpans(s.cpuTempC, HudMetrics.TempSensor.CPU, rowPx, unitPx) .let { if (it.isNotEmpty()) { v += gap(unitPx); v += it } } - v += gap(unitPx); v += numUnit(s.cpuClockMhz?.toString(), "MHz", rowPx, unitPx) rows.add(HudRow(Span("CPU", colCpu, rowPx), v)) } if (showVram && s.vramText() != null) @@ -799,12 +802,13 @@ class DroidDeckHudView(context: Context) : View(context) { "${fmt1(fpsAvg)} avg · ${lowText(lows.low1Fps) ?: "—"} 1%", false)) if (showFPS) tiles.add(Tile("FRAME", colDim, numUnit(fmt1(1000f / max(fpsNow, 1f)), "ms", valPx, unitPx), "${fmt1(graphMinMs)} – ${fmt1(graphMaxMs)}", false)) - if (showGPU) tiles.add(Tile("GPU", colGpu, numUnit(s.gpuPercent?.toString(), "%", valPx, unitPx), + if (showGPU && hasGpuStats(s)) tiles.add(Tile("GPU", colGpu, numUnit(s.gpuPercent?.toString(), "%", valPx, unitPx), if (showGpuTemp && s.gpuTempC != null) HudMetrics.formatTemp(s.gpuTempC.toFloat(), tempDisplay, false) else s.gpuClockMhz?.let { "${it}MHz" }, false)) if (showCPU) tiles.add(Tile("CPU", colCpu, numUnit(s.cpuPercent?.toString(), "%", valPx, unitPx), - listOfNotNull(if (showCpuTemp) s.cpuTempC?.let { HudMetrics.formatTemp(it.toFloat(), tempDisplay, false) } else null, - s.cpuClockMhz?.toString()).joinToString(" · ").ifBlank { null }, false)) + listOfNotNull(s.cpuClockMhz?.let { "${it}MHz" }, + if (showCpuTemp) s.cpuTempC?.let { HudMetrics.formatTemp(it.toFloat(), tempDisplay, false) } else null) + .joinToString(" · ").ifBlank { null }, false)) if (showVram && s.vramText() != null) tiles.add(Tile("VRAM", colVram, valueUnit(s.vramText()!!, valPx, unitPx), null, false)) if (showRAM) tiles.add(Tile("RAM", colRam, numUnit(s.ramPercent.roundToInt().toString(), "%", valPx, unitPx), "${s.ramUsedText()} / ${s.ramTotalText()}", false)) @@ -881,7 +885,7 @@ class DroidDeckHudView(context: Context) : View(context) { val gpuName = if (showGpuModel && gpuNameText().isNotBlank()) gpuModelSpans(stkPx, colDim, compact = true) else null run { val l = ArrayList() - if (showGPU) { l += Span("GPU ${s.gpuPercent ?: "—"}%", colGpu, stkPx) } + if (showGPU && s.gpuPercent != null) { l += Span("GPU ${s.gpuPercent}%", colGpu, stkPx) } if (showCPU) { if (l.isNotEmpty()) l += Span(" · ", colDim, stkPx); l += Span("CPU ${s.cpuPercent ?: "—"}%", colCpu, stkPx) } if (l.isNotEmpty()) stack.add(l) } @@ -1137,7 +1141,7 @@ class DroidDeckHudView(context: Context) : View(context) { val left = ArrayList() if (showGpuModel && gpuNameText().isNotBlank()) left.add(HudRow(Span("GPU", colGpu, rowPx), gpuModelSpans(rowPx, colValue))) - if (showGPU) { + if (showGPU && hasGpuStats(s)) { val v = ArrayList() v += numUnit(s.gpuPercent?.toString(), "%", rowPx, unitPx) if (showGpuTemp) tempSpans(s.gpuTempC, HudMetrics.TempSensor.GPU, rowPx, unitPx) @@ -1148,9 +1152,9 @@ class DroidDeckHudView(context: Context) : View(context) { if (showCPU) { val v = ArrayList() v += numUnit(s.cpuPercent?.toString(), "%", rowPx, unitPx) + v += gap(unitPx); v += numUnit(s.cpuClockMhz?.toString(), "MHz", rowPx, unitPx) if (showCpuTemp) tempSpans(s.cpuTempC, HudMetrics.TempSensor.CPU, rowPx, unitPx) .let { if (it.isNotEmpty()) { v += gap(unitPx); v += it } } - v += gap(unitPx); v += numUnit(s.cpuClockMhz?.toString(), "MHz", rowPx, unitPx) left.add(HudRow(Span("CPU", colCpu, rowPx), v)) } if (showPerCore) { diff --git a/app/src/main/java/com/droiddeck/launcher/hud/MangoReadings.kt b/app/src/main/java/com/droiddeck/launcher/hud/MangoReadings.kt index ade7aa656..d9fbaa4df 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/MangoReadings.kt +++ b/app/src/main/java/com/droiddeck/launcher/hud/MangoReadings.kt @@ -1,7 +1,9 @@ package com.droiddeck.launcher.hud +import android.os.Environment import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File +import kotlin.math.roundToInt internal class MangoReadings(private val sessionDir: File, private val rootDir: File) { class Cpu(val percent: Int, val perCore: IntArray) @@ -48,6 +50,7 @@ internal class MangoReadings(private val sessionDir: File, private val rootDir: } fun cpuTempC(): Int? { + externalCpuTemp()?.let { return it } var sum = 0f var count = 0 for (zone in LinuxRuntime.cpuTempZones()) { @@ -58,6 +61,13 @@ internal class MangoReadings(private val sessionDir: File, private val rootDir: return if (count == 0) null else (sum / count).toInt() } + /** Optional ADB helper feed: /sdcard/DroidDeck/cpu-temp, written from the host while a session runs. */ + private fun externalCpuTemp(): Int? { + val file = File(Environment.getExternalStorageDirectory(), "DroidDeck/cpu-temp") + if (!file.isFile || System.currentTimeMillis() - file.lastModified() > CPU_TEMP_FRESH_MS) return null + return readFloat(file)?.takeIf { it.isFinite() && it in -20f..150f }?.roundToInt() + } + fun gpuPercent(): Int? { val feed = fresh(File(sessionDir, "sys/kgsl-3d0/perf_now")) val source = feed ?: LinuxRuntime.gpuLoadSource()?.let(::File) ?: return null @@ -92,7 +102,8 @@ internal class MangoReadings(private val sessionDir: File, private val rootDir: return Memory((total - available) * 1024, total * 1024, (swapTotal - swapFree) * 1024, swapTotal * 1024) } - fun vramBytes(): Long? = fresh(File(rootDir, VRAM_FILE), VRAM_FRESH_MS)?.let(::readLong) + /** The KGSL total DroidDeck shares with the session (GpuMemComponent). */ + fun vramBytes(): Long? = fresh(File(rootDir, VRAM_FILE), VRAM_FRESH_MS)?.let(::readLong)?.takeIf { it > 0 } fun battery(): Battery? { val dirs = File(sessionDir, "sys/power_supply").listFiles { f -> f.name.contains("BAT") }?.sortedBy { it.name } @@ -132,6 +143,7 @@ internal class MangoReadings(private val sessionDir: File, private val rootDir: const val VRAM_FILE = "run/droiddeck-hud/gpu-mem" private const val FEED_FRESH_MS = 10_000L private const val VRAM_FRESH_MS = 15_000L + private const val CPU_TEMP_FRESH_MS = 10_000L private val WHITESPACE = Regex("\\s+") private fun readLong(file: File): Long? = runCatching { file.readText().trim().toLong() }.getOrNull() 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 e8631c1ea..2d1a9aa40 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt @@ -200,6 +200,7 @@ object BwrapSpawner { LIB_DIRS.map { File(rootfs, "$it/$name") }.firstOrNull { it.exists() } 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 driver = runtimeLib(rootfs, "libvulkan_freedreno.so") if (driver == null || !File("/dev/kgsl-3d0").exists()) return null if (env.containsKey("VK_ICD_FILENAMES") || env.containsKey("VK_DRIVER_FILES")) return null @@ -222,6 +223,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/LinuxNetworkLinkComponent.kt b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxNetworkLinkComponent.kt index 45f07471b..06a034825 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxNetworkLinkComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxNetworkLinkComponent.kt @@ -251,12 +251,36 @@ class LinuxNetworkLinkComponent( } } + private fun localDnsServers(): Pair, List> { + val active = connectivity.activeNetwork?.let { connectivity.getNetworkCapabilities(it) } + if (active?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true) return Pair(emptyList(), emptyList()) + val servers = LinkedHashSet() + val gateways = LinkedHashSet() + for (network in connectivity.allNetworks) { + val capabilities = connectivity.getNetworkCapabilities(network) ?: continue + if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) continue + if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) && + !capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) continue + val link = connectivity.getLinkProperties(network) ?: continue + for (address in link.dnsServers) { + if (address.isLinkLocalAddress || address.isLoopbackAddress || address.isAnyLocalAddress) continue + address.hostAddress?.substringBefore('%')?.let { servers.add(it) } + } + link.routes.firstOrNull { + it.isDefaultRoute && (it.gateway as? Inet4Address)?.let { g -> !g.isLoopbackAddress && !g.isAnyLocalAddress } == true + }?.gateway?.hostAddress?.substringBefore('%')?.let { gateways.add(it) } + } + val order = compareBy({ if (it.contains(':')) 1 else 0 }) + return Pair(servers.sortedWith(order), gateways.sortedWith(order)) + } + private fun writeResolver(properties: LinkProperties?) { val own = properties?.dnsServers.orEmpty() .filter { !it.isLinkLocalAddress && !it.isLoopbackAddress && !it.isAnyLocalAddress } .sortedBy { if (it is Inet4Address) 0 else 1 } .mapNotNull { it.hostAddress?.substringBefore('%') } - val servers = (own + listOf("8.8.8.8", "1.1.1.1", "2001:4860:4860::8888")).distinct().take(6) + val (discovered, gateways) = localDnsServers() + val servers = (own + discovered + listOf("8.8.8.8", "1.1.1.1", "2001:4860:4860::8888") + gateways).distinct().take(6) val text = buildString { append("# Written by the app from the device's active network; edits are overwritten.\n") for (server in servers) append("nameserver $server\n") 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 ab5a58737..01cbf9322 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java @@ -278,10 +278,42 @@ public final class LinuxRuntime { return false; } + static final String MALI_DEVICE = "/dev/mali0"; + + static final String[] DRM_RENDER_NODES = { + "/dev/dri/renderD128", "/dev/dri/renderD129", "/dev/dri/renderD130", "/dev/dri/renderD131", + }; + + public static String gpuStandIn(Context context) { + if (new File(KGSL_DEVICE).exists()) return KGSL_DEVICE; + if (!com.droiddeck.launcher.core.DeviceSupport.INSTANCE.vkBridge(context)) return null; + return bridgeStandIn(path -> new File(path).exists()); + } + + static String bridgeStandIn(java.util.function.Predicate exists) { + if (exists.test(MALI_DEVICE)) return MALI_DEVICE; + for (String node : DRM_RENDER_NODES) if (exists.test(node)) return node; + 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; } @@ -301,13 +333,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) { @@ -315,7 +348,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/ComponentsManager.kt b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt index b1d70902a..8255209d1 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 @@ -638,29 +639,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}") } @@ -708,7 +711,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) } @@ -1011,15 +1014,76 @@ 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 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) {} }, { 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) }, + 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) + val state = loadState(context) + val entry = state.sub("active").optJSONObject(p.id)?.optJSONObject(comp) + 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" + } internal fun ensureForcedPackages( context: Context, @@ -1027,16 +1091,35 @@ object ComponentsManager { 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 = { _, _ -> "" }, + keepProtonDxvk: Boolean = false, + autoPossible: Boolean = true, ): String? { - val rules = FORCED_PACKAGES.filter { it.matches(gpu) } - if (rules.isEmpty()) return 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() && !autoPossible) return null val installed = protons(context) val lines = mutableListOf() - for (rule in rules) { + 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 (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 (autoChoiceStale(entry, null)) lines.add(each(p) { 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) @@ -1044,9 +1127,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("; ") { p -> if (auto) each(p) { applyAuto(p.id, rule.file) } else apply(p.id, rule.file) }) } - return lines.joinToString("; ").ifEmpty { null } + return lines.filter { it.isNotEmpty() }.joinToString("; ").ifEmpty { null } } private fun missingPackage( 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 09122644d..a947bf7e1 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt @@ -171,6 +171,9 @@ object SessionFiles { val optional = (arrayOf( "usr/local/bin/gamescope" to "usr/local/bin/gamescope", "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 bbe6ee2a9..fd54f5c39 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -746,6 +746,29 @@ object SessionPrefs { fun zinkLazy(context: Context): Boolean = prefs(context).getBoolean("zinkLazy", true) + @JvmStatic + 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() + } + + @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) { + 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 1997a5536..e9d322b59 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -523,6 +523,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) @@ -812,7 +813,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) @@ -867,6 +868,27 @@ 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) + 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)}, ${SessionPrefs.vkBridgeMode(this)} mode), 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 eff9453d6..9bf266b83 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -149,6 +149,10 @@ 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 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, @@ -205,6 +209,9 @@ class FrontEndActions( val onInstallPackage: (String) -> Unit, val onRemovePackage: (String) -> Unit, 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 16d18840d..f7969fe6c 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -171,7 +171,7 @@ internal fun SetupPanel( val colors = MaterialTheme.colorScheme val pal = LocalPalette.current val gpu = remember { com.droiddeck.launcher.gpu.GpuInfo.detect() } - val gpuOk = gpu.support == com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED + val gpuOk = gpu.support == com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED || s.vkBridge val gpuName = remember { DeviceSupport.gpuName(ctx) } val limitBlocks = PhantomProcessLimit.blocksSteam(s.phantomProcessStatus) val signedIn = s.offlineAccount != null @@ -232,7 +232,7 @@ internal fun SetupPanel( val runtimeOk = s.ready && !s.busy && !s.removalPending val checks = listOf(gpuOk, runtimeOk, !limitBlocks, signedIn) val passing = checks.count { it } - var showChecks by rememberSaveable { mutableStateOf(false) } + var showChecks by rememberSaveable { mutableStateOf(s.vkBridge) } Rise(0, Modifier.fillMaxSize()) { Column( modifier = Modifier.fillMaxSize().bumpers( @@ -249,17 +249,49 @@ internal fun SetupPanel( val gpuCheck: @Composable (Boolean) -> Unit = { divider -> CheckRow( if (gpuOk) CheckState.OK else CheckState.WARN, - when (gpu.support) { - com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED -> stringResource(R.string.setup_gpu_supported) - com.droiddeck.launcher.gpu.GpuInfo.Support.UNTESTED -> stringResource(R.string.setup_gpu_untested) + when { + gpu.support == com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED -> stringResource(R.string.setup_gpu_supported) + gpu.support == com.droiddeck.launcher.gpu.GpuInfo.Support.UNTESTED -> stringResource(R.string.setup_gpu_untested) + s.vkBridge -> stringResource(R.string.setup_gpu_bridged) else -> stringResource(R.string.setup_gpu_unsupported) }, 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 || (!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), + enabled = !s.sessionRunning, compact = true, onClick = { a.onVkBridge(!s.vkBridge) }, + ) + } }, + ) + 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) }, + ) + }, + ) + 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 -> diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 8ef0684fd..96eb4259a 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -3671,6 +3671,19 @@ زر الشاشة الرئيسية في DroidDeck يمرّر ضغطات زر الشاشة الرئيسية إلى Steam Guide عندما تكون جلسة Steam في المقدمة ويكون DroidDeck تطبيق الشاشة الرئيسية الافتراضي. لا يقرأ محتوى الشاشة. فعّل «السطوع» من الإعداد → تكامل Steam + جسر Mali: متوقف + جسر Mali: يعمل + %1$s ليس Adreno. جسر Vulkan التجريبي يعمل: تعرض الجلسة الرسومات عبر برنامج تشغيل الهاتف نفسه. توقّع وجود أخطاء؛ أرسل سجلات الجلسة. + الوضع الآمن + الوضع الآمن: يعمل + الوضع الآمن: متوقف + الأكثر توافقًا. يوصى به لجهاز جديد. + أسرع حيث اجتاز معالج الرسومات هذا فحوصاته. + DXVK + DXVK: تلقائي + DXVK: الخاص بـ Proton + يستخدم إصدار DXVK أقدم عندما يكون Vulkan في معالج الرسومات هذا أقدم من أن يدعم DXVK الخاص بـ Proton. + يحتفظ كل Proton بـ DXVK الخاص به، حتى عندما قد يكون Vulkan في معالج الرسومات هذا أقدم من اللازم. البحث في التطبيقات لا توجد تطبيقات تطابق ”%1$s“. إضافة أداة 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 9cd7421dc..0b0942c45 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -3536,6 +3536,19 @@ Botón de inicio de DroidDeck Pasa las pulsaciones del botón de inicio a Steam Guide mientras una sesión de Steam está en primer plano y DroidDeck es la app de inicio predeterminada. No lee el contenido de la pantalla. Activa Brillo en Configuración → Integración con Steam + 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. Buscar apps Ninguna app coincide con «%1$s». Agregar widget diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index a8e1c9f74..9bfdc02cd 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -3527,6 +3527,19 @@ Бутон за начален екран на DroidDeck Предава натисканията на бутона за начален екран към Steam Guide, докато сесия на Steam е на фокус и DroidDeck е приложението за начален екран по подразбиране. Не чете съдържанието на екрана. Включете „Яркост“ в Настройка → Интеграция със Steam + Mali мост: изкл. + Mali мост: вкл. + %1$s не е Adreno. Експерименталният Vulkan мост е включен: сесията рисува през собствения драйвер на телефона. Очаквайте грешки; изпратете логовете на сесията. + Безопасен режим + Безопасен режим: вкл. + Безопасен режим: изкл. + Най-съвместим. Препоръчва се за ново устройство. + По-бързо там, където този GPU е минал проверките си. + DXVK + DXVK: автоматично + DXVK: на Proton + Използва по-стар DXVK, когато Vulkan на този GPU е твърде стар за този на Proton. + Всеки Proton запазва своя DXVK, дори когато Vulkan на този GPU може да е твърде стар за него. Търсене на приложения Няма приложения, отговарящи на „%1$s“. Добавяне на приспособление diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index b8b3382b2..78d2002fd 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -3599,6 +3599,19 @@ Tlačítko Domů pro DroidDeck Předává stisky tlačítka Domů do Steam Guide, když je aktivní relace Steamu a DroidDeck je výchozí aplikací domovské obrazovky. Nečte obsah obrazovky. Zapněte Jas v Příprava → Integrace se Steamem + 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ý. Hledat aplikace Výrazu „%1$s“ neodpovídá žádná aplikace. Přidat widget diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 2d4082298..818d624a2 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -3527,6 +3527,19 @@ DroidDeck Hjem-knap Sender tryk på Hjem-knappen til Steam Guide, mens en Steam-session er i fokus, og DroidDeck er standardstartskærmen. Læser ikke skærmindhold. Slå Lysstyrke til under Opsætning → Steam-integration + 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. Søg i apps Ingen apps matcher “%1$s”. Tilføj widget diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8157b7e1e..d4d065606 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -3527,6 +3527,19 @@ DroidDeck-Home-Taste Leitet Drücke der Home-Taste an Steam Guide weiter, während eine Steam-Sitzung im Fokus ist und DroidDeck die Standard-Startbildschirm-App ist. Liest keine Bildschirminhalte. Aktiviere Helligkeit unter Einrichtung → Steam-Integration + 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. Apps durchsuchen Keine Apps passen zu „%1$s“. Widget hinzufügen diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index a627bc416..f14066a2b 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -3527,6 +3527,19 @@ Κουμπί Αρχικής DroidDeck Μεταβιβάζει τα πατήματα του κουμπιού Αρχικής στο Steam Guide όσο μια συνεδρία Steam είναι εστιασμένη και το DroidDeck είναι η προεπιλεγμένη εφαρμογή Αρχικής. Δεν διαβάζει το περιεχόμενο της οθόνης. Ενεργοποιήστε τη Φωτεινότητα στη Ρύθμιση → Ενσωμάτωση Steam + Γέφυρα Mali: ανενεργή + Γέφυρα Mali: ενεργή + Η %1$s δεν είναι Adreno. Η πειραματική γέφυρα Vulkan είναι ενεργή: η συνεδρία σχεδιάζει μέσω του προγράμματος οδήγησης του ίδιου του τηλεφώνου. Αναμένετε σφάλματα· στείλτε τα αρχεία καταγραφής της συνεδρίας. + Ασφαλής λειτουργία + Ασφαλής λειτουργία: ενεργή + Ασφαλής λειτουργία: ανενεργή + Μέγιστη συμβατότητα. Συνιστάται για νέα συσκευή. + Ταχύτερη όπου αυτή η GPU πέρασε τους ελέγχους της. + DXVK + DXVK: αυτόματο + DXVK: του Proton + Χρησιμοποιεί παλαιότερο DXVK όταν το Vulkan αυτής της GPU είναι πολύ παλιό για αυτό του Proton. + Κάθε Proton κρατά το δικό του DXVK, ακόμη κι αν το Vulkan αυτής της GPU ίσως είναι πολύ παλιό για αυτό. Αναζήτηση εφαρμογών Καμία εφαρμογή δεν ταιριάζει με «%1$s». Προσθήκη γραφικού στοιχείου diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index a3247ba1c..b40c7023c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -3563,6 +3563,19 @@ Botón de inicio de DroidDeck Pasa las pulsaciones del botón de inicio a Steam Guide mientras una sesión de Steam está en primer plano y DroidDeck es la app de inicio predeterminada. No lee el contenido de la pantalla. Activa Brillo en Configuración → Integración con Steam + 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. Buscar aplicaciones Ninguna aplicación coincide con «%1$s». Añadir widget diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index a733c907c..e68570467 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -3527,6 +3527,19 @@ DroidDeckin Koti-painike Välittää Koti-painikkeen painallukset Steam Guidelle, kun Steam-istunto on aktiivinen ja DroidDeck on oletusaloitusnäyttösovellus. Ei lue näytön sisältöä. Ota Kirkkaus käyttöön kohdassa Käyttöönotto → Steam-integraatio + 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. Hae sovelluksia Mikään sovellus ei vastaa hakua ”%1$s”. Lisää widget diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a59fa6f5c..c88741960 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -3563,6 +3563,19 @@ Bouton Accueil DroidDeck Transmet les appuis sur le bouton Accueil à Steam Guide lorsqu\'une session Steam est au premier plan et que DroidDeck est l\'application d\'écran d\'accueil par défaut. Ne lit pas le contenu de l\'écran. Activez Luminosité dans Paramètres → Intégration Steam + 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. Rechercher des applications Aucune application ne correspond à « %1$s ». Ajouter un widget diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index c6325ed99..795c2c35d 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -3527,6 +3527,19 @@ DroidDeck होम बटन जब Steam सेशन फ़ोकस में हो और DroidDeck डिफ़ॉल्ट होम ऐप हो, तब होम बटन दबाने को Steam Guide तक भेजता है। स्क्रीन की सामग्री नहीं पढ़ता। Setup → Steam इंटीग्रेशन में ब्राइटनेस चालू करें + Mali ब्रिज: बंद + Mali ब्रिज: चालू + %1$s Adreno नहीं है। प्रायोगिक Vulkan ब्रिज चालू है: सत्र फ़ोन के अपने ड्राइवर से ड्रॉ करता है। बग की उम्मीद रखें; सत्र लॉग भेजें। + सुरक्षित मोड + सुरक्षित मोड: चालू + सुरक्षित मोड: बंद + सबसे अधिक संगत। नए डिवाइस के लिए अनुशंसित। + जहाँ इस GPU ने अपनी जाँचें पास कीं, वहाँ तेज़। + DXVK + DXVK: स्वचालित + DXVK: Proton का अपना + जहाँ इस GPU का Vulkan Proton के DXVK के लिए बहुत पुराना है, वहाँ पुराना DXVK इस्तेमाल करता है। + हर Proton अपना DXVK रखता है, भले ही इस GPU का Vulkan उसके लिए बहुत पुराना हो। ऐप खोजें “%1$s” से कोई ऐप मेल नहीं खाता। विजेट जोड़ें diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index f35b74f26..8a0a742f5 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -3527,6 +3527,19 @@ DroidDeck Kezdőképernyő gomb A Kezdőképernyő gomb lenyomásait továbbítja a Steam Guide-nak, amíg egy Steam-munkamenet van fókuszban, és a DroidDeck az alapértelmezett kezdőképernyő alkalmazás. Nem olvassa a képernyő tartalmát. Kapcsolja be a Fényerőt itt: Beállítás → Steam-integráció + 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á. Alkalmazások keresése Egy alkalmazás sem felel meg ennek: „%1$s”. Modul hozzáadása diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 90d529218..31064ee62 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -3500,6 +3500,19 @@ Tombol Home DroidDeck Meneruskan tekanan tombol Home ke Steam Guide saat sesi Steam sedang aktif dan DroidDeck adalah aplikasi Beranda default. Tidak membaca konten layar. Aktifkan Kecerahan di Penyiapan → Integrasi Steam + 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. Cari aplikasi Tidak ada aplikasi yang cocok dengan “%1$s”. Tambahkan widget diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 2a0664db6..d65453a09 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -3563,6 +3563,19 @@ Tasto Home di DroidDeck Inoltra le pressioni del tasto Home a Steam Guide mentre una sessione Steam è in primo piano e DroidDeck è l\'app Home predefinita. Non legge il contenuto dello schermo. Attiva Luminosità in Configurazione → Integrazione con Steam + 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. Cerca app Nessuna app corrisponde a «%1$s». Aggiungi widget diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index ed27707c1..b6de8491d 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -3500,6 +3500,19 @@ DroidDeckホームボタン Steamセッションがフォーカスされ、DroidDeckがデフォルトのホームアプリの場合に、ホームボタンの操作をSteamガイドに渡します。画面のコンテンツは読み取りません。 「セットアップ」→「Steam連携」で明るさをオンにしてください + Mali ブリッジ: オフ + Mali ブリッジ: オン + %1$s は Adreno ではありません。試験的な Vulkan ブリッジがオンです。セッションはスマートフォン本体のドライバーで描画します。不具合が出る可能性があります。セッションログを送ってください。 + セーフモード + セーフモード: オン + セーフモード: オフ + 最も互換性が高い設定です。新しいデバイスにおすすめです。 + この GPU がチェックに合格した部分で高速になります。 + DXVK + DXVK: 自動 + DXVK: Proton 付属 + この GPU の Vulkan が Proton 付属の DXVK には古すぎる場合、古い DXVK を使います。 + この GPU の Vulkan が古すぎる場合でも、各 Proton は付属の DXVK を使い続けます。 アプリを検索 「%1$s」に一致するアプリはありません。 ウィジェットを追加 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 821641cab..5179b8a36 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -3500,6 +3500,19 @@ DroidDeck 홈 버튼 Steam 세션에 포커스가 있고 DroidDeck이 기본 홈 앱일 때 홈 버튼 입력을 Steam Guide로 전달합니다. 화면 콘텐츠는 읽지 않습니다. 설정 → Steam 연동에서 밝기를 켜세요 + Mali 브리지: 끔 + Mali 브리지: 켬 + %1$s은(는) Adreno가 아닙니다. 실험적 Vulkan 브리지가 켜져 있어 세션이 휴대폰 자체 드라이버로 그립니다. 버그가 있을 수 있으니 세션 로그를 보내 주세요. + 안전 모드 + 안전 모드: 켬 + 안전 모드: 끔 + 호환성이 가장 높습니다. 새 기기에 권장합니다. + 이 GPU가 검사를 통과한 부분에서 더 빠릅니다. + DXVK + DXVK: 자동 + DXVK: Proton 기본 + 이 GPU의 Vulkan이 Proton 기본 DXVK에 너무 오래된 경우 이전 DXVK를 사용합니다. + 이 GPU의 Vulkan이 너무 오래되었을 수 있어도 각 Proton은 자체 DXVK를 유지합니다. 앱 검색 ‘%1$s’와(과) 일치하는 앱이 없습니다. 위젯 추가 diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 1d2b2ff46..8da084500 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -3500,6 +3500,19 @@ Butang Laman Utama DroidDeck Menghantar tekanan butang Laman Utama kepada Steam Guide semasa sesi Steam difokuskan dan DroidDeck ialah apl Laman Utama lalai. Tidak membaca kandungan skrin. Hidupkan Kecerahan dalam Persediaan → Integrasi Steam + 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. Cari apl Tiada apl sepadan dengan “%1$s”. Tambah widget diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index c0ecc73a2..62207f520 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -3527,6 +3527,19 @@ DroidDeck Hjem-knapp Sender trykk på Hjem-knappen til Steam Guide mens en Steam-sesjon er i fokus og DroidDeck er standard startskjerm-app. Leser ikke skjerminnhold. Slå på Lysstyrke i Oppsett → Steam-integrering + 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. Søk i apper Ingen apper samsvarer med «%1$s». Legg til modul diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 576631973..7fc8cd922 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -3527,6 +3527,19 @@ DroidDeck-startknop Geeft drukken op de startknop door aan Steam Guide terwijl een Steam-sessie actief is en DroidDeck de standaard startscherm-app is. Leest geen scherminhoud. Zet Helderheid aan in Installatie → Steam-integratie + 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. Apps zoeken Geen apps gevonden voor ‘%1$s’. Widget toevoegen diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 93b8a830e..56f9e0d1f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -3599,6 +3599,19 @@ Przycisk ekranu głównego DroidDeck Przekazuje naciśnięcia przycisku ekranu głównego do Steam Guide, gdy aktywna jest sesja Steam, a DroidDeck jest domyślną aplikacją ekranu głównego. Nie odczytuje zawartości ekranu. Włącz Jasność w Konfiguracja → Integracja ze Steam + 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. Szukaj aplikacji Żadna aplikacja nie pasuje do „%1$s”. Dodaj widżet diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 5640c1c57..ff09a5aad 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -3563,6 +3563,19 @@ Botão Página inicial do DroidDeck Envia os toques no botão Página inicial para o Guide do Steam enquanto uma sessão do Steam está em foco e o DroidDeck é a app de página inicial predefinida. Não lê o conteúdo do ecrã. Ative Brilho em Configuração → Integração com o Steam + 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. Pesquisar aplicações Nenhuma aplicação corresponde a “%1$s”. Adicionar widget diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index fa704f1e3..a6f9e5cc9 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -3563,6 +3563,19 @@ Botão Início do DroidDeck Repassa os toques no botão Início para o Guide da Steam enquanto uma sessão da Steam está em foco e o DroidDeck é o app de tela inicial padrão. Não lê o conteúdo da tela. Ative Brilho em Configuração → Integração com a Steam + 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. Pesquisar apps Nenhum app corresponde a “%1$s”. Adicionar widget diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index fafe91aab..2eecc56e3 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -3563,6 +3563,19 @@ Butonul Acasă DroidDeck Transmite apăsările butonului Acasă către Steam Guide cât timp o sesiune Steam este activă și DroidDeck este aplicația Acasă implicită. Nu citește conținutul ecranului. Activează Luminozitate în Configurare → Integrare Steam + 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. Caută aplicații Nicio aplicație nu se potrivește cu „%1$s”. Adaugă widget diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d31b5b54a..2187225f6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -3599,6 +3599,19 @@ Кнопка «Домой» DroidDeck Передаёт нажатия кнопки «Домой» в Steam Guide, пока активна сессия Steam и DroidDeck является главным экраном по умолчанию. Не считывает содержимое экрана. Включите «Яркость» в «Настройка → Интеграция со Steam» + Мост Mali: выкл. + Мост Mali: вкл. + %1$s — не Adreno. Включён экспериментальный мост Vulkan: сеанс рисует через собственный драйвер телефона. Возможны ошибки; отправьте журналы сеанса. + Безопасный режим + Безопасный режим: вкл. + Безопасный режим: выкл. + Максимальная совместимость. Рекомендуется для нового устройства. + Быстрее там, где этот GPU прошёл проверки. + DXVK + DXVK: автоматически + DXVK: из Proton + Использует более старый DXVK, если Vulkan этого GPU слишком стар для DXVK из Proton. + Каждый Proton оставляет свой DXVK, даже если Vulkan этого GPU может быть для него слишком старым. Поиск приложений Нет приложений по запросу «%1$s». Добавить виджет diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 645c25857..6ce5d82b8 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -3527,6 +3527,19 @@ DroidDeck hemknapp Skickar tryck på hemknappen till Steam Guide medan en Steam-session är i fokus och DroidDeck är standardappen för startskärmen. Läser inte skärminnehåll. Slå på Ljusstyrka under Konfiguration → Steam-integration + 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. Sök appar Inga appar matchar ”%1$s”. Lägg till widget diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 41318ab7c..0c44a3bc2 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -3500,6 +3500,19 @@ ปุ่มหน้าหลัก DroidDeck ส่งการกดปุ่มหน้าหลักไปยัง Steam Guide ขณะเซสชัน Steam อยู่ในโฟกัสและ DroidDeck เป็นแอปหน้าหลักเริ่มต้น ไม่อ่านเนื้อหาบนหน้าจอ เปิดความสว่างในการตั้งค่า → การผสานรวม Steam + บริดจ์ Mali: ปิด + บริดจ์ Mali: เปิด + %1$s ไม่ใช่ Adreno บริดจ์ Vulkan รุ่นทดลองเปิดอยู่: เซสชันวาดผ่านไดรเวอร์ของโทรศัพท์เอง อาจพบบั๊ก โปรดส่งบันทึกของเซสชัน + โหมดปลอดภัย + โหมดปลอดภัย: เปิด + โหมดปลอดภัย: ปิด + เข้ากันได้มากที่สุด แนะนำสำหรับอุปกรณ์ใหม่ + เร็วขึ้นในส่วนที่ GPU นี้ผ่านการตรวจสอบ + DXVK + DXVK: อัตโนมัติ + DXVK: ของ Proton + ใช้ DXVK รุ่นเก่ากว่าเมื่อ Vulkan ของ GPU นี้เก่าเกินไปสำหรับ DXVK ของ Proton + Proton แต่ละตัวใช้ DXVK ของตัวเอง แม้ Vulkan ของ GPU นี้อาจเก่าเกินไปสำหรับมัน ค้นหาแอป ไม่มีแอปที่ตรงกับ “%1$s” เพิ่มวิดเจ็ต diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e8347fab3..16ca82a75 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -3527,6 +3527,19 @@ DroidDeck Ana ekran düğmesi Bir Steam oturumu odaktayken ve DroidDeck varsayılan Ana ekran uygulamasıyken Ana ekran düğmesine basışları Steam Guide\'a iletir. Ekran içeriğini okumaz. Kurulum → Steam entegrasyonu bölümünde Parlaklık\'ı açın + 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. Uygulama ara “%1$s” ile eşleşen uygulama yok. Widget ekle diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d4aa241f2..bd1780729 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -3599,6 +3599,19 @@ Кнопка «Додому» DroidDeck Передає натискання кнопки «Додому» в Steam Guide, поки активний сеанс Steam і DroidDeck є типовим головним екраном. Не зчитує вміст екрана. Увімкніть «Яскравість» у «Налаштування → Інтеграція зі Steam» + Міст Mali: вимк. + Міст Mali: увімк. + %1$s — не Adreno. Увімкнено експериментальний міст Vulkan: сеанс малює через власний драйвер телефона. Можливі помилки; надішліть журнали сеансу. + Безпечний режим + Безпечний режим: увімк. + Безпечний режим: вимк. + Найкраща сумісність. Рекомендовано для нового пристрою. + Швидше там, де цей GPU пройшов перевірки. + DXVK + DXVK: автоматично + DXVK: з Proton + Використовує старіший DXVK, якщо Vulkan цього GPU застарий для DXVK з Proton. + Кожен Proton залишає свій DXVK, навіть якщо Vulkan цього GPU може бути для нього застарим. Пошук застосунків Немає застосунків за запитом «%1$s». Додати віджет diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index a93d71508..2c11cbbfd 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -3500,6 +3500,19 @@ Nút Home DroidDeck Chuyển các lần nhấn nút Home đến Steam Guide khi phiên Steam đang được chọn và DroidDeck là ứng dụng màn hình chính mặc định. Không đọc nội dung màn hình. Bật Độ sáng trong Thiết lập → Tích hợp Steam + 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ó. Tìm ứng dụng Không có ứng dụng nào khớp với “%1$s”. Thêm tiện ích diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 880890644..15fb5b585 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -802,6 +802,7 @@ 设备受支持 未经测试的 GPU GPU 不受支持 + GPU 通过 Mali 桥接 %1$s · %2$s %1$s:%2$s Steam 需要 Adreno 驱动;%1$s 可能显示黑屏 @@ -2145,6 +2146,8 @@ Adreno 850 实验支持:需要配套的实验版显示驱动和游戏驱动;尚未在此设备上验证 低于已测试的硬件(Adreno 725 及更新):可能无法运行 不受支持:DroidDeck 需要 Adreno(骁龙)GPU + 通过 Mali 桥接运行,使用手机自带的 Vulkan 驱动 + 关闭 Mali 桥接时不受支持:Turnip 需要 Adreno(骁龙)GPU 已达到 GitHub 的请求频率限制 — 请稍后重试 GitHub 返回 HTTP %1$d @@ -3500,6 +3503,19 @@ DroidDeck 主屏幕按钮 当 Steam 会话处于焦点且 DroidDeck 为默认主屏幕应用时,将主屏幕按钮的按下操作传递给 Steam Guide 按钮。不会读取屏幕内容。 请在“配置”→“Steam 集成”中开启亮度 + DXVK + DXVK:自动 + DXVK:Proton 自带 + 此 GPU 的 Vulkan 版本对 Proton 自带的 DXVK 来说太旧时,改用较旧的 DXVK。 + 每个 Proton 都使用自带的 DXVK,即使此 GPU 的 Vulkan 版本可能太旧。 + Mali 桥接:关 + Mali 桥接:开 + %1$s 不是 Adreno。实验性 Vulkan 桥接已开启:会话通过手机自带的驱动绘制。可能会有问题,请发送会话日志。 + 安全模式 + 安全模式:开 + 安全模式:关 + 兼容性最好。推荐新设备使用。 + 在此 GPU 通过检查的地方更快。 搜索应用 没有与“%1$s”匹配的应用。 添加微件 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 095cb890c..d3af069ca 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -3500,6 +3500,19 @@ DroidDeck 主畫面按鈕 當 Steam 會話處於焦點且 DroidDeck 為預設主畫面應用程式時,將主畫面按鈕的按壓傳送給 Steam Guide。不會讀取螢幕內容。 請在「初始設定 → Steam 整合」中開啟「亮度」 + DXVK + DXVK:自動 + DXVK:Proton 內建 + 此 GPU 的 Vulkan 版本對 Proton 內建的 DXVK 來說太舊時,改用較舊的 DXVK。 + 每個 Proton 都使用內建的 DXVK,即使此 GPU 的 Vulkan 版本可能太舊。 + Mali 橋接:關 + Mali 橋接:開 + %1$s 不是 Adreno。實驗性 Vulkan 橋接已開啟:工作階段透過手機內建的驅動程式繪製。可能會有問題,請傳送工作階段記錄。 + 安全模式 + 安全模式:開 + 安全模式:關 + 相容性最好。建議新裝置使用。 + 在此 GPU 通過檢查的地方較快。 搜尋應用程式 沒有符合「%1$s」的應用程式。 新增小工具 @@ -3607,4 +3620,7 @@ 狀態 合作夥伴 已追蹤的遊戲 + GPU 透過 Mali 橋接 + 透過 Mali 橋接執行,使用手機內建的 Vulkan 驅動程式 + 關閉 Mali 橋接時不受支援:Turnip 需要 Adreno(Snapdragon)GPU diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 56005e1ff..d9ffdfae7 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -3500,6 +3500,19 @@ DroidDeck 主畫面按鈕 當 Steam 工作階段處於焦點且 DroidDeck 為預設主畫面應用程式時,將主畫面按鈕的按壓傳送給 Steam Guide。不會讀取螢幕內容。 請在「初始設定 → Steam 整合」中開啟「亮度」 + DXVK + DXVK:自動 + DXVK:Proton 內建 + 此 GPU 的 Vulkan 版本對 Proton 內建的 DXVK 來說太舊時,改用較舊的 DXVK。 + 每個 Proton 都使用內建的 DXVK,即使此 GPU 的 Vulkan 版本可能太舊。 + Mali 橋接:關 + Mali 橋接:開 + %1$s 不是 Adreno。實驗性 Vulkan 橋接已開啟:工作階段透過手機內建的驅動程式繪製。可能會有問題,請傳送工作階段記錄。 + 安全模式 + 安全模式:開 + 安全模式:關 + 相容性最好。建議新裝置使用。 + 在此 GPU 通過檢查的地方較快。 搜尋應用程式 沒有符合「%1$s」的應用程式。 新增小工具 @@ -3607,4 +3620,7 @@ 狀態 合作夥伴 已追蹤的遊戲 + GPU 透過 Mali 橋接 + 透過 Mali 橋接執行,使用手機內建的 Vulkan 驅動程式 + 關閉 Mali 橋接時不受支援:Turnip 需要 Adreno(Snapdragon)GPU diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 32911e9d6..00b2c3a79 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -872,6 +872,7 @@ Device supported Untested GPU GPU not supported + GPU via Mali bridge %1$s · %2$s %1$s: %2$s Steam needs an Adreno driver; %1$s may show a black screen @@ -2250,6 +2251,8 @@ Experimental Adreno 850 support: requires compatible experimental display and game drivers; not verified on this device Below tested hardware (Adreno 725 and newer): it may not run Not supported: DroidDeck needs an Adreno (Snapdragon) GPU + Runs through the Mali bridge, on the phone\'s own Vulkan driver + Not supported with the Mali bridge off: Turnip needs an Adreno (Snapdragon) GPU GitHub\'s rate limit was hit - try again later GitHub answered HTTP %1$d @@ -3655,6 +3658,19 @@ Cloud settings Xbox Cloud Gaming 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. + 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. Steam integration %1$d of %2$d on Set up 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..45afb7785 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/session/BridgeDxvkTest.kt @@ -0,0 +1,267 @@ +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\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)) + } + + @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 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)) + 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()) + } + + @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( + 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, keepProtonDxvk = keep, + ) + + 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()) + } + } +} diff --git a/docs/images/droiddeck-hud-v25-terraria.png b/docs/images/droiddeck-hud-v25-terraria.png new file mode 100644 index 000000000..2739bf590 Binary files /dev/null and b/docs/images/droiddeck-hud-v25-terraria.png differ diff --git a/docs/openmali/README.md b/docs/openmali/README.md new file mode 100644 index 000000000..0760cf2ec --- /dev/null +++ b/docs/openmali/README.md @@ -0,0 +1,244 @@ +# openMali 工作区记录 + +> 本文件记录 2026-10-31 ~ 2026-11-01 在本机与小米平板上的 MaliBridge + DroidDeck 0.6.7 合并、构建、调试和性能测试结果。 +> 这是本地工程记录,不是上游发布文档。未特别说明的 APK 均为 testkey 签名。 + +## 1. 目标与结论 + +- 目标:把 MaliBridge v0.5.4.2 的 Mali GPU 支持合并进 DroidDeck 0.6.7 主线,构建可安装 APK,并在平板上进入桌面、跑通 Steam 和性能测试。 +- 已做到: + - 0.6.7 + MaliBridge v0.5.4.2 的合并包可安装。 + - Ubuntu rc4 运行时 + 内置最新 proot + KWin Zink/Mali 桥接,成功进入 KDE 桌面。 + - Steam 客户端下载/更新 100% 完成,进入 Steam 大屏幕登录页(二维码/账号密码)。 + - 完成 CPU、GPU、IO、内存、温度基线测试。 +- 关键结论:Mali 桥接本身没有成为瓶颈;真正的问题集中在 **运行时 DNS、proot 与 KWin 渲染栈**。当前 rc4 + packaged proot + render-node 是已验证可用的组合。 + +## 2. 设备与环境 + +- 设备:Xiaomi M367FC,SoC xring_o3_asic,GPU Mali-G2-Ultra-NX MC16,Android 17,内核 6.18.21。 +- CPU:10 核;内存 15.7 GB;存储 229 GB;屏幕 3408x2272;120 Hz。 +- Mali 驱动:ARM proprietary,Vulkan 1.4.345;桥接暴露 1.4.341。 +- 新 App:com.droiddeck.launcher,DroidDeck 0.6.7,versionCode 25(0.6.7-dnsfix-v25),testkey。 +- 旧 App:com.droiddeck.launcher.mali,DroidDeck-MaliBridge v0.5.4.2,versionCode 5042;保留用于对比,桌面可用。 + +## 3. 关键路径 + +- 新工程:/home/g/openMali/DroidDeck +- MaliBridge 子模块:/home/g/openMali/DroidDeck/external/MaliBridge,v0.5.4.2,提交 95a1e60 +- 合并基线:main 69ffc096,tag mali-0.6.7-v0.5.4.2 +- 旧版源码:/home/g/openMali/reference/mali0542-src +- 旧版 APK:/home/g/openMali/reference/DroidDeck-MaliBridge-v0.5.4.2.apk +- 最终 APK:/home/g/openMali/output/DroidDeck-0.6.7-Mali-rc4.apk +- 基准/参考工具:/home/g/openMali/reference/vkbridge-bench,/home/g/openMali/reference/oldapk + +## 4. 已验证的修改 + +### 4.1 运行时与 proot + +- 运行时 catalog 从 Debian linuxfs.json(r9) 切到 Ubuntu: + - app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java + - CATALOG_URL = .../linuxfs-ubuntu.json +- 实际安装:ubuntu-r1-rc4;运行时约 3 GB;桌面栈另下载约 898 MB。 +- APK 不再排除 proot,改为内置最新官方 pack: + - pack-proot-172bb48b00150b42 + - 压缩包 SHA-256:172bb48b00150b4292ba171089abecdf7243c06edddd68bf42dddb8c5bb24e4e + - app/src/main/jniLibs/arm64-v8a/libproot.so(246,512 B) + - app/src/main/jniLibs/arm64-v8a/libproot-loader.so(18,136 B) + - app/build.gradle 移除 libproot.so / libproot-loader.so 的 excludes +- LinuxRuntime: + - 优先使用 nativeLibraryDir 中的 packaged proot/loader。 + - 新增 PROOT_PACKAGED 来源;isInstalled 接受 packaged proot。 + - prootSource 显示 packaged;ProotFastPath 只允许 PROOT_PACK,因此当前 fast path 关闭。 +- 旧 r9 + 旧 session/helper/host 的 0.6.7 组合已尝试但放弃: + - KWin 曾能打开 3408x2272 窗口,但新 host 不显示、会话不稳定。 + - 该路线不作为最终方案。 + +### 4.2 proot fast path 实验 + +- 临时允许 packaged proot 走 fast path,实测反而更慢: + - fast path off(当前):sha256 256 MB 207 ms,gzip -1 405 ms,read 3.9 GB/s。 + - fast path on:sha256 259 ms,gzip 461 ms,read 3.8 GB/s。 +- 结论:该 pack 的 fast path 对 CPU/IO 是负优化,已恢复默认关闭;源码 ProotFastPath 也恢复为只允许 PROOT_PACK。 + +### 4.3 桌面 / KWin / Mali 桥接 + +- tools/linuxfs/desktop/droiddeck-desktop 的 KWin OpenGL 分支: + - 设置 KWIN_COMPOSE=O2、MESA_LOADER_DRIVER_OVERRIDE=zink、GALLIUM_DRIVER=zink、LIBGL_KOPPER_DRI2=true。 + - KWIN_RENDER_NODES 自动选择 /dev/dri/renderD0、/dev/dri/renderD307 或任意 /dev/dri/renderD*。 + - 在平板上实际使用 /dev/dri/renderD307(由 /dev/mali0 绑定)。 +- /usr/share/droiddeck/kwin-compositor 为 opengl。 +- 效果: + - DroidDeckWayland 日志出现 [window] opened KDE Wayland Compositor 1280x720。 + - host 统计出现 GPU frames,不再出现 kwin software fallback / black picture。 + - KDE 桌面、任务栏、应用菜单正常。 +- 分辨率: + - displayResolution.lxqt 设为 720p(1280x720)。 + - 720p 与 900p 的 vkgears 都是约 119.8 FPS,说明 GPU 已到 120 Hz 上限;720p 只减少缩放/带宽。 +- Steam 大屏幕模式: + - app 的“游玩 Steam”使用 gamescope/Big Picture。 + - DroidDeckWayland 日志:Steam 大屏幕模式 1280x720,86.4 fps,875 GPU frames。 + - 可用 Steam 登录页。 + +### 4.4 UI / 桥接文案 + +- GpuInfo 恢复 bridge-aware supportText(bridge),新增 gpuinfo_bridge / gpuinfo_bridge_off 字符串。 +- DeviceInfo / DeviceReport / DriverMenus 使用当前 Mali 桥接状态显示支持文案。 +- LinuxRuntime 恢复 bridgeStandIn 的 DRM render node 回退逻辑。 +- 注意:Setup 顶部仍可能显示“GPU 不受支持”的旧标签(GpuDriversState 的 unsupported 标志),但下方已显示“实验性 Vulkan 桥接已开启”,桥接实际工作正常。 + +## 5. DNS 与 Steam + +### 5.1 根因 + +- guest /etc/resolv.conf 被写成: + - 1.1.1.1 + - 8.8.8.8 + - 2001:4860:4860::8888 +- 当前网络下这些公共 DNS 解析超时: + - curl: (28) Resolving timed out after 10001 milliseconds +- 设备实际 WiFi DNS 是 192.168.1.1;guest 改用该 DNS 后: + - getent 能解析 client-update.steamstatic.com。 + - curl -I https://client-update.steamstatic.com/steam_client_linuxarm64 返回 200。 +- 结论:Steam 下载失败是 DNS,不是 Steam/GPU/代理。 + +### 5.2 已修复:App 自动获取本地 DNS + +- 修改 app/src/main/java/com/droiddeck/launcher/runtime/LinuxNetworkLinkComponent.kt: + - 新增 localDnsServers(),用 ConnectivityManager.allNetworks 遍历 WiFi/Ethernet 网络(排除 VPN)。 + - 收集这些网络的 DNS,并把默认路由 gateway 作为本地 DNS 兜底。 + - 私有地址(192.168/10.x)和 IPv4 优先。 + - writeResolver 的顺序:当前 active network DNS + localDnsServers() + 公共 DNS 兜底。 +- 验证(2026-11-01,dnsfix APK): + - logcat:LinuxNetworkLink: resolver: 192.168.1.1 8.8.8.8 1.1.1.1 2001:4860:4860::8888 + - guest /etc/resolv.conf 第一行是 nameserver 192.168.1.1。 + - getent hosts store.steampowered.com 正常;curl -I https://store.steampowered.com 返回 200。 + - 旧 guest DNS 守护 /root/.config/plasma-workspace/env/01-dns-guard.sh 已删除;没有守护进程仍正常。 +- 说明:localDnsServers() 只取 WiFi/Ethernet 的 DNS,网关排在各 DNS 之后;active network 是 VPN 时不追加本地 DNS,只用 VPN 自己的 DNS。 + +### 5.3 Steam 客户端修复 + +- Steam 主程序下载后权限为 666: + - /root/.local/share/Steam/steamrtarm64/steam + - 需 chmod +x;更新后变为 700 也可执行。 +- 需要数据目录软链接: + - /root/.steam/steam -> /root/.local/share/Steam + - /root/.steam/root -> /root/.local/share/Steam +- stale pid: + - /root/.steam/steam.pid 可能残留已死 PID,导致 “Steam is already running”;启动前删除。 +- 不要在 Konsole 里用桌面 UI 方式启动 steam: + - 会走 Xwayland/CEF,遇到 XRRGetOutputInfo() is not available / 无窗口。 + - 应从 DroidDeck 的“游玩 Steam”启动 Big Picture/gamescope。 +- 已验证(2026-11-01): + - Steam 客户端更新 575,927 KB 到 100%。 + - Big Picture 已登录,库中显示 Shadow of the Tomb Raider 等游戏。 + - 32 位 steamrtarm32/steam-runtime-launcher-service 缺失的日志仍会出现,但 Big Picture 可用;游戏启动尚未验证。 + +## 6. 性能测试 + +### 6.1 存储 IO + +| 路径 | 写 | 读 | +|---|---:|---:| +| Android /data/local/tmp | 1.0 GB/s | 4.7 GB/s | +| Android /sdcard | 0.94 GB/s | 4.3 GB/s | +| Guest proot(fast path off) | 1.2 GB/s | 3.9 GB/s | + +Guest 读取比 host 低约 17%,是 proot 路径开销;写入不慢。 + +### 6.2 CPU + +| 项目 | Host | Guest | +|---|---:|---:| +| sha256 256 MB 单线程 | 267 ms | 207 ms | +| sha256 4x256 MB 4 线程 | 256 ms(约 4 GB/s 聚合) | 216 ms(约 4.7 GB/s 聚合) | +| gzip -1 256 MB | 307 ms | 405 ms(+32%) | + +注意:host/guest 的 sha256 实现不同,不能直接比较;gzip 更接近,proot 下约慢 30%。 + +### 6.3 GPU + +- vkgears:119.86 FPS(600 帧/5 秒),720p 与 900p 基本一致,已到 120 Hz 上限。 +- 早期桥接基准:128 MiB copy 20.5 GB/s;空 submit 约 70 µs;4 KiB copy+submit 约 285 µs。 +- Steam Big Picture:1280x720,约 86.4 fps,875 GPU frames。 +- vkcube 在纹理路径触发断言(demo_prepare_texture_image),属于桥接兼容性问题,不是性能问题。 + +### 6.4 温度 / 内存 + +- CPU 29–33 °C,GPU 31–46.5 °C,Thermal Status 0,无节流。 +- 电池约 28 °C,64%。 +- 内存:15.7 GB 总,约 8.3 GB 已用,7.1 GB 可用,swap 约 2.6 GB。 +- 没有内存告急,但 swap 偏高;游戏前可重启/关闭后台。 + +### 6.5 v25 复测(2026-11-01 18:32) + +环境:DroidDeck v25;GPU/IO 在 lxqt 桌面 1920x1080@120 测,CPU 分别测桌面和 Steam 会话;游戏 FPS 取自 Terraria 世界内 HUD。Thermal Status 0,CPU 32.8 °C,GPU max 34.8–46.5 °C,电池 31.8 °C / 92%。 + +| 项目 | v25 实测 | 说明 | +|---|---:|---| +| sha256 256 MB 单线程 | 桌面 283–322 ms;Steam 219–241 ms | Steam 会话约 230 ms,接近此前 207 ms | +| gzip -1 256 MB | 桌面 514–534 ms;Steam 355–357 ms | Steam 会话约 356 ms;此前约 405 ms | +| sha256 4×256 MB | Steam 222–269 ms;桌面 262–275 ms | 4 线程 | +| Host /data/local/tmp O_DIRECT | 写 1060 / 读 1450 MB/s | 绕过 page cache | +| Guest /root ext4 | 写 876 / 读 2337 MB/s | fdatasync;O_DIRECT 写 1270 MB/s | +| Guest /root/Storage | 写 920 / 读 3029 MB/s | 读受 page cache 影响 | +| vkgears 1280x720 | 115.8 FPS,p50 111.1,p99 25 ms | 4 秒采样 | +| vkgears 1920x1080 | 113.2 FPS,p99 31 ms | 2 秒采样 | +| Terraria 1280x720@120 | 59.9 FPS,AVG 49.3,1% 22.7 | HUD;CPU 36% / 2668 MHz / 33 °C,GPU 1% | +| 内存 | 总 15.7 GB,可用 8.7 GB | Guest 已用 6.7 GB,swap 2.2 GB | + +结论:GPU 与之前 119.86 FPS 基本一致(低约 3–4%),Terraria 稳定 60 FPS;IO 正常;测试期间无热节流。CPU 在 Steam 会话约 230 ms / 356 ms,接近或优于此前 207 ms / 405 ms;桌面会话因升频状态较低,v25 无 CPU 回归。 + +## 7. 构建与安装 + +- 构建: + - cd /home/g/openMali/DroidDeck + - ./gradlew :app:assembleRelease -PndkVersion=27.3.13750724 +- 修改 runtime inputs 后先记录: + - python3 tools/release/runtime_inputs.py record +- APK(DNS 自动修复 + GPU 状态文案 + HUD/CPU 温度 + mscoree 兜底,当前安装): + - /home/g/openMali/output/DroidDeck-0.6.7-Mali-rc4-dnsfix-v25.apk + - 76 MB;versionCode 25;versionName 0.6.7-dnsfix-v25 + - SHA-256 9808a8ab17f2d9a39e0b3abc9a05c2ee1c07ec7f4ac7cfa77866b2229fb6eb82 + - 上一版保留:v24(SHA-256 56b0ab03c0120c2a7a6ad0e4ed8d30a748021d7fe9e99a13b60f62a41e13b5e0)、v23(SHA-256 b0fadc5c...)、v22(SHA-256 69cd4f5f...)、v21(SHA-256 508294a6...)、v20(SHA-256 afd1ce12...)、v19(SHA-256 f63f4eae...)、v18(SHA-256 cc469703...)、v16 DroidDeck-0.6.7-Mali-rc4-dnsfix.apk(SHA-256 6c5f8c1f...)和 DroidDeck-0.6.7-Mali-rc4.apk(无 App 内 DNS 修复,SHA-256 c58fcc58...)。 + - testkey 签名;官方 release 私钥不在本机。 +- MIUI 安装: + - 对已安装的旧包,adb push + adb shell pm install -r 可更新。 + - 对全新安装,MIUI 会报 INSTALL_FAILED_USER_RESTRICTED;需用文件管理器打开 APK,走 PackageInstaller,并在“是否允许 Shell 安装应用”点允许。 + - 官方 0.6.9 是 release key,testkey 包不能就地升级/降级;必须完整卸载,App 数据会被清空。 + +## 8. 当前安装状态 + +- com.droiddeck.launcher:0.6.7-dnsfix-v25(versionCode 25,testkey),2026-11-01 18:25 就地更新安装,App 数据保留。 +- GPU 状态:Setup 概览显示“GPU 通过 Mali 桥接”,绿色通过;不再显示“GPU 不受支持”。实现为 FrontEndSetup 的 gpuOk 包含 s.vkBridge,并新增 setup_gpu_bridged 文案。 +- 修复“清数据重启后 GPU 菜单消失”:showChecks 默认值从 false 改为 s.vkBridge;桥接开启时详情默认展开,GPU 行直接可见。已在 pm clear 后重新启动验证。 +- HUD:XRing/Mali 的 GPU load/clock/temp/thermal sysfs 对普通 App 拒绝访问,日志为 thermal zones 0、gpu load none、kgsl gpubusy refused。v21 在无数据时隐藏 GPU 统计行、VRAM=0 时隐藏 VRAM,只保留 GPU 型号;真正 GPU% / 温度 / 共享显存需要 root 或厂商遥测。 +- HUD 分辨率/卡死:v22 的 RES 不再用 Android displayMetrics 兜底,改用会话逻辑分辨率;桌面模式隐藏游戏 HUD,Steam/游戏模式显示。实测 Steam 下 HUD 为 RES 1280x720 @ 120Hz、FPS 42.8 / AVG 53.0。 +- CPU 温度/频率:v23 增加 USB/adb 辅助的 CPU 温度输入(/sdcard/DroidDeck/cpu-temp,由开发者在主机侧每 2 秒写入 cpu_therm,仓库不附带该脚本);v24 把 HUD 的 CPU 行顺序改为“占用 → 频率 → 温度”,Full/Mega/Tiles 三种布局都生效。 +- 安装方式:官方 0.6.9(release key)与 testkey 不兼容,完整卸载 0.6.9 后通过 PackageInstaller 安装;App 数据已被清空,当前是全新状态(首次启动显示配置页)。 +- DNS 自动修复已在 APK 内;运行时 ubuntu-r1-rc4、Mali 桥接、proot、桌面需要重新下载/配置;此前 Steam Big Picture 和 1280x720 KWin 已验证,但当前新数据需重新登录。 +- com.droiddeck.launcher.mali:旧 0.5.4.2 仍安装,桌面可用,用于对比。 +- 注意:Steam chmod/软链接/pid 清理仍是 guest 运行时修改;清除 App 数据或重装运行时会丢失。 +- Terraria 1.4.5.8 启动修复(2026-11-01):`dotnet40` 组件把 `WINEDLLOVERRIDES` 设为 `mscoree=n`,但该组件没有原生 `mscoree.dll`,Wine 报 `mscoree.dll not found` 后直接退出。v25 的 `droiddeck-wincomponents` 对 `mscoree=n` 自动追加 builtin 回退(`mscoree=n,b`);`dotnet40` 仍启用时,Terraria 已稳定进入世界并显示 HUD。 + +## 9. 已知问题 / 下一步 + +1. DNS 自动修复已完成(见 5.2);仍需在不同 WiFi/无 WiFi 环境下回归,确认 localDnsServers 的回退行为。 +2. Steam 桌面 UI 在 Xwayland 下仍可能无窗口;继续使用“游玩 Steam”/Big Picture,不要用 Konsole 直接 steam。 +3. Steam 32 位组件 steamrtarm32 / steam-runtime-launcher-service 缺失;Big Picture 可用。Terraria 1.4.5.8 已进入世界,其他游戏仍待验证。 +4. vkcube 纹理断言是桥接兼容性 bug,可单独排查。 +5. 真实游戏帧率/Proton/DXVK 尚未测试;当前性能数据主要是桌面、vkgears、IO/CPU。 +6. 仍是 testkey;如需官方升级安装,需要官方 release keystore。 +7. 官方 0.6.9 与 testkey 不兼容:只能完整卸载后安装,App 数据会丢;官方 APK 备份在 /tmp/installed-069.apk(宿主)和 /sdcard/Download/DroidDeck-0.6.9.apk(平板),回滚同样要先卸载。 +8. HUD 的 Mali GPU load/temp/VRAM 受驱动 sysfs 权限限制;除非 root/厂商接口,普通 App 无法获取。v21 已移除误导性的 GPU - 和 VRAM 0.0,保留 GPU 型号。 + +## 10. 已放弃 / 不再使用 + +- r9 + 旧 session/helper/host 组合:KWin 窗口不稳定/黑屏,放弃。 +- proot fast path:实测负优化,关闭。 +- 代理强制给 Steam:本地 DNS 直连更稳定;Steam 更新器对 proxy 环境并不可靠。 + +## 截图 + +Terraria 1.4.5.8 在 DroidDeck v25 + Mali 桥接 + Wine Mono 下进入世界。HUD 的 CPU 行是“占用 → 频率 → 温度”: + +![DroidDeck v25 HUD / Terraria](../images/droiddeck-hud-v25-terraria.png) 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..97882e8a6 --- /dev/null +++ b/docs/third-party/malibridge/README.md @@ -0,0 +1,33 @@ +# 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 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. + +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 +- 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): + 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). diff --git a/external/MaliBridge b/external/MaliBridge new file mode 160000 index 000000000..95a1e600a --- /dev/null +++ b/external/MaliBridge @@ -0,0 +1 @@ +Subproject commit 95a1e600a51299294e06bb09db2db8cc509fe6a3 diff --git a/tools/build_local.sh b/tools/build_local.sh index 7952d7a0e..216b1e0df 100755 --- a/tools/build_local.sh +++ b/tools/build_local.sh @@ -41,6 +41,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 @@ -135,6 +139,7 @@ docker run --rm --platform "${docker_platform}" \ -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 diff --git a/tools/components/prepare.py b/tools/components/prepare.py index acccf54e8..122ba98c9 100644 --- a/tools/components/prepare.py +++ b/tools/components/prepare.py @@ -34,6 +34,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 ab7d40b4a..88ae5b79d 100644 --- a/tools/components/recipes.json +++ b/tools/components/recipes.json @@ -225,6 +225,21 @@ "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" + ] + }, "dirt-ws2": { "script": "tools/components/recipes/dirt-ws2.sh", "inputs": [ 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/linuxfs/desktop/droiddeck-desktop b/tools/linuxfs/desktop/droiddeck-desktop index df09229f8..799a3dee8 100644 --- a/tools/linuxfs/desktop/droiddeck-desktop +++ b/tools/linuxfs/desktop/droiddeck-desktop @@ -114,8 +114,10 @@ fi if [ "$(cat /usr/share/droiddeck/kwin-compositor 2>/dev/null)" = opengl ]; then unset LIBGL_ALWAYS_SOFTWARE QT_QUICK_BACKEND export KWIN_COMPOSE=O2 MESA_LOADER_DRIVER_OVERRIDE=zink GALLIUM_DRIVER=zink LIBGL_KOPPER_DRI2=true - if [ -z "${KWIN_RENDER_NODES:-}" ] && [ -c /dev/dri/renderD0 ]; then - export KWIN_RENDER_NODES=/dev/dri/renderD0 + if [ -z "${KWIN_RENDER_NODES:-}" ]; then + for node in /dev/dri/renderD0 /dev/dri/renderD307 /dev/dri/renderD*; do + if [ -c "$node" ]; then export KWIN_RENDER_NODES="$node"; break; fi + done fi mkdir -p /run/droiddeck/kwin printf '#!/bin/sh\nGALLIUM_THREAD=0 exec /usr/bin/kwin_wayland "$@"\n' > /run/droiddeck/kwin/kwin_wayland diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env index 608eefd5f..4b33936d6 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env @@ -478,6 +478,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 + + GAME_DEBUG_CHANNELS = ( "timestamp", "pid", "tid", "seh", "unwind", "threadname", "debugstr", "loaddll", "mscoree", ) @@ -974,6 +982,9 @@ def main(): env["AUDIO_DIRECT_MIC"] = "0" env = directaudio_settings(env) 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) cpu_topology(env, command) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index b0cbc0a55..0c037af47 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -160,7 +160,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') @@ -177,6 +177,83 @@ 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/[.,]/} + 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 + [ -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 + 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}; missing:$bl_vkb_missing)" + [ -n "${BL_INSIDE:-}" ] || exit 69 + fi + unset bl_vkb_icd bl_vkb_missing +fi export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0} start_agent() { @@ -307,6 +384,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) @@ -1023,6 +1107,28 @@ case $mode in _bl_cef=(-cef-disable-gpu) _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)" + 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[@]}") + 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 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_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: diff --git a/tools/tests/test_game_environment.py b/tools/tests/test_game_environment.py index 1d2ea6441..0c68fedb8 100644 --- a/tools/tests/test_game_environment.py +++ b/tools/tests/test_game_environment.py @@ -649,6 +649,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": ""}}} 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() 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 30a41fc7b..7c49e1d76 100644 --- a/tools/tests/test_webhelper_preload.py +++ b/tools/tests/test_webhelper_preload.py @@ -61,6 +61,19 @@ 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_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=""))