mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-02 16:28:13 -07:00
145 lines
5.5 KiB
C++
145 lines
5.5 KiB
C++
#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 <cstdint>
|
|
#include <unordered_set>
|
|
|
|
#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<void *> 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<const uint8_t *>(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
|