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).
This commit is contained in:
MarcelineVQ
2026-03-26 11:38:33 -07:00
parent bf5a7aa624
commit 14ba112b0d
7 changed files with 128 additions and 123 deletions
+2 -1
View File
@@ -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();
}
+15 -46
View File
@@ -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
+12 -40
View File
@@ -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();
}
@@ -0,0 +1,3 @@
#pragma once
/* Stub: libdeflate doesn't use setjmp, but MSVC intrin.h includes it */
typedef int jmp_buf[16];
+2 -2
View File
@@ -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;