Files
nfsmw-online/trace_agent/libc_gles_trace.cpp
T
megboyzzandClaude 7af48860eb Survey dynamic_cast call shapes + native rate counter: 84% need no hierarchy walk
Task #58 asked whether the engine induces the __dynamic_cast flood or the
game does. A native LD_PRELOAD interposer added to trace_agent answers it:
native ARM32 on the A9 calls it up to 824,200/sec, against this engine's
284,986/sec. The game is simply that RTTI-heavy, and native absorbs it
because a real call costs tens of nanoseconds. Our rate is not demand, it is
supply - the shim boundary is throttling the guest.

(Interposing __dynamic_cast collides with libc++abi.a, which the NDK links
statically into the agent; resolved with --allow-multiple-definition scoped
to that target, since our object precedes the archive and wins.)

That makes a guest-side implementation interesting: Shim_dynamic_cast is
already a pure guest-memory walk - it reads the object's vtable, vtable[-1]
(dynamic type) and vtable[-2] (offset-to-top), then compares type_info
records. Nothing comes from the host, so the same algorithm could run as
emulated ARM32 with NO boundary crossing at all.

Whether that is worth doing depends on how big it would have to be, so this
adds a temporary shape survey. Measured over a prologue load, 2.7M calls:

    exact (depth 0)  84.2%
    one base         0.4%
    deeper           0.0%     (max depth reached all run: 3)
    not found       15.4%

84% of calls need NO hierarchy walk - the object's dynamic type already IS
the target. A guest fast path of roughly six instructions (load vtable,
compare vtable[-1] to the target, return ptr + vtable[-2]) with a fallback
to this shim would eliminate 84% of dynamic_cast crossings, which is ~41% of
ALL shim crossings given dynamic_cast is 49% of them.

Also confirms there is no implementation to reuse inside libapp.so: not only
__dynamic_cast but the type_info vtables themselves (__class_type_info,
__si_class_type_info, __vmi_class_type_info, __pointer_type_info) are all
UND - this engine supplies them from rtti_shims.cpp.

Survey left in the tree behind kSurveyDynamicCast, default off.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-19 20:28:01 +03:00

605 lines
29 KiB
C++

