TECHNICAL.md 7.9 KB

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 LoadLibrarys 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.