bone_sse_ref: fix M2 black screen — 5 bugs found via asm comparison

Assembly-level comparison of compiled REF against original 0x714260 revealed:

1. Billboard cross product sign error (types 0x10/0x20): computed +cross
   instead of -cross for components 0/1, corrupting billboard bone matrices
2. colorAnimLoop wrong count field: read model_hdr+0x6C instead of +0x64
3. colorAnimLoop wrong gate offset: checked anim_data+0x04 instead of +0x0C
4. Timestamp delta guard inverted: REF guarded on cur_ts!=0 and always
   wrote to this+0x4C; original guards on this+0x4C!=0 first and never
   seeds the field (something else initializes it)
5. Section 5 emitter_ctx cached instead of re-read after matMul call

Also: build REF with x87-only target (subtract SSE/SSE2 features) to
match original's FLD/FMUL/FSTP codegen, and use callVec3SqMag for all
magnitude computations instead of inline SSE math.

Remaining known issues (not yet fixed):
- texAnimLoop alpha track missing crossfade blend
- colorAnimLoop missing crossfade blend
- Missing word animation section (model_hdr+0x6C/0x70)
- Bisect infrastructure and diagnostic code still present (test scaffolding)
This commit is contained in:
MarcelineVQ
2026-03-15 15:04:05 -07:00
parent 89e49cedf2
commit 36ce1a05ce
3 changed files with 144 additions and 46 deletions
+81 -13
View File
@@ -23,6 +23,7 @@ extern fn rotateMatrixByAxisAngle(u32, u32, u32, u32) void;
extern fn multiplyMatrix4x4(u32, u32, u32) u32;
extern fn transformMatrix4x4_SSE(u32, u32, u32, u32, u32) void;
extern fn transformMatrix4x4_REF(u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void;
extern var bisect_stop_section: u32;
pub const module_name: [*:0]const u8 = "transform44";
@@ -257,7 +258,15 @@ fn diagCompare(label: [*:0]const u8, snap: []const u8, live: u32, len: u32) void
}
}
// Test A: stripped detour — pure passthrough to REF, no profiling, no dispatch logic.
// If black → issue is REF code structure. If renders → detour overhead was the problem.
const DIRECT_REF_TEST = true;
fn transformDetour(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(hook.cc.thiscall) void {
if (DIRECT_REF_TEST) {
transformMatrix4x4_REF(this, mat1, mat2, mat3, mat4);
return;
}
const start = rdtsc();
const model_data = hook.readMem(u32, this + 0x10);
@@ -286,7 +295,7 @@ 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 (!is_early and diag_count < DIAG_MAX and t44_depth == 1) {
} else if (false and !is_early and diag_count < DIAG_MAX and t44_depth == 1) {
// --- DIAGNOSTIC (disabled): run original, snapshot, run REF, compare ---
transform_hook.callOriginal(.{ this, mat1, mat2, mat3, mat4 });
@@ -503,11 +512,12 @@ fn transformDetour(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callco
} else {
// FPU state comparison: capture full x87 state after original vs REF
if (diag_count >= DIAG_MAX and diag_count < DIAG_MAX + 3 and t44_depth == 1 and !is_early) {
// Run original, capture FPU state
// Run original, capture FPU + MXCSR state
transform_hook.callOriginal(.{ this, mat1, mat2, mat3, mat4 });
var fpu_orig: [108]u8 align(16) = undefined;
asm volatile ("fnsave (%[p])\n\tfrstor (%[p])"
:: [p] "r" (@intFromPtr(&fpu_orig))
var mxcsr_orig: u32 = 0;
asm volatile ("fnsave (%[p])\n\tfrstor (%[p])\n\tstmxcsr (%[m])"
:: [p] "r" (@intFromPtr(&fpu_orig)), [m] "r" (@intFromPtr(&mxcsr_orig))
: "memory"
);
@@ -515,8 +525,9 @@ fn transformDetour(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callco
@as(*align(1) u32, @ptrFromInt(this + 0x40)).* = 0;
transformMatrix4x4_REF(this, mat1, mat2, mat3, mat4);
var fpu_ref: [108]u8 align(16) = undefined;
asm volatile ("fnsave (%[p])\n\tfrstor (%[p])"
:: [p] "r" (@intFromPtr(&fpu_ref))
var mxcsr_ref: u32 = 0;
asm volatile ("fnsave (%[p])\n\tfrstor (%[p])\n\tstmxcsr (%[m])"
:: [p] "r" (@intFromPtr(&fpu_ref)), [m] "r" (@intFromPtr(&mxcsr_ref))
: "memory"
);
@@ -530,6 +541,12 @@ fn transformDetour(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callco
const tw_r = @as(*align(1) const u32, @ptrFromInt(@intFromPtr(&fpu_ref) + 8)).*;
log.fmt(" FPU orig: CW=0x{x:0>4} SW=0x{x:0>4} TW=0x{x:0>4}", .{ cw_o & 0xFFFF, sw_o & 0xFFFF, tw_o & 0xFFFF });
log.fmt(" FPU ref: CW=0x{x:0>4} SW=0x{x:0>4} TW=0x{x:0>4}", .{ cw_r & 0xFFFF, sw_r & 0xFFFF, tw_r & 0xFFFF });
// MXCSR comparison — SSE control/status, never checked before
if (mxcsr_orig != mxcsr_ref) {
log.fmt(" *** MXCSR DIFF: orig=0x{x:0>8} ref=0x{x:0>8} (xor=0x{x:0>8})", .{ mxcsr_orig, mxcsr_ref, mxcsr_orig ^ mxcsr_ref });
} else {
log.fmt(" MXCSR match: 0x{x:0>8}", .{mxcsr_orig});
}
// Dump ST0-ST7 (10 bytes each, starting at offset 28)
var sti: u32 = 0;
while (sti < 8) : (sti += 1) {
@@ -546,7 +563,13 @@ fn transformDetour(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callco
}
diag_count += 1;
} else {
// BISECT MODE: REF runs up to bisect_stop_section, then original provides rest
transformMatrix4x4_REF(this, mat1, mat2, mat3, mat4);
if (bisect_stop_section != 0) {
// REF returned early — clear sync so original doesn't early-exit, then run original
@as(*align(1) u32, @ptrFromInt(this + 0x40)).* = 0;
transform_hook.callOriginal(.{ this, mat1, mat2, mat3, mat4 });
}
}
}
@@ -1700,17 +1723,59 @@ pub fn installHooks() void {
if (!g_is_hook_owner) return;
log = logging.Logger.open(module_name, .both);
_ = transform_hook.attach(0x714260, &transformDetour);
original_trampoline = @intCast(transform_hook.inner.trampoline);
// BINARY PATCH TEST: copy REF's x87 code directly over original at 0x714260.
// No hook — game calls REF's code at the original address. Return addresses
// from game function calls will point into game .text, not DLL .text.
// If renders → game functions check return addresses (anti-cheat).
// If black → issue is something else entirely.
const BINARY_PATCH_TEST = false;
if (BINARY_PATCH_TEST) {
const ref_addr = @intFromPtr(&transformMatrix4x4_REF);
const ref_size: u32 = 0x2320; // main function ends at particleLoops start (includes epilogue + FPU stubs)
const game_addr: u32 = 0x714260;
// Copy REF's main function over the original (handles VirtualProtect)
const src: [*]const u8 = @ptrFromInt(ref_addr);
hook.writeProtected(game_addr, src[0..ref_size]);
// INT3-fill the remaining original bytes (crash on overrun)
var cc_buf: [512]u8 = .{0xCC} ** 512;
const remaining = @as(u32, 17703) - ref_size;
var filled: u32 = 0;
while (filled < remaining) {
const chunk = @min(512, remaining - filled);
hook.writeProtected(game_addr + ref_size + filled, cc_buf[0..chunk]);
filled += chunk;
}
// Fix up 2 relative CALL (E8) instructions to helper functions.
// In the DLL, they resolve from ref_addr+site to DLL helper addresses.
// After copy to game_addr, we recompute the rel32 to reach the same targets.
const e8_sites = [_]u32{ 0x22CE, 0x22E6 };
for (e8_sites) |site| {
// Read the resolved rel32 from DLL copy to get absolute target
const dll_call_addr = ref_addr + site;
const target = hook.rel32Target(dll_call_addr);
// Write new rel32 for the game-address copy
const game_call_addr = game_addr + site;
var rel_buf: [4]u8 = undefined;
hook.writeRel32(&rel_buf, game_call_addr + 1, target);
hook.writeProtected(game_call_addr + 1, &rel_buf);
}
log.fmt("BINARY PATCH: {d} bytes REF@0x{x:0>8} -> 0x{x:0>8}, {d} E8 fixups\n", .{ ref_size, ref_addr, game_addr, e8_sites.len });
} else {
_ = transform_hook.attach(0x714260, &transformDetour);
original_trampoline = @intCast(transform_hook.inner.trampoline);
}
_ = teardown_hook.attach(0x491180, &teardownDetour);
_ = render_frame_hook.attach(0x707680, &renderFrameDetour);
_ = exec_render_pass_hook.attach(0x708900, &execRenderPassDetour);
_ = world_update_hook.attach(0x482EA0, &worldUpdateDetour);
_ = teardown_hook.attach(0x491180, &teardownDetour);
_ = render_quads_hook.attach(0x76FB00, &renderQuadsDetour);
_ = movement_hook.attach(0x616620, &movementDetour);
// _ = interp_kf_hook.attach(0x713ea0, &interpKfDetour); // disabled — pure passthrough, REF calls 0x713ea0 directly
// Perf-identified hotspot hooks
// _ = interp_kf_hook.attach(0x713ea0, &interpKfDetour); // disabled: pure passthrough
_ = clip_hook.attach(0x6318c0, &clipDetour);
_ = glyph_hook.attach(0x5ca2d0, &glyphDetour);
_ = particle_hook.attach(0x7b2a50, &particleDetour);
@@ -1731,7 +1796,7 @@ pub fn installHooks() void {
_ = activep_hook.attach(0x7b5a10, &activepDetour);
_ = cbiter_hook.attach(0x404130, &cbiterDetour);
_ = findguid_hook.attach(0x464890, &findguidDetour);
_ = raytri2_hook.attach(0x632700, &raytri2Detour);
// _ = raytri2_hook.attach(0x632700, &raytri2Detour);
_ = drawbatch_hook.attach(0x70cb30, &drawbatchDetour);
_ = findlua_hook.attach(0x702000, &findluaDetour);
_ = spritequad_hook.attach(0x5a0f50, &spritequadDetour);
@@ -1760,6 +1825,9 @@ pub fn installHooks() void {
// blit_hub installed in lateInit() to clobber UnitXP's hook
log.print("transform44: 39 profiling hooks installed (blit_hub deferred)\n");
if (bisect_stop_section != 0) {
log.fmt(" BISECT MODE: REF stops after section {d}, then original trampoline\n", .{bisect_stop_section});
}
}
/// Called from engineInitDetour — after UnitXP has hooked blit_hub.