render: extract M2 animation resource finalizer
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
class_name M2AnimationResourceFinalizer
|
||||
extends RefCounted
|
||||
|
||||
## Polls cached animated M2 ResourceLoader requests and finalizes terminal scenes.
|
||||
## The caller owns permits, material-source lookup and SceneTree materialization.
|
||||
|
||||
const M2_ANIMATED_SCENE_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_animated_scene_finalizer.gd"
|
||||
)
|
||||
|
||||
var _animated_scene_finalizer: RefCounted
|
||||
var _resource_loader_adapter: Object
|
||||
|
||||
|
||||
func _init(
|
||||
animated_scene_finalizer: RefCounted = null,
|
||||
resource_loader_adapter: Object = null
|
||||
) -> void:
|
||||
_animated_scene_finalizer = animated_scene_finalizer
|
||||
if _animated_scene_finalizer == null:
|
||||
_animated_scene_finalizer = M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()
|
||||
_resource_loader_adapter = resource_loader_adapter
|
||||
|
||||
|
||||
## Moves terminal threaded requests into the pipeline finalize FIFO.
|
||||
## Empty resource paths retain the historical immediate static-only outcome.
|
||||
func poll_terminal_requests(
|
||||
load_pipeline_state: RefCounted,
|
||||
prototype_cache_state: RefCounted
|
||||
) -> int:
|
||||
if load_pipeline_state == null or prototype_cache_state == null:
|
||||
return 0
|
||||
var completed_request_count := 0
|
||||
var request_records: Array = load_pipeline_state.call(
|
||||
"request_records_snapshot"
|
||||
)
|
||||
for request_variant in request_records:
|
||||
if not (request_variant is Dictionary):
|
||||
continue
|
||||
var request: Dictionary = request_variant
|
||||
var normalized_relative_path := String(request.get("normalized", ""))
|
||||
var resource_path := String(request.get("path", ""))
|
||||
if resource_path.is_empty():
|
||||
load_pipeline_state.call(
|
||||
"discard_request",
|
||||
normalized_relative_path
|
||||
)
|
||||
prototype_cache_state.call(
|
||||
"mark_animation_static",
|
||||
normalized_relative_path
|
||||
)
|
||||
completed_request_count += 1
|
||||
continue
|
||||
var load_status := load_threaded_get_status(resource_path)
|
||||
if (
|
||||
load_status != ResourceLoader.THREAD_LOAD_LOADED
|
||||
and load_status != ResourceLoader.THREAD_LOAD_FAILED
|
||||
):
|
||||
continue
|
||||
load_pipeline_state.call(
|
||||
"complete_request",
|
||||
normalized_relative_path,
|
||||
load_status
|
||||
)
|
||||
completed_request_count += 1
|
||||
return completed_request_count
|
||||
|
||||
|
||||
## Pops one terminal record and prepares a detached animated scene candidate.
|
||||
## An empty result means the record was skipped or resolved as static-only.
|
||||
func prepare_next_candidate(
|
||||
load_pipeline_state: RefCounted,
|
||||
prototype_cache_state: RefCounted
|
||||
) -> Dictionary:
|
||||
if load_pipeline_state == null or prototype_cache_state == null:
|
||||
return {}
|
||||
var terminal_record: Dictionary = load_pipeline_state.call(
|
||||
"pop_finalize_record"
|
||||
)
|
||||
if terminal_record.is_empty():
|
||||
return {}
|
||||
var normalized_relative_path := String(
|
||||
terminal_record.get("normalized", "")
|
||||
)
|
||||
if (
|
||||
normalized_relative_path.is_empty()
|
||||
or bool(prototype_cache_state.call(
|
||||
"has_animated_prototype",
|
||||
normalized_relative_path
|
||||
))
|
||||
or bool(prototype_cache_state.call(
|
||||
"is_animation_static",
|
||||
normalized_relative_path
|
||||
))
|
||||
):
|
||||
return {}
|
||||
if int(terminal_record.get(
|
||||
"status",
|
||||
ResourceLoader.THREAD_LOAD_FAILED
|
||||
)) != ResourceLoader.THREAD_LOAD_LOADED:
|
||||
prototype_cache_state.call(
|
||||
"mark_animation_static",
|
||||
normalized_relative_path
|
||||
)
|
||||
return {}
|
||||
|
||||
var resource_path := String(terminal_record.get("path", ""))
|
||||
var loaded_resource := load_threaded_get(resource_path)
|
||||
var candidate := _animated_scene_finalizer.call(
|
||||
"instantiate_candidate",
|
||||
loaded_resource
|
||||
) as Node3D
|
||||
if candidate == null:
|
||||
prototype_cache_state.call(
|
||||
"mark_animation_static",
|
||||
normalized_relative_path
|
||||
)
|
||||
return {}
|
||||
return {
|
||||
"normalized": normalized_relative_path,
|
||||
"path": resource_path,
|
||||
"candidate": candidate,
|
||||
}
|
||||
|
||||
|
||||
## Repairs, validates and adopts one prepared candidate. The preparation must be
|
||||
## completed synchronously so its detached candidate always receives an owner.
|
||||
func finalize_prepared_candidate(
|
||||
preparation: Dictionary,
|
||||
material_source_root: Node3D,
|
||||
prototype_cache_state: RefCounted,
|
||||
debug_logging_enabled: bool = false
|
||||
) -> Node3D:
|
||||
if prototype_cache_state == null:
|
||||
return null
|
||||
var normalized_relative_path := String(preparation.get("normalized", ""))
|
||||
var candidate := preparation.get("candidate", null) as Node3D
|
||||
if normalized_relative_path.is_empty() or candidate == null:
|
||||
return null
|
||||
_animated_scene_finalizer.call(
|
||||
"repair_materials",
|
||||
candidate,
|
||||
material_source_root
|
||||
)
|
||||
var finalization: Dictionary = _animated_scene_finalizer.call(
|
||||
"finalize_candidate",
|
||||
candidate
|
||||
)
|
||||
var prototype := finalization.get("prototype", null) as Node3D
|
||||
if prototype == null:
|
||||
prototype_cache_state.call(
|
||||
"mark_animation_static",
|
||||
normalized_relative_path
|
||||
)
|
||||
return null
|
||||
var canonical_prototype := prototype_cache_state.call(
|
||||
"adopt_animated_prototype",
|
||||
normalized_relative_path,
|
||||
prototype
|
||||
) as Node3D
|
||||
if debug_logging_enabled:
|
||||
print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [
|
||||
normalized_relative_path,
|
||||
String(preparation.get("path", "")),
|
||||
int(finalization.get("animation_player_count", 0)),
|
||||
])
|
||||
return canonical_prototype
|
||||
|
||||
|
||||
## Production ResourceLoader status adapter; injectable in synthetic tests.
|
||||
func load_threaded_get_status(resource_path: String) -> int:
|
||||
if _resource_loader_adapter != null:
|
||||
return int(_resource_loader_adapter.call(
|
||||
"load_threaded_get_status",
|
||||
resource_path
|
||||
))
|
||||
return ResourceLoader.load_threaded_get_status(resource_path)
|
||||
|
||||
|
||||
## Production ResourceLoader terminal-result adapter; injectable in tests.
|
||||
func load_threaded_get(resource_path: String) -> Resource:
|
||||
if _resource_loader_adapter != null:
|
||||
return _resource_loader_adapter.call(
|
||||
"load_threaded_get",
|
||||
resource_path
|
||||
) as Resource
|
||||
return ResourceLoader.load_threaded_get(resource_path)
|
||||
@@ -0,0 +1 @@
|
||||
uid://2rnsjucg3dul
|
||||
@@ -96,6 +96,9 @@ const M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT := preload(
|
||||
const M2_ANIMATED_SCENE_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_animated_scene_finalizer.gd"
|
||||
)
|
||||
const M2_ANIMATION_RESOURCE_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_animation_resource_finalizer.gd"
|
||||
)
|
||||
const M2_ANIMATED_INSTANCE_MATERIALIZER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_animated_instance_materializer.gd"
|
||||
)
|
||||
@@ -309,6 +312,9 @@ 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_pipeline_state := M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT.new()
|
||||
var _m2_animated_scene_finalizer := M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()
|
||||
var _m2_animation_resource_finalizer := M2_ANIMATION_RESOURCE_FINALIZER_SCRIPT.new(
|
||||
_m2_animated_scene_finalizer
|
||||
)
|
||||
var _m2_animated_instance_materializer := M2_ANIMATED_INSTANCE_MATERIALIZER_SCRIPT.new()
|
||||
var _wmo_build_jobs: Dictionary = {}
|
||||
var _wmo_build_queue: Array = []
|
||||
@@ -4238,50 +4244,27 @@ func _drain_m2_group_results() -> void:
|
||||
|
||||
|
||||
func _drain_m2_animation_loads() -> void:
|
||||
for pending in _m2_animation_load_pipeline_state.request_records_snapshot():
|
||||
var normalized_rel := String(pending.get("normalized", ""))
|
||||
var path := String(pending.get("path", ""))
|
||||
if path.is_empty():
|
||||
_m2_animation_load_pipeline_state.discard_request(normalized_rel)
|
||||
_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:
|
||||
continue
|
||||
_m2_animation_load_pipeline_state.complete_request(normalized_rel, status)
|
||||
_m2_animation_resource_finalizer.poll_terminal_requests(
|
||||
_m2_animation_load_pipeline_state,
|
||||
_m2_prototype_cache_state
|
||||
)
|
||||
|
||||
while _m2_animation_load_pipeline_state.has_finalize_record() and _render_budget_scheduler.try_consume_permit(
|
||||
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE):
|
||||
var pending: Dictionary = _m2_animation_load_pipeline_state.pop_finalize_record()
|
||||
var normalized_rel := String(pending.get("normalized", ""))
|
||||
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)
|
||||
):
|
||||
var preparation := _m2_animation_resource_finalizer.prepare_next_candidate(
|
||||
_m2_animation_load_pipeline_state,
|
||||
_m2_prototype_cache_state
|
||||
)
|
||||
if preparation.is_empty():
|
||||
continue
|
||||
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
|
||||
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||
continue
|
||||
|
||||
var path := String(pending.get("path", ""))
|
||||
var resource: Resource = ResourceLoader.load_threaded_get(path)
|
||||
var candidate := _m2_animated_scene_finalizer.instantiate_candidate(resource)
|
||||
if candidate != null:
|
||||
var material_source := _get_or_load_m2_material_prototype(normalized_rel)
|
||||
_m2_animated_scene_finalizer.repair_materials(candidate, material_source)
|
||||
var finalization := _m2_animated_scene_finalizer.finalize_candidate(candidate)
|
||||
var prototype: Node3D = finalization.get("prototype", null)
|
||||
if prototype != null:
|
||||
_m2_prototype_cache_state.adopt_animated_prototype(normalized_rel, prototype)
|
||||
if debug_streaming:
|
||||
print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [
|
||||
normalized_rel,
|
||||
path,
|
||||
int(finalization.get("animation_player_count", 0)),
|
||||
])
|
||||
continue
|
||||
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||
var normalized_rel := String(preparation.get("normalized", ""))
|
||||
var material_source := _get_or_load_m2_material_prototype(normalized_rel)
|
||||
_m2_animation_resource_finalizer.finalize_prepared_candidate(
|
||||
preparation,
|
||||
material_source,
|
||||
_m2_prototype_cache_state,
|
||||
debug_streaming
|
||||
)
|
||||
|
||||
|
||||
func _drain_m2_mesh_loads() -> void:
|
||||
|
||||
@@ -5,6 +5,9 @@ extends SceneTree
|
||||
|
||||
const FINALIZER_SCRIPT := preload("res://src/render/m2/m2_animated_scene_finalizer.gd")
|
||||
const FINALIZER_PATH := "res://src/render/m2/m2_animated_scene_finalizer.gd"
|
||||
const RESOURCE_FINALIZER_PATH := (
|
||||
"res://src/render/m2/m2_animation_resource_finalizer.gd"
|
||||
)
|
||||
const MATERIALIZER_PATH := "res://src/render/m2/m2_animated_instance_materializer.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
@@ -116,6 +119,9 @@ func _verify_material_mapping(failures: Array[String]) -> void:
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH)
|
||||
var resource_finalizer_source := FileAccess.get_file_as_string(
|
||||
RESOURCE_FINALIZER_PATH
|
||||
)
|
||||
var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(loader_source.contains("M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()"), "loader composes finalizer", failures)
|
||||
@@ -126,27 +132,41 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
]:
|
||||
_expect_false(loader_source.contains(removed_loader_function), "legacy helper removed: %s" % removed_loader_function, failures)
|
||||
for delegated_call in [
|
||||
"_m2_animated_scene_finalizer.instantiate_candidate(resource)",
|
||||
"_m2_animated_scene_finalizer.repair_materials(candidate, material_source)",
|
||||
"_m2_animated_scene_finalizer.finalize_candidate(candidate)",
|
||||
"_m2_animated_scene_finalizer.mesh_instances_in_subtree(root)",
|
||||
"\"instantiate_candidate\"",
|
||||
"\"repair_materials\"",
|
||||
"\"finalize_candidate\"",
|
||||
]:
|
||||
_expect_equal(loader_source.count(delegated_call), 1, "single loader delegation: %s" % delegated_call, failures)
|
||||
_expect_equal(
|
||||
resource_finalizer_source.count(delegated_call),
|
||||
1,
|
||||
"single resource-finalizer delegation: %s" % delegated_call,
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_animated_scene_finalizer.mesh_instances_in_subtree(root)"),
|
||||
1,
|
||||
"single loader mesh traversal delegation",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
materializer_source.count("_animated_scene_finalizer.animation_players_in_subtree("),
|
||||
1,
|
||||
"single materializer player-inventory delegation",
|
||||
failures
|
||||
)
|
||||
for retained_loader_rule in [
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
for retained_renderer_rule in [
|
||||
"ResourceLoader.load_threaded_get(resource_path)",
|
||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE",
|
||||
"_get_or_load_m2_material_prototype(normalized_rel)",
|
||||
"_m2_prototype_cache_state.adopt_animated_prototype(",
|
||||
"_m2_prototype_cache_state.mark_animation_static(normalized_rel)",
|
||||
"\"adopt_animated_prototype\"",
|
||||
"\"mark_animation_static\"",
|
||||
"M2_ANIM_CACHE path=%s cache=%s players=%d",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
_expect_true(
|
||||
(loader_source + resource_finalizer_source).contains(retained_renderer_rule),
|
||||
"renderer retains %s" % retained_renderer_rule,
|
||||
failures
|
||||
)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
|
||||
@@ -8,6 +8,9 @@ const PIPELINE_PATH := "res://src/render/m2/m2_animation_load_pipeline_state.gd"
|
||||
const OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
|
||||
)
|
||||
const RESOURCE_FINALIZER_PATH := (
|
||||
"res://src/render/m2/m2_animation_resource_finalizer.gd"
|
||||
)
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
@@ -84,6 +87,9 @@ func _verify_discard_metrics_clear_and_diagnostics(failures: Array[String]) -> v
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var pipeline_source := FileAccess.get_file_as_string(PIPELINE_PATH)
|
||||
var observer_source := FileAccess.get_file_as_string(OBSERVER_PATH)
|
||||
var resource_finalizer_source := FileAccess.get_file_as_string(
|
||||
RESOURCE_FINALIZER_PATH
|
||||
)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(loader_source.contains("M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT.new()"), "loader composes pipeline state", failures)
|
||||
_expect_false(loader_source.contains("var _m2_animation_load_requests:"), "legacy request field removed", failures)
|
||||
@@ -95,14 +101,18 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"cached observer owns request admission",
|
||||
failures
|
||||
)
|
||||
for retained_loader_rule in [
|
||||
"ResourceLoader.load_threaded_get_status(path)",
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
for retained_renderer_rule in [
|
||||
"ResourceLoader.load_threaded_get_status(resource_path)",
|
||||
"ResourceLoader.load_threaded_get(resource_path)",
|
||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE",
|
||||
"_m2_prototype_cache_state.adopt_animated_prototype(",
|
||||
"_m2_prototype_cache_state.mark_animation_static(",
|
||||
"\"adopt_animated_prototype\"",
|
||||
"\"mark_animation_static\"",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
_expect_true(
|
||||
(loader_source + resource_finalizer_source).contains(retained_renderer_rule),
|
||||
"renderer retains %s" % retained_renderer_rule,
|
||||
failures
|
||||
)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"WorkerThreadPool.",
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
extends SceneTree
|
||||
|
||||
const FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_animation_resource_finalizer.gd"
|
||||
)
|
||||
const PIPELINE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_animation_load_pipeline_state.gd"
|
||||
)
|
||||
const PROTOTYPE_CACHE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_prototype_cache_state.gd"
|
||||
)
|
||||
const FINALIZER_PATH := "res://src/render/m2/m2_animation_resource_finalizer.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
class FakeResourceLoaderAdapter extends RefCounted:
|
||||
var statuses: Dictionary = {}
|
||||
var resources: Dictionary = {}
|
||||
var status_paths: Array[String] = []
|
||||
var load_paths: Array[String] = []
|
||||
|
||||
func load_threaded_get_status(resource_path: String) -> int:
|
||||
status_paths.append(resource_path)
|
||||
return int(statuses.get(
|
||||
resource_path,
|
||||
ResourceLoader.THREAD_LOAD_IN_PROGRESS
|
||||
))
|
||||
|
||||
func load_threaded_get(resource_path: String) -> Resource:
|
||||
load_paths.append(resource_path)
|
||||
return resources.get(resource_path, null) as Resource
|
||||
|
||||
|
||||
class FakeAnimatedSceneFinalizer extends RefCounted:
|
||||
var candidate_by_resource: Dictionary = {}
|
||||
var finalization_by_candidate: Dictionary = {}
|
||||
var instantiated_resources: Array[Resource] = []
|
||||
var repaired_candidates: Array[Node3D] = []
|
||||
var repaired_material_sources: Array[Node3D] = []
|
||||
|
||||
func instantiate_candidate(resource: Resource) -> Node3D:
|
||||
instantiated_resources.append(resource)
|
||||
return candidate_by_resource.get(resource, null) as Node3D
|
||||
|
||||
func repair_materials(candidate: Node3D, material_source: Node3D) -> void:
|
||||
repaired_candidates.append(candidate)
|
||||
repaired_material_sources.append(material_source)
|
||||
|
||||
func finalize_candidate(candidate: Node3D) -> Dictionary:
|
||||
return finalization_by_candidate.get(candidate, {}) as Dictionary
|
||||
|
||||
|
||||
class EmptyPathPipeline extends RefCounted:
|
||||
var discarded_path: String = ""
|
||||
|
||||
func request_records_snapshot() -> Array[Dictionary]:
|
||||
return [{"normalized": "world/empty.m2", "path": ""}]
|
||||
|
||||
func discard_request(normalized_relative_path: String) -> bool:
|
||||
discarded_path = normalized_relative_path
|
||||
return true
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_polling(failures)
|
||||
_verify_empty_path_polling(failures)
|
||||
_verify_terminal_rejections(failures)
|
||||
_verify_preparation_and_adoption(failures)
|
||||
_verify_finalize_rejection(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_ANIMATION_RESOURCE_FINALIZER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_ANIMATION_RESOURCE_FINALIZER PASS "
|
||||
+ "cases=28 iterations=1000 elapsed_ms=%.3f" % elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_polling(failures: Array[String]) -> void:
|
||||
var resource_loader := FakeResourceLoaderAdapter.new()
|
||||
var scene_finalizer := FakeAnimatedSceneFinalizer.new()
|
||||
var service: RefCounted = FINALIZER_SCRIPT.new(scene_finalizer, resource_loader)
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
for request in [
|
||||
["world/pending.m2", "res://pending.tscn"],
|
||||
["world/loaded.m2", "res://loaded.tscn"],
|
||||
["world/failed.m2", "res://failed.tscn"],
|
||||
]:
|
||||
pipeline.call("remember_request", request[0], request[1])
|
||||
resource_loader.statuses = {
|
||||
"res://pending.tscn": ResourceLoader.THREAD_LOAD_IN_PROGRESS,
|
||||
"res://loaded.tscn": ResourceLoader.THREAD_LOAD_LOADED,
|
||||
"res://failed.tscn": ResourceLoader.THREAD_LOAD_FAILED,
|
||||
}
|
||||
_expect_equal(
|
||||
int(service.call("poll_terminal_requests", pipeline, prototype_cache)),
|
||||
2,
|
||||
"two terminal requests completed",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
int(pipeline.call("pending_request_count")),
|
||||
1,
|
||||
"pending request retained",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
int(pipeline.call("finalize_record_count")),
|
||||
2,
|
||||
"terminal FIFO receives two records",
|
||||
failures
|
||||
)
|
||||
_expect_string_array(
|
||||
resource_loader.status_paths,
|
||||
["res://pending.tscn", "res://loaded.tscn", "res://failed.tscn"],
|
||||
"status polling insertion order",
|
||||
failures
|
||||
)
|
||||
prototype_cache.call("clear_and_release")
|
||||
|
||||
|
||||
func _verify_empty_path_polling(failures: Array[String]) -> void:
|
||||
var resource_loader := FakeResourceLoaderAdapter.new()
|
||||
var service: RefCounted = FINALIZER_SCRIPT.new(
|
||||
FakeAnimatedSceneFinalizer.new(),
|
||||
resource_loader
|
||||
)
|
||||
var pipeline := EmptyPathPipeline.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
_expect_equal(
|
||||
int(service.call("poll_terminal_requests", pipeline, prototype_cache)),
|
||||
1,
|
||||
"empty resource path completed",
|
||||
failures
|
||||
)
|
||||
_expect_string_equal(
|
||||
pipeline.discarded_path,
|
||||
"world/empty.m2",
|
||||
"empty resource path discarded",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(prototype_cache.call("is_animation_static", "world/empty.m2")),
|
||||
"empty resource path marks static",
|
||||
failures
|
||||
)
|
||||
_expect_equal(resource_loader.status_paths.size(), 0, "empty path skips I/O", failures)
|
||||
prototype_cache.call("clear_and_release")
|
||||
|
||||
|
||||
func _verify_terminal_rejections(failures: Array[String]) -> void:
|
||||
var resource_loader := FakeResourceLoaderAdapter.new()
|
||||
var scene_finalizer := FakeAnimatedSceneFinalizer.new()
|
||||
var service: RefCounted = FINALIZER_SCRIPT.new(scene_finalizer, resource_loader)
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
_expect_dictionary_empty(
|
||||
service.call("prepare_next_candidate", pipeline, prototype_cache),
|
||||
"empty finalize FIFO",
|
||||
failures
|
||||
)
|
||||
_enqueue_terminal(
|
||||
pipeline,
|
||||
"world/failed.m2",
|
||||
"res://failed.tscn",
|
||||
ResourceLoader.THREAD_LOAD_FAILED
|
||||
)
|
||||
_expect_dictionary_empty(
|
||||
service.call("prepare_next_candidate", pipeline, prototype_cache),
|
||||
"failed terminal record rejects",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(prototype_cache.call("is_animation_static", "world/failed.m2")),
|
||||
"failed terminal record marks static",
|
||||
failures
|
||||
)
|
||||
var cached := Node3D.new()
|
||||
prototype_cache.call("adopt_animated_prototype", "world/cached.m2", cached)
|
||||
_enqueue_terminal(
|
||||
pipeline,
|
||||
"world/cached.m2",
|
||||
"res://cached.tscn",
|
||||
ResourceLoader.THREAD_LOAD_LOADED
|
||||
)
|
||||
_expect_dictionary_empty(
|
||||
service.call("prepare_next_candidate", pipeline, prototype_cache),
|
||||
"cached terminal record skipped",
|
||||
failures
|
||||
)
|
||||
prototype_cache.call("mark_animation_static", "world/static.m2")
|
||||
_enqueue_terminal(
|
||||
pipeline,
|
||||
"world/static.m2",
|
||||
"res://static.tscn",
|
||||
ResourceLoader.THREAD_LOAD_LOADED
|
||||
)
|
||||
_expect_dictionary_empty(
|
||||
service.call("prepare_next_candidate", pipeline, prototype_cache),
|
||||
"static terminal record skipped",
|
||||
failures
|
||||
)
|
||||
_enqueue_terminal(pipeline, "world/null.m2", "res://null.tscn", ResourceLoader.THREAD_LOAD_LOADED)
|
||||
_expect_dictionary_empty(
|
||||
service.call("prepare_next_candidate", pipeline, prototype_cache),
|
||||
"null Resource candidate rejects",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(prototype_cache.call("is_animation_static", "world/null.m2")),
|
||||
"null Resource candidate marks static",
|
||||
failures
|
||||
)
|
||||
_expect_string_array(
|
||||
resource_loader.load_paths,
|
||||
["res://null.tscn"],
|
||||
"only uncached loaded record performs terminal get",
|
||||
failures
|
||||
)
|
||||
prototype_cache.call("clear_and_release")
|
||||
|
||||
|
||||
func _verify_preparation_and_adoption(failures: Array[String]) -> void:
|
||||
var resource_loader := FakeResourceLoaderAdapter.new()
|
||||
var scene_finalizer := FakeAnimatedSceneFinalizer.new()
|
||||
var service: RefCounted = FINALIZER_SCRIPT.new(scene_finalizer, resource_loader)
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
var packed_scene := PackedScene.new()
|
||||
var candidate := Node3D.new()
|
||||
var prototype := Node3D.new()
|
||||
var material_source := Node3D.new()
|
||||
resource_loader.resources["res://animated.tscn"] = packed_scene
|
||||
scene_finalizer.candidate_by_resource[packed_scene] = candidate
|
||||
scene_finalizer.finalization_by_candidate[candidate] = {
|
||||
"prototype": prototype,
|
||||
"animation_player_count": 3,
|
||||
}
|
||||
_enqueue_terminal(
|
||||
pipeline,
|
||||
"world/animated.m2",
|
||||
"res://animated.tscn",
|
||||
ResourceLoader.THREAD_LOAD_LOADED
|
||||
)
|
||||
var preparation: Dictionary = service.call(
|
||||
"prepare_next_candidate",
|
||||
pipeline,
|
||||
prototype_cache
|
||||
)
|
||||
_expect_same(preparation.get("candidate"), candidate, "candidate identity", failures)
|
||||
_expect_string_equal(
|
||||
String(preparation.get("normalized", "")),
|
||||
"world/animated.m2",
|
||||
"prepared normalized path",
|
||||
failures
|
||||
)
|
||||
_expect_string_equal(
|
||||
String(preparation.get("path", "")),
|
||||
"res://animated.tscn",
|
||||
"prepared Resource path",
|
||||
failures
|
||||
)
|
||||
var adopted: Node3D = service.call(
|
||||
"finalize_prepared_candidate",
|
||||
preparation,
|
||||
material_source,
|
||||
prototype_cache,
|
||||
false
|
||||
)
|
||||
_expect_same(adopted, prototype, "adopted prototype identity", failures)
|
||||
_expect_same(
|
||||
prototype_cache.call("find_animated_prototype", "world/animated.m2"),
|
||||
prototype,
|
||||
"prototype cache adoption",
|
||||
failures
|
||||
)
|
||||
_expect_same(scene_finalizer.repaired_candidates[0], candidate, "repair candidate", failures)
|
||||
_expect_same(
|
||||
scene_finalizer.repaired_material_sources[0],
|
||||
material_source,
|
||||
"repair material source",
|
||||
failures
|
||||
)
|
||||
prototype_cache.call("clear_and_release")
|
||||
material_source.free()
|
||||
candidate.free()
|
||||
|
||||
|
||||
func _verify_finalize_rejection(failures: Array[String]) -> void:
|
||||
var scene_finalizer := FakeAnimatedSceneFinalizer.new()
|
||||
var service: RefCounted = FINALIZER_SCRIPT.new(
|
||||
scene_finalizer,
|
||||
FakeResourceLoaderAdapter.new()
|
||||
)
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
var candidate := Node3D.new()
|
||||
_expect_null(
|
||||
service.call(
|
||||
"finalize_prepared_candidate",
|
||||
{"normalized": "world/rejected.m2", "candidate": candidate},
|
||||
null,
|
||||
prototype_cache
|
||||
),
|
||||
"rejected finalization returns null",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(prototype_cache.call("is_animation_static", "world/rejected.m2")),
|
||||
"rejected finalization marks static",
|
||||
failures
|
||||
)
|
||||
candidate.free()
|
||||
prototype_cache.call("clear_and_release")
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
var service_source := FileAccess.get_file_as_string(FINALIZER_PATH)
|
||||
var drain_start := loader_source.find("func _drain_m2_animation_loads()")
|
||||
var drain_end := loader_source.find("func _drain_m2_mesh_loads()", drain_start)
|
||||
var drain_source := loader_source.substr(drain_start, drain_end - drain_start)
|
||||
_expect_true(
|
||||
drain_source.contains("_m2_animation_resource_finalizer.poll_terminal_requests("),
|
||||
"loader delegates polling",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
drain_source.contains("_m2_animation_resource_finalizer.prepare_next_candidate("),
|
||||
"loader delegates candidate preparation",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
drain_source.contains("_m2_animation_resource_finalizer.finalize_prepared_candidate("),
|
||||
"loader delegates finalization",
|
||||
failures
|
||||
)
|
||||
for removed_token in [
|
||||
"ResourceLoader.load_threaded_get_status",
|
||||
"ResourceLoader.load_threaded_get(",
|
||||
"_m2_animated_scene_finalizer.instantiate_candidate",
|
||||
"_m2_animated_scene_finalizer.repair_materials",
|
||||
"_m2_animated_scene_finalizer.finalize_candidate",
|
||||
"M2_ANIM_CACHE path=",
|
||||
]:
|
||||
_expect_false(drain_source.contains(removed_token), "loader omits %s" % removed_token, failures)
|
||||
for required_token in [
|
||||
"ResourceLoader.load_threaded_get_status",
|
||||
"ResourceLoader.load_threaded_get(",
|
||||
"\"instantiate_candidate\"",
|
||||
"\"repair_materials\"",
|
||||
"\"finalize_candidate\"",
|
||||
"M2_ANIM_CACHE path=%s cache=%s players=%d",
|
||||
]:
|
||||
_expect_true(
|
||||
service_source.contains(required_token),
|
||||
"service owns %s" % required_token,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var service: RefCounted = FINALIZER_SCRIPT.new(
|
||||
FakeAnimatedSceneFinalizer.new(),
|
||||
FakeResourceLoaderAdapter.new()
|
||||
)
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(1000):
|
||||
service.call("poll_terminal_requests", pipeline, prototype_cache)
|
||||
var elapsed_milliseconds := float(
|
||||
Time.get_ticks_usec() - started_microseconds
|
||||
) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "empty polling remains bounded", failures)
|
||||
prototype_cache.call("clear_and_release")
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _enqueue_terminal(
|
||||
pipeline: RefCounted,
|
||||
normalized_relative_path: String,
|
||||
resource_path: String,
|
||||
status: int
|
||||
) -> void:
|
||||
pipeline.call("remember_request", normalized_relative_path, resource_path)
|
||||
pipeline.call("complete_request", normalized_relative_path, status)
|
||||
|
||||
|
||||
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_null(actual: Variant, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(actual == null, label, failures)
|
||||
|
||||
|
||||
func _expect_same(
|
||||
actual: Variant,
|
||||
expected: Variant,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
_expect_true(is_same(actual, expected), label, failures)
|
||||
|
||||
|
||||
func _expect_dictionary_empty(
|
||||
actual: Variant,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
_expect_true(actual is Dictionary and (actual as Dictionary).is_empty(), 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_string_equal(
|
||||
actual: String,
|
||||
expected: String,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_string_array(
|
||||
actual: Array[String],
|
||||
expected: Array[String],
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
||||
@@ -0,0 +1 @@
|
||||
uid://dh16lkxak8dan
|
||||
@@ -10,6 +10,9 @@ const NATIVE_OBSERVER_PATH := (
|
||||
const CACHED_OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
|
||||
)
|
||||
const RESOURCE_FINALIZER_PATH := (
|
||||
"res://src/render/m2/m2_animation_resource_finalizer.gd"
|
||||
)
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
@@ -102,6 +105,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var observer_sources := (
|
||||
FileAccess.get_file_as_string(NATIVE_OBSERVER_PATH)
|
||||
+ FileAccess.get_file_as_string(CACHED_OBSERVER_PATH)
|
||||
+ FileAccess.get_file_as_string(RESOURCE_FINALIZER_PATH)
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("M2_PROTOTYPE_CACHE_STATE_SCRIPT.new()"),
|
||||
@@ -119,9 +123,9 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"_m2_prototype_cache_state.find_static_prototype(",
|
||||
"_m2_prototype_cache_state.adopt_static_prototype(",
|
||||
"\"find_animated_prototype\"",
|
||||
"_m2_prototype_cache_state.adopt_animated_prototype(",
|
||||
"\"adopt_animated_prototype\"",
|
||||
"_m2_prototype_cache_state.mark_model_missing(",
|
||||
"_m2_prototype_cache_state.mark_animation_static(",
|
||||
"\"mark_animation_static\"",
|
||||
"_m2_prototype_cache_state.clear_and_release()",
|
||||
]:
|
||||
_expect_true(
|
||||
|
||||
Reference in New Issue
Block a user