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.
This commit is contained in:
MarcelineVQ
2026-03-14 22:12:04 -07:00
parent 0671ffce02
commit b9603c75f9
2 changed files with 218 additions and 6 deletions
+33 -5
View File
@@ -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:
+185 -1
View File
@@ -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;
}