perf: libdeflate inflate timing hook — runs both, compares results

Hooks DecompressData_WithOptions (0x661A80) to run both the original
zlib inflate and libdeflate on the same data. Times both, logs the
comparison every ~450 frames, counts mismatches.

malloc/free provided via game's Storm memory manager (SMemAlloc/SMemFree)
since we link without libc on the msvc target.

libdeflate compiled with -g0 to avoid .debug_frame COFF section name
warning. AVX-512 disabled (32-bit), SSE2/AVX2 paths active.
This commit is contained in:
MarcelineVQ
2026-03-24 03:01:15 -07:00
parent fb27ff54a0
commit 59ef452f22
3 changed files with 203 additions and 1 deletions
+2 -1
View File
@@ -178,7 +178,8 @@ pub fn build(b: *std.Build) void {
},
// Disable AVX-512 codepaths — 512-bit intrinsics require evex512 which
// isn't available on 32-bit x86. AVX/AVX2/SSE paths remain active.
.flags = &.{"-DLIBDEFLATE_ASSEMBLER_DOES_NOT_SUPPORT_AVX512VNNI"},
// -g0: no debug info (avoids .debug_frame section name warning in COFF linker)
.flags = &.{ "-DLIBDEFLATE_ASSEMBLER_DOES_NOT_SUPPORT_AVX512VNNI", "-g0" },
});
libdeflate.root_module.addIncludePath(b.path("src/performance/libdeflate"));
libdeflate.root_module.addIncludePath(b.path("src/performance/libdeflate/lib"));
+172
View File
@@ -0,0 +1,172 @@
//! inflate_hook — libdeflate timing comparison hook for WoW's inflateStateMachine.
//!
//! Hooks BZip2Decompressor_Decompress (0x660740) which receives complete
//! compressed buffers. Runs both original and libdeflate, times both,
//! logs the comparison.
const hook_lib = @import("zhook");
const logging = @import("../logging.zig");
const FC: std.builtin.CallingConvention = .{ .x86_fastcall = .{} };
const SC: std.builtin.CallingConvention = .{ .x86_stdcall = .{} };
const std = @import("std");
// libdeflate C API (linked from static lib)
extern fn libdeflate_alloc_decompressor() ?*anyopaque;
extern fn libdeflate_free_decompressor(?*anyopaque) void;
extern fn libdeflate_zlib_decompress(
decompressor: ?*anyopaque,
in_ptr: [*]const u8,
in_len: usize,
out_ptr: [*]u8,
out_len: usize,
actual_out: *usize,
) c_int;
extern fn libdeflate_deflate_decompress(
decompressor: ?*anyopaque,
in_ptr: [*]const u8,
in_len: usize,
out_ptr: [*]u8,
out_len: usize,
actual_out: *usize,
) c_int;
var decompressor: ?*anyopaque = null;
var log: logging.Logger = .{};
// Timing accumulators
var orig_total_cycles: u64 = 0;
var fast_total_cycles: u64 = 0;
var call_count: u64 = 0;
var mismatch_count: u64 = 0;
var total_bytes: u64 = 0;
inline fn rdtsc() u64 {
var lo: u32 = undefined;
var hi: u32 = undefined;
asm volatile ("rdtsc"
: [lo] "={eax}" (lo),
[hi] "={edx}" (hi),
);
return (@as(u64, hi) << 32) | lo;
}
// =============================================================================
// Hook: DecompressData_WithOptions (0x661A80)
// __cdecl(outBuf, &outSize, inBuf, &inSize, flags) → returns ptr (0=fail, 1=ok)
//
// This is the top-level decompression dispatcher. It reads the first byte
// as a compression type bitmask and dispatches to the appropriate decompressor.
// We intercept here to run libdeflate on the same data for comparison.
// =============================================================================
const DecompressFn = fn (u32, u32, u32, u32, u32) callconv(SC) u32;
pub var decompress_hook: hook_lib.Detour(DecompressFn) = .{};
pub fn decompressDetour(out_buf: u32, out_size_ptr: u32, in_buf: u32, in_size_ptr: u32, flags: u32) callconv(SC) u32 {
// Read input/output sizes
const in_size = @as(*const u32, @ptrFromInt(in_size_ptr)).*;
const out_size = @as(*const u32, @ptrFromInt(out_size_ptr)).*;
const in_ptr: [*]const u8 = @ptrFromInt(in_buf);
// Run original first (this is the authoritative result)
const t0 = rdtsc();
const ret = decompress_hook.callOriginal(.{ out_buf, out_size_ptr, in_buf, in_size_ptr, flags });
const orig_cycles = rdtsc() - t0;
// Only compare if original succeeded, we have a decompressor, and buffer is non-trivial
if (ret != 0 and decompressor != null and in_size > 16 and out_size > 0) {
// Read the compression type byte — first byte of compressed data
const comp_type = in_ptr[0];
// Only test zlib/deflate streams (type byte has bit patterns for different compressors)
// The actual data starts at byte 1 (after the type byte)
// For now, test on all calls regardless of type — libdeflate handles raw deflate
_ = comp_type;
// Allocate temp buffer for libdeflate output
const actual_out_size = @as(*const u32, @ptrFromInt(out_size_ptr)).*;
if (actual_out_size > 0 and actual_out_size < 4 * 1024 * 1024) {
// Use VirtualAlloc or stack for small buffers
var fast_buf: [65536]u8 = undefined;
const use_buf: [*]u8 = if (actual_out_size <= 65536) &fast_buf else return ret;
var actual_out: usize = 0;
const t1 = rdtsc();
const ld_ret = libdeflate_deflate_decompress(
decompressor,
in_ptr + 1, // skip type byte
in_size - 1,
use_buf,
actual_out_size,
&actual_out,
);
const fast_cycles = rdtsc() - t1;
orig_total_cycles +|= orig_cycles;
fast_total_cycles +|= fast_cycles;
call_count +|= 1;
total_bytes +|= actual_out_size;
// Check if libdeflate succeeded and produced same output
if (ld_ret == 0 and actual_out == actual_out_size) {
// Compare outputs
const orig_out: [*]const u8 = @ptrFromInt(out_buf);
var match = true;
for (0..actual_out_size) |i| {
if (orig_out[i] != use_buf[i]) {
match = false;
break;
}
}
if (!match) mismatch_count +|= 1;
}
}
} else {
orig_total_cycles +|= orig_cycles;
call_count +|= 1;
}
return ret;
}
pub fn dumpStats() void {
if (call_count == 0) return;
const MS_DIV: u64 = 3_000_000;
log.fmt("inflate: {d} calls, {d}MB, orig={d}ms fast={d}ms ({d} mismatches)\n", .{
call_count,
total_bytes / (1024 * 1024),
orig_total_cycles / MS_DIV,
fast_total_cycles / MS_DIV,
mismatch_count,
});
// Reset
orig_total_cycles = 0;
fast_total_cycles = 0;
call_count = 0;
mismatch_count = 0;
total_bytes = 0;
}
pub fn install(logger: logging.Logger) bool {
log = logger;
decompressor = libdeflate_alloc_decompressor();
if (decompressor == null) {
log.print("inflate_hook: failed to allocate libdeflate decompressor\n");
return false;
}
if (decompress_hook.attach(0x661A80, &decompressDetour) == .ok) {
log.print("inflate_hook: hooked DecompressData_WithOptions\n");
return true;
}
log.print("inflate_hook: failed to hook\n");
return false;
}
pub fn remove() void {
decompress_hook.detach();
if (decompressor) |d| {
libdeflate_free_decompressor(d);
decompressor = null;
}
}
+29
View File
@@ -16,9 +16,25 @@
const hook = @import("zhook");
const logging = @import("../logging.zig");
const mod_mutex = @import("../mutex.zig");
const inflate_hook = @import("inflate_hook.zig");
pub const module_name: [*:0]const u8 = "performance";
// Provide malloc/free for libdeflate's default allocator (linked without libc).
// Use game's own SMemAlloc/SMemFree (Storm memory manager) which are always available.
// SMemAlloc at 0x6464B0: __stdcall(size, filename_str, line, flags) → ptr
// SMemFree at 0x646430: __stdcall(ptr, filename_str, flags) → void
const gameMalloc: *const fn (u32, u32, u32, u32) callconv(.{ .x86_stdcall = .{} }) ?*anyopaque = @ptrFromInt(0x6464B0);
const gameFree: *const fn (u32, u32, u32) callconv(.{ .x86_stdcall = .{} }) void = @ptrFromInt(0x646430);
export fn malloc(size: usize) callconv(.c) ?*anyopaque {
return gameMalloc(@intCast(size), 0, 0, 0);
}
export fn free(ptr: ?*anyopaque) callconv(.c) void {
if (ptr) |p| gameFree(@intFromPtr(p), 0, 0);
}
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
var log: logging.Logger = .{};
@@ -123,9 +139,17 @@ fn glyphDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyo
const WorldUpdateFn = fn (u32) callconv(hook.cc.fastcall) void;
var world_update_hook: hook.Detour(WorldUpdateFn) = .{};
var frame_counter: u32 = 0;
fn worldUpdateDetour(frame_count: u32) callconv(hook.cc.fastcall) void {
resetParticleCache();
world_update_hook.callOriginal(.{frame_count});
// Dump inflate stats every ~450 frames (~7.5s at 60fps)
frame_counter +|= 1;
if (frame_counter >= 450) {
inflate_hook.dumpStats();
frame_counter = 0;
}
}
// =============================================================================
@@ -257,11 +281,16 @@ pub fn installHooks() void {
// Silicon SSE binary patches
const patched = installPatches();
// libdeflate inflate comparison hook
if (inflate_hook.install(log)) installed += 1;
log.fmt("performance: {d} hooks, {d} patches installed\n", .{ installed, patched });
}
pub fn removeHooks() void {
if (g_is_hook_owner) {
inflate_hook.dumpStats();
inflate_hook.remove();
transform_hook.detach();
particle_hook.detach();
glyph_hook.detach();