diff --git a/app/src/main/cpp/winlator/gpu_helper.c b/app/src/main/cpp/winlator/gpu_helper.c index 1b576394cc..14347e9c8e 100644 --- a/app/src/main/cpp/winlator/gpu_helper.c +++ b/app/src/main/cpp/winlator/gpu_helper.c @@ -1,9 +1,125 @@ #include #include #include +#include +#include +#include -JNIEXPORT jlong JNICALL -Java_com_winlator_core_GPUHelper_vkGetDeviceExtensions(JNIEnv *env, jclass clazz) +static jobjectArray make_empty_array(JNIEnv *env) +{ + jclass stringCls = (*env)->FindClass(env, "java/lang/String"); + if (!stringCls) return NULL; + return (*env)->NewObjectArray(env, 0, stringCls, NULL); +} + +static jobjectArray vkGetDeviceExtensions_dynamic(JNIEnv *env) +{ + void *libvulkan = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL); + if (!libvulkan) return make_empty_array(env); + + PFN_vkCreateInstance pfn_vkCreateInstance = (PFN_vkCreateInstance) dlsym(libvulkan, "vkCreateInstance"); + PFN_vkEnumeratePhysicalDevices pfn_vkEnumeratePhysicalDevices = (PFN_vkEnumeratePhysicalDevices) dlsym(libvulkan, "vkEnumeratePhysicalDevices"); + PFN_vkEnumerateDeviceExtensionProperties pfn_vkEnumerateDeviceExtensionProperties = + (PFN_vkEnumerateDeviceExtensionProperties) dlsym(libvulkan, "vkEnumerateDeviceExtensionProperties"); + PFN_vkDestroyInstance pfn_vkDestroyInstance = (PFN_vkDestroyInstance) dlsym(libvulkan, "vkDestroyInstance"); + + if (!pfn_vkCreateInstance || !pfn_vkEnumeratePhysicalDevices || + !pfn_vkEnumerateDeviceExtensionProperties || !pfn_vkDestroyInstance) { + dlclose(libvulkan); + return make_empty_array(env); + } + + VkApplicationInfo appInfo = { + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pApplicationName = "GPUHelper", + .applicationVersion = VK_MAKE_VERSION(1, 0, 0), + .pEngineName = "No Engine", + .engineVersion = VK_MAKE_VERSION(1, 0, 0), + .apiVersion = VK_API_VERSION_1_0, + }; + + VkInstanceCreateInfo ci = { + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pApplicationInfo = &appInfo, + }; + + VkInstance instance; + VkResult res = pfn_vkCreateInstance(&ci, NULL, &instance); + if (res != VK_SUCCESS) { + dlclose(libvulkan); + return make_empty_array(env); + } + + uint32_t pdCount = 0; + res = pfn_vkEnumeratePhysicalDevices(instance, &pdCount, NULL); + if (res != VK_SUCCESS || pdCount == 0) { + pfn_vkDestroyInstance(instance, NULL); + dlclose(libvulkan); + return make_empty_array(env); + } + + pdCount = 1; + VkPhysicalDevice pd; + res = pfn_vkEnumeratePhysicalDevices(instance, &pdCount, &pd); + if (!(res == VK_SUCCESS || res == VK_INCOMPLETE)) { + pfn_vkDestroyInstance(instance, NULL); + dlclose(libvulkan); + return make_empty_array(env); + } + + uint32_t extCount = 0; + res = pfn_vkEnumerateDeviceExtensionProperties(pd, NULL, &extCount, NULL); + if (res != VK_SUCCESS || extCount == 0) { + pfn_vkDestroyInstance(instance, NULL); + dlclose(libvulkan); + return make_empty_array(env); + } + + VkExtensionProperties *ext = calloc(extCount, sizeof(VkExtensionProperties)); + if (!ext) { + pfn_vkDestroyInstance(instance, NULL); + dlclose(libvulkan); + return make_empty_array(env); + } + + res = pfn_vkEnumerateDeviceExtensionProperties(pd, NULL, &extCount, ext); + if (res != VK_SUCCESS) { + free(ext); + pfn_vkDestroyInstance(instance, NULL); + dlclose(libvulkan); + return make_empty_array(env); + } + + jclass stringCls = (*env)->FindClass(env, "java/lang/String"); + if (!stringCls) { + free(ext); + pfn_vkDestroyInstance(instance, NULL); + dlclose(libvulkan); + return NULL; + } + jobjectArray arr = (*env)->NewObjectArray(env, (jsize)extCount, stringCls, NULL); + if (!arr) { + free(ext); + pfn_vkDestroyInstance(instance, NULL); + dlclose(libvulkan); + return NULL; + } + + for (jsize i = 0; i < (jsize)extCount; ++i) + { + jstring js = (*env)->NewStringUTF(env, ext[i].extensionName); + if (js) { + (*env)->SetObjectArrayElement(env, arr, i, js); + (*env)->DeleteLocalRef(env, js); + } + } + free(ext); + pfn_vkDestroyInstance(instance, NULL); + dlclose(libvulkan); + return arr; +} + +static jobjectArray vkGetDeviceExtensions_static(JNIEnv *env) { VkInstance instance; VkResult res; @@ -40,12 +156,22 @@ Java_com_winlator_core_GPUHelper_vkGetDeviceExtensions(JNIEnv *env, jclass clazz (*env)->SetObjectArrayElement(env, arr, i, js); } free(ext); - return (jlong)arr; + return arr; make_empty_array: { jclass stringCls = (*env)->FindClass(env, "java/lang/String"); jobjectArray empty = (*env)->NewObjectArray(env, 0, stringCls, NULL); - return (jlong)empty; + return empty; + } +} + +JNIEXPORT jobjectArray JNICALL +Java_com_winlator_core_GPUHelper_vkGetDeviceExtensions(JNIEnv *env, jclass clazz) +{ + (void)clazz; + if (android_get_device_api_level() <= 29) { + return vkGetDeviceExtensions_dynamic(env); } + return vkGetDeviceExtensions_static(env); } diff --git a/app/src/main/java/app/gamenative/db/dao/EpicGameDao.kt b/app/src/main/java/app/gamenative/db/dao/EpicGameDao.kt index 6ec5df7d12..2d97fdfe52 100644 --- a/app/src/main/java/app/gamenative/db/dao/EpicGameDao.kt +++ b/app/src/main/java/app/gamenative/db/dao/EpicGameDao.kt @@ -49,7 +49,8 @@ interface EpicGameDao { @Query("SELECT * FROM epic_games WHERE app_name = :appName") suspend fun getByAppName(appName: String): EpicGame? - @Query("SELECT * FROM epic_games WHERE is_dlc = false AND namespace != 'ue' ORDER BY title ASC") + // Use numeric literals 0/1 for booleans to be compatible with SQLite + @Query("SELECT * FROM epic_games WHERE is_dlc = 0 AND namespace != 'ue' ORDER BY title ASC") fun getAll(): Flow> @Query("SELECT * FROM epic_games WHERE is_installed = :isInstalled ORDER BY title ASC") @@ -58,14 +59,14 @@ interface EpicGameDao { @Query("SELECT * FROM epic_games WHERE base_game_app_name = (SELECT catalog_id FROM epic_games WHERE id = :appId)") fun getDLCForTitle(appId: Int): Flow> - @Query("SELECT * FROM epic_games WHERE base_game_app_name IS NOT NULL AND is_dlc = true") + @Query("SELECT * FROM epic_games WHERE base_game_app_name IS NOT NULL AND is_dlc = 1") fun getAllDlcTitles(): Flow> - @Query("SELECT * FROM epic_games WHERE is_dlc = false AND namespace != 'ue' AND title LIKE '%' || :searchQuery || '%' ORDER BY title ASC") + @Query("SELECT * FROM epic_games WHERE is_dlc = 0 AND namespace != 'ue' AND title LIKE '%' || :searchQuery || '%' ORDER BY title ASC") fun searchByTitle(searchQuery: String): Flow> // Only delete non-installed games from DB - Need to preserve any currently installed games. - @Query("DELETE FROM epic_games WHERE is_installed = false") + @Query("DELETE FROM epic_games WHERE is_installed = 0") suspend fun deleteAllNonInstalledGames() @Query("SELECT COUNT(*) FROM epic_games") diff --git a/app/src/main/java/app/gamenative/db/dao/GOGGameDao.kt b/app/src/main/java/app/gamenative/db/dao/GOGGameDao.kt index 237ebfebd1..596b53acf2 100644 --- a/app/src/main/java/app/gamenative/db/dao/GOGGameDao.kt +++ b/app/src/main/java/app/gamenative/db/dao/GOGGameDao.kt @@ -34,22 +34,23 @@ interface GOGGameDao { @Query("SELECT * FROM gog_games WHERE id = :gameId") suspend fun getById(gameId: String): GOGGame? - @Query("SELECT * FROM gog_games WHERE exclude = false ORDER BY title ASC") + // Use numeric 0/1 for boolean values in SQL for compatibility with SQLite + @Query("SELECT * FROM gog_games WHERE \"exclude\" = 0 ORDER BY title ASC") fun getAll(): Flow> - @Query("SELECT * FROM gog_games WHERE exclude = false ORDER BY title ASC") + @Query("SELECT * FROM gog_games WHERE \"exclude\" = 0 ORDER BY title ASC") suspend fun getAllAsList(): List - @Query("SELECT * FROM gog_games WHERE is_installed = :isInstalled AND exclude = false ORDER BY title ASC") + @Query("SELECT * FROM gog_games WHERE is_installed = :isInstalled AND \"exclude\" = 0 ORDER BY title ASC") fun getByInstallStatus(isInstalled: Boolean): Flow> - @Query("SELECT * FROM gog_games WHERE exclude = false AND title LIKE '%' || :searchQuery || '%' ORDER BY title ASC") + @Query("SELECT * FROM gog_games WHERE \"exclude\" = 0 AND title LIKE '%' || :searchQuery || '%' ORDER BY title ASC") fun searchByTitle(searchQuery: String): Flow> - @Query("DELETE FROM gog_games WHERE is_installed = false") + @Query("DELETE FROM gog_games WHERE is_installed = 0") suspend fun deleteAllNonInstalledGames() - @Query("SELECT COUNT(*) FROM gog_games WHERE exclude = false") + @Query("SELECT COUNT(*) FROM gog_games WHERE \"exclude\" = 0") fun getCount(): Flow @Query("SELECT id FROM gog_games") 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 ebe3e37ef2..7e7b7640e6 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 @@ -1,9 +1,10 @@ package app.gamenative.ui.component.dialog +import android.content.res.Configuration +import android.os.Build import android.widget.Toast import android.widget.Spinner import android.widget.ArrayAdapter -import android.content.res.Configuration import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement @@ -84,6 +85,7 @@ import app.gamenative.ui.theme.settingsTileColors import app.gamenative.ui.theme.settingsTileColorsAlt import app.gamenative.utils.CustomGameScanner import app.gamenative.utils.ContainerUtils +import app.gamenative.utils.GpuCompatHelper import app.gamenative.utils.ManifestComponentHelper import app.gamenative.utils.ManifestContentTypes import app.gamenative.utils.ManifestData @@ -104,6 +106,7 @@ import com.winlator.core.StringUtils import com.winlator.core.envvars.EnvVars import com.winlator.core.DefaultVersion import com.winlator.core.GPUHelper +import com.winlator.core.GPUInformation import com.winlator.core.WineInfo import com.winlator.core.WineInfo.MAIN_WINE_VERSION import com.winlator.fexcore.FEXCoreManager @@ -324,11 +327,11 @@ fun ContainerConfigDialog( val bionicWineManifest = remember(manifestWine, manifestProton) { ManifestComponentHelper.filterManifestByVariant(manifestWine, "bionic") + - ManifestComponentHelper.filterManifestByVariant(manifestProton, "bionic") + ManifestComponentHelper.filterManifestByVariant(manifestProton, "bionic") } val glibcWineManifest = remember(manifestWine, manifestProton) { ManifestComponentHelper.filterManifestByVariant(manifestWine, "glibc") + - ManifestComponentHelper.filterManifestByVariant(manifestProton, "glibc") + ManifestComponentHelper.filterManifestByVariant(manifestProton, "glibc") } val bionicWineOptions = remember(bionicWineEntriesBase, installedWine, installedProton, bionicWineManifest) { ManifestComponentHelper.buildVersionOptionList(bionicWineEntriesBase, installedWine + installedProton, bionicWineManifest) @@ -452,17 +455,42 @@ fun ContainerConfigDialog( val exposedExtIndicesRef = rememberSaveable { mutableStateOf(listOf()) } var exposedExtIndices by exposedExtIndicesRef val inspectionMode = LocalInspectionMode.current - val gpuExtensions = remember(inspectionMode) { - if (inspectionMode) { - listOf( - "VK_KHR_swapchain", - "VK_KHR_maintenance1", - "VK_KHR_timeline_semaphore", + + // START: API 29 Compatibility Fix + // --- + // Note: This state controls the dialog shown when Vulkan extensions cannot be queried + // on Android 10 (API 29) devices with Mali GPUs. These devices can crash in native code + // during the Vulkan extension probe, so we fall back to a safe default list instead. + // --- + var gpuExtensionsErrorDialogState by rememberSaveable(stateSaver = MessageDialogState.Saver) { + mutableStateOf(MessageDialogState(visible = false)) + } + // --- + // END: API 29 Compatibility Fix + + // START: API 29 Compatibility Fix + // --- + // Delegate API 29 + Mali safety to a helper so other devices stay on the original path. + // --- + val gpuExtensionsResult: GpuCompatHelper.VulkanExtensionsResult = remember(context, inspectionMode) { + GpuCompatHelper.resolveVulkanExtensions(context, inspectionMode) + } + val gpuExtensions = gpuExtensionsResult.extensions + + LaunchedEffect(gpuExtensionsResult.usedFallback) { + if (gpuExtensionsResult.usedFallback) { + gpuExtensionsErrorDialogState = MessageDialogState( + visible = true, + title = "Could not get GPU features", + message = "The list of supported Vulkan extensions could not be retrieved from the device. " + + "A default set for Vulkan 1.0 will be used. Some graphics options may not work as expected.", + confirmBtnText = context.getString(R.string.ok), ) - } else { - GPUHelper.vkGetDeviceExtensions().toList() } } + // --- + // END: API 29 Compatibility Fix + LaunchedEffect(config.graphicsDriverConfig) { val cfg = KeyValueSet(config.graphicsDriverConfig) // Sync Vulkan version index from config @@ -751,7 +779,7 @@ fun ContainerConfigDialog( if (wrapperIsDxvk) { // Check if we need to update - only if current version doesn't match selected version val needsUpdate = currentVersion.isEmpty() || - (currentVersion != version && StringUtils.parseIdentifier(currentVersion) != StringUtils.parseIdentifier(version)) + (currentVersion != version && StringUtils.parseIdentifier(currentVersion) != StringUtils.parseIdentifier(version)) if (needsUpdate) { kvs.put("version", version) } @@ -1072,6 +1100,20 @@ fun ContainerConfigDialog( onConfirmClick = onDismissRequest, ) + // START: API 29 Compatibility Fix + MessageDialog( + visible = gpuExtensionsErrorDialogState.visible, + title = gpuExtensionsErrorDialogState.title, + message = gpuExtensionsErrorDialogState.message, + confirmBtnText = gpuExtensionsErrorDialogState.confirmBtnText, + dismissBtnText = gpuExtensionsErrorDialogState.dismissBtnText, + onDismissRequest = { gpuExtensionsErrorDialogState = MessageDialogState(visible = false) }, + onDismissClick = { gpuExtensionsErrorDialogState = MessageDialogState(visible = false) }, + onConfirmClick = { gpuExtensionsErrorDialogState = MessageDialogState(visible = false) }, + ) + // END: API 29 Compatibility Fix + + Dialog( onDismissRequest = onDismissCheck, properties = DialogProperties( diff --git a/app/src/main/java/app/gamenative/utils/GpuCompatHelper.kt b/app/src/main/java/app/gamenative/utils/GpuCompatHelper.kt new file mode 100644 index 0000000000..8d633e1534 --- /dev/null +++ b/app/src/main/java/app/gamenative/utils/GpuCompatHelper.kt @@ -0,0 +1,80 @@ +package app.gamenative.utils + +import android.content.Context +import android.os.Build +import com.winlator.core.GPUHelper +import com.winlator.core.GPUInformation + +object GpuCompatHelper { + data class VulkanExtensionsResult( + val extensions: List, + val usedFallback: Boolean, + ) + //API 29 Compatibility Fix + /** + * Resolves the list of Vulkan device extensions to use on this device. + * + * Applies a compatibility workaround for some Mali GPUs on Android 10 (API 29), where + * probing Vulkan extensions in native code can crash. In such cases, a safe Vulkan 1.0 + * fallback extension list is returned instead. + * + * @param context Android [Context] used to query GPU / renderer information. + * @param inspectionMode If `true`, skips native probing and returns a small, predictable + * extension set intended for inspection tools, without applying compatibility probing. + * @return [VulkanExtensionsResult] containing the resolved extension list and a flag + * indicating whether a fallback list had to be used. + */ + fun resolveVulkanExtensions(context: Context, inspectionMode: Boolean): VulkanExtensionsResult { + if (inspectionMode) { + return VulkanExtensionsResult( + extensions = listOf( + "VK_KHR_swapchain", + "VK_KHR_maintenance1", + "VK_KHR_timeline_semaphore", + ), + usedFallback = false, + ) + } + + val isApi29Mali = Build.VERSION.SDK_INT == 29 && + GPUInformation.getRenderer(context).contains("mali", ignoreCase = true) + if (isApi29Mali) { + return try { + val probed = GPUHelper.vkGetDeviceExtensions().toList() + if (probed.isEmpty()) { + // API 29 compatibility fix: empty results act like a probe failure on Mali. + VulkanExtensionsResult( + extensions = defaultVulkan10Extensions, + usedFallback = true, + ) + } else { + VulkanExtensionsResult( + extensions = probed, + usedFallback = false, + ) + } + } catch (_: Exception) { + VulkanExtensionsResult( + extensions = defaultVulkan10Extensions, + usedFallback = true, + ) + } + } + + return VulkanExtensionsResult( + extensions = GPUHelper.vkGetDeviceExtensions().toList(), + usedFallback = false, + ) + } + + private val defaultVulkan10Extensions = listOf( + "VK_KHR_swapchain", + "VK_KHR_maintenance1", + "VK_KHR_maintenance2", + "VK_KHR_maintenance3", + "VK_KHR_sampler_mirror_clamp_to_edge", + "VK_KHR_surface", + "VK_KHR_android_surface", + "VK_KHR_get_physical_device_properties2", + ) +}