Files
nfsmw-online/app/src/main/java/com/ea/ironmonkey/GameDataUnpackActivity.kt
T
megboyzzandClaude 725ffbd8ed arm64: flat guest mapping, audio, crash reporting, self-extracting data
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>
2026-09-22 23:17:47 +03:00

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),
)
}
}
}
}