The run of work that took the translated build from "boots" to "playable". Engine: - Flat guest mapping replaces the software MMU on aarch64 hosts. A 4 GiB PROT_NONE reservation lets a 32-bit guest address zero-extend safely, so tcg_out_qemu_ld/st short-circuit before tcg_out_tlb_read and the prologue materialises the base into X28. Measured 1.24x (51 vs 41 fps, interleaved A/B). Note the base must be set BEFORE UC_INIT - Unicorn inits lazily, and setting it after uc_open silently falls back to softmmu. - num_get<char> facet implemented, which was the real cause of the crash after the prologue race; a full race is now playable end to end. - Thread-stack free list + ReleaseThreadEngine, fixing the arena leak that showed up as a black screen when entering a race. kMaxGuestThreads 16 -> 64. - Real ARM32 FMOD now runs in-engine via the Java FMODAudioDevice bridge, with a per-thread JNIEnv. Two of the three blockers were our own single-image-era guards. Host/app: - Native crash handler: async-signal-safe, decodes the host fault back to a guest address, writes a report file and nothing else. CrashReportActivity picks it up on the NEXT launch, zips it, and offers to share. No backend, no automatic upload. - Game data ships inside the APK and self-extracts on first launch, so a tester installs one file and plays. Copy-to-.part-then-rename, with a free-space check up front. - EGL context preserved across pause, fixing black textures on resume. - Navigation bar hidden and re-hidden on focus gain; volume keys reported as system keys, checked before the loading-state gate. - x86_64 added to abiFilters: the ARM32 guest runs under tcg/i386 with no houdini in the path. The flat mapping is aarch64-only, so that host falls back to the software MMU - commented at the abiFilters line. Ignore rules added for app/translated/ (611 MB of signed release APK, which also carries the bundled OBB) and ostream_repro/build/. Co-Authored-By: Claude <noreply@anthropic.com>
149 lines
5.4 KiB
Kotlin
149 lines
5.4 KiB
Kotlin
package com.ea.ironmonkey
|
|
|
|
import android.content.Intent
|
|
import android.os.Bundle
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.enableEdgeToEdge
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.material3.Button
|
|
import androidx.compose.material3.LinearProgressIndicator
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Scaffold
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.darkColorScheme
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableFloatStateOf
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.unit.dp
|
|
import kotlin.concurrent.thread
|
|
|
|
/**
|
|
* First-launch unpacking of the game data bundled in the APK.
|
|
*
|
|
* Shown only when [GameDataInstaller.isInstalled] is false, so it appears once
|
|
* after install and never again. It exists because copying ~595 MB takes long
|
|
* enough that a tester staring at a frozen launcher would reasonably assume the
|
|
* game had hung.
|
|
*/
|
|
class GameDataUnpackActivity : ComponentActivity() {
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
enableEdgeToEdge()
|
|
super.onCreate(savedInstanceState)
|
|
|
|
if (GameDataInstaller.isInstalled(this)) {
|
|
startGame()
|
|
return
|
|
}
|
|
|
|
var fraction by mutableFloatStateOf(0f)
|
|
var copiedMb by mutableStateOf(0L)
|
|
var totalMb by mutableStateOf(0L)
|
|
var error by mutableStateOf<String?>(null)
|
|
|
|
setContent {
|
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
UnpackScreen(
|
|
fraction = fraction,
|
|
copiedMb = copiedMb,
|
|
totalMb = totalMb,
|
|
error = error,
|
|
onRetry = { recreate() },
|
|
)
|
|
}
|
|
}
|
|
|
|
thread(name = "game-data-unpack", isDaemon = true) {
|
|
val result = GameDataInstaller.install(this) { copied, total ->
|
|
// Updating Compose state is safe from any thread; the recomposition
|
|
// is scheduled onto the main thread by the snapshot system.
|
|
fraction = if (total > 0) copied.toFloat() / total else 0f
|
|
copiedMb = copied / 1_048_576
|
|
totalMb = total / 1_048_576
|
|
}
|
|
runOnUiThread {
|
|
when (result) {
|
|
is GameDataInstaller.Result.Ready -> startGame()
|
|
is GameDataInstaller.Result.Failed -> error = result.message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun startGame() {
|
|
startActivity(Intent(this, GameActivityMain::class.java))
|
|
finish()
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun UnpackScreen(
|
|
fraction: Float,
|
|
copiedMb: Long,
|
|
totalMb: Long,
|
|
error: String?,
|
|
onRetry: () -> Unit,
|
|
) {
|
|
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(innerPadding)
|
|
.padding(24.dp),
|
|
verticalArrangement = Arrangement.Center,
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
) {
|
|
if (error != null) {
|
|
Text(
|
|
text = "Не удалось подготовить игровые данные",
|
|
style = MaterialTheme.typography.headlineSmall,
|
|
textAlign = TextAlign.Center,
|
|
)
|
|
Text(
|
|
text = error,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
textAlign = TextAlign.Center,
|
|
modifier = Modifier.padding(top = 12.dp),
|
|
)
|
|
Button(onClick = onRetry, modifier = Modifier.padding(top = 24.dp)) {
|
|
Text("Повторить")
|
|
}
|
|
} else {
|
|
Text(
|
|
text = "Подготовка игровых данных",
|
|
style = MaterialTheme.typography.headlineSmall,
|
|
textAlign = TextAlign.Center,
|
|
)
|
|
Text(
|
|
text = "Выполняется один раз после установки.",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
textAlign = TextAlign.Center,
|
|
modifier = Modifier.padding(top = 8.dp),
|
|
)
|
|
LinearProgressIndicator(
|
|
progress = { fraction },
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(top = 24.dp),
|
|
)
|
|
Text(
|
|
text = if (totalMb > 0) "$copiedMb из $totalMb МБ" else "…",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
modifier = Modifier.padding(top = 12.dp),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|