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:
+7
-26
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user