feat: add affine rotation rendering and timeline diagnostics
This commit is contained in:
@@ -5,9 +5,9 @@ Hooks the already-reversed object composite/apply path with plain JavaScript:
|
||||
gfx_object_composite AGE.EXE+0x7f650 (tracks current handle)
|
||||
gfx_object_apply_transform_channels AGE.EXE+0x72f00
|
||||
|
||||
For every changed matrix it records frame-time, base position, anchor, sampled 4x4 matrix, current/target
|
||||
scale and translation channels, and their timing fields. The apply hook sees the exact native composition
|
||||
after T(-anchor) * scale * middle * translation * T(anchor), before the later viewport matrices.
|
||||
For every changed matrix it records frame-time, integer base/anchor coordinates, sampled 4x4 matrix,
|
||||
current/target scale, one-shot axis-angle rotation, translation, and cyclic-rotation state. The apply hook
|
||||
sees the one-shot composition; the composite hook's leave captures the final matrix after cyclic rotation.
|
||||
|
||||
Run while the game is already at an ADV passage, then drive the relevant animation manually:
|
||||
py -3.11 -u -X utf8 tools/frida/capture_native_transforms.py [seconds] [pid|AGE.EXE] [--handle 0xHANDLE]
|
||||
@@ -24,12 +24,16 @@ OUT = REPO / "build" / "native-transform-trace.jsonl"
|
||||
|
||||
COMPOSITE_OFF = 0x7F650
|
||||
APPLY_OFF = 0x72F00
|
||||
OP21F_WORKER_OFF = 0x7EB70
|
||||
OP223_WORKER_OFF = 0x7F440
|
||||
OP234_WORKER_OFF = 0x7F060
|
||||
|
||||
JS = r"""
|
||||
const COMPOSITE_OFF=%d, APPLY_OFF=%d, HANDLE_FILTER=%s;
|
||||
const COMPOSITE_OFF=%d, APPLY_OFF=%d, OP21F=%d, OP223=%d, OP234=%d, HANDLE_FILTER=%s;
|
||||
const mod = Process.getModuleByName('AGE.EXE');
|
||||
const activeHandle = new Map();
|
||||
const last = new Map();
|
||||
const lastFinal = new Map();
|
||||
|
||||
function s32(p, off) { return p.add(off).readS32(); }
|
||||
function u32(p, off) { return p.add(off).readU32(); }
|
||||
@@ -39,7 +43,8 @@ function mat(p, off) {
|
||||
for (let i=0; i<16; i++) a.push(f32(p, off + i*4));
|
||||
return a;
|
||||
}
|
||||
function v3(p, off) { return [f32(p,off), f32(p,off+4), f32(p,off+8)]; }
|
||||
function v3f(p, off) { return [f32(p,off), f32(p,off+4), f32(p,off+8)]; }
|
||||
function v3i(p, off) { return [s32(p,off), s32(p,off+4), s32(p,off+8)]; }
|
||||
function diag(p, off) { return [f32(p,off), f32(p,off+20), f32(p,off+40)]; }
|
||||
function trans(p, off) { return [f32(p,off+48), f32(p,off+52), f32(p,off+56)]; }
|
||||
function rounded(a) { return a.map(x => Math.round(x * 10000) / 10000); }
|
||||
@@ -47,11 +52,28 @@ function rounded(a) { return a.map(x => Math.round(x * 10000) / 10000); }
|
||||
Interceptor.attach(mod.base.add(COMPOSITE_OFF), {
|
||||
onEnter(args) {
|
||||
const tid = Process.getCurrentThreadId();
|
||||
activeHandle.set(tid, args[0].toUInt32());
|
||||
this.handle = args[0].toUInt32(); this.ctx = this.context.ecx;
|
||||
activeHandle.set(tid, this.handle);
|
||||
},
|
||||
onLeave() { activeHandle.delete(Process.getCurrentThreadId()); }
|
||||
onLeave() {
|
||||
activeHandle.delete(Process.getCurrentThreadId());
|
||||
if (HANDLE_FILTER !== null && this.handle !== HANDLE_FILTER) return;
|
||||
const m = rounded(mat(this.ctx,0xb574)), key=this.handle.toString(16), sig=JSON.stringify(m);
|
||||
if (lastFinal.get(key) !== sig) {
|
||||
lastFinal.set(key,sig);
|
||||
send({kind:'transform-final',t:Date.now(),handle:this.handle,frameTime:u32(this.ctx,0xb550),matrix:m});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function stackI(ctx,n) { return ctx.esp.add(4+n*4).readS32(); }
|
||||
function stackF(ctx,n) { return ctx.esp.add(4+n*4).readFloat(); }
|
||||
Interceptor.attach(mod.base.add(OP21F), { onEnter() { send({kind:'op',op:'0x21f',handle:stackI(this.context,0),
|
||||
delay:stackI(this.context,1),duration:stackI(this.context,2),axis:[stackF(this.context,3),stackF(this.context,4),stackF(this.context,5)],angle:stackF(this.context,6)}); } });
|
||||
Interceptor.attach(mod.base.add(OP223), { onEnter() { send({kind:'op',op:'0x223',args:Array.from({length:8},(_,i)=>stackI(this.context,i))}); } });
|
||||
Interceptor.attach(mod.base.add(OP234), { onEnter() { send({kind:'op',op:'0x234',handle:stackI(this.context,0),
|
||||
period:stackI(this.context,1),axis:[stackF(this.context,2),stackF(this.context,3),stackF(this.context,4)]}); } });
|
||||
|
||||
Interceptor.attach(mod.base.add(APPLY_OFF), {
|
||||
onEnter(args) {
|
||||
this.tid = Process.getCurrentThreadId();
|
||||
@@ -69,24 +91,32 @@ Interceptor.attach(mod.base.add(APPLY_OFF), {
|
||||
if (last.get(key) === sig) return;
|
||||
last.set(key, sig);
|
||||
send({
|
||||
kind:'transform',
|
||||
kind:'transform', stage:'one-shot',
|
||||
t:Date.now(),
|
||||
handle:this.handle,
|
||||
frameTime:u32(this.ctx,0xb550),
|
||||
flags:u32(this.obj,0),
|
||||
slot:s32(this.obj,4),
|
||||
src:[s32(this.obj,8),s32(this.obj,12),s32(this.obj,16),s32(this.obj,20)],
|
||||
anchor:v3(this.obj,0x18),
|
||||
base:v3(this.obj,0x24),
|
||||
anchor:v3i(this.obj,0x18),
|
||||
base:v3i(this.obj,0x24),
|
||||
start:u32(this.obj,0x34),
|
||||
scaleDelay:s32(this.obj,0x3c),
|
||||
transDelay:s32(this.obj,0x44),
|
||||
scaleDuration:s32(this.obj,0x50),
|
||||
rotationDuration:s32(this.obj,0x54),
|
||||
transDuration:s32(this.obj,0x58),
|
||||
scaleCurrent:rounded(diag(this.obj,0x6c)),
|
||||
scaleTarget:rounded(diag(this.obj,0xac)),
|
||||
rotationCurrentAxis:rounded(v3f(this.obj,0x1ec)),
|
||||
rotationCurrentAngle:f32(this.obj,0x204),
|
||||
rotationTargetAxis:rounded(v3f(this.obj,0x1f8)),
|
||||
rotationTargetAngle:f32(this.obj,0x208),
|
||||
transCurrent:rounded(trans(this.obj,0x16c)),
|
||||
transTarget:rounded(trans(this.obj,0x1ac)),
|
||||
cycleStart:u32(this.obj,0x214),
|
||||
cyclePeriod:u32(this.obj,0x228),
|
||||
cycleAxis:rounded(v3f(this.obj,0x244)),
|
||||
matrix:m
|
||||
});
|
||||
} catch(e) {
|
||||
@@ -96,7 +126,7 @@ Interceptor.attach(mod.base.add(APPLY_OFF), {
|
||||
});
|
||||
send({kind:'ready', base:mod.base.toString(),
|
||||
composite:mod.base.add(COMPOSITE_OFF).toString(), apply:mod.base.add(APPLY_OFF).toString()});
|
||||
""" % (COMPOSITE_OFF, APPLY_OFF, "%s")
|
||||
""" % (COMPOSITE_OFF, APPLY_OFF, OP21F_WORKER_OFF, OP223_WORKER_OFF, OP234_WORKER_OFF, "%s")
|
||||
|
||||
|
||||
def main():
|
||||
@@ -135,11 +165,13 @@ def main():
|
||||
kind = payload.get("kind")
|
||||
if kind == "ready":
|
||||
print(f"[frida] native transform hooks live: composite={payload['composite']} apply={payload['apply']}")
|
||||
elif kind == "transform":
|
||||
elif kind in ("transform", "transform-final"):
|
||||
rows.append(payload)
|
||||
print(f" t={payload['frameTime']:>10} handle=0x{payload['handle']:x} "
|
||||
f"base={payload['base'][:2]} anchor={payload['anchor'][:2]} "
|
||||
f"scale={payload['scaleCurrent'][:2]} trans={payload['transCurrent'][:2]}")
|
||||
f"stage={payload.get('stage', 'final')}")
|
||||
elif kind == "op":
|
||||
rows.append(payload)
|
||||
print(f" {payload['op']} {payload}")
|
||||
elif kind == "error":
|
||||
print("[capture-error]", payload.get("message"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user