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:
MarcelineVQ
2026-03-24 14:57:28 -07:00
parent b612e6f953
commit 80a7cdea3f
4 changed files with 140 additions and 267 deletions
+21
View File
@@ -193,6 +193,21 @@ end)
local name, rank, icon, castTime, minRange, maxRange, spellId = GetSpellInfo(133) 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. See the [DPSLog wiki page](https://codeberg.org/gwenael/WeirdUtils/wiki/DPSLog) for full event reference and addon developer guide.
**DLL:** `dpslog.dll` **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 ### 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. 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.
+8 -14
View File
@@ -41,7 +41,7 @@ pub fn build(b: *std.Build) void {
const target = b.resolveTargetQuery(.{ const target = b.resolveTargetQuery(.{
.cpu_arch = .x86, .cpu_arch = .x86,
.os_tag = .windows, .os_tag = .windows,
.abi = .msvc, .abi = .gnu,
.cpu_features_add = std.Target.x86.featureSet(&.{ .sse, .sse2 }), .cpu_features_add = std.Target.x86.featureSet(&.{ .sse, .sse2 }),
}); });
const optimize = b.option(std.builtin.OptimizeMode, "optimize", "Optimization mode (default: ReleaseFast)") orelse .ReleaseFast; 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(.{ const bone_sse_target = b.resolveTargetQuery(.{
.cpu_arch = .x86, .cpu_arch = .x86,
.os_tag = .windows, .os_tag = .windows,
.abi = .msvc, .abi = .gnu,
.cpu_features_add = std.Target.x86.featureSet(&.{ .sse, .sse2, .sse3, .sse4_1, .fma, .avx }), .cpu_features_add = std.Target.x86.featureSet(&.{ .sse, .sse2, .sse3, .sse4_1, .fma, .avx }),
}); });
const bone_sse_obj = b.addObject(.{ const bone_sse_obj = b.addObject(.{
@@ -86,7 +86,7 @@ pub fn build(b: *std.Build) void {
const ref_target = b.resolveTargetQuery(.{ const ref_target = b.resolveTargetQuery(.{
.cpu_arch = .x86, .cpu_arch = .x86,
.os_tag = .windows, .os_tag = .windows,
.abi = .msvc, .abi = .gnu,
.cpu_features_sub = std.Target.x86.featureSet(&.{ .sse, .sse2 }), .cpu_features_sub = std.Target.x86.featureSet(&.{ .sse, .sse2 }),
}); });
const bone_sse_ref_obj = b.addObject(.{ 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_sse_obj);
lib.root_module.addObject(particle_ref_obj); lib.root_module.addObject(particle_ref_obj);
// libdeflate — vendored C sources, decompress-only. // libdeflate — compiled as x86-windows-gnu (same ABI as main DLL).
// Built as static library targeting x86-windows-gnu (has libc headers). // Uses Zig's bundled MinGW libc for string.h/stdlib.h.
// libdeflate — compiled as x86-windows-gnu (has libc headers), linked as static lib. // malloc/free resolved at link time to our exports in performance.zig.
// 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.
const libdeflate = b.addLibrary(.{ const libdeflate = b.addLibrary(.{
.linkage = .static, .linkage = .static,
.name = "deflate", .name = "deflate",
.root_module = b.createModule(.{ .root_module = b.createModule(.{
.root_source_file = null, .root_source_file = null,
.target = b.resolveTargetQuery(.{ .target = target,
.cpu_arch = .x86,
.os_tag = .windows,
.abi = .gnu,
}),
.optimize = .ReleaseFast, .optimize = .ReleaseFast,
.link_libc = true, .link_libc = true,
}), }),
@@ -179,7 +173,7 @@ pub fn build(b: *std.Build) void {
"src/performance/libdeflate/lib/adler32.c", "src/performance/libdeflate/lib/adler32.c",
"src/performance/libdeflate/lib/x86/cpu_features.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"));
libdeflate.root_module.addIncludePath(b.path("src/performance/libdeflate/lib")); libdeflate.root_module.addIncludePath(b.path("src/performance/libdeflate/lib"));
+107 -239
View File
@@ -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 //! Hooks DecompressData_WithOptions (0x661A80). For type 0x02 (zlib) streams,
//! compressed buffers. Runs both original and libdeflate, times both, //! uses libdeflate (~2.2x faster than stock zlib). Falls back to original on failure.
//! logs the comparison. //!
//! 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 hook_lib = @import("zhook");
const logging = @import("../logging.zig"); const logging = @import("../logging.zig");
const FC: std.builtin.CallingConvention = .{ .x86_fastcall = .{} };
const SC: std.builtin.CallingConvention = .{ .x86_stdcall = .{} };
const std = @import("std"); const std = @import("std");
// Game memory allocator: ReallocMemory(NULL, size, ...) = malloc, FreeMemory(ptr, ...) = free // libdeflate C API
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)
extern fn libdeflate_alloc_decompressor() ?*anyopaque; extern fn libdeflate_alloc_decompressor() ?*anyopaque;
extern var libdeflate_x86_cpu_features: u32;
extern fn libdeflate_free_decompressor(?*anyopaque) void; extern fn libdeflate_free_decompressor(?*anyopaque) void;
extern fn libdeflate_zlib_decompress( extern fn libdeflate_zlib_decompress(?*anyopaque, [*]const u8, usize, [*]u8, usize, *usize) c_int;
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 lib_available: bool = false;
var log: logging.Logger = .{}; var log: logging.Logger = .{};
// Static decompressor memory — avoids using game allocator which may not be malloc-compatible // --- Thread-local decompressor pool ---
var static_decompressor_mem: [12288]u8 align(16) = undefined; // 12KB > 11564 bytes needed // 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. fn getCurrentThreadId() u32 {
// ld_buf: saves input before original modifies it (256KB) return asm volatile ("movl %%fs:0x24, %[ret]"
// ld_out_buf: libdeflate output (256KB) : [ret] "=r" (-> u32),
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;
} }
// Timing accumulators fn getTlsDecompressor() ?*anyopaque {
var orig_total_cycles: u64 = 0; // ALL calls const tid = getCurrentThreadId();
var orig_matched_cycles: u64 = 0; // only calls where libdeflate also ran for (&tls_pool) |*slot| {
var fast_total_cycles: u64 = 0; // libdeflate time for matched calls 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 call_count: u64 = 0;
var mismatch_count: u64 = 0;
var success_count: u64 = 0; var success_count: u64 = 0;
var total_bytes: u64 = 0; var fallback_count: 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;
inline fn rdtsc() u64 { inline fn rdtsc() u64 {
var lo: u32 = undefined; var lo: u32 = undefined;
var hi: u32 = undefined; var hi: u32 = undefined;
asm volatile ("rdtsc" asm volatile ("rdtsc" : [lo] "={eax}" (lo), [hi] "={edx}" (hi));
: [lo] "={eax}" (lo),
[hi] "={edx}" (hi),
);
return (@as(u64, hi) << 32) | lo; return (@as(u64, hi) << 32) | lo;
} }
// ============================================================================= const DecompressFn = fn (u32, u32, u32, u32, u32) callconv(.{ .x86_stdcall = .{} }) u32;
// 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 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 { pub fn decompressDetour(out_buf: u32, out_size_ptr: u32, in_buf: u32, in_size: u32, flags: u32) callconv(.{ .x86_stdcall = .{} }) 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)).*;
const in_ptr: [*]const u8 = @ptrFromInt(in_buf); 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. // Accept type 0x02 with valid zlib CMF (deflate method = low nibble 0x08)
// Use the static ld_buf_backing (256KB) as the save buffer — it's not used until later. if (lib_available and in_size > 3 and out_capacity > 0 and
const save_len = @min(in_size, ld_buf_size); in_ptr[0] == 0x02 and (in_ptr[1] & 0x0F) == 0x08)
@memcpy(ld_buf[0..save_len], in_ptr[0..save_len]); {
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 if (ld_ret == 0) {
const t0 = rdtsc(); call_count +|= 1;
const ret = decompress_hook.callOriginal(.{ out_buf, out_size_ptr, in_buf, in_size, flags }); success_count +|= 1;
const orig_cycles = rdtsc() - t0; return 1;
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;
} }
} }
} }
// 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; return ret;
} }
pub fn dumpStats() void { pub fn dumpStats() void {
if (call_count == 0) return; if (call_count == 0) return;
const MS_DIV: u64 = 3_000_000; const MS = 3_000_000;
const type_names = [8][]const u8{ "huff", "zlib", "b2", "b3", "bzip", "pkw", "adpcm1", "adpcm2" }; log.fmt("inflate: {d} calls | fast={d} ({d}ms) fallback={d} ({d}ms)\n", .{
const matched = success_count + mismatch_count; call_count, success_count, fast_total_cycles / MS, fallback_count, orig_total_cycles / MS,
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,
}); });
// 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; fast_total_cycles = 0;
orig_total_cycles = 0;
call_count = 0; call_count = 0;
success_count = 0; success_count = 0;
mismatch_count = 0; fallback_count = 0;
total_bytes = 0;
total_in_bytes = 0;
for (&type_counts) |*c| c.* = 0;
for (&raw_type_counts) |*c| c.* = 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 { pub fn install(logger: logging.Logger) bool {
log = logger; log = logger;
// Use static memory — bypass game allocator entirely
decompressor = libdeflate_alloc_decompressor(); // Sanity test
if (decompressor == null) { const test_decomp = libdeflate_alloc_decompressor();
log.print("inflate_hook: failed to allocate libdeflate decompressor\n"); if (test_decomp == null) {
log.print("inflate_hook: alloc failed\n");
return false; return false;
} }
// Quick sanity test: decompress a trivial zlib stream const test_in = [_]u8{ 0x78, 0x9C, 0xCB, 0x48, 0xCD, 0xC9, 0xC9, 0x07, 0x00, 0x06, 0x2C, 0x02, 0x15 };
{ var test_out: [64]u8 = undefined;
// zlib-compressed "hello" (pre-computed) var test_len: usize = 0;
const test_in = [_]u8{ 0x78, 0x9C, 0xCB, 0x48, 0xCD, 0xC9, 0xC9, 0x07, 0x00, 0x06, 0x2C, 0x02, 0x15 }; const test_ret = libdeflate_zlib_decompress(test_decomp, &test_in, test_in.len, &test_out, 64, &test_len);
var test_out: [64]u8 = undefined; log.fmt("inflate_hook: sanity ret={d} len={d} data='{s}'\n", .{ test_ret, test_len, test_out[0..@min(test_len, 32)] });
var test_len: usize = 0; libdeflate_free_decompressor(test_decomp);
const test_ret = libdeflate_zlib_decompress(
decompressor, lib_available = true;
&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});
if (decompress_hook.attach(0x661A80, &decompressDetour) == .ok) { 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; return true;
} }
log.print("inflate_hook: failed to hook\n");
return false; return false;
} }
pub fn remove() void { pub fn remove() void {
decompress_hook.detach(); decompress_hook.detach();
if (decompressor) |d| { lib_available = false;
libdeflate_free_decompressor(d); freeTlsPool();
decompressor = null;
}
} }
+4 -14
View File
@@ -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 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); 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 { 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); return gameRealloc(0, @intCast(size), 0, 0, 0);
} }
export fn free(ptr: ?*anyopaque) callconv(.c) void { 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); 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) { if (entry.font_ptr == a and entry.char_code == c and entry.param2 == d) {
asm volatile ("flds (%[p])" asm volatile ("flds (%[p])"
:: [p] "r" (&entry.width_bits) :
: [p] "r" (&entry.width_bits),
); );
return null; 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; var width_bits: u32 = undefined;
asm volatile ("fsts (%[p])" asm volatile ("fsts (%[p])"
:: [p] "r" (&width_bits) :
: [p] "r" (&width_bits),
); );
entry.* = .{ entry.* = .{