From 26d1a7ff656299bdd565a1a9d2922546b479a6ad Mon Sep 17 00:00:00 2001 From: maxjivi05 Date: Fri, 3 Jul 2026 15:08:31 -0400 Subject: [PATCH 1/4] FEX UnixLibs: native .so backend toggle + wiring Add a smart "Use UnixLibs" toggle (Advanced, above the FEX selectors) that runs FEX translation via the native .so unixlib backend instead of the in-c_drive PE dlls. Detects UnixLib-capable FEX versions, tracks dll/unixlibs mode per launch, and places the .so into the active wine/Proton so_dir. Includes translations for all locales and a .wcp packaging tool. --- .gitignore | 3 + app/src/main/feature/library/GameSettings.kt | 15 ++++ .../ContainerSettingsComposeDialog.kt | 4 ++ .../ShortcutSettingsComposeDialog.kt | 13 ++++ app/src/main/res/values-da/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-hi/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-ro/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/runtime/container/Container.java | 9 +++ .../main/runtime/content/ContentsManager.java | 70 ++++++++++++++++++- .../GuestProgramLauncherComponent.java | 49 +++++++++---- tools/build_fex_unixlibs_wcp.sh | 57 +++++++++++++++ 23 files changed, 220 insertions(+), 15 deletions(-) create mode 100755 tools/build_fex_unixlibs_wcp.sh diff --git a/.gitignore b/.gitignore index 7e71a9864..ecf01c18a 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,6 @@ signing.properties References/ *.hprof android_sysvshm/build64/ + +# local build artifacts (FEX/Proton wcp) +dist/ diff --git a/app/src/main/feature/library/GameSettings.kt b/app/src/main/feature/library/GameSettings.kt index 85aae8e63..08479e6c9 100644 --- a/app/src/main/feature/library/GameSettings.kt +++ b/app/src/main/feature/library/GameSettings.kt @@ -507,6 +507,9 @@ class GameSettingsStateHolder { val selectedFexcoreVersion = mutableIntStateOf(0) val fexcorePresetEntries = mutableStateOf>(emptyList()) val selectedFexcorePreset = mutableIntStateOf(0) + // Per-version flag: true when that FEX version ships native .so UnixLibs. + val fexcoreUnixLibsFlags = mutableStateOf>(emptyList()) + val useUnixLibs = mutableStateOf(true) // Advanced - System val startupSelectionEntries = mutableStateOf>(emptyList()) @@ -3554,6 +3557,18 @@ private fun AdvancedSection( // FEXCore — hidden when FEXCore isn't explicitly in either slot. if (state.showFexcoreFrame.value) { + val fexUnixCapable = state.fexcoreUnixLibsFlags.value + .getOrNull(state.selectedFexcoreVersion.intValue) ?: false + SettingGroup { + SettingSwitch( + label = stringResource(R.string.use_unix_libs), + checked = state.useUnixLibs.value && fexUnixCapable, + onCheckedChange = { state.useUnixLibs.value = it }, + enabled = fexUnixCapable + ) + } + Spacer(Modifier.height(SettingSectionGap)) + val fexcoreUsage = emulatorUsageLabel(state, setOf("fexcore")) EmulatorSectionHeader(stringResource(R.string.container_fexcore_config), fexcoreUsage) Spacer(Modifier.height(8.dp)) diff --git a/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt b/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt index c6757926b..0192754bc 100644 --- a/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt +++ b/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt @@ -422,6 +422,7 @@ class ContainerSettingsComposeDialog @JvmOverloads constructor( } state.fullscreenStretched.value = c?.isFullscreenStretched() ?: false + state.useUnixLibs.value = c?.isUseUnixLibs() ?: true // Steam fields are shortcut-only in the UI; leave any existing steam // state on the container untouched — saveSettings() skips them. @@ -799,6 +800,7 @@ class ContainerSettingsComposeDialog @JvmOverloads constructor( c.setWinComponents(wincomponents) c.setDrives(drivesString) c.setFullscreenStretched(state.fullscreenStretched.value) + c.setUseUnixLibs(state.useUnixLibs.value) c.setInputType(finalInputType) c.setExclusiveXInput(state.containerExclusiveInput.value) c.setStartupSelection(startupSelection) @@ -839,6 +841,7 @@ class ContainerSettingsComposeDialog @JvmOverloads constructor( data.put("wincomponents", wincomponents) data.put("drives", drivesString) data.put("fullscreenStretched", state.fullscreenStretched.value) + data.put("useUnixLibs", state.useUnixLibs.value) data.put("inputType", finalInputType) data.put("exclusiveXInput", state.containerExclusiveInput.value) data.put("startupSelection", startupSelection.toInt()) @@ -989,6 +992,7 @@ class ContainerSettingsComposeDialog @JvmOverloads constructor( if (firstDash >= 0) items.add(entryName.substring(firstDash + 1)) } state.fexcoreVersionEntries.value = items + state.fexcoreUnixLibsFlags.value = items.map { contentsManager.fexcoreVersionHasUnixLibs(it) } val saved = container?.getFEXCoreVersion() ?: "" selectByValue(items, saved, state.selectedFexcoreVersion) } diff --git a/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt b/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt index 0fa0f52ba..d685cf306 100644 --- a/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt +++ b/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt @@ -414,6 +414,10 @@ class ShortcutSettingsComposeDialog private constructor( if (container.isFullscreenStretched) "1" else "0" ) state.fullscreenStretched.value = fullscreenStretched == "1" + state.useUnixLibs.value = getShortcutSetting( + "useUnixLibs", + if (container.isUseUnixLibs) "1" else "0" + ) == "1" // LC_ALL state.lcAll.value = getShortcutSetting("lc_all", container.getLC_ALL()) @@ -824,6 +828,7 @@ class ShortcutSettingsComposeDialog private constructor( } state.fexcoreVersionEntries.value = items + state.fexcoreUnixLibsFlags.value = items.map { contentsManager.fexcoreVersionHasUnixLibs(it) } val savedVersion = if (shouldUseShortcutOverrides(container)) getShortcutSetting("fexcoreVersion", container.getFEXCoreVersion()) else @@ -1071,6 +1076,13 @@ class ShortcutSettingsComposeDialog private constructor( if (container.isFullscreenStretched) "1" else "0" ) + // Use UnixLibs + hasContainerOverride = hasContainerOverride or saveOverride( + "useUnixLibs", + if (state.useUnixLibs.value) "1" else "0", + if (container.isUseUnixLibs) "1" else "0" + ) + // Win components val wincomponents = buildWinComponentsString() hasContainerOverride = @@ -2160,6 +2172,7 @@ class ShortcutSettingsComposeDialog private constructor( state.lcAll.value = container.getLC_ALL() state.fullscreenStretched.value = container.isFullscreenStretched + state.useUnixLibs.value = container.isUseUnixLibs val startupEntries = state.startupSelectionEntries.value state.selectedStartupSelection.intValue = container.getStartupSelection().toInt() diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 232fde91a..4f146e6a8 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -505,6 +505,7 @@ FEXCore FEXCore-forudindstilling FEXCore-version + Brug UnixLibs Grafikdriver diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 54ef2714d..3db58b93d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -505,6 +505,7 @@ FEXCore FEXCore-Voreinstellung FEXCore-Version + UnixLibs verwenden Grafiktreiber diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d5c56ec06..f173826f6 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -505,6 +505,7 @@ FEXCore Preset de FEXCore Versión de FEXCore + Usar UnixLibs Controlador gráfico diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 52d81280e..15d7a9801 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -505,6 +505,7 @@ FEXCore Préréglage FEXCore Version de FEXCore + Utiliser les UnixLibs Pilote graphique diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index a285325bb..73340af82 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -559,6 +559,7 @@ FEXCore FEXCore प्रीसेट FEXCore संस्करण + UnixLibs का उपयोग करें ग्राफ़िक्स ड्राइवर ग्राफ़िक्स ड्राइवर कॉन्फ़िगरेशन ग्राफ़िक्स ड्राइवर संस्करण diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 21d6b2bc9..8ce77f6ba 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -505,6 +505,7 @@ FEXCore Preset FEXCore Versione FEXCore + Usa UnixLibs Driver grafico diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 1d82ebedf..d3e12a025 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -505,6 +505,7 @@ FEXCore FEXCore 프리셋 FEXCore 버전 + UnixLibs 사용 그래픽 드라이버 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index cb133e128..98613951f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -509,6 +509,7 @@ FEXCore Preset FEXCore Wersja FEXCore + Użyj UnixLibs Sterownik grafiki diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 996104f88..7bd36eee2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -505,6 +505,7 @@ FEXCore Predefinicao FEXCore Versao do FEXCore + Usar UnixLibs Driver Grafico diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 0ec0c94c8..8411c5bc1 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -505,6 +505,7 @@ FEXCore Preset FEXCore Versiune FEXCore + Folosește UnixLibs Driver grafic diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f64eb6a8a..392f3549a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -513,6 +513,7 @@ FEXCore Пресет FEXCore Версия FEXCore + Использовать UnixLibs Графический драйвер Конфигурация графического драйвера diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 281acaeb0..45c92c010 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -509,6 +509,7 @@ FEXCore Пресет FEXCore Версія FEXCore + Використовувати UnixLibs Графічний драйвер diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 682ac8177..20e4aec3e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -505,6 +505,7 @@ FEXCore FEXCore 预设 FEXCore 版本 + 使用 UnixLibs 图形驱动 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1dc93cd8f..90001b4e8 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -505,6 +505,7 @@ FEXCore FEXCore 預設 FEXCore 版本 + 使用 UnixLibs 圖形驅動程式 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 540cc9451..bf004118b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -585,6 +585,7 @@ FEXCore FEXCore Preset FEXCore Version + Use UnixLibs Graphics Driver diff --git a/app/src/main/runtime/container/Container.java b/app/src/main/runtime/container/Container.java index 96e6812b1..8f259419a 100644 --- a/app/src/main/runtime/container/Container.java +++ b/app/src/main/runtime/container/Container.java @@ -51,6 +51,7 @@ public class Container { private String drives = DEFAULT_DRIVES; private String wineVersion = WineInfo.MAIN_WINE_VERSION.identifier(); private boolean fullscreenStretched; + private boolean useUnixLibs = true; private byte startupSelection = STARTUP_SELECTION_ESSENTIAL; private String cpuList; private String cpuListWoW64; @@ -207,6 +208,10 @@ public void setLC_ALL(String lc_all) { public void setFullscreenStretched(boolean fullscreenStretched) { this.fullscreenStretched = fullscreenStretched; } + public boolean isUseUnixLibs() { return useUnixLibs; } + + public void setUseUnixLibs(boolean useUnixLibs) { this.useUnixLibs = useUnixLibs; } + public byte getStartupSelection() { return startupSelection; } @@ -424,6 +429,7 @@ public void saveData() { data.put("wincomponents", wincomponents); data.put("drives", drives); data.put("fullscreenStretched", fullscreenStretched); + data.put("useUnixLibs", useUnixLibs); data.put("inputType", inputType); data.put("exclusiveXInput", exclusiveXInput); data.put("startupSelection", startupSelection); @@ -508,6 +514,9 @@ public void loadData(JSONObject data) throws JSONException { case "fullscreenStretched" : setFullscreenStretched(data.getBoolean(key)); break; + case "useUnixLibs" : + setUseUnixLibs(data.getBoolean(key)); + break; case "inputType" : setInputType(data.getInt(key)); break; diff --git a/app/src/main/runtime/content/ContentsManager.java b/app/src/main/runtime/content/ContentsManager.java index 2c85adea3..0fa2e6fbc 100644 --- a/app/src/main/runtime/content/ContentsManager.java +++ b/app/src/main/runtime/content/ContentsManager.java @@ -17,6 +17,7 @@ import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import org.json.JSONArray; import org.json.JSONException; @@ -750,6 +751,72 @@ public ContentProfile getProfileByEntryName(String entryName) { return null; } + // A FEX version exposes native UnixLibs when its install dir ships any .so payload. + public boolean profileHasUnixLibs(ContentProfile profile) { + if (profile == null) return false; + return dirContainsSharedObject(getInstallDir(context, profile)); + } + + public boolean fexcoreVersionHasUnixLibs(String fexcoreVersion) { + if (fexcoreVersion == null || fexcoreVersion.isEmpty()) return false; + return profileHasUnixLibs(getProfileByEntryName("fexcore-" + fexcoreVersion)); + } + + private static boolean dirContainsSharedObject(File dir) { + if (dir == null) return false; + File[] files = dir.listFiles(); + if (files == null) return false; + for (File file : files) { + if (file.isDirectory()) { + if (dirContainsSharedObject(file)) return true; + } else if (isSharedObject(file.getName())) { + return true; + } + } + return false; + } + + private static boolean isSharedObject(String name) { + String lower = name.toLowerCase(Locale.ROOT); + return lower.endsWith(".so") || lower.contains(".so."); + } + + // Delete the .so payload a profile installs (its PE dlls are left in place). + public void removeAppliedUnixLibs(ContentProfile profile) { + if (profile == null || profile.fileList == null) return; + for (ContentProfile.ContentFile contentFile : profile.fileList) { + if (!isSharedObject(new File(contentFile.target).getName())) continue; + File targetFile = new File(getPathFromTemplate(contentFile.target)); + if (targetFile.exists() && targetFile.delete()) { + Log.i("ContentsManager", "UnixLibs: removed " + targetFile.getName()); + } + } + } + + // Copy a profile's .so payload into destDir (the active wine runtime's so_dir). + public void copyUnixLibsToDir(ContentProfile profile, File destDir) { + if (profile == null || profile.fileList == null) return; + for (ContentProfile.ContentFile contentFile : profile.fileList) { + String name = new File(contentFile.target).getName(); + if (!isSharedObject(name)) continue; + File sourceFile = new File(getInstallDir(context, profile), contentFile.source); + if (!sourceFile.exists()) continue; + File destFile = new File(destDir, name); + FileUtils.copy(sourceFile, destFile); + FileUtils.chmod(destFile, 0771); + } + } + + public void deleteUnixLibsFromDir(ContentProfile profile, File destDir) { + if (profile == null || profile.fileList == null) return; + for (ContentProfile.ContentFile contentFile : profile.fileList) { + String name = new File(contentFile.target).getName(); + if (!isSharedObject(name)) continue; + File destFile = new File(destDir, name); + if (destFile.exists()) destFile.delete(); + } + } + public boolean applyContent(ContentProfile profile) { if (profile.type != ContentProfile.ContentType.CONTENT_TYPE_WINE && profile.type != ContentProfile.ContentType.CONTENT_TYPE_PROTON) { @@ -760,7 +827,8 @@ public boolean applyContent(ContentProfile profile) { targetFile.delete(); FileUtils.copy(sourceFile, targetFile); - if (profile.type == ContentProfile.ContentType.CONTENT_TYPE_BOX64) { + if (profile.type == ContentProfile.ContentType.CONTENT_TYPE_BOX64 + || isSharedObject(targetFile.getName())) { FileUtils.chmod(targetFile, 0771); } } diff --git a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java index 58ac5f6ae..2381fac6a 100644 --- a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java +++ b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java @@ -55,6 +55,7 @@ public class GuestProgramLauncherComponent extends EnvironmentComponent { private final Shortcut shortcut; private File workingDir; private Runnable preUnpackCallback; + private boolean fexUnixLibsActive = false; public static File ensureImageFsNativeLibrary( Context context, ImageFs imageFs, String libraryName) { @@ -371,11 +372,13 @@ private void extractEmulatorsDlls() { if (wowbox64Version == null) wowbox64Version = ""; if (fexcoreVersion == null) fexcoreVersion = ""; + boolean unixLibsPref = container.isUseUnixLibs(); if (shortcut != null) { emulator = shortcut.getSettingExtra("emulator", emulator); emulator64 = shortcut.getSettingExtra("emulator64", emulator64); wowbox64Version = shortcut.getSettingExtra("box64Version", wowbox64Version); fexcoreVersion = shortcut.getSettingExtra("fexcoreVersion", fexcoreVersion); + unixLibsPref = "1".equals(shortcut.getSettingExtra("useUnixLibs", unixLibsPref ? "1" : "0")); } boolean usesWowbox64 = emulator.equalsIgnoreCase("wowbox64"); @@ -384,6 +387,9 @@ private void extractEmulatorsDlls() { || emulator64.equalsIgnoreCase("fexcore") || !usesWowbox64; + fexUnixLibsActive = + usesFexcore && unixLibsPref && contentsManager.fexcoreVersionHasUnixLibs(fexcoreVersion); + Log.i( "GuestProgramLauncherComponent", "Launch runtime selected: emulator=" @@ -439,30 +445,45 @@ private void extractEmulatorsDlls() { "WowBox64 already loaded for launch: version=" + wowbox64Version); } - if (usesFexcore - && (fexcoreDllsMissing || !fexcoreVersion.equals(container.getExtra("fexcoreVersion")))) { - if (fexcoreVersion.isEmpty()) { - Log.w("GuestProgramLauncherComponent", "No FEXCore version selected; skipping content extraction"); - } else { - ContentProfile profile = contentsManager.getProfileByEntryName("fexcore-" + fexcoreVersion); - if (profile != null) { + if (usesFexcore) { + ContentProfile profile = + fexcoreVersion.isEmpty() ? null : contentsManager.getProfileByEntryName("fexcore-" + fexcoreVersion); + String wantMode = fexUnixLibsActive ? "unixlibs" : "dll"; + boolean modeChanged = !wantMode.equals(container.getExtra("fexcoreMode")); + boolean versionChanged = !fexcoreVersion.equals(container.getExtra("fexcoreVersion")); + boolean needsExtraction = + versionChanged || modeChanged || (!fexUnixLibsActive && fexcoreDllsMissing); + + if (needsExtraction) { + if (fexcoreVersion.isEmpty()) { + Log.w("GuestProgramLauncherComponent", "No FEXCore version selected; skipping content extraction"); + } else if (profile != null) { Log.i( "GuestProgramLauncherComponent", - "Loading FEXCore content profile: version=" + fexcoreVersion); + "Loading FEXCore content profile: version=" + fexcoreVersion + " mode=" + wantMode); contentsManager.applyContent(profile); + if (!fexUnixLibsActive) contentsManager.removeAppliedUnixLibs(profile); } else { Log.w( "GuestProgramLauncherComponent", "FEXCore content profile not installed; no bundled FEXCore archive will be loaded: version=" + fexcoreVersion); } + container.putExtra("fexcoreVersion", fexcoreVersion); + container.putExtra("fexcoreMode", wantMode); + containerDataChanged = true; + } else { + Log.i( + "GuestProgramLauncherComponent", + "FEXCore already loaded for launch: version=" + fexcoreVersion + " mode=" + wantMode); + } + + // Sync the native .so with the active wine runtime's so_dir per the toggle. + if (profile != null) { + File wineUnixDir = new File(environment.getImageFs().getWinePath(), "lib/wine/aarch64-unix"); + if (fexUnixLibsActive) contentsManager.copyUnixLibsToDir(profile, wineUnixDir); + else contentsManager.deleteUnixLibsFromDir(profile, wineUnixDir); } - container.putExtra("fexcoreVersion", fexcoreVersion); - containerDataChanged = true; - } else if (usesFexcore) { - Log.i( - "GuestProgramLauncherComponent", - "FEXCore already loaded for launch: version=" + fexcoreVersion); } if (containerDataChanged) container.saveData(); } diff --git a/tools/build_fex_unixlibs_wcp.sh b/tools/build_fex_unixlibs_wcp.sh new file mode 100755 index 000000000..70fe4c041 --- /dev/null +++ b/tools/build_fex_unixlibs_wcp.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Package a FEX UnixLibs component (.wcp) for WinNative. +# +# Payload layout (top dir -> WinNative content-path template; sub-paths are preserved): +# lib/wine/aarch64-unix/.so -> ${libdir}/wine/aarch64-unix/.so (FEX host UnixLibs) +# system32/ -> ${system32}/ (FEX PE dlls) +# syswow64/ -> ${syswow64}/ +# bin/ -> ${bindir}/ +# +# A .wcp is an xz-compressed tar of profile.json + the payload tree. +# +# Usage: +# tools/build_fex_unixlibs_wcp.sh -v -c -p -o +set -euo pipefail + +VERSION_NAME=""; VERSION_CODE="0"; PAYLOAD=""; OUT="" +while getopts "v:c:p:o:" opt; do + case "$opt" in + v) VERSION_NAME="$OPTARG" ;; + c) VERSION_CODE="$OPTARG" ;; + p) PAYLOAD="$OPTARG" ;; + o) OUT="$OPTARG" ;; + *) echo "bad option" >&2; exit 2 ;; + esac +done + +[ -n "$VERSION_NAME" ] && [ -n "$PAYLOAD" ] && [ -n "$OUT" ] || { + echo "usage: $0 -v -c -p -o " >&2; exit 2; } +[ -d "$PAYLOAD" ] || { echo "payload dir not found: $PAYLOAD" >&2; exit 1; } + +STAGE="$(mktemp -d)" +trap 'rm -rf "$STAGE"' EXIT +cp -a "$PAYLOAD/." "$STAGE/" + +python3 - "$STAGE" "$VERSION_NAME" "$VERSION_CODE" <<'PY' +import json, os, sys +stage, vname, vcode = sys.argv[1], sys.argv[2], int(sys.argv[3]) +tpl = {"lib": "${libdir}", "system32": "${system32}", "syswow64": "${syswow64}", "bin": "${bindir}"} +files = [] +for root, _, names in os.walk(stage): + for n in sorted(names): + rel = os.path.relpath(os.path.join(root, n), stage) + parts = rel.split(os.sep, 1) + top = parts[0] + if top not in tpl or len(parts) < 2: + continue + files.append({"source": rel, "target": f"{tpl[top]}/{parts[1]}"}) +files.sort(key=lambda f: f["source"]) +profile = {"type": "FEXCore", "versionName": vname, "versionCode": vcode, + "description": vname, "files": files} +with open(os.path.join(stage, "profile.json"), "w") as f: + json.dump(profile, f, indent=2) +print(json.dumps(profile, indent=2)) +PY + +tar -C "$STAGE" -cJf "$OUT" . +echo "wrote $OUT ($(du -h "$OUT" | cut -f1))" From 380b537c39d773e707ad93ac0617c41f9bd303e7 Mon Sep 17 00:00:00 2001 From: MaxsTechReview Date: Fri, 3 Jul 2026 15:46:59 -0400 Subject: [PATCH 2/4] Delete tools/build_fex_unixlibs_wcp.sh --- tools/build_fex_unixlibs_wcp.sh | 57 --------------------------------- 1 file changed, 57 deletions(-) delete mode 100755 tools/build_fex_unixlibs_wcp.sh diff --git a/tools/build_fex_unixlibs_wcp.sh b/tools/build_fex_unixlibs_wcp.sh deleted file mode 100755 index 70fe4c041..000000000 --- a/tools/build_fex_unixlibs_wcp.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash -# Package a FEX UnixLibs component (.wcp) for WinNative. -# -# Payload layout (top dir -> WinNative content-path template; sub-paths are preserved): -# lib/wine/aarch64-unix/.so -> ${libdir}/wine/aarch64-unix/.so (FEX host UnixLibs) -# system32/ -> ${system32}/ (FEX PE dlls) -# syswow64/ -> ${syswow64}/ -# bin/ -> ${bindir}/ -# -# A .wcp is an xz-compressed tar of profile.json + the payload tree. -# -# Usage: -# tools/build_fex_unixlibs_wcp.sh -v -c -p -o -set -euo pipefail - -VERSION_NAME=""; VERSION_CODE="0"; PAYLOAD=""; OUT="" -while getopts "v:c:p:o:" opt; do - case "$opt" in - v) VERSION_NAME="$OPTARG" ;; - c) VERSION_CODE="$OPTARG" ;; - p) PAYLOAD="$OPTARG" ;; - o) OUT="$OPTARG" ;; - *) echo "bad option" >&2; exit 2 ;; - esac -done - -[ -n "$VERSION_NAME" ] && [ -n "$PAYLOAD" ] && [ -n "$OUT" ] || { - echo "usage: $0 -v -c -p -o " >&2; exit 2; } -[ -d "$PAYLOAD" ] || { echo "payload dir not found: $PAYLOAD" >&2; exit 1; } - -STAGE="$(mktemp -d)" -trap 'rm -rf "$STAGE"' EXIT -cp -a "$PAYLOAD/." "$STAGE/" - -python3 - "$STAGE" "$VERSION_NAME" "$VERSION_CODE" <<'PY' -import json, os, sys -stage, vname, vcode = sys.argv[1], sys.argv[2], int(sys.argv[3]) -tpl = {"lib": "${libdir}", "system32": "${system32}", "syswow64": "${syswow64}", "bin": "${bindir}"} -files = [] -for root, _, names in os.walk(stage): - for n in sorted(names): - rel = os.path.relpath(os.path.join(root, n), stage) - parts = rel.split(os.sep, 1) - top = parts[0] - if top not in tpl or len(parts) < 2: - continue - files.append({"source": rel, "target": f"{tpl[top]}/{parts[1]}"}) -files.sort(key=lambda f: f["source"]) -profile = {"type": "FEXCore", "versionName": vname, "versionCode": vcode, - "description": vname, "files": files} -with open(os.path.join(stage, "profile.json"), "w") as f: - json.dump(profile, f, indent=2) -print(json.dumps(profile, indent=2)) -PY - -tar -C "$STAGE" -cJf "$OUT" . -echo "wrote $OUT ($(du -h "$OUT" | cut -f1))" From cab5ab5fbdb88d39fdcc04c7e0af2b7eed014917 Mon Sep 17 00:00:00 2001 From: maxjivi05 Date: Sun, 5 Jul 2026 13:00:09 -0400 Subject: [PATCH 3/4] Remove explanatory comments from UnixLibs code --- UNIXLIBS_README.md | 96 +++++++++++++++++++ app/src/main/feature/library/GameSettings.kt | 1 - .../main/runtime/content/ContentsManager.java | 3 - .../GuestProgramLauncherComponent.java | 1 - 4 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 UNIXLIBS_README.md diff --git a/UNIXLIBS_README.md b/UNIXLIBS_README.md new file mode 100644 index 000000000..c43c0045c --- /dev/null +++ b/UNIXLIBS_README.md @@ -0,0 +1,96 @@ +# FEX UnixLibs for WinNative — What It Is & How It Works + +A short guide to the new **UnixLibs** feature: what changed, what gets installed, +and how the new `.so` files get loaded. + +--- + +## The one-paragraph version + +FEX (the x86→ARM translator used by ARM64EC containers) always ran as **Windows +DLLs living inside the container's C: drive**. Those DLLs are "boxed" inside +Wine's Windows world, so they can't talk to the Android kernel directly. +**UnixLibs** adds a small **native `.so`** that runs on the *Android side* and +gives FEX a direct line to the kernel — unlocking hardware-assisted x86 memory +ordering (TSO) and other speedups. Nothing about how you use containers changes; +it just runs faster where the hardware supports it. + +--- + +## How it works (simple) + +1. The FEX DLLs (`libwow64fex.dll` / `libarm64ecfex.dll`) stay in `system32` — + they're still the entry point Wine loads. +2. On launch, each FEX DLL asks Wine: *"load my native helper by name."* +3. Wine finds the matching **`.so`** (`libwow64fex.so` / `libarm64ecfex.so`) in + the running Wine/Proton's Unix library folder and loads it. +4. FEX now has a bridge from the boxed Windows side out to the Android kernel and + uses it for the fast paths (see performance note at the bottom). + +The DLL and the `.so` are a **pair** — the DLL loads the `.so`; they work +*together*, the `.so` does not replace the DLL. + +--- + +## What's different from the old setup + +| Piece | Old | New (UnixLibs) | +|---|---|---| +| **Proton** | Wine couldn't load a `.so` helper by name, so FEX silently fell back to the slower in-DLL path. | Proton's `ntdll` now supports **load-unixlib-by-name**, so it can find and load the `.so`. | +| **FEX version** | The FEX `.wcp` shipped **only** the two PE `.dll` files. | The FEX `.wcp` ships the `.dll` files **plus** the matching native **`.so`** files (built for Android/Bionic). | +| **WinNative APK** | Only copied the FEX `.dll` into `system32`. | Adds a **"Use UnixLibs" toggle** and, when on, copies the `.so` into the active Proton's Unix lib folder so Wine can find it. | + +--- + +## What gets installed now + +Three matching pieces are needed (all ARM64EC only): + +1. **New Proton** — e.g. `Proton-11-B5-Arm64EC-Steam-UnixLibs` (its `ntdll` + knows how to load the `.so`). +2. **New FEX component** — e.g. `FEX-2607-UnixLibs`, which contains: + - `libwow64fex.dll`, `libarm64ecfex.dll` → copied into `system32` (as before) + - `libwow64fex.so`, `libarm64ecfex.so` → the new native helpers +3. **New WinNative APK** — with the **Use UnixLibs** toggle and the wiring that + places the `.so` where Wine looks for it. + +Install the Proton and FEX components from **Components** like normal, pick them +in the container/shortcut, and make sure **Use UnixLibs** is on (it turns on +automatically when the selected FEX version includes `.so` files). + +--- + +## How the `.so` files get loaded + +- When you launch a container, WinNative copies the FEX `.so` files into the + **active Proton's** `lib/wine/aarch64-unix/` folder (the exact place Wine + searches). This happens per-launch, so it follows whichever Proton you pick. +- Wine's `ntdll` then loads the `.so` by name when FEX asks for it. +- Turning the toggle **off** removes the `.so` again, so FEX goes back to the + plain DLL path. + +**How to confirm it worked:** enable FEX/emulator logs, launch a game, and look +in the FEX log for: + +``` +FEX: Loaded FEXUnixLib +``` + +If you see that (once for each backend), the native helper is active. If it's +missing, FEX is running the old DLL-only path. + +--- + +## What you might feel (performance) + +FEX already translated x86 to native ARM either way — UnixLibs doesn't make +translation "native," it gives FEX **kernel access**. The main win is +**hardware TSO memory ordering**: FEX can drop the expensive software memory +barriers it normally needs for correct x86 behavior and let the CPU enforce it +instead — *fast and correct at the same time* (the same trick Apple's Rosetta +uses). Expect the biggest gains in memory- and multithread-heavy games, plus +smoother frame pacing. + +Caveat: this is **hardware/kernel dependent**. On chips/kernels that expose the +memory-model toggle you'll feel it; on ones that don't, the `.so` still loads but +that speedup isn't available — no gain, but no regression either. diff --git a/app/src/main/feature/library/GameSettings.kt b/app/src/main/feature/library/GameSettings.kt index 08479e6c9..73152df97 100644 --- a/app/src/main/feature/library/GameSettings.kt +++ b/app/src/main/feature/library/GameSettings.kt @@ -507,7 +507,6 @@ class GameSettingsStateHolder { val selectedFexcoreVersion = mutableIntStateOf(0) val fexcorePresetEntries = mutableStateOf>(emptyList()) val selectedFexcorePreset = mutableIntStateOf(0) - // Per-version flag: true when that FEX version ships native .so UnixLibs. val fexcoreUnixLibsFlags = mutableStateOf>(emptyList()) val useUnixLibs = mutableStateOf(true) diff --git a/app/src/main/runtime/content/ContentsManager.java b/app/src/main/runtime/content/ContentsManager.java index 0fa2e6fbc..c9d22b057 100644 --- a/app/src/main/runtime/content/ContentsManager.java +++ b/app/src/main/runtime/content/ContentsManager.java @@ -751,7 +751,6 @@ public ContentProfile getProfileByEntryName(String entryName) { return null; } - // A FEX version exposes native UnixLibs when its install dir ships any .so payload. public boolean profileHasUnixLibs(ContentProfile profile) { if (profile == null) return false; return dirContainsSharedObject(getInstallDir(context, profile)); @@ -781,7 +780,6 @@ private static boolean isSharedObject(String name) { return lower.endsWith(".so") || lower.contains(".so."); } - // Delete the .so payload a profile installs (its PE dlls are left in place). public void removeAppliedUnixLibs(ContentProfile profile) { if (profile == null || profile.fileList == null) return; for (ContentProfile.ContentFile contentFile : profile.fileList) { @@ -793,7 +791,6 @@ public void removeAppliedUnixLibs(ContentProfile profile) { } } - // Copy a profile's .so payload into destDir (the active wine runtime's so_dir). public void copyUnixLibsToDir(ContentProfile profile, File destDir) { if (profile == null || profile.fileList == null) return; for (ContentProfile.ContentFile contentFile : profile.fileList) { diff --git a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java index 2381fac6a..9a79fffe9 100644 --- a/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java +++ b/app/src/main/runtime/display/environment/components/GuestProgramLauncherComponent.java @@ -478,7 +478,6 @@ private void extractEmulatorsDlls() { "FEXCore already loaded for launch: version=" + fexcoreVersion + " mode=" + wantMode); } - // Sync the native .so with the active wine runtime's so_dir per the toggle. if (profile != null) { File wineUnixDir = new File(environment.getImageFs().getWinePath(), "lib/wine/aarch64-unix"); if (fexUnixLibsActive) contentsManager.copyUnixLibsToDir(profile, wineUnixDir); From e592174af8a3c27e8a013c8acd328c3d2bf86a8a Mon Sep 17 00:00:00 2001 From: maxjivi05 Date: Mon, 6 Jul 2026 11:25:18 -0400 Subject: [PATCH 4/4] UnixLibs toggle: CPU-chip button by FEXCore Version, name-based compat --- app/src/main/feature/library/GameSettings.kt | 106 ++++++++++++++---- .../ContainerSettingsComposeDialog.kt | 1 - .../ShortcutSettingsComposeDialog.kt | 1 - 3 files changed, 84 insertions(+), 24 deletions(-) diff --git a/app/src/main/feature/library/GameSettings.kt b/app/src/main/feature/library/GameSettings.kt index 73152df97..c1b7d0f05 100644 --- a/app/src/main/feature/library/GameSettings.kt +++ b/app/src/main/feature/library/GameSettings.kt @@ -507,7 +507,6 @@ class GameSettingsStateHolder { val selectedFexcoreVersion = mutableIntStateOf(0) val fexcorePresetEntries = mutableStateOf>(emptyList()) val selectedFexcorePreset = mutableIntStateOf(0) - val fexcoreUnixLibsFlags = mutableStateOf>(emptyList()) val useUnixLibs = mutableStateOf(true) // Advanced - System @@ -3556,17 +3555,13 @@ private fun AdvancedSection( // FEXCore — hidden when FEXCore isn't explicitly in either slot. if (state.showFexcoreFrame.value) { - val fexUnixCapable = state.fexcoreUnixLibsFlags.value - .getOrNull(state.selectedFexcoreVersion.intValue) ?: false - SettingGroup { - SettingSwitch( - label = stringResource(R.string.use_unix_libs), - checked = state.useUnixLibs.value && fexUnixCapable, - onCheckedChange = { state.useUnixLibs.value = it }, - enabled = fexUnixCapable - ) - } - Spacer(Modifier.height(SettingSectionGap)) + val fexVersionUnix = state.fexcoreVersionEntries.value + .getOrNull(state.selectedFexcoreVersion.intValue) + ?.contains("unix", ignoreCase = true) == true + val protonUnix = state.wineVersionDisplay.value.contains("unix", ignoreCase = true) || + state.wineVersionEntries.value.getOrNull(state.selectedWineVersion.intValue) + ?.contains("unix", ignoreCase = true) == true + val unixCompatible = fexVersionUnix && protonUnix val fexcoreUsage = emulatorUsageLabel(state, setOf("fexcore")) EmulatorSectionHeader(stringResource(R.string.container_fexcore_config), fexcoreUsage) @@ -3578,7 +3573,14 @@ private fun AdvancedSection( label = stringResource(R.string.container_fexcore_version), entries = state.fexcoreVersionEntries.value, selectedIndex = state.selectedFexcoreVersion.intValue, - onSelected = { state.selectedFexcoreVersion.intValue = it } + onSelected = { state.selectedFexcoreVersion.intValue = it }, + labelTrailing = { + UnixLibsChip( + compatible = unixCompatible, + on = state.useUnixLibs.value, + onClick = { state.useUnixLibs.value = !state.useUnixLibs.value } + ) + } ) } Box(Modifier.weight(1f)) { @@ -3839,6 +3841,48 @@ private fun CpuChip( } } +@Composable +private fun UnixLibsChip( + compatible: Boolean, + on: Boolean, + onClick: () -> Unit +) { + val active = compatible && on + val bgColor = if (active) AccentBlue.copy(alpha = 0.15f) else ChipSurface + val borderColor = if (active) AccentBlue.copy(alpha = 0.4f) else ChipBorder + val textColor = if (active) AccentBlue else TextDim + + Box( + modifier = Modifier + .clip(RoundedCornerShape(6.dp)) + .background(bgColor) + .border(1.dp, borderColor, RoundedCornerShape(6.dp)) + .then( + if (compatible) { + Modifier + .clickable(onClick = onClick) + .paneNavItem( + cornerRadius = 6.dp, + onActivate = onClick, + highlightColor = NavHighlight, + tapToSelect = true, + ) + } else { + Modifier + } + ) + .padding(horizontal = 10.dp, vertical = 2.dp), + contentAlignment = Alignment.Center + ) { + Text( + "UnixLibs", + color = textColor, + fontSize = SettingLabelSize, + fontWeight = if (active) FontWeight.SemiBold else FontWeight.Normal + ) + } +} + @Composable private fun HtmlText( @@ -3971,7 +4015,8 @@ private fun SettingDropdown( selectedIndex: Int, onSelected: (Int) -> Unit, enabled: Boolean = true, - disabledAlpha: Float = 0.4f + disabledAlpha: Float = 0.4f, + labelTrailing: (@Composable () -> Unit)? = null ) { var expanded by remember { mutableStateOf(false) } val menuOffset = rememberSmartDropdownOffset() @@ -3992,14 +4037,31 @@ private fun SettingDropdown( } Column(modifier = Modifier.fillMaxWidth().alpha(alpha)) { - Text( - label, - color = TextSecondary, - fontSize = SettingLabelSize, - fontWeight = FontWeight.Medium, - letterSpacing = 0.3.sp, - modifier = Modifier.padding(bottom = SettingTightGap) - ) + if (labelTrailing != null) { + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = SettingTightGap), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + label, + color = TextSecondary, + fontSize = SettingLabelSize, + fontWeight = FontWeight.Medium, + letterSpacing = 0.3.sp + ) + Spacer(Modifier.weight(1f)) + labelTrailing() + } + } else { + Text( + label, + color = TextSecondary, + fontSize = SettingLabelSize, + fontWeight = FontWeight.Medium, + letterSpacing = 0.3.sp, + modifier = Modifier.padding(bottom = SettingTightGap) + ) + } Box { Row( modifier = Modifier diff --git a/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt b/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt index 0192754bc..310795205 100644 --- a/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt +++ b/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt @@ -992,7 +992,6 @@ class ContainerSettingsComposeDialog @JvmOverloads constructor( if (firstDash >= 0) items.add(entryName.substring(firstDash + 1)) } state.fexcoreVersionEntries.value = items - state.fexcoreUnixLibsFlags.value = items.map { contentsManager.fexcoreVersionHasUnixLibs(it) } val saved = container?.getFEXCoreVersion() ?: "" selectByValue(items, saved, state.selectedFexcoreVersion) } diff --git a/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt b/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt index d685cf306..393f670b2 100644 --- a/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt +++ b/app/src/main/feature/shortcuts/ShortcutSettingsComposeDialog.kt @@ -828,7 +828,6 @@ class ShortcutSettingsComposeDialog private constructor( } state.fexcoreVersionEntries.value = items - state.fexcoreUnixLibsFlags.value = items.map { contentsManager.fexcoreVersionHasUnixLibs(it) } val savedVersion = if (shouldUseShortcutOverrides(container)) getShortcutSetting("fexcoreVersion", container.getFEXCoreVersion()) else