Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 130 additions & 4 deletions app/src/main/cpp/winlator/gpu_helper.c
Original file line number Diff line number Diff line change
@@ -1,9 +1,125 @@
#include <jni.h>
#include <vulkan/vulkan.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <string.h>
#include <android/api-level.h>

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;
Expand Down Expand Up @@ -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);
}
9 changes: 5 additions & 4 deletions app/src/main/java/app/gamenative/db/dao/EpicGameDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<EpicGame>>

@Query("SELECT * FROM epic_games WHERE is_installed = :isInstalled ORDER BY title ASC")
Expand All @@ -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<List<EpicGame>>

@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<List<EpicGame>>

@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<List<EpicGame>>

// 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")
Expand Down
13 changes: 7 additions & 6 deletions app/src/main/java/app/gamenative/db/dao/GOGGameDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<GOGGame>>

@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<GOGGame>

@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<List<GOGGame>>

@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<List<GOGGame>>

@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<Int>

@Query("SELECT id FROM gog_games")
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -452,17 +455,42 @@ fun ContainerConfigDialog(
val exposedExtIndicesRef = rememberSaveable { mutableStateOf(listOf<Int>()) }
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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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(
Expand Down
Loading