diff --git a/app/src/main/java/app/gamenative/utils/BestConfigService.kt b/app/src/main/java/app/gamenative/utils/BestConfigService.kt index 87f6c72ca..698ff7c71 100644 --- a/app/src/main/java/app/gamenative/utils/BestConfigService.kt +++ b/app/src/main/java/app/gamenative/utils/BestConfigService.kt @@ -25,7 +25,7 @@ import org.json.JSONObject import timber.log.Timber import java.util.Locale import java.util.concurrent.TimeUnit -import java.util.concurrent.ConcurrentHashMap +import java.util.Collections /** * Service for fetching best configurations for games from GameNative API. @@ -33,14 +33,21 @@ import java.util.concurrent.ConcurrentHashMap object BestConfigService { private const val API_BASE_URL = "https://gamenative-best-config-worker.gamenative.workers.dev/api/best-config" private const val TIMEOUT_SECONDS = 10L + private const val MAX_CACHE_SIZE = 100 private val httpClient = OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build() - // In-memory cache keyed by "${gameName}_${gpuName}" - private val cache = ConcurrentHashMap() + // In-memory cache keyed by "${gameName}_${gpuName}", insertion-ordered for FIFO eviction + private val cache: MutableMap = Collections.synchronizedMap( + object : LinkedHashMap(MAX_CACHE_SIZE, 0.75f, false) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { + return size > MAX_CACHE_SIZE + } + } + ) // Last missing content description from validation (e.g. "DXVK 1.10.3") private var lastMissingContentDescription: String? = null @@ -108,32 +115,33 @@ object BestConfigService { val response = httpClient.newCall(request).execute() - if (!response.isSuccessful) { - Timber.tag("BestConfigService") - .w("API request failed - HTTP ${response.code}") - return@withTimeout null - } + response.use { + if (!it.isSuccessful) { + Timber.tag("BestConfigService") + .w("API request failed - HTTP ${it.code}") + return@withTimeout null + } - val responseBody = response.body?.string() ?: return@withTimeout null - val jsonResponse = JSONObject(responseBody) + val responseBody = it.body?.string() ?: return@withTimeout null + val jsonResponse = JSONObject(responseBody) - val bestConfigJson = jsonResponse.getJSONObject("bestConfig") - val bestConfig = Json.parseToJsonElement(bestConfigJson.toString()).jsonObject + val bestConfigJson = jsonResponse.getJSONObject("bestConfig") + val bestConfig = Json.parseToJsonElement(bestConfigJson.toString()).jsonObject - val bestConfigResponse = BestConfigResponse( - bestConfig = bestConfig, - matchType = jsonResponse.getString("matchType"), - matchedGpu = jsonResponse.getString("matchedGpu"), - matchedDeviceId = jsonResponse.getInt("matchedDeviceId") - ) + val bestConfigResponse = BestConfigResponse( + bestConfig = bestConfig, + matchType = jsonResponse.getString("matchType"), + matchedGpu = jsonResponse.getString("matchedGpu"), + matchedDeviceId = jsonResponse.getInt("matchedDeviceId") + ) - // Cache the response - cache[cacheKey] = bestConfigResponse + cache[cacheKey] = bestConfigResponse - Timber.tag("BestConfigService") - .d("Fetched best config for $gameName on $gpuName (matchType: ${bestConfigResponse.matchType})") + Timber.tag("BestConfigService") + .d("Fetched best config for $gameName on $gpuName (matchType: ${bestConfigResponse.matchType})") - bestConfigResponse + bestConfigResponse + } } } catch (e: java.util.concurrent.TimeoutException) { Timber.tag("BestConfigService") @@ -313,7 +321,6 @@ object BestConfigService { if (version.isNotEmpty() && !ManifestComponentHelper.versionExists(version, availableDxvk)) { Timber.tag("BestConfigService").w("DXVK version $version not found, updating to PrefManager default") return "DXVK $version" - filteredJson.put("dxwrapperConfig", PrefManager.dxWrapperConfig) } } @@ -324,7 +331,6 @@ object BestConfigService { if (version.isNotEmpty() && !ManifestComponentHelper.versionExists(version, availableVkd3d)) { Timber.tag("BestConfigService").w("VKD3D version $version not found, updating to PrefManager default") return "VKD3D $version" - filteredJson.put("dxwrapperConfig", PrefManager.dxWrapperConfig) } } @@ -341,7 +347,6 @@ object BestConfigService { if (!ManifestComponentHelper.versionExists(box64Version, box64VersionsToCheck)) { Timber.tag("BestConfigService").w("Box64 version $box64Version not found in $containerVariant variant entries, updating to PrefManager default") return "Box64 $box64Version" - filteredJson.put("box64Version", PrefManager.box64Version) } } @@ -357,7 +362,6 @@ object BestConfigService { if (fexcoreVersion.isNotEmpty() && !ManifestComponentHelper.versionExists(fexcoreVersion, availableFexcore)) { Timber.tag("BestConfigService").w("FEXCore version $fexcoreVersion not found, updating to PrefManager default") return "FEXCore $fexcoreVersion" - filteredJson.put("fexcoreVersion", PrefManager.fexcoreVersion) } // Validate Wine/Proton version (check separately based on container variant) @@ -373,7 +377,6 @@ object BestConfigService { if (!ManifestComponentHelper.versionExists(wineVersion, wineVersionsToCheck)) { Timber.tag("BestConfigService").w("Wine version $wineVersion not found in $containerVariant variant entries, updating to PrefManager default") return "Wine $wineVersion" - filteredJson.put("wineVersion", PrefManager.wineVersion) } } @@ -399,7 +402,6 @@ object BestConfigService { if (preset == null) { Timber.tag("BestConfigService").w("Box64 preset $box64Preset not found, updating to PrefManager default") return "Box64 preset $box64Preset" - filteredJson.put("box64Preset", PrefManager.box64Preset) } } @@ -410,7 +412,6 @@ object BestConfigService { if (preset == null) { Timber.tag("BestConfigService").w("FEXCore preset $fexcorePreset not found, updating to PrefManager default") return "FEXCore preset $fexcorePreset" - filteredJson.put("fexcorePreset", PrefManager.fexcorePreset) } } diff --git a/app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt b/app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt index 79dc029af..4d5674400 100644 --- a/app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt +++ b/app/src/main/java/app/gamenative/utils/GameCompatibilityCache.kt @@ -6,17 +6,18 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap /** - * Persistent cache for game compatibility responses with 7-day TTL. + * Persistent cache for game compatibility responses with 6-hour TTL. * Uses lazy expiration - checks expiration on access, not on load (optimizes performance). */ object GameCompatibilityCache { private const val CACHE_TTL_MS = 6 * 60 * 60 * 1000L // 6 hours - private val inMemoryCache = mutableMapOf() - private val timestamps = mutableMapOf() - private var cacheLoaded = false + private val inMemoryCache = ConcurrentHashMap() + private val timestamps = ConcurrentHashMap() + @Volatile private var cacheLoaded = false @Serializable data class CachedCompatibilityResponse( diff --git a/app/src/main/java/app/gamenative/utils/GameCompatibilityService.kt b/app/src/main/java/app/gamenative/utils/GameCompatibilityService.kt index ccd63158d..ca56481cc 100644 --- a/app/src/main/java/app/gamenative/utils/GameCompatibilityService.kt +++ b/app/src/main/java/app/gamenative/utils/GameCompatibilityService.kt @@ -101,37 +101,39 @@ object GameCompatibilityService { val response = httpClient.newCall(request).execute() - if (!response.isSuccessful) { - Timber.tag("GameCompatibilityService") - .w("API request failed - HTTP ${response.code}") - return@withTimeout null - } - - val responseBody = response.body?.string() ?: return@withTimeout null - val jsonResponse = JSONObject(responseBody) - - val result = mutableMapOf() - val keys = jsonResponse.keys() + response.use { + if (!it.isSuccessful) { + Timber.tag("GameCompatibilityService") + .w("API request failed - HTTP ${it.code}") + return@withTimeout null + } + + val responseBody = it.body?.string() ?: return@withTimeout null + val jsonResponse = JSONObject(responseBody) + + val result = mutableMapOf() + val keys = jsonResponse.keys() + + while (keys.hasNext()) { + val gameName = keys.next() + val gameData = jsonResponse.getJSONObject(gameName) + + val compatibilityResponse = GameCompatibilityResponse( + gameName = gameName, + totalPlayableCount = gameData.optInt("totalPlayableCount", 0), + gpuPlayableCount = gameData.optInt("gpuPlayableCount", 0), + avgRating = gameData.optDouble("avgRating", 0.0).toFloat(), + hasBeenTried = gameData.optBoolean("hasBeenTried", false), + isNotWorking = gameData.optBoolean("isNotWorking", false) + ) + + result[gameName] = compatibilityResponse + } - while (keys.hasNext()) { - val gameName = keys.next() - val gameData = jsonResponse.getJSONObject(gameName) - - val compatibilityResponse = GameCompatibilityResponse( - gameName = gameName, - totalPlayableCount = gameData.optInt("totalPlayableCount", 0), - gpuPlayableCount = gameData.optInt("gpuPlayableCount", 0), - avgRating = gameData.optDouble("avgRating", 0.0).toFloat(), - hasBeenTried = gameData.optBoolean("hasBeenTried", false), - isNotWorking = gameData.optBoolean("isNotWorking", false) - ) - - result[gameName] = compatibilityResponse + Timber.tag("GameCompatibilityService") + .d("Fetched compatibility for ${result.size} games") + result } - - Timber.tag("GameCompatibilityService") - .d("Fetched compatibility for ${result.size} games") - result } } catch (e: java.util.concurrent.TimeoutException) { Timber.tag("GameCompatibilityService") diff --git a/app/src/main/java/com/winlator/core/GPUInformation.java b/app/src/main/java/com/winlator/core/GPUInformation.java index 7ee9996d0..00a1afea3 100644 --- a/app/src/main/java/com/winlator/core/GPUInformation.java +++ b/app/src/main/java/com/winlator/core/GPUInformation.java @@ -27,11 +27,25 @@ public abstract class GPUInformation { System.loadLibrary("extras"); } + // Cache parsed GPU cards JSON to avoid re-parsing on every call + private static JSONArray cachedGpuCards = null; + + private static synchronized JSONArray getGpuCards(Context context) { + if (cachedGpuCards == null) { + try { + String gpuNameList = FileUtils.readString(context, "gpu_cards.json"); + cachedGpuCards = (gpuNameList != null) ? new JSONArray(gpuNameList) : new JSONArray(); + } catch (JSONException e) { + cachedGpuCards = new JSONArray(); + } + } + return cachedGpuCards; + } + public static String getDeviceIdFromGPUName(Context context, String gpuName) { - String gpuNameList = FileUtils.readString(context, "gpu_cards.json"); String deviceId = ""; try { - JSONArray jsonArray = new JSONArray(gpuNameList); + JSONArray jsonArray = getGpuCards(context); for (int i = 0; i < jsonArray.length(); i++) { JSONObject jobj = jsonArray.getJSONObject(i); if (jobj.getString("name").contains(gpuName)) { @@ -45,10 +59,9 @@ public static String getDeviceIdFromGPUName(Context context, String gpuName) { } public static String getVendorIdFromGPUName(Context context, String gpuName) { - String gpuNameList = FileUtils.readString(context, "gpu_cards.json"); String vendorId = ""; try { - JSONArray jsonArray = new JSONArray(gpuNameList); + JSONArray jsonArray = getGpuCards(context); for (int i = 0; i < jsonArray.length(); i++) { JSONObject jobj = jsonArray.getJSONObject(i); if (jobj.getString("name").contains(gpuName)) { diff --git a/app/src/main/java/com/winlator/xserver/Drawable.java b/app/src/main/java/com/winlator/xserver/Drawable.java index b177af4d6..b8a2cc68b 100644 --- a/app/src/main/java/com/winlator/xserver/Drawable.java +++ b/app/src/main/java/com/winlator/xserver/Drawable.java @@ -119,9 +119,6 @@ public void drawImage(short srcX, short srcY, short dstX, short dstY, short widt copyArea(srcX, srcY, dstX, dstY, width, height, totalWidth, this.getStride(), data, this.data); } - this.data.rewind(); - data.rewind(); - forceUpdate(); } this.data.rewind(); data.rewind();