diff --git a/mpcore/src/main/cpp/emu/rtti_shims.cpp b/mpcore/src/main/cpp/emu/rtti_shims.cpp index e8ab0d1..036c573 100644 --- a/mpcore/src/main/cpp/emu/rtti_shims.cpp +++ b/mpcore/src/main/cpp/emu/rtti_shims.cpp @@ -1,3 +1,5 @@ +#include +#include #include "rtti_shims.h" #include "../util/util.h" #include @@ -64,9 +66,12 @@ struct GuestVmiClassTypeInfoHeader { // __vmi_class_type_info (multiple bases, recurse each, fail on ambiguity). // Virtual-base offsets (need the vbase table, not just a static offset) // are a documented gap - skipped, not silently wrong. +thread_local int g_searchDepthReached = 0; + bool SearchBase(GuestEngine& eng, GuestAddr typeInfoAddr, GuestAddr dstTypeInfoAddr, int32_t curOffset, int32_t* outOffset, int depth) { if (!typeInfoAddr || depth > 32) return false; + if (depth > g_searchDepthReached) g_searchDepthReached = depth; if (typeInfoAddr == dstTypeInfoAddr) { *outOffset = curOffset; return true; @@ -111,9 +116,44 @@ bool SearchBase(GuestEngine& eng, GuestAddr typeInfoAddr, GuestAddr dstTypeInfoA return false; } +// Task #59 shape survey (2026-09-19, temporary). Native calls __dynamic_cast +// up to 824,000 times/sec and this engine can only serve ~285,000 - the +// boundary, not the algorithm, is the bottleneck (task #58). A guest-side +// ARM32 implementation would avoid the crossing entirely, but only pays off +// if it can be SMALL. So: which cases actually occur? If the overwhelming +// majority are "the object already is the target type" or one single- +// inheritance step, a short guest fast path with a fallback to this shim +// captures nearly everything. Counted, not assumed. +namespace { +constexpr bool kSurveyDynamicCast = false; +std::atomic g_dcTotal{0}, g_dcNullArg{0}, g_dcExact{0}, g_dcDepth1{0}, + g_dcDeeper{0}, g_dcMiss{0}, g_dcMaxDepth{0}; +void SurveyReport() { + static std::atomic lastNs{0}; + uint64_t now = (uint64_t)std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + uint64_t last = lastNs.load(std::memory_order_relaxed); + if (now - last < 2000000000ull) return; + if (!lastNs.compare_exchange_strong(last, now, std::memory_order_relaxed) || last == 0) return; + uint64_t t = g_dcTotal.load(std::memory_order_relaxed); + if (!t) return; + auto pct = [t](std::atomic& c) { + return 100.0 * (double)c.load(std::memory_order_relaxed) / (double)t; + }; + Log("rtti_shims: DCSHAPE total=%llu | exact(depth0)=%.1f%% oneBase(depth1)=%.1f%% " + "deeper=%.1f%% notFound=%.1f%% nullArg=%.1f%% | maxDepth=%llu", + (unsigned long long)t, pct(g_dcExact), pct(g_dcDepth1), pct(g_dcDeeper), pct(g_dcMiss), + pct(g_dcNullArg), (unsigned long long)g_dcMaxDepth.load(std::memory_order_relaxed)); +} +} // namespace + uint32_t Shim_dynamic_cast(GuestEngine& eng, uint32_t srcPtr, uint32_t /*srcTypeInfo*/, uint32_t dstTypeInfo, uint32_t /*src2dstHint*/, uint32_t) { - if (!srcPtr || !dstTypeInfo) return 0; + if (kSurveyDynamicCast) { g_dcTotal.fetch_add(1, std::memory_order_relaxed); SurveyReport(); } + if (!srcPtr || !dstTypeInfo) { + if (kSurveyDynamicCast) g_dcNullArg.fetch_add(1, std::memory_order_relaxed); + return 0; + } uint32_t objVtablePtr = 0; memcpy(&objVtablePtr, eng.G2H(srcPtr), 4); if (!objVtablePtr) return 0; @@ -130,7 +170,20 @@ uint32_t Shim_dynamic_cast(GuestEngine& eng, uint32_t srcPtr, uint32_t /*srcType GuestAddr mostDerivedPtr = (GuestAddr)((int32_t)srcPtr + (int32_t)offsetToTopRaw); int32_t foundOffset = 0; - if (!SearchBase(eng, dynTypeInfo, dstTypeInfo, 0, &foundOffset, 0)) return 0; + if (kSurveyDynamicCast && dynTypeInfo == dstTypeInfo) { + g_dcExact.fetch_add(1, std::memory_order_relaxed); + return (uint32_t)mostDerivedPtr; + } + g_searchDepthReached = 0; + if (!SearchBase(eng, dynTypeInfo, dstTypeInfo, 0, &foundOffset, 0)) { + if (kSurveyDynamicCast) g_dcMiss.fetch_add(1, std::memory_order_relaxed); + return 0; + } + if (kSurveyDynamicCast) { + (g_searchDepthReached <= 1 ? g_dcDepth1 : g_dcDeeper).fetch_add(1, std::memory_order_relaxed); + uint64_t d = (uint64_t)g_searchDepthReached, prev = g_dcMaxDepth.load(std::memory_order_relaxed); + while (d > prev && !g_dcMaxDepth.compare_exchange_weak(prev, d, std::memory_order_relaxed)) {} + } return (uint32_t)((int32_t)mostDerivedPtr + foundOffset); } diff --git a/trace_agent/CMakeLists.txt b/trace_agent/CMakeLists.txt index ee1b7ec..79c306d 100644 --- a/trace_agent/CMakeLists.txt +++ b/trace_agent/CMakeLists.txt @@ -31,3 +31,11 @@ target_link_libraries(trace_agent ) target_compile_options(trace_agent PRIVATE -Wall -Wno-unused-parameter) + +# __dynamic_cast is also defined in libc++abi.a, which the NDK links +# statically into this agent. Interposing it (task #58 - counting the real +# call rate on native hardware) therefore produces a duplicate-symbol link +# error. Our definition comes from this target's own objects, which the +# linker sees before the archive, so it wins; the archive copy is simply +# left unused. Scoped to this target rather than set globally. +target_link_options(trace_agent PRIVATE "-Wl,--allow-multiple-definition") diff --git a/trace_agent/libc_gles_trace.cpp b/trace_agent/libc_gles_trace.cpp index d5695b9..36f5b9c 100644 --- a/trace_agent/libc_gles_trace.cpp +++ b/trace_agent/libc_gles_trace.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -564,4 +565,40 @@ void glLinkProgram(GLuint program) { } } + +// ---- __dynamic_cast rate (2026-09-19, task #58) ---- +// The emulated engine's own per-shim counter found __dynamic_cast making up +// 49% of ALL shim crossings during a prologue load - 284,986 calls/sec. The +// obvious question that number does NOT answer: is that the game's own +// behaviour, or something this engine induces? libapp.so imports +// __dynamic_cast as an undefined symbol, so LD_PRELOAD can count the real +// thing on native ARM32 hardware and settle it. +// +// Reports a rate once a second rather than logging each call: at a quarter +// million per second, per-call logging would dominate the measurement (and +// this agent writes to a file, see InitFileLog). Deliberately mirrors the +// engine's TOPSHIMS accounting so the two numbers are directly comparable. +void* __dynamic_cast(const void* sub, const void* src, const void* dst, ptrdiff_t src2dst) { + static auto real = RealSym( + "__dynamic_cast"); + static std::atomic calls{0}; + static std::atomic lastNs{0}; + static uint64_t prevCalls = 0; + + uint64_t n = calls.fetch_add(1, std::memory_order_relaxed) + 1; + + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint64_t now = (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; + uint64_t last = lastNs.load(std::memory_order_relaxed); + if (now - last >= 1000000000ull && + lastNs.compare_exchange_strong(last, now, std::memory_order_relaxed) && last != 0) { + double dt = (now - last) / 1e9; + LOGI("DYNCAST native rate=%.0f/s (total %llu)", (double)(n - prevCalls) / dt, + (unsigned long long)n); + prevCalls = n; + } + return real(sub, src, dst, src2dst); +} + } // extern "C"