render: extract WMO scene resource finalizer
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free terminal-I/O, probe validation/lifetime, adoption, boundary and
|
||||
## timing regression for the cached WMO PackedScene finalizer.
|
||||
|
||||
const FINALIZER_SCRIPT := preload("res://src/render/wmo/wmo_scene_resource_finalizer.gd")
|
||||
const CACHE_STATE_SCRIPT := preload("res://src/render/wmo/wmo_scene_resource_cache_state.gd")
|
||||
const FINALIZER_PATH := "res://src/render/wmo/wmo_scene_resource_finalizer.gd"
|
||||
const CACHE_STATE_PATH := "res://src/render/wmo/wmo_scene_resource_cache_state.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
class FakeSceneCacheValidator extends RefCounted:
|
||||
var validated_nodes: Array[Node] = []
|
||||
|
||||
func is_scene_cache_current(node: Node) -> bool:
|
||||
validated_nodes.append(node)
|
||||
return bool(node.get_meta("current_wmo_cache", false))
|
||||
|
||||
|
||||
class FakeResourceLoaderAdapter extends RefCounted:
|
||||
var status_by_path: Dictionary = {}
|
||||
var resource_by_path: Dictionary = {}
|
||||
var status_paths: Array[String] = []
|
||||
var loaded_paths: Array[String] = []
|
||||
|
||||
func load_threaded_get_status(resource_path: String) -> int:
|
||||
status_paths.append(resource_path)
|
||||
return int(status_by_path.get(resource_path, ResourceLoader.THREAD_LOAD_IN_PROGRESS))
|
||||
|
||||
func load_threaded_get(resource_path: String) -> Resource:
|
||||
loaded_paths.append(resource_path)
|
||||
return resource_by_path.get(resource_path) as Resource
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_null_and_non_terminal_poll(failures)
|
||||
_verify_failed_and_loaded_poll_order(failures)
|
||||
_verify_loaded_resource_rejections(failures)
|
||||
_verify_current_scene_adoption_and_probe_lifetime(failures)
|
||||
_verify_validation_contract(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("WMO_SCENE_RESOURCE_FINALIZER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"WMO_SCENE_RESOURCE_FINALIZER PASS cases=26 iterations=1000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_null_and_non_terminal_poll(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var adapter := FakeResourceLoaderAdapter.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter)
|
||||
_expect_equal(
|
||||
int(finalizer.call("poll_terminal_requests", null)),
|
||||
0,
|
||||
"null cache state rejected",
|
||||
failures
|
||||
)
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
cache_state.call("remember_request", "world/pending.wmo", "res://cache/pending.tscn")
|
||||
_expect_equal(
|
||||
int(finalizer.call("poll_terminal_requests", cache_state)),
|
||||
0,
|
||||
"non-terminal request retained",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(cache_state.call("has_request", "world/pending.wmo")),
|
||||
"pending state remains",
|
||||
failures
|
||||
)
|
||||
_expect_string_array(
|
||||
adapter.status_paths,
|
||||
["res://cache/pending.tscn"],
|
||||
"pending path polled once",
|
||||
failures
|
||||
)
|
||||
_expect_equal(adapter.loaded_paths.size(), 0, "pending scene not loaded", failures)
|
||||
|
||||
|
||||
func _verify_failed_and_loaded_poll_order(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var adapter := FakeResourceLoaderAdapter.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter)
|
||||
var current_scene := _packed_scene(true, true, failures)
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
var requests := {
|
||||
"world/pending.wmo": "res://cache/pending.tscn",
|
||||
"world/failed.wmo": "res://cache/failed.tscn",
|
||||
"world/loaded.wmo": "res://cache/loaded.tscn",
|
||||
}
|
||||
for normalized_relative_path in requests:
|
||||
cache_state.call(
|
||||
"remember_request",
|
||||
normalized_relative_path,
|
||||
requests[normalized_relative_path]
|
||||
)
|
||||
adapter.status_by_path = {
|
||||
"res://cache/failed.tscn": ResourceLoader.THREAD_LOAD_FAILED,
|
||||
"res://cache/loaded.tscn": ResourceLoader.THREAD_LOAD_LOADED,
|
||||
}
|
||||
adapter.resource_by_path["res://cache/loaded.tscn"] = current_scene
|
||||
_expect_equal(
|
||||
int(finalizer.call("poll_terminal_requests", cache_state)),
|
||||
2,
|
||||
"failed and loaded scenes complete",
|
||||
failures
|
||||
)
|
||||
_expect_string_array(
|
||||
adapter.status_paths,
|
||||
[
|
||||
"res://cache/pending.tscn",
|
||||
"res://cache/failed.tscn",
|
||||
"res://cache/loaded.tscn",
|
||||
],
|
||||
"detached insertion order preserved",
|
||||
failures
|
||||
)
|
||||
_expect_string_array(
|
||||
adapter.loaded_paths,
|
||||
["res://cache/loaded.tscn"],
|
||||
"only loaded terminal scene retrieved",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(cache_state.call("is_missing", "world/failed.wmo")),
|
||||
"failed request publishes missing",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
cache_state.call("scene_for", "world/loaded.wmo") == current_scene,
|
||||
"loaded scene identity adopted",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_loaded_resource_rejections(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var adapter := FakeResourceLoaderAdapter.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter)
|
||||
var rejected_resources := {
|
||||
"null": null,
|
||||
"wrong_type": Resource.new(),
|
||||
"stale": _packed_scene(false, true, failures),
|
||||
"non_node_3d": _packed_scene(true, false, failures),
|
||||
}
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
for rejection_name in rejected_resources:
|
||||
var normalized_relative_path := "world/%s.wmo" % rejection_name
|
||||
var cache_path := "res://cache/%s.tscn" % rejection_name
|
||||
cache_state.call("remember_request", normalized_relative_path, cache_path)
|
||||
adapter.status_by_path[cache_path] = ResourceLoader.THREAD_LOAD_LOADED
|
||||
adapter.resource_by_path[cache_path] = rejected_resources[rejection_name]
|
||||
_expect_equal(
|
||||
int(finalizer.call("poll_terminal_requests", cache_state)),
|
||||
4,
|
||||
"all invalid loaded Resources complete",
|
||||
failures
|
||||
)
|
||||
for rejection_name in rejected_resources:
|
||||
_expect_true(
|
||||
bool(cache_state.call("is_missing", "world/%s.wmo" % rejection_name)),
|
||||
"%s loaded Resource publishes missing" % rejection_name,
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
validator.validated_nodes.size(),
|
||||
1,
|
||||
"only Node3D stale scene reaches validator",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
is_instance_valid(validator.validated_nodes[0]),
|
||||
"stale validation probe released",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_current_scene_adoption_and_probe_lifetime(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var adapter := FakeResourceLoaderAdapter.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter)
|
||||
var current_scene := _packed_scene(true, true, failures)
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
cache_state.call("remember_request", "world/current.wmo", "res://cache/current.tscn")
|
||||
adapter.status_by_path["res://cache/current.tscn"] = ResourceLoader.THREAD_LOAD_LOADED
|
||||
adapter.resource_by_path["res://cache/current.tscn"] = current_scene
|
||||
finalizer.call("poll_terminal_requests", cache_state)
|
||||
_expect_true(
|
||||
cache_state.call("scene_for", "world/current.wmo") == current_scene,
|
||||
"current PackedScene exact identity adopted",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
validator.validated_nodes.size(),
|
||||
1,
|
||||
"current scene instantiated exactly once",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
is_instance_valid(validator.validated_nodes[0]),
|
||||
"accepted validation probe released",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_validation_contract(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator)
|
||||
var current_scene := _packed_scene(true, true, failures)
|
||||
_expect_true(
|
||||
bool(finalizer.call("is_scene_cache_current", current_scene)),
|
||||
"current Node3D scene accepted",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
bool(finalizer.call("is_scene_cache_current", null)),
|
||||
"null scene rejected",
|
||||
failures
|
||||
)
|
||||
var missing_validator_finalizer: RefCounted = FINALIZER_SCRIPT.new(null)
|
||||
_expect_false(
|
||||
bool(missing_validator_finalizer.call("is_scene_cache_current", current_scene)),
|
||||
"missing validator rejects scene without instantiation",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
var finalizer_source := _read_text(FINALIZER_PATH, failures)
|
||||
var cache_source := _read_text(CACHE_STATE_PATH, failures)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_SCENE_RESOURCE_FINALIZER_SCRIPT.new("),
|
||||
"loader composes scene finalizer",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("_wmo_scene_resource_finalizer.poll_terminal_requests("),
|
||||
"loader delegates scene terminal polling",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_source.contains("func _is_wmo_scene_cache_current("),
|
||||
"loader releases PackedScene probe validation",
|
||||
failures
|
||||
)
|
||||
for required_token in [
|
||||
"ResourceLoader.load_threaded_get_status(resource_path)",
|
||||
"ResourceLoader.load_threaded_get(resource_path)",
|
||||
"scene.instantiate()",
|
||||
"is_scene_cache_current",
|
||||
"instance.free()",
|
||||
"complete_request_with_scene",
|
||||
"complete_request_as_missing",
|
||||
]:
|
||||
_expect_true(
|
||||
finalizer_source.contains(required_token),
|
||||
"finalizer owns %s" % required_token,
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
cache_source.contains("ResourceLoader."),
|
||||
"cache state remains free of ResourceLoader",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var adapter := FakeResourceLoaderAdapter.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(
|
||||
FakeSceneCacheValidator.new(),
|
||||
adapter
|
||||
)
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(1000):
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
var normalized_relative_path := "world/object_%d.wmo" % iteration
|
||||
var cache_path := "res://cache/object_%d.tscn" % iteration
|
||||
cache_state.call("remember_request", normalized_relative_path, cache_path)
|
||||
adapter.status_by_path[cache_path] = ResourceLoader.THREAD_LOAD_FAILED
|
||||
finalizer.call("poll_terminal_requests", cache_state)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
if elapsed_milliseconds >= 1000.0:
|
||||
failures.append(
|
||||
"1000 terminal polls took %.3fms (budget < 1000ms)" % elapsed_milliseconds
|
||||
)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _packed_scene(
|
||||
is_current: bool,
|
||||
use_node_3d_root: bool,
|
||||
failures: Array[String]
|
||||
) -> PackedScene:
|
||||
var root: Node = Node3D.new() if use_node_3d_root else Node.new()
|
||||
root.set_meta("current_wmo_cache", is_current)
|
||||
var scene := PackedScene.new()
|
||||
var pack_error := scene.pack(root)
|
||||
root.free()
|
||||
if pack_error != OK:
|
||||
failures.append("cannot pack synthetic scene: %d" % pack_error)
|
||||
return scene
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot read %s" % path)
|
||||
return ""
|
||||
var source := file.get_as_text()
|
||||
file.close()
|
||||
return source
|
||||
|
||||
|
||||
func _expect_true(value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not value:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(value: bool, label: String, failures: Array[String]) -> void:
|
||||
if value:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s: expected %d, got %d" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_string_array(
|
||||
actual: Array,
|
||||
expected: Array,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s: expected %s, got %s" % [label, expected, actual])
|
||||
Reference in New Issue
Block a user