inflate: thread-local libdeflate with gnu ABI — 2.2x speedup, 100% coverage
Root cause of crashes was thread safety: libdeflate's decompressor struct has mutable decode tables rebuilt per-block, so sharing between main thread and FMOD audio thread caused wild writes. Fixed with a thread-local pool keyed by Windows thread ID (FS:[0x24]). Switched build ABI from msvc to gnu — eliminates stub headers, libdeflate uses Zig's bundled MinGW libc. Relaxed zlib header filter from == 0x78 to (CMF & 0x0F) == 0x08 to catch both 32K and 4K window sizes. Benchmarked: stock=2681ms, per-call-alloc=1304ms (2.05x), tls=1194ms (2.2x) on 84k calls. Zero errors, zero fallbacks in production.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"));
|
||||
|
||||
+107
-239
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.* = .{
|
||||
|
||||
Reference in New Issue
Block a user