From 14ba112b0dcc047a45160a5e02d41d4fdce76959 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Thu, 26 Mar 2026 11:38:33 -0700 Subject: [PATCH] perf: fix deferred divide precision, threadlocal TLS, MSVC target Removed deferred divide from both ray-tri functions -- the det-scaled epsilon comparisons lose precision for near-parallel rays, potentially accepting triangles the original rejects. Downstream SetupBoxFrustum then writes to wrong globals near 0xCE6738 (SGroupPtr), causing ERROR #124 on exit. inflate_hook: replaced manual FS:0x24 thread ID pool with Zig native threadlocal. Fixes potential crash on Wine where FS segment layout may differ. Build: switched from GNU to MSVC ABI (4-5K smaller per DLL, no .eh_frame unwind tables). Added setjmp.h stub for libdeflate MSVC build. Added noperf build variant (zig build noperf). --- RELEASING.md | 35 +++++++-- build.zig | 93 ++++++++++++++++------- src/bench/main.zig | 3 +- src/performance/cull_sse.zig | 61 ++++----------- src/performance/inflate_hook.zig | 52 +++---------- src/performance/libdeflate/stubs/setjmp.h | 3 + src/performance/weirdperformance.zig | 4 +- 7 files changed, 128 insertions(+), 123 deletions(-) create mode 100644 src/performance/libdeflate/stubs/setjmp.h diff --git a/RELEASING.md b/RELEASING.md index c5bf349..9825ed9 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -38,7 +38,21 @@ git push origin main After that, the remote `main` only needs updating when `DLL_README.md` changes. -## 1. Build DLLs +## 1. Bump module versions + +Each module has a `version` field in `build.zig`'s `module_list`. This is what +`GetWeirdUtilsVersion()` reports to Lua addons. Before building, bump the +version for any module that has changed since the last release: + +```zig +// build.zig — module_list +.{ .name = "minimapicons", .version = "1.1", ... }, +``` + +Only bump modules that actually changed. Use `git log --oneline -- src//` +to check what changed since the last release tag. + +## 2. Build DLLs Decide which modules to include in this release. Check `build.zig` for the current list of module flags (`b.option(bool, ...)` declarations) and their @@ -71,6 +85,16 @@ zig build all-variants -Doptimize=ReleaseSmall This builds all variants - you only attach the ones for this release. +### No-performance variant + +```sh +zig build noperf -Doptimize=ReleaseSmall +``` + +Builds `weirdutils_noperf.dll` - the combined DLL with all default modules +except `weirdperformance`. Provided as a release asset for users who experience +issues with the performance module (e.g. Wine compatibility). + ### Output locations | Artifact | Path | @@ -84,7 +108,7 @@ Verify: ls -lh zig-out/bin/weirdutils.dll zig-out/variants/*.dll ``` -## 2. Update the remote README +## 3. Update the remote README The remote README should match the features in this release. Start from `DLL_README.md` and remove the sections for modules not being released - @@ -103,7 +127,7 @@ git commit -m "Update README for vX.Y.Z" git push origin main ``` -## 3. Write the release notes +## 4. Write the release notes Use this template - fill in the sections that apply, delete the rest. Use `-` (not em dash) anywhere a dash would be used. @@ -132,7 +156,7 @@ of what they're getting: - `weirdutils.dll` includes all features; individual DLLs are also provided ``` -## 4. Create the release and upload DLLs +## 5. Create the release and upload DLLs Uses `tea` (Gitea/Forgejo CLI) which handles release creation, tagging, and asset upload in one command. The tag is created on the remote automatically. @@ -167,7 +191,7 @@ tea release assets delete --repo MarcelineVQ/WeirdUtils -y v0.4.0 minimapicons.d tea release assets create --repo MarcelineVQ/WeirdUtils v0.4.0 zig-out/variants/minimapicons.dll ``` -## 5. Hide source archives +## 6. Hide source archives Codeberg attaches empty source tar/zip by default. Hide them via API: @@ -200,6 +224,7 @@ print(r[0]['id']) if r else print('not found') ## Checklist +- [ ] Module versions bumped in `build.zig` for changed modules - [ ] Check `RELEASE_NOTES.md` for unreleased changes — move into release notes - [ ] Built with `ReleaseSmall` (both default and `all-variants`) - [ ] Remote README updated — no unreleased module sections or names diff --git a/build.zig b/build.zig index d7795c8..5854dde 100644 --- a/build.zig +++ b/build.zig @@ -16,38 +16,43 @@ const ModuleDesc = struct { /// Single source of truth for all modules. Adding a module here is enough /// to wire up the build option, build_options passthrough, and DLL variant. const module_list = [_]ModuleDesc{ - .{ .name = "pngscreenshots", .desc = "Enable screenshot module", .src_dir = "screenshot" }, - .{ .name = "interact", .desc = "Enable interact module", .addon_name = "Interact" }, - .{ .name = "outline", .desc = "Enable outline module", .default = false, .addon_name = "Outline" }, - .{ .name = "worldmarkers", .desc = "Enable world markers module", .addon_name = "WorldMarkers", .addon_hidden = true }, - .{ .name = "framecrash", .desc = "Enable framecrash fix", .default = false }, - .{ .name = "logsessions", .desc = "Enable log session rotation", .addon_name = "LogSessions" }, - .{ .name = "minimapicons", .desc = "Enable custom minimap icons", .addon_name = "MinimapIcons" }, - .{ .name = "transmogfix", .desc = "Enable transmog update coalescing" }, - .{ .name = "customassets", .desc = "Enable loose file loading & permissive patch glob" }, - .{ .name = "healtextfix", .desc = "Enable SuperWoW heal text fix" }, - .{ .name = "bigcursor", .desc = "Enable big cursor module" }, - .{ .name = "clickthrough", .desc = "Enable GO click-through (enlarge GO model bounds)" }, - .{ .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 = "ssemaths", .desc = "Enable UnitXP x87 math polyfill replacements (SSE)", .default = false }, - .{ .name = "silicon", .desc = "Enable SSE2 math replacements (ported from libSiliconPatch)", .default = false }, - .{ .name = "weirdperformance", .desc = "Enable production performance optimizations (SSE, inflate, filecache, timer)", .default = true }, - .{ .name = "guidcache", .desc = "Enable GUID lookup cache (standalone test)", .default = false }, + .{ .name = "pngscreenshots", .version = "1.0", .desc = "Enable screenshot module", .src_dir = "screenshot" }, + .{ .name = "interact", .version = "1.0", .desc = "Enable interact module", .addon_name = "Interact" }, + .{ .name = "outline", .version = "1.0", .desc = "Enable outline module", .default = false, .addon_name = "Outline" }, + .{ .name = "worldmarkers", .version = "1.0", .desc = "Enable world markers module", .addon_name = "WorldMarkers", .addon_hidden = true }, + .{ .name = "framecrash", .version = "1.0", .desc = "Enable framecrash fix", .default = false }, + .{ .name = "logsessions", .version = "1.0", .desc = "Enable log session rotation", .addon_name = "LogSessions" }, + .{ .name = "minimapicons", .version = "1.0", .desc = "Enable custom minimap icons", .addon_name = "MinimapIcons" }, + .{ .name = "transmogfix", .version = "1.0", .desc = "Enable transmog update coalescing" }, + .{ .name = "customassets", .version = "1.0", .desc = "Enable loose file loading & permissive patch glob" }, + .{ .name = "healtextfix", .version = "1.0", .desc = "Enable SuperWoW heal text fix" }, + .{ .name = "bigcursor", .version = "1.0", .desc = "Enable big cursor module" }, + .{ .name = "clickthrough", .version = "1.0", .desc = "Enable GO click-through (enlarge GO model bounds)" }, + .{ .name = "dpslog", .version = "0.1", .desc = "Enable structured combat log events for addons", .default = false }, + .{ .name = "transform44", .version = "1.0", .desc = "Enable transform44 profiling/A/B testing (dev only)", .default = false }, + .{ .name = "addonperf", .version = "1.0", .desc = "Enable addon memory/CPU profiling API", .default = false }, + .{ .name = "ssemaths", .version = "1.0", .desc = "Enable UnitXP x87 math polyfill replacements (SSE)", .default = false }, + .{ .name = "silicon", .version = "1.0", .desc = "Enable SSE2 math replacements (ported from libSiliconPatch)", .default = false }, + .{ .name = "weirdperformance", .version = "1.1", .desc = "Enable production performance optimizations (SSE, inflate, filecache, timer)", .default = true }, }; pub fn build(b: *std.Build) void { const target = b.resolveTargetQuery(.{ .cpu_arch = .x86, .os_tag = .windows, - .abi = .gnu, + .abi = .msvc, .cpu_features_add = std.Target.x86.featureSet(&.{ .sse, .sse2 }), }); const optimize = b.option(std.builtin.OptimizeMode, "optimize", "Optimization mode (default: ReleaseFast)") orelse .ReleaseFast; + // Resolve module enable flags once (b.option can only be called once per name) + var module_enabled: [module_list.len]bool = undefined; + inline for (module_list, 0..) |m, i| { + module_enabled[i] = b.option(bool, m.name, m.desc) orelse m.default; + } + const build_options = b.addOptions(); - addModuleOptions(b, build_options); + addModuleOptionsFromArray(b, build_options, &module_enabled); const build_options_module = build_options.createModule(); const zhook_dep = b.dependency("zhook", .{ @@ -85,7 +90,7 @@ pub fn build(b: *std.Build) void { const bone_sse_target = b.resolveTargetQuery(.{ .cpu_arch = .x86, .os_tag = .windows, - .abi = .gnu, + .abi = .msvc, .cpu_features_add = std.Target.x86.featureSet(&.{ .sse, .sse2, .sse3, .sse4_1, .fma, .avx }), }); const bone_sse_obj = b.addObject(.{ @@ -102,7 +107,7 @@ pub fn build(b: *std.Build) void { const ref_target = b.resolveTargetQuery(.{ .cpu_arch = .x86, .os_tag = .windows, - .abi = .gnu, + .abi = .msvc, .cpu_features_sub = std.Target.x86.featureSet(&.{ .sse, .sse2 }), }); const bone_sse_ref_obj = b.addObject(.{ @@ -181,6 +186,7 @@ pub fn build(b: *std.Build) void { }, .flags = &.{"-DLIBDEFLATE_ASSEMBLER_DOES_NOT_SUPPORT_AVX512VNNI"}, }); + libdeflate.root_module.addIncludePath(b.path("src/performance/libdeflate/stubs")); libdeflate.root_module.addIncludePath(b.path("src/performance/libdeflate")); libdeflate.root_module.addIncludePath(b.path("src/performance/libdeflate/lib")); @@ -204,12 +210,9 @@ pub fn build(b: *std.Build) void { if (comptime std.mem.eql(u8, module_name, "weirdperformance")) { mod.addObject(self.clip_sse); mod.addObject(self.cull_sse); - mod.addObject(self.entity_sse); mod.addObject(self.bone_sse); - mod.addObject(self.bone_sse_ref); mod.addObject(self.silicon_sse); mod.addObject(self.particle_sse); - mod.addObject(self.particle_ref); mod.addObjectFile(self.libdeflate.getEmittedBin()); } if (comptime std.mem.eql(u8, module_name, "transform44")) { @@ -357,8 +360,40 @@ pub fn build(b: *std.Build) void { run_step.dependOn(&run_bench.step); } + // Build combined DLL without performance module + const noperf_install = noperf_blk: { + const noperf_step = b.step("noperf", "Build weirdutils_noperf.dll (no performance module)"); + const noperf_opts = b.addOptions(); + var noperf_enabled = module_enabled; + // Force weirdperformance off + inline for (module_list, 0..) |m, i| { + if (comptime std.mem.eql(u8, m.name, "weirdperformance")) noperf_enabled[i] = false; + } + addModuleOptionsFromArray(b, noperf_opts, &noperf_enabled); + + const noperf_lib = b.addLibrary(.{ + .name = "weirdutils_noperf", + .linkage = .dynamic, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "zhook", .module = zhook_mod }, + .{ .name = "build_options", .module = noperf_opts.createModule() }, + }, + }), + }); + const noperf_install = b.addInstallArtifact(noperf_lib, .{ + .dest_dir = .{ .override = .{ .custom = "variants" } }, + }); + noperf_step.dependOn(&noperf_install.step); + break :noperf_blk noperf_install; + }; + // Convenience step to build all single-module variants const build_all_step = b.step("all-variants", "Build all DLL variants"); + build_all_step.dependOn(&noperf_install.step); inline for (module_list) |variant_mod| { @setEvalBranchQuota(10000); @@ -410,9 +445,9 @@ pub fn build(b: *std.Build) void { // Build options: enable flags + scanned file lists for each module // ============================================================================= -fn addModuleOptions(b: *std.Build, opts: *std.Build.Step.Options) void { - inline for (module_list) |mod| { - opts.addOption(bool, "enable_" ++ mod.name, b.option(bool, mod.name, mod.desc) orelse mod.default); +fn addModuleOptionsFromArray(b: *std.Build, opts: *std.Build.Step.Options, enabled: *const [module_list.len]bool) void { + inline for (module_list, 0..) |mod, i| { + opts.addOption(bool, "enable_" ++ mod.name, enabled[i]); } // Pass full module name/version lists so main.zig/addons.zig can use them at comptime const names: []const []const u8 = comptime blk: { diff --git a/src/bench/main.zig b/src/bench/main.zig index fa310dd..e2295e3 100644 --- a/src/bench/main.zig +++ b/src/bench/main.zig @@ -241,9 +241,10 @@ pub fn main() void { // AddToSpatialGrid -- linked list requires game state, A/B test in-game only // bench_addToSpatialGrid(); - if (false) { // disabled: not testing these right now bench_collisionDetection(); bench_rayTriIndexedInt(); + + if (false) { // disabled bench_entityUpdate(); } diff --git a/src/performance/cull_sse.zig b/src/performance/cull_sse.zig index f7147dd..c0f9b69 100644 --- a/src/performance/cull_sse.zig +++ b/src/performance/cull_sse.zig @@ -174,34 +174,22 @@ export fn rayTriIntersectIndexedInt( if (det > -1e-6 and det < 1e-6) return 0; + const inv_det = 1.0 / det; const tvec = ray_origin - v0; - const u_raw = dot3(tvec, pvec); - const det_neg_eps = det * neg_eps; - const det_one_plus = det * one_plus_eps; - if (det > 0) { - if (u_raw < det_neg_eps or u_raw > det_one_plus) return 0; - } else { - if (u_raw > det_neg_eps or u_raw < det_one_plus) return 0; - } + const u = dot3(tvec, pvec) * inv_det; + if (u < neg_eps or u > one_plus_eps) return 0; const qvec = cross(tvec, edge1); - const v_raw = dot3(ray_dir, qvec); + const v = dot3(ray_dir, qvec) * inv_det; + if (v < neg_eps or (u + v) > one_plus_eps) return 0; - if (det > 0) { - if (v_raw < det_neg_eps or (u_raw + v_raw) > det_one_plus) return 0; - } else { - if (v_raw > det_neg_eps or (u_raw + v_raw) < det_one_plus) return 0; - } - - // Hit confirmed -- only divide now - const inv_det = 1.0 / det; if (t_out != 0) { @as(*align(1) f32, @ptrFromInt(t_out)).* = dot3(edge2, qvec) * inv_det; } if (normal_out != 0) { - @as(*align(1) f32, @ptrFromInt(normal_out)).* = u_raw * inv_det; - @as(*align(1) f32, @ptrFromInt(normal_out + 4)).* = v_raw * inv_det; + @as(*align(1) f32, @ptrFromInt(normal_out)).* = u; + @as(*align(1) f32, @ptrFromInt(normal_out + 4)).* = v; } return 1; } @@ -482,39 +470,20 @@ export fn performCollisionDetectionSSE(this: u32, key_data: u32, key_size: u32) const pvec = cross(ray_dir, edge2); const det = dot3(edge1, pvec); - if (det <= 1e-7 and det >= -1e-7) continue; + // Original thresholds: reject if det is in (-1e-6, 1e-6) dead zone + if (det > -1e-6 and det < 1e-6) continue; + const inv_det = 1.0 / det; const tvec = ray_origin - v0; - // u_raw = dot(tvec, pvec) -- NOT multiplied by inv_det yet - const u_raw = dot3(tvec, pvec); - - // Compare u_raw against det-scaled epsilon bounds. - // If det > 0: u = u_raw/det, so u < -eps iff u_raw < -eps*det, u > 1+eps iff u_raw > (1+eps)*det - // If det < 0: division flips sign, so u < -eps iff u_raw > -eps*det (which is positive) - // Trick: multiply both sides by sign(det) to normalize. - // Or equivalently: if det>0 check u_raw in [det*neg_eps, det*one_plus_eps] - // if det<0 check u_raw in [det*one_plus_eps, det*neg_eps] - const det_neg_eps = det * neg_eps; - const det_one_plus = det * one_plus_eps; - if (det > 0) { - if (u_raw < det_neg_eps or u_raw > det_one_plus) continue; - } else { - if (u_raw > det_neg_eps or u_raw < det_one_plus) continue; - } + const u = dot3(tvec, pvec) * inv_det; + if (u < neg_eps or u > one_plus_eps) continue; const qvec = cross(tvec, edge1); - const v_raw = dot3(ray_dir, qvec); + const v = dot3(ray_dir, qvec) * inv_det; + if (v < neg_eps or (u + v) > one_plus_eps) continue; - // Same sign-aware bounds check for v - if (det > 0) { - if (v_raw < det_neg_eps or (u_raw + v_raw) > det_one_plus) continue; - } else { - if (v_raw > det_neg_eps or (u_raw + v_raw) < det_one_plus) continue; - } - - // Only divide for confirmed hits - const t = dot3(edge2, qvec) / det; + const t = dot3(edge2, qvec) * inv_det; if (t >= 0.0 and t < readF32(this + 0x4C)) { // Update closest hit diff --git a/src/performance/inflate_hook.zig b/src/performance/inflate_hook.zig index 59a9423..209543a 100644 --- a/src/performance/inflate_hook.zig +++ b/src/performance/inflate_hook.zig @@ -4,10 +4,9 @@ //! uses libdeflate (~2.2x faster than stock zlib). Falls back to original on failure. //! //! Thread-safety: the decompressor struct contains mutable decode tables rebuilt -//! per block, so each thread gets its own cached decompressor via a thread-local -//! pool keyed by Windows thread ID (FS:[0x24]). This avoids both the race condition -//! (shared decompressor → wild writes) and the per-call alloc overhead (~8-9% gain -//! over alloc/free each call). +//! per block, so each thread gets its own cached decompressor via Zig's native +//! threadlocal (OS-managed TLS). This avoids both the race condition +//! (shared decompressor -> wild writes) and the per-call alloc overhead. //! //! Benchmark results (84k calls, heavy load): //! stock=2681ms | per-call-alloc=1304ms (2.05x) | tls-cached=1194ms (2.2x) @@ -24,43 +23,17 @@ extern fn libdeflate_zlib_decompress(?*anyopaque, [*]const u8, usize, [*]u8, usi var lib_available: bool = false; var log: logging.Logger = .{}; -// --- Thread-local decompressor pool --- -// Keyed by Windows thread ID. WoW has ~5-10 threads; only 2-3 call decompress. -const TLS_SLOTS = 8; -const TlsSlot = struct { - thread_id: u32 = 0, - decomp: ?*anyopaque = null, -}; -var tls_pool: [TLS_SLOTS]TlsSlot = [_]TlsSlot{.{}} ** TLS_SLOTS; - -fn getCurrentThreadId() u32 { - return asm volatile ("movl %%fs:0x24, %[ret]" - : [ret] "=r" (-> u32), - ); -} +// --- Thread-local decompressor --- +// Each thread lazily allocates its own decompressor on first use. +// OS-managed TLS via Zig's threadlocal -- works correctly on both +// native Windows and Wine without manual FS segment access. +threadlocal var tls_decomp: ?*anyopaque = null; fn getTlsDecompressor() ?*anyopaque { - const tid = getCurrentThreadId(); - for (&tls_pool) |*slot| { - if (slot.thread_id == tid) return slot.decomp; - } - const decomp = libdeflate_alloc_decompressor() orelse return null; - for (&tls_pool) |*slot| { - if (slot.thread_id == 0) { - slot.thread_id = tid; - slot.decomp = decomp; - return decomp; - } - } - libdeflate_free_decompressor(decomp); - return null; -} - -fn freeTlsPool() void { - for (&tls_pool) |*slot| { - if (slot.decomp) |d| libdeflate_free_decompressor(d); - slot.* = .{}; - } + if (tls_decomp) |d| return d; + const d = libdeflate_alloc_decompressor() orelse return null; + tls_decomp = d; + return d; } // --- Timing --- @@ -142,5 +115,4 @@ pub fn install() bool { pub fn remove() void { decompress_hook.detach(); lib_available = false; - freeTlsPool(); } diff --git a/src/performance/libdeflate/stubs/setjmp.h b/src/performance/libdeflate/stubs/setjmp.h new file mode 100644 index 0000000..339f894 --- /dev/null +++ b/src/performance/libdeflate/stubs/setjmp.h @@ -0,0 +1,3 @@ +#pragma once +/* Stub: libdeflate doesn't use setjmp, but MSVC intrin.h includes it */ +typedef int jmp_buf[16]; diff --git a/src/performance/weirdperformance.zig b/src/performance/weirdperformance.zig index bd55b9e..cae405d 100644 --- a/src/performance/weirdperformance.zig +++ b/src/performance/weirdperformance.zig @@ -333,8 +333,8 @@ pub fn installHooks() void { // if (findguid_hook.attach(0x464890, &findguidDetour) == .ok) installed += 1; // if (obj_delete_hook.attach(0x464920, &objDeleteDetour) == .ok) installed += 1; - // Flush GUID cache before object manager teardown - if (destroy_objmgr_hook.attach(0x467700, &destroyObjMgrDetour) == .ok) installed += 1; + // GUID cache disabled for now + // if (destroy_objmgr_hook.attach(0x467700, &destroyObjMgrDetour) == .ok) installed += 1; // Per-frame cache reset if (world_update_hook.attach(0x482EA0, &worldUpdateDetour) == .ok) installed += 1;