Complete M03 renderer closeout validation

This commit is contained in:
2026-08-02 15:28:10 +04:00
parent d9d20cb53f
commit 3bf1c2c657
13 changed files with 588 additions and 36 deletions
@@ -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:
+44 -16
View File
@@ -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(),
])