bakin_minimal.c 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. /*
  2. * bakin_minimal.so - LD_PRELOAD shim that makes Embarrassed Shina-chan run on
  3. * Proton. Three fixes, no game files touched:
  4. *
  5. * - VAO rebind: the engine feeds 2D-sprite/UI vertex attributes to the default
  6. * VAO (0). Fine on a compat-profile GL context, rejected on the core-profile
  7. * one Wine hands it, so those draws (characters, menus, UI) silently vanish.
  8. * Keep a real VAO bound whenever the game is on VAO 0. This is the one that
  9. * brings the characters back.
  10. * - cap the infinite NtWaitForSingleObject the audio init deadlocks on after
  11. * WASAPI setup fails, which otherwise freezes the game once the map loads.
  12. * - drop the IBL BRDF LUT the game can't build under Wine into its temp dir.
  13. *
  14. * The shim is LD_PRELOADed into the whole Proton process tree, so the ntdll
  15. * splice, the IBL dropper and their poller threads run only in the game process
  16. * (bakinplayer.exe on the command line); every other wine and Steam helper just
  17. * carries the harmless GL interposition. Logging is off unless BAKIN_HOOK_LOG is
  18. * set in the environment, in which case each process writes /tmp/bakin-hook.<pid>.log.
  19. *
  20. * README has the long version. build:
  21. * gcc -O2 -shared -fPIC -o bakin_minimal.so bakin_minimal.c -ldl -lpthread
  22. */
  23. #define _GNU_SOURCE
  24. #include <stdio.h>
  25. #include <stdlib.h>
  26. #include <stdint.h>
  27. #include <string.h>
  28. #include <strings.h>
  29. #include <unistd.h>
  30. #include <link.h>
  31. #include <sys/mman.h>
  32. #include <sys/syscall.h>
  33. #include <sys/stat.h>
  34. #include <dirent.h>
  35. #include <fcntl.h>
  36. #include <time.h>
  37. #include <dlfcn.h>
  38. #include <pthread.h>
  39. /* install.sh fills these two in. */
  40. #define IBL_BRDF_SRC "@IBL_SRC@"
  41. #define BAKIN_ENGINE_TMPBASE "@TMPBASE@"
  42. #define LOGPATH_FMT "/tmp/bakin-hook.%d.log"
  43. #define STATUS_SUCCESS 0x00000000UL
  44. #define STATUS_TIMEOUT 0x00000102UL
  45. #define NTDLL_WAITFORSINGLEOBJ_VA 0x5b530UL /* Wine/Proton 11 ntdll.so */
  46. static FILE *logfile = NULL;
  47. static void logln(const char *s) { if (logfile) { fputs(s, logfile); fputc('\n', logfile); fflush(logfile); } }
  48. /*
  49. * Detour a function: copy its first `prologue` bytes to a trampoline (followed
  50. * by a jmp back), then overwrite the entry with an abs jmp to `hook`. Returns
  51. * the trampoline, i.e. a callable pointer to the original. Only used for ntdll.
  52. */
  53. static void *install_splice(uintptr_t func, void *hook, int prologue) {
  54. if (prologue < 14) return NULL; /* need room for the 14-byte jmp */
  55. uint8_t *tramp = mmap(NULL, prologue + 14, PROT_READ | PROT_WRITE | PROT_EXEC,
  56. MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  57. if (tramp == MAP_FAILED) return NULL;
  58. memcpy(tramp, (void *)func, prologue);
  59. uint8_t *back = tramp + prologue;
  60. back[0] = 0xff; back[1] = 0x25; memset(back + 2, 0, 4); /* jmp [rip+0] */
  61. uintptr_t cont = func + prologue;
  62. memcpy(back + 6, &cont, 8);
  63. uintptr_t page = func & ~(uintptr_t)0xFFF;
  64. if (mprotect((void *)page, 0x2000, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
  65. munmap(tramp, prologue + 14);
  66. return NULL;
  67. }
  68. uint8_t *p = (uint8_t *)func;
  69. p[0] = 0xff; p[1] = 0x25; memset(p + 2, 0, 4);
  70. uintptr_t haddr = (uintptr_t)hook;
  71. memcpy(p + 6, &haddr, 8);
  72. memset(p + 14, 0x90, prologue - 14); /* nop the tail */
  73. mprotect((void *)page, 0x2000, PROT_READ | PROT_EXEC);
  74. return tramp;
  75. }
  76. /* --- audio: cap infinite NtWaitForSingleObject waits at 10s ---
  77. * The audio init path blocks forever on a handle that never signals once WASAPI
  78. * setup fails under Wine. We turn every INFINITE wait into a 10s one and report
  79. * it as signaled (STATUS_SUCCESS) rather than STATUS_TIMEOUT, so the caller
  80. * proceeds past the dead handle instead of looping on the timeout. This is a
  81. * blunt instrument - it assumes the game has no genuinely-long INFINITE wait
  82. * that needs to keep blocking - which holds here because the splice is scoped to
  83. * the game process (see is_game_process), not the wine services around it. */
  84. typedef uint32_t (*ntwfso_fn)(uintptr_t, int, const int64_t *);
  85. static ntwfso_fn ntwfso_orig = NULL;
  86. static uint32_t ntwfso_hook(uintptr_t h, int alertable, const int64_t *timeout) {
  87. if (!ntwfso_orig) return STATUS_SUCCESS;
  88. if (timeout) return ntwfso_orig(h, alertable, timeout); /* only touch INFINITE waits */
  89. const int64_t ten_s = -100000000LL; /* negative = relative, 100ns ticks */
  90. uint32_t r = ntwfso_orig(h, alertable, &ten_s);
  91. return r == STATUS_TIMEOUT ? STATUS_SUCCESS : r;
  92. }
  93. static int match_ntdll(struct dl_phdr_info *info, size_t sz, void *out) {
  94. (void)sz;
  95. if (info->dlpi_name && strstr(info->dlpi_name, "ntdll.so")) {
  96. *(uintptr_t *)out = (uintptr_t)info->dlpi_addr;
  97. return 1;
  98. }
  99. return 0;
  100. }
  101. /* ntdll is mapped after our constructor runs, so poll for it. */
  102. static void *ntdll_poller(void *a) {
  103. (void)a;
  104. for (int i = 0; i < 6000; i++) {
  105. uintptr_t base = 0;
  106. dl_iterate_phdr(match_ntdll, &base);
  107. if (base) {
  108. ntwfso_orig = install_splice(base + NTDLL_WAITFORSINGLEOBJ_VA, ntwfso_hook, 16);
  109. logln("[bakin] ntwfso patched");
  110. return NULL;
  111. }
  112. nanosleep(&(struct timespec){0, 10 * 1000 * 1000}, NULL);
  113. }
  114. return NULL;
  115. }
  116. /* --- IBL: the game wants ibl_brdf_lut.bmp in its per-launch temp extraction --- */
  117. static void copy_file(const char *from, const char *to) {
  118. FILE *in = fopen(from, "rb");
  119. if (!in) return;
  120. char tmp[1408];
  121. snprintf(tmp, sizeof(tmp), "%s.wr_%d", to, (int)getpid()); /* write to a sidecar, then rename */
  122. int fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0644);
  123. if (fd >= 0) {
  124. FILE *out = fdopen(fd, "wb");
  125. if (out) {
  126. char buf[65536]; size_t n;
  127. while ((n = fread(buf, 1, sizeof(buf), in)) > 0) fwrite(buf, 1, n, out);
  128. fclose(out);
  129. if (rename(tmp, to) != 0) unlink(tmp);
  130. } else { close(fd); unlink(tmp); }
  131. }
  132. fclose(in);
  133. }
  134. static void place_ibl_brdf_lut(void) {
  135. DIR *d = opendir(BAKIN_ENGINE_TMPBASE);
  136. if (!d) return;
  137. struct dirent *ent;
  138. while ((ent = readdir(d))) {
  139. if (ent->d_name[0] == '.') continue;
  140. char dir[1024], probe[1280], dest[1300];
  141. snprintf(dir, sizeof(dir), "%s/%s", BAKIN_ENGINE_TMPBASE, ent->d_name);
  142. snprintf(probe, sizeof(probe), "%s/lib/sysresource/shader", dir);
  143. struct stat st;
  144. if (stat(probe, &st) != 0) continue; /* not an extraction dir */
  145. snprintf(dest, sizeof(dest), "%s/lib/sysresource/texture/ibl_brdf_lut.bmp", dir);
  146. if (stat(dest, &st) == 0) continue; /* already there */
  147. char texdir[1300];
  148. snprintf(texdir, sizeof(texdir), "%s/lib/sysresource/texture", dir);
  149. mkdir(texdir, 0755);
  150. copy_file(IBL_BRDF_SRC, dest);
  151. }
  152. closedir(d);
  153. }
  154. static void *ibl_poller(void *a) {
  155. (void)a;
  156. for (int i = 0; i < 1400; i++) { /* ~21s, covers the extraction */
  157. place_ibl_brdf_lut();
  158. nanosleep(&(struct timespec){0, 15 * 1000 * 1000}, NULL);
  159. }
  160. return NULL;
  161. }
  162. /* --- VAO fix ---
  163. * We interpose the three GL calls the billboard/UI setup path uses, plus dlsym
  164. * and *GetProcAddress so the game's runtime symbol lookup lands on us. Whenever
  165. * the game is on VAO 0, bind a real one instead. */
  166. typedef void *(*dlsym_fn)(void *, const char *);
  167. static dlsym_fn real_dlsym = NULL;
  168. static void grab_dlsym(void) {
  169. if (real_dlsym) return;
  170. /* our dlsym override shadows the plain symbol, so reach the real one by
  171. * version. One of these two exists on any glibc from the last ~15 years. */
  172. real_dlsym = (dlsym_fn)dlvsym(RTLD_NEXT, "dlsym", "GLIBC_2.34");
  173. if (!real_dlsym) real_dlsym = (dlsym_fn)dlvsym(RTLD_NEXT, "dlsym", "GLIBC_2.2.5");
  174. if (!real_dlsym) {
  175. /* nothing resolves without this: the GL wrappers below would have no
  176. * real function to forward to and would silently drop draws. Make the
  177. * failure loud instead of invisible. */
  178. static int warned = 0;
  179. if (!warned) { warned = 1; fputs("[bakin] FATAL: could not resolve real dlsym\n", stderr); }
  180. }
  181. }
  182. /* dlsym first, then glXGetProcAddressARB - GL extension entry points often
  183. * aren't plain exported symbols. */
  184. static void *resolve_gl(const char *name) {
  185. grab_dlsym();
  186. if (!real_dlsym) return NULL;
  187. void *p = real_dlsym(RTLD_NEXT, name);
  188. if (p) return p;
  189. void *(*gpa)(const char *) = (void *(*)(const char *))real_dlsym(RTLD_NEXT, "glXGetProcAddressARB");
  190. return gpa ? gpa(name) : NULL;
  191. }
  192. typedef void (*glGenVertexArrays_fn)(int, unsigned *);
  193. typedef void (*glBindVertexArray_fn)(unsigned);
  194. typedef void (*glGetIntegerv_fn)(unsigned, int *);
  195. typedef void (*glVertexAttribPointer_fn)(unsigned, int, unsigned, unsigned char, int, const void *);
  196. typedef void (*glEnableVertexAttribArray_fn)(unsigned);
  197. typedef void *(*glXGetCurrentContext_fn)(void);
  198. typedef void *(*eglGetCurrentContext_fn)(void);
  199. static glGenVertexArrays_fn real_glGenVertexArrays;
  200. static glBindVertexArray_fn real_glBindVertexArray;
  201. static glGetIntegerv_fn real_glGetIntegerv;
  202. static glVertexAttribPointer_fn real_glVertexAttribPointer;
  203. static glEnableVertexAttribArray_fn real_glEnableVertexAttribArray;
  204. static glXGetCurrentContext_fn real_glXGetCurrentContext;
  205. static eglGetCurrentContext_fn real_eglGetCurrentContext;
  206. #define RESOLVE(fn) do { if (!real_##fn) real_##fn = (fn##_fn)resolve_gl(#fn); } while (0)
  207. #define GL_VERTEX_ARRAY_BINDING 0x85B5
  208. /* One persistent VAO per GL context. VAO names are not shared between contexts,
  209. * so a single global would fail the moment the game issued a default-VAO draw
  210. * from a second context. Contexts are few (usually one), so a small table under
  211. * a lock is plenty. */
  212. static struct { void *ctx; unsigned vao; } g_vaos[8];
  213. static pthread_mutex_t g_vao_lock = PTHREAD_MUTEX_INITIALIZER;
  214. static void *current_gl_context(void) {
  215. RESOLVE(glXGetCurrentContext); RESOLVE(eglGetCurrentContext);
  216. void *c = real_glXGetCurrentContext ? real_glXGetCurrentContext() : NULL;
  217. if (!c && real_eglGetCurrentContext) c = real_eglGetCurrentContext();
  218. return c; /* NULL is a valid key: one fallback VAO when neither GLX nor EGL answers */
  219. }
  220. static unsigned vao_for_current_context(void) {
  221. RESOLVE(glGenVertexArrays);
  222. if (!real_glGenVertexArrays) return 0;
  223. void *ctx = current_gl_context();
  224. unsigned vao = 0;
  225. int free_slot = -1;
  226. pthread_mutex_lock(&g_vao_lock);
  227. for (unsigned i = 0; i < 8; i++) {
  228. if (g_vaos[i].vao && g_vaos[i].ctx == ctx) { vao = g_vaos[i].vao; break; }
  229. if (!g_vaos[i].vao && free_slot < 0) free_slot = (int)i;
  230. }
  231. if (!vao) {
  232. real_glGenVertexArrays(1, &vao);
  233. if (vao && free_slot >= 0) { g_vaos[free_slot].ctx = ctx; g_vaos[free_slot].vao = vao; }
  234. if (logfile) { fprintf(logfile, "[bakin] VAO fix: created VAO %u for ctx %p\n", vao, ctx); fflush(logfile); }
  235. }
  236. pthread_mutex_unlock(&g_vao_lock);
  237. return vao;
  238. }
  239. static void ensure_vao(void) {
  240. RESOLVE(glBindVertexArray); RESOLVE(glGetIntegerv);
  241. if (!real_glBindVertexArray || !real_glGetIntegerv) return;
  242. int cur = -1;
  243. real_glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &cur);
  244. if (cur != 0) return; /* a real VAO is bound, leave it */
  245. unsigned vao = vao_for_current_context();
  246. if (vao) real_glBindVertexArray(vao);
  247. }
  248. void glVertexAttribPointer(unsigned i, int size, unsigned type, unsigned char norm,
  249. int stride, const void *ptr) {
  250. RESOLVE(glVertexAttribPointer);
  251. ensure_vao();
  252. if (real_glVertexAttribPointer) real_glVertexAttribPointer(i, size, type, norm, stride, ptr);
  253. }
  254. void glEnableVertexAttribArray(unsigned i) {
  255. RESOLVE(glEnableVertexAttribArray);
  256. ensure_vao();
  257. if (real_glEnableVertexAttribArray) real_glEnableVertexAttribArray(i);
  258. }
  259. void glBindVertexArray(unsigned arr) {
  260. RESOLVE(glBindVertexArray);
  261. if (arr == 0) arr = vao_for_current_context(); /* default VAO -> ours */
  262. if (real_glBindVertexArray) real_glBindVertexArray(arr);
  263. }
  264. static void *our_wrapper(const char *name) {
  265. if (!name) return NULL;
  266. if (!strcmp(name, "glVertexAttribPointer")) return glVertexAttribPointer;
  267. if (!strcmp(name, "glEnableVertexAttribArray")) return glEnableVertexAttribArray;
  268. if (!strcmp(name, "glBindVertexArray")) return glBindVertexArray;
  269. return NULL;
  270. }
  271. void *dlsym(void *handle, const char *name) {
  272. grab_dlsym();
  273. void *w = our_wrapper(name);
  274. return w ? w : (real_dlsym ? real_dlsym(handle, name) : NULL);
  275. }
  276. /* glXGetProcAddress / glXGetProcAddressARB / eglGetProcAddress are the same
  277. * shape; the arg is a name string either way. */
  278. #define PROC_ADDR_SHIM(sym) \
  279. void *sym(const void *name) { \
  280. static void *(*next)(const char *); \
  281. grab_dlsym(); \
  282. if (!next && real_dlsym) next = (void *(*)(const char *))real_dlsym(RTLD_NEXT, #sym); \
  283. void *w = our_wrapper((const char *)name); \
  284. return w ? w : (next ? next((const char *)name) : NULL); \
  285. }
  286. PROC_ADDR_SHIM(glXGetProcAddress)
  287. PROC_ADDR_SHIM(glXGetProcAddressARB)
  288. PROC_ADDR_SHIM(eglGetProcAddress)
  289. /* The hook is preloaded into the whole Proton tree; the game's unix process is
  290. * the one with bakinplayer.exe on its command line. Everything else (wineserver,
  291. * services.exe, the Steam reaper) skips the ntdll splice and the pollers. */
  292. static int is_game_process(void) {
  293. int fd = open("/proc/self/cmdline", O_RDONLY);
  294. if (fd < 0) return 0;
  295. char buf[4096];
  296. ssize_t n = read(fd, buf, sizeof(buf) - 1);
  297. close(fd);
  298. if (n <= 0) return 0;
  299. for (ssize_t i = 0; i < n; i++) if (buf[i] == '\0') buf[i] = ' ';
  300. buf[n] = '\0';
  301. for (char *p = buf; *p; p++)
  302. if ((*p == 'b' || *p == 'B') && strncasecmp(p, "bakin", 5) == 0) return 1;
  303. return 0;
  304. }
  305. __attribute__((constructor))
  306. static void bakin_init(void) {
  307. int game = is_game_process();
  308. if (getenv("BAKIN_HOOK_LOG")) { /* logging is opt-in */
  309. char path[256];
  310. snprintf(path, sizeof(path), LOGPATH_FMT, getpid());
  311. logfile = fopen(path, "w");
  312. }
  313. logln(game ? "[bakin] init (game process)" : "[bakin] init");
  314. if (!game) return; /* other procs keep only the GL interposition, which is free without GL */
  315. pthread_attr_t attr;
  316. pthread_attr_init(&attr);
  317. pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
  318. pthread_t t;
  319. pthread_create(&t, &attr, ntdll_poller, NULL);
  320. pthread_create(&t, &attr, ibl_poller, NULL);
  321. pthread_attr_destroy(&attr);
  322. /* the VAO fix works purely by symbol interposition, no thread needed. */
  323. }