Files
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

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;
}