Files
open-wc/src/tools/verify_m2_animated_scene_finalizer.gd
T

205 lines
8.9 KiB
GDScript

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(",
]:
_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)