Stable checkpoint: game reaches playable 3D gameplay on ARM64

Verified live on a Pixel 6a: the game passes its EULA, loads the prologue
and renders real 3D gameplay, with zero heap exhaustion, zero faults and
zero rejected frees over a full session.

Root causes fixed in this state, each backed by a measurement (details and
the list of refuted theories live in ARM64_TRANSLATION_LAYER.md):

* JNI varargs float->double promotion. C promotes float to double in any
  varargs call and every Call*Method form is varargs, so reading one 4-byte
  slot yielded the double's always-zero low half. EVERY float argument
  passed to Java was silently becoming 0; text was just where it showed.

* GuestHeap ~4x memory overhead. Power-of-two size classes carving the full
  class, plus segregated free lists that could never share memory between
  sizes. Reworked to exact sizing with O(log n) best-fit reuse and splitting
  (deliberately not a linear scan - this allocator already had an O(n) perf
  cliff in its history). Peak live now 207MB against the real A9's 199MB,
  fragmentation ~2.5MB. Also fixed: realloc reading past the old block on
  shrink, a 32-bit overflow in calloc, and drifting payload alignment.

* Unbounded FMOD fake-handle leak into the never-freeing permanent arena,
  which is why enlarging that arena had not helped.

* Frame presentation, corrected against A9 ground truth: the real frame has
  three default-framebuffer colour clears and ONE present at the end; this
  engine had been presenting on each of them.

Load-time acceleration (zlib_accel.cpp): host zlib now serves inflate and
crc32, the latter measured by the block profiler as the single hottest guest
routine at 17.7%. Streams are only taken over when this layer saw their own
inflateInit2_, so unknown streams (libpng's, among others) still run the
original emulated path.

name_lookup_accel.cpp is present but its hook is NOT registered - it crashed
on bad assumptions about guest table lifetime and is kept as a starting
point, with both mistakes recorded in its comments.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-19 02:12:03 +03:00
co-authored by Claude
parent 9a5736bf3f
commit d5e6037fc7
744 changed files with 528750 additions and 272 deletions
+67
View File
@@ -0,0 +1,67 @@
#include "profiler.h"
#include "../util/util.h"
#include <atomic>
#include <mutex>
#include <unordered_map>
#include <vector>
#include <algorithm>
#include <chrono>
#include <thread>
namespace {
std::atomic<bool> g_profilingEnabled{false};
std::mutex g_histMutex;
std::unordered_map<uint64_t, uint64_t> g_hist;
std::atomic<uint64_t> g_totalSamples{0};
std::atomic<bool> g_dumpThreadStarted{false};
} // namespace
void EnableProfiling() { g_profilingEnabled.store(true, std::memory_order_relaxed); }
bool ProfilingEnabled() { return g_profilingEnabled.load(std::memory_order_relaxed); }
void ProfileBlockHookCb(uc_engine*, uint64_t address, uint32_t, void*) {
// thread_local, not a shared "last sample" - each real host thread has
// its own uc_engine (see guest_engine.h's "Multithreading model") and
// fires this independently; gating per-thread avoids one busy thread's
// sampling starving another's.
static thread_local std::chrono::steady_clock::time_point lastSample{};
auto now = std::chrono::steady_clock::now();
if (now - lastSample < std::chrono::milliseconds(2)) return;
lastSample = now;
std::lock_guard<std::mutex> lock(g_histMutex);
g_hist[address]++;
g_totalSamples.fetch_add(1, std::memory_order_relaxed);
}
void StartProfileDumpThread() {
bool expected = false;
if (!g_dumpThreadStarted.compare_exchange_strong(expected, true)) return; // already running
std::thread([]() {
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(3));
std::vector<std::pair<uint64_t, uint64_t>> top;
{
std::lock_guard<std::mutex> lock(g_histMutex);
top.assign(g_hist.begin(), g_hist.end());
}
if (top.empty()) continue;
std::sort(top.begin(), top.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
uint64_t total = g_totalSamples.load(std::memory_order_relaxed);
Log("PROFILE: %llu total samples across %zu distinct block addresses - top hot PCs:",
(unsigned long long)total, top.size());
for (size_t i = 0; i < top.size() && i < 15; i++) {
Log("PROFILE: 0x%llx - %llu samples (%.1f%%)",
(unsigned long long)top[i].first, (unsigned long long)top[i].second,
total ? 100.0 * (double)top[i].second / (double)total : 0.0);
}
}
}).detach();
}