perf: SSE spatial culling, inlined ray-tri, clickthrough CC fix

PerformSpatialCulling (0x6B8C60): Zig rewrite with SSE outcode
computation. 1.4x speedup, JMP-patched in weirdperformance.

performCollisionDetection (0x6B88E0): fully inlined SSE Moller-Trumbore
ray-triangle intersection, eliminating 4 SetVector3 calls and the
ray_tri function pointer call per triangle. ~22 cyc/tri in bench.

Both graduated from transform44 A/B testing to production JMP patches.

entity_sse.zig: reimplementations of UpdateEntityAndChunksPositions and
updateEntitiesInBounds (A/B tested, 1.2x bench, not shipped - memory
bound with negligible real-world gain).

clickthrough: fixed CheckObjectTypePermissions hook from fastcall to
thiscall (ECX preservation), fixed ClntObjMgrObjectPtr from fastcall(4)
to fastcall(5) with correct arg count.

bench: added performCollisionDetection bench with synthetic mesh data
and patched FindOrCreateHashEntry stub.
This commit is contained in:
MarcelineVQ
2026-03-25 12:34:57 -07:00
parent 4274729a77
commit 41ca30cfd5
7 changed files with 1219 additions and 56 deletions
+501 -2
View File
@@ -58,6 +58,11 @@ extern fn si_packParticleColor(u32, u32, u32, u32) callconv(cc_tc) void;
extern fn si_setParticleAlpha(u32, u32, u32) callconv(cc_fc) void; // fastcall(ECX=obj, EDX=unused, stack=alpha)
extern fn si_ftol() callconv(.naked) void;
// cull_sse.zig exports
extern fn benchComputeOutcodes(u32, u32, u32, u32) void;
extern fn performCollisionDetectionSSE(u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) u32;
extern fn updateEntityAndChunksPositions(u32) callconv(.{ .x86_fastcall = .{} }) void;
// =========================================================================
// Infrastructure
// =========================================================================
@@ -231,6 +236,14 @@ pub fn main() void {
print("{s:>30} {s:>10} {s:>10} {s}\n", .{ "function", "original", "sse", "status" });
print("{s}\n", .{"-" ** 72});
// Full performCollisionDetection (SSE vs original x87)
bench_collisionDetection();
// UpdateEntityAndChunksPositions (SSE vs original x87)
bench_entityUpdate();
if (false) { // disabled: not working on these right now
// 1: vecMulMat4 -- fastcall(ECX=result, EDX=vec, stack=mat) -> u32
bench_fc3r("vecMulMat4_ColMajor", originals.vecMulMat4_ColMajor, &vecMulMat4_ColMajor, tv3(), tm4(), 3);
@@ -1770,8 +1783,8 @@ pub fn main() void {
}
}
// calcColorValues_SSE -- thiscall(ctx_ECX, time, scale, outColor, outAlpha1, outAlpha2, outFloat)
bench_calcColorValues();
// calcColorValues_SSE -- disabled: no standalone SSE export yet
// bench_calcColorValues();
// si_frustumCullBBox -- fastcall(bbox_ECX, flags_EDX, radius_stack) -> u32
bench_frustumCullBBox();
@@ -1780,6 +1793,8 @@ pub fn main() void {
// Builds a fake linked list with 8 nodes to benchmark AABB overlap test.
bench_processLinkedListCollision();
} // end disabled block
print("\n", .{});
}
@@ -2174,6 +2189,29 @@ fn bench_tc2r(
// =========================================================================
const V4 = @Vector(4, f32);
const ShufMask = @Vector(4, i32);
inline fn benchLoadV3(addr: u32) V4 {
return .{
@as(*align(1) const f32, @ptrFromInt(addr)).*,
@as(*align(1) const f32, @ptrFromInt(addr + 4)).*,
@as(*align(1) const f32, @ptrFromInt(addr + 8)).*,
0,
};
}
inline fn benchCross(av: V4, bv: V4) V4 {
const a_yzx: V4 = @shuffle(f32, av, undefined, ShufMask{ 1, 2, 0, 3 });
const a_zxy: V4 = @shuffle(f32, av, undefined, ShufMask{ 2, 0, 1, 3 });
const b_yzx: V4 = @shuffle(f32, bv, undefined, ShufMask{ 1, 2, 0, 3 });
const b_zxy: V4 = @shuffle(f32, bv, undefined, ShufMask{ 2, 0, 1, 3 });
return a_yzx * b_zxy - a_zxy * b_yzx;
}
inline fn benchDot3(av: V4, bv: V4) f32 {
const p = av * bv;
return p[0] + p[1] + p[2];
}
inline fn inline_x87_dot(va: *const Vec3, vb: *const Vec3, out: *f32) void {
asm volatile (
@@ -2275,3 +2313,464 @@ inline fn inline_sse_horner(c: *const [4]f32, f: f32, out: *volatile f32) void {
r = r * f + c.*[3];
out.* = r;
}
fn sseRayTri(ray_ptr: u32, vert_pool: u32, idx_base: u32, t_out: *f32) bool {
const vi0: u32 = @as(*align(1) const u16, @ptrFromInt(idx_base)).*;
const vi1: u32 = @as(*align(1) const u16, @ptrFromInt(idx_base + 2)).*;
const vi2: u32 = @as(*align(1) const u16, @ptrFromInt(idx_base + 4)).*;
const ray_o = benchLoadV3(ray_ptr);
const ray_d = benchLoadV3(ray_ptr + 12);
const v0 = benchLoadV3(vert_pool + vi0 * 12);
const v1 = benchLoadV3(vert_pool + vi1 * 12);
const v2 = benchLoadV3(vert_pool + vi2 * 12);
const edge1 = v1 - v0;
const edge2 = v2 - v0;
const pvec = benchCross(ray_d, edge2);
const det = benchDot3(edge1, pvec);
if (det <= 1e-7 and det >= -1e-7) return false;
const inv_det = 1.0 / det;
const tvec = ray_o - v0;
const u = benchDot3(tvec, pvec) * inv_det;
if (u < -0.002 or u > 1.002) return false;
const qvec = benchCross(tvec, edge1);
const v = benchDot3(ray_d, qvec) * inv_det;
if (v < -0.002 or (u + v) > 1.002) return false;
t_out.* = benchDot3(edge2, qvec) * inv_det;
return true;
}
fn bench_collisionDetection() void {
// Build synthetic mesh data matching game's hash entry layout.
// 32 vertices forming a grid, 20 triangles, ray aimed through the middle.
const NVERTS = 120;
const NTRIS = 40;
// Hash entry: total size must accommodate all fields up to 0x2206 + NTRIS*2
// Max offset: 0x2206 + 20*2 = 0x222E, round up
var hash_buf: [0x2300]u8 align(4) = [_]u8{0} ** 0x2300;
const he = @intFromPtr(&hash_buf);
// Vertex count at +6
@as(*align(1) u16, @ptrFromInt(he + 6)).* = NVERTS;
// Carefully crafted vertices to produce a mix of hits and misses with
// non-trivial barycentric coordinates. Ray fires from (0,0,-10) along +Z.
// Triangles 0-4: guaranteed hits at various u/v (straddling the ray axis)
// Triangles 5-9: near-misses (edge/corner cases for barycentric bounds)
// Triangles 10-14: clear misses (outside AABB or backfacing)
// Triangles 15-19: more hits with small/large det values (tests divide precision)
const verts = [NVERTS][3]f32{
// --- Group A: clear hits at various depths, u/v values ---
// Tri 0: large centered, hit u~0.33 v~0.33
.{ -2.0, -2.0, 1.0 }, .{ 4.0, -2.0, 1.0 }, .{ -2.0, 4.0, 1.0 },
// Tri 1: small on-axis, hit u~0.5 v~0.25
.{ -0.5, -0.5, 2.0 }, .{ 0.5, -0.5, 2.0 }, .{ 0.0, 0.5, 2.0 },
// Tri 2: very close to origin
.{ -1.0, -1.0, 0.1 }, .{ 1.0, -1.0, 0.1 }, .{ 0.0, 1.0, 0.1 },
// Tri 3: backface hit (wound CW)
.{ -2.0, 4.0, 4.0 }, .{ 4.0, -2.0, 4.0 }, .{ -2.0, -2.0, 4.0 },
// Tri 4: tiny triangle, tests large inv_det
.{ -0.05, -0.05, 1.5 }, .{ 0.05, -0.05, 1.5 }, .{ 0.0, 0.05, 1.5 },
// Tri 5: huge triangle, tests small inv_det
.{ -50.0, -50.0, 2.5 }, .{ 50.0, -50.0, 2.5 }, .{ 0.0, 50.0, 2.5 },
// Tri 6: hit at u~0, v~0 (near vertex 0)
.{ -0.001, -0.001, 3.0 }, .{ 5.0, -0.001, 3.0 }, .{ -0.001, 5.0, 3.0 },
// Tri 7: hit at u~1, v~0 (near vertex 1)
.{ -5.0, -0.001, 3.5 }, .{ 0.001, -0.001, 3.5 }, .{ -5.0, 5.0, 3.5 },
// Tri 8: hit at u~0, v~1 (near vertex 2)
.{ -5.0, -5.0, 4.0 }, .{ 5.0, -5.0, 4.0 }, .{ 0.001, 0.001, 4.0 },
// Tri 9: hit with u+v very close to 1.0 (edge between v1-v2)
.{ -0.01, -0.01, 4.5 }, .{ 2.0, -0.01, 4.5 }, .{ -0.01, 2.0, 4.5 },
// --- Group B: edge cases that should barely miss ---
// Tri 10: ray just outside triangle edge
.{ 0.5, -0.5, 5.0 }, .{ 2.0, -0.5, 5.0 }, .{ 0.5, 1.0, 5.0 },
// Tri 11: ray misses on v side
.{ -3.0, 0.5, 5.5 }, .{ -0.5, 0.5, 5.5 }, .{ -3.0, 2.0, 5.5 },
// Tri 12: triangle behind ray (negative t)
.{ -1.0, -1.0, -15.0 }, .{ 1.0, -1.0, -15.0 }, .{ 0.0, 1.0, -15.0 },
// Tri 13: triangle way off to the side
.{ 10.0, 10.0, 1.0 }, .{ 12.0, 10.0, 1.0 }, .{ 10.0, 12.0, 1.0 },
// Tri 14: triangle off to the other side
.{ -12.0, -12.0, 2.0 }, .{ -10.0, -12.0, 2.0 }, .{ -12.0, -10.0, 2.0 },
// --- Group C: degenerate/parallel ---
// Tri 15: zero-area (all same point)
.{ 1.0, 1.0, 6.0 }, .{ 1.0, 1.0, 6.0 }, .{ 1.0, 1.0, 6.0 },
// Tri 16: collinear vertices
.{ -1.0, 0.0, 7.0 }, .{ 0.0, 0.0, 7.0 }, .{ 1.0, 0.0, 7.0 },
// Tri 17: nearly parallel to ray (plane nearly parallel to Z axis)
.{ -1.0, -100.0, 0.5 }, .{ 1.0, -100.0, 0.5 }, .{ 0.0, 100.0, 0.501 },
// Tri 18: parallel to ray (exactly in XY plane at z=0, ray along Z)
.{ -1.0, -1.0, 0.0 }, .{ 1.0, -1.0, 0.0 }, .{ 0.0, 1.0, 0.0 },
// --- Group D: more hits at various depths for closest-t tracking ---
// Tri 19: closest possible hit
.{ -5.0, -5.0, 0.01 }, .{ 5.0, -5.0, 0.01 }, .{ 0.0, 5.0, 0.01 },
// Tri 20-24: hits at regular depth intervals
.{ -0.3, -0.3, 0.5 }, .{ 0.3, -0.3, 0.5 }, .{ 0.0, 0.3, 0.5 },
.{ -1.0, -1.0, 1.2 }, .{ 1.0, -1.0, 1.2 }, .{ 0.0, 1.0, 1.2 },
.{ -0.8, -0.8, 2.0 }, .{ 0.8, -0.8, 2.0 }, .{ 0.0, 0.8, 2.0 },
.{ -1.5, -1.5, 3.0 }, .{ 1.5, -1.5, 3.0 }, .{ 0.0, 1.5, 3.0 },
.{ -2.0, -2.0, 5.5 }, .{ 2.0, -2.0, 5.5 }, .{ 0.0, 2.0, 5.5 },
// --- Group E: outside AABB (outcode rejects, never reach ray-tri) ---
// Tri 25: all verts above AABB
.{ -1.0, 5.0, 1.0 }, .{ 1.0, 5.0, 1.0 }, .{ 0.0, 6.0, 1.0 },
// Tri 26: all verts below AABB
.{ -1.0, -6.0, 1.0 }, .{ 1.0, -6.0, 1.0 }, .{ 0.0, -5.0, 1.0 },
// Tri 27: all verts left of AABB
.{ -6.0, -1.0, 1.0 }, .{ -5.0, -1.0, 1.0 }, .{ -6.0, 1.0, 1.0 },
// Tri 28: all verts in front of AABB (z < min)
.{ -1.0, -1.0, -5.0 }, .{ 1.0, -1.0, -5.0 }, .{ 0.0, 1.0, -5.0 },
// Tri 29: all verts behind AABB (z > max)
.{ -1.0, -1.0, 5.0 }, .{ 1.0, -1.0, 5.0 }, .{ 0.0, 1.0, 5.0 },
// --- Group F: asymmetric/skewed hits testing det sign & magnitude ---
// Tri 30: very elongated, hit near tip
.{ 0.0, -0.01, 1.8 }, .{ 0.02, -0.01, 1.8 }, .{ 0.0, 10.0, 1.8 },
// Tri 31: very flat (nearly zero Y extent)
.{ -5.0, -0.001, 2.2 }, .{ 5.0, -0.001, 2.2 }, .{ 0.0, 0.001, 2.2 },
// Tri 32: large negative det
.{ -3.0, 3.0, 2.8 }, .{ 3.0, -3.0, 2.8 }, .{ -3.0, -3.0, 2.8 },
// Tri 33: det exactly at threshold boundary
.{ -0.0001, -0.0001, 6.5 }, .{ 0.0001, -0.0001, 6.5 }, .{ 0.0, 0.0001, 6.5 },
// --- Group G: stress closest-t with many competing hits ---
// Tri 34-39: hits at very close z-values to test precision
.{ -1.0, -1.0, 0.100 }, .{ 1.0, -1.0, 0.100 }, .{ 0.0, 1.0, 0.100 },
.{ -1.0, -1.0, 0.101 }, .{ 1.0, -1.0, 0.101 }, .{ 0.0, 1.0, 0.101 },
.{ -1.0, -1.0, 0.099 }, .{ 1.0, -1.0, 0.099 }, .{ 0.0, 1.0, 0.099 },
.{ -1.0, -1.0, 0.102 }, .{ 1.0, -1.0, 0.102 }, .{ 0.0, 1.0, 0.102 },
.{ -1.0, -1.0, 0.098 }, .{ 1.0, -1.0, 0.098 }, .{ 0.0, 1.0, 0.098 },
.{ -1.0, -1.0, 0.103 }, .{ 1.0, -1.0, 0.103 }, .{ 0.0, 1.0, 0.103 },
};
// Write vertices to hash entry at +8
for (0..NVERTS) |vi| {
const off = he + 8 + vi * 12;
@as(*align(1) f32, @ptrFromInt(off)).* = verts[vi][0];
@as(*align(1) f32, @ptrFromInt(off + 4)).* = verts[vi][1];
@as(*align(1) f32, @ptrFromInt(off + 8)).* = verts[vi][2];
}
// Triangle count at +0x18A4
@as(*align(1) u16, @ptrFromInt(he + 0x18A4)).* = NTRIS;
// Each triangle uses 3 consecutive vertices: tri N -> verts N*3, N*3+1, N*3+2
{
var ti: u32 = 0;
while (ti < NTRIS) : (ti += 1) {
const base: u16 = @intCast(ti * 3);
@as(*align(1) u16, @ptrFromInt(he + 0x18A6 + ti * 6)).* = base;
@as(*align(1) u16, @ptrFromInt(he + 0x18A6 + ti * 6 + 2)).* = base + 1;
@as(*align(1) u16, @ptrFromInt(he + 0x18A6 + ti * 6 + 4)).* = base + 2;
@as(*align(1) u16, @ptrFromInt(he + 0x1FAE + ti * 2)).* = 0;
@as(*align(1) u16, @ptrFromInt(he + 0x2206 + ti * 2)).* = @intCast(ti);
}
}
// Build "this" struct (needs ~0x54 bytes)
var this_buf: [0x60]u8 align(4) = [_]u8{0} ** 0x60;
const th = @intFromPtr(&this_buf);
// Visited array: needs at least NTRIS*2 bytes
var visited: [64]u8 = [_]u8{0} ** 64;
// Result float
var result_val: f32 = 0.0;
// this+0x04 = visited array base
@as(*align(1) u32, @ptrFromInt(th + 0x04)).* = @intFromPtr(&visited);
// this+0x08, +0x0C = hash params (must match what FindOrCreateHashEntry expects, but
// we'll call our function directly bypassing the hash lookup, so these don't matter)
// this+0x10 = pointer to result float
@as(*align(1) u32, @ptrFromInt(th + 0x10)).* = @intFromPtr(&result_val);
// this+0x14 = clamp value
@as(*align(1) f32, @ptrFromInt(th + 0x14)).* = 100.0;
// this+0x18..0x2C = AABB extents (will be sorted by function)
@as(*align(1) f32, @ptrFromInt(th + 0x18)).* = -3.0; // ax0
@as(*align(1) f32, @ptrFromInt(th + 0x1C)).* = -3.0; // ay0
@as(*align(1) f32, @ptrFromInt(th + 0x20)).* = -3.0; // az0
@as(*align(1) f32, @ptrFromInt(th + 0x24)).* = 3.0; // ax1
@as(*align(1) f32, @ptrFromInt(th + 0x28)).* = 3.0; // ay1
@as(*align(1) f32, @ptrFromInt(th + 0x2C)).* = 3.0; // az1
// this+0x30..0x3B = ray origin
@as(*align(1) f32, @ptrFromInt(th + 0x30)).* = 0.0;
@as(*align(1) f32, @ptrFromInt(th + 0x34)).* = 0.0;
@as(*align(1) f32, @ptrFromInt(th + 0x38)).* = -10.0;
// this+0x3C..0x47 = ray direction
@as(*align(1) f32, @ptrFromInt(th + 0x3C)).* = 0.0;
@as(*align(1) f32, @ptrFromInt(th + 0x40)).* = 0.0;
@as(*align(1) f32, @ptrFromInt(th + 0x44)).* = 1.0;
// this+0x48 = scale factor
@as(*align(1) f32, @ptrFromInt(th + 0x48)).* = 1.0;
// this+0x4C = closest-t (large initial value)
@as(*align(1) f32, @ptrFromInt(th + 0x4C)).* = 999999.0;
// this+0x50 = collision mask
@as(*align(1) u16, @ptrFromInt(th + 0x50)).* = 0;
// Map globals needed by both original and SSE functions
_ = mapZeroed(0xCA0000, 0x1000); // g_guard at 0xCA03E4
_ = mapZeroed(0xCDE000, 0x1000); // g_render_list at 0xCDE648
_ = mapZeroed(0xCE2000, 0x1000); // g_visible_count/list at 0xCE26E0/E8
_ = mapZeroed(0xCE6000, 0x1000); // g_render_count at 0xCE66FC
// Patch FindOrCreateHashEntry (0x693D60) to return our hash_buf:
// MOV EAX, <hash_buf_addr> ; B8 xx xx xx xx
// RET 0x14 ; C2 14 00
const hash_stub = @as([*]u8, @ptrFromInt(0x693D60));
hash_stub[0] = 0xB8;
@as(*align(1) u32, @ptrFromInt(0x693D61)).* = he;
hash_stub[5] = 0xC2;
hash_stub[6] = 0x14;
hash_stub[7] = 0x00;
// Set g_guard to non-zero (both functions check this)
@as(*align(1) u32, @ptrFromInt(0xCA03E4)).* = 1;
// Original function at 0x6B88E0 and our SSE version
const orig_fn = @as(*const fn (u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) u32, @ptrFromInt(0x6B88E0));
// Reset state helper
const resetState = struct {
fn f(t: u32, v: *[64]u8) void {
@as(*align(1) f32, @ptrFromInt(t + 0x4C)).* = 999999.0;
@memset(v, 0);
@as(*u32, @ptrFromInt(0xCE26E0)).* = 0; // visible count
@as(*u32, @ptrFromInt(0xCE66FC)).* = 0; // render count
}
}.f;
// Get truth values from original x87 function
resetState(th, &visited);
_ = @call(.never_tail, orig_fn, .{ th, 0, 0 });
const orig_closest = @as(*align(1) f32, @ptrFromInt(th + 0x4C)).*;
const orig_result = result_val;
const orig_render_count = @as(*u32, @ptrFromInt(0xCE66FC)).*;
const orig_visible_count = @as(*u32, @ptrFromInt(0xCE26E0)).*;
// Run SSE version
resetState(th, &visited);
_ = @call(.never_tail, performCollisionDetectionSSE, .{ th, 0, 0 });
const sse_closest = @as(*align(1) f32, @ptrFromInt(th + 0x4C)).*;
const sse_result = result_val;
const sse_render_count = @as(*u32, @ptrFromInt(0xCE66FC)).*;
const sse_visible_count = @as(*u32, @ptrFromInt(0xCE26E0)).*;
const t_match = @abs(orig_closest - sse_closest) < 0.01 or (orig_closest > 99999.0 and sse_closest > 99999.0);
const r_match = @abs(orig_result - sse_result) < 0.01;
const ok = t_match and r_match and orig_render_count == sse_render_count and orig_visible_count == sse_visible_count;
if (!ok) {
print(" MISMATCH detail:\n", .{});
print(" closest-t: orig={d:.6} sse={d:.6}\n", .{ orig_closest, sse_closest });
print(" result: orig={d:.6} sse={d:.6}\n", .{ orig_result, sse_result });
} else {
print(" closest-t={d:.4} result={d:.4} hits={d} visible={d}\n", .{
sse_closest, sse_result, sse_render_count, sse_visible_count,
});
}
// Benchmark: our SSE performCollisionDetectionSSE
var best: u64 = std.math.maxInt(u64);
for (0..5) |_| {
var t0 = rdtsc();
for (0..ITERS) |_| {
resetState(th, &visited);
_ = @call(.never_tail, performCollisionDetectionSSE, .{ th, 0, 0 });
}
t0 = rdtsc() - t0;
if (t0 < best) best = t0;
}
const per_call = best / ITERS;
const per_tri = if (NTRIS > 0) per_call / NTRIS else 0;
const status: [*:0]const u8 = if (ok) "OK" else "MISMATCH";
print("{s:>30}: {d} cyc/call {d} cyc/tri ({d} tris) {s}\n", .{
"performCollisionDet", per_call, per_tri, NTRIS, status,
});
}
fn bench_entityUpdate() void {
// Map .bss pages for globals the entity update reads/writes
_ = mapZeroed(0xC62000, 0x2000); // delta time at 0xC62510
_ = mapZeroed(0xC7B000, 0x2000); // view coeffs at 0xC7BCB0, bounds at 0xC7CB5C-C7CB70
_ = mapZeroed(0x866000, 0x4000); // render flags at 0x867960, ptrs at 0x867964/68
_ = mapZeroed(0x80A000, 0x1000); // timer threshold at 0x80A1E8
_ = mapZeroed(0x86B000, 0x1000); // anim table at 0x86B580
_ = mapZeroed(0xC7F000, 0x1000); // anim index at 0xC7F294
// Set up view coefficients (a,b,c,d) at 0xC7BCB0
@as(*align(1) f32, @ptrFromInt(0xC7BCB0)).* = 0.5; // coeff for ent+0x5C
@as(*align(1) f32, @ptrFromInt(0xC7BCB4)).* = 0.3; // coeff for ent+0x60
@as(*align(1) f32, @ptrFromInt(0xC7BCB8)).* = 0.7; // coeff for ent+0x64
@as(*align(1) f32, @ptrFromInt(0xC7BCBC)).* = 1.0; // constant term
// Delta time
@as(*align(1) f32, @ptrFromInt(0xC62510)).* = 0.016; // ~60fps
// Timer threshold
@as(*align(1) f32, @ptrFromInt(0x80A1E8)).* = 999.0; // high so recycling never triggers
// World bounds (set large so bounds check always passes)
@as(*align(1) f32, @ptrFromInt(0xC7CB68)).* = 999.0;
@as(*align(1) f32, @ptrFromInt(0xC7CB6C)).* = 999.0;
@as(*align(1) f32, @ptrFromInt(0xC7CB70)).* = 999.0;
// Disable spatial grid registration by making bounds check fail:
// Set the lower bounds high so IsPointInsideBounds returns false
@as(*align(1) f32, @ptrFromInt(0xC7CB5C)).* = 99999.0;
@as(*align(1) f32, @ptrFromInt(0xC7CB60)).* = 99999.0;
@as(*align(1) f32, @ptrFromInt(0xC7CB64)).* = 99999.0;
// Build a synthetic entity struct (~0x900 bytes to cover all accessed fields)
var ent_buf: [0x900]u8 align(16) = [_]u8{0} ** 0x900;
const ent = @intFromPtr(&ent_buf);
// Entity position fields for dot product
@as(*align(1) f32, @ptrFromInt(ent + 0x5C)).* = 10.0;
@as(*align(1) f32, @ptrFromInt(ent + 0x60)).* = 20.0;
@as(*align(1) f32, @ptrFromInt(ent + 0x64)).* = 30.0;
@as(*align(1) f32, @ptrFromInt(ent + 0x68)).* = 5.0; // depth offset
// Timer at ent+0xAC (start at 0)
@as(*align(1) f32, @ptrFromInt(ent + 0xAC)).* = 0.0;
// No vertex buffers (ent+0x14C = 0), no instances (ent+0xC0 = 0), no chunks
// Bounds fields that won't trigger spatial grid
@as(*align(1) f32, @ptrFromInt(ent + 0x44)).* = 999.0; // will fail < check
const orig_fn = @as(*const fn (u32) callconv(.{ .x86_fastcall = .{} }) void, @ptrFromInt(0x6AFAD0));
// Reset helper
const resetEnt = struct {
fn f(e: u32) void {
@as(*align(1) f32, @ptrFromInt(e + 0xAC)).* = 0.0; // reset timer
@as(*align(1) f32, @ptrFromInt(e + 0x78)).* = 0.0; // reset depth
}
}.f;
// Correctness: both should compute the same depth
resetEnt(ent);
@call(.never_tail, orig_fn, .{ent});
const orig_depth = @as(*align(1) f32, @ptrFromInt(ent + 0x78)).*;
resetEnt(ent);
@call(.never_tail, updateEntityAndChunksPositions, .{ent});
const sse_depth = @as(*align(1) f32, @ptrFromInt(ent + 0x78)).*;
const ok = @abs(orig_depth - sse_depth) < 0.01;
if (!ok) {
print(" MISMATCH: orig_depth={d:.4} sse_depth={d:.4}\n", .{ orig_depth, sse_depth });
}
// Benchmark original
var orig_cyc: u64 = std.math.maxInt(u64);
for (0..5) |_| {
var t0 = rdtsc();
for (0..ITERS) |_| {
resetEnt(ent);
@call(.never_tail, orig_fn, .{ent});
}
t0 = rdtsc() - t0;
if (t0 < orig_cyc) orig_cyc = t0;
}
// Benchmark SSE
var sse_cyc: u64 = std.math.maxInt(u64);
for (0..5) |_| {
var t0 = rdtsc();
for (0..ITERS) |_| {
resetEnt(ent);
@call(.never_tail, updateEntityAndChunksPositions, .{ent});
}
t0 = rdtsc() - t0;
if (t0 < sse_cyc) sse_cyc = t0;
}
report("UpdateEntityChunkPos", orig_cyc, sse_cyc, ok);
}
fn bench_computeOutcodes() void {
// Generate 150 vertices (typical mesh) spread across an AABB
const NVERTS = 150;
var verts: [NVERTS * 3]f32 = undefined;
var seed: u32 = 0xDEADBEEF;
for (0..NVERTS * 3) |j| {
seed = seed *% 1103515245 +% 12345;
// Range roughly -10..+10
verts[j] = @as(f32, @floatFromInt(@as(i32, @bitCast(seed >> 16)) >> 16)) * 0.0003;
}
// AABB bounds: minX,minY,minZ,maxX,maxY,maxZ
var bounds = [6]f32{ -2.0, -2.0, -2.0, 2.0, 2.0, 2.0 };
// Original x87 version: extract the outcode loop from PerformSpatialCulling.
// The original does 6 FCOMP+FNSTSW+TEST sequences per vertex.
// We'll inline a scalar reference implementation for the original.
var out_orig: [NVERTS]u8 = undefined;
var out_sse: [NVERTS]u8 = undefined;
// Scalar reference (matches original x87 logic)
for (0..NVERTS) |i| {
const vx = verts[i * 3];
const vy = verts[i * 3 + 1];
const vz = verts[i * 3 + 2];
var code: u8 = 0;
if (vx < bounds[0]) code |= 0x20;
if (vx >= bounds[3]) code |= 0x10;
if (vy < bounds[1]) code |= 0x08;
if (vy >= bounds[4]) code |= 0x04;
if (vz < bounds[2]) code |= 0x02;
if (vz >= bounds[5]) code |= 0x01;
out_orig[i] = code;
}
// SSE version
benchComputeOutcodes(a(&verts), a(&bounds), a(&out_sse), NVERTS);
// Verify correctness
var ok = true;
for (0..NVERTS) |i| {
if (out_orig[i] != out_sse[i]) {
ok = false;
break;
}
}
// Benchmark: scalar reference
const scalar_fn = struct {
fn run(v: *[NVERTS * 3]f32, b: *[6]f32, out: *[NVERTS]u8) void {
for (0..NVERTS) |i| {
const vx = v[i * 3];
const vy = v[i * 3 + 1];
const vz = v[i * 3 + 2];
var code: u8 = 0;
if (vx < b[0]) code |= 0x20;
if (vx >= b[3]) code |= 0x10;
if (vy < b[1]) code |= 0x08;
if (vy >= b[4]) code |= 0x04;
if (vz < b[2]) code |= 0x02;
if (vz >= b[5]) code |= 0x01;
out[i] = code;
}
}
}.run;
var orig_cyc: u64 = std.math.maxInt(u64);
var sse_cyc: u64 = std.math.maxInt(u64);
for (0..5) |_| {
var t = rdtsc();
for (0..ITERS) |_| scalar_fn(&verts, &bounds, &out_orig);
t = rdtsc() - t;
if (t < orig_cyc) orig_cyc = t;
}
for (0..5) |_| {
var t = rdtsc();
for (0..ITERS) |_| benchComputeOutcodes(a(&verts), a(&bounds), a(&out_sse), NVERTS);
t = rdtsc() - t;
if (t < sse_cyc) sse_cyc = t;
}
report("computeOutcodes(150v)", orig_cyc, sse_cyc, ok);
}