Files
plezy/android/libass/src/main/cpp/SurfaceTxProbe.c
T

322 lines
11 KiB
C

// EXPERIMENTAL SPIKE — does the codec VIDEO plane's actual present time differ from a
// GL OSD overlay's, and is that difference measurable in-app? (See the memory note
// project_libass_present_time_instrumentation.)
//
// Android exposes no present time for a codec-owned video layer. The one runtime lead is
// API 34 `SurfaceView.applyTransactionToFrame()`: it merges an otherwise-empty,
// callback-bearing transaction with the codec's *next buffer* transaction. The completion
// stats then carry, per surface, the PREVIOUS-RELEASE fence — the moment the buffer the new
// frame replaced stopped being read by the display ≈ when the new codec frame actually
// reached the video plane. Compared in Kotlin to that frame's `releaseTimeNs` target (the
// overlay's own present error is ~0), the delta is the inter-plane lag.
//
// The SurfaceControl transaction-stats / fromJava APIs are API 29/34 while this module's
// minSdk is 21; clang's __builtin_available doesn't guard the Android availability domain in
// this toolchain. So the libandroid entry points are resolved by dlsym (no
// availability-attributed declarations get compiled). Every call site is additionally
// hard-gated to API >= 34 in Kotlin (VideoLayerLatencyProbe).
//
// Fence timestamps are read with the raw SYNC_IOC_FILE_INFO ioctl rather than libsync:
// libsync.so is NOT a public NDK library, so an app's linker namespace blocks dlopen of it.
//
// Compiled into the libass "asskt" .so; called from app Kotlin SurfaceTxProbe.
#include <android/log.h>
#include <dlfcn.h>
#include <jni.h>
#include <poll.h>
#include <pthread.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <time.h>
#include <unistd.h>
#define LOG_TAG "SurfaceTxProbe"
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#ifndef SYNC_IOC_MAGIC
#define SYNC_IOC_MAGIC '>'
#endif
struct stp_fence_info {
char obj_name[32];
char driver_name[32];
int32_t status;
uint32_t flags;
uint64_t timestamp_ns;
};
struct stp_file_info {
char name[32];
int32_t status;
uint32_t flags;
uint32_t num_fences;
uint32_t pad;
uint64_t sync_fence_info;
};
#ifndef SYNC_IOC_FILE_INFO
#define SYNC_IOC_FILE_INFO _IOWR(SYNC_IOC_MAGIC, 4, struct stp_file_info)
#endif
// libsync is hidden from app linker namespaces, so use the kernel ioctl directly.
static int64_t readFenceTimeNs(int fd, int* outStatus) {
*outStatus = -1;
if (fd < 0) return -1;
struct stp_file_info probe;
memset(&probe, 0, sizeof(probe));
if (ioctl(fd, SYNC_IOC_FILE_INFO, &probe) < 0) return -1;
*outStatus = probe.status;
if (probe.status != 1 || probe.num_fences == 0) return -1;
struct stp_fence_info* arr =
(struct stp_fence_info*)calloc(probe.num_fences, sizeof(struct stp_fence_info));
if (!arr) return -1;
struct stp_file_info req;
memset(&req, 0, sizeof(req));
req.num_fences = probe.num_fences;
req.sync_fence_info = (uint64_t)(uintptr_t)arr;
int64_t t = -1;
if (ioctl(fd, SYNC_IOC_FILE_INFO, &req) == 0) {
for (uint32_t i = 0; i < req.num_fences; i++) {
if ((int64_t)arr[i].timestamp_ns > t) t = (int64_t)arr[i].timestamp_ns;
}
}
free(arr);
return t;
}
typedef struct ASurfaceTransaction ASurfaceTransaction;
typedef struct ASurfaceControl ASurfaceControl;
typedef struct ASurfaceTransactionStats ASurfaceTransactionStats;
typedef void (*OnCompleteFn)(void* context, ASurfaceTransactionStats* stats);
typedef ASurfaceTransaction* (*pf_fromJava)(JNIEnv*, jobject);
typedef void (*pf_setOnComplete)(ASurfaceTransaction*, void*, OnCompleteFn);
typedef int64_t (*pf_latchTime)(ASurfaceTransactionStats*);
typedef void (*pf_getControls)(ASurfaceTransactionStats*, ASurfaceControl***, size_t*);
typedef int (*pf_prevRelease)(ASurfaceTransactionStats*, ASurfaceControl*);
typedef void (*pf_releaseControls)(ASurfaceControl**);
static pf_fromJava p_fromJava = NULL;
static pf_setOnComplete p_setOnComplete = NULL;
static pf_latchTime p_latchTime = NULL;
static pf_getControls p_getControls = NULL;
static pf_prevRelease p_prevRelease = NULL;
static pf_releaseControls p_releaseControls = NULL;
static int gScResolved = 0;
static int gScOk = 0;
static int gScWarningLogged = 0;
// Runtime lookup keeps this API-34 probe buildable with the module's minSdk 21.
static int resolveSc(void) {
if (gScResolved) return gScOk;
gScResolved = 1;
void* h = dlopen("libandroid.so", RTLD_NOW | RTLD_GLOBAL);
if (!h) return 0;
p_fromJava = (pf_fromJava)dlsym(h, "ASurfaceTransaction_fromJava");
p_setOnComplete = (pf_setOnComplete)dlsym(h, "ASurfaceTransaction_setOnComplete");
p_latchTime = (pf_latchTime)dlsym(h, "ASurfaceTransactionStats_getLatchTime");
p_getControls = (pf_getControls)dlsym(h, "ASurfaceTransactionStats_getASurfaceControls");
p_prevRelease = (pf_prevRelease)dlsym(h, "ASurfaceTransactionStats_getPreviousReleaseFenceFd");
p_releaseControls = (pf_releaseControls)dlsym(h, "ASurfaceTransactionStats_releaseASurfaceControls");
gScOk = (p_fromJava && p_setOnComplete && p_latchTime && p_getControls && p_prevRelease &&
p_releaseControls)
? 1
: 0;
return gScOk;
}
// The completion context is pointer-width, so 32-bit devices need an index side-channel.
#define TAG_RING_SIZE 256
static int64_t gTagRing[TAG_RING_SIZE];
static int gSrcRing[TAG_RING_SIZE];
static int gTagSeq = 0;
static JavaVM* gVm = NULL;
static jclass gProbeClass = NULL;
static jmethodID gOnResult = NULL;
static int gCallbackLookupAttempted = 0;
static pthread_mutex_t gInitLock = PTHREAD_MUTEX_INITIALIZER;
static int64_t nowMonoNs(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec;
}
static void reportResult(JNIEnv* env, int64_t tag, int64_t latchNs, int64_t releaseNs, int count,
int fenceState, int source, int64_t cbNs) {
if (!env || !gProbeClass || !gOnResult) return;
(*env)->CallStaticVoidMethod(env, gProbeClass, gOnResult, (jlong)tag, (jlong)latchNs,
(jlong)releaseNs, (jint)count, (jint)fenceState, (jint)source,
(jlong)cbNs);
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
}
// Previous-release fences can still be pending in the binder callback; a worker
// thread keeps SurfaceFlinger's callback dispatch unblocked.
struct fenceJob {
int fd;
int64_t tag;
int64_t latch;
int count;
int source;
};
#define JOB_RING 64
static struct fenceJob gJobs[JOB_RING];
static int gJobHead = 0, gJobTail = 0;
static int gJobDrops = 0;
static pthread_mutex_t gJobLock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t gJobCond = PTHREAD_COND_INITIALIZER;
static pthread_t gReader;
static int gReaderStarted = 0;
static void* readerMain(void* arg);
static int ensureProbeInit(JNIEnv* env, jclass clazz) {
int scOk = 0;
pthread_mutex_lock(&gInitLock);
if (!gVm && (*env)->GetJavaVM(env, &gVm) != JNI_OK) {
LOGW("GetJavaVM failed — transaction probe disabled");
pthread_mutex_unlock(&gInitLock);
return 0;
}
if (!gProbeClass) {
gProbeClass = (*env)->NewGlobalRef(env, clazz);
if (!gProbeClass) {
LOGW("NewGlobalRef(SurfaceTxProbe) failed — transaction probe disabled");
pthread_mutex_unlock(&gInitLock);
return 0;
}
}
if (!gCallbackLookupAttempted) {
gCallbackLookupAttempted = 1;
gOnResult = (*env)->GetStaticMethodID(env, clazz, "onResult", "(JJJIIIJ)V");
if (!gOnResult) {
(*env)->ExceptionClear(env);
LOGW("onResult(JJJIIIJ)V not found — JNI callback disabled (R8 stripped it?)");
}
}
if (!gOnResult) {
pthread_mutex_unlock(&gInitLock);
return 0;
}
scOk = resolveSc();
if (!scOk) {
if (!gScWarningLogged) {
gScWarningLogged = 1;
LOGW("libandroid SurfaceControl transaction-stats API unavailable");
}
pthread_mutex_unlock(&gInitLock);
return 0;
}
if (!gReaderStarted) {
const int rc = pthread_create(&gReader, NULL, readerMain, NULL);
if (rc != 0) {
LOGW("fence reader thread start failed: %d", rc);
pthread_mutex_unlock(&gInitLock);
return 0;
}
gReaderStarted = 1;
}
pthread_mutex_unlock(&gInitLock);
return scOk;
}
static void* readerMain(void* arg) {
(void)arg;
JNIEnv* env = NULL;
if ((*gVm)->AttachCurrentThread(gVm, &env, NULL) != JNI_OK) return NULL;
for (;;) {
pthread_mutex_lock(&gJobLock);
while (gJobHead == gJobTail) pthread_cond_wait(&gJobCond, &gJobLock);
struct fenceJob job = gJobs[gJobTail];
gJobTail = (gJobTail + 1) % JOB_RING;
pthread_mutex_unlock(&gJobLock);
int64_t releaseNs = -1;
int fenceState;
struct pollfd pfd = {.fd = job.fd, .events = POLLIN, .revents = 0};
int pr = poll(&pfd, 1, 250);
if (pr > 0 && (pfd.revents & POLLIN)) {
int st = -1;
releaseNs = readFenceTimeNs(job.fd, &st);
fenceState = (releaseNs > 0) ? 3 : 1;
} else {
fenceState = 2;
}
close(job.fd);
reportResult(env, job.tag, job.latch, releaseNs, job.count, fenceState, job.source, nowMonoNs());
}
return NULL;
}
static void onComplete(void* context, ASurfaceTransactionStats* stats) {
int slot = (int)(intptr_t)context;
__atomic_thread_fence(__ATOMIC_ACQUIRE);
int64_t tag = gTagRing[slot & (TAG_RING_SIZE - 1)];
int source = gSrcRing[slot & (TAG_RING_SIZE - 1)];
int64_t latchNs = p_latchTime ? p_latchTime(stats) : -1;
// Display-global present fences are abort-prone; per-surface release fences are stable here.
int chosenFd = -1;
int count = 0;
ASurfaceControl** controls = NULL;
size_t n = 0;
if (p_getControls) p_getControls(stats, &controls, &n);
count = (int)n;
for (size_t i = 0; i < n; i++) {
int rfd = p_prevRelease ? p_prevRelease(stats, controls[i]) : -1;
if (rfd < 0) continue;
if (chosenFd < 0)
chosenFd = rfd;
else
close(rfd);
}
if (controls && p_releaseControls) p_releaseControls(controls);
if (chosenFd < 0) {
JNIEnv* env = NULL;
int didAttach = 0;
if ((*gVm)->GetEnv(gVm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
if ((*gVm)->AttachCurrentThread(gVm, &env, NULL) != JNI_OK) return;
didAttach = 1;
}
reportResult(env, tag, latchNs, -1, count, 0, source, nowMonoNs());
if (didAttach) (*gVm)->DetachCurrentThread(gVm);
return;
}
pthread_mutex_lock(&gJobLock);
int next = (gJobHead + 1) % JOB_RING;
if (next == gJobTail) {
close(chosenFd);
if (gJobDrops++ == 0) LOGW("fence job ring full — dropping samples (slow fence reader?)");
} else {
gJobs[gJobHead].fd = chosenFd;
gJobs[gJobHead].tag = tag;
gJobs[gJobHead].latch = latchNs;
gJobs[gJobHead].count = count;
gJobs[gJobHead].source = source;
gJobHead = next;
pthread_cond_signal(&gJobCond);
}
pthread_mutex_unlock(&gJobLock);
}
JNIEXPORT void JNICALL Java_com_edde746_plezy_exoplayer_SurfaceTxProbe_nativeAttach(
JNIEnv* env, jclass clazz, jobject jtransaction, jlong tag, jint source) {
if (!ensureProbeInit(env, clazz)) return;
int slot = __atomic_fetch_add(&gTagSeq, 1, __ATOMIC_RELAXED) & (TAG_RING_SIZE - 1);
gTagRing[slot] = (int64_t)tag;
gSrcRing[slot] = (int)source;
// Binder carries only the slot index, so the ring write has to win publication.
__atomic_thread_fence(__ATOMIC_RELEASE);
ASurfaceTransaction* tx = p_fromJava(env, jtransaction);
if (!tx) {
LOGW("ASurfaceTransaction_fromJava returned null");
return;
}
p_setOnComplete(tx, (void*)(intptr_t)slot, onComplete);
}