Reverse-engineering notes behind each fix. Addresses are for the shipped v1.1.0
build: kmyCore.dll = 5,423,536 bytes, md5 68590231c9418eeab8b9d70203e1f08d.
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.data/data.rbpack (a packed archive) plus loose files under
data/ (models, shader source in data/lib/sysresource/shader/)..dlp plugins (FBXLoader, PNGLoader, BMPLoader,
HDRLoader, OGGLoader, WAVLoader, BulletPhysics): PE32+ DLLs the
engine LoadLibrarys at startup after a scanPlugin sweep.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.
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.
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.
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.
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.
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.
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.GfxError 1282 on editor "pickup"/manipulator shader compiles; does
not affect gameplay.