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>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user