| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- /*
- * 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. */
- }
|