diff --git a/app/src/main/assets/dxwrapper/dxvk-1.11.1-sarek.tzst b/app/src/main/assets/dxwrapper/dxvk-1.11.1-sarek.tzst new file mode 100644 index 000000000..898fffddd Binary files /dev/null and b/app/src/main/assets/dxwrapper/dxvk-1.11.1-sarek.tzst differ diff --git a/app/src/main/assets/graphics_driver/adrenotools-Turnip_Gen8_V23.tzst b/app/src/main/assets/graphics_driver/adrenotools-Turnip_Gen8_V23.tzst new file mode 100644 index 000000000..a4a1682bf Binary files /dev/null and b/app/src/main/assets/graphics_driver/adrenotools-Turnip_Gen8_V23.tzst differ diff --git a/app/src/main/assets/graphics_driver/turnip25.3.0_R11.tzst b/app/src/main/assets/graphics_driver/adrenotools-turnip25.3.0_R11.tzst similarity index 100% rename from app/src/main/assets/graphics_driver/turnip25.3.0_R11.tzst rename to app/src/main/assets/graphics_driver/adrenotools-turnip25.3.0_R11.tzst diff --git a/app/src/main/assets/graphics_driver/turnip26.0.0_R4.tzst b/app/src/main/assets/graphics_driver/adrenotools-turnip26.0.0_R4.tzst similarity index 100% rename from app/src/main/assets/graphics_driver/turnip26.0.0_R4.tzst rename to app/src/main/assets/graphics_driver/adrenotools-turnip26.0.0_R4.tzst diff --git a/app/src/main/assets/graphics_driver/turnip26.0.0_R8.tzst b/app/src/main/assets/graphics_driver/adrenotools-turnip26.0.0_R8.tzst similarity index 100% rename from app/src/main/assets/graphics_driver/turnip26.0.0_R8.tzst rename to app/src/main/assets/graphics_driver/adrenotools-turnip26.0.0_R8.tzst diff --git a/app/src/main/assets/graphics_driver/extra_libs.tzst b/app/src/main/assets/graphics_driver/extra_libs.tzst index eb93a15a3..37867cf05 100644 Binary files a/app/src/main/assets/graphics_driver/extra_libs.tzst and b/app/src/main/assets/graphics_driver/extra_libs.tzst differ diff --git a/app/src/main/assets/graphics_driver/wrapper.tzst b/app/src/main/assets/graphics_driver/wrapper.tzst index b20e311f5..f5b18a73f 100644 Binary files a/app/src/main/assets/graphics_driver/wrapper.tzst and b/app/src/main/assets/graphics_driver/wrapper.tzst differ diff --git a/app/src/main/assets/graphics_driver/zink_dlls.tzst b/app/src/main/assets/graphics_driver/zink_dlls.tzst new file mode 100644 index 000000000..e93fefbb0 Binary files /dev/null and b/app/src/main/assets/graphics_driver/zink_dlls.tzst differ diff --git a/app/src/main/cpp/extras/vulkan.c b/app/src/main/cpp/extras/vulkan.c index 31b77268b..cb8ab511e 100644 --- a/app/src/main/cpp/extras/vulkan.c +++ b/app/src/main/cpp/extras/vulkan.c @@ -271,6 +271,39 @@ Java_com_winlator_core_GPUInformation_getVulkanVersion(JNIEnv *env, jclass obj, return versionString; } +JNIEXPORT jint JNICALL +Java_com_winlator_core_GPUInformation_getVendorID(JNIEnv *env, jclass obj, jstring driverName, jobject context) { + VkPhysicalDeviceProperties props = {}; + uint32_t vendorID = 0; + + if (create_instance(driverName, env, context) != VK_SUCCESS) { + printf("Failed to create instance"); + goto cleanup; + } + + if (enumerate_physical_devices() != VK_SUCCESS) { + printf("Failed to query physical devices"); + goto cleanup; + } + + getPhysicalDeviceProperties(physicalDevice, &props); + vendorID = props.vendorID; + +cleanup: + if (destroyInstance && instance != VK_NULL_HANDLE) { + destroyInstance(instance, NULL); + instance = VK_NULL_HANDLE; + } + physicalDevice = VK_NULL_HANDLE; + + if (vulkan_handle) { + dlclose(vulkan_handle); + vulkan_handle = NULL; + } + + return vendorID; +} + JNIEXPORT jstring JNICALL Java_com_winlator_core_GPUInformation_getRenderer(JNIEnv *env, jclass obj, jstring driverName, jobject context) { VkPhysicalDeviceProperties props = {}; diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/AdvancedTab.kt b/app/src/main/java/app/gamenative/ui/component/dialog/AdvancedTab.kt new file mode 100644 index 000000000..dd2a50436 --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/component/dialog/AdvancedTab.kt @@ -0,0 +1,40 @@ +package app.gamenative.ui.component.dialog + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import app.gamenative.R +import app.gamenative.ui.component.settings.SettingsCPUList +import app.gamenative.ui.component.settings.SettingsListDropdown +import app.gamenative.ui.theme.settingsTileColors +import com.alorma.compose.settings.ui.SettingsGroup + +@Composable +fun AdvancedTabContent(state: ContainerConfigState) { + val config = state.config.value + SettingsGroup() { + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.startup_selection)) }, + value = config.startupSelection.toInt().takeIf { it in state.getStartupSelectionOptions().indices } ?: 1, + items = state.getStartupSelectionOptions(), + onItemSelected = { + state.config.value = config.copy(startupSelection = it.toByte()) + }, + ) + SettingsCPUList( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.processor_affinity)) }, + value = config.cpuList, + onValueChange = { + state.config.value = config.copy(cpuList = it) + }, + ) + SettingsCPUList( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.processor_affinity_32bit)) }, + value = config.cpuListWoW64, + onValueChange = { state.config.value = config.copy(cpuListWoW64 = it) }, + ) + } +} diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt index 0c0a25453..14c32a6d7 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt @@ -76,9 +76,7 @@ import app.gamenative.R import app.gamenative.ui.component.dialog.state.MessageDialogState import app.gamenative.ui.component.settings.SettingsCPUList import app.gamenative.ui.component.settings.SettingsCenteredLabel -import app.gamenative.ui.component.settings.SettingsEnvVars import app.gamenative.ui.component.settings.SettingsListDropdown -import app.gamenative.ui.component.settings.SettingsMultiListDropdown import app.gamenative.ui.components.rememberCustomGameFolderPicker import app.gamenative.ui.components.requestPermissionsForPath import app.gamenative.ui.theme.PluviaTheme @@ -103,9 +101,7 @@ import com.winlator.container.Container import com.winlator.container.ContainerData import com.winlator.core.KeyValueSet import com.winlator.core.StringUtils -import com.winlator.core.envvars.EnvVarInfo import com.winlator.core.envvars.EnvVars -import com.winlator.core.envvars.EnvVarSelectionType import com.winlator.core.DefaultVersion import com.winlator.core.GPUHelper import com.winlator.core.WineInfo @@ -125,7 +121,7 @@ import kotlin.math.roundToInt /** * Gets the component title for Win Components settings group. */ -private fun winComponentsItemTitleRes(string: String): Int { +internal fun winComponentsItemTitleRes(string: String): Int { return when (string) { "direct3d" -> R.string.direct3d "directsound" -> R.string.directsound @@ -162,13 +158,15 @@ fun ContainerConfigDialog( } } - var config by rememberSaveable(stateSaver = ContainerData.Saver) { + val configState = rememberSaveable(stateSaver = ContainerData.Saver) { mutableStateOf(initialConfig) } + var config by configState val screenSizes = stringArrayResource(R.array.screen_size_entries).toList() val baseGraphicsDrivers = stringArrayResource(R.array.graphics_driver_entries).toList() - var graphicsDrivers by remember { mutableStateOf(baseGraphicsDrivers.toMutableList()) } + val graphicsDriversRef = remember { mutableStateOf(baseGraphicsDrivers.toMutableList()) } + var graphicsDrivers by graphicsDriversRef val dxWrappers = stringArrayResource(R.array.dxwrapper_entries).toList() // Start with defaults from resources val dxvkVersionsBase = stringArrayResource(R.array.dxvk_version_entries).toList() @@ -212,21 +210,28 @@ fun ContainerConfigDialog( val containerVariants = stringArrayResource(R.array.container_variant_entries).toList() val bionicWineEntriesBase = stringArrayResource(R.array.bionic_wine_entries).toList() val glibcWineEntriesBase = stringArrayResource(R.array.glibc_wine_entries).toList() - var bionicWineEntries by remember { mutableStateOf(bionicWineEntriesBase) } - var glibcWineEntries by remember { mutableStateOf(glibcWineEntriesBase) } + val bionicWineEntriesRef = remember { mutableStateOf(bionicWineEntriesBase) } + var bionicWineEntries by bionicWineEntriesRef + val glibcWineEntriesRef = remember { mutableStateOf(glibcWineEntriesBase) } + var glibcWineEntries by glibcWineEntriesRef val emulatorEntries = stringArrayResource(R.array.emulator_entries).toList() val bionicGraphicsDrivers = stringArrayResource(R.array.bionic_graphics_driver_entries).toList() val baseWrapperVersions = stringArrayResource(R.array.wrapper_graphics_driver_version_entries).toList() - var wrapperVersions by remember { mutableStateOf(baseWrapperVersions) } - var dxvkVersionsAll by remember { mutableStateOf(dxvkVersionsBase) } - var componentAvailability by remember { mutableStateOf(null) } + val wrapperVersionsRef = remember { mutableStateOf(baseWrapperVersions) } + var wrapperVersions by wrapperVersionsRef + val dxvkVersionsAllRef = remember { mutableStateOf(dxvkVersionsBase) } + var dxvkVersionsAll by dxvkVersionsAllRef + val componentAvailabilityRef = remember { mutableStateOf(null) } + var componentAvailability by componentAvailabilityRef var manifestInstallInProgress by remember { mutableStateOf(false) } var showManifestDownloadDialog by remember { mutableStateOf(false) } var manifestDownloadProgress by remember { mutableStateOf(-1f) } var manifestDownloadLabel by remember { mutableStateOf("") } var versionsLoaded by remember { mutableStateOf(false) } - var showCustomResolutionDialog by remember { mutableStateOf(false) } - var customResolutionValidationError by remember { mutableStateOf(null) } + val showCustomResolutionDialogRef = remember { mutableStateOf(false) } + var showCustomResolutionDialog by showCustomResolutionDialogRef + val customResolutionValidationErrorRef = remember { mutableStateOf(null) } + var customResolutionValidationError by customResolutionValidationErrorRef LaunchedEffect(visible) { if (visible) { @@ -333,28 +338,28 @@ fun ContainerConfigDialog( } val dxvkManifestById = remember(manifestDxvk) { - manifestDxvk.associateBy { StringUtils.parseIdentifier(it.id) } + manifestDxvk.associateBy { it.id } } val vkd3dManifestById = remember(manifestVkd3d) { - manifestVkd3d.associateBy { StringUtils.parseIdentifier(it.id) } + manifestVkd3d.associateBy { it.id } } val box64ManifestById = remember(manifestBox64) { - manifestBox64.associateBy { StringUtils.parseIdentifier(it.id) } + manifestBox64.associateBy { it.id } } val wowBox64ManifestById = remember(manifestWowBox64) { - manifestWowBox64.associateBy { StringUtils.parseIdentifier(it.id) } + manifestWowBox64.associateBy { it.id } } val fexcoreManifestById = remember(manifestFexcore) { - manifestFexcore.associateBy { StringUtils.parseIdentifier(it.id) } + manifestFexcore.associateBy { it.id } } val wrapperManifestById = remember(manifestDrivers) { - manifestDrivers.associateBy { StringUtils.parseIdentifier(it.id) } + manifestDrivers.associateBy { it.id } } val bionicWineManifestById = remember(bionicWineManifest) { - bionicWineManifest.associateBy { StringUtils.parseIdentifier(it.id) } + bionicWineManifest.associateBy { it.id } } val glibcWineManifestById = remember(glibcWineManifest) { - glibcWineManifest.associateBy { StringUtils.parseIdentifier(it.id) } + glibcWineManifest.associateBy { it.id } } suspend fun refreshInstalledLists() { @@ -439,10 +444,13 @@ fun ContainerConfigDialog( onInstalled = onInstalled, ) // Vortek/Adreno graphics driver config (vkMaxVersion, imageCacheSize, exposedDeviceExtensions) - var vkMaxVersionIndex by rememberSaveable { mutableIntStateOf(3) } - var imageCacheIndex by rememberSaveable { mutableIntStateOf(2) } + val vkMaxVersionIndexRef = rememberSaveable { mutableIntStateOf(3) } + var vkMaxVersionIndex by vkMaxVersionIndexRef + val imageCacheIndexRef = rememberSaveable { mutableIntStateOf(2) } + var imageCacheIndex by imageCacheIndexRef // Exposed device extensions selection indices; populated dynamically when UI opens - var exposedExtIndices by rememberSaveable { mutableStateOf(listOf()) } + val exposedExtIndicesRef = rememberSaveable { mutableStateOf(listOf()) } + var exposedExtIndices by exposedExtIndicesRef val inspectionMode = LocalInspectionMode.current val gpuExtensions = remember(inspectionMode) { if (inspectionMode) { @@ -478,8 +486,7 @@ fun ContainerConfigDialog( } // Emulator selections (shown for bionic variant): 64-bit and 32-bit - var emulator64Index by rememberSaveable { - // Default based on wine arch: x86_64 -> Box64 (index 1); arm64ec -> FEXCore (index 0) + val emulator64IndexRef = rememberSaveable { val idx = when { config.wineVersion.contains("x86_64", true) -> 1 config.wineVersion.contains("arm64ec", true) -> 0 @@ -487,11 +494,13 @@ fun ContainerConfigDialog( } mutableIntStateOf(idx) } - var emulator32Index by rememberSaveable { + var emulator64Index by emulator64IndexRef + val emulator32IndexRef = rememberSaveable { val current = config.emulator.ifEmpty { Container.DEFAULT_EMULATOR } val idx = emulatorEntries.indexOfFirst { it.equals(current, true) }.coerceAtLeast(0) mutableIntStateOf(idx) } + var emulator32Index by emulator32IndexRef // Keep emulator defaults in sync when wineVersion changes LaunchedEffect(config.wineVersion) { @@ -506,7 +515,8 @@ fun ContainerConfigDialog( } } // Max Device Memory (MB) for Vortek/Adreno - var maxDeviceMemoryIndex by rememberSaveable { mutableIntStateOf(4) } // default 4096 + val maxDeviceMemoryIndexRef = rememberSaveable { mutableIntStateOf(4) } // default 4096 + var maxDeviceMemoryIndex by maxDeviceMemoryIndexRef LaunchedEffect(config.graphicsDriverConfig) { val cfg = KeyValueSet(config.graphicsDriverConfig) val options = listOf("0", "512", "1024", "2048", "4096") @@ -516,28 +526,41 @@ fun ContainerConfigDialog( } // Bionic-specific state - var bionicDriverIndex by rememberSaveable { + val bionicDriverIndexRef = rememberSaveable { val idx = bionicGraphicsDrivers.indexOfFirst { StringUtils.parseIdentifier(it) == config.graphicsDriver } mutableIntStateOf(if (idx >= 0) idx else 0) } - var wrapperVersionIndex by rememberSaveable { mutableIntStateOf(0) } - var presentModeIndex by rememberSaveable { mutableIntStateOf(0) } - var resourceTypeIndex by rememberSaveable { mutableIntStateOf(0) } - var bcnEmulationIndex by rememberSaveable { mutableIntStateOf(0) } - var bcnEmulationTypeIndex by rememberSaveable { mutableIntStateOf(0) } - var bcnEmulationCacheEnabled by rememberSaveable { mutableStateOf(false) } - var disablePresentWaitChecked by rememberSaveable { mutableStateOf(false) } - var syncEveryFrameChecked by rememberSaveable { mutableStateOf(false) } - var sharpnessEffectIndex by rememberSaveable { + var bionicDriverIndex by bionicDriverIndexRef + val wrapperVersionIndexRef = rememberSaveable { mutableIntStateOf(0) } + var wrapperVersionIndex by wrapperVersionIndexRef + val presentModeIndexRef = rememberSaveable { mutableIntStateOf(0) } + var presentModeIndex by presentModeIndexRef + val resourceTypeIndexRef = rememberSaveable { mutableIntStateOf(0) } + var resourceTypeIndex by resourceTypeIndexRef + val bcnEmulationIndexRef = rememberSaveable { mutableIntStateOf(0) } + var bcnEmulationIndex by bcnEmulationIndexRef + val bcnEmulationTypeIndexRef = rememberSaveable { mutableIntStateOf(0) } + var bcnEmulationTypeIndex by bcnEmulationTypeIndexRef + val bcnEmulationCacheEnabledRef = rememberSaveable { mutableStateOf(false) } + var bcnEmulationCacheEnabled by bcnEmulationCacheEnabledRef + val disablePresentWaitCheckedRef = rememberSaveable { mutableStateOf(false) } + var disablePresentWaitChecked by disablePresentWaitCheckedRef + val syncEveryFrameCheckedRef = rememberSaveable { mutableStateOf(false) } + var syncEveryFrameChecked by syncEveryFrameCheckedRef + val sharpnessEffectIndexRef = rememberSaveable { val idx = sharpnessEffects.indexOfFirst { it.equals(config.sharpnessEffect, true) }.coerceAtLeast(0) mutableIntStateOf(idx) } - var sharpnessLevel by rememberSaveable { mutableIntStateOf(config.sharpnessLevel.coerceIn(0, 100)) } - var sharpnessDenoise by rememberSaveable { mutableIntStateOf(config.sharpnessDenoise.coerceIn(0, 100)) } - var adrenotoolsTurnipChecked by rememberSaveable { + var sharpnessEffectIndex by sharpnessEffectIndexRef + val sharpnessLevelRef = rememberSaveable { mutableIntStateOf(config.sharpnessLevel.coerceIn(0, 100)) } + var sharpnessLevel by sharpnessLevelRef + val sharpnessDenoiseRef = rememberSaveable { mutableIntStateOf(config.sharpnessDenoise.coerceIn(0, 100)) } + var sharpnessDenoise by sharpnessDenoiseRef + val adrenotoolsTurnipCheckedRef = rememberSaveable { val cfg = KeyValueSet(config.graphicsDriverConfig) mutableStateOf(cfg.get("adrenotoolsTurnip", "1") != "0") } + var adrenotoolsTurnipChecked by adrenotoolsTurnipCheckedRef LaunchedEffect(config.graphicsDriverConfig) { val cfg = KeyValueSet(config.graphicsDriverConfig) val presentMode = cfg.get("presentMode", "mailbox") @@ -582,30 +605,30 @@ fun ContainerConfigDialog( if (wrapperVersionIndex != newIdx) wrapperVersionIndex = newIdx } - var screenSizeIndex by rememberSaveable { + val screenSizeIndexRef = rememberSaveable { val searchIndex = screenSizes.indexOfFirst { it.contains(config.screenSize) } mutableIntStateOf(if (searchIndex > 0) searchIndex else 0) } - var customScreenWidth by rememberSaveable { + var screenSizeIndex by screenSizeIndexRef + val customScreenWidthRef = rememberSaveable { val searchIndex = screenSizes.indexOfFirst { it.contains(config.screenSize) } mutableStateOf( - if (searchIndex <= 0) { - config.screenSize.split("x").getOrElse(0) { "1280" } - } else "1280" + if (searchIndex <= 0) config.screenSize.split("x").getOrElse(0) { "1280" } else "1280" ) } - var customScreenHeight by rememberSaveable { + var customScreenWidth by customScreenWidthRef + val customScreenHeightRef = rememberSaveable { val searchIndex = screenSizes.indexOfFirst { it.contains(config.screenSize) } mutableStateOf( - if (searchIndex <= 0) { - config.screenSize.split("x").getOrElse(1) { "720" } - } else "720" + if (searchIndex <= 0) config.screenSize.split("x").getOrElse(1) { "720" } else "720" ) } - var graphicsDriverIndex by rememberSaveable { + var customScreenHeight by customScreenHeightRef + val graphicsDriverIndexRef = rememberSaveable { val driverIndex = graphicsDrivers.indexOfFirst { StringUtils.parseIdentifier(it) == config.graphicsDriver } mutableIntStateOf(if (driverIndex >= 0) driverIndex else 0) } + var graphicsDriverIndex by graphicsDriverIndexRef // Function to get the appropriate version list based on the selected graphics driver fun getVersionsForDriver(): List { @@ -639,12 +662,14 @@ fun ContainerConfigDialog( return startupSelectionEntries.subList(0, 2) } } - var dxWrapperIndex by rememberSaveable { + val dxWrapperIndexRef = rememberSaveable { val driverIndex = dxWrappers.indexOfFirst { StringUtils.parseIdentifier(it) == config.dxwrapper } mutableIntStateOf(if (driverIndex >= 0) driverIndex else 0) } + var dxWrapperIndex by dxWrapperIndexRef - var dxvkVersionIndex by rememberSaveable { mutableIntStateOf(0) } + val dxvkVersionIndexRef = rememberSaveable { mutableIntStateOf(0) } + var dxvkVersionIndex by dxvkVersionIndexRef // VKD3D version control (forced depending on driver) fun vkd3dForcedVersion(): String { @@ -653,163 +678,13 @@ fun ContainerConfigDialog( return if (isVortekLike) "2.6" else "2.14.1" } - @Composable - fun DxWrapperSection() { - // TODO: add way to pick DXVK version - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.dx_wrapper)) }, - value = dxWrapperIndex, - items = dxWrappers, - onItemSelected = { - dxWrapperIndex = it - config = config.copy(dxwrapper = StringUtils.parseIdentifier(dxWrappers[it])) - }, - ) - // DXVK Version Dropdown (conditionally visible and constrained) - run { - val driverType = StringUtils.parseIdentifier(graphicsDrivers[graphicsDriverIndex]) - val isVortekLike = config.containerVariant.equals(Container.GLIBC) && driverType == "vortek" || driverType == "adreno" || driverType == "sd-8-elite" - val isVKD3D = StringUtils.parseIdentifier(dxWrappers[dxWrapperIndex]) == "vkd3d" - val constrainedLabels = listOf("1.10.3", "1.10.9-sarek", "1.9.2", "async-1.10.3") - val constrainedIds = constrainedLabels.map { StringUtils.parseIdentifier(it) } - val useConstrained = - !inspectionMode && isVortekLike && GPUHelper.vkGetApiVersionSafe() < GPUHelper.vkMakeVersion( - 1, - 3, - 0 - ) - val items = - if (useConstrained) constrainedLabels - else if (isBionicVariant) dxvkOptions.labels - else dxvkVersionsBase - val itemIds = - if (useConstrained) constrainedIds - else if (isBionicVariant) dxvkOptions.ids - else dxvkVersionsBase.map { StringUtils.parseIdentifier(it) } - val itemMuted = - if (useConstrained) List(items.size) { false } - else if (isBionicVariant) dxvkOptions.muted - else null - if (!isVKD3D) { - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.dxvk_version)) }, - value = dxvkVersionIndex.coerceIn(0, (items.size - 1).coerceAtLeast(0)), - items = items, - itemMuted = itemMuted, - onItemSelected = { - dxvkVersionIndex = it - val selectedId = itemIds.getOrNull(it).orEmpty() - val isManifestNotInstalled = isBionicVariant && itemMuted?.getOrNull(it) == true - val manifestEntry = if (isBionicVariant) dxvkManifestById[selectedId] else null - if (isManifestNotInstalled && manifestEntry != null) { - launchManifestContentInstall( - manifestEntry, - ContentProfile.ContentType.CONTENT_TYPE_DXVK, - ) { - val currentConfig = KeyValueSet(config.dxwrapperConfig) - currentConfig.put("version", selectedId) - if (selectedId.contains("async", ignoreCase = true)) currentConfig.put("async", "1") - else currentConfig.put("async", "0") - if (selectedId.contains("gplasync", ignoreCase = true)) currentConfig.put("asyncCache", "1") - else currentConfig.put("asyncCache", "0") - config = config.copy(dxwrapperConfig = currentConfig.toString()) - } - return@SettingsListDropdown - } - val version = selectedId.ifEmpty { StringUtils.parseIdentifier(items[it]) } - val currentConfig = KeyValueSet(config.dxwrapperConfig) - currentConfig.put("version", version) - val envVarsSet = EnvVars(config.envVars) - if (version.contains("async", ignoreCase = true)) currentConfig.put("async", "1") - else currentConfig.put("async", "0") - if (version.contains("gplasync", ignoreCase = true)) currentConfig.put("asyncCache", "1") - else currentConfig.put("asyncCache", "0") - config = - config.copy(dxwrapperConfig = currentConfig.toString(), envVars = envVarsSet.toString()) - }, - ) - } else { - // Ensure default version for vortek-like when hidden - val version = if (isVortekLike) "1.10.3" else "2.4.1" - val currentConfig = KeyValueSet(config.dxwrapperConfig) - currentConfig.put("version", version) - config = config.copy(dxwrapperConfig = currentConfig.toString()) - } - } - // VKD3D Version UI (visible only when VKD3D selected) - run { - val isVKD3D = StringUtils.parseIdentifier(dxWrappers[dxWrapperIndex]) == "vkd3d" - if (isVKD3D) { - val label = "VKD3D Version" - val availableVersions = if (isBionicVariant) vkd3dOptions.labels else vkd3dVersionsBase - val availableIds = if (isBionicVariant) vkd3dOptions.ids else vkd3dVersionsBase - val availableMuted = if (isBionicVariant) vkd3dOptions.muted else null - val selectedVersion = - KeyValueSet(config.dxwrapperConfig).get("vkd3dVersion").ifEmpty { vkd3dForcedVersion() } - val selectedIndex = availableIds.indexOf(selectedVersion).coerceAtLeast(0) - - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = label) }, - value = selectedIndex, - items = availableVersions, - itemMuted = availableMuted, - onItemSelected = { idx -> - val selectedId = availableIds.getOrNull(idx).orEmpty() - val isManifestNotInstalled = isBionicVariant && availableMuted?.getOrNull(idx) == true - val manifestEntry = if (isBionicVariant) vkd3dManifestById[selectedId] else null - if (isManifestNotInstalled && manifestEntry != null) { - launchManifestContentInstall( - manifestEntry, - ContentProfile.ContentType.CONTENT_TYPE_VKD3D, - ) { - val currentConfig = KeyValueSet(config.dxwrapperConfig) - currentConfig.put("vkd3dVersion", selectedId) - config = config.copy(dxwrapperConfig = currentConfig.toString()) - } - return@SettingsListDropdown - } - val currentConfig = KeyValueSet(config.dxwrapperConfig) - currentConfig.put("vkd3dVersion", selectedId.ifEmpty { availableVersions[idx] }) - config = config.copy(dxwrapperConfig = currentConfig.toString()) - }, - ) - - // VKD3D Feature Level selector - val featureLevels = listOf("12_2", "12_1", "12_0", "11_1", "11_0") - val cfg = KeyValueSet(config.dxwrapperConfig) - val currentLevel = cfg.get("vkd3dFeatureLevel", "12_1") - val currentLevelIndex = featureLevels.indexOf(currentLevel).coerceAtLeast(0) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.vkd3d_feature_level)) }, - value = currentLevelIndex, - items = featureLevels, - onItemSelected = { - val selected = featureLevels[it] - val currentConfig = KeyValueSet(config.dxwrapperConfig) - currentConfig.put("vkd3dFeatureLevel", selected) - config = config.copy(dxwrapperConfig = currentConfig.toString()) - }, - ) - } - } - } - - var graphicsDriverVersionIndex by rememberSaveable { - // Find the version in the list that matches the configured version + val graphicsDriverVersionIndexRef = rememberSaveable { val version = config.graphicsDriverVersion - val driverIndex = if (version.isEmpty()) { - 0 // Default - } else { - // Try to find the version in the list - val index = getVersionsForDriver().indexOfFirst { it == version } - if (index >= 0) index else 0 - } + val driverIndex = if (version.isEmpty()) 0 + else getVersionsForDriver().indexOfFirst { it == version }.let { if (it >= 0) it else 0 } mutableIntStateOf(driverIndex) } + var graphicsDriverVersionIndex by graphicsDriverVersionIndexRef fun currentDxvkContext(): ManifestComponentHelper.DxvkContext = ManifestComponentHelper.buildDxvkContext( containerVariant = config.containerVariant, @@ -893,27 +768,32 @@ fun ContainerConfigDialog( } config = config.copy(envVars = envSet.toString(), dxwrapperConfig = kvs.toString()) } - var audioDriverIndex by rememberSaveable { + val audioDriverIndexRef = rememberSaveable { val driverIndex = audioDrivers.indexOfFirst { StringUtils.parseIdentifier(it) == config.audioDriver } mutableIntStateOf(if (driverIndex >= 0) driverIndex else 0) } - var gpuNameIndex by rememberSaveable { + var audioDriverIndex by audioDriverIndexRef + val gpuNameIndexRef = rememberSaveable { val gpuInfoIndex = gpuCards.values.indexOfFirst { it.deviceId == config.videoPciDeviceID } mutableIntStateOf(if (gpuInfoIndex >= 0) gpuInfoIndex else 0) } - var renderingModeIndex by rememberSaveable { + var gpuNameIndex by gpuNameIndexRef + val renderingModeIndexRef = rememberSaveable { val index = renderingModes.indexOfFirst { it.lowercase() == config.offScreenRenderingMode } mutableIntStateOf(if (index >= 0) index else 0) } - var videoMemIndex by rememberSaveable { + var renderingModeIndex by renderingModeIndexRef + val videoMemIndexRef = rememberSaveable { val index = videoMemSizes.indexOfFirst { StringUtils.parseNumber(it) == config.videoMemorySize } mutableIntStateOf(if (index >= 0) index else 0) } - var mouseWarpIndex by rememberSaveable { + var videoMemIndex by videoMemIndexRef + val mouseWarpIndexRef = rememberSaveable { val index = mouseWarps.indexOfFirst { it.lowercase() == config.mouseWarpOverride } mutableIntStateOf(if (index >= 0) index else 0) } - var externalDisplayModeIndex by rememberSaveable { + var mouseWarpIndex by mouseWarpIndexRef + val externalDisplayModeIndexRef = rememberSaveable { val index = when (config.externalDisplayMode.lowercase()) { Container.EXTERNAL_DISPLAY_MODE_TOUCHPAD -> 1 Container.EXTERNAL_DISPLAY_MODE_KEYBOARD -> 2 @@ -922,19 +802,25 @@ fun ContainerConfigDialog( } mutableIntStateOf(index) } - var languageIndex by rememberSaveable { + var externalDisplayModeIndex by externalDisplayModeIndexRef + val languageIndexRef = rememberSaveable { val idx = languages.indexOfFirst { it == config.language.lowercase() } mutableIntStateOf(if (idx >= 0) idx else languages.indexOf("english")) } + var languageIndex by languageIndexRef var dismissDialogState by rememberSaveable(stateSaver = MessageDialogState.Saver) { mutableStateOf(MessageDialogState(visible = false)) } - var showEnvVarCreateDialog by rememberSaveable { mutableStateOf(false) } - var showAddDriveDialog by rememberSaveable { mutableStateOf(false) } - var selectedDriveLetter by rememberSaveable { mutableStateOf("") } - var pendingDriveLetter by rememberSaveable { mutableStateOf("") } - var driveLetterMenuExpanded by rememberSaveable { mutableStateOf(false) } + val showEnvVarCreateDialogRef = rememberSaveable { mutableStateOf(false) } + var showEnvVarCreateDialog by showEnvVarCreateDialogRef + val showAddDriveDialogRef = rememberSaveable { mutableStateOf(false) } + val selectedDriveLetterRef = rememberSaveable { mutableStateOf("") } + var selectedDriveLetter by selectedDriveLetterRef + val pendingDriveLetterRef = rememberSaveable { mutableStateOf("") } + var pendingDriveLetter by pendingDriveLetterRef + val driveLetterMenuExpandedRef = rememberSaveable { mutableStateOf(false) } + var driveLetterMenuExpanded by driveLetterMenuExpandedRef val reservedDriveLetters = setOf("C", "Z") val nonDeletableDriveLetters = setOf("A", "C", "D", "Z") @@ -1036,84 +922,144 @@ fun ContainerConfigDialog( val aspectResolutionError = stringResource( R.string.container_config_custom_resolution_error_aspect ) + + val state = ContainerConfigState( + config = configState, + graphicsDrivers = graphicsDriversRef, + bionicWineEntries = bionicWineEntriesRef, + glibcWineEntries = glibcWineEntriesRef, + wrapperVersions = wrapperVersionsRef, + dxvkVersionsAll = dxvkVersionsAllRef, + componentAvailability = componentAvailabilityRef, + showCustomResolutionDialog = showCustomResolutionDialogRef, + customResolutionValidationError = customResolutionValidationErrorRef, + vkMaxVersionIndex = vkMaxVersionIndexRef, + imageCacheIndex = imageCacheIndexRef, + exposedExtIndices = exposedExtIndicesRef, + maxDeviceMemoryIndex = maxDeviceMemoryIndexRef, + bionicDriverIndex = bionicDriverIndexRef, + wrapperVersionIndex = wrapperVersionIndexRef, + presentModeIndex = presentModeIndexRef, + resourceTypeIndex = resourceTypeIndexRef, + bcnEmulationIndex = bcnEmulationIndexRef, + bcnEmulationTypeIndex = bcnEmulationTypeIndexRef, + bcnEmulationCacheEnabled = bcnEmulationCacheEnabledRef, + disablePresentWaitChecked = disablePresentWaitCheckedRef, + syncEveryFrameChecked = syncEveryFrameCheckedRef, + sharpnessEffectIndex = sharpnessEffectIndexRef, + sharpnessLevel = sharpnessLevelRef, + sharpnessDenoise = sharpnessDenoiseRef, + adrenotoolsTurnipChecked = adrenotoolsTurnipCheckedRef, + emulator64Index = emulator64IndexRef, + emulator32Index = emulator32IndexRef, + screenSizeIndex = screenSizeIndexRef, + customScreenWidth = customScreenWidthRef, + customScreenHeight = customScreenHeightRef, + graphicsDriverIndex = graphicsDriverIndexRef, + dxWrapperIndex = dxWrapperIndexRef, + dxvkVersionIndex = dxvkVersionIndexRef, + graphicsDriverVersionIndex = graphicsDriverVersionIndexRef, + audioDriverIndex = audioDriverIndexRef, + gpuNameIndex = gpuNameIndexRef, + renderingModeIndex = renderingModeIndexRef, + videoMemIndex = videoMemIndexRef, + mouseWarpIndex = mouseWarpIndexRef, + externalDisplayModeIndex = externalDisplayModeIndexRef, + languageIndex = languageIndexRef, + showEnvVarCreateDialog = showEnvVarCreateDialogRef, + showAddDriveDialog = showAddDriveDialogRef, + selectedDriveLetter = selectedDriveLetterRef, + pendingDriveLetter = pendingDriveLetterRef, + driveLetterMenuExpanded = driveLetterMenuExpandedRef, + screenSizes = screenSizes, + baseGraphicsDrivers = baseGraphicsDrivers, + dxWrappers = dxWrappers, + dxvkVersionsBase = dxvkVersionsBase, + vkd3dVersionsBase = vkd3dVersionsBase, + audioDrivers = audioDrivers, + presentModes = presentModes, + resourceTypes = resourceTypes, + bcnEmulationEntries = bcnEmulationEntries, + bcnEmulationTypeEntries = bcnEmulationTypeEntries, + sharpnessEffects = sharpnessEffects, + sharpnessDisplayItems = sharpnessDisplayItems, + renderingModes = renderingModes, + videoMemSizes = videoMemSizes, + mouseWarps = mouseWarps, + externalDisplayModes = externalDisplayModes, + winCompOpts = winCompOpts, + box64Versions = box64Versions, + wowBox64VersionsBase = wowBox64VersionsBase, + box64BionicVersionsBase = box64BionicVersionsBase, + fexcoreVersionsBase = fexcoreVersionsBase, + fexcoreTSOPresets = fexcoreTSOPresets, + fexcoreX87Presets = fexcoreX87Presets, + fexcoreMultiblockValues = fexcoreMultiblockValues, + startupSelectionEntries = startupSelectionEntries, + turnipVersions = turnipVersions, + virglVersions = virglVersions, + zinkVersions = zinkVersions, + vortekVersions = vortekVersions, + adrenoVersions = adrenoVersions, + sd8EliteVersions = sd8EliteVersions, + containerVariants = containerVariants, + bionicWineEntriesBase = bionicWineEntriesBase, + glibcWineEntriesBase = glibcWineEntriesBase, + emulatorEntries = emulatorEntries, + bionicGraphicsDrivers = bionicGraphicsDrivers, + baseWrapperVersions = baseWrapperVersions, + languages = languages, + dxvkOptions = dxvkOptions, + vkd3dOptions = vkd3dOptions, + box64Options = box64Options, + box64BionicOptions = box64BionicOptions, + wowBox64Options = wowBox64Options, + fexcoreOptions = fexcoreOptions, + wrapperOptions = wrapperOptions, + bionicWineOptions = bionicWineOptions, + glibcWineOptions = glibcWineOptions, + dxvkManifestById = dxvkManifestById, + vkd3dManifestById = vkd3dManifestById, + box64ManifestById = box64ManifestById, + wowBox64ManifestById = wowBox64ManifestById, + fexcoreManifestById = fexcoreManifestById, + wrapperManifestById = wrapperManifestById, + bionicWineManifestById = bionicWineManifestById, + glibcWineManifestById = glibcWineManifestById, + gpuCards = gpuCards, + box64Presets = box64Presets, + fexcorePresets = fexcorePresets, + gpuExtensions = gpuExtensions, + inspectionMode = inspectionMode, + isBionicVariant = isBionicVariant, + nonDeletableDriveLetters = nonDeletableDriveLetters, + availableDriveLetters = availableDriveLetters, + launchManifestInstall = { entry, label, isDriver, expectedType, onInstalled -> + launchManifestInstall(entry, label, isDriver, expectedType, onInstalled) + }, + launchManifestContentInstall = { entry, expectedType, onInstalled -> + launchManifestContentInstall(entry, expectedType, onInstalled) + }, + launchManifestDriverInstall = { entry, onInstalled -> launchManifestDriverInstall(entry, onInstalled) }, + getStartupSelectionOptions = { getStartupSelectionOptions() }, + launchFolderPicker = { + showAddDriveDialogRef.value = false + pendingDriveLetterRef.value = selectedDriveLetterRef.value + SteamService.keepAlive = true + folderPicker.launchPicker() + }, + getVersionsForDriver = { getVersionsForDriver() }, + getVersionsForBox64 = { getVersionsForBox64() }, + applyScreenSizeToConfig = applyScreenSizeToConfig, + vkd3dForcedVersion = { vkd3dForcedVersion() }, + currentDxvkContext = { currentDxvkContext() }, + ) + LoadingDialog( visible = showManifestDownloadDialog, progress = manifestDownloadProgress, message = manifestDownloadMessage, ) - if (showCustomResolutionDialog) { - AlertDialog( - onDismissRequest = { showCustomResolutionDialog = false }, - title = { Text(text = stringResource(R.string.container_config_custom_resolution_title)) }, - text = { - Column { - Row { - OutlinedTextField( - modifier = Modifier.width(128.dp), - value = customScreenWidth, - onValueChange = { - customScreenWidth = it - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - label = { Text(text = stringResource(R.string.width)) }, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - modifier = Modifier.align(Alignment.CenterVertically), - text = stringResource(R.string.container_config_custom_resolution_separator), - style = TextStyle(fontSize = 16.sp), - ) - Spacer(modifier = Modifier.width(8.dp)) - OutlinedTextField( - modifier = Modifier.width(128.dp), - value = customScreenHeight, - onValueChange = { - customScreenHeight = it - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - label = { Text(text = stringResource(R.string.height)) }, - ) - } - if (customResolutionValidationError != null) { - Text( - text = customResolutionValidationError!!, - color = MaterialTheme.colorScheme.error, - style = TextStyle(fontSize = 16.sp), - modifier = Modifier.padding(top = 8.dp) - ) - } - } - }, - confirmButton = { - TextButton( - onClick = { - val widthInt = customScreenWidth.toIntOrNull() ?: 0 - val heightInt = customScreenHeight.toIntOrNull() ?: 0 - if (widthInt == 0 || heightInt == 0) { - customResolutionValidationError = nonzeroResolutionError - } else if (widthInt <= heightInt) { - customResolutionValidationError = aspectResolutionError - } else { - customResolutionValidationError = null - applyScreenSizeToConfig() - showCustomResolutionDialog = false - } - }, - ) { - Text(text = stringResource(R.string.ok)) - } - }, - dismissButton = { - TextButton( - onClick = { - showCustomResolutionDialog = false - }, - ) { - Text(text = stringResource(R.string.cancel)) - } - } - ) - } MessageDialog( visible = dismissDialogState.visible, @@ -1126,176 +1072,6 @@ fun ContainerConfigDialog( onConfirmClick = onDismissRequest, ) - if (showEnvVarCreateDialog) { - var envVarName by rememberSaveable { mutableStateOf("") } - var envVarValue by rememberSaveable { mutableStateOf("") } - AlertDialog( - onDismissRequest = { showEnvVarCreateDialog = false }, - title = { Text(text = stringResource(R.string.new_environment_variable)) }, - text = { - var knownVarsMenuOpen by rememberSaveable { mutableStateOf(false) } - Column { - Row { - OutlinedTextField( - value = envVarName, - onValueChange = { envVarName = it }, - label = { Text(text = stringResource(R.string.name)) }, - trailingIcon = { - IconButton( - onClick = { knownVarsMenuOpen = true }, - content = { - Icon( - imageVector = Icons.AutoMirrored.Outlined.ViewList, - contentDescription = "List known variable names", - ) - }, - ) - }, - ) - DropdownMenu( - expanded = knownVarsMenuOpen, - onDismissRequest = { knownVarsMenuOpen = false }, - ) { - val knownEnvVars = EnvVarInfo.KNOWN_ENV_VARS.values.filter { - !config.envVars.contains("${it.identifier}=") - } - if (knownEnvVars.isNotEmpty()) { - for (knownVariable in knownEnvVars) { - DropdownMenuItem( - text = { Text(knownVariable.identifier) }, - onClick = { - envVarName = knownVariable.identifier - knownVarsMenuOpen = false - }, - ) - } - } else { - DropdownMenuItem( - text = { Text(text = stringResource(R.string.no_more_known_variables)) }, - onClick = {}, - ) - } - } - } - val selectedEnvVarInfo = EnvVarInfo.KNOWN_ENV_VARS[envVarName] - if (selectedEnvVarInfo?.selectionType == EnvVarSelectionType.MULTI_SELECT) { - var multiSelectedIndices by remember { mutableStateOf(listOf()) } - SettingsMultiListDropdown( - enabled = true, - values = multiSelectedIndices, - items = selectedEnvVarInfo.possibleValues, - fallbackDisplay = "", - onItemSelected = { index -> - val newIndices = if (multiSelectedIndices.contains(index)) { - multiSelectedIndices.filter { it != index } - } else { - multiSelectedIndices + index - } - multiSelectedIndices = newIndices - envVarValue = newIndices.joinToString(",") { selectedEnvVarInfo.possibleValues[it] } - }, - title = { Text(text = stringResource(R.string.value)) }, - colors = settingsTileColors(), - ) - } else { - OutlinedTextField( - value = envVarValue, - onValueChange = { envVarValue = it }, - label = { Text(text = stringResource(R.string.value)) }, - ) - } - } - }, - dismissButton = { - TextButton( - onClick = { showEnvVarCreateDialog = false }, - content = { Text(text = stringResource(R.string.cancel)) }, - ) - }, - confirmButton = { - TextButton( - enabled = envVarName.isNotEmpty(), - onClick = { - val envVars = EnvVars(config.envVars) - envVars.put(envVarName, envVarValue) - config = config.copy(envVars = envVars.toString()) - showEnvVarCreateDialog = false - }, - content = { Text(text = stringResource(R.string.ok)) }, - ) - }, - ) - } - - if (showAddDriveDialog) { - AlertDialog( - onDismissRequest = { showAddDriveDialog = false }, - title = { Text(text = stringResource(R.string.add_drive)) }, - text = { - Column { - OutlinedTextField( - value = selectedDriveLetter, - onValueChange = {}, - readOnly = true, - label = { Text(text = stringResource(R.string.drive_letter)) }, - trailingIcon = { - IconButton( - onClick = { driveLetterMenuExpanded = true }, - content = { - Icon( - imageVector = Icons.AutoMirrored.Outlined.ViewList, - contentDescription = null, - ) - }, - ) - }, - ) - DropdownMenu( - expanded = driveLetterMenuExpanded, - onDismissRequest = { driveLetterMenuExpanded = false }, - ) { - availableDriveLetters.forEach { letter -> - DropdownMenuItem( - text = { Text(text = letter) }, - onClick = { - selectedDriveLetter = letter - driveLetterMenuExpanded = false - }, - ) - } - } - if (availableDriveLetters.isEmpty()) { - Text( - text = stringResource(R.string.no_available_drive_letters), - color = MaterialTheme.colorScheme.error, - style = TextStyle(fontSize = 14.sp), - modifier = Modifier.padding(top = 8.dp), - ) - } - } - }, - confirmButton = { - TextButton( - enabled = selectedDriveLetter.isNotBlank() && - availableDriveLetters.contains(selectedDriveLetter), - onClick = { - pendingDriveLetter = selectedDriveLetter - showAddDriveDialog = false - SteamService.keepAlive = true - folderPicker.launchPicker() - }, - content = { Text(text = stringResource(R.string.ok)) }, - ) - }, - dismissButton = { - TextButton( - onClick = { showAddDriveDialog = false }, - content = { Text(text = stringResource(R.string.cancel)) }, - ) - }, - ) - } - Dialog( onDismissRequest = onDismissCheck, properties = DialogProperties( @@ -1367,1090 +1143,15 @@ fun ContainerConfigDialog( .verticalScroll(scrollState) .weight(1f), ) { - if (selectedTab == 0) SettingsGroup() { - // Variant selector (glibc/bionic) - run { - val variantIndex = rememberSaveable { - mutableIntStateOf(containerVariants.indexOfFirst { it.equals(config.containerVariant, true) } - .coerceAtLeast(0)) - } - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.container_variant)) }, - value = variantIndex.value, - items = containerVariants, - onItemSelected = { idx -> - variantIndex.value = idx - val newVariant = containerVariants[idx] - if (newVariant.equals(Container.GLIBC, ignoreCase = true)) { - // Switch to glibc: reset to default graphics driver and clear wrapper-specific version - val defaultDriver = Container.DEFAULT_GRAPHICS_DRIVER - val newCfg = KeyValueSet(config.graphicsDriverConfig).apply { - put("version", "") - put("syncFrame", "0") - put("disablePresentWait", get("disablePresentWait").ifEmpty { "0" }) - if (get("presentMode").isEmpty()) put("presentMode", "mailbox") - if (get("resourceType").isEmpty()) put("resourceType", "auto") - if (get("bcnEmulation").isEmpty()) put("bcnEmulation", "auto") - if (get("bcnEmulationType").isEmpty()) put("bcnEmulationType", "software") - if (get("bcnEmulationCache").isEmpty()) put("bcnEmulationCache", "0") - put("adrenotoolsTurnip", "1") - } - graphicsDriverIndex = - graphicsDrivers.indexOfFirst { StringUtils.parseIdentifier(it) == defaultDriver } - .coerceAtLeast(0) - graphicsDriverVersionIndex = 0 - syncEveryFrameChecked = false - disablePresentWaitChecked = newCfg.get("disablePresentWait", "0") == "1" - bcnEmulationCacheEnabled = newCfg.get("bcnEmulationCache", "0") == "1" - adrenotoolsTurnipChecked = true - - val defaultGlibcWine = glibcWineEntries.firstOrNull() ?: Container.DEFAULT_WINE_VERSION - config = config.copy( - containerVariant = newVariant, - wineVersion = defaultGlibcWine, - graphicsDriver = defaultDriver, - graphicsDriverVersion = "", - graphicsDriverConfig = newCfg.toString(), - box64Version = "0.3.6", - ) - } else { - // Switch to bionic: set wrapper defaults - val defaultBionicDriver = StringUtils.parseIdentifier(bionicGraphicsDrivers.first()) - val newWine = - if (config.wineVersion == (glibcWineEntries.firstOrNull() ?: Container.DEFAULT_WINE_VERSION)) - bionicWineEntries.firstOrNull() ?: config.wineVersion - else config.wineVersion - val newCfg = KeyValueSet(config.graphicsDriverConfig).apply { - put("version", DefaultVersion.WRAPPER) - put("syncFrame", "0") - put("adrenotoolsTurnip", "1") - put("disablePresentWait", get("disablePresentWait").ifEmpty { "0" }) - if (get("exposedDeviceExtensions").isEmpty()) put("exposedDeviceExtensions", "all") - if (get("maxDeviceMemory").isEmpty()) put("maxDeviceMemory", "4096") - if (get("presentMode").isEmpty()) put("presentMode", "mailbox") - if (get("resourceType").isEmpty()) put("resourceType", "auto") - if (get("bcnEmulation").isEmpty()) put("bcnEmulation", "auto") - if (get("bcnEmulationType").isEmpty()) put("bcnEmulationType", "software") - if (get("bcnEmulationCache").isEmpty()) put("bcnEmulationCache", "0") - } - bionicDriverIndex = 0 - wrapperVersionIndex = wrapperOptions.ids - .indexOfFirst { it.equals(DefaultVersion.WRAPPER, true) } - .let { if (it >= 0) it else 0 } - syncEveryFrameChecked = false - disablePresentWaitChecked = newCfg.get("disablePresentWait", "0") == "1" - bcnEmulationCacheEnabled = newCfg.get("bcnEmulationCache", "0") == "1" - adrenotoolsTurnipChecked = true - maxDeviceMemoryIndex = - listOf("0", "512", "1024", "2048", "4096").indexOf("4096").coerceAtLeast(0) - - // If transitioning from GLIBC -> BIONIC, set Box64 to default and DXVK to async-1.10.3 - val currentConfig = KeyValueSet(config.dxwrapperConfig) - currentConfig.put("version", "async-1.10.3") - currentConfig.put("async", "1") - currentConfig.put("asyncCache", "0") - config = config.copy(dxwrapperConfig = currentConfig.toString()) - - config = config.copy( - containerVariant = newVariant, - wineVersion = newWine, - graphicsDriver = defaultBionicDriver, - graphicsDriverVersion = "", - graphicsDriverConfig = newCfg.toString(), - box64Version = "0.3.7", - dxwrapperConfig = currentConfig.toString(), - ) - } - }, - ) - // Wine version only if bionic variant - if (config.containerVariant.equals(Container.BIONIC, ignoreCase = true)) { - val wineIndex = bionicWineOptions.ids.indexOfFirst { it == config.wineVersion }.coerceAtLeast(0) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.wine_version)) }, - value = wineIndex, - items = bionicWineOptions.labels, - itemMuted = bionicWineOptions.muted, - onItemSelected = { idx -> - val selectedId = bionicWineOptions.ids.getOrNull(idx).orEmpty() - val isManifestNotInstalled = bionicWineOptions.muted.getOrNull(idx) == true - val manifestEntry = bionicWineManifestById[selectedId] - if (isManifestNotInstalled && manifestEntry != null) { - val expectedType = if (selectedId.startsWith("proton", true)) { - ContentProfile.ContentType.CONTENT_TYPE_PROTON - } else { - ContentProfile.ContentType.CONTENT_TYPE_WINE - } - launchManifestContentInstall(manifestEntry, expectedType) { - config = config.copy(wineVersion = selectedId) - } - return@SettingsListDropdown - } - config = config.copy(wineVersion = selectedId.ifEmpty { bionicWineOptions.labels[idx] }) - }, - ) - } - } - // Executable Path dropdown with all EXEs from A: drive - ExecutablePathDropdown( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), - value = config.executablePath, - onValueChange = { config = config.copy(executablePath = it) }, - containerData = config, - ) - OutlinedTextField( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), - value = config.execArgs, - onValueChange = { config = config.copy(execArgs = it) }, - label = { Text(text = stringResource(R.string.exec_arguments)) }, - placeholder = { Text(text = stringResource(R.string.exec_arguments_example)) }, - ) - val displayNameForLanguage: (String) -> String = { code -> - when (code) { - "schinese" -> "Simplified Chinese" - "tchinese" -> "Traditional Chinese" - "koreana" -> "Korean" - "latam" -> "Spanish (Latin America)" - "brazilian" -> "Portuguese (Brazil)" - else -> code.replaceFirstChar { ch -> ch.titlecase(Locale.getDefault()) } - } - } - SettingsListDropdown( - enabled = true, - value = languageIndex, - items = languages.map(displayNameForLanguage), - fallbackDisplay = displayNameForLanguage("english"), - onItemSelected = { index -> - languageIndex = index - config = config.copy(language = languages[index]) - }, - title = { Text(text = stringResource(R.string.language)) }, - colors = settingsTileColors(), - ) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.screen_size)) }, - value = screenSizeIndex, - items = screenSizes, - onItemSelected = { - screenSizeIndex = it - if (it == 0) { - showCustomResolutionDialog = true - } else { - applyScreenSizeToConfig() - } - }, - ) - // Audio Driver Dropdown - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.audio_driver)) }, - value = audioDriverIndex, - items = audioDrivers, - onItemSelected = { - audioDriverIndex = it - config = config.copy(audioDriver = StringUtils.parseIdentifier(audioDrivers[it])) - }, - ) - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.show_fps)) }, - state = config.showFPS, - onCheckedChange = { - config = config.copy(showFPS = it) - }, - ) - - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.force_dlc)) }, - subtitle = { Text(text = stringResource(R.string.force_dlc_description)) }, - state = config.forceDlc, - onCheckedChange = { - config = config.copy(forceDlc = it) - }, - ) - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.use_legacy_drm)) }, - state = config.useLegacyDRM, - onCheckedChange = { - config = config.copy(useLegacyDRM = it) - }, - ) - if (!config.useLegacyDRM) { - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.unpack_files)) }, - subtitle = { Text(text = stringResource(R.string.unpack_files_description)) }, - state = config.unpackFiles, - onCheckedChange = { - config = config.copy(unpackFiles = it) - }, - ) - } - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.launch_steam_client_beta)) }, - subtitle = { Text(text = stringResource(R.string.launch_steam_client_description)) }, - state = config.launchRealSteam, - onCheckedChange = { - config = config.copy(launchRealSteam = it) - }, - ) - if (config.launchRealSteam) { - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.allow_steam_updates)) }, - subtitle = { Text(text = stringResource(R.string.allow_steam_updates_description)) }, - state = config.allowSteamUpdates, - onCheckedChange = { - config = config.copy(allowSteamUpdates = it) - }, - ) - } - // Steam Type Dropdown - val steamTypeItems = listOf("Normal", "Light", "Ultra Light") - val currentSteamTypeIndex = when (config.steamType.lowercase()) { - Container.STEAM_TYPE_LIGHT -> 1 - Container.STEAM_TYPE_ULTRALIGHT -> 2 - else -> 0 - } - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.steam_type)) }, - value = currentSteamTypeIndex, - items = steamTypeItems, - onItemSelected = { - val type = when (it) { - 1 -> Container.STEAM_TYPE_LIGHT - 2 -> Container.STEAM_TYPE_ULTRALIGHT - else -> Container.STEAM_TYPE_NORMAL - } - config = config.copy(steamType = type) - }, - ) - } - if (selectedTab == 1) SettingsGroup() { - if (config.containerVariant.equals(Container.BIONIC, ignoreCase = true)) { - // Bionic: Graphics Driver (Wrapper/Wrapper-v2) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.graphics_driver)) }, - value = bionicDriverIndex, - items = bionicGraphicsDrivers, - onItemSelected = { idx -> - bionicDriverIndex = idx - config = config.copy(graphicsDriver = StringUtils.parseIdentifier(bionicGraphicsDrivers[idx])) - }, - ) - // Bionic: Graphics Driver Version (stored in graphicsDriverConfig.version) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.graphics_driver_version)) }, - value = wrapperVersionIndex, - items = wrapperOptions.labels, - itemMuted = wrapperOptions.muted, - onItemSelected = { idx -> - wrapperVersionIndex = idx - val selectedId = wrapperOptions.ids.getOrNull(idx).orEmpty() - val isManifestNotInstalled = wrapperOptions.muted.getOrNull(idx) == true - val manifestEntry = wrapperManifestById[selectedId] - if (isManifestNotInstalled && manifestEntry != null) { - launchManifestDriverInstall(manifestEntry) { - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("version", selectedId) - config = config.copy(graphicsDriverConfig = cfg.toString()) - } - return@SettingsListDropdown - } - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("version", selectedId) - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - DxWrapperSection() - // Bionic: Exposed Vulkan Extensions (same UI as Vortek) - SettingsMultiListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.exposed_vulkan_extensions)) }, - values = exposedExtIndices, - items = gpuExtensions, - fallbackDisplay = "all", - onItemSelected = { idx -> - exposedExtIndices = - if (exposedExtIndices.contains(idx)) exposedExtIndices.filter { it != idx } else exposedExtIndices + idx - val cfg = KeyValueSet(config.graphicsDriverConfig) - val allSelected = exposedExtIndices.size == gpuExtensions.size - if (allSelected) cfg.put("exposedDeviceExtensions", "all") else cfg.put( - "exposedDeviceExtensions", - exposedExtIndices.sorted().joinToString("|") { gpuExtensions[it] }, - ) - // Maintain blacklist as the complement of exposed selections - val blacklisted = if (allSelected) "" else - gpuExtensions.indices - .filter { it !in exposedExtIndices } - .sorted() - .joinToString(",") { gpuExtensions[it] } - cfg.put("blacklistedExtensions", blacklisted) - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - // Bionic: Max Device Memory (same as Vortek) - run { - val memValues = listOf("0", "512", "1024", "2048", "4096") - val memLabels = listOf("0 MB", "512 MB", "1024 MB", "2048 MB", "4096 MB") - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.max_device_memory)) }, - value = maxDeviceMemoryIndex.coerceIn(0, memValues.lastIndex), - items = memLabels, - onItemSelected = { idx -> - maxDeviceMemoryIndex = idx - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("maxDeviceMemory", memValues[idx]) - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - } - // Bionic: Use Adrenotools Turnip - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.use_adrenotools_turnip)) }, - state = adrenotoolsTurnipChecked, - onCheckedChange = { checked -> - adrenotoolsTurnipChecked = checked - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("adrenotoolsTurnip", if (checked) "1" else "0") - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.present_modes)) }, - value = presentModeIndex.coerceIn(0, presentModes.lastIndex.coerceAtLeast(0)), - items = presentModes, - onItemSelected = { idx -> - presentModeIndex = idx - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("presentMode", presentModes[idx]) - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.resource_type)) }, - value = resourceTypeIndex.coerceIn(0, resourceTypes.lastIndex.coerceAtLeast(0)), - items = resourceTypes, - onItemSelected = { idx -> - resourceTypeIndex = idx - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("resourceType", resourceTypes[idx]) - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.bcn_emulation)) }, - value = bcnEmulationIndex.coerceIn(0, bcnEmulationEntries.lastIndex.coerceAtLeast(0)), - items = bcnEmulationEntries, - onItemSelected = { idx -> - bcnEmulationIndex = idx - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("bcnEmulation", bcnEmulationEntries[idx]) - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.bcn_emulation_type)) }, - value = bcnEmulationTypeIndex.coerceIn(0, bcnEmulationTypeEntries.lastIndex.coerceAtLeast(0)), - items = bcnEmulationTypeEntries, - onItemSelected = { idx -> - bcnEmulationTypeIndex = idx - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("bcnEmulationType", bcnEmulationTypeEntries[idx]) - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.bcn_emulation_cache)) }, - state = bcnEmulationCacheEnabled, - onCheckedChange = { checked -> - bcnEmulationCacheEnabled = checked - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("bcnEmulationCache", if (checked) "1" else "0") - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.disable_present_wait)) }, - state = disablePresentWaitChecked, - onCheckedChange = { checked -> - disablePresentWaitChecked = checked - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("disablePresentWait", if (checked) "1" else "0") - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.sync_frame)) }, - state = syncEveryFrameChecked, - onCheckedChange = { checked -> - syncEveryFrameChecked = checked - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("syncFrame", if (checked) "1" else "0") - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.sharpness_effect)) }, - value = sharpnessEffectIndex.coerceIn(0, sharpnessEffects.lastIndex.coerceAtLeast(0)), - items = sharpnessDisplayItems, - onItemSelected = { idx -> - sharpnessEffectIndex = idx - config = config.copy(sharpnessEffect = sharpnessEffects[idx]) - }, - ) - val selectedBoost = sharpnessEffects - .getOrNull(sharpnessEffectIndex) - ?.equals("None", ignoreCase = true) - ?.not() ?: false - if (selectedBoost) { - Column( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text(text = stringResource(R.string.sharpness_level)) - Slider( - value = sharpnessLevel.toFloat(), - onValueChange = { newValue -> - val clamped = newValue.roundToInt().coerceIn(0, 100) - sharpnessLevel = clamped - config = config.copy(sharpnessLevel = clamped) - }, - valueRange = 0f..100f, - ) - Text(text = "${sharpnessLevel}%") - } - Column( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text(text = stringResource(R.string.sharpness_denoise)) - Slider( - value = sharpnessDenoise.toFloat(), - onValueChange = { newValue -> - val clamped = newValue.roundToInt().coerceIn(0, 100) - sharpnessDenoise = clamped - config = config.copy(sharpnessDenoise = clamped) - }, - valueRange = 0f..100f, - ) - Text(text = "${sharpnessDenoise}%") - } - } - } else { - // Non-bionic: existing driver/version UI and Vortek-specific options - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.graphics_driver)) }, - value = graphicsDriverIndex, - items = graphicsDrivers, - onItemSelected = { - graphicsDriverIndex = it - config = config.copy(graphicsDriver = StringUtils.parseIdentifier(graphicsDrivers[it])) - // Reset version index when driver changes - graphicsDriverVersionIndex = 0 - config = config.copy(graphicsDriverVersion = "") - }, - ) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.graphics_driver_version)) }, - value = graphicsDriverVersionIndex, - items = getVersionsForDriver(), - onItemSelected = { - graphicsDriverVersionIndex = it - val selectedVersion = if (it == 0) "" else getVersionsForDriver()[it] - config = config.copy(graphicsDriverVersion = selectedVersion) - }, - ) - DxWrapperSection() - // Vortek/Adreno specific settings - run { - val driverType = StringUtils.parseIdentifier(graphicsDrivers[graphicsDriverIndex]) - val isVortekLike = config.containerVariant.equals(Container.GLIBC) && driverType == "vortek" || driverType == "adreno" || driverType == "sd-8-elite" - if (isVortekLike) { - // Vulkan Max Version - val vkVersions = listOf("1.0", "1.1", "1.2", "1.3") - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.vulkan_version)) }, - value = vkMaxVersionIndex.coerceIn(0, 3), - items = vkVersions, - onItemSelected = { idx -> - vkMaxVersionIndex = idx - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("vkMaxVersion", vkVersions[idx]) - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - // Exposed Extensions (multi-select) - SettingsMultiListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.exposed_vulkan_extensions)) }, - values = exposedExtIndices, - items = gpuExtensions, - fallbackDisplay = "all", - onItemSelected = { idx -> - exposedExtIndices = - if (exposedExtIndices.contains(idx)) exposedExtIndices.filter { it != idx } else exposedExtIndices + idx - val cfg = KeyValueSet(config.graphicsDriverConfig) - val allSelected = exposedExtIndices.size == gpuExtensions.size - if (allSelected) cfg.put("exposedDeviceExtensions", "all") else cfg.put( - "exposedDeviceExtensions", - exposedExtIndices.sorted().joinToString("|") { gpuExtensions[it] }, - ) - // Maintain blacklist as the complement of exposed selections - val blacklisted = if (allSelected) "" else - gpuExtensions.indices - .filter { it !in exposedExtIndices } - .sorted() - .joinToString(",") { gpuExtensions[it] } - cfg.put("blacklistedExtensions", blacklisted) - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - // Image Cache Size - val imageSizes = listOf("64", "128", "256", "512", "1024") - val imageLabels = listOf("64", "128", "256", "512", "1024").map { "$it MB" } - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.image_cache_size)) }, - value = imageCacheIndex.coerceIn(0, imageSizes.lastIndex), - items = imageLabels, - onItemSelected = { idx -> - imageCacheIndex = idx - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("imageCacheSize", imageSizes[idx]) - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - // Max Device Memory - val memValues = listOf("0", "512", "1024", "2048", "4096") - val memLabels = listOf("0 MB", "512 MB", "1024 MB", "2048 MB", "4096 MB") - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.max_device_memory)) }, - value = maxDeviceMemoryIndex.coerceIn(0, memValues.lastIndex), - items = memLabels, - onItemSelected = { idx -> - maxDeviceMemoryIndex = idx - val cfg = KeyValueSet(config.graphicsDriverConfig) - cfg.put("maxDeviceMemory", memValues[idx]) - config = config.copy(graphicsDriverConfig = cfg.toString()) - }, - ) - } - } - } - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.use_dri3)) }, - subtitle = { Text(text = stringResource(R.string.use_dri3_description)) }, - state = config.useDRI3, - onCheckedChange = { - config = config.copy(useDRI3 = it) - } - ) - } - if (selectedTab == 2) SettingsGroup() { - if (config.containerVariant.equals(Container.BIONIC, ignoreCase = true)) { - // Bionic: Emulators - val wineIsX8664 = config.wineVersion.contains("x86_64", true) - val wineIsArm64Ec = config.wineVersion.contains("arm64ec", true) - - // FEXCore Settings (only when Bionic + Wine arm64ec) placed under Box64 settings - run { - if (wineIsArm64Ec) { - SettingsGroup() { - val fexcoreIndex = fexcoreOptions.ids.indexOfFirst { it == config.fexcoreVersion }.coerceAtLeast(0) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.fexcore_version)) }, - value = fexcoreIndex, - items = fexcoreOptions.labels, - itemMuted = fexcoreOptions.muted, - onItemSelected = { idx -> - val selectedId = fexcoreOptions.ids.getOrNull(idx).orEmpty() - val isManifestNotInstalled = fexcoreOptions.muted.getOrNull(idx) == true - val manifestEntry = fexcoreManifestById[selectedId] - if (isManifestNotInstalled && manifestEntry != null) { - launchManifestContentInstall( - manifestEntry, - ContentProfile.ContentType.CONTENT_TYPE_FEXCORE, - ) { - config = config.copy(fexcoreVersion = selectedId) - } - return@SettingsListDropdown - } - config = config.copy(fexcoreVersion = selectedId.ifEmpty { fexcoreOptions.labels[idx] }) - }, - ) - } - } - } - - // 64-bit Emulator (locked based on wine arch) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.emulator_64bit)) }, - value = emulator64Index, - items = emulatorEntries, - enabled = false, // Always non-editable per requirements - onItemSelected = { /* locked */ }, - ) - // Ensure correct locked value displayed - LaunchedEffect(wineIsX8664, wineIsArm64Ec) { - emulator64Index = if (wineIsX8664) 1 else 0 - } - - // 32-bit Emulator - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.emulator_32bit)) }, - value = emulator32Index, - items = emulatorEntries, - enabled = when { - wineIsX8664 -> false // locked to Box64 - wineIsArm64Ec -> true // editable between FEXCore and Box64 - else -> true - }, - onItemSelected = { idx -> - emulator32Index = idx - // Persist to config.emulator - config = config.copy(emulator = emulatorEntries[idx]) - }, - ) - // Enforce locking defaults when variant/wine changes - LaunchedEffect(wineIsX8664) { - if (wineIsX8664) { - emulator32Index = 1 - if (config.emulator != emulatorEntries[1]) { - config = config.copy(emulator = emulatorEntries[1]) - } - } - } - LaunchedEffect(wineIsArm64Ec) { - if (wineIsArm64Ec) { - if (emulator32Index !in 0..1) emulator32Index = 0 - if (config.emulator.isEmpty()) { - config = config.copy(emulator = emulatorEntries[0]) - } - } - } - } - val box64OptionList = getVersionsForBox64() - val box64Index = box64OptionList.ids.indexOfFirst { it == config.box64Version }.coerceAtLeast(0) - val box64ManifestMap = if (config.wineVersion.contains("arm64ec", true)) { - wowBox64ManifestById - } else { - box64ManifestById - } - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.box64_version)) }, - value = box64Index, - items = box64OptionList.labels, - itemMuted = box64OptionList.muted, - onItemSelected = { idx -> - val selectedId = box64OptionList.ids.getOrNull(idx).orEmpty() - val isManifestNotInstalled = box64OptionList.muted.getOrNull(idx) == true - val manifestEntry = box64ManifestMap[selectedId.lowercase(Locale.ENGLISH)] - if (isManifestNotInstalled && manifestEntry != null) { - val expectedType = if (config.wineVersion.contains("arm64ec", true)) { - ContentProfile.ContentType.CONTENT_TYPE_WOWBOX64 - } else { - ContentProfile.ContentType.CONTENT_TYPE_BOX64 - } - launchManifestContentInstall(manifestEntry, expectedType) { - config = config.copy(box64Version = selectedId) - } - return@SettingsListDropdown - } - config = config.copy(box64Version = selectedId.ifEmpty { StringUtils.parseIdentifier(box64OptionList.labels[idx]) }) - }, - ) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.box64_preset)) }, - value = box64Presets.indexOfFirst { it.id == config.box64Preset }, - items = box64Presets.map { it.name }, - onItemSelected = { - config = config.copy( - box64Preset = box64Presets[it].id, - ) - }, - ) - // FEXCore Preset (only when Bionic + Wine arm64ec) - if (config.containerVariant.equals(Container.BIONIC, ignoreCase = true) - && config.wineVersion.contains("arm64ec", ignoreCase = true)) { - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.fexcore_preset)) }, - value = fexcorePresets.indexOfFirst { it.id == config.fexcorePreset }.coerceAtLeast(0), - items = fexcorePresets.map { it.name }, - onItemSelected = { - config = config.copy( - fexcorePreset = fexcorePresets[it].id, - ) - }, - ) - } - } - if (selectedTab == 3) SettingsGroup() { - if (!default) { - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.use_sdl_api)) }, - state = config.sdlControllerAPI, - onCheckedChange = { - config = config.copy(sdlControllerAPI = it) - }, - ) - } - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.use_steam_input)) }, - state = config.useSteamInput, - onCheckedChange = { - config = config.copy(useSteamInput = it) - }, - ) - // Enable XInput API - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.enable_xinput_api)) }, - state = config.enableXInput, - onCheckedChange = { - config = config.copy(enableXInput = it) - } - ) - // Enable DirectInput API - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.enable_directinput_api)) }, - state = config.enableDInput, - onCheckedChange = { - config = config.copy(enableDInput = it) - } - ) - // DirectInput Mapper Type - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.directinput_mapper_type)) }, - value = if (config.dinputMapperType == 1.toByte()) 0 else 1, - items = listOf("Standard", "XInput Mapper"), - onItemSelected = { index -> - config = config.copy(dinputMapperType = if (index == 0) 1 else 2) - } - ) - // Disable external mouse input - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.disable_mouse_input)) }, - state = config.disableMouseInput, - onCheckedChange = { config = config.copy(disableMouseInput = it) } - ) - - // Touchscreen mode - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.touchscreen_mode)) }, - subtitle = { Text(text = stringResource(R.string.touchscreen_mode_description)) }, - state = config.touchscreenMode, - onCheckedChange = { config = config.copy(touchscreenMode = it) } - ) - // External display handling - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.external_display_input)) }, - subtitle = { Text(text = stringResource(R.string.external_display_input_subtitle)) }, - value = externalDisplayModeIndex, - items = externalDisplayModes, - onItemSelected = { index -> - externalDisplayModeIndex = index - config = config.copy( - externalDisplayMode = when (index) { - 1 -> Container.EXTERNAL_DISPLAY_MODE_TOUCHPAD - 2 -> Container.EXTERNAL_DISPLAY_MODE_KEYBOARD - 3 -> Container.EXTERNAL_DISPLAY_MODE_HYBRID - else -> Container.EXTERNAL_DISPLAY_MODE_OFF - }, - ) - }, - ) - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.external_display_swap)) }, - subtitle = { Text(text = stringResource(R.string.external_display_swap_subtitle)) }, - state = config.externalDisplaySwap, - onCheckedChange = { config = config.copy(externalDisplaySwap = it) } - ) - } - if (selectedTab == 4) SettingsGroup() { - // TODO: add desktop settings - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.renderer)) }, - value = gpuNameIndex, - items = gpuCards.values.map { it.name }, - onItemSelected = { - gpuNameIndex = it - config = config.copy(videoPciDeviceID = gpuCards.values.toList()[it].deviceId) - }, - ) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.gpu_name)) }, - value = gpuNameIndex, - items = gpuCards.values.map { it.name }, - onItemSelected = { - gpuNameIndex = it - config = config.copy(videoPciDeviceID = gpuCards.values.toList()[it].deviceId) - }, - ) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.offscreen_rendering_mode)) }, - value = renderingModeIndex, - items = renderingModes, - onItemSelected = { - renderingModeIndex = it - config = config.copy(offScreenRenderingMode = renderingModes[it].lowercase()) - }, - ) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.video_memory_size)) }, - value = videoMemIndex, - items = videoMemSizes, - onItemSelected = { - videoMemIndex = it - config = config.copy(videoMemorySize = StringUtils.parseNumber(videoMemSizes[it])) - }, - ) - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.enable_csmt)) }, - state = config.csmt, - onCheckedChange = { - config = config.copy(csmt = it) - }, - ) - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(text = stringResource(R.string.enable_strict_shader_math)) }, - state = config.strictShaderMath, - onCheckedChange = { - config = config.copy(strictShaderMath = it) - }, - ) - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.mouse_warp_override)) }, - value = mouseWarpIndex, - items = mouseWarps, - onItemSelected = { - mouseWarpIndex = it - config = config.copy(mouseWarpOverride = mouseWarps[it].lowercase()) - }, - ) - } - if (selectedTab == 5) SettingsGroup() { - for (wincomponent in KeyValueSet(config.wincomponents)) { - val compId = wincomponent[0] - val compNameRes = winComponentsItemTitleRes(compId) - val compValue = wincomponent[1].toInt() - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(stringResource(id = compNameRes)) }, - subtitle = { Text(if (compId.startsWith("direct")) "DirectX" else "General") }, - value = compValue, - items = winCompOpts, - onItemSelected = { - config = config.copy( - wincomponents = config.wincomponents.replace("$compId=$compValue", "$compId=$it"), - ) - }, - ) - } - } - if (selectedTab == 6) SettingsGroup() { - val envVars = EnvVars(config.envVars) - if (config.envVars.isNotEmpty()) { - SettingsEnvVars( - colors = settingsTileColors(), - envVars = envVars, - onEnvVarsChange = { - config = config.copy(envVars = it.toString()) - }, - knownEnvVars = EnvVarInfo.KNOWN_ENV_VARS, - envVarAction = { - IconButton( - onClick = { - envVars.remove(it) - config = config.copy( - envVars = envVars.toString(), - ) - }, - content = { - Icon(Icons.Filled.Delete, contentDescription = "Delete variable") - }, - ) - }, - ) - } else { - SettingsCenteredLabel( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.no_environment_variables)) }, - ) - } - SettingsMenuLink( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - ) { - Icon( - imageVector = Icons.Outlined.AddCircleOutline, - contentDescription = "Add environment variable", - ) - } - }, - onClick = { - showEnvVarCreateDialog = true - }, - ) - } - if (selectedTab == 7) SettingsGroup() { - // TODO: make the game drive un-deletable - // val directoryLauncher = rememberLauncherForActivityResult( - // ActivityResultContracts.OpenDocumentTree() - // ) { uri -> - // uri?.let { - // // Handle the selected directory URI - // val driveLetter = Container.getNextAvailableDriveLetter(config.drives) - // config = config.copy(drives = "${config.drives}$driveLetter:${uri.path}") - // } - // } - - if (config.drives.isNotEmpty()) { - for (drive in Container.drivesIterator(config.drives)) { - val driveLetter = drive[0] - val drivePath = drive[1] - SettingsMenuLink( - colors = settingsTileColors(), - title = { Text(driveLetter) }, - subtitle = { Text(drivePath) }, - onClick = {}, - action = if (driveLetter !in nonDeletableDriveLetters) { - { - IconButton( - onClick = { - // Rebuild drives string excluding the drive to delete - val drivesBuilder = StringBuilder() - for (existingDrive in Container.drivesIterator(config.drives)) { - if (existingDrive[0] != driveLetter) { - drivesBuilder.append("${existingDrive[0]}:${existingDrive[1]}") - } - } - config = config.copy( - drives = drivesBuilder.toString(), - ) - }, - content = { - Icon( - Icons.Filled.Delete, - contentDescription = "Delete drive", - tint = MaterialTheme.colorScheme.error, - ) - }, - ) - } - } else { - null - }, - ) - } - } else { - SettingsCenteredLabel( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.no_drives)) }, - ) - } - - SettingsMenuLink( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - ) { - Icon( - imageVector = Icons.Outlined.AddCircleOutline, - contentDescription = "Add environment variable", - ) - } - }, - onClick = { - if (availableDriveLetters.isEmpty()) { - Toast.makeText( - context, - context.getString(R.string.no_available_drive_letters), - Toast.LENGTH_SHORT, - ).show() - return@SettingsMenuLink - } - selectedDriveLetter = availableDriveLetters.first() - driveLetterMenuExpanded = false - showAddDriveDialog = true - }, - ) - } - if (selectedTab == 8) SettingsGroup() { - SettingsListDropdown( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.startup_selection)) }, - value = config.startupSelection.toInt().takeIf { it in getStartupSelectionOptions().indices } ?: 1, - items = getStartupSelectionOptions(), - onItemSelected = { - config = config.copy( - startupSelection = it.toByte(), - ) - }, - ) - SettingsCPUList( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.processor_affinity)) }, - value = config.cpuList, - onValueChange = { - config = config.copy( - cpuList = it, - ) - }, - ) - SettingsCPUList( - colors = settingsTileColors(), - title = { Text(text = stringResource(R.string.processor_affinity_32bit)) }, - value = config.cpuListWoW64, - onValueChange = { config = config.copy(cpuListWoW64 = it) }, - ) - } + if (selectedTab == 0) GeneralTabContent(state, nonzeroResolutionError, aspectResolutionError) + if (selectedTab == 1) GraphicsTabContent(state) + if (selectedTab == 2) EmulationTabContent(state) + if (selectedTab == 3) ControllerTabContent(state, default) + if (selectedTab == 4) WineTabContent(state) + if (selectedTab == 5) WinComponentsTabContent(state) + if (selectedTab == 6) EnvironmentTabContent(state) + if (selectedTab == 7) DrivesTabContent(state) + if (selectedTab == 8) AdvancedTabContent(state) } } } @@ -2516,7 +1217,7 @@ private fun Preview_ContainerConfigDialog() { */ @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun ExecutablePathDropdown( +internal fun ExecutablePathDropdown( modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigState.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigState.kt new file mode 100644 index 000000000..555f9150d --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigState.kt @@ -0,0 +1,138 @@ +package app.gamenative.ui.component.dialog + +import androidx.compose.runtime.MutableIntState +import androidx.compose.runtime.MutableState +import app.gamenative.utils.ContainerUtils +import app.gamenative.utils.ManifestComponentHelper +import app.gamenative.utils.ManifestEntry +import com.winlator.box86_64.Box86_64Preset +import com.winlator.contents.ContentProfile +import com.winlator.container.ContainerData +import com.winlator.fexcore.FEXCorePreset + +/** + * State holder for ContainerConfigDialog. Built inside the dialog and passed to each tab composable. + * Holds references to all mutable state and read-only data so tabs can read/write without 50+ parameters. + */ +class ContainerConfigState( + val config: MutableState, + val graphicsDrivers: MutableState>, + val bionicWineEntries: MutableState>, + val glibcWineEntries: MutableState>, + val wrapperVersions: MutableState>, + val dxvkVersionsAll: MutableState>, + val componentAvailability: MutableState, + val showCustomResolutionDialog: MutableState, + val customResolutionValidationError: MutableState, + val vkMaxVersionIndex: MutableIntState, + val imageCacheIndex: MutableIntState, + val exposedExtIndices: MutableState>, + val maxDeviceMemoryIndex: MutableIntState, + val bionicDriverIndex: MutableIntState, + val wrapperVersionIndex: MutableIntState, + val presentModeIndex: MutableIntState, + val resourceTypeIndex: MutableIntState, + val bcnEmulationIndex: MutableIntState, + val bcnEmulationTypeIndex: MutableIntState, + val bcnEmulationCacheEnabled: MutableState, + val disablePresentWaitChecked: MutableState, + val syncEveryFrameChecked: MutableState, + val sharpnessEffectIndex: MutableIntState, + val sharpnessLevel: MutableIntState, + val sharpnessDenoise: MutableIntState, + val adrenotoolsTurnipChecked: MutableState, + val emulator64Index: MutableIntState, + val emulator32Index: MutableIntState, + val screenSizeIndex: MutableIntState, + val customScreenWidth: MutableState, + val customScreenHeight: MutableState, + val graphicsDriverIndex: MutableIntState, + val dxWrapperIndex: MutableIntState, + val dxvkVersionIndex: MutableIntState, + val graphicsDriverVersionIndex: MutableIntState, + val audioDriverIndex: MutableIntState, + val gpuNameIndex: MutableIntState, + val renderingModeIndex: MutableIntState, + val videoMemIndex: MutableIntState, + val mouseWarpIndex: MutableIntState, + val externalDisplayModeIndex: MutableIntState, + val languageIndex: MutableIntState, + val showEnvVarCreateDialog: MutableState, + val showAddDriveDialog: MutableState, + val selectedDriveLetter: MutableState, + val pendingDriveLetter: MutableState, + val driveLetterMenuExpanded: MutableState, + val screenSizes: List, + val baseGraphicsDrivers: List, + val dxWrappers: List, + val dxvkVersionsBase: List, + val vkd3dVersionsBase: List, + val audioDrivers: List, + val presentModes: List, + val resourceTypes: List, + val bcnEmulationEntries: List, + val bcnEmulationTypeEntries: List, + val sharpnessEffects: List, + val sharpnessDisplayItems: List, + val renderingModes: List, + val videoMemSizes: List, + val mouseWarps: List, + val externalDisplayModes: List, + val winCompOpts: List, + val box64Versions: List, + val wowBox64VersionsBase: List, + val box64BionicVersionsBase: List, + val fexcoreVersionsBase: List, + val fexcoreTSOPresets: List, + val fexcoreX87Presets: List, + val fexcoreMultiblockValues: List, + val startupSelectionEntries: List, + val turnipVersions: List, + val virglVersions: List, + val zinkVersions: List, + val vortekVersions: List, + val adrenoVersions: List, + val sd8EliteVersions: List, + val containerVariants: List, + val bionicWineEntriesBase: List, + val glibcWineEntriesBase: List, + val emulatorEntries: List, + val bionicGraphicsDrivers: List, + val baseWrapperVersions: List, + val languages: List, + val dxvkOptions: ManifestComponentHelper.VersionOptionList, + val vkd3dOptions: ManifestComponentHelper.VersionOptionList, + val box64Options: ManifestComponentHelper.VersionOptionList, + val box64BionicOptions: ManifestComponentHelper.VersionOptionList, + val wowBox64Options: ManifestComponentHelper.VersionOptionList, + val fexcoreOptions: ManifestComponentHelper.VersionOptionList, + val wrapperOptions: ManifestComponentHelper.VersionOptionList, + val bionicWineOptions: ManifestComponentHelper.VersionOptionList, + val glibcWineOptions: ManifestComponentHelper.VersionOptionList, + val dxvkManifestById: Map, + val vkd3dManifestById: Map, + val box64ManifestById: Map, + val wowBox64ManifestById: Map, + val fexcoreManifestById: Map, + val wrapperManifestById: Map, + val bionicWineManifestById: Map, + val glibcWineManifestById: Map, + val gpuCards: Map, + val box64Presets: List, + val fexcorePresets: List, + val gpuExtensions: List, + val inspectionMode: Boolean, + val isBionicVariant: Boolean, + val nonDeletableDriveLetters: Set, + val availableDriveLetters: List, + val launchManifestInstall: (ManifestEntry, String, Boolean, ContentProfile.ContentType?, () -> Unit) -> Unit, + val launchManifestContentInstall: (ManifestEntry, ContentProfile.ContentType, () -> Unit) -> Unit, + val launchManifestDriverInstall: (ManifestEntry, () -> Unit) -> Unit, + val getStartupSelectionOptions: () -> List, + val launchFolderPicker: () -> Unit, + val getVersionsForDriver: () -> List, + val getVersionsForBox64: () -> ManifestComponentHelper.VersionOptionList, + val applyScreenSizeToConfig: () -> Unit, + val vkd3dForcedVersion: () -> String, + val currentDxvkContext: () -> ManifestComponentHelper.DxvkContext, +) diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ControllerTab.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ControllerTab.kt new file mode 100644 index 000000000..2c2cf65ce --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ControllerTab.kt @@ -0,0 +1,92 @@ +package app.gamenative.ui.component.dialog + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import app.gamenative.R +import app.gamenative.ui.component.settings.SettingsListDropdown +import app.gamenative.ui.theme.settingsTileColors +import app.gamenative.ui.theme.settingsTileColorsAlt +import com.alorma.compose.settings.ui.SettingsGroup +import com.alorma.compose.settings.ui.SettingsSwitch +import com.winlator.container.Container + +@Composable +fun ControllerTabContent(state: ContainerConfigState, default: Boolean) { + val config = state.config.value + SettingsGroup() { + if (!default) { + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.use_sdl_api)) }, + state = config.sdlControllerAPI, + onCheckedChange = { state.config.value = config.copy(sdlControllerAPI = it) }, + ) + } + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.use_steam_input)) }, + state = config.useSteamInput, + onCheckedChange = { state.config.value = config.copy(useSteamInput = it) }, + ) + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.enable_xinput_api)) }, + state = config.enableXInput, + onCheckedChange = { state.config.value = config.copy(enableXInput = it) }, + ) + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.enable_directinput_api)) }, + state = config.enableDInput, + onCheckedChange = { state.config.value = config.copy(enableDInput = it) }, + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.directinput_mapper_type)) }, + value = if (config.dinputMapperType == 1.toByte()) 0 else 1, + items = listOf("Standard", "XInput Mapper"), + onItemSelected = { index -> + state.config.value = config.copy(dinputMapperType = if (index == 0) 1 else 2) + }, + ) + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.disable_mouse_input)) }, + state = config.disableMouseInput, + onCheckedChange = { state.config.value = config.copy(disableMouseInput = it) }, + ) + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.touchscreen_mode)) }, + subtitle = { Text(text = stringResource(R.string.touchscreen_mode_description)) }, + state = config.touchscreenMode, + onCheckedChange = { state.config.value = config.copy(touchscreenMode = it) }, + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.external_display_input)) }, + subtitle = { Text(text = stringResource(R.string.external_display_input_subtitle)) }, + value = state.externalDisplayModeIndex.value, + items = state.externalDisplayModes, + onItemSelected = { index -> + state.externalDisplayModeIndex.value = index + state.config.value = config.copy( + externalDisplayMode = when (index) { + 1 -> Container.EXTERNAL_DISPLAY_MODE_TOUCHPAD + 2 -> Container.EXTERNAL_DISPLAY_MODE_KEYBOARD + 3 -> Container.EXTERNAL_DISPLAY_MODE_HYBRID + else -> Container.EXTERNAL_DISPLAY_MODE_OFF + }, + ) + }, + ) + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.external_display_swap)) }, + subtitle = { Text(text = stringResource(R.string.external_display_swap_subtitle)) }, + state = config.externalDisplaySwap, + onCheckedChange = { state.config.value = config.copy(externalDisplaySwap = it) }, + ) + } +} diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/DrivesTab.kt b/app/src/main/java/app/gamenative/ui/component/dialog/DrivesTab.kt new file mode 100644 index 000000000..aba197894 --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/component/dialog/DrivesTab.kt @@ -0,0 +1,174 @@ +package app.gamenative.ui.component.dialog + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ViewList +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.outlined.AddCircleOutline +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.gamenative.R +import app.gamenative.ui.component.settings.SettingsCenteredLabel +import app.gamenative.ui.theme.settingsTileColors +import com.alorma.compose.settings.ui.SettingsGroup +import com.alorma.compose.settings.ui.SettingsMenuLink +import com.winlator.container.Container + +@Composable +fun DrivesTabContent(state: ContainerConfigState) { + val context = LocalContext.current + val config = state.config.value + SettingsGroup() { + if (config.drives.isNotEmpty()) { + for (drive in Container.drivesIterator(config.drives)) { + val driveLetter = drive[0] + val drivePath = drive[1] + SettingsMenuLink( + colors = settingsTileColors(), + title = { Text(driveLetter) }, + subtitle = { Text(drivePath) }, + onClick = {}, + action = if (driveLetter !in state.nonDeletableDriveLetters) { + { + IconButton( + onClick = { + val drivesBuilder = StringBuilder() + for (existingDrive in Container.drivesIterator(config.drives)) { + if (existingDrive[0] != driveLetter) { + drivesBuilder.append("${existingDrive[0]}:${existingDrive[1]}") + } + } + state.config.value = config.copy(drives = drivesBuilder.toString()) + }, + content = { + Icon( + Icons.Filled.Delete, + contentDescription = "Delete drive", + tint = MaterialTheme.colorScheme.error, + ) + }, + ) + } + } else { + null + }, + ) + } + } else { + SettingsCenteredLabel( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.no_drives)) }, + ) + } + + SettingsMenuLink( + title = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = Icons.Outlined.AddCircleOutline, + contentDescription = "Add drive", + ) + } + }, + onClick = { + if (state.availableDriveLetters.isEmpty()) { + Toast.makeText( + context, + context.getString(R.string.no_available_drive_letters), + Toast.LENGTH_SHORT, + ).show() + return@SettingsMenuLink + } + state.selectedDriveLetter.value = state.availableDriveLetters.first() + state.driveLetterMenuExpanded.value = false + state.showAddDriveDialog.value = true + }, + ) + } + + if (state.showAddDriveDialog.value) { + AlertDialog( + onDismissRequest = { state.showAddDriveDialog.value = false }, + title = { Text(text = stringResource(R.string.add_drive)) }, + text = { + Column { + OutlinedTextField( + value = state.selectedDriveLetter.value, + onValueChange = {}, + readOnly = true, + label = { Text(text = stringResource(R.string.drive_letter)) }, + trailingIcon = { + IconButton( + onClick = { state.driveLetterMenuExpanded.value = true }, + content = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ViewList, + contentDescription = null, + ) + }, + ) + }, + ) + DropdownMenu( + expanded = state.driveLetterMenuExpanded.value, + onDismissRequest = { state.driveLetterMenuExpanded.value = false }, + ) { + state.availableDriveLetters.forEach { letter -> + DropdownMenuItem( + text = { Text(text = letter) }, + onClick = { + state.selectedDriveLetter.value = letter + state.driveLetterMenuExpanded.value = false + }, + ) + } + } + if (state.availableDriveLetters.isEmpty()) { + Text( + text = stringResource(R.string.no_available_drive_letters), + color = MaterialTheme.colorScheme.error, + style = TextStyle(fontSize = 14.sp), + modifier = Modifier.padding(top = 8.dp), + ) + } + } + }, + confirmButton = { + TextButton( + enabled = state.selectedDriveLetter.value.isNotBlank() && + state.availableDriveLetters.contains(state.selectedDriveLetter.value), + onClick = { state.launchFolderPicker() }, + content = { Text(text = stringResource(R.string.ok)) }, + ) + }, + dismissButton = { + TextButton( + onClick = { state.showAddDriveDialog.value = false }, + content = { Text(text = stringResource(R.string.cancel)) }, + ) + }, + ) + } +} diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/EmulationTab.kt b/app/src/main/java/app/gamenative/ui/component/dialog/EmulationTab.kt new file mode 100644 index 000000000..e9ff796bf --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/component/dialog/EmulationTab.kt @@ -0,0 +1,149 @@ +package app.gamenative.ui.component.dialog + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.res.stringResource +import app.gamenative.R +import app.gamenative.ui.component.settings.SettingsListDropdown +import app.gamenative.ui.theme.settingsTileColors +import com.alorma.compose.settings.ui.SettingsGroup +import com.winlator.contents.ContentProfile +import com.winlator.container.Container +import com.winlator.core.StringUtils +import java.util.Locale + +@Composable +fun EmulationTabContent(state: ContainerConfigState) { + val config = state.config.value + val wineIsX8664 = config.wineVersion.contains("x86_64", true) + val wineIsArm64Ec = config.wineVersion.contains("arm64ec", true) + + SettingsGroup() { + if (config.containerVariant.equals(Container.BIONIC, ignoreCase = true)) { + if (wineIsArm64Ec) { + SettingsGroup() { + val fexcoreIndex = state.fexcoreOptions.ids.indexOfFirst { it == config.fexcoreVersion }.coerceAtLeast(0) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.fexcore_version)) }, + value = fexcoreIndex, + items = state.fexcoreOptions.labels, + itemMuted = state.fexcoreOptions.muted, + onItemSelected = { idx -> + val selectedId = state.fexcoreOptions.ids.getOrNull(idx).orEmpty() + val isManifestNotInstalled = state.fexcoreOptions.muted.getOrNull(idx) == true + val manifestEntry = state.fexcoreManifestById[selectedId] + if (isManifestNotInstalled && manifestEntry != null) { + state.launchManifestContentInstall( + manifestEntry, + ContentProfile.ContentType.CONTENT_TYPE_FEXCORE, + ) { + state.config.value = config.copy(fexcoreVersion = selectedId) + } + return@SettingsListDropdown + } + state.config.value = config.copy(fexcoreVersion = selectedId.ifEmpty { state.fexcoreOptions.labels[idx] }) + }, + ) + } + } + + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.emulator_64bit)) }, + value = state.emulator64Index.value, + items = state.emulatorEntries, + enabled = false, + onItemSelected = { }, + ) + LaunchedEffect(wineIsX8664, wineIsArm64Ec) { + state.emulator64Index.value = if (wineIsX8664) 1 else 0 + } + + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.emulator_32bit)) }, + value = state.emulator32Index.value, + items = state.emulatorEntries, + enabled = when { + wineIsX8664 -> false + wineIsArm64Ec -> true + else -> true + }, + onItemSelected = { idx -> + state.emulator32Index.value = idx + state.config.value = config.copy(emulator = state.emulatorEntries[idx]) + }, + ) + LaunchedEffect(wineIsX8664) { + if (wineIsX8664) { + state.emulator32Index.value = 1 + if (config.emulator != state.emulatorEntries[1]) { + state.config.value = config.copy(emulator = state.emulatorEntries[1]) + } + } + } + LaunchedEffect(wineIsArm64Ec) { + if (wineIsArm64Ec) { + if (state.emulator32Index.value !in 0..1) state.emulator32Index.value = 0 + if (config.emulator.isEmpty()) { + state.config.value = config.copy(emulator = state.emulatorEntries[0]) + } + } + } + } + val box64OptionList = state.getVersionsForBox64() + val box64Index = box64OptionList.ids.indexOfFirst { it == config.box64Version }.coerceAtLeast(0) + val box64ManifestMap = if (config.wineVersion.contains("arm64ec", true)) { + state.wowBox64ManifestById + } else { + state.box64ManifestById + } + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.box64_version)) }, + value = box64Index, + items = box64OptionList.labels, + itemMuted = box64OptionList.muted, + onItemSelected = { idx -> + val selectedId = box64OptionList.ids.getOrNull(idx).orEmpty() + val isManifestNotInstalled = box64OptionList.muted.getOrNull(idx) == true + val manifestEntry = box64ManifestMap[selectedId.lowercase(Locale.ENGLISH)] + if (isManifestNotInstalled && manifestEntry != null) { + val expectedType = if (config.wineVersion.contains("arm64ec", true)) { + ContentProfile.ContentType.CONTENT_TYPE_WOWBOX64 + } else { + ContentProfile.ContentType.CONTENT_TYPE_BOX64 + } + state.launchManifestContentInstall(manifestEntry, expectedType) { + state.config.value = config.copy(box64Version = selectedId) + } + return@SettingsListDropdown + } + state.config.value = config.copy(box64Version = selectedId.ifEmpty { StringUtils.parseIdentifier(box64OptionList.labels[idx]) }) + }, + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.box64_preset)) }, + value = state.box64Presets.indexOfFirst { it.id == config.box64Preset }.coerceAtLeast(0), + items = state.box64Presets.map { it.name }, + onItemSelected = { + state.config.value = config.copy(box64Preset = state.box64Presets[it].id) + }, + ) + if (config.containerVariant.equals(Container.BIONIC, ignoreCase = true) + && config.wineVersion.contains("arm64ec", ignoreCase = true)) { + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.fexcore_preset)) }, + value = state.fexcorePresets.indexOfFirst { it.id == config.fexcorePreset }.coerceAtLeast(0), + items = state.fexcorePresets.map { it.name }, + onItemSelected = { + state.config.value = config.copy(fexcorePreset = state.fexcorePresets[it].id) + }, + ) + } + } +} diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/EnvironmentTab.kt b/app/src/main/java/app/gamenative/ui/component/dialog/EnvironmentTab.kt new file mode 100644 index 000000000..f04240122 --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/component/dialog/EnvironmentTab.kt @@ -0,0 +1,184 @@ +package app.gamenative.ui.component.dialog + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ViewList +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.outlined.AddCircleOutline +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import app.gamenative.R +import app.gamenative.ui.component.settings.SettingsCenteredLabel +import app.gamenative.ui.component.settings.SettingsEnvVars +import app.gamenative.ui.component.settings.SettingsMultiListDropdown +import app.gamenative.ui.theme.settingsTileColors +import com.alorma.compose.settings.ui.SettingsGroup +import com.alorma.compose.settings.ui.SettingsMenuLink +import com.winlator.core.envvars.EnvVarInfo +import com.winlator.core.envvars.EnvVars +import com.winlator.core.envvars.EnvVarSelectionType + +@Composable +fun EnvironmentTabContent(state: ContainerConfigState) { + val config = state.config.value + val envVars = EnvVars(config.envVars) + SettingsGroup() { + if (config.envVars.isNotEmpty()) { + SettingsEnvVars( + colors = settingsTileColors(), + envVars = envVars, + onEnvVarsChange = { + state.config.value = config.copy(envVars = it.toString()) + }, + knownEnvVars = EnvVarInfo.KNOWN_ENV_VARS, + envVarAction = { + IconButton( + onClick = { + envVars.remove(it) + state.config.value = config.copy(envVars = envVars.toString()) + }, + content = { + Icon(Icons.Filled.Delete, contentDescription = "Delete variable") + }, + ) + }, + ) + } else { + SettingsCenteredLabel( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.no_environment_variables)) }, + ) + } + SettingsMenuLink( + title = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = Icons.Outlined.AddCircleOutline, + contentDescription = "Add environment variable", + ) + } + }, + onClick = { state.showEnvVarCreateDialog.value = true }, + ) + } + + if (state.showEnvVarCreateDialog.value) { + var envVarName by rememberSaveable { mutableStateOf("") } + var envVarValue by rememberSaveable { mutableStateOf("") } + val config = state.config.value + AlertDialog( + onDismissRequest = { state.showEnvVarCreateDialog.value = false }, + title = { Text(text = stringResource(R.string.new_environment_variable)) }, + text = { + var knownVarsMenuOpen by rememberSaveable { mutableStateOf(false) } + Column { + Row { + OutlinedTextField( + value = envVarName, + onValueChange = { envVarName = it }, + label = { Text(text = stringResource(R.string.name)) }, + trailingIcon = { + IconButton( + onClick = { knownVarsMenuOpen = true }, + content = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ViewList, + contentDescription = "List known variable names", + ) + }, + ) + }, + ) + androidx.compose.material3.DropdownMenu( + expanded = knownVarsMenuOpen, + onDismissRequest = { knownVarsMenuOpen = false }, + ) { + val knownEnvVars = EnvVarInfo.KNOWN_ENV_VARS.values.filter { + !config.envVars.contains("${it.identifier}=") + } + if (knownEnvVars.isNotEmpty()) { + for (knownVariable in knownEnvVars) { + androidx.compose.material3.DropdownMenuItem( + text = { Text(knownVariable.identifier) }, + onClick = { + envVarName = knownVariable.identifier + knownVarsMenuOpen = false + }, + ) + } + } else { + androidx.compose.material3.DropdownMenuItem( + text = { Text(text = stringResource(R.string.no_more_known_variables)) }, + onClick = {}, + ) + } + } + } + val selectedEnvVarInfo = EnvVarInfo.KNOWN_ENV_VARS[envVarName] + if (selectedEnvVarInfo?.selectionType == EnvVarSelectionType.MULTI_SELECT) { + var multiSelectedIndices by remember { mutableStateOf(listOf()) } + SettingsMultiListDropdown( + enabled = true, + values = multiSelectedIndices, + items = selectedEnvVarInfo.possibleValues, + fallbackDisplay = "", + onItemSelected = { index -> + val newIndices = if (multiSelectedIndices.contains(index)) { + multiSelectedIndices.filter { it != index } + } else { + multiSelectedIndices + index + } + multiSelectedIndices = newIndices + envVarValue = newIndices.joinToString(",") { selectedEnvVarInfo.possibleValues[it] } + }, + title = { Text(text = stringResource(R.string.value)) }, + colors = settingsTileColors(), + ) + } else { + OutlinedTextField( + value = envVarValue, + onValueChange = { envVarValue = it }, + label = { Text(text = stringResource(R.string.value)) }, + ) + } + } + }, + dismissButton = { + TextButton( + onClick = { state.showEnvVarCreateDialog.value = false }, + content = { Text(text = stringResource(R.string.cancel)) }, + ) + }, + confirmButton = { + TextButton( + enabled = envVarName.isNotEmpty(), + onClick = { + val envVars = EnvVars(config.envVars) + envVars.put(envVarName, envVarValue) + state.config.value = config.copy(envVars = envVars.toString()) + state.showEnvVarCreateDialog.value = false + }, + content = { Text(text = stringResource(R.string.ok)) }, + ) + }, + ) + } +} diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt b/app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt new file mode 100644 index 000000000..0fcee27a2 --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt @@ -0,0 +1,364 @@ +package app.gamenative.ui.component.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.gamenative.R +import app.gamenative.ui.component.settings.SettingsListDropdown +import com.alorma.compose.settings.ui.SettingsSwitch +import app.gamenative.ui.theme.settingsTileColors +import app.gamenative.ui.theme.settingsTileColorsAlt +import com.alorma.compose.settings.ui.SettingsGroup +import com.winlator.container.Container +import com.winlator.container.ContainerData +import com.winlator.core.DefaultVersion +import com.winlator.core.KeyValueSet +import com.winlator.core.StringUtils +import com.winlator.contents.ContentProfile +import java.util.Locale + +@Composable +fun GeneralTabContent( + state: ContainerConfigState, + nonzeroResolutionError: String, + aspectResolutionError: String, +) { + val config = state.config.value + val graphicsDrivers = state.graphicsDrivers.value + val glibcWineEntries = state.glibcWineEntries.value + val bionicWineEntries = state.bionicWineEntries.value + + if (state.showCustomResolutionDialog.value) { + AlertDialog( + onDismissRequest = { state.showCustomResolutionDialog.value = false }, + title = { Text(text = stringResource(R.string.container_config_custom_resolution_title)) }, + text = { + Column { + Row { + OutlinedTextField( + modifier = Modifier.width(128.dp), + value = state.customScreenWidth.value, + onValueChange = { state.customScreenWidth.value = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + label = { Text(text = stringResource(R.string.width)) }, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = stringResource(R.string.container_config_custom_resolution_separator), + style = TextStyle(fontSize = 16.sp), + ) + Spacer(modifier = Modifier.width(8.dp)) + OutlinedTextField( + modifier = Modifier.width(128.dp), + value = state.customScreenHeight.value, + onValueChange = { state.customScreenHeight.value = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + label = { Text(text = stringResource(R.string.height)) }, + ) + } + if (state.customResolutionValidationError.value != null) { + Text( + text = state.customResolutionValidationError.value!!, + color = androidx.compose.material3.MaterialTheme.colorScheme.error, + style = TextStyle(fontSize = 16.sp), + modifier = Modifier.padding(top = 8.dp) + ) + } + } + }, + confirmButton = { + TextButton( + onClick = { + val widthInt = state.customScreenWidth.value.toIntOrNull() ?: 0 + val heightInt = state.customScreenHeight.value.toIntOrNull() ?: 0 + if (widthInt == 0 || heightInt == 0) { + state.customResolutionValidationError.value = nonzeroResolutionError + } else if (widthInt <= heightInt) { + state.customResolutionValidationError.value = aspectResolutionError + } else { + state.customResolutionValidationError.value = null + state.applyScreenSizeToConfig() + state.showCustomResolutionDialog.value = false + } + }, + ) { + Text(text = stringResource(R.string.ok)) + } + }, + dismissButton = { + TextButton( + onClick = { state.showCustomResolutionDialog.value = false }, + ) { + Text(text = stringResource(R.string.cancel)) + } + } + ) + } + + SettingsGroup() { + run { + val variantIndex = rememberSaveable { + mutableIntStateOf( + state.containerVariants.indexOfFirst { it.equals(config.containerVariant, true) }.coerceAtLeast(0) + ) + } + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.container_variant)) }, + value = variantIndex.value, + items = state.containerVariants, + onItemSelected = { idx -> + variantIndex.value = idx + val newVariant = state.containerVariants[idx] + if (newVariant.equals(Container.GLIBC, ignoreCase = true)) { + val defaultDriver = Container.DEFAULT_GRAPHICS_DRIVER + val newCfg = KeyValueSet(config.graphicsDriverConfig).apply { + put("version", "") + put("syncFrame", "0") + put("disablePresentWait", get("disablePresentWait").ifEmpty { "0" }) + if (get("presentMode").isEmpty()) put("presentMode", "mailbox") + if (get("resourceType").isEmpty()) put("resourceType", "auto") + if (get("bcnEmulation").isEmpty()) put("bcnEmulation", "auto") + if (get("bcnEmulationType").isEmpty()) put("bcnEmulationType", "compute") + if (get("bcnEmulationCache").isEmpty()) put("bcnEmulationCache", "0") + put("adrenotoolsTurnip", "1") + } + state.graphicsDriverIndex.value = + graphicsDrivers.indexOfFirst { StringUtils.parseIdentifier(it) == defaultDriver }.coerceAtLeast(0) + state.graphicsDriverVersionIndex.value = 0 + state.syncEveryFrameChecked.value = false + state.disablePresentWaitChecked.value = newCfg.get("disablePresentWait", "0") == "1" + state.bcnEmulationCacheEnabled.value = newCfg.get("bcnEmulationCache", "0") == "1" + state.adrenotoolsTurnipChecked.value = true + + val defaultGlibcWine = glibcWineEntries.firstOrNull() ?: Container.DEFAULT_WINE_VERSION + state.config.value = config.copy( + containerVariant = newVariant, + wineVersion = defaultGlibcWine, + graphicsDriver = defaultDriver, + graphicsDriverVersion = "", + graphicsDriverConfig = newCfg.toString(), + box64Version = "0.3.6", + ) + } else { + val defaultBionicDriver = StringUtils.parseIdentifier(state.bionicGraphicsDrivers.first()) + val newWine = if (config.wineVersion == (glibcWineEntries.firstOrNull() ?: Container.DEFAULT_WINE_VERSION)) + bionicWineEntries.firstOrNull() ?: config.wineVersion + else config.wineVersion + val newCfg = KeyValueSet(config.graphicsDriverConfig).apply { + put("version", DefaultVersion.WRAPPER) + put("syncFrame", "0") + put("adrenotoolsTurnip", "1") + put("disablePresentWait", get("disablePresentWait").ifEmpty { "0" }) + if (get("exposedDeviceExtensions").isEmpty()) put("exposedDeviceExtensions", "all") + if (get("maxDeviceMemory").isEmpty()) put("maxDeviceMemory", "4096") + if (get("presentMode").isEmpty()) put("presentMode", "mailbox") + if (get("resourceType").isEmpty()) put("resourceType", "auto") + if (get("bcnEmulation").isEmpty()) put("bcnEmulation", "auto") + if (get("bcnEmulationType").isEmpty()) put("bcnEmulationType", "compute") + if (get("bcnEmulationCache").isEmpty()) put("bcnEmulationCache", "0") + } + state.bionicDriverIndex.value = 0 + state.wrapperVersionIndex.value = state.wrapperOptions.ids + .indexOfFirst { it.equals(DefaultVersion.WRAPPER, true) } + .let { if (it >= 0) it else 0 } + state.syncEveryFrameChecked.value = false + state.disablePresentWaitChecked.value = newCfg.get("disablePresentWait", "0") == "1" + state.bcnEmulationCacheEnabled.value = newCfg.get("bcnEmulationCache", "0") == "1" + state.adrenotoolsTurnipChecked.value = true + state.maxDeviceMemoryIndex.value = + listOf("0", "512", "1024", "2048", "4096").indexOf("4096").coerceAtLeast(0) + + val currentConfig = KeyValueSet(config.dxwrapperConfig) + currentConfig.put("version", "async-1.10.3") + currentConfig.put("async", "1") + currentConfig.put("asyncCache", "0") + state.config.value = config.copy(dxwrapperConfig = currentConfig.toString()) + + state.config.value = config.copy( + containerVariant = newVariant, + wineVersion = newWine, + graphicsDriver = defaultBionicDriver, + graphicsDriverVersion = "", + graphicsDriverConfig = newCfg.toString(), + box64Version = "0.3.7", + dxwrapperConfig = currentConfig.toString(), + ) + } + }, + ) + if (config.containerVariant.equals(Container.BIONIC, ignoreCase = true)) { + val wineIndex = state.bionicWineOptions.ids.indexOfFirst { it == config.wineVersion }.coerceAtLeast(0) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.wine_version)) }, + value = wineIndex, + items = state.bionicWineOptions.labels, + itemMuted = state.bionicWineOptions.muted, + onItemSelected = { idx -> + val selectedId = state.bionicWineOptions.ids.getOrNull(idx).orEmpty() + val isManifestNotInstalled = state.bionicWineOptions.muted.getOrNull(idx) == true + val manifestEntry = state.bionicWineManifestById[selectedId] + if (isManifestNotInstalled && manifestEntry != null) { + val expectedType = if (selectedId.startsWith("proton", true)) { + ContentProfile.ContentType.CONTENT_TYPE_PROTON + } else { + ContentProfile.ContentType.CONTENT_TYPE_WINE + } + state.launchManifestContentInstall(manifestEntry, expectedType) { + state.config.value = config.copy(wineVersion = selectedId) + } + return@SettingsListDropdown + } + state.config.value = config.copy(wineVersion = selectedId.ifEmpty { state.bionicWineOptions.labels[idx] }) + }, + ) + } + } + ExecutablePathDropdown( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), + value = config.executablePath, + onValueChange = { state.config.value = config.copy(executablePath = it) }, + containerData = config, + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), + value = config.execArgs, + onValueChange = { state.config.value = config.copy(execArgs = it) }, + label = { Text(text = stringResource(R.string.exec_arguments)) }, + placeholder = { Text(text = stringResource(R.string.exec_arguments_example)) }, + ) + val displayNameForLanguage: (String) -> String = { code -> + when (code) { + "schinese" -> "Simplified Chinese" + "tchinese" -> "Traditional Chinese" + "koreana" -> "Korean" + "latam" -> "Spanish (Latin America)" + "brazilian" -> "Portuguese (Brazil)" + else -> code.replaceFirstChar { ch -> ch.titlecase(Locale.getDefault()) } + } + } + SettingsListDropdown( + enabled = true, + value = state.languageIndex.value, + items = state.languages.map(displayNameForLanguage), + fallbackDisplay = displayNameForLanguage("english"), + onItemSelected = { index -> + state.languageIndex.value = index + state.config.value = config.copy(language = state.languages[index]) + }, + title = { Text(text = stringResource(R.string.language)) }, + colors = settingsTileColors(), + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.screen_size)) }, + value = state.screenSizeIndex.value, + items = state.screenSizes, + onItemSelected = { + state.screenSizeIndex.value = it + if (it == 0) { + state.showCustomResolutionDialog.value = true + } else { + state.applyScreenSizeToConfig() + } + }, + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.audio_driver)) }, + value = state.audioDriverIndex.value, + items = state.audioDrivers, + onItemSelected = { + state.audioDriverIndex.value = it + state.config.value = config.copy(audioDriver = StringUtils.parseIdentifier(state.audioDrivers[it])) + }, + ) + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.show_fps)) }, + state = config.showFPS, + onCheckedChange = { state.config.value = config.copy(showFPS = it) }, + ) + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.force_dlc)) }, + subtitle = { Text(text = stringResource(R.string.force_dlc_description)) }, + state = config.forceDlc, + onCheckedChange = { state.config.value = config.copy(forceDlc = it) }, + ) + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.use_legacy_drm)) }, + state = config.useLegacyDRM, + onCheckedChange = { state.config.value = config.copy(useLegacyDRM = it) }, + ) + if (!config.useLegacyDRM) { + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.unpack_files)) }, + subtitle = { Text(text = stringResource(R.string.unpack_files_description)) }, + state = config.unpackFiles, + onCheckedChange = { state.config.value = config.copy(unpackFiles = it) }, + ) + } + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.launch_steam_client_beta)) }, + subtitle = { Text(text = stringResource(R.string.launch_steam_client_description)) }, + state = config.launchRealSteam, + onCheckedChange = { state.config.value = config.copy(launchRealSteam = it) }, + ) + if (config.launchRealSteam) { + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.allow_steam_updates)) }, + subtitle = { Text(text = stringResource(R.string.allow_steam_updates_description)) }, + state = config.allowSteamUpdates, + onCheckedChange = { state.config.value = config.copy(allowSteamUpdates = it) }, + ) + } + val steamTypeItems = listOf("Normal", "Light", "Ultra Light") + val currentSteamTypeIndex = when (config.steamType.lowercase()) { + Container.STEAM_TYPE_LIGHT -> 1 + Container.STEAM_TYPE_ULTRALIGHT -> 2 + else -> 0 + } + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.steam_type)) }, + value = currentSteamTypeIndex, + items = steamTypeItems, + onItemSelected = { + val type = when (it) { + 1 -> Container.STEAM_TYPE_LIGHT + 2 -> Container.STEAM_TYPE_ULTRALIGHT + else -> Container.STEAM_TYPE_NORMAL + } + state.config.value = config.copy(steamType = type) + }, + ) + } +} diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.kt b/app/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.kt new file mode 100644 index 000000000..43cc5a7e6 --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/component/dialog/GraphicsTab.kt @@ -0,0 +1,455 @@ +package app.gamenative.ui.component.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.gamenative.R +import app.gamenative.ui.component.settings.SettingsListDropdown +import app.gamenative.ui.component.settings.SettingsMultiListDropdown +import app.gamenative.ui.theme.settingsTileColors +import app.gamenative.ui.theme.settingsTileColorsAlt +import com.alorma.compose.settings.ui.SettingsGroup +import com.alorma.compose.settings.ui.SettingsSwitch +import com.winlator.contents.ContentProfile +import com.winlator.container.Container +import com.winlator.core.KeyValueSet +import com.winlator.core.StringUtils +import com.winlator.core.envvars.EnvVars +import kotlin.math.roundToInt + +@Composable +fun GraphicsTabContent(state: ContainerConfigState) { + val config = state.config.value + SettingsGroup() { + if (config.containerVariant.equals(Container.BIONIC, ignoreCase = true)) { + // Bionic: Graphics Driver (Wrapper/Wrapper-v2) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.graphics_driver)) }, + value = state.bionicDriverIndex.value, + items = state.bionicGraphicsDrivers, + onItemSelected = { idx -> + state.bionicDriverIndex.value = idx + state.config.value = config.copy(graphicsDriver = StringUtils.parseIdentifier(state.bionicGraphicsDrivers[idx])) + }, + ) + // Bionic: Graphics Driver Version (stored in graphicsDriverConfig.version; list from manifest + installed) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.graphics_driver_version)) }, + value = state.wrapperVersionIndex.value.coerceIn(0, (state.wrapperOptions.labels.size - 1).coerceAtLeast(0)), + items = state.wrapperOptions.labels, + itemMuted = state.wrapperOptions.muted, + onItemSelected = { idx -> + val selectedId = state.wrapperOptions.ids.getOrNull(idx).orEmpty() + val isManifestNotInstalled = state.wrapperOptions.muted.getOrNull(idx) == true + val manifestEntry = state.wrapperManifestById[selectedId] + if (isManifestNotInstalled && manifestEntry != null) { + state.launchManifestDriverInstall(manifestEntry) { + val cfg = KeyValueSet(config.graphicsDriverConfig) + cfg.put("version", state.wrapperOptions.labels[idx]) + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + } + return@SettingsListDropdown + } + state.wrapperVersionIndex.value = idx + val cfg = KeyValueSet(config.graphicsDriverConfig) + cfg.put("version", selectedId.ifEmpty { state.wrapperOptions.labels[idx] }) + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + }, + ) + DxWrapperSection(state) + // Bionic: Exposed Vulkan Extensions (same UI as Vortek) + SettingsMultiListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.exposed_vulkan_extensions)) }, + values = state.exposedExtIndices.value, + items = state.gpuExtensions, + fallbackDisplay = "all", + onItemSelected = { idx -> + val current = state.exposedExtIndices.value + state.exposedExtIndices.value = + if (current.contains(idx)) current.filter { it != idx } else current + idx + val cfg = KeyValueSet(config.graphicsDriverConfig) + val allSelected = state.exposedExtIndices.value.size == state.gpuExtensions.size + if (allSelected) cfg.put("exposedDeviceExtensions", "all") else cfg.put( + "exposedDeviceExtensions", + state.exposedExtIndices.value.sorted().joinToString("|") { state.gpuExtensions[it] }, + ) + val blacklisted = if (allSelected) "" else + state.gpuExtensions.indices + .filter { it !in state.exposedExtIndices.value } + .sorted() + .joinToString(",") { state.gpuExtensions[it] } + cfg.put("blacklistedExtensions", blacklisted) + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + }, + ) + // Bionic: Max Device Memory (same as Vortek) + run { + val memValues = listOf("0", "512", "1024", "2048", "4096") + val memLabels = listOf("0 MB", "512 MB", "1024 MB", "2048 MB", "4096 MB") + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.max_device_memory)) }, + value = state.maxDeviceMemoryIndex.value.coerceIn(0, memValues.lastIndex), + items = memLabels, + onItemSelected = { idx -> + state.maxDeviceMemoryIndex.value = idx + val cfg = KeyValueSet(config.graphicsDriverConfig) + cfg.put("maxDeviceMemory", memValues[idx]) + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + }, + ) + } + // Bionic: Use Adrenotools Turnip + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.use_adrenotools_turnip)) }, + state = state.adrenotoolsTurnipChecked.value, + onCheckedChange = { checked -> + state.adrenotoolsTurnipChecked.value = checked + val cfg = KeyValueSet(config.graphicsDriverConfig) + cfg.put("adrenotoolsTurnip", if (checked) "1" else "0") + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + }, + ) + if (config.wineVersion.contains("arm64ec", true)) { + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.present_modes)) }, + value = state.presentModeIndex.value.coerceIn(0, state.presentModes.lastIndex.coerceAtLeast(0)), + items = state.presentModes, + onItemSelected = { idx -> + state.presentModeIndex.value = idx + val cfg = KeyValueSet(config.graphicsDriverConfig) + cfg.put("presentMode", state.presentModes[idx]) + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + }, + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.resource_type)) }, + value = state.resourceTypeIndex.value.coerceIn(0, state.resourceTypes.lastIndex.coerceAtLeast(0)), + items = state.resourceTypes, + onItemSelected = { idx -> + state.resourceTypeIndex.value = idx + val cfg = KeyValueSet(config.graphicsDriverConfig) + cfg.put("resourceType", state.resourceTypes[idx]) + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + }, + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.bcn_emulation)) }, + value = state.bcnEmulationIndex.value.coerceIn(0, state.bcnEmulationEntries.lastIndex.coerceAtLeast(0)), + items = state.bcnEmulationEntries, + onItemSelected = { idx -> + state.bcnEmulationIndex.value = idx + val cfg = KeyValueSet(config.graphicsDriverConfig) + cfg.put("bcnEmulation", state.bcnEmulationEntries[idx]) + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + }, + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.bcn_emulation_type)) }, + value = state.bcnEmulationTypeIndex.value.coerceIn(0, state.bcnEmulationTypeEntries.lastIndex.coerceAtLeast(0)), + items = state.bcnEmulationTypeEntries, + onItemSelected = { i -> + state.bcnEmulationTypeIndex.value = i + val cfg = KeyValueSet(config.graphicsDriverConfig) + cfg.put("bcnEmulationType", state.bcnEmulationTypeEntries[i]) + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + }, + ) + // Sharpness (vkBasalt) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.sharpness_effect)) }, + value = state.sharpnessEffectIndex.value.coerceIn(0, state.sharpnessEffects.lastIndex.coerceAtLeast(0)), + items = state.sharpnessDisplayItems, + onItemSelected = { idx -> + state.sharpnessEffectIndex.value = idx + state.config.value = config.copy(sharpnessEffect = state.sharpnessEffects[idx]) + }, + ) + val selectedBoost = state.sharpnessEffects + .getOrNull(state.sharpnessEffectIndex.value) + ?.equals("None", ignoreCase = true) + ?.not() ?: false + if (selectedBoost) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Text(text = stringResource(R.string.sharpness_level)) + Slider( + value = state.sharpnessLevel.value.toFloat(), + onValueChange = { newValue -> + val clamped = newValue.roundToInt().coerceIn(0, 100) + state.sharpnessLevel.value = clamped + state.config.value = config.copy(sharpnessLevel = clamped) + }, + valueRange = 0f..100f, + ) + Text(text = "${state.sharpnessLevel.value}%") + } + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Text(text = stringResource(R.string.sharpness_denoise)) + Slider( + value = state.sharpnessDenoise.value.toFloat(), + onValueChange = { newValue -> + val clamped = newValue.roundToInt().coerceIn(0, 100) + state.sharpnessDenoise.value = clamped + state.config.value = config.copy(sharpnessDenoise = clamped) + }, + valueRange = 0f..100f, + ) + Text(text = "${state.sharpnessDenoise.value}%") + } + } + } + } else { + // Non-bionic: existing driver/version UI and Vortek-specific options + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.graphics_driver)) }, + value = state.graphicsDriverIndex.value, + items = state.graphicsDrivers.value, + onItemSelected = { + state.graphicsDriverIndex.value = it + state.graphicsDriverVersionIndex.value = 0 + state.config.value = config.copy( + graphicsDriver = StringUtils.parseIdentifier(state.graphicsDrivers.value[it]), + graphicsDriverVersion = "", + ) + }, + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.graphics_driver_version)) }, + value = state.graphicsDriverVersionIndex.value, + items = state.getVersionsForDriver(), + onItemSelected = { + state.graphicsDriverVersionIndex.value = it + val selectedVersion = if (it == 0) "" else state.getVersionsForDriver()[it] + state.config.value = config.copy(graphicsDriverVersion = selectedVersion) + }, + ) + DxWrapperSection(state) + // Vortek/Adreno specific settings + run { + val driverType = StringUtils.parseIdentifier(state.graphicsDrivers.value.getOrNull(state.graphicsDriverIndex.value).orEmpty()) + val isVortekLike = config.containerVariant.equals(Container.GLIBC) && (driverType == "vortek" || driverType == "adreno" || driverType == "sd-8-elite") + if (isVortekLike) { + val vkVersions = listOf("1.0", "1.1", "1.2", "1.3") + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.vulkan_version)) }, + value = state.vkMaxVersionIndex.value.coerceIn(0, 3), + items = vkVersions, + onItemSelected = { idx -> + state.vkMaxVersionIndex.value = idx + val cfg = KeyValueSet(config.graphicsDriverConfig) + cfg.put("vkMaxVersion", vkVersions[idx]) + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + }, + ) + SettingsMultiListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.exposed_vulkan_extensions)) }, + values = state.exposedExtIndices.value, + items = state.gpuExtensions, + fallbackDisplay = "all", + onItemSelected = { idx -> + val current = state.exposedExtIndices.value + state.exposedExtIndices.value = + if (current.contains(idx)) current.filter { it != idx } else current + idx + val cfg = KeyValueSet(config.graphicsDriverConfig) + val allSelected = state.exposedExtIndices.value.size == state.gpuExtensions.size + if (allSelected) cfg.put("exposedDeviceExtensions", "all") else cfg.put( + "exposedDeviceExtensions", + state.exposedExtIndices.value.sorted().joinToString("|") { state.gpuExtensions[it] }, + ) + val blacklisted = if (allSelected) "" else + state.gpuExtensions.indices + .filter { it !in state.exposedExtIndices.value } + .sorted() + .joinToString(",") { state.gpuExtensions[it] } + cfg.put("blacklistedExtensions", blacklisted) + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + }, + ) + val imageSizes = listOf("64", "128", "256", "512", "1024") + val imageLabels = listOf("64", "128", "256", "512", "1024").map { "$it MB" } + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.image_cache_size)) }, + value = state.imageCacheIndex.value.coerceIn(0, imageSizes.lastIndex), + items = imageLabels, + onItemSelected = { idx -> + state.imageCacheIndex.value = idx + val cfg = KeyValueSet(config.graphicsDriverConfig) + cfg.put("imageCacheSize", imageSizes[idx]) + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + }, + ) + val memValues = listOf("0", "512", "1024", "2048", "4096") + val memLabels = listOf("0 MB", "512 MB", "1024 MB", "2048 MB", "4096 MB") + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.max_device_memory)) }, + value = state.maxDeviceMemoryIndex.value.coerceIn(0, memValues.lastIndex), + items = memLabels, + onItemSelected = { idx -> + state.maxDeviceMemoryIndex.value = idx + val cfg = KeyValueSet(config.graphicsDriverConfig) + cfg.put("maxDeviceMemory", memValues[idx]) + state.config.value = config.copy(graphicsDriverConfig = cfg.toString()) + }, + ) + } + } + } + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.use_dri3)) }, + subtitle = { Text(text = stringResource(R.string.use_dri3_description)) }, + state = config.useDRI3, + onCheckedChange = { + state.config.value = config.copy(useDRI3 = it) + }, + ) + } +} + +@Composable +private fun DxWrapperSection(state: ContainerConfigState) { + val config = state.config.value + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.dx_wrapper)) }, + value = state.dxWrapperIndex.value, + items = state.dxWrappers, + onItemSelected = { + state.dxWrapperIndex.value = it + state.config.value = config.copy(dxwrapper = StringUtils.parseIdentifier(state.dxWrappers[it])) + }, + ) + // DXVK Version Dropdown (conditionally visible and constrained) + run { + val context = state.currentDxvkContext() + val isVKD3D = StringUtils.parseIdentifier(state.dxWrappers.getOrNull(state.dxWrapperIndex.value).orEmpty()) == "vkd3d" + if (!isVKD3D) { + val items = context.labels + val itemIds = context.ids + val itemMuted = context.muted + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.dxvk_version)) }, + value = state.dxvkVersionIndex.value.coerceIn(0, (items.size - 1).coerceAtLeast(0)), + items = items, + itemMuted = itemMuted, + onItemSelected = { + state.dxvkVersionIndex.value = it + val selectedId = itemIds.getOrNull(it).orEmpty() + val isManifestNotInstalled = state.isBionicVariant && itemMuted?.getOrNull(it) == true + val manifestEntry = if (state.isBionicVariant) state.dxvkManifestById[selectedId] else null + if (isManifestNotInstalled && manifestEntry != null) { + state.launchManifestContentInstall( + manifestEntry, + ContentProfile.ContentType.CONTENT_TYPE_DXVK, + ) { + val currentConfig = KeyValueSet(config.dxwrapperConfig) + currentConfig.put("version", selectedId) + if (selectedId.contains("async", ignoreCase = true)) currentConfig.put("async", "1") + else currentConfig.put("async", "0") + if (selectedId.contains("gplasync", ignoreCase = true)) currentConfig.put("asyncCache", "1") + else currentConfig.put("asyncCache", "0") + state.config.value = config.copy(dxwrapperConfig = currentConfig.toString()) + } + return@SettingsListDropdown + } + val version = selectedId.ifEmpty { StringUtils.parseIdentifier(items.getOrNull(it).orEmpty()) } + val currentConfig = KeyValueSet(config.dxwrapperConfig) + currentConfig.put("version", version) + val envVarsSet = EnvVars(config.envVars) + if (version.contains("async", ignoreCase = true)) currentConfig.put("async", "1") + else currentConfig.put("async", "0") + if (version.contains("gplasync", ignoreCase = true)) currentConfig.put("asyncCache", "1") + else currentConfig.put("asyncCache", "0") + state.config.value = + config.copy(dxwrapperConfig = currentConfig.toString(), envVars = envVarsSet.toString()) + }, + ) + } else { + // Ensure default version for vortek-like when hidden + val driverType = StringUtils.parseIdentifier(state.graphicsDrivers.value.getOrNull(state.graphicsDriverIndex.value).orEmpty()) + val isVortekLike = config.containerVariant.equals(Container.GLIBC) && (driverType == "vortek" || driverType == "adreno" || driverType == "sd-8-elite") + val version = if (isVortekLike) "1.10.3" else "2.4.1" + val currentConfig = KeyValueSet(config.dxwrapperConfig) + currentConfig.put("version", version) + state.config.value = config.copy(dxwrapperConfig = currentConfig.toString()) + } + } + // VKD3D Version UI (visible only when VKD3D selected) + run { + val isVKD3D = StringUtils.parseIdentifier(state.dxWrappers.getOrNull(state.dxWrapperIndex.value).orEmpty()) == "vkd3d" + if (isVKD3D) { + val label = "VKD3D Version" + val availableVersions = if (state.isBionicVariant) state.vkd3dOptions.labels else state.vkd3dVersionsBase + val availableIds = if (state.isBionicVariant) state.vkd3dOptions.ids else state.vkd3dVersionsBase + val availableMuted = if (state.isBionicVariant) state.vkd3dOptions.muted else null + val selectedVersion = + KeyValueSet(config.dxwrapperConfig).get("vkd3dVersion").ifEmpty { state.vkd3dForcedVersion() } + val selectedIndex = availableIds.indexOf(selectedVersion).coerceAtLeast(0) + + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = label) }, + value = selectedIndex, + items = availableVersions, + itemMuted = availableMuted, + onItemSelected = { idx -> + val selectedId = availableIds.getOrNull(idx).orEmpty() + val isManifestNotInstalled = state.isBionicVariant && availableMuted?.getOrNull(idx) == true + val manifestEntry = if (state.isBionicVariant) state.vkd3dManifestById[selectedId] else null + if (isManifestNotInstalled && manifestEntry != null) { + state.launchManifestContentInstall( + manifestEntry, + ContentProfile.ContentType.CONTENT_TYPE_VKD3D, + ) { + val currentConfig = KeyValueSet(config.dxwrapperConfig) + currentConfig.put("vkd3dVersion", selectedId) + state.config.value = config.copy(dxwrapperConfig = currentConfig.toString()) + } + return@SettingsListDropdown + } + val currentConfig = KeyValueSet(config.dxwrapperConfig) + currentConfig.put("vkd3dVersion", selectedId.ifEmpty { availableVersions.getOrNull(idx).orEmpty() }) + state.config.value = config.copy(dxwrapperConfig = currentConfig.toString()) + }, + ) + + val featureLevels = listOf("12_2", "12_1", "12_0", "11_1", "11_0") + val cfg = KeyValueSet(config.dxwrapperConfig) + val currentLevel = cfg.get("vkd3dFeatureLevel", "12_1") + val currentLevelIndex = featureLevels.indexOf(currentLevel).coerceAtLeast(0) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.vkd3d_feature_level)) }, + value = currentLevelIndex, + items = featureLevels, + onItemSelected = { + val selected = featureLevels[it] + val currentConfig = KeyValueSet(config.dxwrapperConfig) + currentConfig.put("vkd3dFeatureLevel", selected) + state.config.value = config.copy(dxwrapperConfig = currentConfig.toString()) + }, + ) + } + } +} diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/WinComponentsTab.kt b/app/src/main/java/app/gamenative/ui/component/dialog/WinComponentsTab.kt new file mode 100644 index 000000000..faf272a5e --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/component/dialog/WinComponentsTab.kt @@ -0,0 +1,33 @@ +package app.gamenative.ui.component.dialog + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import app.gamenative.ui.component.settings.SettingsListDropdown +import app.gamenative.ui.theme.settingsTileColors +import com.alorma.compose.settings.ui.SettingsGroup +import com.winlator.core.KeyValueSet + +@Composable +fun WinComponentsTabContent(state: ContainerConfigState) { + val config = state.config.value + SettingsGroup() { + for (wincomponent in KeyValueSet(config.wincomponents)) { + val compId = wincomponent[0] + val compNameRes = winComponentsItemTitleRes(compId) + val compValue = wincomponent[1].toInt() + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(stringResource(id = compNameRes)) }, + subtitle = { Text(if (compId.startsWith("direct")) "DirectX" else "General") }, + value = compValue, + items = state.winCompOpts, + onItemSelected = { + state.config.value = config.copy( + wincomponents = config.wincomponents.replace("$compId=$compValue", "$compId=$it"), + ) + }, + ) + } + } +} diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/WineTab.kt b/app/src/main/java/app/gamenative/ui/component/dialog/WineTab.kt new file mode 100644 index 000000000..3f06b010a --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/component/dialog/WineTab.kt @@ -0,0 +1,87 @@ +package app.gamenative.ui.component.dialog + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import app.gamenative.R +import app.gamenative.ui.component.settings.SettingsListDropdown +import app.gamenative.ui.theme.settingsTileColors +import app.gamenative.ui.theme.settingsTileColorsAlt +import com.alorma.compose.settings.ui.SettingsGroup +import com.alorma.compose.settings.ui.SettingsSwitch +import com.winlator.core.StringUtils + +@Composable +fun WineTabContent(state: ContainerConfigState) { + val config = state.config.value + val gpuCardsValues = state.gpuCards.values.toList() + SettingsGroup() { + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.renderer)) }, + value = state.gpuNameIndex.value, + items = state.gpuCards.values.map { it.name }, + onItemSelected = { + state.gpuNameIndex.value = it + val cfg = com.winlator.core.KeyValueSet(config.graphicsDriverConfig) + cfg.put("gpuName", gpuCardsValues[it].deviceId) + state.config.value = config.copy( + videoPciDeviceID = gpuCardsValues[it].deviceId, + graphicsDriverConfig = cfg.toString() + ) + }, + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.gpu_name)) }, + value = state.gpuNameIndex.value, + items = state.gpuCards.values.map { it.name }, + onItemSelected = { + state.gpuNameIndex.value = it + state.config.value = config.copy(videoPciDeviceID = gpuCardsValues[it].deviceId) + }, + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.offscreen_rendering_mode)) }, + value = state.renderingModeIndex.value, + items = state.renderingModes, + onItemSelected = { + state.renderingModeIndex.value = it + state.config.value = config.copy(offScreenRenderingMode = state.renderingModes[it].lowercase()) + }, + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.video_memory_size)) }, + value = state.videoMemIndex.value, + items = state.videoMemSizes, + onItemSelected = { + state.videoMemIndex.value = it + state.config.value = config.copy(videoMemorySize = StringUtils.parseNumber(state.videoMemSizes[it])) + }, + ) + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.enable_csmt)) }, + state = config.csmt, + onCheckedChange = { state.config.value = config.copy(csmt = it) }, + ) + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.enable_strict_shader_math)) }, + state = config.strictShaderMath, + onCheckedChange = { state.config.value = config.copy(strictShaderMath = it) }, + ) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.mouse_warp_override)) }, + value = state.mouseWarpIndex.value, + items = state.mouseWarps, + onItemSelected = { + state.mouseWarpIndex.value = it + state.config.value = config.copy(mouseWarpOverride = state.mouseWarps[it].lowercase()) + }, + ) + } +} 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 05f3eb4ce..1d376246a 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 @@ -2986,7 +2986,7 @@ private fun extractWinComponentFiles( val identifier = wincomponent[0] val useNative = wincomponent[1].equals("1") - if (!container.wineVersion.contains("proton-9.0-arm64ec") && identifier.contains("opengl") && useNative) continue + if (!container.wineVersion.contains("arm64ec") && identifier.contains("opengl") && useNative) continue if (useNative) { TarCompressorUtils.extract( @@ -3076,6 +3076,11 @@ private fun extractGraphicsDriverFiles( } if (dxwrapper.contains("dxvk")) { DXVKHelper.setEnvVars(context, dxwrapperConfig, envVars) + val version = dxwrapperConfig.get("version") + if (version == "1.11.1-sarek") { + Timber.tag("GraphicsDriverExtraction").d("Disabling Wrapper PATCH_OPCONSTCOMP SPIR-V pass") + envVars.put("WRAPPER_NO_PATCH_OPCONSTCOMP", "1") + } } else if (dxwrapper.contains("vkd3d")) { DXVKHelper.setVKD3DEnvVars(context, dxwrapperConfig, envVars) } @@ -3214,21 +3219,12 @@ private fun extractGraphicsDriverFiles( envVars.put("GALLIUM_DRIVER", "zink") envVars.put("LIBGL_KOPPER_DISABLE", "true") - - // if (firstTimeBoot) { -// Log.d("XServerDisplayActivity", "First time container boot, re-extracting wrapper"); -// TarCompressorUtils.extract(TarCompressorUtils.Type.ZSTD, this, "graphics_driver/wrapper" + ".tzst", rootDir); -// TarCompressorUtils.extract(TarCompressorUtils.Type.ZSTD, this, "graphics_driver/extra_libs" + ".tzst", rootDir); -// } - // 1. Get the main WRAPPER selection (e.g., "Wrapper-v2") from the class field. val mainWrapperSelection: String = graphicsDriver - // 2. Get the WRAPPER that was last saved to the container's settings. val lastInstalledMainWrapper = container.getExtra("lastInstalledMainWrapper") - // 3. Check if we need to extract a new wrapper file. if (ALWAYS_REEXTRACT || firstTimeBoot || mainWrapperSelection != lastInstalledMainWrapper) { // We only extract if the selection is actually a wrapper file. @@ -3241,12 +3237,22 @@ private fun extractGraphicsDriverFiles( container.putExtra("lastInstalledMainWrapper", mainWrapperSelection) container.saveData() } - } - - // 4. Extract common libraries, but only when the container is first created. - if (firstTimeBoot) { Log.d("XServerDisplayActivity", "First time container boot, extracting extra_libs.tzst") - TarCompressorUtils.extract(TarCompressorUtils.Type.ZSTD, context.getAssets(), "graphics_driver/extra_libs.tzst", rootDir) + TarCompressorUtils.extract( + TarCompressorUtils.Type.ZSTD, + context.getAssets(), + "graphics_driver/extra_libs.tzst", + rootDir, + ) + val renderer = GPUInformation.getRenderer(null, null) + if (container.wineVersion.contains("arm64ec") && renderer?.contains("Mali") != true) { + TarCompressorUtils.extract( + TarCompressorUtils.Type.ZSTD, + context.assets, + "graphics_driver/zink_dlls" + ".tzst", + File(rootDir, ImageFs.WINEPREFIX + "/drive_c/windows"), + ) + } } } @@ -3264,6 +3270,13 @@ private fun extractGraphicsDriverFiles( val blacklistedExtensions: String? = graphicsDriverConfig.get("blacklistedExtensions") envVars.put("WRAPPER_EXTENSION_BLACKLIST", blacklistedExtensions) + val gpuName = graphicsDriverConfig.get("gpuName") + if (gpuName != "Device") { + envVars.put("WRAPPER_DEVICE_NAME", gpuName) + envVars.put("WRAPPER_DEVICE_ID", GPUInformation.getDeviceIdFromGPUName(context, gpuName)) + envVars.put("WRAPPER_VENDOR_ID", GPUInformation.getVendorIdFromGPUName(context, gpuName)) + } + val maxDeviceMemory: String? = graphicsDriverConfig.get("maxDeviceMemory", "0") if (maxDeviceMemory != null && maxDeviceMemory.toInt() > 0) envVars.put("WRAPPER_VMEM_MAX_SIZE", maxDeviceMemory) @@ -3284,9 +3297,22 @@ private fun extractGraphicsDriverFiles( envVars.put("WRAPPER_DISABLE_PRESENT_WAIT", disablePresentWait) val bcnEmulation = graphicsDriverConfig.get("bcnEmulation") + val bcnEmulationType = graphicsDriverConfig.get("bcnEmulationType") when (bcnEmulation) { - "auto" -> envVars.put("WRAPPER_EMULATE_BCN", "3") - "full" -> envVars.put("WRAPPER_EMULATE_BCN", "2") + "auto" -> { + if (bcnEmulationType.equals("compute") && GPUInformation.getVendorID(null, null) != 0x5143) { + envVars.put("ENABLE_BCN_COMPUTE", "1"); + envVars.put("BCN_COMPUTE_AUTO", "1"); + } + envVars.put("WRAPPER_EMULATE_BCN", "3"); + } + "full" -> { + if (bcnEmulationType.equals("compute") && GPUInformation.getVendorID(null, null) != 0x5143) { + envVars.put("ENABLE_BCN_COMPUTE", "1"); + envVars.put("BCN_COMPUTE_AUTO", "0"); + } + envVars.put("WRAPPER_EMULATE_BCN", "2"); + } "none" -> envVars.put("WRAPPER_EMULATE_BCN", "0") else -> envVars.put("WRAPPER_EMULATE_BCN", "1") } diff --git a/app/src/main/java/app/gamenative/utils/ContainerUtils.kt b/app/src/main/java/app/gamenative/utils/ContainerUtils.kt index f659707a9..ce92b0926 100644 --- a/app/src/main/java/app/gamenative/utils/ContainerUtils.kt +++ b/app/src/main/java/app/gamenative/utils/ContainerUtils.kt @@ -44,11 +44,20 @@ object ContainerUtils { DefaultVersion.VARIANT = Container.BIONIC DefaultVersion.WINE_VERSION = "proton-9.0-arm64ec" DefaultVersion.DEFAULT_GRAPHICS_DRIVER = "Wrapper" - DefaultVersion.DXVK = "async-1.10.3" + DefaultVersion.DXVK = "2.4.1-gplasync" DefaultVersion.VKD3D = "2.14.1" - DefaultVersion.WRAPPER = "turnip25.3.0_R3_Auto" + DefaultVersion.WRAPPER = "turnip26.0.0_R8" DefaultVersion.STEAM_TYPE = Container.STEAM_TYPE_NORMAL - DefaultVersion.ASYNC_CACHE = "0" + DefaultVersion.ASYNC_CACHE = "1" + } else if (GPUInformation.isAdreno8Elite(context)) { + DefaultVersion.VARIANT = Container.BIONIC + DefaultVersion.WINE_VERSION = "proton-9.0-arm64ec" + DefaultVersion.DEFAULT_GRAPHICS_DRIVER = "Wrapper" + DefaultVersion.DXVK = "2.4.1-gplasync" + DefaultVersion.VKD3D = "2.14.1" + DefaultVersion.WRAPPER = "Turnip_Gen8_V23" + DefaultVersion.STEAM_TYPE = Container.STEAM_TYPE_NORMAL + DefaultVersion.ASYNC_CACHE = "1" } else { DefaultVersion.VARIANT = Container.BIONIC DefaultVersion.WINE_VERSION = "proton-9.0-arm64ec" diff --git a/app/src/main/java/app/gamenative/utils/ManifestComponentHelper.kt b/app/src/main/java/app/gamenative/utils/ManifestComponentHelper.kt index a69d93056..f51c26f15 100644 --- a/app/src/main/java/app/gamenative/utils/ManifestComponentHelper.kt +++ b/app/src/main/java/app/gamenative/utils/ManifestComponentHelper.kt @@ -128,16 +128,14 @@ object ManifestComponentHelper { val options = LinkedHashMap() (base + installed).forEach { label -> - val id = StringUtils.parseIdentifier(label) - options[id] = VersionOption(label, id, false, true) + options[label] = VersionOption(label, label, false, true) } val availableIds = options.keys.toSet() manifest.forEach { entry -> - val normalizedEntryId = StringUtils.parseIdentifier(entry.id) - if (!options.containsKey(normalizedEntryId)) { - val isInstalled = availableIds.contains(normalizedEntryId) - options[normalizedEntryId] = VersionOption(entry.id, normalizedEntryId, isManifest = true, isInstalled = isInstalled) + if (!options.containsKey(entry.id)) { + val isInstalled = availableIds.contains(entry.id) + options[entry.id] = VersionOption(entry.id, entry.id, isManifest = true, isInstalled = isInstalled) } } diff --git a/app/src/main/java/com/winlator/container/Container.java b/app/src/main/java/com/winlator/container/Container.java index 399065edd..421e9cebc 100644 --- a/app/src/main/java/com/winlator/container/Container.java +++ b/app/src/main/java/com/winlator/container/Container.java @@ -41,7 +41,7 @@ public enum XrControllerMapping { public static final String DEFAULT_DXWRAPPER = "dxvk"; public static final String DEFAULT_DDRAWRAPPER = "none"; public static final String DEFAULT_DXWRAPPERCONFIG = "version=" + DefaultVersion.DXVK + ",framerate=0,maxDeviceMemory=0,async=" + DefaultVersion.ASYNC + ",asyncCache=" + DefaultVersion.ASYNC_CACHE + ",vkd3dVersion=" + DefaultVersion.VKD3D + ",vkd3dLevel=12_1" + ",ddrawrapper=" + Container.DEFAULT_DDRAWRAPPER + ",csmt=3" + ",gpuName=NVIDIA GeForce GTX 480" + ",videoMemorySize=2048" + ",strict_shader_math=1" + ",OffscreenRenderingMode=fbo" + ",renderer=gl";; - public static final String DEFAULT_GRAPHICSDRIVERCONFIG = "vulkanVersion=1.3" + ",version=" + DefaultVersion.WRAPPER + ",blacklistedExtensions=" + ",maxDeviceMemory=0" + ",presentMode=mailbox" + ",syncFrame=0" + ",disablePresentWait=0" + ",resourceType=auto" + ",bcnEmulation=auto" + ",bcnEmulationType=software" + ",bcnEmulationCache=0"; + public static final String DEFAULT_GRAPHICSDRIVERCONFIG = "vulkanVersion=1.3" + ",version=" + DefaultVersion.WRAPPER + ",blacklistedExtensions=" + ",maxDeviceMemory=0" + ",presentMode=mailbox" + ",syncFrame=0" + ",disablePresentWait=0" + ",resourceType=auto" + ",bcnEmulation=auto" + ",bcnEmulationType=compute" + ",bcnEmulationCache=0" + ",gpuName=Device"; public static final String DEFAULT_WINCOMPONENTS = "direct3d=1,directsound=1,directmusic=0,directshow=0,directplay=0,vcrun2010=1,wmdecoder=1,opengl=0"; public static final String FALLBACK_WINCOMPONENTS = "direct3d=1,directsound=1,directmusic=1,directshow=1,directplay=1,vcrun2010=1,wmdecoder=1,opengl=0"; public static final String[] MEDIACONV_ENV_VARS = { diff --git a/app/src/main/java/com/winlator/contents/AdrenotoolsManager.java b/app/src/main/java/com/winlator/contents/AdrenotoolsManager.java index f4dc36fb1..910ef4c50 100644 --- a/app/src/main/java/com/winlator/contents/AdrenotoolsManager.java +++ b/app/src/main/java/com/winlator/contents/AdrenotoolsManager.java @@ -200,6 +200,10 @@ public void setDriverById(EnvVars envVars, ImageFs imagefs, String adrenotoolsDr FileUtils.writeToBinaryFile(driverPath + "notadreno_utils.so", 0x2680, 2); } } + } else if (adrenotoolsDriverId != null && !adrenotoolsDriverId.isEmpty() + && !adrenotoolsDriverId.equalsIgnoreCase("System")) { + Log.w("AdrenotoolsManager", "Driver not found: " + adrenotoolsDriverId + + " - Falling back to System driver"); } } } diff --git a/app/src/main/java/com/winlator/core/GPUInformation.java b/app/src/main/java/com/winlator/core/GPUInformation.java index fcbd3a779..7ee9996d0 100644 --- a/app/src/main/java/com/winlator/core/GPUInformation.java +++ b/app/src/main/java/com/winlator/core/GPUInformation.java @@ -8,6 +8,10 @@ import com.winlator.PrefManager; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + import java.util.Locale; import java.util.Objects; @@ -23,6 +27,40 @@ public abstract class GPUInformation { System.loadLibrary("extras"); } + public static String getDeviceIdFromGPUName(Context context, String gpuName) { + String gpuNameList = FileUtils.readString(context, "gpu_cards.json"); + String deviceId = ""; + try { + JSONArray jsonArray = new JSONArray(gpuNameList); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jobj = jsonArray.getJSONObject(i); + if (jobj.getString("name").contains(gpuName)) { + deviceId = jobj.getString("deviceID"); + } + } + } + catch (JSONException e) { + } + return deviceId; + } + + public static String getVendorIdFromGPUName(Context context, String gpuName) { + String gpuNameList = FileUtils.readString(context, "gpu_cards.json"); + String vendorId = ""; + try { + JSONArray jsonArray = new JSONArray(gpuNameList); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jobj = jsonArray.getJSONObject(i); + if (jobj.getString("name").contains(gpuName)) { + vendorId = jobj.getString("vendorID"); + } + } + } + catch (JSONException e) { + } + return vendorId; + } + private static ArrayMap loadGPUInformation(Context context) { final Thread thread = Thread.currentThread(); final ArrayMap gpuInfo = new ArrayMap<>(); @@ -115,6 +153,12 @@ public static boolean isAdreno6xx(Context context) { return getRenderer(context).toLowerCase(Locale.ENGLISH).matches(".*adreno[^6]+6[0-9]{2}.*"); } + public static boolean isAdreno8Elite(Context context) { + String r = getRenderer(context).toLowerCase(Locale.ENGLISH); + // “adreno … 83x / 84x / 85x” + return r.contains("adreno") && r.matches(".*\\b8(3[0-9]|4[0-9]|5[0-9])\\b.*"); + } + public static boolean isTurnipCapable(Context context) { String r = getRenderer(context).toLowerCase(Locale.ENGLISH); // match “adreno 610…699” or “adreno 710…799” @@ -150,4 +194,5 @@ public static boolean isDriverSupported(String driverName, Context context) { public native static String getVulkanVersion(String driverName, Context context); public native static String getRenderer(String driverName, Context context); public native static String[] enumerateExtensions(String driverName, Context context); + public native static int getVendorID(String driverName, Context context); } diff --git a/app/src/main/java/com/winlator/core/envvars/EnvVarInfo.kt b/app/src/main/java/com/winlator/core/envvars/EnvVarInfo.kt index fc806c686..939b0c0aa 100644 --- a/app/src/main/java/com/winlator/core/envvars/EnvVarInfo.kt +++ b/app/src/main/java/com/winlator/core/envvars/EnvVarInfo.kt @@ -238,6 +238,11 @@ data class EnvVarInfo( selectionType = EnvVarSelectionType.TOGGLE, possibleValues = listOf("0", "1"), ), + "GALLIUM_HUD" to EnvVarInfo( + identifier = "GALLIUM_HUD", + selectionType = EnvVarSelectionType.MULTI_SELECT, + possibleValues = listOf("simple", "fps", "frametime"), + ), ) } } diff --git a/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java b/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java index 9b74e2692..cfa9a6d9a 100644 --- a/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java +++ b/app/src/main/java/com/winlator/xenvironment/ImageFsInstaller.java @@ -42,7 +42,7 @@ import java.util.concurrent.atomic.AtomicLong; public abstract class ImageFsInstaller { - public static final byte LATEST_VERSION = 25; + public static final byte LATEST_VERSION = 26; private static void resetContainerImgVersions(Context context) { ContainerManager manager = new ContainerManager(context); diff --git a/app/src/main/jniLibs/arm64-v8a/libextras.so b/app/src/main/jniLibs/arm64-v8a/libextras.so index b22364e92..1c6213e04 100755 Binary files a/app/src/main/jniLibs/arm64-v8a/libextras.so and b/app/src/main/jniLibs/arm64-v8a/libextras.so differ diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 633f8078c..2a31f614d 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -33,13 +33,14 @@ System v762 v805 + Turnip_Gen8_V23 turnip25.1.0 turnip25.3.0_R3_Auto turnip25.3.0_R3_Gmem turnip25.3.0_R6_Gmem - 25.3.0_R11 - 26.0.0_R4 - 26.0.0_R8 + turnip25.3.0_R11 + turnip26.0.0_R4 + turnip26.0.0_R8 25.1.0 @@ -86,6 +87,7 @@ 1.10.3 1.10.1 1.10.9-sarek + 1.11.1-sarek 1.9.2 2.4.1 2.4.1-gplasync @@ -105,6 +107,9 @@ 1024 MB 2048 MB 4096 MB + 8192 MB + 12288 MB + 16384 MB Never @@ -134,6 +139,7 @@ software + compute 0