diff --git a/DLL_README.md b/DLL_README.md index d5e4f75..515daed 100644 --- a/DLL_README.md +++ b/DLL_README.md @@ -193,6 +193,21 @@ end) local name, rank, icon, castTime, minRange, maxRange, spellId = GetSpellInfo(133) ``` +- `UnitCastingInfo("unit")` -- TBC/WotLK cast bar query (works on any visible unit): + +```lua +local name, rank, text, icon, startTime, endTime, isTradeSkill, castID, notInterruptible = UnitCastingInfo("target") +if name then + -- startTime/endTime are in milliseconds (compare with GetTime()*1000) +end +``` + +- `UnitChannelInfo("unit")` -- TBC/WotLK channel bar query: + +```lua +local name, rank, text, icon, startTime, endTime, isTradeSkill, notInterruptible = UnitChannelInfo("target") +``` + See the [DPSLog wiki page](https://codeberg.org/gwenael/WeirdUtils/wiki/DPSLog) for full event reference and addon developer guide. **DLL:** `dpslog.dll` @@ -239,6 +254,12 @@ No configuration needed. Enabled by default when using `weirdutils.dll`. --- +### Performance + +Replaces 20+ internal math functions with SIMD (SSE/AVX) equivalents and swaps the game's 2004-era zlib with a modern decompression library (2.2x faster). Covers skeletal animation, particle rendering, frustum culling, collision detection, text glyph caching, and float-to-integer conversion. Most noticeable in cities, raids, and during zone transitions. No visual difference, no configuration needed. Included in `weirdutils.dll`. + +--- + ### Timer Calibration Improves the game's internal timer precision by recalibrating the TSC (Time Stamp Counter) frequency using the OS performance counter as a reference. The vanilla client's built-in calibration is inaccurate, which can cause animation stutter and timing jitter on some systems. diff --git a/build.zig b/build.zig index b69664d..305fabf 100644 --- a/build.zig +++ b/build.zig @@ -41,7 +41,7 @@ pub fn build(b: *std.Build) void { const target = b.resolveTargetQuery(.{ .cpu_arch = .x86, .os_tag = .windows, - .abi = .msvc, + .abi = .gnu, .cpu_features_add = std.Target.x86.featureSet(&.{ .sse, .sse2 }), }); const optimize = b.option(std.builtin.OptimizeMode, "optimize", "Optimization mode (default: ReleaseFast)") orelse .ReleaseFast; @@ -69,7 +69,7 @@ pub fn build(b: *std.Build) void { const bone_sse_target = b.resolveTargetQuery(.{ .cpu_arch = .x86, .os_tag = .windows, - .abi = .msvc, + .abi = .gnu, .cpu_features_add = std.Target.x86.featureSet(&.{ .sse, .sse2, .sse3, .sse4_1, .fma, .avx }), }); const bone_sse_obj = b.addObject(.{ @@ -86,7 +86,7 @@ pub fn build(b: *std.Build) void { const ref_target = b.resolveTargetQuery(.{ .cpu_arch = .x86, .os_tag = .windows, - .abi = .msvc, + .abi = .gnu, .cpu_features_sub = std.Target.x86.featureSet(&.{ .sse, .sse2 }), }); const bone_sse_ref_obj = b.addObject(.{ @@ -152,21 +152,15 @@ pub fn build(b: *std.Build) void { lib.root_module.addObject(particle_sse_obj); lib.root_module.addObject(particle_ref_obj); - // libdeflate — vendored C sources, decompress-only. - // Built as static library targeting x86-windows-gnu (has libc headers). - // libdeflate — compiled as x86-windows-gnu (has libc headers), linked as static lib. - // Disable x86 SIMD dispatch to avoid ABI mismatch between gnu and msvc objects. - // Generic C fallback is still ~2x faster than WoW's embedded zlib. + // libdeflate — compiled as x86-windows-gnu (same ABI as main DLL). + // Uses Zig's bundled MinGW libc for string.h/stdlib.h. + // malloc/free resolved at link time to our exports in performance.zig. const libdeflate = b.addLibrary(.{ .linkage = .static, .name = "deflate", .root_module = b.createModule(.{ .root_source_file = null, - .target = b.resolveTargetQuery(.{ - .cpu_arch = .x86, - .os_tag = .windows, - .abi = .gnu, - }), + .target = target, .optimize = .ReleaseFast, .link_libc = true, }), @@ -179,7 +173,7 @@ pub fn build(b: *std.Build) void { "src/performance/libdeflate/lib/adler32.c", "src/performance/libdeflate/lib/x86/cpu_features.c", }, - .flags = &.{ "-DLIBDEFLATE_ASSEMBLER_DOES_NOT_SUPPORT_AVX512VNNI", "-g0" }, + .flags = &.{"-DLIBDEFLATE_ASSEMBLER_DOES_NOT_SUPPORT_AVX512VNNI"}, }); 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 index 8539477..1e28d83 100644 --- a/src/performance/inflate_hook.zig +++ b/src/performance/inflate_hook.zig @@ -1,295 +1,163 @@ -//! inflate_hook — libdeflate timing comparison hook for WoW's inflateStateMachine. +//! inflate_hook — libdeflate replacement for WoW's zlib inflate. //! -//! Hooks BZip2Decompressor_Decompress (0x660740) which receives complete -//! compressed buffers. Runs both original and libdeflate, times both, -//! logs the comparison. +//! Hooks DecompressData_WithOptions (0x661A80). For type 0x02 (zlib) streams, +//! uses libdeflate (~2.2x faster than stock zlib). Falls back to original on failure. +//! +//! Thread-safety: the decompressor struct contains mutable decode tables rebuilt +//! per block, so each thread gets its own cached decompressor via a thread-local +//! pool keyed by Windows thread ID (FS:[0x24]). This avoids both the race condition +//! (shared decompressor → wild writes) and the per-call alloc overhead (~8-9% gain +//! over alloc/free each call). +//! +//! Benchmark results (84k calls, heavy load): +//! stock=2681ms | per-call-alloc=1304ms (2.05x) | tls-cached=1194ms (2.2x) 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"); -// Game memory allocator: ReallocMemory(NULL, size, ...) = malloc, FreeMemory(ptr, ...) = free -const gameRealloc: *const fn (u32, u32, u32, u32, u32) callconv(SC) ?*anyopaque = @ptrFromInt(0x646320); -const gameFreeMemory: *const fn (u32, u32, u32, u32) callconv(SC) u32 = @ptrFromInt(0x646430); - -fn gameAlloc(size: u32) ?*anyopaque { - return gameRealloc(0, size, 0, 0, 0); -} - -fn gameFree(ptr: *anyopaque) void { - _ = gameFreeMemory(@intFromPtr(ptr), 0, 0, 0); -} - -// libdeflate C API (linked from static lib) +// libdeflate C API extern fn libdeflate_alloc_decompressor() ?*anyopaque; -extern var libdeflate_x86_cpu_features: u32; 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; +extern fn libdeflate_zlib_decompress(?*anyopaque, [*]const u8, usize, [*]u8, usize, *usize) c_int; -var decompressor: ?*anyopaque = null; +var lib_available: bool = false; var log: logging.Logger = .{}; -// Static decompressor memory — avoids using game allocator which may not be malloc-compatible -var static_decompressor_mem: [12288]u8 align(16) = undefined; // 12KB > 11564 bytes needed +// --- Thread-local decompressor pool --- +// Keyed by Windows thread ID. WoW has ~5-10 threads; only 2-3 call decompress. +const TLS_SLOTS = 8; +const TlsSlot = struct { + thread_id: u32 = 0, + decomp: ?*anyopaque = null, +}; +var tls_pool: [TLS_SLOTS]TlsSlot = [_]TlsSlot{.{}} ** TLS_SLOTS; -// Static buffers — no heap allocation needed. -// ld_buf: saves input before original modifies it (256KB) -// ld_out_buf: libdeflate output (256KB) -var ld_buf_backing: [256 * 1024]u8 align(16) = undefined; -var ld_buf: [*]u8 = &ld_buf_backing; -const ld_buf_size: u32 = 256 * 1024; - -var ld_out_buf_backing: [256 * 1024]u8 align(16) = undefined; -const ld_out_buf_size: u32 = 256 * 1024; - -// Thread safety: only run libdeflate on the main thread (ESP in 0x00Exxxxx range) -var main_thread_id: u32 = 0; -extern "kernel32" fn GetCurrentThreadId() callconv(.{ .x86_stdcall = .{} }) u32; - -fn isMainThread() bool { - const tid = GetCurrentThreadId(); - if (main_thread_id == 0) { - main_thread_id = tid; // first call captures main thread - return true; - } - return tid == main_thread_id; +fn getCurrentThreadId() u32 { + return asm volatile ("movl %%fs:0x24, %[ret]" + : [ret] "=r" (-> u32), + ); } -// Timing accumulators -var orig_total_cycles: u64 = 0; // ALL calls -var orig_matched_cycles: u64 = 0; // only calls where libdeflate also ran -var fast_total_cycles: u64 = 0; // libdeflate time for matched calls +fn getTlsDecompressor() ?*anyopaque { + const tid = getCurrentThreadId(); + for (&tls_pool) |*slot| { + if (slot.thread_id == tid) return slot.decomp; + } + const decomp = libdeflate_alloc_decompressor() orelse return null; + for (&tls_pool) |*slot| { + if (slot.thread_id == 0) { + slot.thread_id = tid; + slot.decomp = decomp; + return decomp; + } + } + libdeflate_free_decompressor(decomp); + return null; +} + +fn freeTlsPool() void { + for (&tls_pool) |*slot| { + if (slot.decomp) |d| libdeflate_free_decompressor(d); + slot.* = .{}; + } +} + +// --- Timing --- +var fast_total_cycles: u64 = 0; +var orig_total_cycles: u64 = 0; var call_count: u64 = 0; -var mismatch_count: u64 = 0; var success_count: u64 = 0; -var total_bytes: u64 = 0; -var total_in_bytes: u64 = 0; -// Per-type counters: index by compression mask bit -var type_counts: [8]u64 = .{0} ** 8; // bits 0-7 -// Per raw type byte counter (full byte value) -var raw_type_counts: [256]u32 = .{0} ** 256; -// Track first-seen header bytes per type for format identification -var type_headers_logged: [256]bool = .{false} ** 256; +var fallback_count: u64 = 0; inline fn rdtsc() u64 { var lo: u32 = undefined; var hi: u32 = undefined; - asm volatile ("rdtsc" - : [lo] "={eax}" (lo), - [hi] "={edx}" (hi), - ); + 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; +const DecompressFn = fn (u32, u32, u32, u32, u32) callconv(.{ .x86_stdcall = .{} }) u32; pub var decompress_hook: hook_lib.Detour(DecompressFn) = .{}; -pub fn decompressDetour(out_buf: u32, out_size_ptr: u32, in_buf: u32, in_size: u32, flags: u32) callconv(SC) u32 { - // param1 = outBuf, param2 = &outSize (ptr), param3 = inBuf, param4 = inSize (value), param5 = flags - // Read output buffer capacity BEFORE the original modifies *out_size_ptr - const out_capacity = @as(*const u32, @ptrFromInt(out_size_ptr)).*; +pub fn decompressDetour(out_buf: u32, out_size_ptr: u32, in_buf: u32, in_size: u32, flags: u32) callconv(.{ .x86_stdcall = .{} }) u32 { const in_ptr: [*]const u8 = @ptrFromInt(in_buf); + const out_capacity = @as(*const u32, @ptrFromInt(out_size_ptr)).*; - // Save input data BEFORE calling original — the original may modify the input buffer. - // Use the static ld_buf_backing (256KB) as the save buffer — it's not used until later. - const save_len = @min(in_size, ld_buf_size); - @memcpy(ld_buf[0..save_len], in_ptr[0..save_len]); + // Accept type 0x02 with valid zlib CMF (deflate method = low nibble 0x08) + if (lib_available and in_size > 3 and out_capacity > 0 and + in_ptr[0] == 0x02 and (in_ptr[1] & 0x0F) == 0x08) + { + if (getTlsDecompressor()) |decomp| { + var ld_out: usize = 0; + const t1 = rdtsc(); + const ld_ret = libdeflate_zlib_decompress( + decomp, + in_ptr + 1, + in_size - 1, + @ptrFromInt(out_buf), + out_capacity, + &ld_out, + ); + fast_total_cycles +|= rdtsc() - t1; - // Run original and time it - const t0 = rdtsc(); - const ret = decompress_hook.callOriginal(.{ out_buf, out_size_ptr, in_buf, in_size, flags }); - const orig_cycles = rdtsc() - t0; - - orig_total_cycles +|= orig_cycles; - call_count +|= 1; - - if (ret != 0 and in_size > 2 and out_capacity > 0) { - const comp_type = in_ptr[0]; - const actual_out = @as(*const u32, @ptrFromInt(out_size_ptr)).*; - total_bytes +|= actual_out; - total_in_bytes +|= in_size; - - inline for (0..8) |bit| { - if ((comp_type & (@as(u8, 1) << @intCast(bit))) != 0) - type_counts[bit] +|= 1; - } - raw_type_counts[comp_type] +|= 1; - - // Log first-seen header per type - if (!type_headers_logged[comp_type]) { - type_headers_logged[comp_type] = true; - log.fmt(" type=0x{x:0>2} in={d} out={d} hdr:", .{ comp_type, in_size, actual_out }); - const dump_len = @min(in_size - 1, 16); - for (0..dump_len) |i| log.fmt(" {x:0>2}", .{in_ptr[1 + i]}); - log.print("\n"); - } - - // Time libdeflate on zlib-only streams (type == 0x02, header 78 xx) - if (comp_type == 0x02 and decompressor != null and actual_out > 0) { - // Use actual_out (post-original) as the exact expected size. - // out_capacity (pre-original) may be larger than needed but actual_out - // is what the original produced — libdeflate should produce the same. - const ld_out_size = actual_out; - - // Only proceed if: input fits, output fits, valid zlib header, - // AND we're on the main thread (static buffers aren't thread-safe) - if (in_size <= save_len and ld_out_size <= ld_out_buf_size and - save_len > 2 and ld_buf[1] == 0x78 and isMainThread()) - { - // Log call number and sizes for first few calls - if (success_count + mismatch_count < 3) { - log.fmt(" ld #{d}: in_size={d} out_size={d} saved_hdr={x:0>2}{x:0>2}\n", .{ - success_count + mismatch_count, - save_len - 1, ld_out_size, - ld_buf[1], ld_buf[2], - }); - } - var ld_out: usize = 0; - const t1 = rdtsc(); - const ld_ret = libdeflate_zlib_decompress( - decompressor, - ld_buf + 1, // skip type byte in saved input - save_len - 1, - &ld_out_buf_backing, - ld_out_size, - &ld_out, - ); - const fast_cycles = rdtsc() - t1; - fast_total_cycles +|= fast_cycles; - orig_matched_cycles +|= orig_cycles; // track original time for same calls - if (ld_ret == 0 and ld_out == actual_out) - success_count +|= 1 - else - mismatch_count +|= 1; + if (ld_ret == 0) { + call_count +|= 1; + success_count +|= 1; + return 1; } } } + // Fallback to original + call_count +|= 1; + const t0 = rdtsc(); + const ret = decompress_hook.callOriginal(.{ out_buf, out_size_ptr, in_buf, in_size, flags }); + orig_total_cycles +|= rdtsc() - t0; + fallback_count +|= 1; return ret; } pub fn dumpStats() void { if (call_count == 0) return; - const MS_DIV: u64 = 3_000_000; - const type_names = [8][]const u8{ "huff", "zlib", "b2", "b3", "bzip", "pkw", "adpcm1", "adpcm2" }; - const matched = success_count + mismatch_count; - log.fmt("inflate: {d} calls, in={d}KB out={d}KB, orig_all={d}ms | matched={d}: orig={d}ms fast={d}ms (ok={d} fail={d})\n", .{ - call_count, - total_in_bytes / 1024, - total_bytes / 1024, - orig_total_cycles / MS_DIV, - matched, - orig_matched_cycles / MS_DIV, - fast_total_cycles / MS_DIV, - success_count, - mismatch_count, + const MS = 3_000_000; + log.fmt("inflate: {d} calls | fast={d} ({d}ms) fallback={d} ({d}ms)\n", .{ + call_count, success_count, fast_total_cycles / MS, fallback_count, orig_total_cycles / MS, }); - // Type bits distribution - log.print(" bits:"); - for (type_names, 0..) |name, i| { - if (type_counts[i] > 0) { - log.fmt(" {s}={d}", .{ name, type_counts[i] }); - } - } - log.print("\n"); - // Raw type byte distribution (shows exact combos used) - log.print(" raw:"); - for (raw_type_counts, 0..) |count, i| { - if (count > 0) { - log.fmt(" 0x{x:0>2}={d}", .{ i, count }); - } - } - log.print("\n"); - // Reset - orig_total_cycles = 0; - orig_matched_cycles = 0; fast_total_cycles = 0; + orig_total_cycles = 0; call_count = 0; success_count = 0; - mismatch_count = 0; - total_bytes = 0; - total_in_bytes = 0; - for (&type_counts) |*c| c.* = 0; - for (&raw_type_counts) |*c| c.* = 0; + fallback_count = 0; } -fn staticMalloc(size: usize) callconv(.c) ?*anyopaque { - if (size <= static_decompressor_mem.len) { - return @ptrCast(&static_decompressor_mem); - } - return null; -} - -fn staticFree(_: ?*anyopaque) callconv(.c) void {} - -extern fn libdeflate_alloc_decompressor_with_funcs(?*const fn (usize) callconv(.c) ?*anyopaque, ?*const fn (?*anyopaque) callconv(.c) void) ?*anyopaque; - pub fn install(logger: logging.Logger) bool { log = logger; - // Use static memory — bypass game allocator entirely - decompressor = libdeflate_alloc_decompressor(); - if (decompressor == null) { - log.print("inflate_hook: failed to allocate libdeflate decompressor\n"); + + // Sanity test + const test_decomp = libdeflate_alloc_decompressor(); + if (test_decomp == null) { + log.print("inflate_hook: alloc failed\n"); return false; } - // Quick sanity test: decompress a trivial zlib stream - { - // zlib-compressed "hello" (pre-computed) - const test_in = [_]u8{ 0x78, 0x9C, 0xCB, 0x48, 0xCD, 0xC9, 0xC9, 0x07, 0x00, 0x06, 0x2C, 0x02, 0x15 }; - var test_out: [64]u8 = undefined; - var test_len: usize = 0; - const test_ret = libdeflate_zlib_decompress( - decompressor, - &test_in, - test_in.len, - &test_out, - test_out.len, - &test_len, - ); - log.fmt("inflate_hook: sanity test ret={d} len={d} data='{s}'\n", .{ - test_ret, test_len, test_out[0..@min(test_len, 32)], - }); - } - log.fmt("inflate_hook: cpu_features=0x{x:0>8}\n", .{libdeflate_x86_cpu_features}); + const test_in = [_]u8{ 0x78, 0x9C, 0xCB, 0x48, 0xCD, 0xC9, 0xC9, 0x07, 0x00, 0x06, 0x2C, 0x02, 0x15 }; + var test_out: [64]u8 = undefined; + var test_len: usize = 0; + const test_ret = libdeflate_zlib_decompress(test_decomp, &test_in, test_in.len, &test_out, 64, &test_len); + log.fmt("inflate_hook: sanity ret={d} len={d} data='{s}'\n", .{ test_ret, test_len, test_out[0..@min(test_len, 32)] }); + libdeflate_free_decompressor(test_decomp); + + lib_available = true; + if (decompress_hook.attach(0x661A80, &decompressDetour) == .ok) { - log.print("inflate_hook: hooked DecompressData_WithOptions\n"); + log.print("inflate_hook: hooked (libdeflate tls-cached, 2.2x speedup)\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; - } + lib_available = false; + freeTlsPool(); } diff --git a/src/performance/performance.zig b/src/performance/performance.zig index 808ae84..5c9d769 100644 --- a/src/performance/performance.zig +++ b/src/performance/performance.zig @@ -28,23 +28,11 @@ pub const module_name: [*:0]const u8 = "performance"; const gameRealloc: *const fn (u32, u32, u32, u32, u32) callconv(.{ .x86_stdcall = .{} }) ?*anyopaque = @ptrFromInt(0x646320); const gameFree: *const fn (u32, u32, u32, u32) callconv(.{ .x86_stdcall = .{} }) u32 = @ptrFromInt(0x646430); -// Static buffer for libdeflate's decompressor struct (~11.5KB). -// Avoids game allocator which may not be fully malloc-compatible. -var static_alloc_buf: [16384]u8 align(16) = undefined; -var static_alloc_used: bool = false; - export fn malloc(size: usize) callconv(.c) ?*anyopaque { - // First allocation goes to static buffer (the decompressor struct) - if (!static_alloc_used and size <= static_alloc_buf.len) { - static_alloc_used = true; - return @ptrCast(&static_alloc_buf); - } return gameRealloc(0, @intCast(size), 0, 0, 0); } export fn free(ptr: ?*anyopaque) callconv(.c) void { - // Don't free static buffer - if (ptr == @as(?*anyopaque, @ptrCast(&static_alloc_buf))) return; if (ptr) |p| _ = gameFree(@intFromPtr(p), 0, 0, 0); } @@ -123,7 +111,8 @@ fn glyphDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyo if (entry.font_ptr == a and entry.char_code == c and entry.param2 == d) { asm volatile ("flds (%[p])" - :: [p] "r" (&entry.width_bits) + : + : [p] "r" (&entry.width_bits), ); return null; } @@ -132,7 +121,8 @@ fn glyphDetour(a: u32, b: u32, c: u32, d: u32) callconv(hook.cc.fastcall) ?*anyo var width_bits: u32 = undefined; asm volatile ("fsts (%[p])" - :: [p] "r" (&width_bits) + : + : [p] "r" (&width_bits), ); entry.* = .{