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>
133 lines
7.1 KiB
C++
133 lines
7.1 KiB
C++
// 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;
|
|
}
|