perf: findInterpIdx dedup, processLinkedListCollision SSE, permanent t44

- bone_sse: deduplicate findInterpIdx calls in bone loop — rotation's
  search result reused for scale/translation when tracks share temporal
  structure (canReuseInterp guard). Est. ~23% bone loop cycle reduction.
- silicon: SSE replacement for processLinkedListCollision (0x6ABC40,
  1.57% CPU). V4 AABB overlap test replaces 6 x87 FCOMP/FNSTSW.
  Benched at 3.2x speedup (378→115 cyc/call, 8 nodes).
- transform44: remove A/B toggle, always use SSE path (teardown guard
  kept). A/B infrastructure remains for other hooks.
- bench: add processLinkedListCollision benchmark with fake linked list
  test fixture and stubbed addGeometryToBuffer.
This commit is contained in:
MarcelineVQ
2026-03-23 20:27:26 -07:00
parent b0b70a744e
commit 7819d6d914
5 changed files with 283 additions and 8 deletions
+143
View File
@@ -49,6 +49,7 @@ extern fn si_mulMat3x4InPlace(u32, u32) callconv(cc_tc) u32;
extern fn si_normalizeVec3InPlace(u32) callconv(cc_tc) void;
extern fn si_vec3Dot(u32, u32) callconv(cc_fc) f64;
extern fn si_translateBoundingVol(u32, u32) callconv(cc_tc) void;
extern fn si_processLinkedListCollision(u32, u32, u32, u32) callconv(cc_fc) u32;
extern fn si_addVec3ToAccumulator(u32, u32) callconv(cc_tc) void;
extern fn si_addToColorAccumulator(u32, u32) callconv(cc_tc) void;
extern fn si_packParticleColor(u32, u32, u32, u32) callconv(cc_tc) void;
@@ -1767,9 +1768,151 @@ pub fn main() void {
}
}
// si_processLinkedListCollision -- fastcall(listHead_ECX, queryBox_EDX, resultBuf_stack, flags_stack) -> u32
// Builds a fake linked list with 8 nodes to benchmark AABB overlap test.
bench_processLinkedListCollision();
print("\n", .{});
}
fn bench_processLinkedListCollision() void {
// Map page for sentinel global at 0xC89F20
_ = mapZeroed(0xC89000, 0x1000);
// Map page for addGeometryToBuffer's result_buf writes (just needs writable memory)
// Also need pages at 0xCA0000 range for any globals addGeometryToBuffer touches
const NODE_COUNT = 8;
// Sentinel: just a unique non-zero value. Original code reads *(u32*)0xC89F20.
const sentinel: u32 = 0xDEADBEEF;
@as(*u32, @ptrFromInt(0xC89F20)).* = sentinel;
// --- Build fake node data blocks (need offsets: +0x0C, +0x88, +0x8C, +0x14C-0x164, +0x180, +0x184) ---
// Each node_data needs at least 0x188 bytes
const NODE_DATA_SIZE = 0x190;
var node_data_buf: [NODE_COUNT * NODE_DATA_SIZE]u8 align(4) = std.mem.zeroes([NODE_COUNT * NODE_DATA_SIZE]u8);
// Query box: min=(0,0,0), max=(10,10,10)
var query_box = [6]f32{ 0.0, 0.0, 0.0, 10.0, 10.0, 10.0 };
// Stub addGeometryToBuffer at 0x6ABD90 → RET 0x4 (just returns, no side effects).
// Both original and SSE call the same stub, isolating the linked list walk + AABB test.
// Original bytes are in mapped .text — overwrite with: C2 04 00 (RET 4)
@as(*[3]u8, @ptrFromInt(0x6ABD90)).* = .{ 0xC2, 0x04, 0x00 };
// Set up each node_data
for (0..NODE_COUNT) |i| {
const nd = @intFromPtr(&node_data_buf) + i * NODE_DATA_SIZE;
// flags at +0x0C: bit 0x80 set (required, else returns 0), no 0x100 (not skipped)
@as(*align(1) u16, @ptrFromInt(nd + 0x0C)).* = 0x80;
// active at +0x88: non-zero (just needs to pass != 0 check)
@as(*align(1) u32, @ptrFromInt(nd + 0x88)).* = 1;
// visited at +0x8C: NOT sentinel (so it gets processed)
@as(*align(1) u32, @ptrFromInt(nd + 0x8C)).* = 0;
// type discriminator: both zero → use flags & 0xF
@as(*align(1) u32, @ptrFromInt(nd + 0x180)).* = 0;
@as(*align(1) u32, @ptrFromInt(nd + 0x184)).* = 0;
// AABB at +0x14C: alternate overlapping and non-overlapping
const aabb: *align(1) [6]f32 = @ptrFromInt(nd + 0x14C);
if (i % 2 == 0) {
// Overlapping: min=(1,1,1), max=(5,5,5)
aabb.* = .{ 1.0, 1.0, 1.0, 5.0, 5.0, 5.0 };
} else {
// Non-overlapping: min=(20,20,20), max=(30,30,30)
aabb.* = .{ 20.0, 20.0, 20.0, 30.0, 30.0, 30.0 };
}
}
// --- Build linked list nodes ---
// Intrusive list: node = { ??, node_data_ptr, ... }
// link_offset stored at listHead[0], next at *(link_offset + node + 4)
// Simplest: link_offset = 0, so next = *(node + 4) ... no wait.
// Re-reading assembly: next = *(*(listHead) + prev_node + 4)
// listHead[0] = link_offset (byte offset within node to find next-ptr)
// Actually from the asm: MOV EAX,[EBP-0xc] (=listHead), MOV EAX,[EAX] (=*listHead = link_offset)
// MOV ECX,[EAX + EDX*1 + 4] where EDX=node
// So: next = *(link_offset + node + 4)
// If link_offset = 0: next = *(node + 4), but node+4 is node_data_ptr!
// We need link_offset such that (link_offset + node + 4) points to a "next" field.
// Let's use link_offset = 4, so next = *(node + 8).
// Node layout: [node_data_ptr(+0), ?(+4), next(+8)]
// But wait, node+4 is where node_data is read: MOV EBX,[EDX+4] (EDX=node)
// So node = { pad(+0), node_data(+4), next(+8) } and link_offset = 4.
const NODE_SIZE = 12; // pad, node_data_ptr, next_ptr
var nodes: [NODE_COUNT * NODE_SIZE]u8 align(4) = std.mem.zeroes([NODE_COUNT * NODE_SIZE]u8);
for (0..NODE_COUNT) |i| {
const n = @intFromPtr(&nodes) + i * NODE_SIZE;
// node+4 = node_data pointer
@as(*align(1) u32, @ptrFromInt(n + 4)).* = @intCast(@intFromPtr(&node_data_buf) + i * NODE_DATA_SIZE);
// node+8 = next node (link_offset=4, so *(link_offset + node + 4) = *(node + 8))
if (i + 1 < NODE_COUNT) {
@as(*align(1) u32, @ptrFromInt(n + 8)).* = @intCast(@intFromPtr(&nodes) + (i + 1) * NODE_SIZE);
} else {
@as(*align(1) u32, @ptrFromInt(n + 8)).* = 0; // end: NULL terminates
}
}
// listHead: [0]=link_offset, [4]=??, [8]=first_node
var list_head = [3]u32{
4, // link_offset
0,
@intCast(@intFromPtr(&nodes)), // first node
};
// Result buffer: addGeometryToBuffer writes here. Just needs writable memory.
var result_buf: [4096]u8 = std.mem.zeroes([4096]u8);
// flags: 0xF (low nibble set, matching type discriminator for both-zero type)
const flags: u32 = 0x8F; // bit 7 set + low nibble
// --- Correctness check ---
const of = origFn(fn (u32, u32, u32, u32) callconv(cc_fc) u32, 0x6ABC40);
// Reset visited markers before each call
for (0..NODE_COUNT) |i| {
@as(*align(1) u32, @ptrFromInt(@intFromPtr(&node_data_buf) + i * NODE_DATA_SIZE + 0x8C)).* = 0;
}
const ret_orig = of(a(&list_head), a(&query_box), a(&result_buf), flags);
for (0..NODE_COUNT) |i| {
@as(*align(1) u32, @ptrFromInt(@intFromPtr(&node_data_buf) + i * NODE_DATA_SIZE + 0x8C)).* = 0;
}
const ret_sse = si_processLinkedListCollision(a(&list_head), a(&query_box), a(&result_buf), flags);
const ok = ret_orig == ret_sse;
// --- Benchmark ---
var t: u64 = std.math.maxInt(u64);
for (0..5) |_| {
const _t0 = rdtsc();
for (0..ITERS) |_| {
// Reset visited markers each iteration (original marks them)
for (0..NODE_COUNT) |i| {
@as(*align(1) u32, @ptrFromInt(@intFromPtr(&node_data_buf) + i * NODE_DATA_SIZE + 0x8C)).* = 0;
}
_ = of(a(&list_head), a(&query_box), a(&result_buf), flags);
}
const _te = rdtsc() - _t0;
if (_te < t) t = _te;
}
var s: u64 = std.math.maxInt(u64);
for (0..5) |_| {
const _t0 = rdtsc();
for (0..ITERS) |_| {
for (0..NODE_COUNT) |i| {
@as(*align(1) u32, @ptrFromInt(@intFromPtr(&node_data_buf) + i * NODE_DATA_SIZE + 0x8C)).* = 0;
}
_ = si_processLinkedListCollision(a(&list_head), a(&query_box), a(&result_buf), flags);
}
const _te = rdtsc() - _t0;
if (_te < s) s = _te;
}
report("processLinkedListCollision", t, s, ok);
}
// =========================================================================
// Generic benchmarks for common signatures (called versions)
// =========================================================================