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>
80 lines
4.0 KiB
C++
80 lines
4.0 KiB
C++
// Standalone native ARM32 EXECUTABLE (not a .so, unlike ostream_repro.cpp's
|
|
// own SHARED library target - see this directory's CMakeLists.txt) for the
|
|
// 2026-09-17 "does real hardware ever see runaway basic_stringbuf::overflow()
|
|
// growth" check (ARM64_TRANSLATION_LAYER.md). Built c++_static (same as
|
|
// ostream_repro.cpp's own build.sh switched to - real, compiled
|
|
// basic_string<char>/basic_stringbuf<char> logic baked directly into this
|
|
// binary's own .text, not external imports), meant to run DIRECTLY on the
|
|
// Galaxy A9 via `adb shell` - no Unicorn, no GuestEngine, no shims of any
|
|
// kind involved. Pure ground truth: does the SAME growth pattern observed
|
|
// under GuestEngine (push_back-driven capacity doubling via
|
|
// basic_stringbuf<char>::overflow(), real disasm at sub_2700E4/sub_27036C/
|
|
// sub_27003C) ever get stuck, or does it complete normally no matter how far
|
|
// it's pushed?
|
|
#include <cstdio>
|
|
#include <cstdint>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <chrono>
|
|
|
|
using Clock = std::chrono::steady_clock;
|
|
|
|
int main(int argc, char** argv) {
|
|
// Default: push well past the ~8MB (0x800000) point where GuestEngine's
|
|
// own trace showed the crash - if real hardware sails through this same
|
|
// magnitude with no trouble, that's strong evidence the growth mechanism
|
|
// itself is fine and the bug is specific to GuestEngine (its own malloc
|
|
// shim, or something else in its translation of these particular
|
|
// instructions), not a genuine bug in the shipped game/libc++ pairing.
|
|
long targetChars = (argc > 1) ? atol(argv[1]) : 20L * 1024 * 1024; // 20M
|
|
printf("ostream_stress: appending %ld chars one at a time via push_back\n", targetChars);
|
|
fflush(stdout);
|
|
|
|
auto t0 = Clock::now();
|
|
std::string s;
|
|
for (long i = 0; i < targetChars; i++) {
|
|
s.push_back((char)('a' + (i % 26)));
|
|
// Progress heartbeat every 1M chars, and explicitly flag whenever
|
|
// capacity crosses the same doubling milestones GuestEngine's own
|
|
// register trace captured (0xfffff, 0x1fffff, 0x3fffff, 0x7fffff) -
|
|
// lets a hang be diagnosed by "last milestone reached" even if the
|
|
// process needs to be killed rather than exiting cleanly.
|
|
if (i != 0 && (i % (1L * 1024 * 1024)) == 0) {
|
|
auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count();
|
|
printf(" [%ldms] i=%ld cap=%zu size=%zu\n", (long)elapsedMs, i, s.capacity(), s.size());
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
auto totalMs = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count();
|
|
printf("ostream_stress: push_back loop done in %ldms - final size=%zu cap=%zu\n",
|
|
(long)totalMs, s.size(), s.capacity());
|
|
fflush(stdout);
|
|
|
|
// Second phase: the ACTUAL crashing pattern is via basic_stringbuf's own
|
|
// xsputn/overflow chain (operator<<), not raw std::string::push_back -
|
|
// exercise that path too, via repeated small ostringstream writes,
|
|
// mirroring sub_4702D8's own "several separate writes before one
|
|
// extraction" shape but looped enough times to force the same magnitude
|
|
// of reallocation.
|
|
printf("ostream_stress: now stress-testing via ostringstream operator<<\n");
|
|
fflush(stdout);
|
|
auto t1 = Clock::now();
|
|
std::ostringstream oss;
|
|
long chunkCount = targetChars / 16; // ~16 chars per write, same order of magnitude
|
|
for (long i = 0; i < chunkCount; i++) {
|
|
oss << "0123456789ABCDEF";
|
|
if (i != 0 && (i % (65536)) == 0) {
|
|
auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t1).count();
|
|
std::string cur = oss.str();
|
|
printf(" [oss %ldms] i=%ld size-so-far=%zu\n", (long)elapsedMs, i, cur.size());
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
std::string result = oss.str();
|
|
auto ossMs = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t1).count();
|
|
printf("ostream_stress: ostringstream loop done in %ldms - final size=%zu\n",
|
|
(long)ossMs, result.size());
|
|
fflush(stdout);
|
|
return 0;
|
|
}
|