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>
230 lines
10 KiB
Kotlin
230 lines
10 KiB
Kotlin
// Inside the android { } block the name `java` resolves to Gradle's own
|
|
// java extension and shadows the package, so these types have to be
|
|
// imported here rather than fully qualified at the use site.
|
|
import java.util.Properties
|
|
|
|
plugins {
|
|
alias(libs.plugins.android.application)
|
|
alias(libs.plugins.kotlin.android)
|
|
alias(libs.plugins.kotlin.compose)
|
|
}
|
|
|
|
android {
|
|
// ARM64-only-device prototype (see /ARM64_TRANSLATION_LAYER.md): "_arm"
|
|
// instead of "_mod" so this build can be installed side by side with
|
|
// the real armeabi-v7a mod, not overwrite it.
|
|
namespace = "com.ea.games.nfs13_arm"
|
|
compileSdk = 35
|
|
|
|
defaultConfig {
|
|
applicationId = "com.ea.games.nfs13_arm"
|
|
minSdk = 27
|
|
targetSdk = 35
|
|
versionCode = 1003128
|
|
versionName = "1.3.128"
|
|
|
|
// mpcore is now plain native ARM64 code driving an embedded ARM32
|
|
// CPU-emulation core - no armeabi-v7a native code is built/shipped
|
|
// for this app at all.
|
|
ndk.abiFilters.add("arm64-v8a")
|
|
// x86_64 (2026-09-22): lets the translated build install in WayDroid,
|
|
// whose Android image is x86_64. Unicorn picks its tcg/i386 backend
|
|
// there automatically, so the ARM32 guest is translated by US rather
|
|
// than by houdini - which is what made earlier WayDroid attempts with
|
|
// the native ARM build unreliable.
|
|
//
|
|
// NOTE: the flat guest mapping (task #61, worth 1.24x) is implemented
|
|
// only in tcg/aarch64, so an x86_64 host silently falls back to the
|
|
// software MMU and will be slower. Fine for a test target; port the
|
|
// same change into tcg/i386 if it ever becomes a shipping target.
|
|
ndk.abiFilters.add("x86_64")
|
|
|
|
// Default: false. Overridden to true by the "native32" flavor - see
|
|
// its own comment below.
|
|
buildConfigField("boolean", "NATIVE32", "false")
|
|
|
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
}
|
|
ndkVersion = "27.0.12077973"
|
|
|
|
// Release signing. The keystore and its password live OUTSIDE this repo
|
|
// (~/keystores/), deliberately: this is a git working tree, and a signing
|
|
// identity that can be committed eventually is committed. Gradle reads
|
|
// them from a properties file if it is there, and falls back to the debug
|
|
// key if it is not - so a checkout without the key still builds, it just
|
|
// produces something that cannot be shipped.
|
|
//
|
|
// To create the key (once, and then back it up - losing it means never
|
|
// being able to update an installed build):
|
|
//
|
|
// keytool -genkeypair -v -keystore ~/keystores/nfsmw-release.jks \
|
|
// -alias nfsmw -keyalg RSA -keysize 4096 -validity 10950
|
|
//
|
|
// then write ~/keystores/nfsmw-release.properties with:
|
|
// storeFile=/home/<you>/keystores/nfsmw-release.jks
|
|
// storePassword=...
|
|
// keyAlias=nfsmw
|
|
// keyPassword=...
|
|
signingConfigs {
|
|
val releaseProps = File(System.getProperty("user.home"), "keystores/nfsmw-release.properties")
|
|
if (releaseProps.isFile) {
|
|
val props = Properties().apply { releaseProps.inputStream().use { load(it) } }
|
|
create("release") {
|
|
storeFile = file(props.getProperty("storeFile"))
|
|
storePassword = props.getProperty("storePassword")
|
|
keyAlias = props.getProperty("keyAlias")
|
|
keyPassword = props.getProperty("keyPassword")
|
|
// Both modern signature schemes: v2 covers the whole APK and
|
|
// is what Android 7+ verifies, v3 allows rotating the key
|
|
// later without invalidating existing installs.
|
|
enableV2Signing = true
|
|
enableV3Signing = true
|
|
}
|
|
}
|
|
}
|
|
|
|
buildTypes {
|
|
release {
|
|
isMinifyEnabled = false
|
|
proguardFiles(
|
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
"proguard-rules.pro"
|
|
)
|
|
// Local-testing-only (2026-09-05, "why is this ~100x slower than
|
|
// native" investigation - see ARM64_TRANSLATION_LAYER.md): no
|
|
// real release signing config exists in this prototype, and
|
|
// there's no intent to ship this build anywhere - reusing the
|
|
// auto-generated debug keystore just makes `assembleTranslatedRelease`
|
|
// installable on a test device the same way translatedDebug
|
|
// already is. The actual point of testing this build type is the
|
|
// NATIVE side (mpcore's CMakeLists.txt now forces
|
|
// CMAKE_BUILD_TYPE=RelWithDebInfo unconditionally, so this
|
|
// isn't even required for that fix to take effect - it's here so
|
|
// this variant can be installed at all for a genuine debuggable=false
|
|
// comparison, e.g. CheckJNI's behavior).
|
|
// Real key when it exists, debug key otherwise. Announced at
|
|
// configuration time rather than discovered later from a
|
|
// mysteriously unshippable APK.
|
|
signingConfig = signingConfigs.findByName("release") ?: run {
|
|
logger.warn(
|
|
"RELEASE SIGNING: ~/keystores/nfsmw-release.properties not found - " +
|
|
"signing with the DEBUG key. This APK must NOT be distributed."
|
|
)
|
|
signingConfigs.getByName("debug")
|
|
}
|
|
}
|
|
}
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_11
|
|
targetCompatibility = JavaVersion.VERSION_11
|
|
}
|
|
kotlinOptions {
|
|
jvmTarget = "11"
|
|
}
|
|
androidResources {
|
|
// The bundled game data (assets/game_data.obb, ~595 MB) is an already
|
|
// compressed archive - deflating it again would cost a long build and
|
|
// a slower install for no size gain, and it must stay STORED so it can
|
|
// be streamed straight out of the APK.
|
|
noCompress += "obb"
|
|
}
|
|
|
|
buildFeatures {
|
|
compose = true
|
|
buildConfig = true
|
|
}
|
|
|
|
// "native32" flavor - diagnostic-only reference build for the native
|
|
// ARM32 tracing investigation (see ARM64_TRANSLATION_LAYER.md /
|
|
// sorted-popping-plum.md's "Native ARM32 tracing harness" plan). Real
|
|
// armeabi-v7a support and a BuildConfig.NATIVE32 flag GameActivityMain
|
|
// uses to load the real, unmodified native libraries directly
|
|
// (System.loadLibrary) instead of going through mpcore's emulation path
|
|
// - only meaningful on genuinely 32-bit-capable hardware (the Galaxy
|
|
// A9), never intended to ship. MUST be armeabi-v7a-ONLY (not just
|
|
// "armeabi-v7a added alongside arm64-v8a") - confirmed live on-device
|
|
// that shipping both ABIs makes Android launch the process via the
|
|
// 64-bit app_process64/Zygote (arm64-v8a is present, so it's preferred),
|
|
// which can never dlopen/LD_PRELOAD a 32-bit .so at all (a process's
|
|
// bitness is fixed for its whole lifetime, not per-library) - defeats
|
|
// the entire point of this flavor. ndk.abiFilters.clear() first,
|
|
// since AGP's per-flavor abiFilters otherwise ADD to defaultConfig's
|
|
// set rather than replace it.
|
|
flavorDimensions += "abi"
|
|
productFlavors {
|
|
create("translated") {
|
|
dimension = "abi"
|
|
// Default/existing behavior - the emulated arm64-v8a path,
|
|
// unchanged from before this flavor split existed.
|
|
}
|
|
create("native32") {
|
|
dimension = "abi"
|
|
ndk.abiFilters.clear()
|
|
ndk.abiFilters.add("armeabi-v7a")
|
|
buildConfigField("boolean", "NATIVE32", "true")
|
|
}
|
|
}
|
|
|
|
// 2026-09-16 (ARM64_TRANSLATION_LAYER.md - trace_agent wrap.sh deployment):
|
|
// manually injecting a bundled wrap.sh into the packaged native32 APK
|
|
// (AGP's own native-lib merge/strip pipeline only recognizes *.so files
|
|
// and silently drops anything else) failed to install at all with
|
|
// "Failed to extract native libraries, res=-2" under the default
|
|
// extractNativeLibs=false packaging - PackageManager validates every
|
|
// entry under lib/<abi>/ as a loadable library when it plans to mmap
|
|
// straight from the APK, and a plain shell script fails that check.
|
|
// Legacy (extracted-to-disk) packaging doesn't do that same strict
|
|
// validation. Only applied to native32 - the translated flavor doesn't
|
|
// need or want this (slightly larger install, marginally slower first
|
|
// native-lib load).
|
|
productFlavors.getByName("native32") {
|
|
packaging {
|
|
jniLibs {
|
|
useLegacyPackaging = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2026-09-06 (native32-on-real-hardware launch investigation - see
|
|
// ARM64_TRANSLATION_LAYER.md): ndk.abiFilters above only restricts THIS
|
|
// module's own native code - the "native32" flavor still needs `mpcore` on
|
|
// the compile classpath (GameActivityMain.kt's shared, non-flavor-specific
|
|
// source calls mpcore functions in its non-NATIVE32 branch), and mpcore's
|
|
// own CMakeLists always builds arm64-v8a regardless of which app flavor
|
|
// pulls it in. That arm64-v8a .so was silently merged into the native32
|
|
// APK too, so a real device with both an arm64-v8a AND an armeabi-v7a lib
|
|
// present chose to launch the process as 64-bit (app_process64) - which can
|
|
// never load this flavor's 32-bit fmodex/fmodevent/Nimble/app libraries at
|
|
// all, confirmed live via `UnsatisfiedLinkError: couldn't find "libfmodex.so"`
|
|
// (nativeLibraryDirectories only listed arm64/arm64-v8a paths). Explicitly
|
|
// drop every arm64-v8a .so from the native32 variant's packaging so the
|
|
// APK is genuinely armeabi-v7a-only, restoring 32-bit process selection.
|
|
androidComponents {
|
|
onVariants(selector().withFlavor("abi" to "native32")) { variant ->
|
|
variant.packaging.jniLibs.excludes.add("lib/arm64-v8a/*.so")
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
|
|
implementation(libs.androidx.core.ktx)
|
|
implementation(libs.androidx.appcompat)
|
|
implementation(libs.material)
|
|
testImplementation(libs.junit)
|
|
androidTestImplementation(libs.androidx.junit)
|
|
androidTestImplementation(libs.androidx.espresso.core)
|
|
|
|
implementation(platform(libs.androidx.compose.bom))
|
|
implementation(libs.androidx.ui)
|
|
implementation(libs.androidx.ui.graphics)
|
|
implementation(libs.androidx.material3)
|
|
implementation(libs.androidx.activity.compose)
|
|
debugImplementation(libs.androidx.ui.tooling)
|
|
|
|
implementation("com.parse.bolts:bolts-tasks:1.4.0")
|
|
implementation("org.apache.httpcomponents:httpclient-android:4.3.5.1")
|
|
implementation("com.google.code.gson:gson:2.7")
|
|
implementation(project(":mpcore"))
|
|
//implementation("com.android.support:support-v4:28.0.0")
|
|
} |