refactor(M03): extract M2 mesh resource traversal

Work-Package: M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001
Agent: sindo-main-codex
Tests: 36 headless renderer/coordinate contracts pass; checkpoint dry-run 7/7; documentation and coordination gates pass
Fidelity: preserves first-Mesh preorder and temporary PackedScene cleanup; no visual parity claim
This commit is contained in:
2026-07-17 13:02:40 +04:00
parent 6607100686
commit 7b0e1eac5f
12 changed files with 471 additions and 35 deletions
@@ -0,0 +1,34 @@
class_name M2MeshResourceExtractor
extends RefCounted
## Selects the first Mesh from loaded M2 Resources or existing Node subtrees.
## The caller owns ResourceLoader I/O, cache adoption and material preparation.
## Returns the direct Mesh Resource or the first Mesh in a temporary PackedScene
## instance. The temporary instance is freed before this method returns.
func extract_first_mesh(resource: Resource) -> Mesh:
if resource is Mesh:
return resource as Mesh
if not (resource is PackedScene):
return null
var temporary_root := (resource as PackedScene).instantiate()
if temporary_root == null:
return null
var mesh := find_first_mesh_in_subtree(temporary_root)
temporary_root.free()
return mesh
## Returns the first MeshInstance3D Mesh in depth-first preorder. The subtree
## and Mesh ownership remain unchanged.
func find_first_mesh_in_subtree(root: Node) -> Mesh:
if root == null:
return null
if root is MeshInstance3D:
return (root as MeshInstance3D).mesh
for child in root.get_children():
var mesh := find_first_mesh_in_subtree(child)
if mesh != null:
return mesh
return null
@@ -0,0 +1 @@
uid://dea4gekrxshjf
+7 -26
View File
@@ -69,6 +69,9 @@ const M2_MESH_LOAD_PIPELINE_STATE_SCRIPT := preload(
const M2_MESH_RESOURCE_CACHE_STATE_SCRIPT := preload(
"res://src/render/m2/m2_mesh_resource_cache_state.gd"
)
const M2_MESH_RESOURCE_EXTRACTOR_SCRIPT := preload(
"res://src/render/m2/m2_mesh_resource_extractor.gd"
)
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
@@ -260,6 +263,7 @@ var _m2_runtime_mesh_rebuild_classifier := (
M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()
)
var _m2_mesh_resource_cache_state := M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new()
var _m2_mesh_resource_extractor := M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()
var _m2_mesh_load_pipeline_state := M2_MESH_LOAD_PIPELINE_STATE_SCRIPT.new()
var _m2_animation_load_requests: Dictionary = {}
var _m2_animation_finalize_queue: Array = []
@@ -4277,7 +4281,7 @@ func _drain_m2_mesh_loads() -> void:
var path := String(pending.get("path", ""))
var resource: Resource = ResourceLoader.load_threaded_get(path)
var mesh := _extract_first_mesh_from_m2_resource(resource)
var mesh := _m2_mesh_resource_extractor.extract_first_mesh(resource)
if mesh != null:
_m2_mesh_resource_cache_state.store_mesh(
normalized_rel,
@@ -4695,34 +4699,11 @@ func _rebuild_m2_mesh_from_data(data: Dictionary) -> Mesh:
var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir)
if prototype == null:
return null
var rebuilt := _find_first_mesh_recursive(prototype)
var rebuilt := _m2_mesh_resource_extractor.find_first_mesh_in_subtree(prototype)
prototype.free()
return rebuilt
func _extract_first_mesh_from_m2_resource(resource: Resource) -> Mesh:
if resource is Mesh:
return resource as Mesh
if resource is PackedScene:
var node := (resource as PackedScene).instantiate()
if node == null:
return null
var mesh := _find_first_mesh_recursive(node)
node.free()
return mesh
return null
func _find_first_mesh_recursive(node: Node) -> Mesh:
if node is MeshInstance3D:
return (node as MeshInstance3D).mesh
for child in node.get_children():
var mesh := _find_first_mesh_recursive(child)
if mesh != null:
return mesh
return null
func _get_or_load_m2_mesh(rel_path: String) -> Mesh:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if _m2_mesh_resource_cache_state.has_mesh(normalized_rel):
@@ -4730,7 +4711,7 @@ func _get_or_load_m2_mesh(rel_path: String) -> Mesh:
var prototype: Node3D = _get_or_load_m2_prototype(rel_path)
if prototype == null:
return null
var mesh := _find_first_mesh_recursive(prototype)
var mesh := _m2_mesh_resource_extractor.find_first_mesh_in_subtree(prototype)
if mesh != null and not normalized_rel.is_empty():
mesh = _prepare_m2_mesh_for_runtime(normalized_rel, mesh)
_m2_mesh_resource_cache_state.store_mesh(normalized_rel, mesh)
@@ -94,7 +94,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
)
for retained_loader_rule in [
"ResourceLoader.load_threaded_get(path)",
"_extract_first_mesh_from_m2_resource(resource)",
"_m2_mesh_resource_extractor.extract_first_mesh(resource)",
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
"_m2_missing_cache[normalized_rel]",
"_m2_scene_cache[normalized_rel]",
@@ -0,0 +1,185 @@
extends SceneTree
## Synthetic direct-Resource/PackedScene/subtree/order/lifetime/boundary/timing
## regression for first-Mesh extraction.
const EXTRACTOR_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_extractor.gd")
const EXTRACTOR_PATH := "res://src/render/m2/m2_mesh_resource_extractor.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_direct_and_invalid_resources(failures)
_verify_depth_first_subtree_order(failures)
_verify_packed_scene_mesh_and_temporary_lifetime(failures)
_verify_ownership_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_MESH_RESOURCE_EXTRACTOR: %s" % failure)
quit(1)
return
print(
"M2_MESH_RESOURCE_EXTRACTOR PASS cases=10 iterations=10000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_direct_and_invalid_resources(failures: Array[String]) -> void:
var extractor: RefCounted = EXTRACTOR_SCRIPT.new()
var direct_mesh := ArrayMesh.new()
_expect_same(
extractor.call("extract_first_mesh", direct_mesh),
direct_mesh,
"direct Mesh reference retained",
failures
)
_expect_same(extractor.call("extract_first_mesh", Resource.new()), null, "unsupported Resource returns null", failures)
_expect_same(extractor.call("extract_first_mesh", null), null, "null Resource returns null", failures)
_expect_same(extractor.call("find_first_mesh_in_subtree", null), null, "null subtree returns null", failures)
func _verify_depth_first_subtree_order(failures: Array[String]) -> void:
var extractor: RefCounted = EXTRACTOR_SCRIPT.new()
var root := Node3D.new()
var first_branch := Node3D.new()
var second_branch := Node3D.new()
var first_mesh_instance := MeshInstance3D.new()
var second_mesh_instance := MeshInstance3D.new()
var first_mesh := ArrayMesh.new()
var second_mesh := ArrayMesh.new()
first_mesh_instance.mesh = first_mesh
second_mesh_instance.mesh = second_mesh
root.add_child(first_branch)
root.add_child(second_branch)
first_branch.add_child(first_mesh_instance)
second_branch.add_child(second_mesh_instance)
_expect_same(
extractor.call("find_first_mesh_in_subtree", root),
first_mesh,
"depth-first child order retained",
failures
)
_expect_same(first_mesh_instance.get_parent(), first_branch, "subtree ownership unchanged", failures)
first_mesh_instance.mesh = null
_expect_same(
extractor.call("find_first_mesh_in_subtree", root),
second_mesh,
"null MeshInstance Mesh skipped by parent traversal",
failures
)
root.free()
func _verify_packed_scene_mesh_and_temporary_lifetime(failures: Array[String]) -> void:
var extractor: RefCounted = EXTRACTOR_SCRIPT.new()
var source_root := Node3D.new()
var mesh_instance := MeshInstance3D.new()
var packed_mesh := ArrayMesh.new()
mesh_instance.mesh = packed_mesh
source_root.add_child(mesh_instance)
mesh_instance.owner = source_root
var packed_scene := PackedScene.new()
_expect_equal(packed_scene.pack(source_root), OK, "synthetic PackedScene packs", failures)
source_root.free()
var node_count_before := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
var extracted_mesh: Mesh = extractor.call("extract_first_mesh", packed_scene)
var node_count_after := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
_expect_same(extracted_mesh, packed_mesh, "PackedScene first Mesh retained", failures)
_expect_equal(node_count_after, node_count_before, "temporary PackedScene root freed", failures)
func _verify_ownership_boundaries(failures: Array[String]) -> void:
var extractor_source := FileAccess.get_file_as_string(EXTRACTOR_PATH)
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
_expect_true(
loader_source.contains("M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()"),
"loader composes Mesh resource extractor",
failures
)
_expect_false(
loader_source.contains("func _extract_first_mesh_from_m2_resource("),
"legacy Resource extractor removed",
failures
)
_expect_false(
loader_source.contains("func _find_first_mesh_recursive("),
"legacy subtree extractor removed",
failures
)
_expect_equal(
loader_source.count("_m2_mesh_resource_extractor.extract_first_mesh("),
1,
"threaded Resource finalization delegates",
failures
)
_expect_equal(
loader_source.count("_m2_mesh_resource_extractor.find_first_mesh_in_subtree("),
2,
"rebuild and synchronous fallback delegate",
failures
)
for retained_loader_rule in [
"ResourceLoader.load_threaded_get(path)",
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
"_m2_mesh_resource_cache_state.store_mesh(",
"M2_BUILDER_SCRIPT.build(data, extracted_dir)",
"_m2_missing_cache[normalized_rel]",
]:
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
for forbidden_dependency in [
"ResourceLoader.",
"FileAccess.",
"WorkerThreadPool.",
"M2Builder",
"_m2_mesh_resource_cache_state",
"_m2_missing_cache",
"MultiMesh",
]:
_expect_false(
extractor_source.contains(forbidden_dependency),
"extractor omits %s ownership" % forbidden_dependency,
failures
)
_expect_true(extractor_source.contains("temporary_root.free()"), "temporary root free remains explicit", failures)
func _verify_bounded_timing(failures: Array[String]) -> float:
var extractor: RefCounted = EXTRACTOR_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
var mesh_instance := MeshInstance3D.new()
mesh_instance.mesh = ArrayMesh.new()
branch.add_child(mesh_instance)
var started_microseconds := Time.get_ticks_usec()
for _iteration in range(10000):
extractor.call("find_first_mesh_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 lookups under 1 second", failures)
return elapsed_milliseconds
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)
@@ -0,0 +1 @@
uid://bgvidp5u31sud