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
-249
View File
@@ -1,249 +0,0 @@
//! timer_fix -- TSC calibration + OS timer tweaks (ported from VanillaFixes)
//!
//! The vanilla client poorly calibrates its RDTSC frequency, causing animation
//! stutter. We recalibrate using QueryPerformanceFrequency as reference and
//! patch the game's timer globals. A/B tested: BASELINE restores original
//! values, CUSTOM applies calibrated values.
//!
//! Timer globals (1.12.1 build 5875):
//! pUseTSC: 0x00884c80 (BOOL)
//! pTimerTicksPerSecond: 0x008332c0 (u64)
//! pTimerToMilliseconds: 0x008332c8 (f64)
//! pTimerOffset: 0x00884c88 (f64)
const hook = @import("zhook");
const ADDR_USE_TSC: usize = 0x00884c80;
const ADDR_TICKS_PER_SEC: usize = 0x008332c0;
const ADDR_TO_MS: usize = 0x008332c8;
const ADDR_OFFSET: usize = 0x00884c88;
const WINAPI = @import("std").builtin.CallingConvention.winapi;
const LARGE_INTEGER = extern struct { QuadPart: i64 };
extern "kernel32" fn QueryPerformanceCounter(lpPerformanceCount: *LARGE_INTEGER) callconv(WINAPI) i32;
extern "kernel32" fn QueryPerformanceFrequency(lpFrequency: *LARGE_INTEGER) callconv(WINAPI) i32;
extern "kernel32" fn GetTickCount() callconv(WINAPI) u32;
extern "kernel32" fn GetCurrentThread() callconv(WINAPI) usize;
extern "kernel32" fn SetThreadPriority(hThread: usize, nPriority: i32) callconv(WINAPI) i32;
extern "kernel32" fn GetCurrentProcessorNumber() callconv(WINAPI) u32;
extern "kernel32" fn SetThreadAffinityMask(hThread: usize, dwThreadAffinityMask: usize) callconv(WINAPI) usize;
extern "kernel32" fn Sleep(dwMilliseconds: u32) callconv(WINAPI) void;
// Windows API for OS timer tweaks
extern "kernel32" fn GetModuleHandleA(lpModuleName: [*:0]const u8) callconv(WINAPI) ?*anyopaque;
extern "kernel32" fn GetProcAddress(hModule: *anyopaque, lpProcName: [*:0]const u8) callconv(WINAPI) ?*anyopaque;
extern "kernel32" fn GetCurrentProcess() callconv(WINAPI) usize;
fn increaseTimerResolution() void {
const ntdll = GetModuleHandleA("ntdll") orelse return;
const NtQueryFn = *const fn (*u32, *u32, *u32) callconv(WINAPI) i32;
const NtSetFn = *const fn (u32, i32, *u32) callconv(WINAPI) i32;
const query_ptr = GetProcAddress(ntdll, "NtQueryTimerResolution");
const set_ptr = GetProcAddress(ntdll, "NtSetTimerResolution");
if (query_ptr != null and set_ptr != null) {
const query: NtQueryFn = @ptrCast(query_ptr.?);
const set: NtSetFn = @ptrCast(set_ptr.?);
var min_res: u32 = 0;
var max_res: u32 = 0;
var cur_res: u32 = 0;
_ = query(&min_res, &max_res, &cur_res);
// Request 0.5ms (5000 * 100ns) or hardware max, whichever is larger
const desired = @max(max_res, 5000);
var actual: u32 = 0;
_ = set(desired, 1, &actual); // 1 = TRUE (enable)
return;
}
// Fallback: timeBeginPeriod(1) via winmm
const winmm = GetModuleHandleA("winmm") orelse return;
const TbpFn = *const fn (u32) callconv(WINAPI) u32;
const tbp_ptr = GetProcAddress(winmm, "timeBeginPeriod") orelse return;
const tbp: TbpFn = @ptrCast(tbp_ptr);
_ = tbp(1);
}
fn disablePowerThrottling() void {
const kernel32 = GetModuleHandleA("kernel32") orelse return;
const SpiPtr = GetProcAddress(kernel32, "SetProcessInformation") orelse return;
// ProcessPowerThrottling = 4
const PowerThrottlingState = extern struct {
Version: u32,
ControlMask: u32,
StateMask: u32,
};
const SetProcessInfoFn = *const fn (usize, u32, *PowerThrottlingState, u32) callconv(WINAPI) i32;
const setProcessInfo: SetProcessInfoFn = @ptrCast(SpiPtr);
const process = GetCurrentProcess();
// PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION = 2
var state1 = PowerThrottlingState{ .Version = 1, .ControlMask = 2, .StateMask = 0 };
_ = setProcessInfo(process, 4, &state1, @sizeOf(PowerThrottlingState));
// PROCESS_POWER_THROTTLING_EXECUTION_SPEED = 1
var state2 = PowerThrottlingState{ .Version = 1, .ControlMask = 1, .StateMask = 0 };
_ = setProcessInfo(process, 4, &state2, @sizeOf(PowerThrottlingState));
}
// Saved original values (before calibration) for A/B testing
var orig_ticks_per_sec: u64 = 0;
var orig_to_ms: f64 = 0;
var orig_offset: f64 = 0;
// Calibrated values
var cal_ticks_per_sec: u64 = 0;
var cal_to_ms: f64 = 0;
var cal_offset: f64 = 0;
var is_calibrated: bool = false;
inline fn rdtsc() u64 {
var lo: u32 = undefined;
var hi: u32 = undefined;
asm volatile ("rdtsc"
: [lo] "={eax}" (lo),
[hi] "={edx}" (hi),
);
return @as(u64, hi) << 32 | lo;
}
fn timeSample() struct { qpc: i64, tsc: u64 } {
Sleep(0); // yield to get fresh timeslice
var qpc: LARGE_INTEGER = .{ .QuadPart = 0 };
_ = QueryPerformanceCounter(&qpc);
const tsc = rdtsc();
asm volatile ("lfence" ::: "memory");
return .{ .qpc = qpc.QuadPart, .tsc = tsc };
}
fn calibrateTSC() u64 {
const thread = GetCurrentThread();
_ = SetThreadPriority(thread, 2); // THREAD_PRIORITY_HIGHEST
const core: u5 = @truncate(GetCurrentProcessorNumber());
_ = SetThreadAffinityMask(thread, @as(usize, 1) << core);
const baseline = timeSample();
Sleep(500);
const after = timeSample();
_ = SetThreadPriority(thread, 0); // THREAD_PRIORITY_NORMAL
_ = SetThreadAffinityMask(thread, 0); // all cores
var freq: LARGE_INTEGER = .{ .QuadPart = 0 };
_ = QueryPerformanceFrequency(&freq);
const qpc_delta: f64 = @floatFromInt(after.qpc - baseline.qpc);
const tsc_delta: f64 = @floatFromInt(after.tsc - baseline.tsc);
const elapsed_sec = qpc_delta / @as(f64, @floatFromInt(freq.QuadPart));
return @intFromFloat(@round(tsc_delta / elapsed_sec));
}
fn writeTimerValues(ticks: u64, to_ms: f64, offset: f64) void {
const ticks_bytes = @as([8]u8, @bitCast(ticks));
hook.writeProtected(ADDR_TICKS_PER_SEC, &ticks_bytes);
const ms_bytes = @as([8]u8, @bitCast(to_ms));
hook.writeProtected(ADDR_TO_MS, &ms_bytes);
const off_bytes = @as([8]u8, @bitCast(offset));
hook.writeProtected(ADDR_OFFSET, &off_bytes);
}
/// Calibrate and save both original and calibrated values.
/// Called once from transform44.installHooks().
pub fn init() void {
// OS-level tweaks (independent of TSC calibration)
increaseTimerResolution();
disablePowerThrottling();
const tsc_was_enabled = hook.readMem(u32, ADDR_USE_TSC) != 0;
// Save original values (if TSC was already enabled)
if (tsc_was_enabled) {
orig_ticks_per_sec = hook.readMem(u64, ADDR_TICKS_PER_SEC);
const orig_ms_bytes = hook.readMem(u64, ADDR_TO_MS);
orig_to_ms = @bitCast(orig_ms_bytes);
const orig_off_bytes = hook.readMem(u64, ADDR_OFFSET);
orig_offset = @bitCast(orig_off_bytes);
} else {
// TSC not in use -- game is using GetTickCount (1000 ticks/sec).
// Save those as "original" for A/B comparison.
orig_ticks_per_sec = 1000;
orig_to_ms = 1.0;
orig_offset = 0;
}
// Calibrate
cal_ticks_per_sec = calibrateTSC();
cal_to_ms = 1000.0 / @as(f64, @floatFromInt(cal_ticks_per_sec));
// Align TSC epoch with GetTickCount
const gtc: f64 = @floatFromInt(GetTickCount());
const tsc_ms: f64 = @as(f64, @floatFromInt(rdtsc())) * cal_to_ms;
cal_offset = gtc - tsc_ms;
// Check if already well-calibrated (within 0.1%)
if (tsc_was_enabled) {
const orig_f: f64 = @floatFromInt(orig_ticks_per_sec);
const cal_f: f64 = @floatFromInt(cal_ticks_per_sec);
const diff_pct = @abs(cal_f - orig_f) / cal_f * 100.0;
if (diff_pct < 0.1) {
// Already calibrated (VanillaFixes or similar), keep original
is_calibrated = false;
return;
}
}
is_calibrated = true;
// Enable TSC mode if it wasn't already
if (!tsc_was_enabled) {
const one = @as([4]u8, @bitCast(@as(u32, 1)));
hook.writeProtected(ADDR_USE_TSC, &one);
}
// Apply calibrated values
writeTimerValues(cal_ticks_per_sec, cal_to_ms, cal_offset);
}
/// Switch to calibrated (CUSTOM) timer values.
pub fn applyCalibrated() void {
if (!is_calibrated) return;
const one = @as([4]u8, @bitCast(@as(u32, 1)));
hook.writeProtected(ADDR_USE_TSC, &one);
writeTimerValues(cal_ticks_per_sec, cal_to_ms, cal_offset);
}
/// Restore original (BASELINE) timer values.
pub fn applyOriginal() void {
if (!is_calibrated) return;
// If TSC was originally disabled, restore that state
if (orig_ticks_per_sec == 1000) {
const zero = @as([4]u8, @bitCast(@as(u32, 0)));
hook.writeProtected(ADDR_USE_TSC, &zero);
}
writeTimerValues(orig_ticks_per_sec, orig_to_ms, orig_offset);
}
pub const TimerInfo = struct {
calibrated: bool,
orig_freq: u64,
cal_freq: u64,
diff_pct_x10: u64, // tenths of a percent
};
pub fn getInfo() TimerInfo {
const orig_f: f64 = @floatFromInt(orig_ticks_per_sec);
const cal_f: f64 = @floatFromInt(cal_ticks_per_sec);
const diff = if (cal_f > 0) @abs(cal_f - orig_f) / cal_f * 1000.0 else 0;
return .{
.calibrated = is_calibrated,
.orig_freq = orig_ticks_per_sec,
.cal_freq = cal_ticks_per_sec,
.diff_pct_x10 = @intFromFloat(diff),
};
}
+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");