diff --git a/build.zig b/build.zig index c5eb302..8821221 100644 --- a/build.zig +++ b/build.zig @@ -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")); diff --git a/src/performance/inflate_hook.zig b/src/performance/inflate_hook.zig new file mode 100644 index 0000000..61ced37 --- /dev/null +++ b/src/performance/inflate_hook.zig @@ -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; + } +} diff --git a/src/performance/performance.zig b/src/performance/performance.zig index 676ca73..3bc2947 100644 --- a/src/performance/performance.zig +++ b/src/performance/performance.zig @@ -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();