Skip to content
Closed
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
61 changes: 31 additions & 30 deletions app/src/main/java/app/gamenative/utils/BestConfigService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,29 @@ 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.
*/
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<String, BestConfigResponse>()
// In-memory cache keyed by "${gameName}_${gpuName}", insertion-ordered for FIFO eviction
private val cache: MutableMap<String, BestConfigResponse> = Collections.synchronizedMap(
object : LinkedHashMap<String, BestConfigResponse>(MAX_CACHE_SIZE, 0.75f, false) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, BestConfigResponse>?): Boolean {
return size > MAX_CACHE_SIZE
}
}
)

// Last missing content description from validation (e.g. "DXVK 1.10.3")
private var lastMissingContentDescription: String? = null
Expand Down Expand Up @@ -120,32 +127,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")
Expand Down Expand Up @@ -325,7 +333,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)
}
}

Expand All @@ -336,7 +343,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)
}
}

Expand All @@ -353,7 +359,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)
}
}

Expand All @@ -369,7 +374,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)
Expand All @@ -385,7 +389,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)
}
}

Expand All @@ -411,7 +414,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)
}
}

Expand All @@ -422,7 +424,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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, GameCompatibilityService.GameCompatibilityResponse>()
private val timestamps = mutableMapOf<String, Long>()
private var cacheLoaded = false
private val inMemoryCache = ConcurrentHashMap<String, GameCompatibilityService.GameCompatibilityResponse>()
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
private val timestamps = ConcurrentHashMap<String, Long>()
@Volatile private var cacheLoaded = false

@Serializable
data class CachedCompatibilityResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,37 +113,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<String, GameCompatibilityResponse>()
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<String, GameCompatibilityResponse>()
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")
Expand Down
21 changes: 17 additions & 4 deletions app/src/main/java/com/winlator/core/GPUInformation.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
cachedGpuCards = (gpuNameList != null) ? new JSONArray(gpuNameList) : new JSONArray();
} catch (JSONException e) {
cachedGpuCards = new JSONArray();
}
}
return cachedGpuCards;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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)) {
Expand All @@ -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)) {
Expand Down
3 changes: 0 additions & 3 deletions app/src/main/java/com/winlator/xserver/Drawable.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down