Add MPQ archive file cache: skip redundant chain walks on repeat file opens

Hook File_FindInArchive (0x6549a0) with a direct-mapped filename-verified
cache. First open does the full MPQ chain walk (~60K cycles), subsequent
opens hit the cache (~300 cycles). 80% hit rate in gameplay testing.

Cache design: 16384 entries, FNV-1a hash for slot index, raw filename
comparison (128 bytes) for collision safety. Stores outer_archive,
inner_archive, and block_entry per file. Negative cache for not-found files.
Refcount at +0x38 incremented on all output archives to match original
File_FindInArchive behavior (verified via Ghidra: FindAndIncrementResourceReference
at 0x650780, DecrementResourceReference at 0x6507e0).

Also: default build changed to ReleaseFast (works around Zig fastcall inreg
bug in Debug mode), logging gate changed to != ReleaseSmall, file cache
integrated as transform44 sub-module with A/B comparison timing in readout.
This commit is contained in:
MarcelineVQ
2026-03-13 23:05:38 -07:00
parent bb8a6ef8a6
commit 55735d7b9e
6 changed files with 295 additions and 47 deletions
+49 -7
View File
@@ -15,11 +15,13 @@ const std = @import("std");
const hook = @import("zhook");
const logging = @import("../logging.zig");
const mod_mutex = @import("../mutex.zig");
pub const file_cache = @import("file_cache.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;
extern fn rotateMatrixByAxisAngle(u32, u32, u32, u32) void;
extern fn multiplyMatrix4x4(u32, u32, u32) u32;
extern fn transformMatrix4x4_SSE(u32, u32, u32, u32, u32) void;
pub const module_name: [*:0]const u8 = "transform44";
@@ -155,6 +157,9 @@ const ProfState = struct {
matmul_cycles: u64 = 0,
textline_calls: u64 = 0, // renderTextLine (0x5ce0c0)
textline_cycles: u64 = 0,
filefind_calls: u64 = 0, // File_FindInArchive cache (0x6549a0)
filefind_cycles: u64 = 0, // cached path
filefind_baseline_cycles: u64 = 0, // original path (same calls)
};
// =============================================================================
@@ -185,6 +190,17 @@ inline fn rdtsc() u64 {
return @as(u64, hi) << 32 | lo;
}
/// Called from main.zig fileFindDetour to accumulate per-call timing.
/// cached_cycles = time for cache path, baseline_cycles = time for original path.
pub fn addFilefindCycles(cached_cycles: u64, baseline_cycles: u64) void {
prof.filefind_cycles +|= cached_cycles;
prof.filefind_baseline_cycles +|= baseline_cycles;
prof.filefind_calls +|= 1;
}
/// Expose rdtsc for use by main.zig's fileFindDetour.
pub const rdtscPub = rdtsc;
// =============================================================================
// Hook: transformMatrix4x4 (0x714260)
// __thiscall(ECX=SceneObject*, stack: Matrix4x4* ×4)
@@ -229,6 +245,8 @@ fn transformDetour(this: u32, edx: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u
t44_depth +|= 1;
if (t44_depth > prof.t44_max_depth) prof.t44_max_depth = t44_depth;
// bone_sse disabled — investigating crash in post-bone-loop sections
_ = transformMatrix4x4_SSE;
transform_hook.callOriginal(.{ this, edx, mat1, mat2, mat3, mat4 });
t44_depth -|= 1;
@@ -1238,16 +1256,29 @@ fn dumpStats() void {
.{ .name = "partsetup", .cycles = prof.partsetup_cycles, .calls = prof.partsetup_calls },
.{ .name = "matmul", .cycles = prof.matmul_cycles, .calls = prof.matmul_calls },
.{ .name = "textline", .cycles = prof.textline_cycles, .calls = prof.textline_calls },
.{ .name = "ff_cache", .cycles = prof.filefind_cycles, .calls = prof.filefind_calls },
.{ .name = "ff_orig", .cycles = prof.filefind_baseline_cycles, .calls = prof.filefind_calls },
};
for (hotspots) |h| {
if (h.calls > 0) {
const hp = pct(h.cycles, wall);
log.fmt(" {s}: {d}.{d}% {d}ms {d}c/f\n", .{
h.name,
hp / 10, hp % 10,
h.cycles / MS_DIVISOR,
h.calls / f,
});
const name = std.mem.span(h.name);
const is_micro = std.mem.eql(u8, name, "ff_cache") or std.mem.eql(u8, name, "ff_orig") or std.mem.eql(u8, name, "glyph");
if (is_micro) {
log.fmt(" {s}: {d}.{d}% {d}us {d}c/f\n", .{
h.name,
hp / 10, hp % 10,
h.cycles / 3_000,
h.calls / f,
});
} else {
log.fmt(" {s}: {d}.{d}% {d}ms {d}c/f\n", .{
h.name,
hp / 10, hp % 10,
h.cycles / MS_DIVISOR,
h.calls / f,
});
}
}
}
@@ -1262,7 +1293,18 @@ fn dumpStats() void {
});
}
// Flip A/B mode for next period
// File cache stats
const fc = file_cache.getCacheStats();
if (fc.total > 0) {
const fc_hit_pct = pct(fc.hits + fc.neg_hits, fc.total);
log.fmt(" file_cache: {d}.{d}% hit ({d}hit/{d}neg/{d}miss) {d} entries\n", .{
fc_hit_pct / 10, fc_hit_pct % 10,
fc.hits, fc.neg_hits, fc.misses, fc.entries,
});
}
// Reset per-period cache stats, flip A/B mode
file_cache.resetStats();
ab_use_custom = !ab_use_custom;
prof = ProfState{};
}