From c470ca4255ce1a450b58636bbcbe669569b6dd22 Mon Sep 17 00:00:00 2001 From: Catpotatos Date: Sat, 21 Feb 2026 16:52:22 +0000 Subject: [PATCH 1/5] fix: implement API 29 and Mali compatibility for GPU extension retrieval -- still to be tested. currently being stuck at log in due to SQLite error from ( @Query("SELECT * FROM epic_games WHERE is_dlc .... ) will come back to this once i can test --- .../component/dialog/ContainerConfigDialog.kt | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) 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..9cbef4a74f 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 @@ -117,6 +117,13 @@ import kotlinx.coroutines.withContext import java.io.File import java.util.Locale import kotlin.math.roundToInt +import android.os.Build +import com.winlator.core.GPUHelper.* +import com.winlator.core.GPUInformation +// Note: These imports are required to check the Android version (Build) +// and get information about the device's GPU (GPUInformation) to apply +// a targeted fix for the crash on specific hardware. +// --- /** * Gets the component title for Win Components settings group. @@ -233,6 +240,31 @@ fun ContainerConfigDialog( val customResolutionValidationErrorRef = remember { mutableStateOf(null) } var customResolutionValidationError by customResolutionValidationErrorRef + // START: API 29 Compatibility Fix + // --- + // Note: This state variable controls the visibility and content of the error + // dialog that will be shown if the app fails to get GPU extensions. + // This is part of the graceful error handling to prevent a crash. + // --- + var gpuExtensionsErrorDialogState by rememberSaveable(stateSaver = MessageDialogState.Saver) { + mutableStateOf(MessageDialogState(visible = false)) + } + + // --- + // Note: This dialog will be displayed to the user only when the app fails to + // retrieve the GPU's Vulkan extensions on the problematic hardware. It + // informs them that a default set of extensions is being used instead. + // --- + MessageDialog( + visible = gpuExtensionsErrorDialogState.visible, + 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 = "OK", + onDismissRequest = { gpuExtensionsErrorDialogState = MessageDialogState(visible = false) }, + onConfirmClick = { gpuExtensionsErrorDialogState = MessageDialogState(visible = false) } + ) + // END: API 29 Compatibility Fix + LaunchedEffect(visible) { if (visible) { showCustomResolutionDialog = false @@ -453,15 +485,47 @@ fun ContainerConfigDialog( var exposedExtIndices by exposedExtIndicesRef val inspectionMode = LocalInspectionMode.current val gpuExtensions = remember(inspectionMode) { + // START: API 29 and Mali Compatibility Fix + // Provide a safe default while ensuring the problematic native call is + // guarded on Android 10 (API 29) devices that report a Mali renderer. + // Also return a short default list when running in Compose preview (inspectionMode). if (inspectionMode) { + // In preview mode we can't call native functions, return a minimal list listOf( "VK_KHR_swapchain", "VK_KHR_maintenance1", "VK_KHR_timeline_semaphore", ) } else { - GPUHelper.vkGetDeviceExtensions().toList() + val renderer = try { + GPUInformation.getRenderer(context) + } catch (_: Throwable) { + "" + } + + if (Build.VERSION.SDK_INT == 29 && renderer.contains("mali", ignoreCase = true)) { + // Do NOT call the native function on known-affected devices. + // Native crashes (SIGABRT) cannot be caught by try/catch, so we must + // avoid the JNI call entirely on Android 10 (API 29) + Mali renderers. + 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 = "OK" + ) + listOf( + "VK_KHR_surface", + "VK_KHR_android_surface", + "VK_KHR_swapchain", + "VK_KHR_maintenance1", + "VK_KHR_get_physical_device_properties2" + ) + } else { + // Normal path for devices that are not API 29 + Mali + vkGetDeviceExtensions().toList() + } } + // END: API 29 and Mali Compatibility Fix } LaunchedEffect(config.graphicsDriverConfig) { val cfg = KeyValueSet(config.graphicsDriverConfig) @@ -1122,7 +1186,8 @@ fun ContainerConfigDialog( Column( modifier = Modifier .padding( - top = app.gamenative.utils.PaddingUtils.statusBarAwarePadding().calculateTopPadding() + paddingValues.calculateTopPadding(), + top = app.gamenative.utils.PaddingUtils.statusBarAwarePadding() + .calculateTopPadding() + paddingValues.calculateTopPadding(), bottom = 32.dp + paddingValues.calculateBottomPadding(), start = paddingValues.calculateStartPadding(LayoutDirection.Ltr), end = paddingValues.calculateEndPadding(LayoutDirection.Ltr), @@ -1293,3 +1358,5 @@ internal fun ExecutablePathDropdown( } } } + + From ed98f3bc0726ae03682140328cf9802b1e392a34 Mon Sep 17 00:00:00 2001 From: Catpotatos Date: Sat, 21 Feb 2026 17:07:39 +0000 Subject: [PATCH 2/5] fix: update SQL queries to use numeric literals for boolean values for SQLite compatibility - changed DAO for EpicGame and GOGGame. This fixed crash in API 29 (Android 10) otherwise Skip steam login or steam log in triggers native crash. --- .../main/java/app/gamenative/db/dao/EpicGameDao.kt | 9 +++++---- .../main/java/app/gamenative/db/dao/GOGGameDao.kt | 13 +++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) 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") From 57473eb4447c6ab613864b078119074f673a563f Mon Sep 17 00:00:00 2001 From: Catpotatos Date: Sun, 22 Feb 2026 01:08:24 +0000 Subject: [PATCH 3/5] refactor: move API 29+Mali GPU compatibility logic to GpuCompatHelper Fix - ContainerConfigDialog.kt, gpuExtensions is computed inside remember { ... } and mutates gpuExtensionsErrorDialogState inside the catch. That is a side-effect during composition (inside remember init), which can lead to invalid bytecode / verifier failure on some devices (especially older runtimes like API 29) - created GpuCompatHelper to avoid the issue. This commit refactors the handling of Vulkan extension retrieval for API 29 devices with Mali GPUs. The logic, which previously existed directly within `ContainerConfigDialog.kt`, has been extracted into a new `GpuCompatHelper` class. This helper now safely attempts to get the device's Vulkan extensions and provides a default list if the native call fails, preventing a crash. The `ContainerConfigDialog` now uses this helper to get the extensions and displays an informational dialog to the user if the fallback list was used. This improves code organization and isolates the compatibility workaround. --- .../component/dialog/ContainerConfigDialog.kt | 135 +++++++----------- .../app/gamenative/utils/GpuCompatHelper.kt | 80 +++++++++++ 2 files changed, 135 insertions(+), 80 deletions(-) create mode 100644 app/src/main/java/app/gamenative/utils/GpuCompatHelper.kt 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 9cbef4a74f..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 @@ -117,13 +120,6 @@ import kotlinx.coroutines.withContext import java.io.File import java.util.Locale import kotlin.math.roundToInt -import android.os.Build -import com.winlator.core.GPUHelper.* -import com.winlator.core.GPUInformation -// Note: These imports are required to check the Android version (Build) -// and get information about the device's GPU (GPUInformation) to apply -// a targeted fix for the crash on specific hardware. -// --- /** * Gets the component title for Win Components settings group. @@ -240,31 +236,6 @@ fun ContainerConfigDialog( val customResolutionValidationErrorRef = remember { mutableStateOf(null) } var customResolutionValidationError by customResolutionValidationErrorRef - // START: API 29 Compatibility Fix - // --- - // Note: This state variable controls the visibility and content of the error - // dialog that will be shown if the app fails to get GPU extensions. - // This is part of the graceful error handling to prevent a crash. - // --- - var gpuExtensionsErrorDialogState by rememberSaveable(stateSaver = MessageDialogState.Saver) { - mutableStateOf(MessageDialogState(visible = false)) - } - - // --- - // Note: This dialog will be displayed to the user only when the app fails to - // retrieve the GPU's Vulkan extensions on the problematic hardware. It - // informs them that a default set of extensions is being used instead. - // --- - MessageDialog( - visible = gpuExtensionsErrorDialogState.visible, - 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 = "OK", - onDismissRequest = { gpuExtensionsErrorDialogState = MessageDialogState(visible = false) }, - onConfirmClick = { gpuExtensionsErrorDialogState = MessageDialogState(visible = false) } - ) - // END: API 29 Compatibility Fix - LaunchedEffect(visible) { if (visible) { showCustomResolutionDialog = false @@ -356,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) @@ -484,49 +455,42 @@ fun ContainerConfigDialog( val exposedExtIndicesRef = rememberSaveable { mutableStateOf(listOf()) } var exposedExtIndices by exposedExtIndicesRef val inspectionMode = LocalInspectionMode.current - val gpuExtensions = remember(inspectionMode) { - // START: API 29 and Mali Compatibility Fix - // Provide a safe default while ensuring the problematic native call is - // guarded on Android 10 (API 29) devices that report a Mali renderer. - // Also return a short default list when running in Compose preview (inspectionMode). - if (inspectionMode) { - // In preview mode we can't call native functions, return a minimal list - listOf( - "VK_KHR_swapchain", - "VK_KHR_maintenance1", - "VK_KHR_timeline_semaphore", - ) - } else { - val renderer = try { - GPUInformation.getRenderer(context) - } catch (_: Throwable) { - "" - } - if (Build.VERSION.SDK_INT == 29 && renderer.contains("mali", ignoreCase = true)) { - // Do NOT call the native function on known-affected devices. - // Native crashes (SIGABRT) cannot be caught by try/catch, so we must - // avoid the JNI call entirely on Android 10 (API 29) + Mali renderers. - 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 = "OK" - ) - listOf( - "VK_KHR_surface", - "VK_KHR_android_surface", - "VK_KHR_swapchain", - "VK_KHR_maintenance1", - "VK_KHR_get_physical_device_properties2" - ) - } else { - // Normal path for devices that are not API 29 + Mali - vkGetDeviceExtensions().toList() - } + // 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), + ) } - // END: API 29 and Mali Compatibility Fix } + // --- + // END: API 29 Compatibility Fix + LaunchedEffect(config.graphicsDriverConfig) { val cfg = KeyValueSet(config.graphicsDriverConfig) // Sync Vulkan version index from config @@ -815,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) } @@ -1136,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( @@ -1186,8 +1164,7 @@ fun ContainerConfigDialog( Column( modifier = Modifier .padding( - top = app.gamenative.utils.PaddingUtils.statusBarAwarePadding() - .calculateTopPadding() + paddingValues.calculateTopPadding(), + top = app.gamenative.utils.PaddingUtils.statusBarAwarePadding().calculateTopPadding() + paddingValues.calculateTopPadding(), bottom = 32.dp + paddingValues.calculateBottomPadding(), start = paddingValues.calculateStartPadding(LayoutDirection.Ltr), end = paddingValues.calculateEndPadding(LayoutDirection.Ltr), @@ -1358,5 +1335,3 @@ internal fun ExecutablePathDropdown( } } } - - 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", + ) +} From 5c67adceb4c5ee1aca0b63b83b4b2cd1433734f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:23:53 +0000 Subject: [PATCH 4/5] Initial plan From e4459e1c7ca8737396eb9d967af975e84b62521b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:35:59 +0000 Subject: [PATCH 5/5] refactor: restructure gpu_helper.c with runtime API-level branching for Android 10 fix Co-authored-by: Catpotatos <221862400+Catpotatos@users.noreply.github.com> --- app/src/main/cpp/winlator/gpu_helper.c | 134 ++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 4 deletions(-) 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); }