|
@@ -0,0 +1,353 @@
|
|
|
|
|
+/*
|
|
|
|
|
+ * bakin_minimal.so - LD_PRELOAD shim that makes Embarrassed Shina-chan run on
|
|
|
|
|
+ * Proton. Three fixes, no game files touched:
|
|
|
|
|
+ *
|
|
|
|
|
+ * - VAO rebind: the engine feeds 2D-sprite/UI vertex attributes to the default
|
|
|
|
|
+ * VAO (0). Fine on a compat-profile GL context, rejected on the core-profile
|
|
|
|
|
+ * one Wine hands it, so those draws (characters, menus, UI) silently vanish.
|
|
|
|
|
+ * Keep a real VAO bound whenever the game is on VAO 0. This is the one that
|
|
|
|
|
+ * brings the characters back.
|
|
|
|
|
+ * - cap the infinite NtWaitForSingleObject the audio init deadlocks on after
|
|
|
|
|
+ * WASAPI setup fails, which otherwise freezes the game once the map loads.
|
|
|
|
|
+ * - drop the IBL BRDF LUT the game can't build under Wine into its temp dir.
|
|
|
|
|
+ *
|
|
|
|
|
+ * The shim is LD_PRELOADed into the whole Proton process tree, so the ntdll
|
|
|
|
|
+ * splice, the IBL dropper and their poller threads run only in the game process
|
|
|
|
|
+ * (bakinplayer.exe on the command line); every other wine and Steam helper just
|
|
|
|
|
+ * carries the harmless GL interposition. Logging is off unless BAKIN_HOOK_LOG is
|
|
|
|
|
+ * set in the environment, in which case each process writes /tmp/bakin-hook.<pid>.log.
|
|
|
|
|
+ *
|
|
|
|
|
+ * README has the long version. build:
|
|
|
|
|
+ * gcc -O2 -shared -fPIC -o bakin_minimal.so bakin_minimal.c -ldl -lpthread
|
|
|
|
|
+ */
|
|
|
|
|
+#define _GNU_SOURCE
|
|
|
|
|
+#include <stdio.h>
|
|
|
|
|
+#include <stdlib.h>
|
|
|
|
|
+#include <stdint.h>
|
|
|
|
|
+#include <string.h>
|
|
|
|
|
+#include <strings.h>
|
|
|
|
|
+#include <unistd.h>
|
|
|
|
|
+#include <link.h>
|
|
|
|
|
+#include <sys/mman.h>
|
|
|
|
|
+#include <sys/syscall.h>
|
|
|
|
|
+#include <sys/stat.h>
|
|
|
|
|
+#include <dirent.h>
|
|
|
|
|
+#include <fcntl.h>
|
|
|
|
|
+#include <time.h>
|
|
|
|
|
+#include <dlfcn.h>
|
|
|
|
|
+#include <pthread.h>
|
|
|
|
|
+
|
|
|
|
|
+/* install.sh fills these two in. */
|
|
|
|
|
+#define IBL_BRDF_SRC "@IBL_SRC@"
|
|
|
|
|
+#define BAKIN_ENGINE_TMPBASE "@TMPBASE@"
|
|
|
|
|
+#define LOGPATH_FMT "/tmp/bakin-hook.%d.log"
|
|
|
|
|
+
|
|
|
|
|
+#define STATUS_SUCCESS 0x00000000UL
|
|
|
|
|
+#define STATUS_TIMEOUT 0x00000102UL
|
|
|
|
|
+#define NTDLL_WAITFORSINGLEOBJ_VA 0x5b530UL /* Wine/Proton 11 ntdll.so */
|
|
|
|
|
+
|
|
|
|
|
+static FILE *logfile = NULL;
|
|
|
|
|
+static void logln(const char *s) { if (logfile) { fputs(s, logfile); fputc('\n', logfile); fflush(logfile); } }
|
|
|
|
|
+
|
|
|
|
|
+/*
|
|
|
|
|
+ * Detour a function: copy its first `prologue` bytes to a trampoline (followed
|
|
|
|
|
+ * by a jmp back), then overwrite the entry with an abs jmp to `hook`. Returns
|
|
|
|
|
+ * the trampoline, i.e. a callable pointer to the original. Only used for ntdll.
|
|
|
|
|
+ */
|
|
|
|
|
+static void *install_splice(uintptr_t func, void *hook, int prologue) {
|
|
|
|
|
+ if (prologue < 14) return NULL; /* need room for the 14-byte jmp */
|
|
|
|
|
+ uint8_t *tramp = mmap(NULL, prologue + 14, PROT_READ | PROT_WRITE | PROT_EXEC,
|
|
|
|
|
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
|
|
|
|
+ if (tramp == MAP_FAILED) return NULL;
|
|
|
|
|
+ memcpy(tramp, (void *)func, prologue);
|
|
|
|
|
+ uint8_t *back = tramp + prologue;
|
|
|
|
|
+ back[0] = 0xff; back[1] = 0x25; memset(back + 2, 0, 4); /* jmp [rip+0] */
|
|
|
|
|
+ uintptr_t cont = func + prologue;
|
|
|
|
|
+ memcpy(back + 6, &cont, 8);
|
|
|
|
|
+
|
|
|
|
|
+ uintptr_t page = func & ~(uintptr_t)0xFFF;
|
|
|
|
|
+ if (mprotect((void *)page, 0x2000, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
|
|
|
|
|
+ munmap(tramp, prologue + 14);
|
|
|
|
|
+ return NULL;
|
|
|
|
|
+ }
|
|
|
|
|
+ uint8_t *p = (uint8_t *)func;
|
|
|
|
|
+ p[0] = 0xff; p[1] = 0x25; memset(p + 2, 0, 4);
|
|
|
|
|
+ uintptr_t haddr = (uintptr_t)hook;
|
|
|
|
|
+ memcpy(p + 6, &haddr, 8);
|
|
|
|
|
+ memset(p + 14, 0x90, prologue - 14); /* nop the tail */
|
|
|
|
|
+ mprotect((void *)page, 0x2000, PROT_READ | PROT_EXEC);
|
|
|
|
|
+ return tramp;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* --- audio: cap infinite NtWaitForSingleObject waits at 10s ---
|
|
|
|
|
+ * The audio init path blocks forever on a handle that never signals once WASAPI
|
|
|
|
|
+ * setup fails under Wine. We turn every INFINITE wait into a 10s one and report
|
|
|
|
|
+ * it as signaled (STATUS_SUCCESS) rather than STATUS_TIMEOUT, so the caller
|
|
|
|
|
+ * proceeds past the dead handle instead of looping on the timeout. This is a
|
|
|
|
|
+ * blunt instrument - it assumes the game has no genuinely-long INFINITE wait
|
|
|
|
|
+ * that needs to keep blocking - which holds here because the splice is scoped to
|
|
|
|
|
+ * the game process (see is_game_process), not the wine services around it. */
|
|
|
|
|
+
|
|
|
|
|
+typedef uint32_t (*ntwfso_fn)(uintptr_t, int, const int64_t *);
|
|
|
|
|
+static ntwfso_fn ntwfso_orig = NULL;
|
|
|
|
|
+
|
|
|
|
|
+static uint32_t ntwfso_hook(uintptr_t h, int alertable, const int64_t *timeout) {
|
|
|
|
|
+ if (!ntwfso_orig) return STATUS_SUCCESS;
|
|
|
|
|
+ if (timeout) return ntwfso_orig(h, alertable, timeout); /* only touch INFINITE waits */
|
|
|
|
|
+ const int64_t ten_s = -100000000LL; /* negative = relative, 100ns ticks */
|
|
|
|
|
+ uint32_t r = ntwfso_orig(h, alertable, &ten_s);
|
|
|
|
|
+ return r == STATUS_TIMEOUT ? STATUS_SUCCESS : r;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+static int match_ntdll(struct dl_phdr_info *info, size_t sz, void *out) {
|
|
|
|
|
+ (void)sz;
|
|
|
|
|
+ if (info->dlpi_name && strstr(info->dlpi_name, "ntdll.so")) {
|
|
|
|
|
+ *(uintptr_t *)out = (uintptr_t)info->dlpi_addr;
|
|
|
|
|
+ return 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ return 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* ntdll is mapped after our constructor runs, so poll for it. */
|
|
|
|
|
+static void *ntdll_poller(void *a) {
|
|
|
|
|
+ (void)a;
|
|
|
|
|
+ for (int i = 0; i < 6000; i++) {
|
|
|
|
|
+ uintptr_t base = 0;
|
|
|
|
|
+ dl_iterate_phdr(match_ntdll, &base);
|
|
|
|
|
+ if (base) {
|
|
|
|
|
+ ntwfso_orig = install_splice(base + NTDLL_WAITFORSINGLEOBJ_VA, ntwfso_hook, 16);
|
|
|
|
|
+ logln("[bakin] ntwfso patched");
|
|
|
|
|
+ return NULL;
|
|
|
|
|
+ }
|
|
|
|
|
+ nanosleep(&(struct timespec){0, 10 * 1000 * 1000}, NULL);
|
|
|
|
|
+ }
|
|
|
|
|
+ return NULL;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* --- IBL: the game wants ibl_brdf_lut.bmp in its per-launch temp extraction --- */
|
|
|
|
|
+
|
|
|
|
|
+static void copy_file(const char *from, const char *to) {
|
|
|
|
|
+ FILE *in = fopen(from, "rb");
|
|
|
|
|
+ if (!in) return;
|
|
|
|
|
+ char tmp[1408];
|
|
|
|
|
+ snprintf(tmp, sizeof(tmp), "%s.wr_%d", to, (int)getpid()); /* write to a sidecar, then rename */
|
|
|
|
|
+ int fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0644);
|
|
|
|
|
+ if (fd >= 0) {
|
|
|
|
|
+ FILE *out = fdopen(fd, "wb");
|
|
|
|
|
+ if (out) {
|
|
|
|
|
+ char buf[65536]; size_t n;
|
|
|
|
|
+ while ((n = fread(buf, 1, sizeof(buf), in)) > 0) fwrite(buf, 1, n, out);
|
|
|
|
|
+ fclose(out);
|
|
|
|
|
+ if (rename(tmp, to) != 0) unlink(tmp);
|
|
|
|
|
+ } else { close(fd); unlink(tmp); }
|
|
|
|
|
+ }
|
|
|
|
|
+ fclose(in);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+static void place_ibl_brdf_lut(void) {
|
|
|
|
|
+ DIR *d = opendir(BAKIN_ENGINE_TMPBASE);
|
|
|
|
|
+ if (!d) return;
|
|
|
|
|
+ struct dirent *ent;
|
|
|
|
|
+ while ((ent = readdir(d))) {
|
|
|
|
|
+ if (ent->d_name[0] == '.') continue;
|
|
|
|
|
+ char dir[1024], probe[1280], dest[1300];
|
|
|
|
|
+ snprintf(dir, sizeof(dir), "%s/%s", BAKIN_ENGINE_TMPBASE, ent->d_name);
|
|
|
|
|
+ snprintf(probe, sizeof(probe), "%s/lib/sysresource/shader", dir);
|
|
|
|
|
+ struct stat st;
|
|
|
|
|
+ if (stat(probe, &st) != 0) continue; /* not an extraction dir */
|
|
|
|
|
+ snprintf(dest, sizeof(dest), "%s/lib/sysresource/texture/ibl_brdf_lut.bmp", dir);
|
|
|
|
|
+ if (stat(dest, &st) == 0) continue; /* already there */
|
|
|
|
|
+ char texdir[1300];
|
|
|
|
|
+ snprintf(texdir, sizeof(texdir), "%s/lib/sysresource/texture", dir);
|
|
|
|
|
+ mkdir(texdir, 0755);
|
|
|
|
|
+ copy_file(IBL_BRDF_SRC, dest);
|
|
|
|
|
+ }
|
|
|
|
|
+ closedir(d);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+static void *ibl_poller(void *a) {
|
|
|
|
|
+ (void)a;
|
|
|
|
|
+ for (int i = 0; i < 1400; i++) { /* ~21s, covers the extraction */
|
|
|
|
|
+ place_ibl_brdf_lut();
|
|
|
|
|
+ nanosleep(&(struct timespec){0, 15 * 1000 * 1000}, NULL);
|
|
|
|
|
+ }
|
|
|
|
|
+ return NULL;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* --- VAO fix ---
|
|
|
|
|
+ * We interpose the three GL calls the billboard/UI setup path uses, plus dlsym
|
|
|
|
|
+ * and *GetProcAddress so the game's runtime symbol lookup lands on us. Whenever
|
|
|
|
|
+ * the game is on VAO 0, bind a real one instead. */
|
|
|
|
|
+
|
|
|
|
|
+typedef void *(*dlsym_fn)(void *, const char *);
|
|
|
|
|
+static dlsym_fn real_dlsym = NULL;
|
|
|
|
|
+static void grab_dlsym(void) {
|
|
|
|
|
+ if (real_dlsym) return;
|
|
|
|
|
+ /* our dlsym override shadows the plain symbol, so reach the real one by
|
|
|
|
|
+ * version. One of these two exists on any glibc from the last ~15 years. */
|
|
|
|
|
+ real_dlsym = (dlsym_fn)dlvsym(RTLD_NEXT, "dlsym", "GLIBC_2.34");
|
|
|
|
|
+ if (!real_dlsym) real_dlsym = (dlsym_fn)dlvsym(RTLD_NEXT, "dlsym", "GLIBC_2.2.5");
|
|
|
|
|
+ if (!real_dlsym) {
|
|
|
|
|
+ /* nothing resolves without this: the GL wrappers below would have no
|
|
|
|
|
+ * real function to forward to and would silently drop draws. Make the
|
|
|
|
|
+ * failure loud instead of invisible. */
|
|
|
|
|
+ static int warned = 0;
|
|
|
|
|
+ if (!warned) { warned = 1; fputs("[bakin] FATAL: could not resolve real dlsym\n", stderr); }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* dlsym first, then glXGetProcAddressARB - GL extension entry points often
|
|
|
|
|
+ * aren't plain exported symbols. */
|
|
|
|
|
+static void *resolve_gl(const char *name) {
|
|
|
|
|
+ grab_dlsym();
|
|
|
|
|
+ if (!real_dlsym) return NULL;
|
|
|
|
|
+ void *p = real_dlsym(RTLD_NEXT, name);
|
|
|
|
|
+ if (p) return p;
|
|
|
|
|
+ void *(*gpa)(const char *) = (void *(*)(const char *))real_dlsym(RTLD_NEXT, "glXGetProcAddressARB");
|
|
|
|
|
+ return gpa ? gpa(name) : NULL;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+typedef void (*glGenVertexArrays_fn)(int, unsigned *);
|
|
|
|
|
+typedef void (*glBindVertexArray_fn)(unsigned);
|
|
|
|
|
+typedef void (*glGetIntegerv_fn)(unsigned, int *);
|
|
|
|
|
+typedef void (*glVertexAttribPointer_fn)(unsigned, int, unsigned, unsigned char, int, const void *);
|
|
|
|
|
+typedef void (*glEnableVertexAttribArray_fn)(unsigned);
|
|
|
|
|
+typedef void *(*glXGetCurrentContext_fn)(void);
|
|
|
|
|
+typedef void *(*eglGetCurrentContext_fn)(void);
|
|
|
|
|
+static glGenVertexArrays_fn real_glGenVertexArrays;
|
|
|
|
|
+static glBindVertexArray_fn real_glBindVertexArray;
|
|
|
|
|
+static glGetIntegerv_fn real_glGetIntegerv;
|
|
|
|
|
+static glVertexAttribPointer_fn real_glVertexAttribPointer;
|
|
|
|
|
+static glEnableVertexAttribArray_fn real_glEnableVertexAttribArray;
|
|
|
|
|
+static glXGetCurrentContext_fn real_glXGetCurrentContext;
|
|
|
|
|
+static eglGetCurrentContext_fn real_eglGetCurrentContext;
|
|
|
|
|
+
|
|
|
|
|
+#define RESOLVE(fn) do { if (!real_##fn) real_##fn = (fn##_fn)resolve_gl(#fn); } while (0)
|
|
|
|
|
+#define GL_VERTEX_ARRAY_BINDING 0x85B5
|
|
|
|
|
+
|
|
|
|
|
+/* One persistent VAO per GL context. VAO names are not shared between contexts,
|
|
|
|
|
+ * so a single global would fail the moment the game issued a default-VAO draw
|
|
|
|
|
+ * from a second context. Contexts are few (usually one), so a small table under
|
|
|
|
|
+ * a lock is plenty. */
|
|
|
|
|
+static struct { void *ctx; unsigned vao; } g_vaos[8];
|
|
|
|
|
+static pthread_mutex_t g_vao_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
|
+
|
|
|
|
|
+static void *current_gl_context(void) {
|
|
|
|
|
+ RESOLVE(glXGetCurrentContext); RESOLVE(eglGetCurrentContext);
|
|
|
|
|
+ void *c = real_glXGetCurrentContext ? real_glXGetCurrentContext() : NULL;
|
|
|
|
|
+ if (!c && real_eglGetCurrentContext) c = real_eglGetCurrentContext();
|
|
|
|
|
+ return c; /* NULL is a valid key: one fallback VAO when neither GLX nor EGL answers */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+static unsigned vao_for_current_context(void) {
|
|
|
|
|
+ RESOLVE(glGenVertexArrays);
|
|
|
|
|
+ if (!real_glGenVertexArrays) return 0;
|
|
|
|
|
+ void *ctx = current_gl_context();
|
|
|
|
|
+ unsigned vao = 0;
|
|
|
|
|
+ int free_slot = -1;
|
|
|
|
|
+ pthread_mutex_lock(&g_vao_lock);
|
|
|
|
|
+ for (unsigned i = 0; i < 8; i++) {
|
|
|
|
|
+ if (g_vaos[i].vao && g_vaos[i].ctx == ctx) { vao = g_vaos[i].vao; break; }
|
|
|
|
|
+ if (!g_vaos[i].vao && free_slot < 0) free_slot = (int)i;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!vao) {
|
|
|
|
|
+ real_glGenVertexArrays(1, &vao);
|
|
|
|
|
+ if (vao && free_slot >= 0) { g_vaos[free_slot].ctx = ctx; g_vaos[free_slot].vao = vao; }
|
|
|
|
|
+ if (logfile) { fprintf(logfile, "[bakin] VAO fix: created VAO %u for ctx %p\n", vao, ctx); fflush(logfile); }
|
|
|
|
|
+ }
|
|
|
|
|
+ pthread_mutex_unlock(&g_vao_lock);
|
|
|
|
|
+ return vao;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+static void ensure_vao(void) {
|
|
|
|
|
+ RESOLVE(glBindVertexArray); RESOLVE(glGetIntegerv);
|
|
|
|
|
+ if (!real_glBindVertexArray || !real_glGetIntegerv) return;
|
|
|
|
|
+ int cur = -1;
|
|
|
|
|
+ real_glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &cur);
|
|
|
|
|
+ if (cur != 0) return; /* a real VAO is bound, leave it */
|
|
|
|
|
+ unsigned vao = vao_for_current_context();
|
|
|
|
|
+ if (vao) real_glBindVertexArray(vao);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void glVertexAttribPointer(unsigned i, int size, unsigned type, unsigned char norm,
|
|
|
|
|
+ int stride, const void *ptr) {
|
|
|
|
|
+ RESOLVE(glVertexAttribPointer);
|
|
|
|
|
+ ensure_vao();
|
|
|
|
|
+ if (real_glVertexAttribPointer) real_glVertexAttribPointer(i, size, type, norm, stride, ptr);
|
|
|
|
|
+}
|
|
|
|
|
+void glEnableVertexAttribArray(unsigned i) {
|
|
|
|
|
+ RESOLVE(glEnableVertexAttribArray);
|
|
|
|
|
+ ensure_vao();
|
|
|
|
|
+ if (real_glEnableVertexAttribArray) real_glEnableVertexAttribArray(i);
|
|
|
|
|
+}
|
|
|
|
|
+void glBindVertexArray(unsigned arr) {
|
|
|
|
|
+ RESOLVE(glBindVertexArray);
|
|
|
|
|
+ if (arr == 0) arr = vao_for_current_context(); /* default VAO -> ours */
|
|
|
|
|
+ if (real_glBindVertexArray) real_glBindVertexArray(arr);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+static void *our_wrapper(const char *name) {
|
|
|
|
|
+ if (!name) return NULL;
|
|
|
|
|
+ if (!strcmp(name, "glVertexAttribPointer")) return glVertexAttribPointer;
|
|
|
|
|
+ if (!strcmp(name, "glEnableVertexAttribArray")) return glEnableVertexAttribArray;
|
|
|
|
|
+ if (!strcmp(name, "glBindVertexArray")) return glBindVertexArray;
|
|
|
|
|
+ return NULL;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void *dlsym(void *handle, const char *name) {
|
|
|
|
|
+ grab_dlsym();
|
|
|
|
|
+ void *w = our_wrapper(name);
|
|
|
|
|
+ return w ? w : (real_dlsym ? real_dlsym(handle, name) : NULL);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* glXGetProcAddress / glXGetProcAddressARB / eglGetProcAddress are the same
|
|
|
|
|
+ * shape; the arg is a name string either way. */
|
|
|
|
|
+#define PROC_ADDR_SHIM(sym) \
|
|
|
|
|
+ void *sym(const void *name) { \
|
|
|
|
|
+ static void *(*next)(const char *); \
|
|
|
|
|
+ grab_dlsym(); \
|
|
|
|
|
+ if (!next && real_dlsym) next = (void *(*)(const char *))real_dlsym(RTLD_NEXT, #sym); \
|
|
|
|
|
+ void *w = our_wrapper((const char *)name); \
|
|
|
|
|
+ return w ? w : (next ? next((const char *)name) : NULL); \
|
|
|
|
|
+ }
|
|
|
|
|
+PROC_ADDR_SHIM(glXGetProcAddress)
|
|
|
|
|
+PROC_ADDR_SHIM(glXGetProcAddressARB)
|
|
|
|
|
+PROC_ADDR_SHIM(eglGetProcAddress)
|
|
|
|
|
+
|
|
|
|
|
+/* The hook is preloaded into the whole Proton tree; the game's unix process is
|
|
|
|
|
+ * the one with bakinplayer.exe on its command line. Everything else (wineserver,
|
|
|
|
|
+ * services.exe, the Steam reaper) skips the ntdll splice and the pollers. */
|
|
|
|
|
+static int is_game_process(void) {
|
|
|
|
|
+ int fd = open("/proc/self/cmdline", O_RDONLY);
|
|
|
|
|
+ if (fd < 0) return 0;
|
|
|
|
|
+ char buf[4096];
|
|
|
|
|
+ ssize_t n = read(fd, buf, sizeof(buf) - 1);
|
|
|
|
|
+ close(fd);
|
|
|
|
|
+ if (n <= 0) return 0;
|
|
|
|
|
+ for (ssize_t i = 0; i < n; i++) if (buf[i] == '\0') buf[i] = ' ';
|
|
|
|
|
+ buf[n] = '\0';
|
|
|
|
|
+ for (char *p = buf; *p; p++)
|
|
|
|
|
+ if ((*p == 'b' || *p == 'B') && strncasecmp(p, "bakin", 5) == 0) return 1;
|
|
|
|
|
+ return 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+__attribute__((constructor))
|
|
|
|
|
+static void bakin_init(void) {
|
|
|
|
|
+ int game = is_game_process();
|
|
|
|
|
+ if (getenv("BAKIN_HOOK_LOG")) { /* logging is opt-in */
|
|
|
|
|
+ char path[256];
|
|
|
|
|
+ snprintf(path, sizeof(path), LOGPATH_FMT, getpid());
|
|
|
|
|
+ logfile = fopen(path, "w");
|
|
|
|
|
+ }
|
|
|
|
|
+ logln(game ? "[bakin] init (game process)" : "[bakin] init");
|
|
|
|
|
+ if (!game) return; /* other procs keep only the GL interposition, which is free without GL */
|
|
|
|
|
+
|
|
|
|
|
+ pthread_attr_t attr;
|
|
|
|
|
+ pthread_attr_init(&attr);
|
|
|
|
|
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
|
|
|
|
+ pthread_t t;
|
|
|
|
|
+ pthread_create(&t, &attr, ntdll_poller, NULL);
|
|
|
|
|
+ pthread_create(&t, &attr, ibl_poller, NULL);
|
|
|
|
|
+ pthread_attr_destroy(&attr);
|
|
|
|
|
+ /* the VAO fix works purely by symbol interposition, no thread needed. */
|
|
|
|
|
+}
|