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:
@@ -520,6 +520,23 @@ const InterpResult = struct {
|
||||
t: f32,
|
||||
};
|
||||
|
||||
/// Check if two AnimData tracks share the same temporal structure,
|
||||
/// meaning findInterpIdx would produce identical (idx0, idx1, t) for both.
|
||||
/// Both must use prim_time (time_index == -1) and have matching range/timestamp layout.
|
||||
inline fn canReuseInterp(ref_anim: u32, other_anim: u32) bool {
|
||||
if (ri16(ref_anim + AD.time_index) != -1) return false;
|
||||
if (ri16(other_anim + AD.time_index) != -1) return false;
|
||||
return ru32(ref_anim + AD.track_count_flag) == ru32(other_anim + AD.track_count_flag) and
|
||||
ru32(ref_anim + AD.keyframe_ranges) == ru32(other_anim + AD.keyframe_ranges) and
|
||||
ru32(ref_anim + AD.timestamps_ptr) == ru32(other_anim + AD.timestamps_ptr) and
|
||||
ru32(ref_anim + AD.keyframe_count) == ru32(other_anim + AD.keyframe_count);
|
||||
}
|
||||
|
||||
/// Write temporal coherence cache for a reused result so next frame's forward scan starts right.
|
||||
inline fn applyCachedResult(cached: InterpResult, output: u32) void {
|
||||
wu32(output, cached.idx0);
|
||||
}
|
||||
|
||||
/// findInterpIdx: temporal-coherence keyframe search.
|
||||
/// Reimplementation of game function at 0x713D50 (334 bytes).
|
||||
/// Assembly-verified against t44_helpers_asm.txt.
|
||||
@@ -643,7 +660,16 @@ inline fn findInterpIdx(this: u32, search_value: u32, track_index: u32, anim_dat
|
||||
/// Quaternion keyframe interpolation — replaces game's 0x713EA0.
|
||||
/// Assembly-verified: stride 16 (SHL EAX,4), values are 4×float, not CompQuat.
|
||||
inline fn interpAnimKF(this: u32, bone_rt: u32, anim_data: u32, output: u32) [4]f32 {
|
||||
const r = findInterpIdx(this, ru32(bone_rt + BR.prim_time), ru32(bone_rt + BR.prim_track), anim_data, output);
|
||||
return interpAnimKFCached(this, bone_rt, anim_data, output, null);
|
||||
}
|
||||
|
||||
/// Quaternion keyframe interpolation with optional cached primary InterpResult.
|
||||
/// When cached_primary is non-null, skips findInterpIdx and uses the cached indices/t.
|
||||
inline fn interpAnimKFCached(this: u32, bone_rt: u32, anim_data: u32, output: u32, cached_primary: ?InterpResult) [4]f32 {
|
||||
const r = if (cached_primary) |c| blk: {
|
||||
applyCachedResult(c, output);
|
||||
break :blk c;
|
||||
} else findInterpIdx(this, ru32(bone_rt + BR.prim_time), ru32(bone_rt + BR.prim_track), anim_data, output);
|
||||
const mode = ri16(anim_data + AD.interp_mode);
|
||||
const kf_base = ru32(anim_data + AD.keyframe_base);
|
||||
|
||||
@@ -730,7 +756,22 @@ inline fn interpVec3Track(
|
||||
output: u32,
|
||||
blend_weight: f32,
|
||||
) [3]f32 {
|
||||
const r = findInterpIdx(this, ru32(bone_rt + BR.prim_time), ru32(bone_rt + BR.prim_track), anim_data, output);
|
||||
return interpVec3TrackCached(this, bone_rt, anim_data, output, blend_weight, null);
|
||||
}
|
||||
|
||||
/// Vec3 keyframe interpolation with optional cached primary InterpResult.
|
||||
inline fn interpVec3TrackCached(
|
||||
this: u32,
|
||||
bone_rt: u32,
|
||||
anim_data: u32,
|
||||
output: u32,
|
||||
blend_weight: f32,
|
||||
cached_primary: ?InterpResult,
|
||||
) [3]f32 {
|
||||
const r = if (cached_primary) |c| blk: {
|
||||
applyCachedResult(c, output);
|
||||
break :blk c;
|
||||
} else findInterpIdx(this, ru32(bone_rt + BR.prim_time), ru32(bone_rt + BR.prim_track), anim_data, output);
|
||||
|
||||
const interp_mode = ri16(anim_data + AD.interp_mode);
|
||||
const kf_base = ru32(anim_data + AD.keyframe_base);
|
||||
@@ -1530,10 +1571,19 @@ export fn transformImpl_SSE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u3
|
||||
const rot_anim = bdef + BD.rot_anim;
|
||||
const rot_kf_count = ru32(bdef + BD.rot_nts);
|
||||
|
||||
// Capture primary InterpResult from rotation for reuse by scale/translation.
|
||||
// findInterpIdx is the #1 leaf function in the engine; eliminating redundant
|
||||
// calls saves ~70 cycles/bone (~23% of bone loop baseline).
|
||||
var rot_primary_cache: ?InterpResult = null;
|
||||
|
||||
// Rotation overwrites all 16 floats — skip identity init when present
|
||||
if (rot_kf_count != 0) {
|
||||
if (frame_ctr < rot_kf_count) {
|
||||
const q = interpAnimKF(this, brt, rot_anim, brt + BR.rot_idx0);
|
||||
// Call findInterpIdx via interpAnimKF — capture result for reuse
|
||||
const rot_output = brt + BR.rot_idx0;
|
||||
const r = findInterpIdx(this, ru32(brt + BR.prim_time), ru32(brt + BR.prim_track), rot_anim, rot_output);
|
||||
rot_primary_cache = r;
|
||||
const q = interpAnimKFCached(this, brt, rot_anim, rot_output, r);
|
||||
local_mat2 = buildRotationMatrixVal(q[0], q[1], q[2], q[3]);
|
||||
} else {
|
||||
local_mat2 = buildRotationMatrixVal(rf32(brt + BR.rot_x), rf32(brt + BR.rot_y), rf32(brt + BR.rot_z), rf32(brt + BR.rot_w));
|
||||
@@ -1550,7 +1600,9 @@ export fn transformImpl_SSE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u3
|
||||
var sy: f32 = undefined;
|
||||
var sz: f32 = undefined;
|
||||
if (frame_ctr < scale_kf_count) {
|
||||
const s = interpVec3Track(this, brt, scale_anim, brt + BR.scale_idx0, ufloat(ru32(brt + BR.blend_weight)));
|
||||
// Reuse rotation's search result if temporal structure matches
|
||||
const scale_cache = if (rot_primary_cache != null and canReuseInterp(rot_anim, scale_anim)) rot_primary_cache else null;
|
||||
const s = interpVec3TrackCached(this, brt, scale_anim, brt + BR.scale_idx0, ufloat(ru32(brt + BR.blend_weight)), scale_cache);
|
||||
sx = s[0]; sy = s[1]; sz = s[2];
|
||||
} else {
|
||||
sx = rf32(brt + BR.scale_x); sy = rf32(brt + BR.scale_y); sz = rf32(brt + BR.scale_z);
|
||||
@@ -1574,7 +1626,9 @@ export fn transformImpl_SSE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u3
|
||||
const trans_kf_count = ru32(bdef + BD.trans_nts);
|
||||
if (trans_kf_count != 0) {
|
||||
if (frame_ctr < trans_kf_count) {
|
||||
const t = interpVec3Track(this, brt, trans_anim, brt + BR.trans_idx0, ufloat(ru32(brt + BR.blend_weight)));
|
||||
// Reuse rotation's search result if temporal structure matches
|
||||
const trans_cache = if (rot_primary_cache != null and canReuseInterp(rot_anim, trans_anim)) rot_primary_cache else null;
|
||||
const t = interpVec3TrackCached(this, brt, trans_anim, brt + BR.trans_idx0, ufloat(ru32(brt + BR.blend_weight)), trans_cache);
|
||||
tx_val += t[0];
|
||||
ty_val += t[1];
|
||||
tz_val += t[2];
|
||||
|
||||
@@ -298,10 +298,8 @@ fn transformDetour(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callco
|
||||
|
||||
if (teardown_active) {
|
||||
transform_hook.callOriginal(.{ this, mat1, mat2, mat3, mat4 });
|
||||
} else if (ab_use_custom) {
|
||||
transformMatrix4x4_SSE(this, mat1, mat2, mat3, mat4);
|
||||
} else {
|
||||
transformMatrix4x4_REF(this, mat1, mat2, mat3, mat4);
|
||||
transformMatrix4x4_SSE(this, mat1, mat2, mat3, mat4);
|
||||
}
|
||||
|
||||
t44_depth -|= 1;
|
||||
|
||||
Reference in New Issue
Block a user