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,132 @@
|
||||
// Standalone, minimal armeabi-v7a artifact for the 2026-09-16
|
||||
// ARM64_TRANSLATION_LAYER.md "isolated std::ostringstream repro" test - see
|
||||
// that doc's entry of the same date for the full methodology and the
|
||||
// GuestEngine-side driver (mpcore/src/main/cpp/emu/ostream_repro_test.cpp)
|
||||
// that loads and calls this.
|
||||
//
|
||||
// Why this exists: the real game's own libapp.so, deep inside its shader-
|
||||
// variant builder (sub_4702D8, per ARM64_TRANSLATION_LAYER.md's 2026-09-16
|
||||
// entries), writes several string literals to a real, compiled
|
||||
// std::ostringstream-shaped object via operator<< and then extracts the
|
||||
// accumulated text via what's effectively basic_stringbuf<char>::str() -
|
||||
// and the extraction always comes back empty under GuestEngine, even though
|
||||
// register/memory probes confirmed real content was genuinely written
|
||||
// first. Rather than keep chasing that one binary's own hardcoded
|
||||
// addresses (explicitly the wrong approach per the user's own direction -
|
||||
// "ты сейчас пытаешься подогнать эмуляцию к одному единственному бинарнику,
|
||||
// это не правильный подход" - that only tells us about THIS game, not
|
||||
// whether GuestEngine itself has a real, general ostringstream-extraction
|
||||
// bug), this reproduces the EXACT SAME write-then-extract shape in total
|
||||
// isolation from every other line of game code, compiled fresh by the same
|
||||
// NDK toolchain and loaded as its own tiny ELF image
|
||||
// (GuestEngine::LoadSecondaryImage) alongside (not instead of) libapp.so.
|
||||
//
|
||||
// Deliberately built the SAME way libapp.so itself is (default NDK
|
||||
// ANDROID_STL=c++_shared, not a static-libstdc++/c++_static build) rather
|
||||
// than statically linking libc++ into this .so: rtti_shims.cpp's own
|
||||
// RegisterRttiImportShims already shows GuestEngine hand-reimplements a
|
||||
// SPECIFIC set of libc++-internal symbols (locale/ios_base/ctype<char>/
|
||||
// num_put<char>/__shared_weak_count/std::mutex - all undefined imports in
|
||||
// libapp.so's own .dynsym, meaning libapp.so links these dynamically
|
||||
// against libc++_shared.so too, while template-heavy code like
|
||||
// basic_stringbuf<char>'s own methods gets compiled directly into
|
||||
// libapp.so's .text, same as here). A c++_static build of this artifact
|
||||
// would sidestep ALL of those hand-shims entirely and test a completely
|
||||
// different code path - less isolated from "does the real game's runtime
|
||||
// dependency surface work," not more. Matching libapp.so's own linkage
|
||||
// means this test exercises the EXACT SAME shim functions the real crash
|
||||
// investigation already spent most of 2026-09-16 on (Shim_locale_ctor,
|
||||
// Shim_use_facet, Shim_ctype_char_do_widen, Shim_ios_base_init) - if the
|
||||
// bug lives in one of those, this test should reproduce it; if it doesn't,
|
||||
// that's real evidence the bug is specific to something in libapp.so's own
|
||||
// state/control flow instead.
|
||||
//
|
||||
// GuestEngine::LoadSecondaryImage resolves every undefined symbol through
|
||||
// the SAME ResolveOrCreateImportStub/RegisterImportShim table libapp.so's
|
||||
// own imports already use (see that function's own comment,
|
||||
// guest_engine.cpp) - so this artifact needs no special-casing on the
|
||||
// engine side beyond what already exists, with one confirmed exception:
|
||||
// operator new/delete (_Znwj/_ZdlPv et al.) had NO shim registered anywhere
|
||||
// in this codebase before this test - grepped the whole emu/ tree and came
|
||||
// up empty. Not a pre-existing bug: libapp.so's own build apparently
|
||||
// defines ITS OWN operator new/delete locally (a common AAA-engine pattern,
|
||||
// pooled/custom allocators overriding the global operator) - a LOCALLY
|
||||
// DEFINED symbol never touches ResolveOrCreateImportStub at all, so this
|
||||
// engine never had to shim it before. A plain artifact like this one that
|
||||
// does NOT override global operator new/delete needs the real ones, added
|
||||
// to rtti_shims.cpp (Shim_operator_new/Shim_operator_delete) specifically
|
||||
// to make this test possible - see that file's own comment.
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
|
||||
extern "C" __attribute__((visibility("default")))
|
||||
int TestOstreamAssembly(char* outBuf, int outBufSize) {
|
||||
// Matches the real game's own pattern (sub_4702D8): several SEPARATE
|
||||
// writes via operator<< before one .str() extraction, not a single
|
||||
// combined write - see this session's investigation notes on why that
|
||||
// distinction might matter (a single write could mask a bug that only
|
||||
// shows up across multiple overflow()/sputn() calls into the same
|
||||
// streambuf). Content itself doesn't matter - it's deliberately
|
||||
// boilerplate-shaped (comment lines + a function skeleton) to loosely
|
||||
// mirror the real shader source text without claiming to BE a shader.
|
||||
std::ostringstream oss;
|
||||
oss << "//FRAGMENT SHADER\n";
|
||||
oss << "//===========\n\n";
|
||||
oss << "void main()\n{\n";
|
||||
oss << "}\n";
|
||||
|
||||
std::string result = oss.str();
|
||||
|
||||
int32_t len = (int32_t)result.size();
|
||||
if (outBuf && outBufSize >= 4) {
|
||||
memcpy(outBuf, &len, sizeof(len));
|
||||
int avail = outBufSize - 4;
|
||||
int copyLen = (int)result.size();
|
||||
if (copyLen > avail) copyLen = avail;
|
||||
if (copyLen > 0) memcpy(outBuf + 4, result.data(), (size_t)copyLen);
|
||||
if (copyLen < avail) outBuf[4 + copyLen] = 0; // NUL-terminate for easy logging, if room
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
// 2026-09-17, ARM64_TRANSLATION_LAYER.md - the 0x3d3d3d3d heap-overflow
|
||||
// chase. TestOstreamAssembly's own pattern (above) uses 4 short, similarly-
|
||||
// sized writes with no intervening function call - none of them alone force
|
||||
// an IMMEDIATE SSO->heap transition, and there's no real CALL boundary
|
||||
// between writes into the SAME stream. The real crash's own sequence
|
||||
// (sub_46FD58) is different in both respects: its FIRST write is
|
||||
// "//VERTEX SHADER\n//=============\n\n" (33 bytes - by itself already past
|
||||
// libc++'s ~22-byte SSO capacity, forcing an immediate heap transition on
|
||||
// the very first write), immediately followed by a call into a SEPARATE
|
||||
// function (sub_4711C8) that writes MORE content ("//Attributes\n
|
||||
// //==========\n", 26 bytes) into the SAME shared stream object, passed by
|
||||
// pointer across that real call boundary. Reproducing that exact shape here
|
||||
// - noinline to guarantee a genuine function-call boundary, not something
|
||||
// the compiler could inline away - to test whether THIS specific pattern
|
||||
// (not just "several small writes in one function") is what's needed to
|
||||
// expose the bug under GuestEngine.
|
||||
extern "C" __attribute__((noinline))
|
||||
void WriteAttributesSectionNested(std::ostringstream& oss) {
|
||||
oss << "//Attributes\n//==========\n";
|
||||
}
|
||||
|
||||
extern "C" __attribute__((visibility("default")))
|
||||
int TestOstreamAssemblyNested(char* outBuf, int outBufSize) {
|
||||
std::ostringstream oss;
|
||||
oss << "//VERTEX SHADER\n//=============\n\n";
|
||||
WriteAttributesSectionNested(oss);
|
||||
|
||||
std::string result = oss.str();
|
||||
|
||||
int32_t len = (int32_t)result.size();
|
||||
if (outBuf && outBufSize >= 4) {
|
||||
memcpy(outBuf, &len, sizeof(len));
|
||||
int avail = outBufSize - 4;
|
||||
int copyLen = (int)result.size();
|
||||
if (copyLen > avail) copyLen = avail;
|
||||
if (copyLen > 0) memcpy(outBuf + 4, result.data(), (size_t)copyLen);
|
||||
if (copyLen < avail) outBuf[4 + copyLen] = 0;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
Reference in New Issue
Block a user