Files
nfsmw-online/app/build.gradle.kts
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

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