refactor(M03): extract M2 prototype cache state

This commit is contained in:
2026-07-17 16:29:11 +04:00
parent 87bf067493
commit cd664eb4cb
19 changed files with 715 additions and 89 deletions
+157
View File
@@ -0,0 +1,157 @@
class_name M2PrototypeCacheState
extends RefCounted
## Owns detached static/animated M2 prototypes and their negative lookup
## outcomes until final renderer shutdown.
var _static_prototypes_by_path: Dictionary = {}
var _animated_prototypes_by_path: Dictionary = {}
var _missing_model_paths: Dictionary = {}
var _static_animation_paths: Dictionary = {}
## Returns whether a valid static prototype is retained for the normalized path.
func has_static_prototype(normalized_relative_path: String) -> bool:
return find_static_prototype(normalized_relative_path) != null
## Returns the exact retained static prototype or null.
func find_static_prototype(normalized_relative_path: String) -> Node3D:
return _find_valid_prototype(_static_prototypes_by_path, normalized_relative_path)
## Adopts a static prototype and returns the canonical retained reference. The
## first valid prototype wins; a later detached candidate is released.
func adopt_static_prototype(
normalized_relative_path: String,
prototype: Node3D) -> Node3D:
return _adopt_prototype(
_static_prototypes_by_path,
normalized_relative_path,
prototype
)
## Returns whether a valid animated prototype is retained for the normalized path.
func has_animated_prototype(normalized_relative_path: String) -> bool:
return find_animated_prototype(normalized_relative_path) != null
## Returns the exact retained animated prototype or null.
func find_animated_prototype(normalized_relative_path: String) -> Node3D:
return _find_valid_prototype(_animated_prototypes_by_path, normalized_relative_path)
## Adopts an animated prototype and returns the canonical retained reference.
func adopt_animated_prototype(
normalized_relative_path: String,
prototype: Node3D) -> Node3D:
return _adopt_prototype(
_animated_prototypes_by_path,
normalized_relative_path,
prototype
)
## Records that no model prototype can currently be loaded for this path.
func mark_model_missing(normalized_relative_path: String) -> bool:
return _mark_path(_missing_model_paths, normalized_relative_path)
## Returns whether the model path has a retained missing outcome.
func is_model_missing(normalized_relative_path: String) -> bool:
return not normalized_relative_path.is_empty() and _missing_model_paths.has(
normalized_relative_path
)
## Records that the path must use static presentation instead of animation.
func mark_animation_static(normalized_relative_path: String) -> bool:
return _mark_path(_static_animation_paths, normalized_relative_path)
## Returns whether the path has a retained static-animation outcome.
func is_animation_static(normalized_relative_path: String) -> bool:
return not normalized_relative_path.is_empty() and _static_animation_paths.has(
normalized_relative_path
)
## Returns detached path-only cache state for diagnostics and verification.
func diagnostic_snapshot() -> Dictionary:
return {
"static_prototype_paths": _sorted_paths(_static_prototypes_by_path),
"animated_prototype_paths": _sorted_paths(_animated_prototypes_by_path),
"missing_model_paths": _sorted_paths(_missing_model_paths),
"static_animation_paths": _sorted_paths(_static_animation_paths),
}
## Releases every retained prototype and clears positive and negative state.
## Detached Nodes are freed synchronously; in-tree Nodes are queued for deletion.
func clear_and_release() -> void:
_release_prototypes(_static_prototypes_by_path)
_release_prototypes(_animated_prototypes_by_path)
_missing_model_paths.clear()
_static_animation_paths.clear()
func _find_valid_prototype(
prototypes_by_path: Dictionary,
normalized_relative_path: String) -> Node3D:
if normalized_relative_path.is_empty():
return null
var prototype_variant: Variant = prototypes_by_path.get(normalized_relative_path)
if not (prototype_variant is Node3D):
return null
var prototype := prototype_variant as Node3D
return prototype if is_instance_valid(prototype) else null
func _adopt_prototype(
prototypes_by_path: Dictionary,
normalized_relative_path: String,
prototype: Node3D) -> Node3D:
if normalized_relative_path.is_empty() or prototype == null or not is_instance_valid(prototype):
return null
var retained_prototype := _find_valid_prototype(
prototypes_by_path,
normalized_relative_path
)
if retained_prototype != null:
if not is_same(retained_prototype, prototype):
_release_prototype(prototype)
return retained_prototype
prototypes_by_path[normalized_relative_path] = prototype
return prototype
func _mark_path(paths: Dictionary, normalized_relative_path: String) -> bool:
if normalized_relative_path.is_empty():
return false
paths[normalized_relative_path] = true
return true
func _release_prototypes(prototypes_by_path: Dictionary) -> void:
for prototype_variant in prototypes_by_path.values():
if prototype_variant is Node3D:
_release_prototype(prototype_variant as Node3D)
prototypes_by_path.clear()
func _release_prototype(prototype: Node3D) -> void:
if prototype == null or not is_instance_valid(prototype):
return
if prototype.is_inside_tree():
prototype.queue_free()
else:
prototype.free()
func _sorted_paths(paths: Dictionary) -> Array[String]:
var result: Array[String] = []
for path_variant in paths.keys():
result.append(String(path_variant))
result.sort()
return result
@@ -0,0 +1 @@
uid://srnqsus8pddp
+62 -43
View File
@@ -66,6 +66,9 @@ const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
const M2_RAW_MODEL_REPOSITORY_SCRIPT := preload(
"res://src/render/m2/m2_raw_model_repository.gd"
)
const M2_PROTOTYPE_CACHE_STATE_SCRIPT := preload(
"res://src/render/m2/m2_prototype_cache_state.gd"
)
const M2_MESH_LOAD_PIPELINE_STATE_SCRIPT := preload(
"res://src/render/m2/m2_mesh_load_pipeline_state.gd"
)
@@ -263,6 +266,7 @@ var _m2_placement_grouper := M2_PLACEMENT_GROUPER_SCRIPT.new()
var _m2_build_batch_planner := M2_BUILD_BATCH_PLANNER_SCRIPT.new()
var _m2_runtime_mesh_finalizer := M2_RUNTIME_MESH_FINALIZER_SCRIPT.new()
var _m2_raw_model_repository := M2_RAW_MODEL_REPOSITORY_SCRIPT.new()
var _m2_prototype_cache_state := M2_PROTOTYPE_CACHE_STATE_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()
@@ -309,10 +313,6 @@ var _wmo_render_build_queue := WMO_RENDER_BUILD_QUEUE_SCRIPT.new()
var _wmo_scene_resource_cache_state := WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new()
var _wmo_missing_cache: Dictionary = {}
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
var _m2_scene_cache: Dictionary = {}
var _m2_animated_scene_cache: Dictionary = {}
var _m2_static_animation_cache: Dictionary = {}
var _m2_missing_cache: Dictionary = {}
var _world_wmo_root: Node3D
var _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_SCRIPT.new()
var _wmo_render_build_step_planner := WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.new()
@@ -481,12 +481,9 @@ func _render_operation_limits_for_frame() -> Dictionary:
## loader after all asynchronous work has completed. Map refreshes retain these
## caches; only scene shutdown destroys them.
func _release_runtime_caches_for_shutdown() -> void:
_free_detached_node_cache(_m2_scene_cache)
_free_detached_node_cache(_m2_animated_scene_cache)
_m2_prototype_cache_state.clear_and_release()
_free_detached_node_cache(_wmo_prototype_cache)
_m2_mesh_resource_cache_state.clear()
_m2_static_animation_cache.clear()
_m2_missing_cache.clear()
_wmo_render_resource_cache_state.clear_all()
_wmo_scene_resource_cache_state.clear_all()
_wmo_missing_cache.clear()
@@ -4219,7 +4216,7 @@ func _drain_m2_animation_loads() -> void:
var path := String(pending.get("path", ""))
if path.is_empty():
_m2_animation_load_requests.erase(normalized_rel)
_m2_static_animation_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
@@ -4232,10 +4229,14 @@ func _drain_m2_animation_loads() -> void:
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE):
var pending: Dictionary = _m2_animation_finalize_queue.pop_front()
var normalized_rel := String(pending.get("normalized", ""))
if normalized_rel.is_empty() or _m2_animated_scene_cache.has(normalized_rel) or _m2_static_animation_cache.has(normalized_rel):
if (
normalized_rel.is_empty()
or _m2_prototype_cache_state.has_animated_prototype(normalized_rel)
or _m2_prototype_cache_state.is_animation_static(normalized_rel)
):
continue
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
_m2_static_animation_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
continue
var path := String(pending.get("path", ""))
@@ -4246,12 +4247,15 @@ func _drain_m2_animation_loads() -> void:
_repair_m2_animated_materials(normalized_rel, node as Node3D)
var players := _find_animation_players_recursive(node)
if not players.is_empty():
_m2_animated_scene_cache[normalized_rel] = node as Node3D
_m2_prototype_cache_state.adopt_animated_prototype(
normalized_rel,
node as Node3D
)
if debug_streaming:
print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [normalized_rel, path, players.size()])
continue
node.free()
_m2_static_animation_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
func _drain_m2_mesh_loads() -> void:
@@ -4260,7 +4264,7 @@ func _drain_m2_mesh_loads() -> void:
var path: String = String(pending.get("path", ""))
if path.is_empty():
_m2_mesh_load_pipeline_state.discard_request(normalized_rel)
_m2_missing_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
@@ -4277,7 +4281,7 @@ func _drain_m2_mesh_loads() -> void:
):
continue
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
_m2_missing_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
continue
var path := String(pending.get("path", ""))
@@ -4289,7 +4293,7 @@ func _drain_m2_mesh_loads() -> void:
_prepare_m2_mesh_for_runtime(normalized_rel, mesh)
)
else:
_m2_missing_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
func _process_m2_build_jobs() -> void:
@@ -4357,7 +4361,7 @@ func _process_m2_build_jobs() -> void:
else:
var mesh := _get_m2_mesh_or_request(rel_path)
if mesh == null:
if not _m2_missing_cache.has(normalized_rel):
if not _m2_prototype_cache_state.is_model_missing(normalized_rel):
_m2_build_queue.pop_front()
_m2_build_queue.append(key)
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD)
@@ -4624,7 +4628,7 @@ func _get_m2_mesh_or_request(rel_path: String) -> Mesh:
return null
if _m2_mesh_resource_cache_state.has_mesh(normalized_rel):
return _m2_mesh_resource_cache_state.find_mesh(normalized_rel)
if _m2_missing_cache.has(normalized_rel):
if _m2_prototype_cache_state.is_model_missing(normalized_rel):
return null
_request_m2_mesh_load(normalized_rel)
return null
@@ -4649,7 +4653,7 @@ func _request_m2_mesh_load(normalized_rel: String) -> void:
return
break
_m2_missing_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
@@ -4840,11 +4844,11 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if normalized_rel.is_empty():
return null
if _m2_scene_cache.has(normalized_rel):
var cached: Node3D = _m2_scene_cache[normalized_rel]
var cached := _m2_prototype_cache_state.find_static_prototype(normalized_rel)
if cached != null:
_prepare_m2_node_for_runtime(normalized_rel, cached)
return cached
if _m2_missing_cache.has(normalized_rel):
if _m2_prototype_cache_state.is_model_missing(normalized_rel):
return null
# Try pre-baked cache (.tscn first, then legacy .glb)
@@ -4858,8 +4862,10 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
var node = (resource as PackedScene).instantiate()
if node is Node3D:
_prepare_m2_node_for_runtime(normalized_rel, node as Node3D)
_m2_scene_cache[normalized_rel] = node as Node3D
return node as Node3D
return _m2_prototype_cache_state.adopt_static_prototype(
normalized_rel,
node as Node3D
)
break
# Fall back to the raw native M2 repository.
@@ -4868,25 +4874,30 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
normalized_rel
)
if data.is_empty():
_m2_missing_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
return null
var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir)
if prototype == null:
_m2_missing_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
return null
_m2_scene_cache[normalized_rel] = prototype
return prototype
return _m2_prototype_cache_state.adopt_static_prototype(
normalized_rel,
prototype
)
func _get_or_load_m2_native_animated_prototype(rel_path: String) -> Node3D:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if normalized_rel.is_empty() or not _is_m2_native_animation_candidate(normalized_rel):
return null
if _m2_animated_scene_cache.has(normalized_rel):
return _m2_animated_scene_cache[normalized_rel]
if _m2_static_animation_cache.has(normalized_rel):
var cached_animated := _m2_prototype_cache_state.find_animated_prototype(
normalized_rel
)
if cached_animated != null:
return cached_animated
if _m2_prototype_cache_state.is_animation_static(normalized_rel):
return null
var data := _m2_raw_model_repository.load_animated_model_data(
extracted_dir,
@@ -4894,15 +4905,18 @@ func _get_or_load_m2_native_animated_prototype(rel_path: String) -> Node3D:
)
var animated_surfaces: Array = data.get("animated_surfaces", [])
if data.is_empty() or animated_surfaces.is_empty():
_m2_static_animation_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
return null
var prototype: Node3D = M2_NATIVE_ANIMATED_BUILDER_SCRIPT.build(data, extracted_dir)
if prototype == null or prototype.get_child_count() <= 0:
_m2_static_animation_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
return null
_m2_animated_scene_cache[normalized_rel] = prototype
prototype = _m2_prototype_cache_state.adopt_animated_prototype(
normalized_rel,
prototype
)
if debug_streaming:
print("M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d anim_id=%d seq=%d score=%d length=%.2f" % [
normalized_rel,
@@ -4924,9 +4938,12 @@ func _get_or_load_m2_animated_prototype(rel_path: String) -> Node3D:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if normalized_rel.is_empty():
return null
if _m2_animated_scene_cache.has(normalized_rel):
return _m2_animated_scene_cache[normalized_rel]
if _m2_static_animation_cache.has(normalized_rel):
var cached_animated := _m2_prototype_cache_state.find_animated_prototype(
normalized_rel
)
if cached_animated != null:
return cached_animated
if _m2_prototype_cache_state.is_animation_static(normalized_rel):
return null
if _m2_animation_load_requests.has(normalized_rel):
return null
@@ -4940,7 +4957,7 @@ func _request_m2_animation_load(normalized_rel: String) -> void:
return
var cache_res_path := _find_m2_animated_glb_cache_path(normalized_rel)
if cache_res_path.is_empty():
_m2_static_animation_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
return
var err := ResourceLoader.load_threaded_request(
cache_res_path,
@@ -4953,7 +4970,7 @@ func _request_m2_animation_load(normalized_rel: String) -> void:
"path": cache_res_path,
}
else:
_m2_static_animation_cache[normalized_rel] = true
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
func _find_m2_animated_glb_cache_path(normalized_rel: String) -> String:
@@ -5018,8 +5035,8 @@ func _repair_m2_animated_materials(normalized_rel: String, animated_root: Node3D
func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
if normalized_rel.is_empty():
return null
if _m2_scene_cache.has(normalized_rel):
var cached: Node3D = _m2_scene_cache[normalized_rel]
var cached := _m2_prototype_cache_state.find_static_prototype(normalized_rel)
if cached != null:
_prepare_m2_node_for_runtime(normalized_rel, cached)
return cached
@@ -5031,8 +5048,10 @@ func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
var node = (resource as PackedScene).instantiate()
if node is Node3D:
_prepare_m2_node_for_runtime(normalized_rel, node as Node3D)
_m2_scene_cache[normalized_rel] = node as Node3D
return node as Node3D
return _m2_prototype_cache_state.adopt_static_prototype(
normalized_rel,
node as Node3D
)
return null
@@ -111,7 +111,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
"ResourceLoader.load_threaded_get(path)",
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE",
"_m2_mesh_resource_cache_state.store_mesh(",
"_m2_missing_cache[normalized_rel]",
"_m2_prototype_cache_state.mark_model_missing(normalized_rel)",
]:
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
for forbidden_dependency in [
@@ -122,7 +122,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
"RID(",
"_m2_mesh_cache",
"_m2_mesh_resource_cache_state",
"_m2_missing_cache",
"_m2_prototype_cache_state",
]:
_expect_false(
pipeline_source.contains(forbidden_dependency),
@@ -96,8 +96,8 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
"ResourceLoader.load_threaded_get(path)",
"_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]",
"_m2_prototype_cache_state.mark_model_missing(normalized_rel)",
"_m2_prototype_cache_state.adopt_static_prototype(",
]:
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
for forbidden_dependency in [
@@ -107,8 +107,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
"PackedScene",
"Node3D",
"M2Builder",
"_m2_missing_cache",
"_m2_scene_cache",
"_m2_prototype_cache_state",
]:
_expect_false(
cache_source.contains(forbidden_dependency),
@@ -126,7 +126,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
"_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]",
"_m2_prototype_cache_state.mark_model_missing(normalized_rel)",
]:
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
for forbidden_dependency in [
@@ -135,7 +135,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
"WorkerThreadPool.",
"M2Builder",
"_m2_mesh_resource_cache_state",
"_m2_missing_cache",
"_m2_prototype_cache_state",
"MultiMesh",
]:
_expect_false(
@@ -0,0 +1,162 @@
extends SceneTree
## Synthetic admission/identity/negative-state/release/source/timing contract.
const CACHE_STATE_SCRIPT := preload("res://src/render/m2/m2_prototype_cache_state.gd")
const CACHE_STATE_PATH := "res://src/render/m2/m2_prototype_cache_state.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_admission_identity_and_negative_state(failures)
_verify_release_ownership(failures)
_verify_ownership_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_PROTOTYPE_CACHE_STATE: %s" % failure)
quit(1)
return
print(
"M2_PROTOTYPE_CACHE_STATE PASS cases=16 iterations=100 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_admission_identity_and_negative_state(failures: Array[String]) -> void:
var state: RefCounted = CACHE_STATE_SCRIPT.new()
_expect_same(state.call("find_static_prototype", "world/missing.m2"), null, "unknown static miss", failures)
_expect_false(state.call("mark_model_missing", ""), "empty missing path rejected", failures)
_expect_false(state.call("mark_animation_static", ""), "empty static-animation path rejected", failures)
var static_prototype := Node3D.new()
var animated_prototype := Node3D.new()
_expect_same(
state.call("adopt_static_prototype", "world/tree.m2", static_prototype),
static_prototype,
"static prototype adopted",
failures
)
_expect_same(
state.call("find_static_prototype", "world/tree.m2"),
static_prototype,
"static identity retained",
failures
)
_expect_same(
state.call("adopt_animated_prototype", "world/tree.m2", animated_prototype),
animated_prototype,
"animated prototype independent",
failures
)
var duplicate_static := Node3D.new()
_expect_same(
state.call("adopt_static_prototype", "world/tree.m2", duplicate_static),
static_prototype,
"first static prototype wins",
failures
)
_expect_false(is_instance_valid(duplicate_static), "duplicate detached prototype released", failures)
_expect_true(state.call("mark_model_missing", "world/missing.m2"), "missing path marked", failures)
_expect_true(state.call("is_model_missing", "world/missing.m2"), "missing path retained", failures)
_expect_true(state.call("mark_animation_static", "world/rock.m2"), "static animation marked", failures)
_expect_true(state.call("is_animation_static", "world/rock.m2"), "static animation retained", failures)
var snapshot: Dictionary = state.call("diagnostic_snapshot")
_expect_equal((snapshot["static_prototype_paths"] as Array).size(), 1, "one static path", failures)
_expect_equal((snapshot["animated_prototype_paths"] as Array).size(), 1, "one animated path", failures)
state.call("clear_and_release")
func _verify_release_ownership(failures: Array[String]) -> void:
var state: RefCounted = CACHE_STATE_SCRIPT.new()
var detached_static := Node3D.new()
var in_tree_animated := Node3D.new()
get_root().add_child(in_tree_animated)
state.call("adopt_static_prototype", "world/static.m2", detached_static)
state.call("adopt_animated_prototype", "world/animated.m2", in_tree_animated)
state.call("mark_model_missing", "world/missing.m2")
state.call("mark_animation_static", "world/static.m2")
state.call("clear_and_release")
state.call("clear_and_release")
_expect_false(is_instance_valid(detached_static), "detached prototype freed", failures)
_expect_true(
not is_instance_valid(in_tree_animated) or in_tree_animated.is_queued_for_deletion(),
"in-tree prototype queued or released by headless teardown",
failures
)
var snapshot: Dictionary = state.call("diagnostic_snapshot")
for state_key in snapshot.keys():
_expect_true((snapshot[state_key] as Array).is_empty(), "%s cleared" % state_key, failures)
func _verify_ownership_boundaries(failures: Array[String]) -> void:
var state_source := FileAccess.get_file_as_string(CACHE_STATE_PATH)
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
_expect_true(
loader_source.contains("M2_PROTOTYPE_CACHE_STATE_SCRIPT.new()"),
"loader composes prototype cache state",
failures
)
for removed_loader_field in [
"var _m2_scene_cache:",
"var _m2_animated_scene_cache:",
"var _m2_static_animation_cache:",
"var _m2_missing_cache:",
]:
_expect_false(loader_source.contains(removed_loader_field), "loader omits %s" % removed_loader_field, failures)
for required_loader_rule in [
"_m2_prototype_cache_state.find_static_prototype(",
"_m2_prototype_cache_state.adopt_static_prototype(",
"_m2_prototype_cache_state.find_animated_prototype(",
"_m2_prototype_cache_state.adopt_animated_prototype(",
"_m2_prototype_cache_state.mark_model_missing(",
"_m2_prototype_cache_state.mark_animation_static(",
"_m2_prototype_cache_state.clear_and_release()",
]:
_expect_true(loader_source.contains(required_loader_rule), "loader delegates %s" % required_loader_rule, failures)
for forbidden_dependency in [
"ResourceLoader.",
"FileAccess.",
"ClassDB.",
"M2Builder",
"WorkerThreadPool",
"MultiMesh",
"MeshInstance3D",
]:
_expect_false(state_source.contains(forbidden_dependency), "state omits %s" % forbidden_dependency, failures)
func _verify_bounded_timing(failures: Array[String]) -> float:
var started_microseconds := Time.get_ticks_usec()
for _iteration in range(100):
var state: RefCounted = CACHE_STATE_SCRIPT.new()
for path_index in range(256):
var normalized_path := "world/model_%d.m2" % path_index
state.call("mark_model_missing", normalized_path)
state.call("mark_animation_static", normalized_path)
state.call("is_model_missing", normalized_path)
state.call("is_animation_static", normalized_path)
state.call("clear_and_release")
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 path-state cycles 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://dkjneibuiqjss
+1 -1
View File
@@ -96,7 +96,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
for forbidden_dependency in [
"ResourceLoader.",
"M2_BUILDER",
"_m2_missing_cache",
"_m2_prototype_cache_state",
"Mesh",
"Node",
"WorkerThreadPool",
@@ -129,7 +129,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
"M2_RAW_MODEL_REPOSITORY_SCRIPT.new()",
"_m2_raw_model_repository.load_static_model_data(",
"_m2_mesh_resource_cache_state.store_mesh(",
"_m2_missing_cache[normalized_rel]",
"_m2_prototype_cache_state.mark_model_missing(normalized_rel)",
]:
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
for raw_repository_rule in [
@@ -152,7 +152,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
"ClassDB.",
"load_m2",
"_m2_mesh_resource_cache_state",
"_m2_missing_cache",
"_m2_prototype_cache_state",
"MultiMesh",
]:
_expect_false(
@@ -32,13 +32,14 @@ func _verify_runtime_cache_ownership() -> void:
var animated_m2_prototype := Node3D.new()
var wmo_prototype := Node3D.new()
loader.set("_m2_scene_cache", {"static": static_m2_prototype})
loader.set("_m2_animated_scene_cache", {"animated": animated_m2_prototype})
var m2_prototype_cache_state: RefCounted = loader.get("_m2_prototype_cache_state")
m2_prototype_cache_state.call("adopt_static_prototype", "static", static_m2_prototype)
m2_prototype_cache_state.call("adopt_animated_prototype", "animated", animated_m2_prototype)
loader.set("_wmo_prototype_cache", {"wmo": wmo_prototype})
var m2_mesh_resource_cache_state: RefCounted = loader.get("_m2_mesh_resource_cache_state")
m2_mesh_resource_cache_state.call("store_mesh", "mesh", ArrayMesh.new())
loader.set("_m2_static_animation_cache", {"static": true})
loader.set("_m2_missing_cache", {"missing": true})
m2_prototype_cache_state.call("mark_animation_static", "static")
m2_prototype_cache_state.call("mark_model_missing", "missing")
var wmo_render_cache_state: RefCounted = loader.get("_wmo_render_resource_cache_state")
wmo_render_cache_state.call("remember_request", "render", "res://render.res")
wmo_render_cache_state.call("complete_request_with_resource", "render", Resource.new())
@@ -56,12 +57,16 @@ func _verify_runtime_cache_ownership() -> void:
_expect(not is_instance_valid(static_m2_prototype), "static M2 prototype should be freed")
_expect(not is_instance_valid(animated_m2_prototype), "animated M2 prototype should be freed")
_expect(not is_instance_valid(wmo_prototype), "WMO prototype should be freed")
var m2_prototype_snapshot: Dictionary = m2_prototype_cache_state.call(
"diagnostic_snapshot"
)
for state_key in m2_prototype_snapshot.keys():
_expect(
(m2_prototype_snapshot[state_key] as Array).is_empty(),
"M2 prototype %s should be empty after shutdown" % state_key
)
for cache_name in [
"_m2_scene_cache",
"_m2_animated_scene_cache",
"_wmo_prototype_cache",
"_m2_static_animation_cache",
"_m2_missing_cache",
"_wmo_missing_cache",
"_shared_tex_cache",
]: