From dd43f6aca387bcb8c4ec801de2e9922a861cfa70 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Mon, 16 Mar 2026 16:25:26 -0700 Subject: [PATCH] bone_sse: return interp values in registers, hoist runtime constants, value-based local matrix - interpAnimKF returns [4]f32, interpVec3Track returns [3]f32, interpFloatTrack returns f32 - Internal crossfade blends stay in registers instead of writing then re-reading from memory - Bone loop uses returned values directly for buildRotationMatrix/scaleMatrix3x3/translation - Hoist getShortToFloat() reads to function entry in section functions (Proposal D) - Remove dead blendVec3, getHermite5, HERMITE_3 - buildRotationMatrixVal returns [16]f32; bone loop uses array ops instead of u32 pointer casts - matMul4x4Local/matMul4x4InPlace variants for local array operands - shortInterpToFloat takes pre-read stf parameter 3574 cycles (-14% vs 4176 baseline), parity PASS (9120 bytes). --- src/transform44/bone_sse.zig | 389 +++++++++++++++++++---------------- 1 file changed, 210 insertions(+), 179 deletions(-) diff --git a/src/transform44/bone_sse.zig b/src/transform44/bone_sse.zig index dc438db..5590ce8 100644 --- a/src/transform44/bone_sse.zig +++ b/src/transform44/bone_sse.zig @@ -190,12 +190,6 @@ fn getBillboardEpsilon() f32 { fn getShortToFloat() f32 { return rf32(0x00811610); } -const HERMITE_3: f32 = 3.0; // DAT_0080297c -// getHermite5(): runtime value is 0x40c00000 (6.0), NOT static 0x40a00000 (5.0) from Ghidra -fn getHermite5() f32 { - return rf32(0x00802990); -} - // MSVC CRT sin/cos — linked from the WoW process extern fn sinf(f32) f32; extern fn cosf(f32) f32; @@ -264,15 +258,6 @@ inline fn lerpVec3(a_addr: u32, b_addr: u32, t: f32) [3]f32 { }; } -/// Blend primary and secondary results: primary + (secondary - primary) * weight -inline fn blendVec3(primary: [3]f32, secondary: [3]f32, weight: f32) [3]f32 { - return .{ - (secondary[0] - primary[0]) * weight + primary[0], - (secondary[1] - primary[1]) * weight + primary[1], - (secondary[2] - primary[2]) * weight + primary[2], - }; -} - /// Scale 3x3 rotation portion of a row-major 4x4 matrix by per-axis scale. /// Row 0 *= scale.x, Row 1 *= scale.y, Row 2 *= scale.z inline fn scaleMatrix3x3(mat: u32, sx: f32, sy: f32, sz: f32) void { @@ -301,11 +286,8 @@ inline fn applyTranslation(mat: u32, tx: f32, ty: f32, tz: f32) void { wf32(mat + 0x38, @mulAdd(f32, tz, rf32(mat + 0x28), @mulAdd(f32, ty, rf32(mat + 0x18), @mulAdd(f32, tx, rf32(mat + 0x08), rf32(mat + 0x38))))); } -/// Quaternion → rotation matrix: OVERWRITES mat with the rotation matrix. -/// Matches the original game function at 0x74B6BB which writes directly -/// without multiplying by existing matrix contents. -/// Used in the bone loop where the matrix starts as identity. -inline fn buildRotationMatrix(mat: u32, qx: f32, qy: f32, qz: f32, qw: f32) void { +/// Quaternion → rotation matrix as value. No memory writes. +inline fn buildRotationMatrixVal(qx: f32, qy: f32, qz: f32, qw: f32) [16]f32 { const xx2 = qx * (qx + qx); const xy2 = qx * (qy + qy); const xz2 = qx * (qz + qz); @@ -316,13 +298,21 @@ inline fn buildRotationMatrix(mat: u32, qx: f32, qy: f32, qz: f32, qw: f32) void const wy2 = qw * (qy + qy); const wz2 = qw * (qz + qz); - const r0 = V4{ 1.0 - (yy2 + zz2), xy2 + wz2, xz2 - wy2, 0 }; - const r1 = V4{ xy2 - wz2, 1.0 - (xx2 + zz2), yz2 + wx2, 0 }; - const r2 = V4{ xz2 + wy2, yz2 - wx2, 1.0 - (xx2 + yy2), 0 }; - wf32(mat, r0[0]); wf32(mat + 4, r0[1]); wf32(mat + 8, r0[2]); wf32(mat + 12, r0[3]); - wf32(mat + 16, r1[0]); wf32(mat + 20, r1[1]); wf32(mat + 24, r1[2]); wf32(mat + 28, r1[3]); - wf32(mat + 32, r2[0]); wf32(mat + 36, r2[1]); wf32(mat + 40, r2[2]); wf32(mat + 44, r2[3]); - wf32(mat + 48, 0); wf32(mat + 52, 0); wf32(mat + 56, 0); wf32(mat + 60, 1); + return .{ + 1.0 - (yy2 + zz2), xy2 + wz2, xz2 - wy2, 0, + xy2 - wz2, 1.0 - (xx2 + zz2), yz2 + wx2, 0, + xz2 + wy2, yz2 - wx2, 1.0 - (xx2 + yy2), 0, + 0, 0, 0, 1, + }; +} + +/// Quaternion → rotation matrix: writes to game memory via u32 address. +/// Used by boneKeyframeLoop where the matrix is in game memory. +inline fn buildRotationMatrix(mat: u32, qx: f32, qy: f32, qz: f32, qw: f32) void { + const m = buildRotationMatrixVal(qx, qy, qz, qw); + inline for (0..16) |i| { + wf32(mat + @as(u32, @intCast(i * 4)), m[i]); + } } /// Quaternion → rotation matrix × mat. Fused: builds quat rows as V4, multiplies in-register. @@ -404,6 +394,59 @@ inline fn matMul4x4(dst: u32, a: u32, b: u32) void { } } +/// 4x4 matrix multiply: dst = a * b. Left operand is a local array, right is game memory. +inline fn matMul4x4Local(dst: u32, a: [16]f32, b: u32) void { + const b0 = V4{ rf32(b), rf32(b + 4), rf32(b + 8), rf32(b + 12) }; + const b1 = V4{ rf32(b + 16), rf32(b + 20), rf32(b + 24), rf32(b + 28) }; + const b2 = V4{ rf32(b + 32), rf32(b + 36), rf32(b + 40), rf32(b + 44) }; + const b3 = V4{ rf32(b + 48), rf32(b + 52), rf32(b + 56), rf32(b + 60) }; + const rows = [4]V4{ + V4{ a[0], a[1], a[2], a[3] }, + V4{ a[4], a[5], a[6], a[7] }, + V4{ a[8], a[9], a[10], a[11] }, + V4{ a[12], a[13], a[14], a[15] }, + }; + inline for (0..4) |i| { + const s0: V4 = @splat(rows[i][0]); + const s1: V4 = @splat(rows[i][1]); + const s2: V4 = @splat(rows[i][2]); + const s3: V4 = @splat(rows[i][3]); + const row = @mulAdd(V4, s3, b3, @mulAdd(V4, s2, b2, @mulAdd(V4, s1, b1, s0 * b0))); + const off: u32 = @intCast(i * 16); + wf32(dst + off, row[0]); + wf32(dst + off + 4, row[1]); + wf32(dst + off + 8, row[2]); + wf32(dst + off + 12, row[3]); + } +} + +/// In-place multiply: a = a * b (b from game memory). Returns new array. +inline fn matMul4x4InPlace(a: [16]f32, b: u32) [16]f32 { + const b0 = V4{ rf32(b), rf32(b + 4), rf32(b + 8), rf32(b + 12) }; + const b1 = V4{ rf32(b + 16), rf32(b + 20), rf32(b + 24), rf32(b + 28) }; + const b2 = V4{ rf32(b + 32), rf32(b + 36), rf32(b + 40), rf32(b + 44) }; + const b3 = V4{ rf32(b + 48), rf32(b + 52), rf32(b + 56), rf32(b + 60) }; + const rows = [4]V4{ + V4{ a[0], a[1], a[2], a[3] }, + V4{ a[4], a[5], a[6], a[7] }, + V4{ a[8], a[9], a[10], a[11] }, + V4{ a[12], a[13], a[14], a[15] }, + }; + var result: [16]f32 = undefined; + inline for (0..4) |i| { + const s0: V4 = @splat(rows[i][0]); + const s1: V4 = @splat(rows[i][1]); + const s2: V4 = @splat(rows[i][2]); + const s3: V4 = @splat(rows[i][3]); + const row = @mulAdd(V4, s3, b3, @mulAdd(V4, s2, b2, @mulAdd(V4, s1, b1, s0 * b0))); + result[i * 4] = row[0]; + result[i * 4 + 1] = row[1]; + result[i * 4 + 2] = row[2]; + result[i * 4 + 3] = row[3]; + } + return result; +} + /// Set identity matrix (16 floats) inline fn setIdentity(dst: u32) void { inline for (0..16) |i| { @@ -599,49 +642,46 @@ 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) void { +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); const mode = ri16(anim_data + AD.interp_mode); const kf_base = ru32(anim_data + AD.keyframe_base); + var result: [4]f32 = undefined; + if (mode == 0) { const src = kf_base + r.idx0 * 16; - wu32(output + 0x0C, ru32(src)); - wu32(output + 0x10, ru32(src + 4)); - wu32(output + 0x14, ru32(src + 8)); - wu32(output + 0x18, ru32(src + 12)); - return; + inline for (0..4) |i| { + result[i] = rf32(src + @as(u32, @intCast(i * 4))); + } + } else { + const src0 = kf_base + r.idx0 * 16; + const src1 = kf_base + r.idx1 * 16; + inline for (0..4) |i| { + const off: u32 = @intCast(i * 4); + result[i] = @mulAdd(f32, rf32(src1 + off) - rf32(src0 + off), r.t, rf32(src0 + off)); + } + + // Crossfade — blend in registers, no re-read from output buffer + if (rf32(bone_rt + BR.blend_weight) != 0.0 and ri16(anim_data + AD.time_index) == -1) { + const sr = findInterpIdx(this, ru32(bone_rt + BR.sec_time), ru32(bone_rt + BR.sec_track), anim_data, output + 0x1C); + const ssrc0 = kf_base + sr.idx0 * 16; + const ssrc1 = kf_base + sr.idx1 * 16; + const bw = rf32(bone_rt + BR.blend_weight); + inline for (0..4) |i| { + const off: u32 = @intCast(i * 4); + const sec = @mulAdd(f32, rf32(ssrc1 + off) - rf32(ssrc0 + off), sr.t, rf32(ssrc0 + off)); + wf32(output + 0x28 + off, sec); + result[i] = @mulAdd(f32, sec - result[i], bw, result[i]); + } + } } - const src0 = kf_base + r.idx0 * 16; - const src1 = kf_base + r.idx1 * 16; + // Write final result to memory for persistence (read on frames where gate is false) inline for (0..4) |i| { - const off: u32 = @intCast(i * 4); - const a = rf32(src0 + off); - const b = rf32(src1 + off); - wf32(output + 0x0C + off, @mulAdd(f32, b - a, r.t, a)); - } - - // Crossfade - if (rf32(bone_rt + BR.blend_weight) != 0.0 and ri16(anim_data + AD.time_index) == -1) { - const sr = findInterpIdx(this, ru32(bone_rt + BR.sec_time), ru32(bone_rt + BR.sec_track), anim_data, output + 0x1C); - const ssrc0 = kf_base + sr.idx0 * 16; - const ssrc1 = kf_base + sr.idx1 * 16; - inline for (0..4) |i| { - const off: u32 = @intCast(i * 4); - const a = rf32(ssrc0 + off); - const b = rf32(ssrc1 + off); - wf32(output + 0x28 + off, @mulAdd(f32, b - a, sr.t, a)); - } - // Blend: primary = primary + (secondary - primary) * weight - const bw = rf32(bone_rt + BR.blend_weight); - inline for (0..4) |i| { - const off: u32 = @intCast(i * 4); - const pri = rf32(output + 0x0C + off); - const sec = rf32(output + 0x28 + off); - wf32(output + 0x0C + off, @mulAdd(f32, sec - pri, bw, pri)); - } + wf32(output + 0x0C + @as(u32, @intCast(i * 4)), result[i]); } + return result; } // ============================================================================= @@ -680,46 +720,38 @@ inline fn interpVec3Track( anim_data: u32, output: u32, blend_weight: f32, -) void { +) [3]f32 { const r = 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); + var result: [3]f32 = undefined; + if (interp_mode == 0) { - // No interpolation — copy keyframe directly const src = kf_base + r.idx0 * 0xC; - wu32(output + 0x0C, ru32(src)); - wu32(output + 0x10, ru32(src + 4)); - wu32(output + 0x14, ru32(src + 8)); - return; + result = .{ rf32(src), rf32(src + 4), rf32(src + 8) }; + } else { + result = lerpVec3(kf_base + r.idx0 * 0xC, kf_base + r.idx1 * 0xC, r.t); + + // Crossfade — blend in registers + if (blend_weight != 0.0 and ri16(anim_data + AD.time_index) == -1) { + const sr = findInterpIdx(this, ru32(bone_rt + BR.sec_time), ru32(bone_rt + BR.sec_track), anim_data, output + 0x18); + const sec = lerpVec3(kf_base + sr.idx0 * 0xC, kf_base + sr.idx1 * 0xC, sr.t); + wu32(output + 0x24, fbits(sec[0])); + wu32(output + 0x28, fbits(sec[1])); + wu32(output + 0x2C, fbits(sec[2])); + inline for (0..3) |i| { + result[i] = @mulAdd(f32, sec[i] - result[i], blend_weight, result[i]); + } + } } - const a = kf_base + r.idx0 * 0xC; - const b = kf_base + r.idx1 * 0xC; - const result = lerpVec3(a, b, r.t); + // Write final result to memory for persistence wu32(output + 0x0C, fbits(result[0])); wu32(output + 0x10, fbits(result[1])); wu32(output + 0x14, fbits(result[2])); - - // Crossfade blend - if (blend_weight != 0.0 and ri16(anim_data + AD.time_index) == -1) { - const sr = findInterpIdx(this, ru32(bone_rt + BR.sec_time), ru32(bone_rt + BR.sec_track), anim_data, output + 0x18); - const sa = kf_base + sr.idx0 * 0xC; - const sb = kf_base + sr.idx1 * 0xC; - const sec = lerpVec3(sa, sb, sr.t); - wu32(output + 0x24, fbits(sec[0])); - wu32(output + 0x28, fbits(sec[1])); - wu32(output + 0x2C, fbits(sec[2])); - - // Blend - const pri_x = ufloat(ru32(output + 0x0C)); - const pri_y = ufloat(ru32(output + 0x10)); - const pri_z = ufloat(ru32(output + 0x14)); - wu32(output + 0x0C, fbits(@mulAdd(f32, sec[0] - pri_x, blend_weight, pri_x))); - wu32(output + 0x10, fbits(@mulAdd(f32, sec[1] - pri_y, blend_weight, pri_y))); - wu32(output + 0x14, fbits(@mulAdd(f32, sec[2] - pri_z, blend_weight, pri_z))); - } + return result; } /// Interpolate a single float track (4 bytes per keyframe) with crossfade. @@ -730,31 +762,34 @@ inline fn interpFloatTrack( anim_data: u32, output: u32, blend_weight: f32, -) void { +) f32 { const r = 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); + var result: f32 = undefined; + if (interp_mode == 0) { - wu32(output + 0x0C, ru32(kf_base + r.idx0 * 4)); - return; + result = rf32(kf_base + r.idx0 * 4); + } else { + const a = rf32(kf_base + r.idx0 * 4); + const b = rf32(kf_base + r.idx1 * 4); + result = @mulAdd(f32, b - a, r.t, a); + + // Crossfade — blend in registers + if (blend_weight != 0.0 and ri16(anim_data + AD.time_index) == -1) { + const sr = findInterpIdx(this, ru32(bone_rt + BR.sec_time), ru32(bone_rt + BR.sec_track), anim_data, output + 0x10); + const sa = rf32(kf_base + sr.idx0 * 4); + const sb = rf32(kf_base + sr.idx1 * 4); + const sec = (sb - sa) * sr.t + sa; + wu32(output + 0x1C, fbits(sec)); + result = @mulAdd(f32, sec - result, blend_weight, result); + } } - const a = rf32(kf_base + r.idx0 * 4); - const b = rf32(kf_base + r.idx1 * 4); - wf32(output + 0x0C, @mulAdd(f32, b - a, r.t, a)); - - // Crossfade — only for bone loop callers (particles pass 0.0) - if (blend_weight != 0.0 and ri16(anim_data + AD.time_index) == -1) { - const sr = findInterpIdx(this, ru32(bone_rt + BR.sec_time), ru32(bone_rt + BR.sec_track), anim_data, output + 0x10); - const sa = rf32(kf_base + sr.idx0 * 4); - const sb = rf32(kf_base + sr.idx1 * 4); - const sec = (sb - sa) * sr.t + sa; - wu32(output + 0x1C, fbits(sec)); - const pri = ufloat(ru32(output + 0x0C)); - wf32(output + 0x0C, @mulAdd(f32, sec - pri, blend_weight, pri)); - } + wf32(output + 0x0C, result); + return result; } // ============================================================================= @@ -1483,41 +1518,42 @@ export fn transformImpl_SSE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u3 const dst = bone_out_base + bone_idx * 0x40; copyMat4(dst, src_mat); } else { - const lm2_addr = @intFromPtr(&local_mat2); const rot_anim = bdef + BD.rot_anim; const rot_kf_count = ru32(bdef + BD.rot_nts); // Rotation overwrites all 16 floats — skip identity init when present if (rot_kf_count != 0) { if (frame_ctr < rot_kf_count) { - interpAnimKF(this, brt, rot_anim, brt + BR.rot_idx0); + const q = interpAnimKF(this, brt, rot_anim, brt + BR.rot_idx0); + 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)); } - buildRotationMatrix(lm2_addr, rf32(brt + BR.rot_x), rf32(brt + BR.rot_y), rf32(brt + BR.rot_z), rf32(brt + BR.rot_w)); } else { local_mat2 = .{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; } - // Step 2: Scale interpolation — applied after rotation + // Step 2: Scale interpolation — applied after rotation (inline array ops) const scale_anim = bdef + BD.scale_anim; const scale_kf_count = ru32(bdef + BD.scale_nts); if (scale_kf_count != 0) { + var sx: f32 = undefined; + var sy: f32 = undefined; + var sz: f32 = undefined; if (frame_ctr < scale_kf_count) { - interpVec3Track(this, brt, scale_anim, brt + BR.scale_idx0, ufloat(ru32(brt + BR.blend_weight))); + const s = interpVec3Track(this, brt, scale_anim, brt + BR.scale_idx0, ufloat(ru32(brt + BR.blend_weight))); + 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); } - scaleMatrix3x3(lm2_addr, rf32(brt + BR.scale_x), rf32(brt + BR.scale_y), rf32(brt + BR.scale_z)); + local_mat2[0] *= sx; local_mat2[1] *= sx; local_mat2[2] *= sx; + local_mat2[4] *= sy; local_mat2[5] *= sy; local_mat2[6] *= sy; + local_mat2[8] *= sz; local_mat2[9] *= sz; local_mat2[10] *= sz; } - // Conditional multiply: if flag bit 0x80 set AND bone_rt[0xF0] != 0, - // multiply bone_local by the matrix pointed to by bone_rt[0xF0]. - // Assembly at 0x714F7F-0x714F9C: - // TEST CL, CL / JNS skip - // MOV EAX, [ESI+0xF0] / TEST EAX, EAX / JZ skip - // PUSH EAX (right), PUSH &bone_local (left), PUSH &bone_local (output) - // CALL 0x74A7C0 (multiplyMatrix4x4: output = left * right) - // This is bone_local *= *(bone_rt+0xF0) + // Conditional multiply: bone_local *= *(bone_rt+0xF0) if ((@as(i8, @bitCast(@as(u8, @truncate(combined_flags)))) < 0) and ru32(brt + BR.bone_flag_cache) != 0) { - const extra_mat = ru32(brt + BR.bone_flag_cache); - matMul4x4(lm2_addr, lm2_addr, extra_mat); + local_mat2 = matMul4x4InPlace(local_mat2, ru32(brt + BR.bone_flag_cache)); } // Step 3: Translation interpolation @@ -1529,15 +1565,18 @@ 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) { - interpVec3Track(this, brt, trans_anim, brt + BR.trans_idx0, ufloat(ru32(brt + BR.blend_weight))); + const t = interpVec3Track(this, brt, trans_anim, brt + BR.trans_idx0, ufloat(ru32(brt + BR.blend_weight))); + tx_val += t[0]; + ty_val += t[1]; + tz_val += t[2]; + } else { + tx_val += rf32(brt + BR.trans_x); + ty_val += rf32(brt + BR.trans_y); + tz_val += rf32(brt + BR.trans_z); } - tx_val += ufloat(ru32(brt + BR.trans_x)); - ty_val += ufloat(ru32(brt + BR.trans_y)); - tz_val += ufloat(ru32(brt + BR.trans_z)); } // Step 4: Compute translation offset using the ROTATED+SCALED matrix. - // translation = (pivot + interp_trans) - bone_local_matrix * pivot const piv_x = rf32(bdef + BD.pivot_x); const piv_y = rf32(bdef + BD.pivot_y); const piv_z = rf32(bdef + BD.pivot_z); @@ -1546,8 +1585,7 @@ export fn transformImpl_SSE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u3 local_mat2[14] = tz_val - (local_mat2[2] * piv_x + local_mat2[6] * piv_y + local_mat2[10] * piv_z); // Write final composed matrix to output: dst = bone_local * parent - // Assembly: CALL 0x74A7C0 (JMP table → SSE matmul) at 0x7151BA - matMul4x4(bone_out_base + bone_idx * 0x40, lm2_addr, src_mat); + matMul4x4Local(bone_out_base + bone_idx * 0x40, local_mat2, src_mat); } // --- Billboard post-processing (flags & 0x78) --- @@ -1747,6 +1785,7 @@ fn texAnimLoop(this: u32, model_hdr: u32, frame_ctr: u32) void { const data_base = ru32(model_hdr + 0x58); const bone_rt_base = ru32(this + SO.bone_rt_base); const out_base = ru32(this + SO.tex_anim_out); + const stf = getShortToFloat(); var i: u32 = 0; var data_off: u32 = 0; @@ -1759,32 +1798,26 @@ fn texAnimLoop(this: u32, model_hdr: u32, frame_ctr: u32) void { const anim_data = data_base + data_off; const output = out_base + out_off; if (frame_ctr < ru32(data_base + data_off + 0x0C)) { - interpVec3Track(this, bone_rt_base, anim_data, output, ufloat(ru32(bone_rt_base + BR.blend_weight))); + _ = interpVec3Track(this, bone_rt_base, anim_data, output, ufloat(ru32(bone_rt_base + BR.blend_weight))); } // Alpha/opacity track (assembly 0x715AF1-0x715C5E) - // Gate: anim_data+0x28 (alpha kf count) > anim_frame_ctr if (frame_ctr < ru32(anim_data + 0x28)) { const alpha_anim = anim_data + 0x1C; - // ESI = output + 0x30 in original (alpha output area) const alpha_out = output + 0x30; const ar = findInterpIdx(this, ru32(bone_rt_base + BR.prim_time), ru32(bone_rt_base + BR.prim_track), alpha_anim, alpha_out); const mode = ri16(alpha_anim); if (mode == 0) { - // Mode 0: direct short→float copy. Assembly JMPs past crossfade. const kf_data = ru32(alpha_anim + 0x18); const sv = @as(f32, @floatFromInt(@as(i32, @as(*align(1) const i16, @ptrFromInt(kf_data + ar.idx0 * 2)).*))); - wf32(alpha_out + 0x0C, sv * getShortToFloat()); + wf32(alpha_out + 0x0C, sv * stf); } else { - // Mode != 0: lerp + crossfade - const primary = shortInterpToFloat(alpha_anim, ar); + const primary = shortInterpToFloat(alpha_anim, ar, stf); wf32(alpha_out + 0x0C, primary); - // Crossfade (assembly 0x715BAF-0x715C5E) - // Only runs for mode != 0 — mode 0 JMPs past this const bw = rf32(bone_rt_base + BR.blend_weight); if (bw != 0.0 and ri16(alpha_anim + 0x02) == -1) { const asr = findInterpIdx(this, ru32(bone_rt_base + BR.sec_time), ru32(bone_rt_base + BR.sec_track), alpha_anim, alpha_out + 0x10); - const secondary = shortInterpToFloat(alpha_anim, asr); + const secondary = shortInterpToFloat(alpha_anim, asr, stf); wf32(alpha_out + 0x1C, secondary); wf32(alpha_out + 0x0C, @mulAdd(f32, secondary - primary, bw, primary)); } @@ -1795,15 +1828,15 @@ fn texAnimLoop(this: u32, model_hdr: u32, frame_ctr: u32) void { /// Short-value interpolation: uses InterpResult indices, looks up short values, interpolates. /// Shared by texAnimLoop alpha, colorAnimLoop, and word animation crossfade. -inline fn shortInterpToFloat(anim_data: u32, r: InterpResult) f32 { +inline fn shortInterpToFloat(anim_data: u32, r: InterpResult, stf: f32) f32 { const mode = ri16(anim_data); const table = anim_data + AD.nvalues; if (mode == 0) { - return @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, r.idx0)))) * getShortToFloat(); + return @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, r.idx0)))) * stf; } else { const v1 = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, r.idx1)))); const v0 = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, r.idx0)))); - return (v1 * getShortToFloat() - v0 * getShortToFloat()) * r.t + v0 * getShortToFloat(); + return (v1 * stf - v0 * stf) * r.t + v0 * stf; } } @@ -1815,6 +1848,7 @@ fn colorAnimLoop(this: u32, model_hdr: u32, frame_ctr: u32) void { const data_base = ru32(model_hdr + 0x68); const bone_rt_base = ru32(this + SO.bone_rt_base); const out_base = ru32(this + SO.color_anim_out); + const stf = getShortToFloat(); var i: u32 = 0; var data_off: u32 = 0; @@ -1826,25 +1860,21 @@ fn colorAnimLoop(this: u32, model_hdr: u32, frame_ctr: u32) void { }) { const anim_data = data_base + data_off; const output = out_base + out_off; - // Gate: anim_data+0x0C (kf count) > anim_frame_ctr if (frame_ctr < ru32(anim_data + 0x0C)) { const cr = findInterpIdx(this, ru32(bone_rt_base + BR.prim_time), ru32(bone_rt_base + BR.prim_track), anim_data, output); const mode = ri16(anim_data); if (mode == 0) { - // Mode 0: direct short→float. Assembly JMPs past crossfade (0x715D05). const kf_data = ru32(anim_data + 0x18); const sv = @as(f32, @floatFromInt(@as(i32, @as(*align(1) const i16, @ptrFromInt(kf_data + cr.idx0 * 2)).*))); - wf32(output + 0x0C, sv * getShortToFloat()); + wf32(output + 0x0C, sv * stf); } else { - // Mode != 0: lerp + crossfade - const primary = shortInterpToFloat(anim_data, cr); + const primary = shortInterpToFloat(anim_data, cr, stf); wf32(output + 0x0C, primary); - // Crossfade (assembly 0x715D6B-0x715E1B) const bw = rf32(bone_rt_base + BR.blend_weight); if (bw != 0.0 and ri16(anim_data + 0x02) == -1) { const csr = findInterpIdx(this, ru32(bone_rt_base + BR.sec_time), ru32(bone_rt_base + BR.sec_track), anim_data, output + 0x10); - const secondary = shortInterpToFloat(anim_data, csr); + const secondary = shortInterpToFloat(anim_data, csr, stf); wf32(output + 0x1C, secondary); wf32(output + 0x0C, @mulAdd(f32, secondary - primary, bw, primary)); } @@ -1937,22 +1967,22 @@ fn boneKeyframeLoop(this: u32, model_hdr: u32) void { // Rotation: AnimData at kf_entry+0x1C, gate at kf_entry+0x28 // Assembly at 0x715FDB: CMP [ECX+0x28], 0; AnimData at EDX+0x1C if (ru32(kf_data + 0x28) != 0) { - interpAnimKF(this, bone_rt_base, kf_data + 0x1C, output + 0x30); + const q = interpAnimKF(this, bone_rt_base, kf_data + 0x1C, output + 0x30); applyTranslation(mat_out, rf32(0xCF043C), rf32(0xCF0440), rf32(0xCF0444)); - rotateByQuaternion(mat_out, rf32(output + 0x3C), rf32(output + 0x40), rf32(output + 0x44), rf32(output + 0x48)); + rotateByQuaternion(mat_out, q[0], q[1], q[2], q[3]); applyTranslation(mat_out, -rf32(0xCF043C), -rf32(0xCF0440), -rf32(0xCF0444)); } if (ru32(kf_data + 0x44) != 0) { - interpVec3Track(this, bone_rt_base, kf_data + 0x38, output + 0x68, ufloat(ru32(bone_rt_base + BR.blend_weight))); + const s = interpVec3Track(this, bone_rt_base, kf_data + 0x38, output + 0x68, ufloat(ru32(bone_rt_base + BR.blend_weight))); applyTranslation(mat_out, rf32(0xCF043C), rf32(0xCF0440), rf32(0xCF0444)); - scaleMatrix3x3(mat_out, rf32(output + 0x74), rf32(output + 0x78), rf32(output + 0x7C)); + scaleMatrix3x3(mat_out, s[0], s[1], s[2]); applyTranslation(mat_out, -rf32(0xCF043C), -rf32(0xCF0440), -rf32(0xCF0444)); } if (ru32(kf_data + 0x0C) != 0) { - interpVec3Track(this, bone_rt_base, kf_data, output, ufloat(ru32(bone_rt_base + BR.blend_weight))); - applyTranslation(mat_out, rf32(output + 0x0C), rf32(output + 0x10), rf32(output + 0x14)); + const tv = interpVec3Track(this, bone_rt_base, kf_data, output, ufloat(ru32(bone_rt_base + BR.blend_weight))); + applyTranslation(mat_out, tv[0], tv[1], tv[2]); } } } @@ -2013,32 +2043,32 @@ fn ribbonEmitterLoop(this: u32, model_hdr: u32, frame_ctr: u32) void { // ---- Track A (float): gate=entry+0x38, AD=entry+0x2C, output+0x30 ---- if (frame_ctr < ru32(entry + 0x38)) { - interpFloatTrack(this, bone_rt, entry + 0x2C, output + 0x30, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0x2C, output + 0x30, ufloat(ru32(bone_rt + BR.blend_weight))); } // ---- Track B (Vec3): gate=entry+0x1C, AD=entry+0x10, output+0x00 ---- if (frame_ctr < ru32(entry + 0x1C)) { - interpVec3Track(this, bone_rt, entry + 0x10, output, ufloat(ru32(bone_rt + BR.blend_weight))); + const v = interpVec3Track(this, bone_rt, entry + 0x10, output, ufloat(ru32(bone_rt + BR.blend_weight))); // Post-processing 1 (asm 0x71678A-0x7167CE) const scale1 = rf32(output + 0x3C) * rf32(this + SO.render_scale_z); - wf32(output + 0x134, rf32(output + 0x0C) * scale1); - wf32(output + 0x138, rf32(output + 0x10) * scale1); - wf32(output + 0x13C, rf32(output + 0x14) * scale1); + wf32(output + 0x134, v[0] * scale1); + wf32(output + 0x138, v[1] * scale1); + wf32(output + 0x13C, v[2] * scale1); } // ---- Track C (float): gate=entry+0x70, AD=entry+0x64, output+0x80 ---- if (frame_ctr < ru32(entry + 0x70)) { - interpFloatTrack(this, bone_rt, entry + 0x64, output + 0x80, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0x64, output + 0x80, ufloat(ru32(bone_rt + BR.blend_weight))); } // ---- Track D (Vec3): gate=entry+0x54, AD=entry+0x48, output+0x50 ---- if (frame_ctr < ru32(entry + 0x54)) { - interpVec3Track(this, bone_rt, entry + 0x48, output + 0x50, ufloat(ru32(bone_rt + BR.blend_weight))); + const v2 = interpVec3Track(this, bone_rt, entry + 0x48, output + 0x50, ufloat(ru32(bone_rt + BR.blend_weight))); // Post-processing 2 (asm 0x716A67-0x716AA6) const scale2 = rf32(output + 0x8C) * rf32(this + SO.render_scale_z); - wf32(output + 0x140, rf32(output + 0x5C) * scale2); - wf32(output + 0x144, rf32(output + 0x60) * scale2); - wf32(output + 0x148, rf32(output + 0x64) * scale2); + wf32(output + 0x140, v2[0] * scale2); + wf32(output + 0x144, v2[1] * scale2); + wf32(output + 0x148, v2[2] * scale2); } } } @@ -2078,6 +2108,7 @@ fn particleEmitterLoop(this: u32, model_hdr: u32, frame_ctr: u32) void { fn additionalParticleLoops(this: u32, model_hdr: u32, frame_ctr: u32) void { @setEvalBranchQuota(50000); + const stf = getShortToFloat(); // Assembly: model_hdr+0x134 section (asm 0x71763E-0x717D6A) // Then additional_remaining reset at 0x717D6F // Then model_hdr+0x13C section (asm 0x717D75-0x7185E3) @@ -2128,7 +2159,7 @@ fn additionalParticleLoops(this: u32, model_hdr: u32, frame_ctr: u32) void { if (frame_ctr < ru32(entry + 0x30)) { const bone_idx = @as(u32, ru16(entry + 0x04)); const bone_rt = bone_rt_base + bone_idx * 0x118; - interpVec3Track(this, bone_rt, entry + 0x24, output, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpVec3Track(this, bone_rt, entry + 0x24, output, ufloat(ru32(bone_rt + BR.blend_weight))); } // Alpha track: entry+0x40 vs entry+0x4C @@ -2141,11 +2172,11 @@ fn additionalParticleLoops(this: u32, model_hdr: u32, frame_ctr: u32) void { const table = entry + 0x40 + AD.nvalues; if (alpha_mode == 0) { const sv = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, par.idx0)))); - wf32(output + 0x3C, sv * getShortToFloat()); + wf32(output + 0x3C, sv * stf); } else { const v1 = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, par.idx1)))); const v0 = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, par.idx0)))); - wf32(output + 0x3C, (v1 * getShortToFloat() - v0 * getShortToFloat()) * par.t + v0 * getShortToFloat()); + wf32(output + 0x3C, (v1 * stf - v0 * stf) * par.t + v0 * stf); } } @@ -2153,14 +2184,14 @@ fn additionalParticleLoops(this: u32, model_hdr: u32, frame_ctr: u32) void { if (frame_ctr < ru32(entry + 0x68)) { const bone_idx = @as(u32, ru16(entry + 0x04)); const bone_rt = bone_rt_base + bone_idx * 0x118; - interpFloatTrack(this, bone_rt, entry + 0x5C, output + 0x50, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0x5C, output + 0x50, ufloat(ru32(bone_rt + BR.blend_weight))); } // Emission rate: entry+0x78 vs entry+0x84 if (frame_ctr < ru32(entry + 0x84)) { const bone_idx = @as(u32, ru16(entry + 0x04)); const bone_rt = bone_rt_base + bone_idx * 0x118; - interpFloatTrack(this, bone_rt, entry + 0x78, output + 0x70, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0x78, output + 0x70, ufloat(ru32(bone_rt + BR.blend_weight))); } // Scale track: entry+0xA4 vs entry+0xB0 @@ -2247,37 +2278,37 @@ fn additionalParticleLoops(this: u32, model_hdr: u32, frame_ctr: u32) void { if (vis_byte != 0 or frame_ctr == 0) { // Track 1: emission rate — gate=+0x40, AnimData=+0x34, output=+0x00 if (frame_ctr < ru32(entry + 0x40)) { - interpFloatTrack(this, bone_rt, entry + 0x34, output, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0x34, output, ufloat(ru32(bone_rt + BR.blend_weight))); } // Track 2: speed — gate=+0x5C, AnimData=+0x50, output=+0x20 if (frame_ctr < ru32(entry + 0x5C)) { - interpFloatTrack(this, bone_rt, entry + 0x50, output + 0x20, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0x50, output + 0x20, ufloat(ru32(bone_rt + BR.blend_weight))); } // Track 3: color — gate=+0x78, AnimData=+0x6C, output=+0x40 if (frame_ctr < ru32(entry + 0x78)) { - interpFloatTrack(this, bone_rt, entry + 0x6C, output + 0x40, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0x6C, output + 0x40, ufloat(ru32(bone_rt + BR.blend_weight))); } // Track 4 — gate=+0x94, AnimData=+0x88, output=+0x60 if (frame_ctr < ru32(entry + 0x94)) { - interpFloatTrack(this, bone_rt, entry + 0x88, output + 0x60, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0x88, output + 0x60, ufloat(ru32(bone_rt + BR.blend_weight))); } // Track 5 (Vec3 spline) — gate=+0xB0, AnimData=+0xA4, output=+0x80 if (frame_ctr < ru32(entry + 0xB0)) { - interpFloatTrack(this, bone_rt, entry + 0xA4, output + 0x80, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0xA4, output + 0x80, ufloat(ru32(bone_rt + BR.blend_weight))); } // Track 6 — gate=+0xCC, AnimData=+0xC0, output=+0xA0 if (frame_ctr < ru32(entry + 0xCC)) { - interpFloatTrack(this, bone_rt, entry + 0xC0, output + 0xA0, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0xC0, output + 0xA0, ufloat(ru32(bone_rt + BR.blend_weight))); } // Tracks 7-10: same as interpFloatTrack (0x71AF20 is identical logic) if (frame_ctr < ru32(entry + 0xE8)) - interpFloatTrack(this, bone_rt, entry + 0xDC, output + 0xC0, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0xDC, output + 0xC0, ufloat(ru32(bone_rt + BR.blend_weight))); if (frame_ctr < ru32(entry + 0x104)) - interpFloatTrack(this, bone_rt, entry + 0xF8, output + 0xE0, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0xF8, output + 0xE0, ufloat(ru32(bone_rt + BR.blend_weight))); if (frame_ctr < ru32(entry + 0x120)) - interpFloatTrack(this, bone_rt, entry + 0x114, output + 0x100, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0x114, output + 0x100, ufloat(ru32(bone_rt + BR.blend_weight))); if (frame_ctr < ru32(entry + 0x13C)) - interpFloatTrack(this, bone_rt, entry + 0x130, output + 0x120, ufloat(ru32(bone_rt + BR.blend_weight))); + _ = interpFloatTrack(this, bone_rt, entry + 0x130, output + 0x120, ufloat(ru32(bone_rt + BR.blend_weight))); } } }