diff --git a/app/src/main/assets/redirect.tzst b/app/src/main/assets/redirect.tzst index 3e9bc47bd1..dccc7e590a 100644 Binary files a/app/src/main/assets/redirect.tzst and b/app/src/main/assets/redirect.tzst differ diff --git a/app/src/main/java/app/gamenative/service/DownloadService.kt b/app/src/main/java/app/gamenative/service/DownloadService.kt index 511f9e55f4..c05e72541d 100644 --- a/app/src/main/java/app/gamenative/service/DownloadService.kt +++ b/app/src/main/java/app/gamenative/service/DownloadService.kt @@ -2,6 +2,7 @@ package app.gamenative.service import android.content.Context import android.os.Environment +import app.gamenative.PrefManager import app.gamenative.utils.StorageUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -37,10 +38,27 @@ object DownloadService { baseExternalAppDirPath = extFiles?.parentFile?.path ?: "" val sm = context.getSystemService(android.os.storage.StorageManager::class.java) - externalVolumePaths = StorageUtils.getAllExternalFilesDirs(context) + val appFilesDirs = StorageUtils.getAllExternalFilesDirs(context) .filter { Environment.getExternalStorageState(it) == Environment.MEDIA_MOUNTED } .filter { sm?.getStorageVolume(it)?.isPrimary != true } - .map { it.absolutePath } + // both layouts per volume: legacy Android/data (existing installs) + public root (new installs) + externalVolumePaths = appFilesDirs + .flatMap { dir -> listOfNotNull(dir.absolutePath, StorageUtils.publicInstallRoot(dir)?.absolutePath) } + .distinct() + + migrateExternalStoragePath() + } + + // Android/data paths pay a ~1000x FUSE metadata penalty (MediaProvider disables kernel + // caching there); repoint the install pref at the public root so new installs avoid it + private fun migrateExternalStoragePath() { + val pref = PrefManager.externalStoragePath + if (pref.isBlank() || !pref.contains("/Android/data/")) return + val public = StorageUtils.publicInstallRoot(File(pref)) ?: return + if (StorageUtils.ensureInstallRoot(public)) { + Timber.i("Migrating external install root from $pref to ${public.absolutePath}") + PrefManager.externalStoragePath = public.absolutePath + } } @Synchronized diff --git a/app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt b/app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt index b8175a6ed3..5a992888e9 100644 --- a/app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt +++ b/app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt @@ -561,7 +561,7 @@ fun SettingsGroupInterface( useExternalStorage = it PrefManager.useExternalStorage = it if (it && dirs.isNotEmpty()) { - PrefManager.externalStoragePath = dirs[0].absolutePath + PrefManager.externalStoragePath = StorageUtils.preferredInstallRoot(dirs[0]) } }, ) @@ -569,8 +569,10 @@ fun SettingsGroupInterface( // Currently selected item var selectedIndex by rememberSaveable { mutableStateOf( - dirs.indexOfFirst { it.absolutePath == PrefManager.externalStoragePath } - .takeIf { it >= 0 } ?: 0, + dirs.indexOfFirst { dir -> + dir.absolutePath == PrefManager.externalStoragePath || + StorageUtils.publicInstallRoot(dir)?.absolutePath == PrefManager.externalStoragePath + }.takeIf { it >= 0 } ?: 0, ) } SettingsListDropdown( @@ -579,7 +581,7 @@ fun SettingsGroupInterface( value = selectedIndex, onItemSelected = { idx -> selectedIndex = idx - PrefManager.externalStoragePath = dirs[idx].absolutePath + PrefManager.externalStoragePath = StorageUtils.preferredInstallRoot(dirs[idx]) }, colors = settingsTileColorsAlt(), ) diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index ee2220c9ac..0266fd28df 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -3658,6 +3658,12 @@ private fun setupXEnvironment( envVars.remove("DXVK_FRAME_RATE") envVars.remove("VKD3D_FRAME_RATE") if (!envVars.has("WINEESYNC")) envVars.put("WINEESYNC", "1") + + val ffpGameDir = runCatching { + File(SteamService.getAppDirPath(ContainerUtils.extractGameIdFromContainerId(appId))).canonicalFile.path + }.getOrDefault("") + if (ffpGameDir.startsWith("/storage/")) envVars.put("FFP_ENABLE", "1") + val graphicsDriverConfig = KeyValueSet(container.getGraphicsDriverConfig()) if (graphicsDriverConfig.get("version").lowercase(Locale.getDefault()).contains("gen8")) { var tuDebug = envVars.get("TU_DEBUG") diff --git a/app/src/main/java/app/gamenative/utils/ContainerUtils.kt b/app/src/main/java/app/gamenative/utils/ContainerUtils.kt index 476fdbb062..bfca448e3a 100644 --- a/app/src/main/java/app/gamenative/utils/ContainerUtils.kt +++ b/app/src/main/java/app/gamenative/utils/ContainerUtils.kt @@ -1041,11 +1041,13 @@ object ContainerUtils { } } - if (gameFolderPath != null) { + val resolvedGameFolderPath = StorageUtils.migrateLegacyGameDir(gameFolderPath) + + if (resolvedGameFolderPath != null) { // Check if A: drive is already mapped to the correct path var hasCorrectADrive = false for (drive in Container.drivesIterator(container.drives)) { - if (drive[0] == "A" && drive[1] == gameFolderPath) { + if (drive[0] == "A" && drive[1] == resolvedGameFolderPath) { hasCorrectADrive = true break } @@ -1056,7 +1058,7 @@ object ContainerUtils { val currentDrives = container.drives // Rebuild drives string, excluding existing A: drive and adding new one val drivesBuilder = StringBuilder() - drivesBuilder.append("A:$gameFolderPath") + drivesBuilder.append("A:$resolvedGameFolderPath") // Add all other drives (excluding A:) for (drive in Container.drivesIterator(currentDrives)) { diff --git a/app/src/main/java/app/gamenative/utils/StorageUtils.kt b/app/src/main/java/app/gamenative/utils/StorageUtils.kt index 4bade589e3..b861c1eb9d 100644 --- a/app/src/main/java/app/gamenative/utils/StorageUtils.kt +++ b/app/src/main/java/app/gamenative/utils/StorageUtils.kt @@ -98,6 +98,55 @@ object StorageUtils { return result } + private const val PUBLIC_INSTALL_DIR_NAME = "GameNative" + + /** + * Maps an app-specific dir (/Android/data//files) to a public install root + * (/GameNative). MediaProvider disables FUSE kernel caching under Android/data, + * making per-open metadata ops ~1000x slower there; public dirs get normal dcache treatment. + */ + fun publicInstallRoot(appFilesDir: File): File? { + val path = appFilesDir.absolutePath + val idx = path.indexOf("/Android/data/") + if (idx <= 0) return null + return File(path.substring(0, idx), PUBLIC_INSTALL_DIR_NAME) + } + + fun ensureInstallRoot(dir: File): Boolean { + if (!dir.isDirectory && !dir.mkdirs()) return false + runCatching { File(dir, ".nomedia").createNewFile() } + return true + } + + fun preferredInstallRoot(appFilesDir: File): String { + val public = publicInstallRoot(appFilesDir) + if (public != null && ensureInstallRoot(public)) return public.absolutePath + return appFilesDir.absolutePath + } + + fun migrateLegacyGameDir(path: String?): String? { + if (path.isNullOrBlank()) return path + val idx = path.indexOf("/Android/data/") + if (idx <= 0) return path + val filesIdx = path.indexOf("/files/", idx) + if (filesIdx < 0) return path + val legacyRoot = File(path.substring(0, filesIdx + "/files".length)) + val rel = path.substring(filesIdx + "/files/".length) + val src = File(path) + if (!src.isDirectory) return path + val publicRoot = publicInstallRoot(legacyRoot) ?: return path + val dst = File(publicRoot, rel) + if (dst.exists() || !ensureInstallRoot(publicRoot)) return path + dst.parentFile?.mkdirs() + return if (src.renameTo(dst)) { + Timber.i("Migrated game dir $path to ${dst.absolutePath}") + dst.absolutePath + } else { + Timber.w("Could not migrate $path; leaving in place") + path + } + } + /** * Gets all app-specific external files directories, using StorageManager as a fallback * for cases where context.getExternalFilesDirs(null) might return null or incomplete results diff --git a/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java b/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java index a21237db97..23807b0f4c 100644 --- a/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java +++ b/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java @@ -43,7 +43,7 @@ import java.util.concurrent.atomic.AtomicLong; public abstract class ImageFsInstaller { - public static final byte LATEST_VERSION = 28; + public static final byte LATEST_VERSION = 29; private static void resetContainerImgVersions(Context context) { ContainerManager manager = new ContainerManager(context); diff --git a/app/src/modern/assets/libredirect-bionic-wx.so b/app/src/modern/assets/libredirect-bionic-wx.so index 5722cc6052..b4184771cc 100755 Binary files a/app/src/modern/assets/libredirect-bionic-wx.so and b/app/src/modern/assets/libredirect-bionic-wx.so differ