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:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
|
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density|keyboard|keyboardHidden|navigation|uiMode"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|||||||
@@ -31,13 +31,25 @@ class CoverArtManager(private val context: Context) {
|
|||||||
|
|
||||||
val dest = cacheFile(gameTitle, consoleType)
|
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 {
|
val namesToTry = buildList {
|
||||||
add(gameTitle)
|
add(gameTitle)
|
||||||
val expanded = expandRegions(gameTitle)
|
val expanded = expandRegions(gameTitle)
|
||||||
expanded.filter { it != gameTitle }.forEach { add(it) }
|
expanded.filter { it != gameTitle }.forEach { add(it) }
|
||||||
val cleaned = cleanTitle(gameTitle)
|
val cleaned = cleanTitle(gameTitle)
|
||||||
if (cleaned != gameTitle && cleaned !in expanded) add(cleaned)
|
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) {
|
for (name in namesToTry) {
|
||||||
@@ -45,10 +57,12 @@ class CoverArtManager(private val context: Context) {
|
|||||||
.replace("+", "%20")
|
.replace("+", "%20")
|
||||||
val url = "$BASE_URL/${consoleType.libretroThumbnailRepo}/master/Named_Boxarts/$encoded.png"
|
val url = "$BASE_URL/${consoleType.libretroThumbnailRepo}/master/Named_Boxarts/$encoded.png"
|
||||||
if (downloadFile(url, dest)) {
|
if (downloadFile(url, dest)) {
|
||||||
|
Log.d(TAG, "Cover art hit for '$gameTitle' via '$name'")
|
||||||
return dest.absolutePath
|
return dest.absolutePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "No cover art match for '$gameTitle' after ${namesToTry.size} attempts")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,13 +103,14 @@ class CoverArtManager(private val context: Context) {
|
|||||||
private const val TAG = "CoverArtManager"
|
private const val TAG = "CoverArtManager"
|
||||||
private const val BASE_URL = "https://raw.githubusercontent.com/libretro-thumbnails"
|
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(
|
private val regionExpansions = mapOf(
|
||||||
"(U)" to listOf("(USA, Europe)", "(USA)"),
|
"(U)" to listOf("(USA)", "(USA, Europe)", "(World)"),
|
||||||
"(E)" to listOf("(Europe)", "(Europe, Australia)"),
|
"(E)" to listOf("(Europe)", "(Europe, Australia)", "(World)"),
|
||||||
"(J)" to listOf("(Japan)", "(Japan, USA)"),
|
"(J)" to listOf("(Japan)", "(Japan, USA)", "(World)"),
|
||||||
"(UE)" to listOf("(USA, Europe)"),
|
"(UE)" to listOf("(USA, Europe)", "(USA)", "(World)"),
|
||||||
"(JU)" to listOf("(Japan, USA)"),
|
"(JU)" to listOf("(Japan, USA)", "(USA)", "(Japan)"),
|
||||||
"(W)" to listOf("(World)"),
|
"(W)" to listOf("(World)"),
|
||||||
"(F)" to listOf("(France)"),
|
"(F)" to listOf("(France)"),
|
||||||
"(G)" to listOf("(Germany)"),
|
"(G)" to listOf("(Germany)"),
|
||||||
@@ -104,6 +119,19 @@ class CoverArtManager(private val context: Context) {
|
|||||||
"(Unl)" to listOf("(USA) (Unl)", "(Unl)"),
|
"(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> {
|
fun expandRegions(title: String): List<String> {
|
||||||
val stripped = title
|
val stripped = title
|
||||||
.replace(Regex("\\s*\\[[^]]*]"), "") // strip flags like [!], [b], [p1]
|
.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.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import coil.size.Scale
|
||||||
import com.lazy.emulate.data.model.Game
|
import com.lazy.emulate.data.model.Game
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -55,8 +58,16 @@ fun GameCard(
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
if (game.coverArtPath != null) {
|
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(
|
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",
|
contentDescription = "${game.title} cover art",
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
|
|||||||
Reference in New Issue
Block a user