cfc252f161
Hooks executeSceneRenderPass (0x708900), renderFrame (0x707680), transformMatrix4x4 (0x714260), RenderTextureQuads (0x76FB00), and CMovement::ProcessUnitMovementUpdate (0x616620). Unified stats dump every 180 render passes shows per-frame call counts, cycle costs, bone counts, recursion depth, and quad item counts.
285 lines
10 KiB
Zig
285 lines
10 KiB
Zig
//! transform44 — render pipeline profiling & M2 bone transform optimization
|
||
//!
|
||
//! Hooks 5 functions in the render/update pipeline to measure per-frame costs:
|
||
//!
|
||
//! executeSceneRenderPass (0x708900) — top-level render pass dispatch
|
||
//! renderFrame (0x707680) — per-model render (calls transformMatrix4x4)
|
||
//! transformMatrix4x4 (0x714260) — per-model bone transform engine (17703 bytes)
|
||
//! RenderTextureQuads (0x76FB00) — batched quad rendering
|
||
//! CMovement::Process (0x616620) — per-unit movement update
|
||
//!
|
||
//! Stats are dumped every DUMP_FRAMES render passes (~2s at 60fps).
|
||
|
||
const std = @import("std");
|
||
const hook = @import("zhook");
|
||
const logging = @import("../logging.zig");
|
||
const mod_mutex = @import("../mutex.zig");
|
||
|
||
pub const module_name: [*:0]const u8 = "transform44";
|
||
|
||
var g_mutex: ?*anyopaque = null;
|
||
var g_is_hook_owner: bool = false;
|
||
var log: logging.Logger = .{};
|
||
|
||
pub fn isActive() bool {
|
||
return g_is_hook_owner;
|
||
}
|
||
|
||
// =============================================================================
|
||
// Profiling state — unified dump every DUMP_FRAMES render passes
|
||
// =============================================================================
|
||
|
||
const DUMP_FRAMES: u32 = 180;
|
||
|
||
var prof = ProfState{};
|
||
var t44_depth: u32 = 0; // recursion depth — survives resets
|
||
|
||
const ProfState = struct {
|
||
// Frame counter (incremented by executeSceneRenderPass)
|
||
frames: u32 = 0,
|
||
|
||
// transformMatrix4x4 (0x714260)
|
||
t44_calls: u32 = 0,
|
||
t44_early: u32 = 0,
|
||
t44_cycles: u64 = 0,
|
||
t44_bones: u64 = 0,
|
||
t44_max_bones: u32 = 0,
|
||
t44_max_depth: u32 = 0,
|
||
|
||
// renderFrame (0x707680)
|
||
rf_calls: u32 = 0,
|
||
rf_cycles: u64 = 0,
|
||
|
||
// executeSceneRenderPass (0x708900)
|
||
erp_calls: u32 = 0,
|
||
erp_cycles: u64 = 0,
|
||
|
||
// RenderTextureQuads (0x76FB00)
|
||
rtq_calls: u32 = 0,
|
||
rtq_cycles: u64 = 0,
|
||
rtq_items: u64 = 0,
|
||
|
||
// CMovement::ProcessUnitMovementUpdate (0x616620)
|
||
mov_calls: u32 = 0,
|
||
mov_cycles: u64 = 0,
|
||
};
|
||
|
||
inline fn rdtsc() u64 {
|
||
var lo: u32 = undefined;
|
||
var hi: u32 = undefined;
|
||
asm volatile ("rdtsc"
|
||
: [lo] "={eax}" (lo),
|
||
[hi] "={edx}" (hi),
|
||
);
|
||
return @as(u64, hi) << 32 | lo;
|
||
}
|
||
|
||
// =============================================================================
|
||
// Hook: transformMatrix4x4 (0x714260)
|
||
// __thiscall(ECX=SceneObject*, stack: Matrix4x4* ×4)
|
||
// Fastcall mapping: ECX=this, EDX=unused, stack: mat1, mat2, mat3, mat4
|
||
// RET 0x10
|
||
// =============================================================================
|
||
|
||
const TransformFn = fn (u32, u32, u32, u32, u32, u32) callconv(hook.cc.fastcall) void;
|
||
var transform_hook: hook.Detour(TransformFn) = .{};
|
||
|
||
fn transformDetour(this: u32, edx: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(hook.cc.fastcall) void {
|
||
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
||
|
||
const start = rdtsc();
|
||
|
||
// Check sync gate — predict early exit
|
||
const model_data = hook.readMem(u32, this + 0x10);
|
||
var is_early = false;
|
||
var bone_count: u32 = 0;
|
||
if (model_data == 0) {
|
||
is_early = true;
|
||
} else {
|
||
const anim_ctx = hook.readMem(u32, this + 0x30);
|
||
if (anim_ctx != 0) {
|
||
const sync_val = hook.readMem(u32, this + 0x40);
|
||
const anim_sync = hook.readMem(u32, anim_ctx + 0x10);
|
||
if (sync_val == anim_sync) is_early = true;
|
||
}
|
||
const model_hdr = hook.readMem(u32, this + 0x2C);
|
||
if (model_hdr != 0) {
|
||
bone_count = hook.readMem(u32, model_hdr + 0x34);
|
||
}
|
||
}
|
||
|
||
t44_depth += 1;
|
||
if (t44_depth > prof.t44_max_depth) prof.t44_max_depth = t44_depth;
|
||
|
||
transform_hook.callOriginal(.{ this, edx, mat1, mat2, mat3, mat4 });
|
||
|
||
t44_depth -= 1;
|
||
const elapsed = rdtsc() - start;
|
||
prof.t44_cycles +|= elapsed;
|
||
prof.t44_calls += 1;
|
||
if (is_early) prof.t44_early += 1;
|
||
prof.t44_bones +|= bone_count;
|
||
if (bone_count > prof.t44_max_bones) prof.t44_max_bones = bone_count;
|
||
}
|
||
|
||
// =============================================================================
|
||
// Hook: renderFrame (0x707680)
|
||
// __thiscall(ECX=this, stack: float* cameraPosition)
|
||
// Fastcall mapping: ECX=this, EDX=unused, stack: cameraPos
|
||
// RET 0x4
|
||
// =============================================================================
|
||
|
||
const RenderFrameFn = fn (u32, u32, u32) callconv(hook.cc.fastcall) ?*anyopaque;
|
||
var render_frame_hook: hook.Detour(RenderFrameFn) = .{};
|
||
|
||
fn renderFrameDetour(this: u32, edx: u32, camera_pos: u32) callconv(hook.cc.fastcall) ?*anyopaque {
|
||
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
||
|
||
const start = rdtsc();
|
||
const ret = render_frame_hook.callOriginal(.{ this, edx, camera_pos });
|
||
prof.rf_cycles +|= rdtsc() - start;
|
||
prof.rf_calls += 1;
|
||
return ret;
|
||
}
|
||
|
||
// =============================================================================
|
||
// Hook: executeSceneRenderPass (0x708900)
|
||
// __thiscall(ECX=scene, stack: renderPassIndex) — Ghidra labels __stdcall but
|
||
// prologue does MOV ESI,ECX (saves this). RET 0x4.
|
||
// Fastcall mapping: ECX=this, EDX=unused, stack: passIndex
|
||
// =============================================================================
|
||
|
||
const ExecRenderPassFn = fn (u32, u32, u32) callconv(hook.cc.fastcall) ?*anyopaque;
|
||
var exec_render_pass_hook: hook.Detour(ExecRenderPassFn) = .{};
|
||
|
||
fn execRenderPassDetour(this: u32, edx: u32, pass_index: u32) callconv(hook.cc.fastcall) ?*anyopaque {
|
||
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
||
|
||
const start = rdtsc();
|
||
const ret = exec_render_pass_hook.callOriginal(.{ this, edx, pass_index });
|
||
prof.erp_cycles +|= rdtsc() - start;
|
||
prof.erp_calls += 1;
|
||
|
||
// Use render pass as frame boundary for dump trigger
|
||
prof.frames += 1;
|
||
if (prof.frames >= DUMP_FRAMES) {
|
||
dumpStats();
|
||
}
|
||
return ret;
|
||
}
|
||
|
||
// =============================================================================
|
||
// Hook: RenderTextureQuads (0x76FB00)
|
||
// __fastcall(ECX=RenderBatch*) — no stack params, RET
|
||
// RenderBatch+0xC = item_count
|
||
// =============================================================================
|
||
|
||
const RenderQuadsFn = fn (u32, u32) callconv(hook.cc.fastcall) void;
|
||
var render_quads_hook: hook.Detour(RenderQuadsFn) = .{};
|
||
|
||
fn renderQuadsDetour(batch: u32, edx: u32) callconv(hook.cc.fastcall) void {
|
||
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
||
|
||
// Read item count before call (batch+0xC)
|
||
const items: u32 = if (batch != 0) hook.readMem(u32, batch + 0xC) else 0;
|
||
const start = rdtsc();
|
||
render_quads_hook.callOriginal(.{ batch, edx });
|
||
prof.rtq_cycles +|= rdtsc() - start;
|
||
prof.rtq_calls += 1;
|
||
prof.rtq_items +|= items;
|
||
}
|
||
|
||
// =============================================================================
|
||
// Hook: CMovement::ProcessUnitMovementUpdate (0x616620)
|
||
// __thiscall(ECX=this, stack: timeNow(u32), lastUpdate(u32))
|
||
// RET 0x8 (2 stack params)
|
||
// Fastcall mapping: ECX=this, EDX=unused, stack: timeNow, lastUpdate
|
||
// =============================================================================
|
||
|
||
const MovementFn = fn (u32, u32, u32, u32) callconv(hook.cc.fastcall) void;
|
||
var movement_hook: hook.Detour(MovementFn) = .{};
|
||
|
||
fn movementDetour(this: u32, edx: u32, time_now: u32, last_update: u32) callconv(hook.cc.fastcall) void {
|
||
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
||
|
||
const start = rdtsc();
|
||
movement_hook.callOriginal(.{ this, edx, time_now, last_update });
|
||
prof.mov_cycles +|= rdtsc() - start;
|
||
prof.mov_calls += 1;
|
||
}
|
||
|
||
// =============================================================================
|
||
// Unified stats dump
|
||
// =============================================================================
|
||
|
||
fn dumpStats() void {
|
||
const f = prof.frames;
|
||
if (f == 0) return;
|
||
|
||
// transformMatrix4x4
|
||
const t44_real = prof.t44_calls - prof.t44_early;
|
||
const t44_avg = if (t44_real > 0) prof.t44_cycles / t44_real else 0;
|
||
const t44_avg_bones = if (t44_real > 0) prof.t44_bones / t44_real else 0;
|
||
|
||
// Per-frame averages
|
||
const rf_per_f = prof.rf_calls / f;
|
||
const t44_per_f = prof.t44_calls / f;
|
||
const rtq_per_f = prof.rtq_calls / f;
|
||
const mov_per_f = prof.mov_calls / f;
|
||
const erp_per_f = prof.erp_calls / f;
|
||
|
||
// Cycle totals per frame
|
||
const rf_cyc_f = prof.rf_cycles / f;
|
||
const t44_cyc_f = prof.t44_cycles / f;
|
||
const rtq_cyc_f = prof.rtq_cycles / f;
|
||
const mov_cyc_f = prof.mov_cycles / f;
|
||
const erp_cyc_f = prof.erp_cycles / f;
|
||
|
||
log.fmt(
|
||
\\[prof] {d} frames | per-frame: erp={d} rf={d} t44={d}({d}skip) rtq={d} mov={d}
|
||
\\ cycles/frame: erp={d}K rf={d}K t44={d}K rtq={d}K mov={d}K
|
||
\\ t44: {d}cyc/work bones_avg={d} max={d} depth={d} | rtq_items/frame={d}
|
||
\\
|
||
, .{
|
||
f,
|
||
erp_per_f, rf_per_f, t44_per_f, prof.t44_early / f, rtq_per_f, mov_per_f,
|
||
erp_cyc_f / 1000, rf_cyc_f / 1000, t44_cyc_f / 1000, rtq_cyc_f / 1000, mov_cyc_f / 1000,
|
||
t44_avg, t44_avg_bones, prof.t44_max_bones, prof.t44_max_depth,
|
||
prof.rtq_items / f,
|
||
});
|
||
|
||
prof = ProfState{};
|
||
}
|
||
|
||
// =============================================================================
|
||
// Install / remove
|
||
// =============================================================================
|
||
|
||
pub fn installHooks() void {
|
||
const result = mod_mutex.acquire(module_name);
|
||
g_mutex = result.handle;
|
||
g_is_hook_owner = result.is_owner;
|
||
if (!g_is_hook_owner) return;
|
||
|
||
log = logging.Logger.open(module_name, .both);
|
||
_ = transform_hook.attach(0x714260, &transformDetour);
|
||
_ = render_frame_hook.attach(0x707680, &renderFrameDetour);
|
||
_ = exec_render_pass_hook.attach(0x708900, &execRenderPassDetour);
|
||
_ = render_quads_hook.attach(0x76FB00, &renderQuadsDetour);
|
||
_ = movement_hook.attach(0x616620, &movementDetour);
|
||
log.print("transform44: 5 profiling hooks installed\n");
|
||
}
|
||
|
||
pub fn removeHooks() void {
|
||
if (g_is_hook_owner) {
|
||
transform_hook.detach();
|
||
render_frame_hook.detach();
|
||
exec_render_pass_hook.detach();
|
||
render_quads_hook.detach();
|
||
movement_hook.detach();
|
||
log.close();
|
||
mod_mutex.release(&g_mutex);
|
||
}
|
||
g_is_hook_owner = false;
|
||
}
|