refactor(M03): extract M2 animated scene finalizer
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
extends SceneTree
|
||||
|
||||
## Synthetic candidate, material, ownership and timing regression for animated
|
||||
## M2 scene finalization.
|
||||
|
||||
const FINALIZER_SCRIPT := preload("res://src/render/m2/m2_animated_scene_finalizer.gd")
|
||||
const FINALIZER_PATH := "res://src/render/m2/m2_animated_scene_finalizer.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_candidate_instantiation_and_invalid_lifetime(failures)
|
||||
_verify_animation_validation_and_transfer(failures)
|
||||
_verify_material_mapping(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_ANIMATED_SCENE_FINALIZER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_ANIMATED_SCENE_FINALIZER PASS cases=13 iterations=10000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_candidate_instantiation_and_invalid_lifetime(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
_expect_same(finalizer.call("instantiate_candidate", null), null, "null Resource rejected", failures)
|
||||
_expect_same(finalizer.call("instantiate_candidate", Resource.new()), null, "unsupported Resource rejected", failures)
|
||||
|
||||
var invalid_source := Node.new()
|
||||
var invalid_scene := PackedScene.new()
|
||||
_expect_equal(invalid_scene.pack(invalid_source), OK, "non-Node3D scene packs", failures)
|
||||
invalid_source.free()
|
||||
var node_count_before := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||
_expect_same(finalizer.call("instantiate_candidate", invalid_scene), null, "non-Node3D root rejected", failures)
|
||||
var node_count_after := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||
_expect_equal(node_count_after, node_count_before, "invalid instantiated root freed", failures)
|
||||
|
||||
var valid_source := Node3D.new()
|
||||
var valid_scene := PackedScene.new()
|
||||
_expect_equal(valid_scene.pack(valid_source), OK, "Node3D scene packs", failures)
|
||||
valid_source.free()
|
||||
var candidate: Node3D = finalizer.call("instantiate_candidate", valid_scene)
|
||||
_expect_true(candidate != null, "Node3D candidate returned", failures)
|
||||
if candidate != null:
|
||||
candidate.free()
|
||||
|
||||
|
||||
func _verify_animation_validation_and_transfer(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var empty_candidate := Node3D.new()
|
||||
var empty_candidate_id := empty_candidate.get_instance_id()
|
||||
var rejected: Dictionary = finalizer.call("finalize_candidate", empty_candidate)
|
||||
_expect_true(rejected.is_empty(), "candidate without AnimationPlayer rejected", failures)
|
||||
_expect_false(is_instance_id_valid(empty_candidate_id), "rejected candidate freed", failures)
|
||||
|
||||
var animated_candidate := Node3D.new()
|
||||
var branch := Node3D.new()
|
||||
var first_player := AnimationPlayer.new()
|
||||
var second_player := AnimationPlayer.new()
|
||||
animated_candidate.add_child(branch)
|
||||
branch.add_child(first_player)
|
||||
animated_candidate.add_child(second_player)
|
||||
var players: Array[AnimationPlayer] = finalizer.call(
|
||||
"animation_players_in_subtree",
|
||||
animated_candidate
|
||||
)
|
||||
_expect_equal(players.size(), 2, "nested players discovered", failures)
|
||||
_expect_same(players[0], first_player, "player depth-first order retained", failures)
|
||||
var accepted: Dictionary = finalizer.call("finalize_candidate", animated_candidate)
|
||||
_expect_same(accepted.get("prototype"), animated_candidate, "exact candidate transferred", failures)
|
||||
_expect_equal(int(accepted.get("animation_player_count", 0)), 2, "player count transferred", failures)
|
||||
animated_candidate.free()
|
||||
|
||||
|
||||
func _verify_material_mapping(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var material_source_root := Node3D.new()
|
||||
var source_first := MeshInstance3D.new()
|
||||
var source_second := MeshInstance3D.new()
|
||||
source_first.mesh = _make_triangle_mesh(2)
|
||||
source_second.mesh = _make_triangle_mesh(1)
|
||||
var first_override := StandardMaterial3D.new()
|
||||
var second_surface_material := StandardMaterial3D.new()
|
||||
source_first.set_surface_override_material(0, first_override)
|
||||
source_second.mesh.surface_set_material(0, second_surface_material)
|
||||
material_source_root.add_child(source_first)
|
||||
material_source_root.add_child(source_second)
|
||||
|
||||
var animated_root := Node3D.new()
|
||||
var target_first := MeshInstance3D.new()
|
||||
var target_second := MeshInstance3D.new()
|
||||
target_first.mesh = _make_triangle_mesh(2)
|
||||
target_second.mesh = _make_triangle_mesh(2)
|
||||
animated_root.add_child(target_first)
|
||||
animated_root.add_child(target_second)
|
||||
var target_meshes: Array[MeshInstance3D] = finalizer.call(
|
||||
"mesh_instances_in_subtree",
|
||||
animated_root
|
||||
)
|
||||
_expect_same(target_meshes[0], target_first, "mesh depth-first order retained", failures)
|
||||
finalizer.call("repair_materials", animated_root, material_source_root)
|
||||
_expect_same(target_first.get_surface_override_material(0), first_override, "source override has priority", failures)
|
||||
_expect_same(target_first.get_surface_override_material(1), first_override, "missing source surface uses first fallback", failures)
|
||||
_expect_same(target_second.get_surface_override_material(0), second_surface_material, "source index maps to target index", failures)
|
||||
_expect_same(target_second.get_surface_override_material(1), first_override, "clamped source missing surface uses fallback", failures)
|
||||
animated_root.free()
|
||||
material_source_root.free()
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(loader_source.contains("M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()"), "loader composes finalizer", failures)
|
||||
for removed_loader_function in [
|
||||
"func _repair_m2_animated_materials(",
|
||||
"func _find_animation_players_recursive(",
|
||||
"func _find_mesh_instances_recursive(",
|
||||
]:
|
||||
_expect_false(loader_source.contains(removed_loader_function), "legacy helper removed: %s" % removed_loader_function, failures)
|
||||
for delegated_call in [
|
||||
"_m2_animated_scene_finalizer.instantiate_candidate(resource)",
|
||||
"_m2_animated_scene_finalizer.repair_materials(candidate, material_source)",
|
||||
"_m2_animated_scene_finalizer.finalize_candidate(candidate)",
|
||||
"_m2_animated_scene_finalizer.mesh_instances_in_subtree(root)",
|
||||
"_m2_animated_scene_finalizer.animation_players_in_subtree(root)",
|
||||
]:
|
||||
_expect_equal(loader_source.count(delegated_call), 1, "single loader delegation: %s" % delegated_call, failures)
|
||||
for retained_loader_rule in [
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE",
|
||||
"_get_or_load_m2_material_prototype(normalized_rel)",
|
||||
"_m2_prototype_cache_state.adopt_animated_prototype(",
|
||||
"_m2_prototype_cache_state.mark_animation_static(normalized_rel)",
|
||||
"M2_ANIM_CACHE path=%s cache=%s players=%d",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
"WorkerThreadPool.",
|
||||
"_m2_prototype_cache_state",
|
||||
"_render_budget_scheduler",
|
||||
"M2Builder",
|
||||
"M2RawModelRepository",
|
||||
]:
|
||||
_expect_false(finalizer_source.contains(forbidden_dependency), "finalizer omits %s ownership" % forbidden_dependency, failures)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var root := Node3D.new()
|
||||
var branch := root
|
||||
for _depth_index in range(8):
|
||||
var child_branch := Node3D.new()
|
||||
branch.add_child(child_branch)
|
||||
branch = child_branch
|
||||
branch.add_child(AnimationPlayer.new())
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(10000):
|
||||
finalizer.call("animation_players_in_subtree", root)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
root.free()
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "10000 subtree queries under 1 second", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _make_triangle_mesh(surface_count: int) -> ArrayMesh:
|
||||
var mesh := ArrayMesh.new()
|
||||
for _surface_index in range(surface_count):
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array([
|
||||
Vector3.ZERO,
|
||||
Vector3.RIGHT,
|
||||
Vector3.UP,
|
||||
])
|
||||
arrays[Mesh.ARRAY_INDEX] = PackedInt32Array([0, 1, 2])
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return mesh
|
||||
|
||||
|
||||
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(not condition, label, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||
if not is_same(actual, expected):
|
||||
failures.append(label)
|
||||
Reference in New Issue
Block a user