From 3b803b0480d52bc6d44bbb851492c1e3f21ce13e Mon Sep 17 00:00:00 2001 From: Kunal Siyag Date: Wed, 28 Oct 2026 23:35:16 +0530 Subject: [PATCH] perf(fp): run skeleton modifiers on the idle frame, not per physics frame Every Skeleton3D defaults to modifier_callback_mode_process=PHYSICS, so the GDScript modifiers (DogRig/CatRig on every animal body, the hero's strike pose and head look, the T0 conversation look) ran once per physics frame and were paid again for every physics catch-up step a slow web frame absorbs. The fp animation mixers are all hand-advanced on the idle frame already; run the modifiers there too (IDLE), so a rendered frame pays them exactly once. Pinned by a mode assert in the animals, cats, people-prewarm and player suites. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- scripts/fp/animals/cat_bodies.gd | 2 ++ scripts/fp/animals/dog_bodies.gd | 2 ++ scripts/fp/player/hero_look.gd | 2 ++ scripts/fp/player/hero_strike_pose.gd | 3 +++ scripts/fp/traffic/people_pool.gd | 4 ++++ tools/tests/fp_animals_test.gd | 12 ++++++++++++ tools/tests/fp_cats_test.gd | 12 ++++++++++++ tools/tests/fp_kit.gd | 21 +++++++++++++++++++++ tools/tests/fp_people_prewarm_test.gd | 13 +++++++++++++ tools/tests/fp_player_test.gd | 18 ++++++++++++++++++ 10 files changed, 89 insertions(+) diff --git a/scripts/fp/animals/cat_bodies.gd b/scripts/fp/animals/cat_bodies.gd index 50c10e75..fb226dff 100644 --- a/scripts/fp/animals/cat_bodies.gd +++ b/scripts/fp/animals/cat_bodies.gd @@ -92,6 +92,8 @@ func _bind(b: DB.Body, scene: Node3D) -> void: b.head = b.skel.find_bone("Head") var rig := CAT_RIG.new() rig.name = "CatRig" + # The rig runs on the idle frame, not once per physics frame (people_pool.gd says why). + b.skel.modifier_callback_mode_process = Skeleton3D.MODIFIER_CALLBACK_MODE_PROCESS_IDLE b.skel.add_child(rig) rig.bind(b.skel, scene, CAT_RIG.CAT_TAIL, "Neck") b.rig = rig diff --git a/scripts/fp/animals/dog_bodies.gd b/scripts/fp/animals/dog_bodies.gd index 5687f923..a73c1376 100644 --- a/scripts/fp/animals/dog_bodies.gd +++ b/scripts/fp/animals/dog_bodies.gd @@ -176,6 +176,8 @@ func _bind(b: Body, scene: Node3D) -> void: b.head = b.skel.find_bone("Head") b.rig = RIG.new() b.rig.name = "DogRig" + # The rig runs on the idle frame, not once per physics frame (people_pool.gd says why). + b.skel.modifier_callback_mode_process = Skeleton3D.MODIFIER_CALLBACK_MODE_PROCESS_IDLE b.skel.add_child(b.rig) b.rig.bind(b.skel, scene) diff --git a/scripts/fp/player/hero_look.gd b/scripts/fp/player/hero_look.gd index 61894da7..8b136cd1 100644 --- a/scripts/fp/player/hero_look.gd +++ b/scripts/fp/player/hero_look.gd @@ -120,6 +120,8 @@ func _modifier(n: String, bone: String, yaw_lim: float, pitch_lim: float) -> Loo m.transition_type = Tween.TRANS_SINE m.ease_type = Tween.EASE_IN_OUT m.origin_safe_margin = LOOK_SAFE_MARGIN + # Runs on the idle frame, not once per physics frame (people_pool.gd says why). + _skeleton.modifier_callback_mode_process = Skeleton3D.MODIFIER_CALLBACK_MODE_PROCESS_IDLE _skeleton.add_child(m) return m diff --git a/scripts/fp/player/hero_strike_pose.gd b/scripts/fp/player/hero_strike_pose.gd index 60defff2..0210c8ea 100644 --- a/scripts/fp/player/hero_strike_pose.gd +++ b/scripts/fp/player/hero_strike_pose.gd @@ -160,6 +160,9 @@ func _init( _curl.append_array(fist_joints(p_sk, s)) _mod = Mod.new(self) _mod.name = "StrikePose" + # Runs on the idle frame, after the hand-advanced tree, not once per physics frame + # (the web build's catch-up steps would multiply that; people_pool.gd says more). + p_sk.modifier_callback_mode_process = Skeleton3D.MODIFIER_CALLBACK_MODE_PROCESS_IDLE p_sk.add_child(_mod) _mod.active = false diff --git a/scripts/fp/traffic/people_pool.gd b/scripts/fp/traffic/people_pool.gd index e6345e80..205baef9 100644 --- a/scripts/fp/traffic/people_pool.gd +++ b/scripts/fp/traffic/people_pool.gd @@ -212,6 +212,10 @@ func make(ai: int) -> Body: b.root.queue_free() return null b.head = b.skel.find_bone("Head") + # Skeleton modifiers (the T0 head look) run on the idle frame with the hand-advanced + # mixers; the engine default PHYSICS mode would pay them once per physics frame, + # multiplied by the web build's catch-up steps on slow frames. + b.skel.modifier_callback_mode_process = Skeleton3D.MODIFIER_CALLBACK_MODE_PROCESS_IDLE for n: Node in b.scene.find_children("*", "GeometryInstance3D", true, false): b.meshes.append(n as GeometryInstance3D) var lib: AnimationLibrary = ppl._libs[a.gender] diff --git a/tools/tests/fp_animals_test.gd b/tools/tests/fp_animals_test.gd index 1adbff95..ca7a399a 100644 --- a/tools/tests/fp_animals_test.gd +++ b/tools/tests/fp_animals_test.gd @@ -263,6 +263,18 @@ func _check_lead() -> WK.Walker: rigged and b.ap.has_animation(&"Gallop") and b.ap.has_animation(&"Attack"), "lead: the dog is the rigged %s with its clips and a DogRig" % d.breed ) + K.check( + fails, + ( + b.skel != null + and ( + b.skel.modifier_callback_mode_process + == Skeleton3D.MODIFIER_CALLBACK_MODE_PROCESS_IDLE + ) + ), + "lead: the DogRig runs on the idle frame, not once per physics frame" + ) + K.check_modifier_modes(fails, get_root()) var worst := 0.0 var left := 0.0 var n := 0 diff --git a/tools/tests/fp_cats_test.gd b/tools/tests/fp_cats_test.gd index 231f88ed..0ef5b197 100644 --- a/tools/tests/fp_cats_test.gd +++ b/tools/tests/fp_cats_test.gd @@ -334,6 +334,18 @@ func _check_spawn() -> LIFE.Cat: dressed and clips and c.body.rig is CB.CAT_RIG and not c.name.is_empty(), "spawn: %s, a %s %s cat in the cat's body and its coat" % [c.name, c.temper, c.coat] ) + K.check( + fails, + ( + c.body.skel != null + and ( + c.body.skel.modifier_callback_mode_process + == Skeleton3D.MODIFIER_CALLBACK_MODE_PROCESS_IDLE + ) + ), + "spawn: the CatRig runs on the idle frame, not once per physics frame" + ) + K.check_modifier_modes(fails, get_root()) cats.scan(ctx.player.position, false, true, {}) var gone := _cat() == null _step(AF.SCAN_S + 0.1) diff --git a/tools/tests/fp_kit.gd b/tools/tests/fp_kit.gd index dac8ca5c..4b5c3b9b 100644 --- a/tools/tests/fp_kit.gd +++ b/tools/tests/fp_kit.gd @@ -426,6 +426,27 @@ static func check(fails: Array, ok: bool, what: String) -> void: fails.append(what) +## Every SkeletonModifier3D under root rides a skeleton in the IDLE modifier callback mode: +## the engine default PHYSICS mode pays each modifier once per physics frame, multiplied by +## the web build's catch-up steps on slow frames (the day-10 tick-frame attribution). +static func check_modifier_modes(fails: Array, root: Node) -> void: + var mods := root.find_children("*", "SkeletonModifier3D", true, false) + var bad := 0 + for m: Node in mods: + var sk := m.get_parent() as Skeleton3D + var idle := ( + sk != null + and sk.modifier_callback_mode_process == Skeleton3D.MODIFIER_CALLBACK_MODE_PROCESS_IDLE + ) + if not idle: + bad += 1 + check( + fails, + bad == 0, + "every SkeletonModifier3D runs on the idle frame (%d of %d off)" % [bad, mods.size()] + ) + + ## Input events for a feature's _unhandled_input: action a pressed. static func action_event(a: String) -> InputEventAction: var e := InputEventAction.new() diff --git a/tools/tests/fp_people_prewarm_test.gd b/tools/tests/fp_people_prewarm_test.gd index 58d61a12..15aad146 100644 --- a/tools/tests/fp_people_prewarm_test.gd +++ b/tools/tests/fp_people_prewarm_test.gd @@ -60,4 +60,17 @@ func _init() -> void: pe.get("scene_loads") == settled, "a seat after warming initiates no load (still %d)" % settled ) + + # The body a seat lends runs its skeleton modifiers (the T0 head look) on the idle frame + # with the hand-advanced mixers, not once per physics frame (web catch-up multiplies that). + var seated: Array = (pe.get("_pool") as RefCounted).get("_seated") + var sk := (seated[0].get("skel") if not seated.is_empty() else null) as Skeleton3D + K.check( + fails, + ( + sk != null + and sk.modifier_callback_mode_process == Skeleton3D.MODIFIER_CALLBACK_MODE_PROCESS_IDLE + ), + "a seated body's modifiers run on the idle frame, not once per physics frame" + ) K.finish(self, "FP_PEOPLE_PREWARM_TEST", fails) diff --git a/tools/tests/fp_player_test.gd b/tools/tests/fp_player_test.gd index 74da3b93..0a932acb 100644 --- a/tools/tests/fp_player_test.gd +++ b/tools/tests/fp_player_test.gd @@ -63,6 +63,7 @@ func _init() -> void: _check_thin_wall(ctx) _check_stuck(ctx) _check_cam_trunk(ctx) + _check_hero_modifier_mode(ctx) K.finish(self, "FP_PLAYER_TEST", fails) @@ -1486,3 +1487,20 @@ func _check_cam_trunk(ctx: FpContext) -> void: "a trunk where the camera sat: inside it or looking through it on %d of 240 frames" % bad ) K.check(fails, vaultable.is_empty(), "parkour's boxes_near still leaves the trunk out") + + +## The hero's strike-pose and head-look modifiers run on the idle frame, after the +## hand-advanced AnimationTree, not once per physics frame (the web build's catch-up steps +## would multiply that). +func _check_hero_modifier_mode(ctx: FpContext) -> void: + var rig: Variant = (ctx.player as PL).avatar.get("rig") + var sk := (rig.get("skeleton") if rig != null else null) as Skeleton3D + K.check( + fails, + ( + sk != null + and sk.modifier_callback_mode_process == Skeleton3D.MODIFIER_CALLBACK_MODE_PROCESS_IDLE + ), + "hero: the skeleton's modifiers run on the idle frame, not once per physics frame" + ) + K.check_modifier_modes(fails, get_root())