diff --git a/build.zig b/build.zig index d466f08..2078655 100644 --- a/build.zig +++ b/build.zig @@ -30,7 +30,6 @@ const module_list = [_]ModuleDesc{ .{ .name = "dpslog", .desc = "Enable structured combat log events for addons", .default = false }, .{ .name = "transform44", .desc = "Enable transformMatrix4x4 hook", .default = false }, .{ .name = "addonperf", .desc = "Enable addon memory/CPU profiling API" }, - .{ .name = "file_perf", .desc = "Enable file access profiling", .default = false, .addon_name = "FilePerf", .addon_hidden = true }, }; pub fn build(b: *std.Build) void { @@ -40,7 +39,7 @@ pub fn build(b: *std.Build) void { .abi = .msvc, .cpu_features_add = std.Target.x86.featureSet(&.{ .sse, .sse2 }), }); - const optimize = b.standardOptimizeOption(.{}); + const optimize = b.option(std.builtin.OptimizeMode, "optimize", "Optimization mode (default: ReleaseFast)") orelse .ReleaseFast; const build_options = b.addOptions(); addModuleOptions(b, build_options); @@ -52,7 +51,7 @@ pub fn build(b: *std.Build) void { }); const zhook_mod = zhook_dep.module("zhook"); - // Hot math — separate compilation unit, always ReleaseFast + // Hot math — separate compilation units, always ReleaseFast const clip_sse_obj = b.addObject(.{ .name = "clip_sse", .root_module = b.createModule(.{ @@ -61,6 +60,14 @@ pub fn build(b: *std.Build) void { .optimize = .ReleaseFast, }), }); + const bone_sse_obj = b.addObject(.{ + .name = "bone_sse", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/transform44/bone_sse.zig"), + .target = target, + .optimize = .ReleaseFast, + }), + }); const lib = b.addLibrary(.{ .name = "weirdutils", @@ -76,6 +83,7 @@ pub fn build(b: *std.Build) void { }), }); lib.root_module.addObject(clip_sse_obj); + lib.root_module.addObject(bone_sse_obj); b.installArtifact(lib); // Convenience step to build all single-module variants diff --git a/src/addons.zig b/src/addons.zig index 9538372..bad7133 100644 --- a/src/addons.zig +++ b/src/addons.zig @@ -267,7 +267,7 @@ const all_prefixes = buildAllPrefixes(); var active_prefixes: [all_prefixes.len]*const AddonPrefix = undefined; var active_count: usize = 0; -fn pruneInactivePrefixes() void { +pub fn pruneInactivePrefixes() void { active_count = 0; for (all_prefixes) |*prefix| { const active = if (prefix.module_name) |mod_name| module_active.isActive(mod_name) else true; diff --git a/src/logging.zig b/src/logging.zig index fcc04f2..7f31319 100644 --- a/src/logging.zig +++ b/src/logging.zig @@ -1,4 +1,5 @@ -//! Debug logging - compiles out entirely in non-Debug builds. +//! Logging - available in Debug, ReleaseSafe, and ReleaseFast builds. +//! Compiles out entirely in ReleaseSmall. //! //! Per-module Logger with optional file output and per-call destination routing. //! Log files are created lazily on first write -- modules that never log to @@ -28,7 +29,7 @@ const std = @import("std"); const builtin = @import("builtin"); -const debug = builtin.mode == .Debug; +const debug = builtin.mode != .ReleaseSmall; const WINAPI = std.builtin.CallingConvention.winapi; diff --git a/src/main.zig b/src/main.zig index b84a210..429df3c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -20,7 +20,6 @@ const build_opts = struct { const dpslog = @import("build_options").enable_dpslog; const transform44 = @import("build_options").enable_transform44; const addonperf = @import("build_options").enable_addonperf; - const file_perf = @import("build_options").enable_file_perf; }; // Conditional module imports @@ -39,7 +38,7 @@ const clickthrough = if (build_opts.clickthrough) @import("clickthrough/clickthr 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 file_perf = if (build_opts.file_perf) @import("file_perf/file_perf.zig") else struct {}; +const file_cache = if (build_opts.transform44) @import("transform44/file_cache.zig") else struct {}; const module_active = @import("module_active.zig"); @@ -113,9 +112,6 @@ fn registerLuaFunctions() void { registerFunction("ResetAddOnCPUUsage", @intFromPtr(&addonperf.luaResetAddOnCPUUsage)); registerFunction("GetScriptCPUUsage", @intFromPtr(&addonperf.luaGetScriptCPUUsage)); } - if (build_opts.file_perf and file_perf.isActive()) { - registerFunction("ResetFilePerfCounters", @intFromPtr(&file_perf.luaResetCounters)); - } if (build_opts.worldmarkers and markers.isActive()) { // User-facing functions stay global registerFunction("WorldMarker", @intFromPtr(&markers.luaWorldMarker)); @@ -206,6 +202,13 @@ var process_async_hook: hook.Detour(ProcessAsyncFn) = .{}; const LoadModelFn = fn (u32, u32, u32) callconv(hook.cc.thiscall) u32; var model_load_hook: hook.Detour(LoadModelFn) = .{}; +// File_FindInArchive (0x6549a0) — Storm internal MPQ file lookup +// __fastcall(ECX=archive_or_group, EDX=filename, stack: flags, out_inner_archive, +// out_outer_archive, out_block_entry, out_disk_path) → int +// Returns: 0=not found, 1=found in MPQ, 2=found on disk, 3=deleted +const FileFindFn = fn (u32, u32, u32, u32, u32, u32, u32) callconv(hook.cc.fastcall) u32; +var file_find_hook: hook.Detour(FileFindFn) = .{}; + // Windows API imports for async handling extern "kernel32" fn EnterCriticalSection(lpCriticalSection: *anyopaque) callconv(WINAPI) void; extern "kernel32" fn LeaveCriticalSection(lpCriticalSection: *anyopaque) callconv(WINAPI) void; @@ -273,10 +276,7 @@ fn openFileDetour( return 2; // success (non-zero type code) } - const tsc_start = if (build_opts.file_perf) file_perf.beginOpen() else 0; - const ret = open_file_hook.callOriginal(.{ archive_ptr, path, flags, handle_out }); - if (build_opts.file_perf) file_perf.endOpen(path, tsc_start); - return ret; + return open_file_hook.callOriginal(.{ archive_ptr, path, flags, handle_out }); } // --- Hook 2: GetFileSizeFromHandle (0x6487f0) --- @@ -292,17 +292,7 @@ fn getFileSizeDetour( return size; } - const size = get_file_size_hook.callOriginal(.{ file_ctx, high_size_out }); - - // Track file sizes for cache budget estimation - if (build_opts.file_perf and file_perf.isActive()) { - const path_ptr = hook.readMem(u32, file_ctx + 0x0C); - if (path_ptr != 0) { - file_perf.recordFileSize(@as([*:0]const u8, @ptrFromInt(path_ptr)), size); - } - } - - return size; + return get_file_size_hook.callOriginal(.{ file_ctx, high_size_out }); } // --- Hook 3: ReadFileFromMultipleSources (0x648460) --- @@ -338,19 +328,7 @@ fn readFileDetour( return 1; // success } - const ret = read_file_hook.callOriginal(.{ ctx, buffer, size, bytes_read_out, async_ptr, param6 }); - - // Track reads for profiling — caller return address tells us who calls ReadFile - if (build_opts.file_perf and file_perf.isActive() and ret != 0) { - const path_ptr = hook.readMem(u32, ctx + 0x0C); - if (path_ptr != 0) { - const actual_read = if (bytes_read_out) |out| out.* else size; - const caller_addr: u32 = @truncate(@returnAddress()); - file_perf.recordRead(@as([*:0]const u8, @ptrFromInt(path_ptr)), actual_read, caller_addr); - } - } - - return ret; + return read_file_hook.callOriginal(.{ ctx, buffer, size, bytes_read_out, async_ptr, param6 }); } // --- Hook 4: processAsyncFileOperation (0x647350) --- @@ -537,6 +515,112 @@ fn checkFileExistenceDetour(filename_ptr: u32, flags: u32, output_buffer_ptr: u3 return cfe_hook.callOriginal(.{ filename_ptr, flags, output_buffer_ptr }); } +// --- Hook 7: File_FindInArchive (0x6549a0) — archive cache short-circuit --- +// Intercepts the core MPQ file lookup to skip both chain walk and hash lookup +// on repeat opens. Caches {outer_archive, inner_archive, block_entry} per file. +// +// Called from two paths during each open: +// 1. FindFileInArchive wrapper (param_1=0): walks all archives, uses param_5 for output +// 2. File_FindInStorage (param_1=specific): single archive, uses param_4 + param_6 +// +// We cache on path 2 (has all data), serve both paths from cache on subsequent opens. + +fn fileFindDetour( + archive_or_group: u32, // ECX: 0 = search all, else specific archive/group + filename_ptr: u32, // EDX: filename string + flags: u32, + out_inner_archive: u32, // ptr to ptr: inner archive (File_FindInStorage uses this) + out_outer_archive: u32, // ptr to ptr: outer archive (FindFileInArchive wrapper uses this) + 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.transform44 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 }); + + // --- Time the original (baseline) path first --- + const baseline_start = transform44.rdtscPub(); + const orig_ret = file_find_hook.callOriginal(.{ archive_or_group, filename_ptr, flags, out_inner_archive, out_outer_archive, out_block_entry, out_disk_path }); + const baseline_elapsed = transform44.rdtscPub() - baseline_start; + + // --- Now time the cached path (overwrites output ptrs with same or cached values) --- + const cache_start = transform44.rdtscPub(); + + const path: [*:0]const u8 = @ptrFromInt(filename_ptr); + const h = file_cache.hashPath(path); + + if (file_cache.archiveCacheLookup(h, path)) |cached| { + if (cached.is_negative and archive_or_group == 0) { + file_cache.recordNegativeHit(); + const cache_elapsed = transform44.rdtscPub() - cache_start; + transform44.addFilefindCycles(cache_elapsed, baseline_elapsed); + return orig_ret; + } + + if (!cached.is_negative) { + // Path 1: search-all + if (archive_or_group == 0 and out_outer_archive != 0) { + @as(*align(1) u32, @ptrFromInt(out_outer_archive)).* = cached.outer_archive; + if (cached.outer_archive != 0) { + @as(*align(1) i32, @ptrFromInt(cached.outer_archive + 0x38)).* += 1; + } + if (out_inner_archive != 0) { + @as(*align(1) u32, @ptrFromInt(out_inner_archive)).* = cached.inner_archive; + if (cached.inner_archive != 0) { + @as(*align(1) i32, @ptrFromInt(cached.inner_archive + 0x38)).* += 1; + } + } + if (out_block_entry != 0) { + @as(*align(1) u32, @ptrFromInt(out_block_entry)).* = cached.block_entry; + } + file_cache.recordCacheHit(); + const cache_elapsed = transform44.rdtscPub() - cache_start; + transform44.addFilefindCycles(cache_elapsed, baseline_elapsed); + return orig_ret; + } + + // Path 2: specific archive + if (archive_or_group != 0 and (archive_or_group == cached.outer_archive or archive_or_group == cached.inner_archive)) { + if (out_outer_archive != 0) { + @as(*align(1) u32, @ptrFromInt(out_outer_archive)).* = cached.outer_archive; + if (cached.outer_archive != 0) { + @as(*align(1) i32, @ptrFromInt(cached.outer_archive + 0x38)).* += 1; + } + } + if (out_inner_archive != 0) { + @as(*align(1) u32, @ptrFromInt(out_inner_archive)).* = cached.inner_archive; + if (cached.inner_archive != 0) { + @as(*align(1) i32, @ptrFromInt(cached.inner_archive + 0x38)).* += 1; + } + } + if (out_block_entry != 0) { + @as(*align(1) u32, @ptrFromInt(out_block_entry)).* = cached.block_entry; + } + file_cache.recordCacheHit(); + const cache_elapsed = transform44.rdtscPub() - cache_start; + transform44.addFilefindCycles(cache_elapsed, baseline_elapsed); + return orig_ret; + } + } + } + + // Cache miss — populate cache from the original's results + file_cache.recordCacheMiss(); + if (archive_or_group != 0 and out_inner_archive != 0 and out_block_entry != 0) { + if (orig_ret == 1) { + const inner = hook.readMem(u32, out_inner_archive); + const block = hook.readMem(u32, out_block_entry); + file_cache.archiveCacheInsert(h, path, archive_or_group, inner, block, false); + } + } + if (archive_or_group == 0 and orig_ret == 0) { + file_cache.archiveCacheInsert(h, path, 0, 0, 0, true); + } + + const cache_elapsed = transform44.rdtscPub() - cache_start; + transform44.addFilefindCycles(cache_elapsed, baseline_elapsed); + return orig_ret; +} + // --- Install/remove in-memory file hooks --- fn installFileHooks() void { @@ -546,10 +630,15 @@ fn installFileHooks() void { _ = cleanup_file_handle_hook.attach(0x648730, &cleanupFileHandleDetour); _ = model_load_hook.attach(0x71d4e0, &loadModelAsyncDetour); _ = cfe_hook.attach(0x654DD0, &checkFileExistenceDetour); + if (build_opts.transform44) { + _ = file_find_hook.attach(0x6549a0, &fileFindDetour); + log.print("archive cache hook installed\n"); + } log.print("in-memory file hooks installed\n"); } fn removeFileHooks() void { + file_find_hook.detach(); cfe_hook.detach(); model_load_hook.detach(); process_async_hook.detach(); @@ -662,7 +751,6 @@ const modules = [_]ModuleHooks{ if (build_opts.dpslog) .{ .name = dpslog.module_name, .install = dpslog.installHooks, .remove = dpslog.removeHooks, .is_active = dpslog.isActive } else .{}, 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.file_perf) .{ .name = file_perf.module_name, .install = file_perf.installHooks, .remove = file_perf.removeHooks, .is_active = file_perf.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 .{}, @@ -763,6 +851,7 @@ fn disableModule(name: [*:0]const u8) callconv(.c) i32 { if (asciiEqlIgnoreCase(name, mod_name)) { if (m.remove) |rm| { rm(); + addons.pruneInactivePrefixes(); return 1; } return 0; @@ -786,6 +875,7 @@ fn disableAll() callconv(.c) i32 { count += 1; } } + addons.pruneInactivePrefixes(); // Detach core hooks shutdown_hook.detach(); diff --git a/src/transform44/file_cache.zig b/src/transform44/file_cache.zig new file mode 100644 index 0000000..b62c0ad --- /dev/null +++ b/src/transform44/file_cache.zig @@ -0,0 +1,107 @@ +//! file_cache — MPQ archive pointer cache for File_FindInArchive (0x6549a0) +//! +//! Direct-mapped cache: hash(filename) → {outer_archive, inner_archive, block_entry}. +//! Populated on successful File_FindInStorage calls (where all three values are +//! available). Used to skip both the archive chain walk and per-archive hash lookup. +//! +//! MPQ archives are loaded at startup and never modified, so cached pointers +//! (archive structs, block table entries) remain valid for the entire session. +//! +//! Cache verification uses stored filename comparison (not hash alone) to +//! guarantee no wrong data is ever returned on hash collisions. + +const std = @import("std"); + +// FNV-1a 32-bit, case-insensitive, backslash-normalized (for cache slot indexing). +pub fn hashPath(path: [*:0]const u8) u32 { + var h: u32 = 0x811c9dc5; + var i: usize = 0; + while (path[i] != 0) : (i += 1) { + var c = path[i]; + if (c >= 'A' and c <= 'Z') c += 32; + if (c == '\\') c = '/'; + h ^= c; + h *%= 0x01000193; + } + return h; +} + +// Longest filename across all MPQ archives is 122 chars (macOS .nib path in base.MPQ). +// Longest game-relevant path is 114 chars (WMO models). 128 covers all with margin. +// Paths exceeding this are not cached (safe fallback to original function). +const CACHE_NAME_LEN = 128; +const CACHE_SIZE = 16384; // power of 2, direct-mapped + +pub const ArchiveCacheEntry = struct { + outer_archive: u32 = 0, + inner_archive: u32 = 0, + block_entry: u32 = 0, + is_negative: bool = false, + name: [CACHE_NAME_LEN]u8 = .{0} ** CACHE_NAME_LEN, + name_len: u8 = 0, +}; + +var archive_cache: [CACHE_SIZE]ArchiveCacheEntry = @splat(ArchiveCacheEntry{}); +var cache_entries: u32 = 0; +var cache_hits: u64 = 0; +var cache_negative_hits: u64 = 0; +var cache_misses: u64 = 0; + +/// Lookup by hash (slot index) + filename verification. Returns null on miss or name mismatch. +pub fn archiveCacheLookup(h: u32, path: [*:0]const u8) ?ArchiveCacheEntry { + const idx = h & (CACHE_SIZE - 1); + const entry = &archive_cache[idx]; + if (entry.name_len == 0) return null; + const span = std.mem.span(path); + if (span.len != entry.name_len) return null; + const len: usize = entry.name_len; + if (!std.mem.eql(u8, entry.name[0..len], span[0..len])) return null; + return entry.*; +} + +/// Insert: hash picks slot, filename stored for verification on future lookups. +pub fn archiveCacheInsert(h: u32, path: [*:0]const u8, outer: u32, inner: u32, block: u32, negative: bool) void { + const span = std.mem.span(path); + if (span.len > CACHE_NAME_LEN) return; // too long to cache, skip + const idx = h & (CACHE_SIZE - 1); + if (archive_cache[idx].name_len == 0) cache_entries += 1; + const entry = &archive_cache[idx]; + entry.outer_archive = outer; + entry.inner_archive = inner; + entry.block_entry = block; + entry.is_negative = negative; + const len: u8 = @intCast(span.len); + @memcpy(entry.name[0..len], span[0..len]); + entry.name_len = len; +} + +pub fn recordCacheHit() void { + cache_hits +|= 1; +} + +pub fn recordNegativeHit() void { + cache_negative_hits +|= 1; +} + +pub fn recordCacheMiss() void { + cache_misses +|= 1; +} + +pub const CacheStats = struct { hits: u64, neg_hits: u64, misses: u64, entries: u32, total: u64 }; + +pub fn getCacheStats() CacheStats { + return .{ + .hits = cache_hits, + .neg_hits = cache_negative_hits, + .misses = cache_misses, + .entries = cache_entries, + .total = cache_hits + cache_negative_hits + cache_misses, + }; +} + +/// Reset cache stats but preserve cached data (archive pointers remain valid). +pub fn resetStats() void { + cache_hits = 0; + cache_negative_hits = 0; + cache_misses = 0; +} diff --git a/src/transform44/transform44.zig b/src/transform44/transform44.zig index eb5150c..ad53100 100644 --- a/src/transform44/transform44.zig +++ b/src/transform44/transform44.zig @@ -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{}; }