Extracts original x87 FPU bytes from WoW.exe via Ghidra, mmaps them executable, and benchmarks against our SSE replacements. Covers all 17 UnitXP polyfill functions with correctness validation and cycle counts. Maps a page at 0x7ff000 for the float 1.0 constant referenced by rotMat3x3/rotMat4x4/planeNormal via absolute address 0x7ff9d8. Build: zig build bench / zig build run-bench
380 lines
15 KiB
Zig
380 lines
15 KiB
Zig
//! Micro-benchmark harness for math_sse replacements.
|
|
//!
|
|
//! Extracts original x87 FPU function bytes from WoW.exe, maps them executable,
|
|
//! and benchmarks against our SSE replacements. Runs on x86 Linux (32-bit).
|
|
//!
|
|
//! Build: zig build bench
|
|
//! Run: zig build run-bench
|
|
|
|
const std = @import("std");
|
|
const posix = std.posix;
|
|
const linux = std.os.linux;
|
|
const originals = @import("originals.zig");
|
|
|
|
// SSE implementations (C ABI — export fn from math_sse.zig)
|
|
extern fn vecMulMat4_ColMajor(u32, u32, u32) u32;
|
|
extern fn matMulVec3_RowMajor(u32, u32, u32) u32;
|
|
extern fn quatMulMat4(u32, u32, u32) u32;
|
|
extern fn vec3MulScalar(u32, u32, u32) u32;
|
|
extern fn vec3MulAssign(u32, u32) u32;
|
|
extern fn applyTranslationMatrix(u32, u32) u32;
|
|
extern fn scaleMatrix3x3ByVector(u32, u32) u32;
|
|
extern fn scaleMatrix3x3ByScalar(u32, u32) void;
|
|
extern fn multiply3x3Matrix(u32, u32, u32) u32;
|
|
extern fn createAxisAngleRotMat3x3(u32, u32, u32, u32) u32;
|
|
extern fn createAxisAngleRotMat4x4(u32, u32, u32, u32) u32;
|
|
extern fn crossProduct(u32, u32, u32) u32;
|
|
extern fn dotProduct(u32, u32) f64;
|
|
extern fn squaredMagnitude(u32) f64;
|
|
extern fn evaluatePolynomial(u32, u32, u32) f64;
|
|
extern fn calculatePlaneNormal(u32, u32, u32, u32) void;
|
|
extern fn transformAABox(u32, u32, u32, u32, u32) void;
|
|
|
|
// =========================================================================
|
|
// Infrastructure
|
|
// =========================================================================
|
|
|
|
fn print(comptime fmt: []const u8, args: anytype) void {
|
|
var buf: [1024]u8 = undefined;
|
|
const msg = std.fmt.bufPrint(&buf, fmt, args) catch return;
|
|
_ = linux.write(1, msg.ptr, msg.len);
|
|
}
|
|
|
|
fn makeExecutable(comptime bytes: []const u8) ?[*]const u8 {
|
|
const mem = posix.mmap(
|
|
null, 4096,
|
|
.{ .READ = true, .WRITE = true, .EXEC = true },
|
|
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
|
|
-1, 0,
|
|
) catch return null;
|
|
@memcpy(mem[0..bytes.len], bytes);
|
|
return mem.ptr;
|
|
}
|
|
|
|
fn mapGameConstants() bool {
|
|
const mem = posix.mmap(
|
|
@ptrFromInt(0x007ff000), 4096,
|
|
.{ .READ = true, .WRITE = true },
|
|
.{ .TYPE = .PRIVATE, .ANONYMOUS = true, .FIXED = true },
|
|
-1, 0,
|
|
) catch return false;
|
|
const p: *f32 = @ptrCast(@alignCast(&mem[0x9d8]));
|
|
p.* = 1.0;
|
|
return true;
|
|
}
|
|
|
|
inline fn rdtsc() u64 {
|
|
var lo: u32 = undefined;
|
|
var hi: u32 = undefined;
|
|
asm volatile ("rdtsc"
|
|
: [lo] "={eax}" (lo),
|
|
[hi] "={edx}" (hi),
|
|
);
|
|
return (@as(u64, hi) << 32) | lo;
|
|
}
|
|
|
|
fn a(ptr: anytype) u32 {
|
|
return @intFromPtr(ptr);
|
|
}
|
|
|
|
fn compareF32(x: f32, y: f32) bool {
|
|
if (x == y) return true;
|
|
const d = @abs(x - y);
|
|
const m = @max(@abs(x), @abs(y));
|
|
if (m < 1e-7) return d < 1e-7;
|
|
return d / m < 1e-4;
|
|
}
|
|
|
|
fn cmpSlice(x: []const f32, y: []const f32) bool {
|
|
for (x, y) |a2, b| if (!compareF32(a2, b)) return false;
|
|
return true;
|
|
}
|
|
|
|
fn report(name: []const u8, orig_cyc: u64, sse_cyc: u64, ok: bool) void {
|
|
const N = ITERS;
|
|
const op = orig_cyc / N;
|
|
const sp = sse_cyc / N;
|
|
const sx10 = if (sp > 0) op * 10 / sp else 0;
|
|
print("{s:>30}: orig={d:>4} sse={d:>4} cyc/call {d}.{d}x {s}\n", .{
|
|
name, op, sp, sx10 / 10, sx10 % 10, if (ok) "OK" else "MISMATCH",
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// Calling convention types for original x87 functions (game binary)
|
|
// =========================================================================
|
|
|
|
const cc_fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} };
|
|
const cc_tc: std.builtin.CallingConvention = .{ .x86_thiscall = .{} };
|
|
|
|
const ITERS: u64 = 2_000_000;
|
|
|
|
// =========================================================================
|
|
// Test data
|
|
// =========================================================================
|
|
|
|
const Vec3 = [3]f32;
|
|
const Vec4 = [4]f32;
|
|
const Mat3 = [9]f32;
|
|
const Mat4 = [16]f32;
|
|
|
|
fn tv3() Vec3 { return .{ 1.5, -2.3, 0.7 }; }
|
|
fn tv3b() Vec3 { return .{ 0.4, 3.1, -1.2 }; }
|
|
fn tv3c() Vec3 { return .{ -0.8, 1.6, 2.5 }; }
|
|
fn tq4() Vec4 { return .{ 0.5, -0.5, 0.5, 0.5 }; }
|
|
fn tm4() Mat4 {
|
|
return .{ 1.0, 0.2, 0.3, 0.0, 0.1, 2.0, 0.4, 0.0, 0.2, 0.1, 1.5, 0.0, 1.0, 2.0, 3.0, 1.0 };
|
|
}
|
|
fn tm3() Mat3 { return .{ 1.0, 0.2, 0.3, 0.1, 2.0, 0.4, 0.2, 0.1, 1.5 }; }
|
|
fn tm3b() Mat3 { return .{ 0.5, -0.1, 0.3, 0.2, 1.0, -0.2, -0.1, 0.4, 0.8 }; }
|
|
|
|
// =========================================================================
|
|
// Main
|
|
// =========================================================================
|
|
|
|
pub fn main() void {
|
|
if (!mapGameConstants()) {
|
|
print("WARNING: could not map game constants at 0x7ff000\n", .{});
|
|
}
|
|
|
|
print("\nmath_sse benchmark -- {d}M iterations per function\n", .{ITERS / 1_000_000});
|
|
print("{s:>30} {s:>10} {s:>10} {s}\n", .{ "function", "original", "sse", "status" });
|
|
print("{s}\n", .{"-" ** 72});
|
|
|
|
// 1: vecMulMat4 -- fastcall(ECX=result, EDX=vec, stack=mat) -> u32
|
|
bench_fc3r("vecMulMat4_ColMajor", originals.vecMulMat4_ColMajor, &vecMulMat4_ColMajor, tv3(), tm4(), 3);
|
|
|
|
// 2: matMulVec3 -- fastcall(ECX=result, EDX=mat, stack=vec) -> u32
|
|
bench_fc3r("matMulVec3_RowMajor", originals.matMulVec3_RowMajor, &matMulVec3_RowMajor, tm4(), tv3(), 3);
|
|
|
|
// 3: quatMulMat4 -- fastcall(ECX=result, EDX=quat, stack=mat) -> u32
|
|
bench_fc3r("quatMulMat4", originals.quatMulMat4, &quatMulMat4, tq4(), tm4(), 4);
|
|
|
|
// 4: vec3MulScalar -- fastcall(ECX=result, EDX=vec, stack=factor_bits) -> u32
|
|
{
|
|
const factor: f32 = 2.5;
|
|
const fb: u32 = @bitCast(factor);
|
|
const v = tv3();
|
|
var ro: Vec3 = undefined;
|
|
var rs: Vec3 = undefined;
|
|
const of: *const fn (u32, u32, u32) callconv(cc_fc) u32 = @ptrCast(makeExecutable(&originals.vec3MulScalar) orelse unreachable);
|
|
_ = of(a(&ro), a(&v), fb);
|
|
_ = vec3MulScalar(a(&rs), a(&v), fb);
|
|
const ok = cmpSlice(&ro, &rs);
|
|
var t: u64 = 0;
|
|
var s: u64 = 0;
|
|
t = rdtsc(); for (0..ITERS) |_| { _ = of(a(&ro), a(&v), fb); } t = rdtsc() - t;
|
|
s = rdtsc(); for (0..ITERS) |_| { _ = vec3MulScalar(a(&rs), a(&v), fb); } s = rdtsc() - s;
|
|
report("vec3MulScalar", t, s, ok);
|
|
}
|
|
|
|
// 5: vec3MulAssign -- thiscall(ECX=self, stack=factor_bits) -> u32
|
|
{
|
|
const fb: u32 = @bitCast(@as(f32, 2.5));
|
|
var do = tv3();
|
|
var ds = tv3();
|
|
const of: *const fn (u32, u32) callconv(cc_tc) u32 = @ptrCast(makeExecutable(&originals.vec3MulAssign) orelse unreachable);
|
|
_ = of(a(&do), fb);
|
|
_ = vec3MulAssign(a(&ds), fb);
|
|
const ok = cmpSlice(&do, &ds);
|
|
do = tv3();
|
|
ds = tv3();
|
|
var t = rdtsc(); for (0..ITERS) |_| { _ = of(a(&do), fb); } t = rdtsc() - t;
|
|
var s = rdtsc(); for (0..ITERS) |_| { _ = vec3MulAssign(a(&ds), fb); } s = rdtsc() - s;
|
|
report("vec3MulAssign", t, s, ok);
|
|
}
|
|
|
|
// 6: applyTranslation -- thiscall(ECX=mat, stack=vec) -> u32
|
|
bench_tc2r("applyTranslation", originals.applyTranslationMatrix, &applyTranslationMatrix, tm4(), tv3(), 16);
|
|
|
|
// 7: scaleByVec -- thiscall(ECX=mat, stack=vec) -> u32
|
|
bench_tc2r("scaleByVec", originals.scaleMatrix3x3ByVector, &scaleMatrix3x3ByVector, tm4(), tv3(), 16);
|
|
|
|
// 8: scaleByScalar -- thiscall(ECX=mat, stack=factor_bits) -> void
|
|
{
|
|
const fb: u32 = @bitCast(@as(f32, 0.5));
|
|
var mo = tm4();
|
|
var ms = tm4();
|
|
const of: *const fn (u32, u32) callconv(cc_tc) void = @ptrCast(makeExecutable(&originals.scaleMatrix3x3ByScalar) orelse unreachable);
|
|
of(a(&mo), fb);
|
|
scaleMatrix3x3ByScalar(a(&ms), fb);
|
|
const ok = cmpSlice(&mo, &ms);
|
|
mo = tm4();
|
|
ms = tm4();
|
|
var t = rdtsc(); for (0..ITERS) |_| { of(a(&mo), fb); } t = rdtsc() - t;
|
|
var s = rdtsc(); for (0..ITERS) |_| { scaleMatrix3x3ByScalar(a(&ms), fb); } s = rdtsc() - s;
|
|
report("scaleByScalar", t, s, ok);
|
|
}
|
|
|
|
// 9: mul3x3 -- fastcall(ECX=result, EDX=matA, stack=matB) -> u32
|
|
bench_fc3r("multiply3x3", originals.multiply3x3Matrix, &multiply3x3Matrix, tm3(), tm3b(), 9);
|
|
|
|
// 10: rotMat3x3 -- fastcall(ECX=result, EDX=axis, stack=angle_bits, is_unit) -> u32
|
|
{
|
|
const axis = Vec3{ 0.0, 1.0, 0.0 };
|
|
const ab: u32 = @bitCast(@as(f32, 0.7854));
|
|
var ro: Mat3 = undefined;
|
|
var rs: Mat3 = undefined;
|
|
const of: *const fn (u32, u32, u32, u32) callconv(cc_fc) u32 = @ptrCast(makeExecutable(&originals.createAxisAngleRotMat3x3) orelse unreachable);
|
|
_ = of(a(&ro), a(&axis), ab, 1);
|
|
_ = createAxisAngleRotMat3x3(a(&rs), a(&axis), ab, 1);
|
|
const ok = cmpSlice(&ro, &rs);
|
|
var t = rdtsc(); for (0..ITERS) |_| { _ = of(a(&ro), a(&axis), ab, 1); } t = rdtsc() - t;
|
|
var s = rdtsc(); for (0..ITERS) |_| { _ = createAxisAngleRotMat3x3(a(&rs), a(&axis), ab, 1); } s = rdtsc() - s;
|
|
report("rotMat3x3", t, s, ok);
|
|
}
|
|
|
|
// 11: rotMat4x4
|
|
{
|
|
const axis = Vec3{ 0.0, 1.0, 0.0 };
|
|
const ab: u32 = @bitCast(@as(f32, 0.7854));
|
|
var ro: Mat4 = undefined;
|
|
var rs: Mat4 = undefined;
|
|
const of: *const fn (u32, u32, u32, u32) callconv(cc_fc) u32 = @ptrCast(makeExecutable(&originals.createAxisAngleRotMat4x4) orelse unreachable);
|
|
_ = of(a(&ro), a(&axis), ab, 1);
|
|
_ = createAxisAngleRotMat4x4(a(&rs), a(&axis), ab, 1);
|
|
const ok = cmpSlice(&ro, &rs);
|
|
var t = rdtsc(); for (0..ITERS) |_| { _ = of(a(&ro), a(&axis), ab, 1); } t = rdtsc() - t;
|
|
var s = rdtsc(); for (0..ITERS) |_| { _ = createAxisAngleRotMat4x4(a(&rs), a(&axis), ab, 1); } s = rdtsc() - s;
|
|
report("rotMat4x4", t, s, ok);
|
|
}
|
|
|
|
// 12: cross -- fastcall(ECX=result, EDX=vecA, stack=vecB) -> u32
|
|
bench_fc3r("crossProduct", originals.crossProduct, &crossProduct, tv3(), tv3b(), 3);
|
|
|
|
// 13: dot -- fastcall(ECX=vecA, EDX=vecB) -> f64
|
|
{
|
|
const va = tv3();
|
|
const vb = tv3b();
|
|
const of: *const fn (u32, u32) callconv(cc_fc) f64 = @ptrCast(makeExecutable(&originals.dotProduct) orelse unreachable);
|
|
const ov = of(a(&va), a(&vb));
|
|
const sv = dotProduct(a(&va), a(&vb));
|
|
const ok = @abs(ov - sv) < 1e-4;
|
|
var t = rdtsc(); for (0..ITERS) |_| { _ = of(a(&va), a(&vb)); } t = rdtsc() - t;
|
|
var s = rdtsc(); for (0..ITERS) |_| { _ = dotProduct(a(&va), a(&vb)); } s = rdtsc() - s;
|
|
report("dotProduct", t, s, ok);
|
|
}
|
|
|
|
// 14: sqmag -- thiscall(ECX=vec) -> f64
|
|
{
|
|
const v = tv3();
|
|
const of: *const fn (u32) callconv(cc_tc) f64 = @ptrCast(makeExecutable(&originals.squaredMagnitude) orelse unreachable);
|
|
const ov = of(a(&v));
|
|
const sv = squaredMagnitude(a(&v));
|
|
const ok = @abs(ov - sv) < 1e-4;
|
|
var t = rdtsc(); for (0..ITERS) |_| { _ = of(a(&v)); } t = rdtsc() - t;
|
|
var s = rdtsc(); for (0..ITERS) |_| { _ = squaredMagnitude(a(&v)); } s = rdtsc() - s;
|
|
report("squaredMagnitude", t, s, ok);
|
|
}
|
|
|
|
// 16: evalPoly -- fastcall(ECX=count, EDX=coeffs, stack=factor_bits) -> f64
|
|
{
|
|
const coeffs = [4]f32{ 3.0, -2.0, 1.0, 0.5 };
|
|
const fb: u32 = @bitCast(@as(f32, 1.5));
|
|
const of: *const fn (u32, u32, u32) callconv(cc_fc) f64 = @ptrCast(makeExecutable(&originals.evaluatePolynomial) orelse unreachable);
|
|
const ov = of(3, a(&coeffs), fb);
|
|
const sv = evaluatePolynomial(3, a(&coeffs), fb);
|
|
const ok = @abs(ov - sv) < 1e-4;
|
|
var t = rdtsc(); for (0..ITERS) |_| { _ = of(3, a(&coeffs), fb); } t = rdtsc() - t;
|
|
var s = rdtsc(); for (0..ITERS) |_| { _ = evaluatePolynomial(3, a(&coeffs), fb); } s = rdtsc() - s;
|
|
report("evaluatePolynomial", t, s, ok);
|
|
}
|
|
|
|
// 17: planeNormal -- thiscall(ECX=result, stack=p1,p2,p3) -> void
|
|
{
|
|
const p1 = tv3();
|
|
const p2 = tv3b();
|
|
const p3 = tv3c();
|
|
var ro: Vec4 = undefined;
|
|
var rs: Vec4 = undefined;
|
|
const of: *const fn (u32, u32, u32, u32) callconv(cc_tc) void = @ptrCast(makeExecutable(&originals.calculatePlaneNormal) orelse unreachable);
|
|
of(a(&ro), a(&p1), a(&p2), a(&p3));
|
|
calculatePlaneNormal(a(&rs), a(&p1), a(&p2), a(&p3));
|
|
const ok = cmpSlice(&ro, &rs);
|
|
var t = rdtsc(); for (0..ITERS) |_| { of(a(&ro), a(&p1), a(&p2), a(&p3)); } t = rdtsc() - t;
|
|
var s = rdtsc(); for (0..ITERS) |_| { calculatePlaneNormal(a(&rs), a(&p1), a(&p2), a(&p3)); } s = rdtsc() - s;
|
|
report("planeNormal", t, s, ok);
|
|
}
|
|
|
|
// 18: transformAABox -- fastcall(ECX=mat, EDX=vecA, stack=vecB,boxIn,boxOut) -> void
|
|
{
|
|
const mat = tm3();
|
|
const va = tv3();
|
|
const vb = tv3b();
|
|
const box_in = [6]f32{ -1.0, -1.0, -1.0, 1.0, 1.0, 1.0 };
|
|
var bo: [6]f32 = .{ 0, 0, 0, 0, 0, 0 };
|
|
var bs: [6]f32 = .{ 0, 0, 0, 0, 0, 0 };
|
|
const of: *const fn (u32, u32, u32, u32, u32) callconv(cc_fc) void = @ptrCast(makeExecutable(&originals.transformAABox) orelse unreachable);
|
|
of(a(&mat), a(&va), a(&vb), a(&box_in), a(&bo));
|
|
transformAABox(a(&mat), a(&va), a(&vb), a(&box_in), a(&bs));
|
|
const ok = cmpSlice(&bo, &bs);
|
|
var t = rdtsc(); for (0..ITERS) |_| { bo = .{ 0, 0, 0, 0, 0, 0 }; of(a(&mat), a(&va), a(&vb), a(&box_in), a(&bo)); } t = rdtsc() - t;
|
|
var s = rdtsc(); for (0..ITERS) |_| { bs = .{ 0, 0, 0, 0, 0, 0 }; transformAABox(a(&mat), a(&va), a(&vb), a(&box_in), a(&bs)); } s = rdtsc() - s;
|
|
report("transformAABox", t, s, ok);
|
|
}
|
|
|
|
print("\n", .{});
|
|
}
|
|
|
|
// =========================================================================
|
|
// Generic benchmarks for common signatures
|
|
// =========================================================================
|
|
|
|
/// fastcall(ECX=result, EDX=paramA, stack=paramB) -> u32
|
|
fn bench_fc3r(
|
|
comptime name: []const u8,
|
|
comptime orig_bytes: anytype,
|
|
sse_fn: *const fn (u32, u32, u32) callconv(.c) u32,
|
|
param_a: anytype,
|
|
param_b: anytype,
|
|
comptime result_len: usize,
|
|
) void {
|
|
const of: *const fn (u32, u32, u32) callconv(cc_fc) u32 = @ptrCast(makeExecutable(&orig_bytes) orelse {
|
|
print("{s:>30}: FAILED to map\n", .{name});
|
|
return;
|
|
});
|
|
var ro: [16]f32 = undefined;
|
|
var rs: [16]f32 = undefined;
|
|
_ = of(a(&ro), a(¶m_a), a(¶m_b));
|
|
_ = sse_fn(a(&rs), a(¶m_a), a(¶m_b));
|
|
const ok = cmpSlice(ro[0..result_len], rs[0..result_len]);
|
|
|
|
var t = rdtsc();
|
|
for (0..ITERS) |_| { _ = of(a(&ro), a(¶m_a), a(¶m_b)); }
|
|
t = rdtsc() - t;
|
|
var s = rdtsc();
|
|
for (0..ITERS) |_| { _ = sse_fn(a(&rs), a(¶m_a), a(¶m_b)); }
|
|
s = rdtsc() - s;
|
|
report(name, t, s, ok);
|
|
}
|
|
|
|
/// thiscall(ECX=self, stack=param) -> u32 (in-place modification)
|
|
fn bench_tc2r(
|
|
comptime name: []const u8,
|
|
comptime orig_bytes: anytype,
|
|
sse_fn: *const fn (u32, u32) callconv(.c) u32,
|
|
self_init: anytype,
|
|
param: anytype,
|
|
comptime result_len: usize,
|
|
) void {
|
|
const of: *const fn (u32, u32) callconv(cc_tc) u32 = @ptrCast(makeExecutable(&orig_bytes) orelse {
|
|
print("{s:>30}: FAILED to map\n", .{name});
|
|
return;
|
|
});
|
|
var so: @TypeOf(self_init) = self_init;
|
|
var ss: @TypeOf(self_init) = self_init;
|
|
_ = of(a(&so), a(¶m));
|
|
_ = sse_fn(a(&ss), a(¶m));
|
|
const ok = cmpSlice(@as([*]const f32, @ptrCast(&so))[0..result_len], @as([*]const f32, @ptrCast(&ss))[0..result_len]);
|
|
|
|
so = self_init;
|
|
ss = self_init;
|
|
var t = rdtsc();
|
|
for (0..ITERS) |_| { _ = of(a(&so), a(¶m)); }
|
|
t = rdtsc() - t;
|
|
var s = rdtsc();
|
|
for (0..ITERS) |_| { _ = sse_fn(a(&ss), a(¶m)); }
|
|
s = rdtsc() - s;
|
|
report(name, t, s, ok);
|
|
}
|