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).
This commit is contained in:
MarcelineVQ
2026-03-16 16:25:26 -07:00
parent d9d2e41eb4
commit dd43f6aca3
+210 -179
View File
@@ -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)));
}
}
}