perf: consolidate filecache, timer calibration into performance module

Move filecache.zig and timer_fix.zig from standalone modules into
src/performance/. Filecache no longer has its own hooks/mutex — stats
are dumped by performance's worldupdate hook. Timer calibration runs
during performance install instead of transform44. Remove filecache
from build module list (enabled automatically with performance).
Merge DLL_README sections into single Performance entry.
This commit is contained in:
MarcelineVQ
2026-03-24 17:28:19 -07:00
parent 3cab0db8c8
commit 979e5729bc
7 changed files with 49 additions and 133 deletions
+7 -26
View File
@@ -243,35 +243,16 @@ Lua API for addon developers:
---
### MPQ File Cache
Caches the results of MPQ archive file lookups so repeat file opens skip the expensive archive chain walk and hash table probe. The game re-opens the same model and texture files hundreds of times per second during gameplay -- each lookup normally searches through every loaded MPQ archive. The cache remembers which archive contains each file and returns the answer directly.
Cache hits cost roughly 1/30th of a full search. During heavy gameplay (cities, raids, zone transitions), this saves 50-160ms every 15 seconds. In quiet scenes with few new models loading, there is little to save because the game does fewer lookups.
The cache validates that archives are still alive before returning cached results, so if the game closes or reloads an archive, the cache falls through to the original search.
No configuration needed. Enabled by default when using `weirdutils.dll`.
**DLL:** `filecache.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`.
Engine-level optimizations that reduce CPU time on math, rendering helpers, file lookups, and data decompression. No visual difference, no configuration needed. Included in `weirdutils.dll`.
---
- **SIMD Math** — replaces 20+ internal math functions with SSE/AVX equivalents covering skeletal animation, particle rendering, frustum culling, collision detection, text glyph caching, and float-to-integer conversion
- **Data Decompression** — swaps the game's 2004-era zlib with a modern library (2.2x faster). Loading screen times reduced by at least 13%
- **MPQ File Cache** — caches archive file lookups so repeat file opens skip the expensive archive chain walk. Cache hits cost ~1/30th of a full search, saving 50-160ms every 15 seconds during heavy gameplay
- **Timer Calibration** — recalibrates the TSC frequency using the OS performance counter for accurate animation timing. Requests higher OS timer resolution (0.5ms) and disables Windows 11 power throttling. Ported from [VanillaFixes](https://github.com/hannesmann/vanillafixes)
### 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.
Also requests higher OS timer resolution (0.5ms instead of the default 15.6ms) and disables Windows 11 power throttling for the game process.
Ported from [VanillaFixes](https://github.com/hannesmann/vanillafixes). Primarily benefits native Windows. On Wine/Linux the game typically uses GetTickCount instead of TSC, so this module enables TSC mode with a proper calibration.
No configuration needed. Included in `weirdutils.dll`.
Most noticeable in cities, raids, and during zone transitions.
---
@@ -300,7 +281,7 @@ WeirdUtils exports three functions for querying and disabling modules at runtime
Module names are case-insensitive and match the released dll names:
`customassets`, `framecrash`, `logsessions`, `transmogfix`, `minimapicons`, `healtextfix`, `bigcursor`, `worldmarkers`, `interact`, `outline`, `pngscreenshots`, `clickthrough`, `dpslog`
`customassets`, `framecrash`, `logsessions`, `transmogfix`, `minimapicons`, `healtextfix`, `bigcursor`, `worldmarkers`, `interact`, `outline`, `pngscreenshots`, `clickthrough`, `dpslog`, `filecache`, `performance`
There is no re-enable API.
-1
View File
@@ -31,7 +31,6 @@ const module_list = [_]ModuleDesc{
.{ .name = "dpslog", .desc = "Enable structured combat log events for addons", .default = false },
.{ .name = "transform44", .desc = "Enable transform44 profiling/A/B testing (dev only)", .default = false },
.{ .name = "addonperf", .desc = "Enable addon memory/CPU profiling API", .default = false },
.{ .name = "filecache", .desc = "Enable MPQ archive file cache" },
.{ .name = "ssemaths", .desc = "Enable UnitXP x87 math polyfill replacements (SSE)", .default = false },
.{ .name = "silicon", .desc = "Enable SSE2 math replacements (ported from libSiliconPatch)", .default = false },
.{ .name = "performance", .desc = "Enable production SSE hooks (bone, particle, glyph cache)", .default = true },
+3 -4
View File
@@ -43,7 +43,7 @@ const dpslog = if (build_opts.dpslog) @import("dpslog/dpslog.zig") else struct {
const transform44 = if (build_opts.transform44) @import("transform44/transform44.zig") else struct {};
const addonperf = if (build_opts.addonperf) @import("addonperf/addonperf.zig") else struct {};
const ssemaths = if (build_opts.ssemaths) @import("ssemaths/ssemaths.zig") else struct {};
const file_cache = if (build_opts.filecache) @import("filecache/filecache.zig") else struct {};
const file_cache = if (build_opts.performance) @import("performance/filecache.zig") else struct {};
const silicon = if (build_opts.silicon) @import("silicon/silicon.zig") else struct {};
const performance = if (build_opts.performance) @import("performance/performance.zig") else struct {};
@@ -615,7 +615,7 @@ fn fileFindDetour(
out_block_entry: u32, // ptr to ptr: block table entry data
out_disk_path: u32, // ptr to buf: disk path output
) callconv(hook.cc.fastcall) u32 {
if (!build_opts.filecache or filename_ptr == 0)
if (!build_opts.performance or filename_ptr == 0)
return file_find_hook.callOriginal(.{ archive_or_group, filename_ptr, flags, out_inner_archive, out_outer_archive, out_block_entry, out_disk_path });
const tsc_start = file_cache.rdtsc();
@@ -712,7 +712,7 @@ fn installFileHooks() void {
_ = cleanup_file_handle_hook.attach(0x648730, &cleanupFileHandleDetour);
_ = model_load_hook.attach(0x71d4e0, &loadModelAsyncDetour);
_ = cfe_hook.attach(0x654DD0, &checkFileExistenceDetour);
if (build_opts.filecache) {
if (build_opts.performance) {
_ = file_find_hook.attach(0x6549a0, &fileFindDetour);
log.print("archive cache hook installed\n");
}
@@ -851,7 +851,6 @@ const modules = [_]ModuleHooks{
if (build_opts.transform44) .{ .name = transform44.module_name, .install = transform44.installHooks, .remove = transform44.removeHooks, .is_active = transform44.isActive } else .{},
if (build_opts.addonperf) .{ .name = addonperf.module_name, .install = addonperf.installHooks, .remove = addonperf.removeHooks, .is_active = addonperf.isActive } else .{},
if (build_opts.ssemaths) .{ .name = ssemaths.module_name, .install = ssemaths.installHooks, .remove = ssemaths.removeHooks, .is_active = ssemaths.isActive } else .{},
if (build_opts.filecache) .{ .name = file_cache.module_name, .install = file_cache.installHooks, .remove = file_cache.removeHooks, .is_active = file_cache.isActive } else .{},
if (build_opts.worldmarkers) .{ .name = markers.module_name, .install = markers.installHooks, .remove = markers.removeHooks, .is_active = markers.isActive } else .{},
if (build_opts.interact) .{ .name = interact.module_name, .install = interact.installHooks, .remove = interact.removeHooks, .is_active = interact.isActive } else .{},
if (build_opts.outline) .{ .name = outline.module_name, .remove = outline.cleanup, .is_active = outline.isActive } else .{},
@@ -3,17 +3,12 @@
//! 2-way set-associative cache: hash(filename) picks a set of 2 entries.
//! Filename stored and compared for collision safety. On eviction, the
//! least recently used way is replaced.
//!
//! Lifecycle managed by the performance module (no own hooks or mutex).
const std = @import("std");
const hook = @import("zhook");
const logging = @import("../logging.zig");
const mod_mutex = @import("../mutex.zig");
pub const module_name: [*:0]const u8 = "filecache";
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
var log_state: logging.Logger = .{};
pub inline fn rdtsc() u64 {
var lo: u32 = undefined;
@@ -88,7 +83,6 @@ pub fn archiveCacheLookup(h: u32, path: [*:0]const u8) ?ArchiveCacheEntry {
return null;
}
/// Insert: hash picks set, store in empty way or evict LRU.
/// Recompute block_entry pointer from archive's current block table + cached index.
/// block_entry = archive->block_table_data + index * 0x2C
pub fn computeBlockEntry(archive: u32, index: u32) u32 {
@@ -117,7 +111,6 @@ pub fn archiveCacheInsert(h: u32, path: [*:0]const u8, outer: u32, inner: u32, b
const entry = &set.entries[target];
entry.outer_archive = outer;
entry.inner_archive = inner;
// Convert block_entry pointer to index: (ptr - base) / 0x2C
if (block != 0 and inner != 0) {
const base = hook.readMem(u32, inner + 0x290);
entry.block_index = if (base != 0) (block - base) / 0x2C else 0;
@@ -151,27 +144,12 @@ pub fn getSlotOccupant(h: u32) ?[]const u8 {
return null;
}
pub const CacheStats = struct {
hits: u64, neg_hits: u64, misses: u64, stale: u64,
miss_p1: u64, miss_p2: u64, miss_p2_archive: u64,
entries: u32, total: u64,
};
pub fn getCacheStats() CacheStats {
return .{
.hits = cache_hits,
.neg_hits = cache_negative_hits,
.misses = cache_misses,
.stale = cache_stale,
.miss_p1 = cache_miss_p1,
.miss_p2 = cache_miss_p2,
.miss_p2_archive = cache_miss_p2_archive,
.entries = cache_entries,
.total = cache_hits + cache_negative_hits + cache_misses + cache_stale,
};
fn pct(part: u64, total: u64) u64 {
if (total == 0) return 0;
return part *| 1000 / total;
}
pub fn resetStats() void {
fn resetStats() void {
cache_hits = 0;
cache_negative_hits = 0;
cache_misses = 0;
@@ -183,72 +161,26 @@ pub fn resetStats() void {
miss_cycles = 0;
}
fn pct(part: u64, total: u64) u64 {
if (total == 0) return 0;
return part *| 1000 / total;
}
pub fn dumpStats() void {
const fc = getCacheStats();
if (fc.total == 0) return;
const hit_pct = pct(fc.hits + fc.neg_hits, fc.total);
// @3GHz: cycles/3000 = us, cycles/3000000 = ms
const total_hit_calls = fc.hits + fc.neg_hits;
pub fn dumpStats(lg: *logging.Logger) void {
const total = cache_hits + cache_negative_hits + cache_misses + cache_stale;
if (total == 0) return;
const hit_pct = pct(cache_hits + cache_negative_hits, total);
const total_hit_calls = cache_hits + cache_negative_hits;
const avg_hit = if (total_hit_calls > 0) hit_cycles / total_hit_calls else 0;
const avg_miss = if (fc.misses > 0) miss_cycles / fc.misses else 0;
// Without cache, hits would have cost avg_miss each
const avg_miss = if (cache_misses > 0) miss_cycles / cache_misses else 0;
const saved_us = if (avg_miss > avg_hit) total_hit_calls * (avg_miss - avg_hit) / 3000 else 0;
if (saved_us >= 2000) {
log_state.fmt(" file_cache: {d}.{d}% hit ({d}hit/{d}neg/{d}miss) {d} entries | saved {d}ms (avg hit={d}cy miss={d}cy)\n", .{
lg.fmt("file_cache: {d}.{d}% hit ({d}hit/{d}neg/{d}miss) {d} entries | saved {d}ms\n", .{
hit_pct / 10, hit_pct % 10,
fc.hits, fc.neg_hits, fc.misses, fc.entries,
saved_us / 1000, avg_hit, avg_miss,
cache_hits, cache_negative_hits, cache_misses, cache_entries,
saved_us / 1000,
});
} else {
log_state.fmt(" file_cache: {d}.{d}% hit ({d}hit/{d}neg/{d}miss) {d} entries | saved {d}us (avg hit={d}cy miss={d}cy)\n", .{
lg.fmt("file_cache: {d}.{d}% hit ({d}hit/{d}neg/{d}miss) {d} entries | saved {d}us\n", .{
hit_pct / 10, hit_pct % 10,
fc.hits, fc.neg_hits, fc.misses, fc.entries,
saved_us, avg_hit, avg_miss,
cache_hits, cache_negative_hits, cache_misses, cache_entries,
saved_us,
});
}
resetStats();
}
// Frame counting for periodic stats dump
const DUMP_FRAMES: u64 = 900; // ~15s at 60fps
var frame_count: u64 = 0;
const WorldUpdateFn = fn (u32) callconv(hook.cc.fastcall) void;
var world_update_hook: hook.Detour(WorldUpdateFn) = .{};
fn worldUpdateDetour(fc: u32) callconv(hook.cc.fastcall) void {
world_update_hook.callOriginal(.{fc});
frame_count +|= 1;
if (frame_count >= DUMP_FRAMES) {
dumpStats();
frame_count = 0;
}
}
// Module lifecycle
pub fn isActive() bool { return g_is_hook_owner; }
pub fn installHooks() void {
const result = mod_mutex.acquire(module_name);
g_mutex = result.handle;
g_is_hook_owner = result.is_owner;
if (!g_is_hook_owner) return;
log_state = logging.Logger.open(module_name, .console);
_ = world_update_hook.attach(0x482EA0, &worldUpdateDetour);
log_state.print("filecache: active\n");
}
pub fn removeHooks() void {
if (g_is_hook_owner) {
world_update_hook.detach();
dumpStats();
log_state.close();
mod_mutex.release(&g_mutex);
}
g_is_hook_owner = false;
}
+20 -3
View File
@@ -17,6 +17,8 @@ const hook = @import("zhook");
const logging = @import("../logging.zig");
const mod_mutex = @import("../mutex.zig");
const inflate_hook = @import("inflate_hook.zig");
const timer_fix = @import("timer_fix.zig");
const filecache = @import("filecache.zig");
pub const module_name: [*:0]const u8 = "performance";
@@ -147,10 +149,11 @@ 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)
// Dump stats every ~900 frames (~15s at 60fps)
frame_counter +|= 1;
if (frame_counter >= 450) {
if (frame_counter >= 900) {
inflate_hook.dumpStats();
filecache.dumpStats(&log);
frame_counter = 0;
}
}
@@ -284,15 +287,29 @@ pub fn installHooks() void {
// Silicon SSE binary patches
const patched = installPatches();
// libdeflate inflate comparison hook
// libdeflate inflate replacement
if (inflate_hook.install(log)) installed += 1;
// TSC timer calibration + OS timer tweaks
timer_fix.init();
const ti = timer_fix.getInfo();
if (ti.calibrated) {
if (ti.orig_freq == 1000) {
log.fmt("timer: TSC was OFF, enabled with freq {d}\n", .{ti.cal_freq});
} else {
log.fmt("timer: recalibrated TSC freq {d} -> {d} ({d}.{d}% drift)\n", .{ ti.orig_freq, ti.cal_freq, ti.diff_pct_x10 / 10, ti.diff_pct_x10 % 10 });
}
} else {
log.print("timer: already calibrated, skipping\n");
}
log.fmt("performance: {d} hooks, {d} patches installed\n", .{ installed, patched });
}
pub fn removeHooks() void {
if (g_is_hook_owner) {
inflate_hook.dumpStats();
filecache.dumpStats(&log);
inflate_hook.remove();
transform_hook.detach();
particle_hook.detach();
+1 -13
View File
@@ -15,7 +15,6 @@ const std = @import("std");
const hook = @import("zhook");
const logging = @import("../logging.zig");
const mod_mutex = @import("../mutex.zig");
const timer_fix = @import("timer_fix.zig");
extern fn clipPolygonToSinglePlane(u32, u32, u32) void;
extern fn buildTrianglePlanes(u32, u32, u32, u32, u32) u32;
extern fn rayTriangleIntersection(u32, u32, u32, u32, u32, u32) u32;
@@ -1571,18 +1570,7 @@ pub fn installHooks() void {
_ = matmul_hook.attach(0x7bc6a0, &matmulDetour);
_ = textline_hook.attach(0x5ce0c0, &textlineDetour);
// TSC timer calibration (ported from VanillaFixes)
timer_fix.init();
const ti = timer_fix.getInfo();
if (ti.calibrated) {
if (ti.orig_freq == 1000) {
log.fmt("timer_fix: TSC was OFF, enabled with freq {d}\n", .{ti.cal_freq});
} else {
log.fmt("timer_fix: recalibrated TSC freq {d} -> {d} ({d}.{d}% drift)\n", .{ ti.orig_freq, ti.cal_freq, ti.diff_pct_x10 / 10, ti.diff_pct_x10 % 10 });
}
} else if (ti.cal_freq > 0) {
log.print("timer_fix: already calibrated, skipping\n");
}
// Timer calibration now handled by performance module.
// blit_hub installed in lateInit() to clobber UnitXP's hook
log.print("transform44: 39 profiling hooks installed (blit_hub deferred)\n");