Fix white-screen on USB detach and OOM from cover art bitmaps
Three independent stability fixes that surfaced while testing the Joy-Con controller path: 1. Manifest: add keyboard|keyboardHidden|navigation|uiMode to MainActivity's configChanges. Without these, unplugging the OTG cable mid-game triggered a navigation/keyboard configuration change that destroyed and recreated the activity. The new activity instance never re-established the emulator surface cleanly, leaving a white screen, and Samsung Freecess then froze the seemingly-idle process. 2. GameCard: cap Coil's image decode at 384px. LibRetro thumbnail PNGs are often 1500x2000+, which burns ~12 MB of bitmap per card. With 30 visible tiles that's hundreds of MB and was OOM-ing the app during in-game input delivery (NewObjectV in nativePollOnce → JNI abort). 3. CoverArtManager: try (World) as a fallback region tag and add a cleaned- title-plus-common-region cascade. LibRetro stores Super Mario Bros. as '(World).png' but most US dumps tag the ROM '(U)', so the previous expansion table only tried (USA)/(USA, Europe) and missed it.
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density|keyboard|keyboardHidden|navigation|uiMode"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -31,13 +31,25 @@ class CoverArtManager(private val context: Context) {
|
||||
|
||||
val dest = cacheFile(gameTitle, consoleType)
|
||||
|
||||
// Try the title as-is, with expanded regions, then fully cleaned
|
||||
// Build the list of candidate filenames to try, in priority order:
|
||||
// 1. Title as-is
|
||||
// 2. Title with GoodTools-style region tags expanded into LibRetro forms
|
||||
// (e.g. "(U)" -> "(USA)", "(USA, Europe)", "(World)")
|
||||
// 3. Fully cleaned title (no parens at all) — rare hit
|
||||
// 4. Cleaned title with each common LibRetro region tag appended — this catches
|
||||
// cases like "Super Mario Bros. (U)" where LibRetro stores "(World)" but our
|
||||
// region table didn't know that this specific game used the World tag.
|
||||
val namesToTry = buildList {
|
||||
add(gameTitle)
|
||||
val expanded = expandRegions(gameTitle)
|
||||
expanded.filter { it != gameTitle }.forEach { add(it) }
|
||||
val cleaned = cleanTitle(gameTitle)
|
||||
if (cleaned != gameTitle && cleaned !in expanded) add(cleaned)
|
||||
// Append fallback region tags to the bare cleaned title
|
||||
for (region in COMMON_LIBRETRO_REGIONS) {
|
||||
val candidate = "$cleaned $region"
|
||||
if (candidate !in this) add(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
for (name in namesToTry) {
|
||||
@@ -45,10 +57,12 @@ class CoverArtManager(private val context: Context) {
|
||||
.replace("+", "%20")
|
||||
val url = "$BASE_URL/${consoleType.libretroThumbnailRepo}/master/Named_Boxarts/$encoded.png"
|
||||
if (downloadFile(url, dest)) {
|
||||
Log.d(TAG, "Cover art hit for '$gameTitle' via '$name'")
|
||||
return dest.absolutePath
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "No cover art match for '$gameTitle' after ${namesToTry.size} attempts")
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -89,13 +103,14 @@ class CoverArtManager(private val context: Context) {
|
||||
private const val TAG = "CoverArtManager"
|
||||
private const val BASE_URL = "https://raw.githubusercontent.com/libretro-thumbnails"
|
||||
|
||||
// GoodNES (U) can mean (USA) or (USA, Europe) — try both
|
||||
// GoodTools-style short region tags can map to several LibRetro long forms.
|
||||
// Order matters — earlier entries are tried first.
|
||||
private val regionExpansions = mapOf(
|
||||
"(U)" to listOf("(USA, Europe)", "(USA)"),
|
||||
"(E)" to listOf("(Europe)", "(Europe, Australia)"),
|
||||
"(J)" to listOf("(Japan)", "(Japan, USA)"),
|
||||
"(UE)" to listOf("(USA, Europe)"),
|
||||
"(JU)" to listOf("(Japan, USA)"),
|
||||
"(U)" to listOf("(USA)", "(USA, Europe)", "(World)"),
|
||||
"(E)" to listOf("(Europe)", "(Europe, Australia)", "(World)"),
|
||||
"(J)" to listOf("(Japan)", "(Japan, USA)", "(World)"),
|
||||
"(UE)" to listOf("(USA, Europe)", "(USA)", "(World)"),
|
||||
"(JU)" to listOf("(Japan, USA)", "(USA)", "(Japan)"),
|
||||
"(W)" to listOf("(World)"),
|
||||
"(F)" to listOf("(France)"),
|
||||
"(G)" to listOf("(Germany)"),
|
||||
@@ -104,6 +119,19 @@ class CoverArtManager(private val context: Context) {
|
||||
"(Unl)" to listOf("(USA) (Unl)", "(Unl)"),
|
||||
)
|
||||
|
||||
// Region tags to append to a cleaned (paren-stripped) title as a fallback for ROMs
|
||||
// whose original filename had no region tag, an unrecognized region tag, or a tag
|
||||
// that doesn't match what LibRetro stores for that specific game (e.g. SMB1 is
|
||||
// "(World)" but most US dumps tag it "(U)").
|
||||
private val COMMON_LIBRETRO_REGIONS = listOf(
|
||||
"(USA)",
|
||||
"(World)",
|
||||
"(USA, Europe)",
|
||||
"(Europe)",
|
||||
"(Japan)",
|
||||
"(Japan, USA)",
|
||||
)
|
||||
|
||||
fun expandRegions(title: String): List<String> {
|
||||
val stripped = title
|
||||
.replace(Regex("\\s*\\[[^]]*]"), "") // strip flags like [!], [b], [p1]
|
||||
|
||||
@@ -24,9 +24,12 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Scale
|
||||
import com.lazy.emulate.data.model.Game
|
||||
import java.io.File
|
||||
|
||||
@@ -55,8 +58,16 @@ fun GameCard(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (game.coverArtPath != null) {
|
||||
// Cap decode size — LibRetro thumbnail PNGs can be 1500x2000+, which
|
||||
// burns ~12 MB of bitmap memory each. With 30 cards visible that's
|
||||
// hundreds of MB and was OOM-ing the app during in-game input delivery.
|
||||
// 384px is more than enough for a tile in a grid.
|
||||
AsyncImage(
|
||||
model = File(game.coverArtPath),
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(File(game.coverArtPath))
|
||||
.size(384)
|
||||
.scale(Scale.FILL)
|
||||
.build(),
|
||||
contentDescription = "${game.title} cover art",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
|
||||
Reference in New Issue
Block a user