# Technical write-up Reverse-engineering notes behind each fix. Addresses are for the shipped v1.1.0 build: `kmyCore.dll` = 5,423,536 bytes, md5 `68590231c9418eeab8b9d70203e1f08d`. ## Architecture - `shina.exe` is a 32-bit launcher. The real game is `data/bakinplayer.exe` (x64, .NET/`Yukar.Player`), which loads the native engine `data/kmyCore.dll`. - Assets live in `data/data.rbpack` (a packed archive) plus loose files under `data/` (models, shader *source* in `data/lib/sysresource/shader/`). - Asset loaders are `.dlp` plugins (`FBXLoader`, `PNGLoader`, `BMPLoader`, `HDRLoader`, `OGGLoader`, `WAVLoader`, `BulletPhysics`): PE32+ DLLs the engine `LoadLibrary`s at startup after a `scanPlugin` sweep. ## Fix 1: plugin filesystem (grey screen) `kmyPlugin::PluginMgr::scanPlugin` (RVA `0xD1700`) builds a search dir and calls `kmyIO::FS::newFS(type=0, path=NULL)` (RVA `0xAA9B0`), then enumerates `*.dlp` through the FS vtable. In packaged mode `newFS(0,...)` returns the in-memory `data.rbpack` filesystem, which does not contain the `.dlp` files, so the scan finds zero loaders and no models/textures/audio ever load. The result is an empty scene (grey). Under `WINEDEBUG=+file` there is no `*.dlp` access at all. The disk-FS creator lives at RVA `0xAAA10` and safely handles a NULL path. The `call` at file offset `0xD0B53` (bytes `e8 58 92 fd ff`) encodes its target in a rel32 displacement whose low byte, at file offset `0xD0B54`, is `0x58`; changing it to `0xB8` retargets `0xAA9B0 -> 0xAAA10`. One byte. Steam does not re-validate `kmyCore.dll`, so the change persists. ## Fix 2: plugin path (grey screen, cont.) Even with fix 1, `scanPlugin` needs a valid directory. The managed side calls: ``` Yukar.Player.Program ... -> SharpKmy.Entry.initialize("", null) // pluginpath = null ``` With `pluginpath == NULL`, native `scanPlugin` falls back to `GetModuleFileNameW(NULL)` to locate the exe dir, which under Wine does not resolve to the on-disk `data/` dir where the `.dlp` live. The IL rewrite (`patches/02-pluginpath/Program.cs`, Mono.Cecil) replaces the `ldnull` before the 2-arg `Entry.initialize` with `AppDomain.CurrentDomain.BaseDirectory`, so `scanPlugin` takes its explicit-path branch and finds the loaders. Fixes 1 and 2 are both required. ## Fix 3: vertex attributes (white screen / freeze) The engine queries attribute locations by GLSL name, e.g. `glGetAttribLocation(prog, "vin_position")` / `"vin_uv0"`. Mesa's GLSL linker runs cross-stage dead-code elimination: any vertex input not statically used in a way that survives optimization is marked inactive and its location becomes -1. The engine treats -1 as a fatal "Vertex Attribute Problem", refuses the draw, and after enough failures aborts the frame loop (`KMY MAIN LOOP BREAK BY NO TASK`), leaving a white screen. `data/lib/sysresource/shader/include/v2_vpcommon.cgh` is the common vertex-program include (the game copies `data/lib` to a temp dir at launch and compiles from there, so editing the on-disk source is picked up). The fix adds `VIN_KEEPALIVE`, a term folded into `gl_Position` that references every declared `vin_*` input, each guarded by the same `#ifdef` as its declaration and multiplied by `float(drawCount == -9999)`. `drawCount` is a uniform Mesa cannot constant-fold, and it is always `>= 0` at runtime, so the term is exactly `0.0`: attributes stay "used" (never DCE'd) with zero effect on output. This drops the Vertex Attribute Problem count from 200+ to 0 on the title screen and the vast majority of map models. The `_USE_GPROGRAM` path is intentionally left alone; geometry programs re-emit position downstream. ## Fix 4a: the default VAO in a core profile (invisible characters and menus) This is the fix that makes the 2D layer appear, and it was the hardest to find. Symptom: the 3D world renders, but the player/NPC character billboards (`kmyGfx::BillboardChr`, `2dchar_lit` shader), the title and in-game menus, and the 2D UI are completely invisible. Diagnosis: instrumenting the character's draw with an `LD_PRELOAD` GL hook showed its draw call (`glDrawArraysInstanced`) executing on the same thread, to the same framebuffer (same physical color/depth textures), with identical GL state to the visible 3D models (depth func, blend, cull, stencil, scissor, rasterizer-discard, viewport, color mask, draw buffers) and a valid compiled and linked program. Forcing the character's shader to output solid magenta at the near plane still produced nothing. Calling `glGetError()` immediately after the character's draw returned `GL_INVALID_OPERATION` (`0x502`), and `glDebugMessageCallback` gave the exact reason: ``` GL_INVALID_OPERATION in glVertexAttribPointer(no array object bound) ``` Root cause: the engine sets up its 2D-sprite/billboard/UI vertex attributes against the default vertex array object (name 0). That is legal in an OpenGL compatibility profile (what the game targets on Windows), but Wine/Proton give the process a core profile, where the default VAO does not exist. So `glVertexAttribPointer` fails, the attributes are never established, and every draw that relies on the default VAO is rejected by the driver with `GL_INVALID_OPERATION` and never rasterizes. The 3D models load their own VAOs from FBX data, so they were unaffected, which is exactly why only the 2D layer vanished. Fix: in the `LD_PRELOAD` hook, interpose `glVertexAttribPointer`, `glEnableVertexAttribArray`, and `glBindVertexArray` (resolution reaches them via the interposed `dlsym` / `glX*GetProcAddress`). `ensure_vao()` queries `GL_VERTEX_ARRAY_BINDING`; if it is 0, it lazily `glGenVertexArrays` one persistent VAO and binds it. `glBindVertexArray(0)` from the game is redirected to that same VAO. Whenever the game believes it is on the default VAO, a real VAO is actually bound, so the attribute setup and the draws are valid. Confirmed: the character's `glDrawArraysInstanced` then returns `glGetError() == 0` and the character, menus and UI render. ## Fix 4b: audio deadlock (freeze after map load) `kmySound` init fails under Wine's WASAPI with `SndError 923` (no matching device format; non-fatal to the engine, which is supposed to continue). But a game thread then blocks in `NtWaitForSingleObject(event, INFINITE)` on an "audio ready" event the failed audio thread never signals, and all threads idle at 0% CPU. `bakin_minimal.c` (`LD_PRELOAD`) splices ntdll's `NtWaitForSingleObject` and substitutes a 10 s timeout for INFINITE waits, returning `STATUS_SUCCESS` on expiry. Real waits (vsync fences, message events) complete far under 10 s and are untouched. ntdll is mapped by Wine's preloader after our constructor runs, so a poller retries the splice every 10 ms until ntdll appears. Note: do not also patch `NtWaitForAlertByThreadId`. Bypassing its INFINITE waits corrupts the render thread's synchronization and produces a blank/black screen. `NtWaitForSingleObject` only is the right scope. ## Fix 5: Wayland keyboard focus On KDE Plasma Wayland the game runs as an X11 client via Xwayland. Its top window advertises the ICCCM "Globally Active" input model (`WM_HINTS: input = False` + `WM_PROTOCOLS: WM_TAKE_FOCUS`), Wine/WinForms' default. The compositor never grants it Wayland keyboard focus (mouse is position-based so it works; keyboard is focus-based so it doesn't). Neither focus-follows-mouse, `_NET_ACTIVE_WINDOW`, nor `XSetInputFocus` delivered keys. Setting `HKCU\Software\Wine\X11 Driver` -> `"UseTakeFocus"="N"` disables the take-focus model; the window then advertises the passive model (`input=True`) and the compositor grants keyboard focus. Harmless on native X11. Must be written with the prefix's wineserver stopped so it sticks. ## Environment notes - `WINEDLLOVERRIDES="WebView2Loader="`: the bundled WebView2Loader hangs Wine. - `mesa_glthread=false`: avoids a NULL-vtable crash in Mesa GL worker threads. - `WINE_DISABLE_FULLSCREEN_HACK=1`: Wine's fullscreen-hack gamma shader fails to compile here and crashes early. - Benign: `GfxError 1282` on editor "pickup"/manipulator shader compiles; does not affect gameplay.