Files
megboyzzandClaude d5e6037fc7 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>
2026-09-19 02:12:03 +03:00

502 lines
22 KiB
C++

// JNI tracing for the native ARM32 investigation (see
// ARM64_TRANSLATION_LAYER.md's "Native ARM32 tracing harness" plan).
//
// JNI calls go through the JNIEnv->functions function-pointer table, not
// the dynamic symbol table - LD_PRELOAD interposition (libc_gles_trace.cpp)
// can't see them. Real Android ART shares ONE JNINativeInterface table
// across every thread's JNIEnv (confirmed by design, not assumed - this is
// standard JVM/ART behavior), so this patches the table'S CONTENTS in
// place (not swapping which struct `functions` points to) - covers every
// thread automatically, including whichever one actually drives the game's
// real onDrawFrame/RunLoop-tick GL thread, not just whichever thread
// happens to call the installer below.
//
// 2026-09-06: widened from the original curated dozen to a much broader
// call-flow set (method/field access, object lifecycle, exceptions, local
// frames) - the point of a "full" trace is reconstructing execution order
// on real hardware, and the original subset was too narrow for that. Still
// deliberately skips a few genuinely high-frequency, low-information calls
// (DeleteLocalRef, array element accessors) - see their own comments below.
#include <jni.h>
#include <cstring>
#include <sys/mman.h>
#include <unistd.h>
#include "trace_log.h"
using trace_agent::LookupName;
using trace_agent::RememberName;
using trace_agent::TraceLog;
namespace {
JNINativeInterface g_orig{}; // real function pointers, saved before patching, so wrappers can call through
jclass Wrap_FindClass(JNIEnv* env, const char* name) {
jclass r = g_orig.FindClass(env, name);
TraceLog("JNI FindClass(\"%s\") -> %p%s", name ? name : "(null)", (void*)r, r == nullptr ? " [FAILED]" : "");
return r;
}
// Diagnostic for the ARM64_TRANSLATION_LAYER.md "external-storage overlay
// at VFS root" investigation - is GameActivityMain.useAssetsFileSystem()
// even called on real native ARM32, and what does it actually return?
// The Java-side implementation is this project's own (native32 shares
// GameActivityMain.kt with the translated/emulated flavor), so a
// same-vs-different result here isn't about different source code - it's
// about whether the same code produces a different real answer natively.
jmethodID g_useAssetsFileSystemId = nullptr;
jmethodID Wrap_GetMethodID(JNIEnv* env, jclass clazz, const char* name, const char* sig) {
jmethodID r = g_orig.GetMethodID(env, clazz, name, sig);
TraceLog("JNI GetMethodID(\"%s\", \"%s\") -> %p%s", name ? name : "(null)", sig ? sig : "(null)",
(void*)r, r == nullptr ? " [FAILED]" : "");
if (r) RememberName(r, name, sig);
if (name && r && strcmp(name, "useAssetsFileSystem") == 0) {
g_useAssetsFileSystemId = r;
TraceLog("JNI GetMethodID: cached useAssetsFileSystem jmethodID=%p for CallBooleanMethod tracing", (void*)r);
}
return r;
}
jboolean Wrap_CallBooleanMethod(JNIEnv* env, jobject obj, jmethodID id, ...) {
va_list ap;
va_start(ap, id);
jboolean r = g_orig.CallBooleanMethodV(env, obj, id, ap);
va_end(ap);
TraceLog("JNI CallBooleanMethod: %s(obj=%p) -> %s", LookupName(id).c_str(), (void*)obj, r ? "true" : "false");
return r;
}
jboolean Wrap_CallBooleanMethodV(JNIEnv* env, jobject obj, jmethodID id, va_list args) {
// va_list can only be consumed once - copy before forwarding so the
// real call still gets a fresh, valid list.
va_list copy;
va_copy(copy, args);
jboolean r = g_orig.CallBooleanMethodV(env, obj, id, copy);
va_end(copy);
TraceLog("JNI CallBooleanMethodV: %s(obj=%p) -> %s", LookupName(id).c_str(), (void*)obj, r ? "true" : "false");
return r;
}
jboolean Wrap_CallBooleanMethodA(JNIEnv* env, jobject obj, jmethodID id, const jvalue* args) {
jboolean r = g_orig.CallBooleanMethodA(env, obj, id, args);
TraceLog("JNI CallBooleanMethodA: %s(obj=%p) -> %s", LookupName(id).c_str(), (void*)obj, r ? "true" : "false");
return r;
}
jmethodID Wrap_GetStaticMethodID(JNIEnv* env, jclass clazz, const char* name, const char* sig) {
jmethodID r = g_orig.GetStaticMethodID(env, clazz, name, sig);
TraceLog("JNI GetStaticMethodID(\"%s\", \"%s\") -> %p%s", name ? name : "(null)", sig ? sig : "(null)",
(void*)r, r == nullptr ? " [FAILED]" : "");
if (r) RememberName(r, name, sig);
return r;
}
jfieldID Wrap_GetFieldID(JNIEnv* env, jclass clazz, const char* name, const char* sig) {
jfieldID r = g_orig.GetFieldID(env, clazz, name, sig);
TraceLog("JNI GetFieldID(\"%s\", \"%s\") -> %p%s", name ? name : "(null)", sig ? sig : "(null)",
(void*)r, r == nullptr ? " [FAILED]" : "");
if (r) RememberName(r, name, sig);
return r;
}
jfieldID Wrap_GetStaticFieldID(JNIEnv* env, jclass clazz, const char* name, const char* sig) {
jfieldID r = g_orig.GetStaticFieldID(env, clazz, name, sig);
TraceLog("JNI GetStaticFieldID(\"%s\", \"%s\") -> %p%s", name ? name : "(null)", sig ? sig : "(null)",
(void*)r, r == nullptr ? " [FAILED]" : "");
if (r) RememberName(r, name, sig);
return r;
}
jint Wrap_RegisterNatives(JNIEnv* env, jclass clazz, const JNINativeMethod* methods, jint n) {
jint r = g_orig.RegisterNatives(env, clazz, methods, n);
TraceLog("JNI RegisterNatives(count=%d) -> %d%s", n, r, r != 0 ? " [FAILED]" : "");
for (jint i = 0; i < n; i++) {
TraceLog("JNI RegisterNatives: %s %s", methods[i].name, methods[i].signature);
}
return r;
}
jstring Wrap_NewStringUTF(JNIEnv* env, const char* bytes) {
jstring r = g_orig.NewStringUTF(env, bytes);
TraceLog("JNI NewStringUTF(\"%s\") -> %p", bytes ? bytes : "(null)", (void*)r);
return r;
}
const char* Wrap_GetStringUTFChars(JNIEnv* env, jstring str, jboolean* isCopy) {
const char* r = g_orig.GetStringUTFChars(env, str, isCopy);
TraceLog("JNI GetStringUTFChars() -> \"%s\"", r ? r : "(null)");
return r;
}
jboolean Wrap_ExceptionCheck(JNIEnv* env) {
jboolean r = g_orig.ExceptionCheck(env);
if (r) TraceLog("JNI ExceptionCheck() -> TRUE");
return r;
}
void Wrap_ExceptionDescribe(JNIEnv* env) {
TraceLog("JNI ExceptionDescribe() called");
g_orig.ExceptionDescribe(env);
}
// ---- object lifecycle / class checks ----
jclass Wrap_GetObjectClass(JNIEnv* env, jobject obj) {
jclass r = g_orig.GetObjectClass(env, obj);
TraceLog("JNI GetObjectClass(obj=%p) -> %p", (void*)obj, (void*)r);
return r;
}
jboolean Wrap_IsInstanceOf(JNIEnv* env, jobject obj, jclass clazz) {
jboolean r = g_orig.IsInstanceOf(env, obj, clazz);
TraceLog("JNI IsInstanceOf(obj=%p, class=%p) -> %s", (void*)obj, (void*)clazz, r ? "true" : "false");
return r;
}
jboolean Wrap_IsSameObject(JNIEnv* env, jobject a, jobject b) {
jboolean r = g_orig.IsSameObject(env, a, b);
TraceLog("JNI IsSameObject(%p, %p) -> %s", (void*)a, (void*)b, r ? "true" : "false");
return r;
}
jobject Wrap_NewGlobalRef(JNIEnv* env, jobject obj) {
jobject r = g_orig.NewGlobalRef(env, obj);
TraceLog("JNI NewGlobalRef(%p) -> %p", (void*)obj, (void*)r);
return r;
}
void Wrap_DeleteGlobalRef(JNIEnv* env, jobject obj) {
TraceLog("JNI DeleteGlobalRef(%p)", (void*)obj);
g_orig.DeleteGlobalRef(env, obj);
}
// DeleteLocalRef is deliberately NOT wrapped - by far the highest-frequency
// JNI call in any real run (every local ref a native function creates gets
// cleaned up through it), and it carries no name/signature information of
// its own. Logging it would reintroduce the exact volume problem
// libc_gles_trace.cpp's IsInteresting() was added to avoid, for a line that
// never says which method/field it's tied to.
jint Wrap_PushLocalFrame(JNIEnv* env, jint cap) {
TraceLog("JNI PushLocalFrame(%d)", (int)cap);
return g_orig.PushLocalFrame(env, cap);
}
jobject Wrap_PopLocalFrame(JNIEnv* env, jobject result) {
TraceLog("JNI PopLocalFrame()");
return g_orig.PopLocalFrame(env, result);
}
// ---- exceptions ----
jint Wrap_Throw(JNIEnv* env, jthrowable obj) {
TraceLog("JNI Throw(%p)", (void*)obj);
return g_orig.Throw(env, obj);
}
jint Wrap_ThrowNew(JNIEnv* env, jclass clazz, const char* msg) {
TraceLog("JNI ThrowNew(class=%p, \"%s\")", (void*)clazz, msg ? msg : "(null)");
return g_orig.ThrowNew(env, clazz, msg);
}
jthrowable Wrap_ExceptionOccurred(JNIEnv* env) {
jthrowable r = g_orig.ExceptionOccurred(env);
if (r) TraceLog("JNI ExceptionOccurred() -> %p", (void*)r);
return r;
}
void Wrap_ExceptionClear(JNIEnv* env) {
TraceLog("JNI ExceptionClear()");
g_orig.ExceptionClear(env);
}
// ---- object construction / arrays ----
jobject Wrap_NewObject(JNIEnv* env, jclass clazz, jmethodID id, ...) {
va_list ap;
va_start(ap, id);
jobject r = g_orig.NewObjectV(env, clazz, id, ap);
va_end(ap);
TraceLog("JNI NewObject: %s -> %p", LookupName(id).c_str(), (void*)r);
return r;
}
jobject Wrap_NewObjectV(JNIEnv* env, jclass clazz, jmethodID id, va_list args) {
va_list copy;
va_copy(copy, args);
jobject r = g_orig.NewObjectV(env, clazz, id, copy);
va_end(copy);
TraceLog("JNI NewObjectV: %s -> %p", LookupName(id).c_str(), (void*)r);
return r;
}
jsize Wrap_GetArrayLength(JNIEnv* env, jarray arr) {
jsize r = g_orig.GetArrayLength(env, arr);
TraceLog("JNI GetArrayLength(%p) -> %d", (void*)arr, (int)r);
return r;
}
jobjectArray Wrap_NewObjectArray(JNIEnv* env, jsize len, jclass clazz, jobject init) {
jobjectArray r = g_orig.NewObjectArray(env, len, clazz, init);
TraceLog("JNI NewObjectArray(len=%d, class=%p) -> %p", (int)len, (void*)clazz, (void*)r);
return r;
}
// ---- instance Call*Method / Call*MethodV families (mechanical - same
// shape as jni_shim.cpp's own DoCall/DoCallV on the emulated side) ----
#define DEF_CALL_METHOD(RetType, Name) \
RetType Wrap_Call##Name##Method(JNIEnv* env, jobject obj, jmethodID id, ...) { \
va_list ap; \
va_start(ap, id); \
RetType r = g_orig.Call##Name##MethodV(env, obj, id, ap); \
va_end(ap); \
TraceLog("JNI Call" #Name "Method: %s(obj=%p)", LookupName(id).c_str(), (void*)obj); \
return r; \
} \
RetType Wrap_Call##Name##MethodV(JNIEnv* env, jobject obj, jmethodID id, va_list args) { \
va_list copy; \
va_copy(copy, args); \
RetType r = g_orig.Call##Name##MethodV(env, obj, id, copy); \
va_end(copy); \
TraceLog("JNI Call" #Name "MethodV: %s(obj=%p)", LookupName(id).c_str(), (void*)obj); \
return r; \
}
// void has no return value to capture, so it can't share DEF_CALL_METHOD's
// RetType-returning shape.
void Wrap_CallVoidMethod(JNIEnv* env, jobject obj, jmethodID id, ...) {
va_list ap;
va_start(ap, id);
g_orig.CallVoidMethodV(env, obj, id, ap);
va_end(ap);
TraceLog("JNI CallVoidMethod: %s(obj=%p)", LookupName(id).c_str(), (void*)obj);
}
void Wrap_CallVoidMethodV(JNIEnv* env, jobject obj, jmethodID id, va_list args) {
va_list copy;
va_copy(copy, args);
g_orig.CallVoidMethodV(env, obj, id, copy);
va_end(copy);
TraceLog("JNI CallVoidMethodV: %s(obj=%p)", LookupName(id).c_str(), (void*)obj);
}
DEF_CALL_METHOD(jobject, Object)
DEF_CALL_METHOD(jint, Int)
DEF_CALL_METHOD(jlong, Long)
DEF_CALL_METHOD(jfloat, Float)
DEF_CALL_METHOD(jdouble, Double)
#undef DEF_CALL_METHOD
// ---- static Call*Method / Call*MethodV families ----
#define DEF_CALL_STATIC_METHOD(RetType, Name) \
RetType Wrap_CallStatic##Name##Method(JNIEnv* env, jclass clazz, jmethodID id, ...) { \
va_list ap; \
va_start(ap, id); \
RetType r = g_orig.CallStatic##Name##MethodV(env, clazz, id, ap); \
va_end(ap); \
TraceLog("JNI CallStatic" #Name "Method: %s", LookupName(id).c_str()); \
return r; \
} \
RetType Wrap_CallStatic##Name##MethodV(JNIEnv* env, jclass clazz, jmethodID id, va_list args) { \
va_list copy; \
va_copy(copy, args); \
RetType r = g_orig.CallStatic##Name##MethodV(env, clazz, id, copy); \
va_end(copy); \
TraceLog("JNI CallStatic" #Name "MethodV: %s", LookupName(id).c_str()); \
return r; \
}
void Wrap_CallStaticVoidMethod(JNIEnv* env, jclass clazz, jmethodID id, ...) {
va_list ap;
va_start(ap, id);
g_orig.CallStaticVoidMethodV(env, clazz, id, ap);
va_end(ap);
TraceLog("JNI CallStaticVoidMethod: %s", LookupName(id).c_str());
}
void Wrap_CallStaticVoidMethodV(JNIEnv* env, jclass clazz, jmethodID id, va_list args) {
va_list copy;
va_copy(copy, args);
g_orig.CallStaticVoidMethodV(env, clazz, id, copy);
va_end(copy);
TraceLog("JNI CallStaticVoidMethodV: %s", LookupName(id).c_str());
}
DEF_CALL_STATIC_METHOD(jobject, Object)
DEF_CALL_STATIC_METHOD(jboolean, Boolean)
DEF_CALL_STATIC_METHOD(jint, Int)
DEF_CALL_STATIC_METHOD(jlong, Long)
DEF_CALL_STATIC_METHOD(jfloat, Float)
DEF_CALL_STATIC_METHOD(jdouble, Double)
#undef DEF_CALL_STATIC_METHOD
// ---- instance Get*Field / Set*Field families ----
#define DEF_GET_FIELD(RetType, Name) \
RetType Wrap_Get##Name##Field(JNIEnv* env, jobject obj, jfieldID id) { \
RetType r = g_orig.Get##Name##Field(env, obj, id); \
TraceLog("JNI Get" #Name "Field: %s(obj=%p)", LookupName(id).c_str(), (void*)obj); \
return r; \
}
#define DEF_SET_FIELD(ValType, Name) \
void Wrap_Set##Name##Field(JNIEnv* env, jobject obj, jfieldID id, ValType val) { \
TraceLog("JNI Set" #Name "Field: %s(obj=%p)", LookupName(id).c_str(), (void*)obj); \
g_orig.Set##Name##Field(env, obj, id, val); \
}
DEF_GET_FIELD(jobject, Object)
DEF_GET_FIELD(jboolean, Boolean)
DEF_GET_FIELD(jint, Int)
DEF_GET_FIELD(jlong, Long)
DEF_GET_FIELD(jfloat, Float)
DEF_GET_FIELD(jdouble, Double)
DEF_SET_FIELD(jobject, Object)
DEF_SET_FIELD(jboolean, Boolean)
DEF_SET_FIELD(jint, Int)
DEF_SET_FIELD(jlong, Long)
DEF_SET_FIELD(jfloat, Float)
DEF_SET_FIELD(jdouble, Double)
#undef DEF_GET_FIELD
#undef DEF_SET_FIELD
// ---- static Get*Field / Set*Field families ----
#define DEF_GET_STATIC_FIELD(RetType, Name) \
RetType Wrap_GetStatic##Name##Field(JNIEnv* env, jclass clazz, jfieldID id) { \
RetType r = g_orig.GetStatic##Name##Field(env, clazz, id); \
TraceLog("JNI GetStatic" #Name "Field: %s", LookupName(id).c_str()); \
return r; \
}
#define DEF_SET_STATIC_FIELD(ValType, Name) \
void Wrap_SetStatic##Name##Field(JNIEnv* env, jclass clazz, jfieldID id, ValType val) { \
TraceLog("JNI SetStatic" #Name "Field: %s", LookupName(id).c_str()); \
g_orig.SetStatic##Name##Field(env, clazz, id, val); \
}
DEF_GET_STATIC_FIELD(jobject, Object)
DEF_GET_STATIC_FIELD(jboolean, Boolean)
DEF_GET_STATIC_FIELD(jint, Int)
DEF_SET_STATIC_FIELD(jobject, Object)
DEF_SET_STATIC_FIELD(jboolean, Boolean)
DEF_SET_STATIC_FIELD(jint, Int)
#undef DEF_GET_STATIC_FIELD
#undef DEF_SET_STATIC_FIELD
} // namespace
// Kotlin side: nfs.mod.traceagent.TraceAgentBridge.install(outputPath) - a
// tiny new class added purely for this investigation (see the plan's Phase
// 3 file list), called from a new minimal Application.attachBaseContext, as
// early as possible, before any real game library gets a chance to grab its
// own JNIEnv reference. outputPath is the app's own private files dir (see
// TraceApplication.kt) - trace_log.h opens it for the whole process's life.
extern "C" JNIEXPORT void JNICALL
Java_nfs_mod_traceagent_TraceAgentBridge_install(JNIEnv* env, jclass, jstring outputPath) {
if (outputPath) {
const char* path = env->GetStringUTFChars(outputPath, nullptr);
trace_agent::InitFileLog(path);
env->ReleaseStringUTFChars(outputPath, path);
}
auto* table = const_cast<JNINativeInterface*>(env->functions);
g_orig = *table;
long pageSize = sysconf(_SC_PAGESIZE);
uintptr_t base = (uintptr_t)table & ~(uintptr_t)(pageSize - 1);
size_t span = ((uintptr_t)table + sizeof(*table)) - base;
if (mprotect((void*)base, span, PROT_READ | PROT_WRITE) != 0) {
TraceLog("InstallJniTraceHooks: mprotect failed - JNI tracing disabled, libc/GLES tracing (if LD_PRELOAD'd) still active");
return;
}
table->FindClass = Wrap_FindClass;
table->GetMethodID = Wrap_GetMethodID;
table->CallBooleanMethod = Wrap_CallBooleanMethod;
table->CallBooleanMethodV = Wrap_CallBooleanMethodV;
table->CallBooleanMethodA = Wrap_CallBooleanMethodA;
table->GetStaticMethodID = Wrap_GetStaticMethodID;
table->GetFieldID = Wrap_GetFieldID;
table->GetStaticFieldID = Wrap_GetStaticFieldID;
table->RegisterNatives = Wrap_RegisterNatives;
table->NewStringUTF = Wrap_NewStringUTF;
table->GetStringUTFChars = Wrap_GetStringUTFChars;
table->ExceptionCheck = Wrap_ExceptionCheck;
table->ExceptionDescribe = Wrap_ExceptionDescribe;
table->GetObjectClass = Wrap_GetObjectClass;
table->IsInstanceOf = Wrap_IsInstanceOf;
table->IsSameObject = Wrap_IsSameObject;
table->NewGlobalRef = Wrap_NewGlobalRef;
table->DeleteGlobalRef = Wrap_DeleteGlobalRef;
table->PushLocalFrame = Wrap_PushLocalFrame;
table->PopLocalFrame = Wrap_PopLocalFrame;
table->Throw = Wrap_Throw;
table->ThrowNew = Wrap_ThrowNew;
table->ExceptionOccurred = Wrap_ExceptionOccurred;
table->ExceptionClear = Wrap_ExceptionClear;
table->NewObject = Wrap_NewObject;
table->NewObjectV = Wrap_NewObjectV;
table->GetArrayLength = Wrap_GetArrayLength;
table->NewObjectArray = Wrap_NewObjectArray;
table->CallVoidMethod = Wrap_CallVoidMethod;
table->CallVoidMethodV = Wrap_CallVoidMethodV;
table->CallObjectMethod = Wrap_CallObjectMethod;
table->CallObjectMethodV = Wrap_CallObjectMethodV;
table->CallIntMethod = Wrap_CallIntMethod;
table->CallIntMethodV = Wrap_CallIntMethodV;
table->CallLongMethod = Wrap_CallLongMethod;
table->CallLongMethodV = Wrap_CallLongMethodV;
table->CallFloatMethod = Wrap_CallFloatMethod;
table->CallFloatMethodV = Wrap_CallFloatMethodV;
table->CallDoubleMethod = Wrap_CallDoubleMethod;
table->CallDoubleMethodV = Wrap_CallDoubleMethodV;
table->CallStaticVoidMethod = Wrap_CallStaticVoidMethod;
table->CallStaticVoidMethodV = Wrap_CallStaticVoidMethodV;
table->CallStaticObjectMethod = Wrap_CallStaticObjectMethod;
table->CallStaticObjectMethodV = Wrap_CallStaticObjectMethodV;
table->CallStaticBooleanMethod = Wrap_CallStaticBooleanMethod;
table->CallStaticBooleanMethodV = Wrap_CallStaticBooleanMethodV;
table->CallStaticIntMethod = Wrap_CallStaticIntMethod;
table->CallStaticIntMethodV = Wrap_CallStaticIntMethodV;
table->CallStaticLongMethod = Wrap_CallStaticLongMethod;
table->CallStaticLongMethodV = Wrap_CallStaticLongMethodV;
table->CallStaticFloatMethod = Wrap_CallStaticFloatMethod;
table->CallStaticFloatMethodV = Wrap_CallStaticFloatMethodV;
table->CallStaticDoubleMethod = Wrap_CallStaticDoubleMethod;
table->CallStaticDoubleMethodV = Wrap_CallStaticDoubleMethodV;
table->GetObjectField = Wrap_GetObjectField;
table->GetBooleanField = Wrap_GetBooleanField;
table->GetIntField = Wrap_GetIntField;
table->GetLongField = Wrap_GetLongField;
table->GetFloatField = Wrap_GetFloatField;
table->GetDoubleField = Wrap_GetDoubleField;
table->SetObjectField = Wrap_SetObjectField;
table->SetBooleanField = Wrap_SetBooleanField;
table->SetIntField = Wrap_SetIntField;
table->SetLongField = Wrap_SetLongField;
table->SetFloatField = Wrap_SetFloatField;
table->SetDoubleField = Wrap_SetDoubleField;
table->GetStaticObjectField = Wrap_GetStaticObjectField;
table->GetStaticBooleanField = Wrap_GetStaticBooleanField;
table->GetStaticIntField = Wrap_GetStaticIntField;
table->SetStaticObjectField = Wrap_SetStaticObjectField;
table->SetStaticBooleanField = Wrap_SetStaticBooleanField;
table->SetStaticIntField = Wrap_SetStaticIntField;
mprotect((void*)base, span, PROT_READ); // restore - real ART keeps this read-only
TraceLog("InstallJniTraceHooks: installed (full call/field/exception/lifecycle set, file+logcat)");
}