#include "d3d9_live2d.h" // only the Live2D-capable SDVX versions are 64-bit, so the entire implementation // is compiled out of 32-bit builds (the header supplies inline no-op stubs there). #ifdef SPICE64 #include #include #include "hooks/graphics/graphics.h" // how the Live2D draw filtering works // ------------------------------------ // SDVX draws its Live2D characters with a small, fixed set of pixel and // vertex shaders. to skip those draws (and save GPU) we have to recognise them at // the exact moment the game issues a draw call. the d3d9 device hooks feed three // kinds of events into this module: // // 1. shader creation (on_create_pixel_shader / on_create_vertex_shader) // the game compiles its shaders once at load. we can't trust the shader // *object pointer* to identify a shader (it's just a heap address that // varies per run and can be recycled), so instead we hash the shader's // D3D9 *bytecode* - that fingerprint is stable across runs because the // game ships the same shaders. if the hash matches a known Live2D shader // we remember that object pointer in g_live2d_shaders. // // 2. shader binding (on_set_pixel_shader / on_set_vertex_shader) // whenever the game binds a shader we look it up in that set once and cache // the yes/no answer in g_cur_ps_is_live2d / g_cur_vs_is_live2d. binds happen // far less often than draws, so this is where the lookup cost lives. // // 3. draw call (should_skip_draw, called from every Draw* hook) // the per-draw question "is this a Live2D draw?" is then just reading those // two cached bools - no hashing, no map lookups. if the skip is currently // active (see graphics_sdvx_live2d_should_skip) and either bound shader is // Live2D, the Draw* hook drops the call instead of forwarding it. // // everything is gated on the feature being enabled (mode != Off); when it's Off // every entry point is a single predicted-not-taken branch. d3d9 rendering for a // device is single-threaded, so none of this state needs locking. namespace { // shader state is tracked whenever the feature might act (mode != Off) so the // known-shader set is populated before a song starts. when Off, every entry // point is a single cheap branch. bool tracking_enabled() { return GRAPHICS_SDVX_LIVE2D_MODE != SdvxLive2dMode::Off; } // the set of shader objects (pixel or vertex) whose bytecode matched a known // Live2D fingerprint. only matching shaders are stored, so this stays tiny. std::unordered_set g_live2d_shaders; // whether the currently-bound shaders are known Live2D shaders. cached at set // time so the per-draw check is just two bool reads. bool g_cur_ps_is_live2d = false; bool g_cur_vs_is_live2d = false; // FNV-1a 64 over a D3D9 shader token stream (ends with D3DSIO_END = 0x0000FFFF) uint64_t bytecode_hash(const DWORD *func) { if (func == nullptr) { return 0; } const DWORD *p = func; const DWORD *cap = func + 65536; // safety bound while (p < cap && *p != 0x0000FFFF) { p++; } const size_t n_bytes = ((size_t)(p - func) + 1) * sizeof(DWORD); uint64_t h = 1469598103934665603ULL; const auto *bytes = reinterpret_cast(func); for (size_t i = 0; i < n_bytes; i++) { h ^= bytes[i]; h *= 1099511628211ULL; } return h; } // known SDVX Live2D shader bytecode hashes (4 pixel + 3 vertex). stable // across runs because the game ships fixed shaders. the two sets are disjoint so // a single shader can be classified by its own hash alone. bool hash_is_live2d(uint64_t hash) { switch (hash) { case 0x75c89951817421a4ULL: // pixel: dominant model draw (~4.9M prims/120f in-song) case 0x2d7ce428c6b4775dULL: // pixel: masked model draw case 0x3ce00cc6111c10e7ULL: // pixel: mask generation case 0x8bb3a2f37150ac34ULL: // pixel: mask generation (variant) case 0xe9cf898c331e2a51ULL: // vertex case 0x94dc84e7b7c0f437ULL: // vertex case 0xc872937c5cc04309ULL: // vertex return true; } return false; } // classify a shader at creation time and record it if it is Live2D. erasing on a // miss keeps the set correct if the runtime reuses a freed shader pointer. void classify_shader(void *shader, const DWORD *func) { if (hash_is_live2d(bytecode_hash(func))) { g_live2d_shaders.insert(shader); } else { g_live2d_shaders.erase(shader); } } } // namespace namespace d3d9_live2d { // stage 1: fingerprint each shader as the game creates it void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func) { if (tracking_enabled() && shader != nullptr) [[unlikely]] { classify_shader(shader, func); } } void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func) { if (tracking_enabled() && shader != nullptr) [[unlikely]] { classify_shader(shader, func); } } // stage 2: remember whether the just-bound shader is a Live2D one void on_set_vertex_shader(IDirect3DVertexShader9 *shader) { if (tracking_enabled()) [[unlikely]] { g_cur_vs_is_live2d = g_live2d_shaders.count(shader) != 0; } } void on_set_pixel_shader(IDirect3DPixelShader9 *shader) { if (tracking_enabled()) [[unlikely]] { g_cur_ps_is_live2d = g_live2d_shaders.count(shader) != 0; } } // stage 3: drop the draw if the skip is active and a Live2D shader is bound bool should_skip_draw() { return graphics_sdvx_live2d_should_skip() && (g_cur_ps_is_live2d || g_cur_vs_is_live2d); } } // namespace d3d9_live2d #endif // SPICE64