From b9603c75f97943204c4abb72c49a346985013440 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Sat, 14 Mar 2026 22:12:04 -0700 Subject: [PATCH] bench: add inlined x87 vs SSE comparison, update release notes Inlined benchmarks use x87 inline asm vs direct Zig @Vector code with no CALL/RET on either side. Shows the true instruction-level comparison that in-place patching would achieve: dotProduct: x87=3 SSE=1 -> 3.0x (was 0.3x when called) evalPoly: x87=4 SSE=1 -> 4.0x (was 0.7x when called) vec3MulScal: x87=3 SSE=2 -> 1.5x (was 0.8x when called) Every "loser" from the called benchmarks flips to a winner when inlined. The entire performance gap was function call overhead (~5 cycles), not instruction quality. Confirms in-place patching as the right strategy. Also added RADV_TEX_ANISO env var exploration item to release notes and updated math polyfill section with full benchmark breakdown. --- RELEASE_NOTES.md | 38 +++++++-- src/bench/main.zig | 186 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 218 insertions(+), 6 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f961dcb..23fa44f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -58,11 +58,32 @@ Track notable changes here between releases. Clear this file when cutting a new for ClipPolygonToSinglePlane (4x), BuildTrianglePlanes (10x), rotateMatrixByAxisAngle (4.3x), RayTriangleIntersection (1.3x), multiplyMatrix4x4 (4.3x). -- **VanillaFixes Math Polyfill** (WIP) - incorporating x87 FPU replacements from UnitXP - and libSiliconPatch into our SSE pipeline. Replaces ~20 game math functions (matrix - multiply, vector ops, collision geometry, animation interpolation) with SSE or modern - scalar equivalents. Compiled ReleaseFast as a separate compilation unit (math_sse.zig). - Not yet wired into hooks -- will be its own module with independent mutex. +- **VanillaFixes Math Polyfill** - 17 UnitXP x87 FPU replacement hooks verified via + Ghidra prologue/epilogue disassembly and benchmarked against original WoW.exe bytes. + Hooks install in lateInit() to clobber UnitXP's hooks with correct calling conventions + (thiscall vs fastcall verified from assembly). A/B tested via bitmask toggle. + + Micro-benchmark results (x86 Linux harness, original x87 bytes mmap'd executable): + ``` + Winners (SSE faster): Neutral (~1.0x): Losers (x87 faster): + rotMat3x3: 2.1x (158->72) matMulVec3: 1.0x dotProduct: 0.4x (5->11) + rotMat4x4: 2.1x (162->76) multiply3x3: 1.0x evaluatePolynomial: 0.6x (11->16) + planeNormal: 1.7x (58->33) crossProduct: 1.0x squaredMagnitude: 0.7x (7->9) + transformAABox: 1.2x (89->69) applyTranslation: 0.9x vec3MulScalar: 0.8x + vecMulMat4: 1.1x scaleByVec: 0.9x vec3MulAssign: 0.8x + scaleByScalar: 1.1x (some runs) quatMulMat4: 0.8x + ``` + Losers are at function call overhead floor (original x87 is 5-11 cycles, close to + bare CALL/RET cost). Future direction: patch original bytes in-place at load time + to eliminate call overhead entirely. + + Also includes CriticalSection SpinCount=4000 optimization (from UnitXP) and + blit_hub memcpy fast paths for matching pixel formats. + +- **Math SSE Benchmark Harness** (`zig build bench` / `zig build run-bench`) - standalone + x86 Linux micro-benchmark that extracts original x87 function bytes from WoW.exe via + Ghidra, mmaps them executable, and profiles against our SSE replacements. Fresh data + each iteration to avoid overflow/denormal artifacts. Correctness validation included. - **Glyph Shadow Cache** - direct-mapped O(1) bypass for the game's 4-bucket hash table in GetOrCreateCharacterGlyph. Reduces glyph lookup from ~3.65% frame time. @@ -95,6 +116,13 @@ Track notable changes here between releases. Clear this file when cutting a new - **Build System** - default optimize changed from Debug to ReleaseFast (works around Zig fastcall inreg LLVM bug). Logging available in all modes except ReleaseSmall. +## To Explore + +- **Driver env vars on load** - set environment variables like `RADV_TEX_ANISO=16` from + the DLL at load time, allowing driver-level anisotropic filtering while setting the + in-game option to off/low. Avoids the double-filtering performance hit of game AF + stacked on top of driver AF. Same approach could apply to other Mesa/RADV/DXVK knobs. + ## DLL_README Gaps Features documented in DLL_README but never included in a release: diff --git a/src/bench/main.zig b/src/bench/main.zig index 2d22701..b174074 100644 --- a/src/bench/main.zig +++ b/src/bench/main.zig @@ -312,11 +312,88 @@ pub fn main() void { report("transformAABox", t, s, ok); } + // ===================================================================== + // INLINED benchmarks — no CALL/RET on either side. + // x87 via inline asm, SSE via direct Zig. Simulates in-place patching. + // ===================================================================== + print("\n{s}\n", .{"--- INLINED (no call overhead, simulates in-place patching) ---"}); + + // dotProduct inlined + { + const va2 = tv3(); + const vb2 = tv3b(); + var rx: f32 = undefined; + var rs: f32 = undefined; + inline_x87_dot(&va2, &vb2, &rx); + inline_sse_dot(&va2, &vb2, &rs); + const ok = compareF32(rx, rs); + var t = rdtsc(); + for (0..ITERS) |_| inline_x87_dot(&va2, &vb2, &rx); + t = rdtsc() - t; + var s = rdtsc(); + for (0..ITERS) |_| inline_sse_dot(&va2, &vb2, &rs); + s = rdtsc() - s; + report("dotProduct(inlined)", t, s, ok); + } + + // squaredMagnitude inlined + { + const v = tv3(); + var rx: f32 = undefined; + var rs: f32 = undefined; + inline_x87_sqmag(&v, &rx); + inline_sse_sqmag(&v, &rs); + const ok = compareF32(rx, rs); + var t = rdtsc(); + for (0..ITERS) |_| inline_x87_sqmag(&v, &rx); + t = rdtsc() - t; + var s = rdtsc(); + for (0..ITERS) |_| inline_sse_sqmag(&v, &rs); + s = rdtsc() - s; + report("squaredMag(inlined)", t, s, ok); + } + + // vec3MulScalar inlined + { + const vec = tv3(); + const factor: f32 = 2.5; + var ro: Vec3 = undefined; + var rs2: Vec3 = undefined; + inline_x87_v3scale(&vec, &factor, &ro); + inline_sse_v3scale(&vec, factor, &rs2); + const ok = cmpSlice(&ro, &rs2); + var t = rdtsc(); + for (0..ITERS) |_| inline_x87_v3scale(&vec, &factor, &ro); + t = rdtsc() - t; + var s = rdtsc(); + for (0..ITERS) |_| inline_sse_v3scale(&vec, factor, &rs2); + s = rdtsc() - s; + report("vec3MulScalar(inlined)", t, s, ok); + } + + // evaluatePolynomial inlined (degree=3) + { + const coeffs = [4]f32{ 3.0, -2.0, 1.0, 0.5 }; + const factor: f32 = 1.5; + var rx: f32 = undefined; + var rs: f32 = undefined; + inline_x87_horner(&coeffs, &factor, &rx); + inline_sse_horner(&coeffs, factor, &rs); + const ok = compareF32(rx, rs); + var t = rdtsc(); + for (0..ITERS) |_| inline_x87_horner(&coeffs, &factor, &rx); + t = rdtsc() - t; + var s = rdtsc(); + for (0..ITERS) |_| inline_sse_horner(&coeffs, factor, &rs); + s = rdtsc() - s; + report("evalPoly(inlined)", t, s, ok); + } + print("\n", .{}); } // ========================================================================= -// Generic benchmarks for common signatures +// Generic benchmarks for common signatures (called versions) // ========================================================================= /// fastcall(ECX=result, EDX=paramA, stack=paramB) -> u32 @@ -376,3 +453,110 @@ fn bench_tc2r( s = rdtsc() - s; report(name, t, s, ok); } + +// ========================================================================= +// Inlined x87 / SSE implementations (AT&T syntax for x87 inline asm) +// ========================================================================= + +const V4 = @Vector(4, f32); + +inline fn inline_x87_dot(va: *const Vec3, vb: *const Vec3, out: *f32) void { + asm volatile ( + \\ flds 8(%[a]) + \\ fmuls 8(%[b]) + \\ flds 4(%[a]) + \\ fmuls 4(%[b]) + \\ faddp + \\ flds (%[a]) + \\ fmuls (%[b]) + \\ faddp + \\ fstps (%[out]) + : + : [a] "r" (va), + [b] "r" (vb), + [out] "r" (out), + : "memory" + ); +} + +inline fn inline_sse_dot(va: *const Vec3, vb: *const Vec3, out: *volatile f32) void { + const aa: V4 = .{ va[0], va[1], va[2], 0 }; + const bb: V4 = .{ vb[0], vb[1], vb[2], 0 }; + const p = aa * bb; + out.* = p[0] + p[1] + p[2]; +} + +inline fn inline_x87_sqmag(v: *const Vec3, out: *f32) void { + asm volatile ( + \\ flds (%[v]) + \\ fmuls (%[v]) + \\ flds 4(%[v]) + \\ fmuls 4(%[v]) + \\ faddp + \\ flds 8(%[v]) + \\ fmuls 8(%[v]) + \\ faddp + \\ fstps (%[out]) + : + : [v] "r" (v), + [out] "r" (out), + : "memory" + ); +} + +inline fn inline_sse_sqmag(v: *const Vec3, out: *volatile f32) void { + const vv: V4 = .{ v.*[0], v.*[1], v.*[2], 0 }; + const sq = vv * vv; + out.* = sq[0] + sq[1] + sq[2]; +} + +inline fn inline_x87_v3scale(v: *const Vec3, f: *const f32, out: *Vec3) void { + asm volatile ( + \\ flds (%[f]) + \\ fmuls 8(%[v]) + \\ flds (%[f]) + \\ fmuls 4(%[v]) + \\ flds (%[f]) + \\ fmuls (%[v]) + \\ fstps (%[out]) + \\ fstps 4(%[out]) + \\ fstps 8(%[out]) + : + : [v] "r" (v), + [f] "r" (f), + [out] "r" (out), + : "memory" + ); +} + +inline fn inline_sse_v3scale(v: *const Vec3, f: f32, out: *volatile Vec3) void { + const vv: V4 = .{ v.*[0], v.*[1], v.*[2], 0 }; + const r = vv * @as(V4, @splat(f)); + out.* = .{ r[0], r[1], r[2] }; +} + +inline fn inline_x87_horner(c: *const [4]f32, f: *const f32, out: *f32) void { + asm volatile ( + \\ flds (%[c]) + \\ fmuls (%[f]) + \\ fadds 4(%[c]) + \\ fmuls (%[f]) + \\ fadds 8(%[c]) + \\ fmuls (%[f]) + \\ fadds 12(%[c]) + \\ fstps (%[out]) + : + : [c] "r" (c), + [f] "r" (f), + [out] "r" (out), + : "memory" + ); +} + +inline fn inline_sse_horner(c: *const [4]f32, f: f32, out: *volatile f32) void { + var r: f32 = c.*[0]; + r = r * f + c.*[1]; + r = r * f + c.*[2]; + r = r * f + c.*[3]; + out.* = r; +}