Assetfix: hook CheckFileExistence to serve loose Data\ files. The original flags|1 approach failed because game paths contain backslashes, causing CheckFileExistence to skip BuildFilePath and check the raw path (no Data\ prefix). Fix: write the correct Data\-prefixed disk path to the output buffer directly and return 1, bypassing the original function for hash map hits. This preserves hook chaining (filename argument is never transformed). Also adds transmogfix (transmog update coalescing), minimapicons (stub), new build options for all three modules, mutex-based multi-DLL safety, embed .skin data into .m2 models, and various module improvements.
19 KiB
WoW 1.12.1 GPU Skinning - Hook Specifications
Overview
This document provides detailed specifications for all function hooks required to implement GPU skinning in WoW 1.12.1. All addresses, signatures, and behaviors have been verified through Ghidra decompilation.
Critical Hook: RenderMesh
Function Information
Address: 0x00719ac0
Module: Wow.exe
Calling Convention: __thiscall (ECX = this pointer, stack = parameters)
Purpose: Renders a skinned mesh by performing CPU skinning and drawing
Original Function Signature
undefined* __thiscall RenderMesh(void* this, int meshDataPtr);
Parameters:
this(ECX): Pointer to Model objectmeshDataPtr(stack +0x04): Pointer to MeshData structure
Return Value:
undefined*: Success (0x1) or failure (0x0)
Decompiled Code
undefined * __thiscall RenderMesh(void *this,int param_1)
{
undefined *puVar1;
float *pfVar2;
// Create dynamic vertex buffer (0x28 = 40 bytes per vertex)
puVar1 = (undefined *)CreateVertexBuffer(0, 0x28, (uint)*(ushort *)(param_1 + 6));
// Lock buffer for CPU write
pfVar2 = (float *)LockVertexBuffer(puVar1);
if (pfVar2 == (float *)0x0) {
return (undefined *)0x0;
}
// **CPU SKINNING BOTTLENECK**
applyBoneTransforms((int)this, param_1, pfVar2);
// Upload to GPU
UnlockVertexBuffer((int)puVar1, (undefined *)0x0);
// Draw
DrawPrimitive((int)puVar1, 5); // Type 5 = D3DPT_TRIANGLESTRIP
return (undefined *)0x1;
}
Assembly (First 32 Bytes)
0x00719ac0: 55 PUSH EBP
0x00719ac1: 8B EC MOV EBP, ESP
0x00719ac3: 56 PUSH ESI
0x00719ac4: 8B F1 MOV ESI, ECX ; this -> ESI
0x00719ac6: 57 PUSH EDI
0x00719ac7: 8B 7D 08 MOV EDI, [EBP+0x8] ; meshDataPtr -> EDI
0x00719aca: 0F B7 47 06 MOVZX EAX, word [EDI+0x6] ; vertexCount
0x00719ace: 50 PUSH EAX
0x00719acf: 6A 28 PUSH 0x28 ; vertexSize = 40 bytes
0x00719ad1: 6A 00 PUSH 0x0 ; bufferType = 0
0x00719ad3: E8 68 F6 EE FF CALL 0x0058a140 ; CreateVertexBuffer
Hook Strategy
Type: Detour hook (replace entire function)
Implementation:
typedef void* (__thiscall *RenderMesh_t)(void* thisPtr, int meshDataPtr);
RenderMesh_t g_originalRenderMesh = nullptr;
void* __fastcall RenderMesh_Hook(void* thisPtr, void* /* EDX unused */, int meshDataPtr) {
// GPU skinning implementation
// (see IMPLEMENTATION_PLAN.md for full code)
// 1. Get Model and MeshData pointers
// 2. Check cache for this mesh+pose combination
// 3. If not cached, perform GPU skinning
// 4. Upload bone matrices to shader constants
// 5. Set skinning vertex shader
// 6. Draw from T-pose VB (skinning happens in shader)
return (void*)1;
}
// Install hook using MinHook
MH_CreateHook((LPVOID)0x00719ac0, (LPVOID)&RenderMesh_Hook, (LPVOID*)&g_originalRenderMesh);
MH_EnableHook((LPVOID)0x00719ac0);
Register State at Entry
| Register | Value | Description |
|---|---|---|
| ECX | this | Pointer to Model object |
| EDX | varies | Undefined (not used) |
| [EBP+8] | meshDataPtr | Pointer to MeshData structure |
| ESP | stack | Return address at [ESP] |
Call Graph
Called By:
DrawBatchProj@ 0x0070cb30 (corpses, projected geometry)
Calls:
CreateVertexBuffer@ 0x0058a140LockVertexBuffer@ 0x0058a080applyBoneTransforms@ 0x0071a460 (TARGET FOR ELIMINATION)UnlockVertexBuffer@ 0x0058a0a0DrawPrimitive@ 0x0058a7c0
Hook Impact
Before Hook:
- CPU: 50-70% utilization in skinning
- Frame time: 40-60ms (2000-vertex model, 3 passes)
After Hook:
- CPU: 10-20% utilization (only matrix uploads)
- Frame time: 8-15ms (GPU skinning)
- Expected speedup: 3-5× for skinning operation
Testing Checklist
- Character renders correctly (no visual artifacts)
- Animations play smoothly (no jitter)
- Textures and normals are correct
- Works with LOD system
- Works with mounted characters
- Works with shapeshifted forms
- Corpses render correctly
- NPCs render correctly
- Multiple characters render simultaneously
Supporting Hook: applyBoneTransforms
Function Information
Address: 0x0071a460
Module: Wow.exe
Calling Convention: __fastcall (ECX = param_1, EDX = param_2, stack = param_3)
Purpose: Performs CPU matrix skinning (BOTTLENECK)
Original Function Signature
void __fastcall applyBoneTransforms(int param_1, int param_2, float* param_3);
Parameters:
param_1(ECX): Pointer to Model object (offset +0x00 in Model structure)param_2(EDX): Pointer to MeshData structureparam_3(stack +0x04): Output buffer for skinned vertices
Return Value: None (void)
Critical Offsets (Verified)
Model Structure Offsets (param_1):
+0x30: Pointer to some data structure+0x130(nested): Pointer to geometry data
+0x94: BoneMatrix boneArray* (CRITICAL - bone matrices here)
MeshData Structure Offsets (param_2):
+0x04:uint16_t vertexOffset+0x06:uint16_t vertexCount(loop terminator)
Geometry Data Offsets (nested from Model+0x30+0x130):
+0x48: Pointer to vertex array
Vertex Format (input, T-pose):
+0x00:float[3]position+0x0C:uint8_tblendWeight0 (normalized to 0-1 by multiplying by 0.003921569 = 1/255)+0x0D:uint8_t[4]blendIndices (bone indices 0-255)+0x10:float(blendWeight1 as float, from earlier extraction)+0x14:float[3]normal+0x20:float[2]texcoord0+0x28:float[2]texcoord1
Bone Matrix Format:
- Size: 0x40 bytes (64 bytes)
- Format: 4x4 float matrix (row-major)
- Access:
bones[boneIndex * 0x40]
Decompiled Code (Simplified)
void __fastcall applyBoneTransforms(int param_1, int param_2, float *param_3)
{
int vertexCount = *(short *)(param_2 + 6);
int geometryData = *(int *)(*(int *)(param_1 + 0x30) + 0x130);
float* boneArray = (float*)(*(int*)(param_1 + 0x94));
for (int v = 0; v < vertexCount; v++) {
// Get vertex pointer (stride 0x30 = 48 bytes)
float* vertex = (float*)((v + *(ushort*)(param_2 + 4)) * 0x30 + geometryData + 0x48);
// Accumulate weighted bone transforms
float matrix[12] = {0}; // 4x3 accumulated matrix
// First bone (weight at +0x0C, index at +0x10)
uint8_t boneIndex0 = *(uint8_t*)(vertex + 4) & 0xFF;
float weight0 = *(uint8_t*)(vertex + 3) * 0.003921569; // Normalize byte to 0-1
float* bone0 = &boneArray[boneIndex0 * 0x40 / 4]; // Convert byte offset to float offset
for (int i = 0; i < 12; i++) {
matrix[i] = weight0 * bone0[i];
}
// Additional bones (up to 4 total)
for (int b = 1; b < 4; b++) {
uint8_t boneIndex = *(uint8_t*)((char*)vertex + 0x0D + b);
if (boneIndex == 0) break;
float weight = *(uint8_t*)((char*)vertex + 0x0C + b) * 0.003921569;
float* bone = &boneArray[boneIndex * 0x40 / 4];
for (int i = 0; i < 12; i++) {
matrix[i] += weight * bone[i];
}
}
// Transform position
param_3[v * 10 + 0] = matrix[0] * vertex[0] + matrix[1] * vertex[1] + matrix[2] * vertex[2] + matrix[3];
param_3[v * 10 + 1] = matrix[4] * vertex[0] + matrix[5] * vertex[1] + matrix[6] * vertex[2] + matrix[7];
param_3[v * 10 + 2] = matrix[8] * vertex[0] + matrix[9] * vertex[1] + matrix[10] * vertex[2] + matrix[11];
// Transform normal (3x3 part of matrix)
float* normal = &vertex[5]; // Normal at +0x14 (5 floats from start)
param_3[v * 10 + 3] = matrix[0] * normal[0] + matrix[1] * normal[1] + matrix[2] * normal[2];
param_3[v * 10 + 4] = matrix[4] * normal[0] + matrix[5] * normal[1] + matrix[6] * normal[2];
param_3[v * 10 + 5] = matrix[8] * normal[0] + matrix[9] * normal[1] + matrix[10] * normal[2];
// Copy texture coordinates (unskinned)
param_3[v * 10 + 6] = vertex[8]; // texcoord0.x
param_3[v * 10 + 7] = vertex[9]; // texcoord0.y
param_3[v * 10 + 8] = vertex[10]; // texcoord1.x
param_3[v * 10 + 9] = vertex[11]; // texcoord1.y
}
}
Assembly (First 64 Bytes)
0x0071a460: 55 PUSH EBP
0x0071a461: 8B EC MOV EBP, ESP
0x0071a463: 83 EC 5C SUB ESP, 0x5C ; Stack frame
0x0071a466: D9 05 D8 F9 7F 00 FLD dword [0x7ff9d8] ; Load 1.0
0x0071a46c: 8B 41 30 MOV EAX, [ECX+0x30] ; this+0x30
0x0071a46f: D9 05 74 FD 7F 00 FLD dword [0x7ffd74] ; Load 0.0
0x0071a475: 8B 80 30 01 00 00 MOV EAX, [EAX+0x130] ; nested+0x130
0x0071a47b: D9 05 74 FD 7F 00 FLD dword [0x7ffd74] ; Load 0.0
0x0071a481: 56 PUSH ESI
0x0071a482: D9 05 74 FD 7F 00 FLD dword [0x7ffd74] ; Load 0.0
0x0071a488: 33 F6 XOR ESI, ESI ; vertexIndex = 0
0x0071a48a: 66 39 72 06 CMP [EDX+0x6], SI ; Compare vertexCount
0x0071a48e: D9 05 D8 F9 7F 00 FLD dword [0x7ff9d8] ; Load 1.0
0x0071a494: D9 05 74 FD 7F 00 FLD dword [0x7ffd74] ; Load 0.0
0x0071a49a: 89 55 F4 MOV [EBP-0xC], EDX ; Save meshDataPtr
0x0071a49d: 89 4D EC MOV [EBP-0x14], ECX ; Save this
0x0071a4a0: 89 45 E8 MOV [EBP-0x18], EAX ; Save geometryData
Performance Analysis
Per-Vertex Cost:
- Load vertex data: ~10 cycles
- Load 1-4 bone matrices: 64-256 bytes (4-16 cache lines)
- Matrix accumulation: 48-192 FP ops (12-48 muls + 12-48 adds per bone)
- Transform position: 16 FP ops (4 muls + 3 adds per component)
- Transform normal: 12 FP ops
- Total: ~200-500 CPU cycles per vertex
2000-Vertex Model:
- Total: 400,000 - 1,000,000 cycles
- At 3 GHz CPU: 0.13-0.33ms per model (best case)
- With cache misses: 2-5ms per model (realistic)
- ×3 render passes: 6-15ms per frame per character
40-Man Raid:
- 40 characters × 6-15ms = 240-600ms per frame
- Frame rate: 1.6-4 FPS (CPU-bound!)
Hook Strategy
Type: Not directly hooked (replaced by GPU implementation in RenderMesh hook)
Alternative: Could hook to replace with SIMD-optimized version if staying CPU-side:
typedef void (__fastcall *applyBoneTransforms_t)(int param_1, int param_2, float* param_3);
applyBoneTransforms_t g_originalApplyBoneTransforms = nullptr;
void __fastcall applyBoneTransforms_SSE2(int param_1, int param_2, float* param_3) {
// SSE2-optimized skinning (see IMPLEMENTATION_PLAN.md)
// Expected speedup: 2-3× vs. scalar
}
MH_CreateHook((LPVOID)0x0071a460, (LPVOID)&applyBoneTransforms_SSE2,
(LPVOID*)&g_originalApplyBoneTransforms);
Recommended: Do NOT hook this function directly. Instead, bypass it entirely by hooking RenderMesh.
Optional Hook: CreateVertexBuffer
Function Information
Address: 0x0058a140
Purpose: Allocates dynamic vertex buffer (currently creates new VB every frame)
Original Function Signature
void* __fastcall CreateVertexBuffer(int bufferType, int vertexSize, int vertexCount);
Decompiled Code
void __fastcall CreateVertexBuffer(int bufferType, int vertexSize, int vertexCount)
{
D3D_CreateVertexBuffer(CGxDeviceD3d__device, bufferType, vertexSize, vertexCount);
return;
}
Hook Strategy
Purpose: Implement vertex buffer pooling to reduce allocation overhead
Type: Detour hook
Implementation:
// VB Pool
struct VBPoolEntry {
IDirect3DVertexBuffer9* vb;
uint32_t size;
bool inUse;
};
std::vector<VBPoolEntry> g_vbPool;
void* __fastcall CreateVertexBuffer_Hook(void* /* EDX unused */, int bufferType,
int vertexSize, int vertexCount) {
uint32_t requestedSize = vertexSize * vertexCount;
// Check pool for reusable buffer
for (auto& entry : g_vbPool) {
if (!entry.inUse && entry.size >= requestedSize) {
entry.inUse = true;
return entry.vb;
}
}
// No suitable buffer, create new one
IDirect3DVertexBuffer9* vb = nullptr;
IDirect3DDevice9* device = *(IDirect3DDevice9**)0x00c0ed38;
HRESULT hr = device->CreateVertexBuffer(requestedSize, D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY,
0, D3DPOOL_DEFAULT, &vb, nullptr);
if (SUCCEEDED(hr)) {
g_vbPool.push_back({vb, requestedSize, true});
return vb;
}
// Fallback to original
return g_originalCreateVertexBuffer(bufferType, vertexSize, vertexCount);
}
Expected Gain: +5-10% FPS (reduced allocation overhead)
Optional Hook: LockVertexBuffer
Function Information
Address: 0x0058a080
Purpose: Maps vertex buffer for CPU write
Original Function Signature
float* __fastcall LockVertexBuffer(void* vb);
Decompiled Code
void __fastcall LockVertexBuffer(undefined *param_1)
{
// Call D3D device method at offset 0xa8 (IDirect3DVertexBuffer9::Lock)
(**(code **)(*(int *)CGxDeviceD3d__device + 0xa8))(param_1);
return;
}
Hook Strategy
Purpose: Detect skinning pattern for DXVK interception
Type: Inline hook (record call pattern)
Implementation:
enum OpType { OpLockVB, OpUnlockVB, OpDraw };
struct OpRecord {
OpType type;
void* vb;
uint32_t timestamp;
};
std::deque<OpRecord> g_recentOps;
float* __fastcall LockVertexBuffer_Hook(void* /* EDX unused */, void* vb) {
g_recentOps.push_back({OpLockVB, vb, GetTickCount()});
if (g_recentOps.size() > 10) g_recentOps.pop_front();
return g_originalLockVertexBuffer(vb);
}
// Pattern detector (called before draw)
bool IsCPUSkinningPattern() {
if (g_recentOps.size() < 3) return false;
return g_recentOps[g_recentOps.size()-3].type == OpLockVB &&
g_recentOps[g_recentOps.size()-2].type == OpUnlockVB &&
g_recentOps[g_recentOps.size()-1].type == OpDraw;
}
Use Case: Custom DXVK fork that automatically detects and replaces CPU skinning
Frame Start Hook
Function Information
Address: TBD (need to find via Ghidra or use Present hook) Purpose: Update frame counter for cache invalidation
Hook Strategy
Type: Hook IDirect3DDevice9::Present or WoW's frame update function
Implementation:
typedef HRESULT (__stdcall *Present_t)(IDirect3DDevice9* device, const RECT* pSourceRect,
const RECT* pDestRect, HWND hDestWindowOverride,
const RGNDATA* pDirtyRegion);
Present_t g_originalPresent = nullptr;
HRESULT __stdcall Present_Hook(IDirect3DDevice9* device, const RECT* pSourceRect,
const RECT* pDestRect, HWND hDestWindowOverride,
const RGNDATA* pDirtyRegion) {
// Frame has ended, increment counter
g_currentFrame++;
// Clean up old cache entries
OnFrameStart();
return g_originalPresent(device, pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion);
}
// Install by hooking device vtable
void HookPresent(IDirect3DDevice9* device) {
void** vtable = *(void***)device;
MH_CreateHook(vtable[17], (LPVOID)&Present_Hook, (LPVOID*)&g_originalPresent);
MH_EnableHook(vtable[17]);
}
Global Data Pointers
CGxDeviceD3d__device
Address: 0x00c0ed38
Type: IDirect3DDevice9** (pointer to pointer)
Purpose: Global D3D9 device pointer
Usage:
IDirect3DDevice9** g_devicePtr = (IDirect3DDevice9**)0x00c0ed38;
IDirect3DDevice9* device = *g_devicePtr;
// Now can call device methods
device->SetVertexShader(...);
Verification
How to verify this address is correct:
- Set breakpoint at RenderMesh @ 0x00719ac0
- Step through until D3D device is accessed
- Check memory at 0x00c0ed38
- Should point to valid IDirect3DDevice9 vtable
Hook Installation Order
Recommended Order:
-
Initialize MinHook
MH_Initialize(); -
Hook Present (for frame counter)
HookPresent(device); -
Hook CreateVertexBuffer (optional, for pooling)
MH_CreateHook((LPVOID)0x0058a140, ...); -
Hook RenderMesh (critical)
MH_CreateHook((LPVOID)0x00719ac0, ...); -
Enable All Hooks
MH_EnableHook(MH_ALL_HOOKS);
Debugging Hooks
Verification Steps
-
Check Hook Installation:
MH_STATUS status = MH_CreateHook(...); if (status != MH_OK) { OutputDebugStringA("Hook failed: %d\n", status); } -
Log Hook Calls:
void* __fastcall RenderMesh_Hook(...) { static int callCount = 0; char buf[256]; sprintf(buf, "RenderMesh_Hook called: %d times\n", ++callCount); OutputDebugStringA(buf); // Your code here } -
Verify D3D Device:
IDirect3DDevice9* device = *(IDirect3DDevice9**)0x00c0ed38; if (!device || IsBadReadPtr(device, sizeof(void*))) { OutputDebugStringA("Invalid D3D device pointer!\n"); } -
Test Fallback Path:
void* __fastcall RenderMesh_Hook(...) { static bool gpuSkinningEnabled = true; // Toggle with hotkey for testing if (GetAsyncKeyState(VK_F9) & 0x8000) { gpuSkinningEnabled = !gpuSkinningEnabled; } if (!gpuSkinningEnabled) { return g_originalRenderMesh(thisPtr, meshDataPtr); // Fallback } // GPU skinning code }
Safety and Anti-Cheat Considerations
Warden Detection Avoidance
-
Don't modify .text section
- Use MinHook which allocates trampoline in new memory
- Don't patch bytes directly in Wow.exe
-
Don't scan for known patterns
- Warden may scan for common hooking libraries
- Use legitimate hooking (MinHook is generally safe)
-
Don't modify game data
- Only intercept rendering path
- Don't modify player positions, stats, etc.
-
Be reversible
- Allow disabling GPU skinning at runtime
- Provide fallback to original behavior
Crash Prevention
-
Validate All Pointers
if (!device || IsBadReadPtr(device, sizeof(void*))) { return g_originalRenderMesh(thisPtr, meshDataPtr); } -
Use SEH (Structured Exception Handling)
__try { // GPU skinning code } __except(EXCEPTION_EXECUTE_HANDLER) { OutputDebugStringA("Exception in GPU skinning, falling back\n"); return g_originalRenderMesh(thisPtr, meshDataPtr); } -
Test on Multiple GPUs
- Intel integrated
- NVIDIA discrete
- AMD discrete
Summary
Critical Hooks
| Function | Address | Priority | Difficulty |
|---|---|---|---|
| RenderMesh | 0x00719ac0 | CRITICAL | Medium |
| Present (frame counter) | Vtable[17] | High | Easy |
Optional Hooks
| Function | Address | Priority | Difficulty |
|---|---|---|---|
| CreateVertexBuffer | 0x0058a140 | Low | Easy |
| LockVertexBuffer | 0x0058a080 | Low | Easy |
| applyBoneTransforms | 0x0071a460 | Low (bypass) | N/A |
Success Criteria
- RenderMesh hook installs without crashes
- Characters render identically to CPU skinning
- FPS improves by 40-80% in crowded areas
- No Warden detection or bans
- Stable for 10+ hours of gameplay
Document Version: 1.0 Last Updated: 2025-12-04 All addresses verified: Ghidra decompilation of Wow.exe (1.12.1)