Complete M03 renderer closeout validation
This commit is contained in:
@@ -39,9 +39,12 @@ func start_instance_playback(
|
||||
var phase := phase_for_instance(relative_path, instance_index)
|
||||
var native_diagnostics: Array[Dictionary] = []
|
||||
for animator in native_animators_in_subtree(root, native_animator_script):
|
||||
if animator.has_method("prepare_runtime"):
|
||||
animator.prepare_runtime()
|
||||
animator.set_phase(phase)
|
||||
if animator.has_method("prepare_runtime_at_phase"):
|
||||
animator.prepare_runtime_at_phase(phase)
|
||||
else:
|
||||
if animator.has_method("prepare_runtime"):
|
||||
animator.prepare_runtime()
|
||||
animator.set_phase(phase)
|
||||
if collect_native_diagnostics and animator.has_method("runtime_debug_state"):
|
||||
var diagnostic_variant = animator.runtime_debug_state()
|
||||
if diagnostic_variant is Dictionary:
|
||||
|
||||
@@ -26,11 +26,13 @@ func setup(target_mesh_instance: MeshInstance3D, bone_data: Array, surface_data:
|
||||
_capture_materials()
|
||||
_make_mesh_unique()
|
||||
_rebuild_mesh(0.0)
|
||||
set_process(mesh != null and not bones.is_empty() and not surfaces.is_empty() and animation_length > 0.0)
|
||||
_prepared = _has_runtime_animation_data()
|
||||
set_process(_prepared)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
prepare_runtime()
|
||||
if not _prepared:
|
||||
prepare_runtime()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
@@ -41,24 +43,49 @@ func _process(delta: float) -> void:
|
||||
|
||||
|
||||
func set_phase(phase: float) -> void:
|
||||
if animation_length <= 0.0:
|
||||
_time = 0.0
|
||||
else:
|
||||
_time = fposmod(animation_length * phase, animation_length)
|
||||
_set_phase_time(phase)
|
||||
_rebuild_mesh(_time)
|
||||
|
||||
|
||||
func prepare_runtime() -> bool:
|
||||
return _prepare_runtime(false)
|
||||
|
||||
|
||||
## Rebinds a duplicated animator to its local mesh and applies its deterministic
|
||||
## phase with one deformation rebuild. This must happen before attachment so
|
||||
## _ready() can remain idempotent for already prepared runtime instances.
|
||||
func prepare_runtime_at_phase(phase: float) -> bool:
|
||||
_set_phase_time(phase)
|
||||
return _prepare_runtime(true)
|
||||
|
||||
|
||||
func _prepare_runtime(force_rebuild: bool) -> bool:
|
||||
if _prepared and not force_rebuild:
|
||||
set_process(true)
|
||||
return true
|
||||
_resolve_mesh_instance()
|
||||
if force_rebuild:
|
||||
_materials.clear()
|
||||
_capture_materials()
|
||||
_unique_mesh_ready = false
|
||||
_make_mesh_unique()
|
||||
_rebuild_mesh(_time)
|
||||
_prepared = mesh != null and not bones.is_empty() and not surfaces.is_empty() and animation_length > 0.0
|
||||
_prepared = _has_runtime_animation_data()
|
||||
set_process(_prepared)
|
||||
return _prepared
|
||||
|
||||
|
||||
func _has_runtime_animation_data() -> bool:
|
||||
return mesh != null and not bones.is_empty() and not surfaces.is_empty() and animation_length > 0.0
|
||||
|
||||
|
||||
func _set_phase_time(phase: float) -> void:
|
||||
if animation_length <= 0.0:
|
||||
_time = 0.0
|
||||
else:
|
||||
_time = fposmod(animation_length * phase, animation_length)
|
||||
|
||||
|
||||
func runtime_debug_state() -> Dictionary:
|
||||
return {
|
||||
"prepared": _prepared,
|
||||
@@ -81,11 +108,12 @@ func _resolve_mesh_instance() -> void:
|
||||
func _make_mesh_unique() -> void:
|
||||
if _unique_mesh_ready or mesh_instance == null or mesh_instance.mesh == null:
|
||||
return
|
||||
var duplicated := mesh_instance.mesh.duplicate(true) as ArrayMesh
|
||||
if duplicated == null:
|
||||
return
|
||||
mesh_instance.mesh = duplicated
|
||||
mesh = duplicated
|
||||
# _rebuild_mesh() replaces every surface from the retained native arrays, so
|
||||
# copying the source ArrayMesh would only duplicate data that is discarded.
|
||||
# Materials were captured before this call and are intentionally shared.
|
||||
var instance_mesh := ArrayMesh.new()
|
||||
mesh_instance.mesh = instance_mesh
|
||||
mesh = instance_mesh
|
||||
_unique_mesh_ready = true
|
||||
|
||||
|
||||
@@ -143,20 +171,20 @@ func _rebuild_mesh(time: float) -> void:
|
||||
continue
|
||||
var transform: Transform3D = bone_matrices[bone_index]
|
||||
skinned_pos += transform * base_vertices[vertex_index] * weight
|
||||
if normals.size() == base_normals.size():
|
||||
if not normals.is_empty():
|
||||
skinned_nrm += (transform.basis * base_normals[vertex_index]) * weight
|
||||
total_weight += weight
|
||||
if total_weight > 0.0:
|
||||
vertices[vertex_index] = skinned_pos / total_weight
|
||||
if normals.size() == base_normals.size():
|
||||
if not normals.is_empty():
|
||||
normals[vertex_index] = (skinned_nrm / total_weight).normalized()
|
||||
else:
|
||||
vertices[vertex_index] = base_vertices[vertex_index]
|
||||
if normals.size() == base_normals.size():
|
||||
if not normals.is_empty():
|
||||
normals[vertex_index] = base_normals[vertex_index]
|
||||
else:
|
||||
vertices[vertex_index] = base_vertices[vertex_index]
|
||||
if normals.size() == base_normals.size():
|
||||
if not normals.is_empty():
|
||||
normals[vertex_index] = base_normals[vertex_index]
|
||||
|
||||
var arrays := []
|
||||
|
||||
@@ -16,6 +16,7 @@ func _initialize() -> void:
|
||||
_verify_animation_selection_priority(failures)
|
||||
_verify_player_loop_play_and_seek(failures)
|
||||
_verify_native_copy_start_and_diagnostics(failures)
|
||||
_verify_native_single_rebuild_preparation(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
@@ -24,7 +25,7 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_ANIMATION_PLAYBACK_CONTROLLER PASS cases=15 iterations=20000 elapsed_ms=%.3f"
|
||||
"M2_ANIMATION_PLAYBACK_CONTROLLER PASS cases=20 iterations=20000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
@@ -142,6 +143,47 @@ func _verify_native_copy_start_and_diagnostics(failures: Array[String]) -> void:
|
||||
target_root.free()
|
||||
|
||||
|
||||
func _verify_native_single_rebuild_preparation(failures: Array[String]) -> void:
|
||||
var fixture_root := Node3D.new()
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = "Mesh"
|
||||
var source_mesh := ArrayMesh.new()
|
||||
var source_arrays := []
|
||||
source_arrays.resize(Mesh.ARRAY_MAX)
|
||||
source_arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array([
|
||||
Vector3.ZERO,
|
||||
Vector3.RIGHT,
|
||||
Vector3.UP,
|
||||
])
|
||||
source_arrays[Mesh.ARRAY_INDEX] = PackedInt32Array([0, 1, 2])
|
||||
source_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, source_arrays)
|
||||
var shared_material := StandardMaterial3D.new()
|
||||
source_mesh.surface_set_material(0, shared_material)
|
||||
mesh_instance.mesh = source_mesh
|
||||
fixture_root.add_child(mesh_instance)
|
||||
|
||||
var animator: Node = NATIVE_ANIMATOR_SCRIPT.new()
|
||||
fixture_root.add_child(animator)
|
||||
animator.setup(
|
||||
mesh_instance,
|
||||
[{"parent": -1, "pivot": Vector3.ZERO}],
|
||||
[{
|
||||
"vertices": PackedVector3Array([Vector3.ZERO, Vector3.RIGHT, Vector3.UP]),
|
||||
"indices": PackedInt32Array([0, 1, 2]),
|
||||
}],
|
||||
4.0
|
||||
)
|
||||
var prepared := bool(animator.prepare_runtime_at_phase(0.25))
|
||||
var phased_mesh: Mesh = mesh_instance.mesh
|
||||
animator.call("_ready")
|
||||
_expect_true(prepared, "native phased preparation succeeds", failures)
|
||||
_expect_float_equal(float(animator.get("_time")), 1.0, "native phased preparation time", failures)
|
||||
_expect_true(phased_mesh != source_mesh, "native phased Mesh is instance-local", failures)
|
||||
_expect_same(phased_mesh.surface_get_material(0), shared_material, "native Material remains shared", failures)
|
||||
_expect_same(mesh_instance.mesh, phased_mesh, "native ready does not duplicate prepared mesh", failures)
|
||||
fixture_root.free()
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var controller_source := FileAccess.get_file_as_string(CONTROLLER_PATH)
|
||||
var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH)
|
||||
@@ -156,6 +198,17 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
_expect_false(loader_source.contains(removed_loader_function), "legacy helper removed: %s" % removed_loader_function, failures)
|
||||
_expect_equal(materializer_source.count("_animation_playback_controller.copy_native_animator_data("), 1, "native copy delegates once", failures)
|
||||
_expect_equal(materializer_source.count("_animation_playback_controller.start_instance_playback("), 1, "playback delegates once", failures)
|
||||
_expect_true(
|
||||
controller_source.contains("animator.prepare_runtime_at_phase(phase)"),
|
||||
"native phase preparation uses one rebuild",
|
||||
failures
|
||||
)
|
||||
var native_animator_source := FileAccess.get_file_as_string("res://src/scenes/streaming/m2_native_animator.gd")
|
||||
_expect_true(
|
||||
native_animator_source.contains("if not _prepared:\n\t\tprepare_runtime()"),
|
||||
"ready is idempotent after pre-attachment preparation",
|
||||
failures
|
||||
)
|
||||
for retained_materializer_rule in [
|
||||
"Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS",
|
||||
"batch_root.add_child(instance)",
|
||||
|
||||
@@ -58,7 +58,7 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("RENDERER_CLOSEOUT_CONTRACTS PASS workers=%d frame_steps=%d cache_versions=7" % [
|
||||
print("RENDERER_CLOSEOUT_CONTRACTS PASS workers=%d frame_steps=%d cache_versions=7 nested_glb=1" % [
|
||||
WORKER_FUNCTIONS.size(),
|
||||
MAIN_THREAD_FRAME_STEPS.size(),
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user