refactor(M03): extract M2 prototype cache state
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]:
|
||||
|
||||
Reference in New Issue
Block a user