// LD_PRELOAD interposition for libc file I/O (including whatever the game
// does to read its .obb data) and GLESv2 draw calls, on the REAL,
// unmodified armeabi-v7a libapp.so running natively on the Galaxy A9 (see
// ARM64_TRANSLATION_LAYER.md's "Native ARM32 tracing harness" plan). This
// is the ground-truth counterpart to this session's own gles_shim.cpp
// counters and libc_shims.cpp - deployed via Android's per-app
// `wrap.<packageName>` debuggable-app mechanism (no root needed), so every
// dynamically-resolved libc/GLESv2 symbol the real game calls resolves to
// these wrappers first (standard dlsym(RTLD_NEXT, ...) interposition), each
// logging then calling straight through to the real implementation -
// observation only, never changes behavior.
#include <dlfcn.h>
#include <fcntl.h>
#include <atomic>
#include <cstdarg>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <dirent.h>
#include <link.h>
#include <pthread.h>
#include <GLES2/gl2.h>
#include <EGL/egl.h>
#include "trace_log.h"
using trace_agent::TraceLog;
#define LOGI TraceLog
namespace {
template <typename Fn>
Fn RealSym(const char* name) {
void* p = dlsym(RTLD_NEXT, name);
return reinterpret_cast<Fn>(p);
}
// Declared here (used by open() below) rather than down near pthread_once's
// own definition - see pthread_once's comment for why this flag exists.
// Plain namespace-scope atomic (constant-initialized), NOT a function-local
// static - see pthread_once's own comment for why lazy function-local
// statics are unsafe to touch from inside this specific interposer.
std::atomic<bool> g_pastBootstrap{false};
std::atomic<bool>& PastBootstrap() { return g_pastBootstrap; }
// Ground-truth counterpart (2026-09-16, ARM64_TRANSLATION_LAYER.md - the
// dword_AE00D8 reentrancy investigation) to the emulated engine's own
// SnapshotAE00D8Window (guest_engine.h's G2H-based version). Since this
// agent runs INSIDE the real process via LD_PRELOAD, it shares the exact
// same address space as the real armeabi-v7a libapp.so - no guest/host
// translation needed, just the real ASLR load bias for this one shared
// object, resolved once via dl_iterate_phdr and cached.
uintptr_t LibappBase() {
// A POD `static` with a literal 0 initializer gets plain constant
// (zero) initialization, not a dynamic/guarded one - safe from the
// lazy-function-local-static hazard pthread_once's own comment
// documents (no emutls/pthread_once guard is generated for this kind
// of trivial static at all). Deliberately NOT cached on failure though:
// the first few pthread_once() calls observed live fire before
// libapp.so itself has even been dlopen'd (its own early static-init
// routines, other libraries' locale setup, etc.), so caching a "not
// found" result after the very first attempt would permanently blind
// this to libapp.so once it DOES load moments later - confirmed live
// (every pthread_once call this session logged "base unresolved" even
// well after real gameplay/glClear activity had started). Only cache a
// SUCCESSFUL resolution.
static uintptr_t base = 0;
if (base) return base;
dl_iterate_phdr(
[](struct dl_phdr_info* info, size_t, void* data) -> int {
if (info->dlpi_name && strstr(info->dlpi_name, "libapp.so")) {
*static_cast<uintptr_t*>(data) = info->dlpi_addr;
return 1; // stop iterating
}
return 0;
},
&base);
return base;
}
// Same window as the emulated engine's probe: [0xae00d8, 0xae0108) -
// dword_AE00D8/dword_AE00DC/dword_AE00E0/dword_AE00FC, the small cluster of
// globals sub_56962C's crash-site arithmetic reads directly by fixed
// offset. libapp.so's own file vaddrs equal these offsets 1:1 (see
// guest_engine.cpp's LoadImage - no rebasing in the emulated engine either),
// so `LibappBase() + 0xae00d8` is the real, live runtime address.
constexpr uintptr_t kAE00D8Offset = 0xae00d8;
constexpr size_t kAE00D8WindowLen = 0x30;
std::string SnapshotAE00D8Window() {
uintptr_t base = LibappBase();
if (!base) return std::string();
return std::string(reinterpret_cast<char*>(base + kAE00D8Offset), kAE00D8WindowLen);
}
void LogAE00D8WindowDiff(const std::string& before, const std::string& after) {
if (before.empty() || after.empty()) {
LOGI("pthread_once: dword_AE00D8 window snapshot unavailable (libapp.so base not resolved)");
return;
}
std::string diffs;
for (size_t i = 0; i + 4 <= kAE00D8WindowLen; i += 4) {
uint32_t b = 0, a = 0;
memcpy(&b, before.data() + i, 4);
memcpy(&a, after.data() + i, 4);
if (b != a) {
char buf[64];
snprintf(buf, sizeof(buf), " [AE00D8+0x%zx]:0x%x->0x%x", i, b, a);
diffs += buf;
}
}
LOGI("pthread_once: dword_AE00D8 window [0x%lx,0x%lx) diffs:%s",
(unsigned long)kAE00D8Offset, (unsigned long)(kAE00D8Offset + kAE00D8WindowLen),
diffs.empty() ? " (none)" : diffs.c_str());
}
// Confirmed live on-device: logging EVERY single libc file call (including
// the many thousands ART's own dex/oat/vdex loading does during normal app
// startup, well before any game code runs) adds enough per-call overhead
// to intermittently blow past ActivityManager's ~10s process-attach
// timeout, getting the whole process killed before it ever reaches game
// code - not a deadlock, just volume. Filtering to paths actually relevant
// to this investigation (game data, not framework/system housekeeping)
// keeps the signal without the ART-startup noise. A null path (rare) is
// always logged, since that's unusual enough to be worth seeing regardless.
bool IsInteresting(const char* path) {
if (!path) return true;
static const char* kKeywords[] = {
"nfs13", "libapp", ".obb", ".sb", "Nimble", "fmod", "var/", "var1/",
};
for (const char* kw : kKeywords) {
if (strstr(path, kw)) return true;
}
return false;
}
// 2026-09-18 (ARM64_TRANSLATION_LAYER.md task #39 - missing text overlay,
// A9 ground-truth comparison): logcat alone lost the exact frames this
// investigation needed - the game's own per-frame draw-call volume wraps
// logcat's small ring buffer in well under a second, so by the time `adb
// logcat -d` was dumped, the texture-upload/draw lines from the moment
// that actually mattered were already gone (confirmed live: 95,000+ lines
// captured, zero glTexImage2D lines survived). trace_log.h already writes
// every line to an append-only file too (flushed per-line, exactly for
// this reason) - the missing piece was simply that nothing ever called
// InitFileLog(). Fixed here: resolve OUR OWN library's on-disk path via
// dl_iterate_phdr (same technique LibappBase() uses for the real
// libapp.so) and write the trace alongside it, in the app's own private
// files dir (writable - that's literally where this .so was pushed to
// run at all). Deliberately reuses the SAME "wait for the first
// proven-safe hook" bootstrap gate as LibappBase()/pthread_once's own
// diagnostics (see their extensive comments above) - dl_iterate_phdr this
// early is the same documented re-entrant-linker-lock hazard regardless
// of which library it's trying to locate.
std::string SelfLibraryDir() {
std::string dir;
dl_iterate_phdr(
[](struct dl_phdr_info* info, size_t, void* data) -> int {
if (info->dlpi_name && strstr(info->dlpi_name, "libtrace_agent.so")) {
std::string path(info->dlpi_name);
size_t slash = path.find_last_of('/');
*static_cast<std::string*>(data) = slash != std::string::npos
? path.substr(0, slash) : std::string(".");
return 1; // stop iterating
}
return 0;
},
&dir);
return dir;
}
std::atomic<bool> g_fileLogInitAttempted{false};
void InitFileLogOnceSafe() {
bool expected = false;
if (!g_fileLogInitAttempted.compare_exchange_strong(expected, true)) return;
std::string dir = SelfLibraryDir();
if (dir.empty()) {
__android_log_print(ANDROID_LOG_INFO, "TRACE_AGENT",
"InitFileLogOnceSafe: could not resolve own library path, "
"file logging stays off this run (logcat-only)");
return;
}
std::string path = dir + "/trace_output.log";
trace_agent::InitFileLog(path.c_str());
}
} // namespace
extern "C" {
// ---- libc file I/O ----
int open(const char* path, int flags, ...) {
static auto real = RealSym<int (*)(const char*, int, ...)>("open");
mode_t mode = 0;
if (flags & O_CREAT) {
va_list ap;
va_start(ap, flags);
mode = (mode_t)va_arg(ap, int);
va_end(ap);
}
int fd = real(path, flags, mode);
// First proven-safe hook to fire each run (confirmed across prior
// sessions) - marks bootstrap as over for pthread_once's own guard, see
// its comment above for why that matters. Also the earliest safe point
// to turn on file-based logging (see InitFileLogOnceSafe's own comment)
// - logcat's ring buffer wraps almost immediately once real gameplay
// starts, so the file is the only reliable record for anything beyond
// the first fraction of a second.
PastBootstrap().store(true, std::memory_order_relaxed);
InitFileLogOnceSafe();
if (IsInteresting(path)) {
LOGI("open(\"%s\", flags=0x%x) -> fd=%d%s", path ? path : "(null)", flags, fd,
fd < 0 ? " [FAILED]" : "");
}
return fd;
}
int openat(int dirfd, const char* path, int flags, ...) {
static auto real = RealSym<int (*)(int, const char*, int, ...)>("openat");
mode_t mode = 0;
if (flags & O_CREAT) {
va_list ap;
va_start(ap, flags);
mode = (mode_t)va_arg(ap, int);
va_end(ap);
}
int fd = real(dirfd, path, flags, mode);
if (IsInteresting(path)) {
LOGI("openat(%d, \"%s\", flags=0x%x) -> fd=%d%s", dirfd, path ? path : "(null)", flags,
fd, fd < 0 ? " [FAILED]" : "");
}
return fd;
}
FILE* fopen(const char* path, const char* mode) {
static auto real = RealSym<FILE* (*)(const char*, const char*)>("fopen");
FILE* f = real(path, mode);
if (IsInteresting(path)) {
LOGI("fopen(\"%s\", \"%s\") -> %p%s", path ? path : "(null)", mode ? mode : "(null)",
(void*)f, f == nullptr ? " [FAILED]" : "");
}
return f;
}
// Deliberately NOT interposing read() - by far the single highest-frequency
// libc call during ART's own startup (every byte-buffer read of every dex/
// oat/vdex file goes through it), and it doesn't carry a path - open/openat/
// fopen already show WHICH file was touched, which is what this
// investigation actually needs; logging read() too would reintroduce the
// exact volume problem IsInteresting() above was added to avoid, for very
// little extra information.
int stat(const char* path, struct stat* buf) {
static auto real = RealSym<int (*)(const char*, struct stat*)>("stat");
int r = real(path, buf);
if (IsInteresting(path)) {
LOGI("stat(\"%s\") -> %d%s", path ? path : "(null)", r, r != 0 ? " [FAILED]" : "");
}
return r;
}
int access(const char* path, int mode) {
static auto real = RealSym<int (*)(const char*, int)>("access");
int r = real(path, mode);
if (IsInteresting(path)) {
LOGI("access(\"%s\", 0x%x) -> %d%s", path ? path : "(null)", mode, r,
r != 0 ? " [FAILED]" : "");
}
return r;
}
// opendir - added specifically to check whether the real game also polls
// the (on the emulated engine, confirmed empty) external-files directory
// the same way the emulated engine's own guest thread was found looping
// on (ARM64_TRANSLATION_LAYER.md's file-I/O-path investigation) - "nfs13"
// is already a substring of the package's own external-files path
// (.../Android/data/com.ea.games.nfs13_arm/files), so IsInteresting()
// already covers it once this call is actually interposed.
DIR* opendir(const char* path) {
static auto real = RealSym<DIR* (*)(const char*)>("opendir");
DIR* d = real(path);
if (IsInteresting(path)) {
LOGI("opendir(\"%s\") -> %p%s", path ? path : "(null)", (void*)d,
d == nullptr ? " [FAILED]" : "");
}
return d;
}
// Added 2026-09-16 (ARM64_TRANSLATION_LAYER.md - dword_AE00D8 reentrancy
// investigation): ground-truth counterpart to Shim_pthread_once's own
// register/memory-diff probes in the emulated engine, which found that
// EVERY reentrant CallGuestFunction call it could find (including into the
// real sub_87B968/sub_88CCD0 init routines this exact libc call reaches on
// the emulated side) leaves both the caller's registers and this same
// dword_AE00D8 memory window completely unchanged - a clean result that
// doesn't explain the crash. This answers a different but related
// question: does REAL, unmodified pthread_once() on REAL hardware ever
// touch this memory window at all, for comparison against the emulated
// engine's own (also clean) result.
//
// CONFIRMED LIVE this session: an earlier version of this wrapper that
// unconditionally called LibappBase() (dl_iterate_phdr) on every
// pthread_once() call crashed the WHOLE process with SIGSEGV inside
// libtrace_agent.so itself, before Application.attachBaseContext even ran
// (tombstone showed the fault inside our own .so, called from deep within
// app_process32's own bootstrap). pthread_once() is called extremely early
// by the dynamic linker/bionic itself (library static-initializer guards,
// TLS setup) - calling dl_iterate_phdr from inside one of those calls means
// re-entering the dynamic linker's own internal module-list lock while it
// may already be held on the SAME thread by an ancestor frame (the linker
// loading a library and running ITS constructors) - the exact "chicken and
// egg" bootstrap hazard this file's mmap()-avoidance comment (below)
// already documents for a different symbol. Fix: skip ALL of this
// diagnostic's work (dl_iterate_phdr, string snapshots) until the FIRST
// already-proven-safe hook (open(), which prior sessions confirmed fires
// without incident) has fired at least once - by then bootstrap is long
// over and dl_iterate_phdr is safe. Before that point, pthread_once() is
// passed straight through with no diagnostics at all - purely linker/libc
// bookkeeping this investigation was never after in the first place.
//
// SECOND, DEEPER hazard found once wrap.sh (real process-wide LD_PRELOAD,
// active from exec() itself) replaced the earlier System.load()-based
// partial activation: a real tombstone (llvm-addr2line confirmed) showed
// THIS function's own `static auto real = RealSym<...>(...)` line crashing
// via __emutls_get_address - this toolchain/ABI implements function-local
// static thread-safe initialization using emulated TLS, which internally
// calls pthread_once() ITSELF to lazily set up the TLS key. Since we're
// LD_PRELOAD'd, that internal call gets intercepted by THIS SAME wrapper -
// which then tries to initialize ITS OWN `real` static again -> infinite
// recursion -> stack overflow -> SIGSEGV, on literally the first call, no
// PastBootstrap() check even reached. Fixed by resolving `real` eagerly, at
// LIBRARY LOAD time via a real ELF constructor (single-threaded by
// construction, no lazy-static guard machinery involved at all) instead of
// a lazy function-local static - sidesteps the whole class of hazard.
using PthreadOnceFn = int (*)(pthread_once_t*, void (*)(void));
PthreadOnceFn g_realPthreadOnce = nullptr;
__attribute__((constructor)) void ResolveRealPthreadOnceEarly() {
g_realPthreadOnce = RealSym<PthreadOnceFn>("pthread_once");
}
// THIRD hazard, one level deeper still: even with `real` resolved eagerly,
// the diagnostic body's own LOGI(...) call touches TraceLog()'s
// thread_local InTraceLog() reentrancy guard (trace_log.h) - a SEPARATE
// thread_local variable that needs its OWN one-time emutls/pthread_once
// setup, the first time ANY thread ever reaches it. That nested setup
// calls back into THIS SAME pthread_once wrapper, which (bootstrap flag
// already true by then) takes the logging branch again, touching
// InTraceLog() again, recursing without end - a live tombstone confirmed
// this exact chain (pthread_once -> TraceLog -> __emutls_get_address).
// Fixed with a reentrancy guard that cannot itself need emutls: a plain
// namespace-scope std::atomic<int>, constant-initialized (no runtime lazy-
// static guard at all, unlike a thread_local or a guarded function-local
// static) - if pthread_once is already executing on ANY thread when
// re-entered, skip every bit of the diagnostic (dl_iterate_phdr AND
// logging alike) and just pass straight through to the real
// implementation. Slightly imprecise across threads (a global, not
// per-thread, counter) but that's an acceptable tradeoff for a
// diagnostic-only tool, and avoids needing anything TLS-shaped at all.
std::atomic<int> g_pthreadOnceDepth{0};
int pthread_once(pthread_once_t* onceCtl, void (*initRoutine)(void)) {
PthreadOnceFn real = g_realPthreadOnce;
if (!real) {
// Constructor hasn't run yet (shouldn't happen once loaded, but a
// defensive fallback beats calling through a null pointer) - resolve
// directly via RTLD_NEXT this one time. Still safer than before:
// this is a plain function call, not a lazy-static-guarded one.
real = RealSym<PthreadOnceFn>("pthread_once");
}
int depthAtEntry = g_pthreadOnceDepth.fetch_add(1, std::memory_order_relaxed);
struct DepthGuard {
~DepthGuard() { g_pthreadOnceDepth.fetch_sub(1, std::memory_order_relaxed); }
} depthGuard;
if (depthAtEntry > 0 || !PastBootstrap().load(std::memory_order_relaxed)) {
return real(onceCtl, initRoutine);
}
std::string before = SnapshotAE00D8Window();
uintptr_t base = LibappBase();
if (base && (uintptr_t)initRoutine >= base) {
LOGI("pthread_once: initRoutine=%p (libapp.so+0x%lx)", (void*)initRoutine,
(unsigned long)((uintptr_t)initRoutine - base));
} else {
LOGI("pthread_once: initRoutine=%p (not in libapp.so, or base unresolved)",
(void*)initRoutine);
}
int r = real(onceCtl, initRoutine);
std::string after = SnapshotAE00D8Window();
LogAE00D8WindowDiff(before, after);
return r;
}
// Deliberately NOT interposing mmap() - confirmed live on-device that it
// hangs the whole app process indefinitely (ActivityManager kills it after
// a start-timeout, zero TRACE_AGENT log output the entire time - the hang
// happens before our first successful call). The dynamic linker uses
// mmap() itself to map every shared library into memory, including ours,
// mid-load; interposing it risks exactly this kind of bootstrap
// chicken-and-egg deadlock (our wrapper's own dlsym(RTLD_NEXT, "mmap")
// requires the linker to already be in a stable state that a call arriving
// mid-bootstrap may not have reached yet). Not needed for this
// investigation's goal anyway - open/fopen/stat/access already show which
// files get touched.
// ---- GLESv2 (same 4 entry points this session's own gles_shim.cpp counts,
// for direct comparison against the emulated engine's counters) ----
// 2026-09-19: frame-structure ground truth (see glBindFramebuffer below).
// The emulated engine has no eglSwapBuffers of its own to key off -
// nativeOnResume never returns, so Android's automatic post-onDrawFrame swap
// never fires (gles_shim.cpp's Shim_glClear) - and it synthesizes one per
// default-framebuffer colour clear instead. That is a GUESS about how the
// game structures a frame. Spelling out the mask bits and the bound
// framebuffer here, against the already-traced eglSwapBuffers below, gives
// the real ratio and ordering the emulated engine should be reproducing.
GLuint g_tracedBoundFramebuffer = 0;
void glClear(GLbitfield mask) {
static auto real = RealSym<void (*)(GLbitfield)>("glClear");
LOGI("glClear(mask=0x%x%s%s%s) fb=%u", mask,
(mask & GL_COLOR_BUFFER_BIT) ? " COLOR" : "",
(mask & GL_DEPTH_BUFFER_BIT) ? " DEPTH" : "",
(mask & GL_STENCIL_BUFFER_BIT) ? " STENCIL" : "",
g_tracedBoundFramebuffer);
real(mask);
}
void glDrawArrays(GLenum mode, GLint first, GLsizei count) {
static auto real = RealSym<void (*)(GLenum, GLint, GLsizei)>("glDrawArrays");
LOGI("glDrawArrays(mode=0x%x, first=%d, count=%d)", mode, first, count);
real(mode, first, count);
}
// 2026-09-18 (ARM64_TRANSLATION_LAYER.md task #39 - missing text overlay):
// ground-truth counterpart to gles_shim.cpp's own Shim_glTexImage2D/
// Shim_glDrawElements instrumentation, which found a narrow 738x302
// texture (real anti-aliased glyph data, matching the missing safety-
// disclaimer/copyright text) uploads correctly on the emulated engine but
// is NEVER referenced by any subsequent draw call. Question this answers:
// on REAL hardware, does the equivalent texture get uploaded with similar
// dimensions, and - critically - does a real draw call actually reference
// it (proving the real game DOES intend to draw it, ruling out "the text
// is legitimately decorative-only and never drawn as geometry").
GLuint g_currentBoundTexture2D = 0;
void glBindTexture(GLenum target, GLuint texture) {
static auto real = RealSym<void (*)(GLenum, GLuint)>("glBindTexture");
if (target == GL_TEXTURE_2D) g_currentBoundTexture2D = texture;
real(target, texture);
}
void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width,
GLsizei height, GLint border, GLenum format, GLenum type,
const void* pixels) {
static auto real = RealSym<void (*)(GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum,
GLenum, const void*)>("glTexImage2D");
LOGI("glTexImage2D tex=%u target=0x%x level=%d internalformat=0x%x %dx%d format=0x%x type=0x%x",
g_currentBoundTexture2D, target, level, internalformat, width, height, format, type);
real(target, level, internalformat, width, height, border, format, type, pixels);
}
void glDrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
static auto real =
RealSym<void (*)(GLenum, GLsizei, GLenum, const void*)>("glDrawElements");
LOGI("glDrawElements(mode=0x%x, count=%d, type=0x%x) tex0=%u", mode, count, type,
g_currentBoundTexture2D);
real(mode, count, type, indices);
}
void glUseProgram(GLuint program) {
static auto real = RealSym<void (*)(GLuint)>("glUseProgram");
LOGI("glUseProgram(program=%u)", program);
real(program);
}
// 2026-09-19: frame-structure ground truth. The emulated engine has no
// eglSwapBuffers of its own to key off (nativeOnResume never returns, so
// Android's automatic post-onDrawFrame swap never fires - see gles_shim.cpp's
// Shim_glClear), and synthesizes one per default-framebuffer colour clear
// instead. That is a GUESS about how the game structures a frame. These three
// interposers measure the real thing on hardware: which framebuffer is bound
// at each clear, what the clear mask actually is, and exactly where the real
// swaps fall relative to those clears. The ratio and ordering are what the
// emulated engine needs to reproduce - the swap CALL itself can't be copied,
// since on real hardware it is Android's framework making it, not the game.
void glBindFramebuffer(GLenum target, GLuint framebuffer) {
static auto real = RealSym<void (*)(GLenum, GLuint)>("glBindFramebuffer");
g_tracedBoundFramebuffer = framebuffer;
real(target, framebuffer);
}
// 2026-09-18 (ARM64_TRANSLATION_LAYER.md - the "frozen splash, real
// rendering never reaches the screen" chase): the emulated engine's own
// logs show 0 calls to eglSwapBuffers/eglGetProcAddress across every
// capture, and IDA static analysis of the exact same real armeabi-v7a
// libapp.so found the IsisApp render-pipeline's own vtable+168/+116 slots
// to be empty no-ops. Ground-truth check: does the REAL app, running
// natively on the Galaxy A9, actually call eglSwapBuffers at all, and if
// so how often relative to the GLES draw calls above?
EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surface) {
static auto real = RealSym<EGLBoolean (*)(EGLDisplay, EGLSurface)>("eglSwapBuffers");
EGLBoolean result = real(dpy, surface);
LOGI("eglSwapBuffers(dpy=%p, surface=%p) -> %d", dpy, surface, (int)result);
return result;
}
__eglMustCastToProperFunctionPointerType eglGetProcAddress(const char* procname) {
static auto real =
RealSym<__eglMustCastToProperFunctionPointerType (*)(const char*)>("eglGetProcAddress");
__eglMustCastToProperFunctionPointerType result = real(procname);
LOGI("eglGetProcAddress(\"%s\") -> %p", procname ? procname : "(null)", (void*)result);
return result;
}
// Added 2026-09-16 - ground-truth counterpart to this session's own
// gles_shim.cpp investigation (ARM64_TRANSLATION_LAYER.md): our emulated
// engine's shader-variant cache (sub_43FDE0/dword_ADBFB8) consistently
// fails, producing an EMPTY shader source that the real host GLSL compiler
// rejects with "Missing main() function". Question this trace answers: on
// REAL native ARM32 execution (no CPU translation at all), does the same
// shader-variant lookup ever produce an empty source too (and get silently
// recovered from), or does it never happen at all? Logs the real source
// text/length and real GL_COMPILE_STATUS/GL_LINK_STATUS, same as
// Shim_glShaderSource/Shim_glCompileShader/Shim_glLinkProgram do in our own
// gles_shim.cpp, for direct comparison.
void glShaderSource(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length) {
static auto real =
RealSym<void (*)(GLuint, GLsizei, const GLchar* const*, const GLint*)>("glShaderSource");
for (GLsizei i = 0; i < count; i++) {
GLint len = length ? length[i] : (string[i] ? (GLint)strlen(string[i]) : 0);
LOGI("glShaderSource(%u) fragment[%d/%d] len=%d lengths_provided=%d: \"%.300s\"",
shader, i, count, len, length != nullptr, string[i] ? string[i] : "(null)");
}
real(shader, count, string, length);
}
void glCompileShader(GLuint shader) {
static auto real = RealSym<void (*)(GLuint)>("glCompileShader");
real(shader);
static auto realGetShaderiv = RealSym<void (*)(GLuint, GLenum, GLint*)>("glGetShaderiv");
static auto realGetShaderInfoLog =
RealSym<void (*)(GLuint, GLsizei, GLsizei*, GLchar*)>("glGetShaderInfoLog");
GLint status = GL_FALSE;
realGetShaderiv(shader, GL_COMPILE_STATUS, &status);
LOGI("glCompileShader(%u) status=%s", shader, status == GL_TRUE ? "OK" : "FAILED");
if (status != GL_TRUE) {
char log[512] = {0};
realGetShaderInfoLog(shader, sizeof(log), nullptr, log);
LOGI("glCompileShader(%u) FAILED: %s", shader, log);
}
}
void glLinkProgram(GLuint program) {
static auto real = RealSym<void (*)(GLuint)>("glLinkProgram");
real(program);
static auto realGetProgramiv = RealSym<void (*)(GLuint, GLenum, GLint*)>("glGetProgramiv");
static auto realGetProgramInfoLog =
RealSym<void (*)(GLuint, GLsizei, GLsizei*, GLchar*)>("glGetProgramInfoLog");
GLint status = GL_FALSE;
realGetProgramiv(program, GL_LINK_STATUS, &status);
LOGI("glLinkProgram(%u) status=%s", program, status == GL_TRUE ? "OK" : "FAILED");
if (status != GL_TRUE) {
char log[512] = {0};
realGetProgramInfoLog(program, sizeof(log), nullptr, log);
LOGI("glLinkProgram(%u) FAILED: %s", program, log);
}
}
// ---- __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<void* (*)(const void*, const void*, const void*, ptrdiff_t)>(
"__dynamic_cast");
static std::atomic<uint64_t> calls{0};
static std::atomic<uint64_t> 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"