bone_sse_ref: add crossfade blending, word animation section, fix cross product z

- texAnimLoop alpha: add crossfade blend for mode != 0 (mode 0 skips
  crossfade per original assembly JMP at 0x715B49). Fix alpha output
  base to output+0x30 matching original ESI.
- colorAnimLoop: add crossfade blend, same mode 0 skip pattern.
  Mode 0 uses direct short->float copy matching original.
- New wordAnimLoop: implements model_hdr+0x6C/0x70 word animation
  section (assembly 0x715E46-0x715F25). Word copy with crossfade,
  no float blending. Data stride 0x1C, output stride 0x20.
- Fix billboard cross product z-component for types 0x10/0x20:
  was +cross.z, should be -cross.z (r0y*r1x - r0x*r1y).
- Extract shortInterpToFloat helper shared by alpha/color crossfade.

Known: particle emitter crash (pre-existing, idx=0x7FFFFFFF in
secondary findInterpIdx) — exposed by corrected colorAnimLoop count.
This commit is contained in:
MarcelineVQ
2026-03-15 16:21:33 -07:00
parent 36ce1a05ce
commit 582b0dc132
+106 -24
View File
@@ -1529,7 +1529,7 @@ export fn transformMatrix4x4_REF(this: u32, mat1: u32, mat2: u32, mat3: u32, mat
// row2 = -cross(row0, row1) — assembly uses negated cross product
wf32(om + 0x20, rf32(om + 0x08) * rf32(om + 0x14) - rf32(om + 0x04) * rf32(om + 0x18));
wf32(om + 0x24, rf32(om) * rf32(om + 0x18) - rf32(om + 0x08) * rf32(om + 0x10));
wf32(om + 0x28, rf32(om) * rf32(om + 0x14) - rf32(om + 0x04) * rf32(om + 0x10));
wf32(om + 0x28, rf32(om + 0x04) * rf32(om + 0x10) - rf32(om) * rf32(om + 0x14));
},
0x20 => {
// Type 32: normalize row1, set row0={-row1.y, row1.x, 0}, normalize,
@@ -1544,7 +1544,7 @@ export fn transformMatrix4x4_REF(this: u32, mat1: u32, mat2: u32, mat3: u32, mat
// row2 = -cross(row0, row1) — assembly uses negated cross product
wf32(om + 0x20, rf32(om + 0x08) * rf32(om + 0x14) - rf32(om + 0x04) * rf32(om + 0x18));
wf32(om + 0x24, rf32(om) * rf32(om + 0x18) - rf32(om + 0x08) * rf32(om + 0x10));
wf32(om + 0x28, rf32(om) * rf32(om + 0x14) - rf32(om + 0x04) * rf32(om + 0x10));
wf32(om + 0x28, rf32(om + 0x04) * rf32(om + 0x10) - rf32(om) * rf32(om + 0x14));
},
0x40 => {
// Type 64: normalize row2, set row1={row2.y, -row2.x, 0}, normalize,
@@ -1618,6 +1618,11 @@ export fn transformMatrix4x4_REF(this: u32, mat1: u32, mat2: u32, mat3: u32, mat
colorAnimLoop(this, model_hdr);
if (bisect_stop_section == 9) return;
// Section 9b: Word animation loop (assembly 0x715E46-0x715F25)
// model_hdr+0x6C = count, model_hdr+0x70 = data, output at this+0xAC (SO.scale1)
// Data stride 0x1C, output stride 0x20. Word copy with crossfade.
wordAnimLoop(this, model_hdr);
// Section 10: Bone keyframe processing
boneKeyframeLoop(this, model_hdr);
if (bisect_stop_section == 10) return;
@@ -1660,31 +1665,54 @@ fn texAnimLoop(this: u32, model_hdr: u32) void {
if (ru32(this + SO.anim_frame_ctr) < ru32(data_base + data_off + 0x0C)) {
interpVec3Track(this, bone_rt_base, anim_data, output, ufloat(ru32(bone_rt_base + BR.blend_weight)));
}
// Alpha/opacity track
// Alpha/opacity track (assembly 0x715AF1-0x715C5E)
// Gate: anim_data+0x28 (alpha kf count) > anim_frame_ctr
if (ru32(this + SO.anim_frame_ctr) < ru32(anim_data + 0x28)) {
// Short value interpolation via getIndexOffset/setShortValue pattern
// This accesses short values at anim_data + 0x1C
const alpha_anim = anim_data + 0x1C;
const alpha_out = output + 0xC * 4;
findInterpIdx(this, ru32(bone_rt_base + 0x98), ru32(bone_rt_base + 0x9C), alpha_anim, alpha_out);
// Short value interpolation via game's getIndexOffset/setShortValue
// Assembly: CALL 0x71AFF0 (getIndexOffset) + CALL 0x71B010 (setShortValue)
// ESI = output + 0x30 in original (alpha output area)
const alpha_out = output + 0x30;
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);
const table = alpha_anim + AD.nvalues; // ECX = anim_data + 0x14
if (mode == 0) {
const sv = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, ru32(alpha_out)))));
wf32(output + 0xF * 4, sv * getShortToFloat());
// Mode 0: direct short→float copy. Assembly JMPs past crossfade.
const kf_data = ru32(alpha_anim + 0x18);
const idx = ru32(alpha_out);
const sv = @as(f32, @floatFromInt(@as(i32, @as(*align(1) const i16, @ptrFromInt(kf_data + idx * 2)).*)));
wf32(alpha_out + 0x0C, sv * getShortToFloat());
} else {
const t = ufloat(ru32(alpha_out + 8));
// Assembly reads idx1 first, then idx0 (pairs A1/A2)
const v1 = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, ru32(alpha_out + 4)))));
const v0 = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, ru32(alpha_out)))));
wf32(output + 0xF * 4, (v1 * getShortToFloat() - v0 * getShortToFloat()) * t + v0 * getShortToFloat());
// Mode != 0: lerp + crossfade
const primary = shortInterpToFloat(alpha_anim, alpha_out);
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) {
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, alpha_out + 0x10);
wf32(alpha_out + 0x1C, secondary);
wf32(alpha_out + 0x0C, primary + (secondary - primary) * bw);
}
}
}
}
}
/// Short-value interpolation: reads indices from output, looks up short values, interpolates.
/// Shared by texAnimLoop alpha, colorAnimLoop, and word animation crossfade.
fn shortInterpToFloat(anim_data: u32, output: u32) f32 {
const mode = ri16(anim_data);
const table = anim_data + AD.nvalues;
if (mode == 0) {
return @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, ru32(output))))) * getShortToFloat();
} else {
const t = ufloat(ru32(output + 8));
const v1 = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, ru32(output + 4)))));
const v0 = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, ru32(output)))));
return (v1 * getShortToFloat() - v0 * getShortToFloat()) * t + v0 * getShortToFloat();
}
}
fn colorAnimLoop(this: u32, model_hdr: u32) void {
// Assembly: model_hdr+0x64 is both entry gate AND loop count
const count = ru32(model_hdr + 0x64);
@@ -1703,19 +1731,73 @@ fn colorAnimLoop(this: u32, model_hdr: 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 (ru32(this + SO.anim_frame_ctr) < ru32(anim_data + 0x0C)) {
findInterpIdx(this, ru32(bone_rt_base + BR.prim_time), ru32(bone_rt_base + BR.prim_track), anim_data, output);
// Short value interpolation via game's getIndexOffset/setShortValue
const mode = ri16(anim_data);
const table = anim_data + AD.nvalues; // ECX = anim_data + 0x14
if (mode == 0) {
const sv = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, ru32(output)))));
// Mode 0: direct short→float. Assembly JMPs past crossfade (0x715D05).
const kf_data = ru32(anim_data + 0x18);
const idx = ru32(output);
const sv = @as(f32, @floatFromInt(@as(i32, @as(*align(1) const i16, @ptrFromInt(kf_data + idx * 2)).*)));
wf32(output + 0x0C, sv * getShortToFloat());
} else {
const t = ufloat(ru32(output + 8));
const v1 = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, ru32(output + 4)))));
const v0 = @as(f32, @floatFromInt(@as(i32, readShortViaGame(table, ru32(output)))));
wf32(output + 0x0C, (v1 * getShortToFloat() - v0 * getShortToFloat()) * t + v0 * getShortToFloat());
// Mode != 0: lerp + crossfade
const primary = shortInterpToFloat(anim_data, output);
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) {
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, output + 0x10);
wf32(output + 0x1C, secondary);
wf32(output + 0x0C, primary + (secondary - primary) * bw);
}
}
}
}
}
fn wordAnimLoop(this: u32, model_hdr: u32) void {
// Assembly 0x715E46-0x715F25: word/byte animation section
// model_hdr+0x6C = count, model_hdr+0x70 = data base
// Output at this+0xAC (SO.scale1), data stride 0x1C, output stride 0x20
const count = ru32(model_hdr + 0x6C);
if (count == 0) return;
const data_base = ru32(model_hdr + 0x70);
const bone_rt_base = ru32(this + SO.bone_rt_base);
const out_base = ru32(this + SO.scale1);
var i: u32 = 0;
var data_off: u32 = 0;
var out_off: u32 = 0;
while (i < count) : ({
i += 1;
data_off += 0x1C;
out_off += 0x20;
}) {
const anim_data = data_base + data_off;
const output = out_base + out_off;
if (ru32(this + SO.anim_frame_ctr) < ru32(anim_data + 0x0C)) {
findInterpIdx(this, ru32(bone_rt_base + BR.prim_time), ru32(bone_rt_base + BR.prim_track), anim_data, output);
// Word copy: read word from keyframe data via direct indexing
// Assembly (0x715EA3): MOV AX,[kf_data+idx*2]; MOV [output+0x0C],AX
const kf_data = ru32(anim_data + 0x18);
const idx = ru32(output);
wu16(output + 0x0C, ru16(kf_data + idx * 2));
// Crossfade (assembly 0x715EB4-0x715EFA)
// Original: JZ skip if mode==0, then check blend_weight > 0, then time_index == -1
if (ri16(anim_data) == 0) {
// mode 0: no crossfade, skip
} else {
const bw = rf32(bone_rt_base + BR.blend_weight);
if (bw > 0.0 and ri16(anim_data + 0x02) == -1) {
findInterpIdx(this, ru32(bone_rt_base + BR.sec_time), ru32(bone_rt_base + BR.sec_track), anim_data, output + 0x10);
const sec_idx = ru32(output + 0x10);
wu16(output + 0x1C, ru16(kf_data + sec_idx * 2));
}
}
}
}