refactor(M03): extract WMO placement registry
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
class_name WmoPlacementRegistry
|
||||
extends RefCounted
|
||||
|
||||
## Owns WMO placement-key to tile/global reference-key sets without Nodes.
|
||||
|
||||
var _reference_keys_by_placement_key: Dictionary = {}
|
||||
|
||||
|
||||
## Adds one idempotent reference and returns true only for a newly registered
|
||||
## placement key. Empty placement/reference keys are rejected.
|
||||
func add_reference(placement_key: String, reference_key: String) -> bool:
|
||||
if placement_key.is_empty() or reference_key.is_empty():
|
||||
return false
|
||||
var is_new_placement := not _reference_keys_by_placement_key.has(placement_key)
|
||||
if is_new_placement:
|
||||
_reference_keys_by_placement_key[placement_key] = {}
|
||||
var reference_keys: Dictionary = _reference_keys_by_placement_key[placement_key]
|
||||
reference_keys[reference_key] = true
|
||||
return is_new_placement
|
||||
|
||||
|
||||
## Releases one reference and returns true only when the final reference removes
|
||||
## the placement key. Unknown and non-owner references leave state unchanged.
|
||||
func release_reference(placement_key: String, reference_key: String) -> bool:
|
||||
if placement_key.is_empty() or reference_key.is_empty():
|
||||
return false
|
||||
if not _reference_keys_by_placement_key.has(placement_key):
|
||||
return false
|
||||
var reference_keys: Dictionary = _reference_keys_by_placement_key[placement_key]
|
||||
if not reference_keys.has(reference_key):
|
||||
return false
|
||||
reference_keys.erase(reference_key)
|
||||
if not reference_keys.is_empty():
|
||||
return false
|
||||
_reference_keys_by_placement_key.erase(placement_key)
|
||||
return true
|
||||
|
||||
|
||||
## Returns whether the placement key currently has at least one reference.
|
||||
func contains(placement_key: String) -> bool:
|
||||
return not placement_key.is_empty() and _reference_keys_by_placement_key.has(placement_key)
|
||||
|
||||
|
||||
## Removes every placement/reference record. Safe to call repeatedly.
|
||||
func clear() -> void:
|
||||
_reference_keys_by_placement_key.clear()
|
||||
|
||||
|
||||
## Returns the number of currently referenced placement keys.
|
||||
func active_count() -> int:
|
||||
return _reference_keys_by_placement_key.size()
|
||||
|
||||
|
||||
## Returns detached, placement/reference-key-sorted diagnostic records.
|
||||
func diagnostic_snapshot() -> Array[Dictionary]:
|
||||
var placement_keys: Array = _reference_keys_by_placement_key.keys()
|
||||
placement_keys.sort()
|
||||
var snapshot: Array[Dictionary] = []
|
||||
for placement_key_variant in placement_keys:
|
||||
var placement_key := String(placement_key_variant)
|
||||
var reference_keys: Array = (
|
||||
_reference_keys_by_placement_key[placement_key] as Dictionary
|
||||
).keys()
|
||||
reference_keys.sort()
|
||||
snapshot.append({
|
||||
"placement_key": placement_key,
|
||||
"reference_keys": reference_keys.duplicate(),
|
||||
})
|
||||
return snapshot
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfbo5fw33vw4f
|
||||
@@ -12,6 +12,9 @@ const WMO_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/wmo_buil
|
||||
const WMO_PLACEMENT_RESOLVER_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_placement_resolver.gd"
|
||||
)
|
||||
const WMO_PLACEMENT_REGISTRY_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_placement_registry.gd"
|
||||
)
|
||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||
const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
|
||||
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
|
||||
@@ -286,7 +289,8 @@ var _m2_animated_scene_cache: Dictionary = {}
|
||||
var _m2_static_animation_cache: Dictionary = {}
|
||||
var _m2_missing_cache: Dictionary = {}
|
||||
var _world_wmo_root: Node3D
|
||||
var _wmo_registry: Dictionary = {}
|
||||
var _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_SCRIPT.new()
|
||||
var _wmo_instance_nodes_by_unique_key: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -1031,7 +1035,7 @@ func render_baseline_snapshot() -> Dictionary:
|
||||
},
|
||||
"cache_entries": _tile_mesh_cache.size(),
|
||||
"m2_active_unique_ids": _m2_unique_placement_registry.active_count(),
|
||||
"wmo_instances": _wmo_registry.size(),
|
||||
"wmo_instances": _wmo_placement_registry.active_count(),
|
||||
}
|
||||
|
||||
|
||||
@@ -1313,7 +1317,7 @@ func _tick_runtime_stats(delta: float) -> void:
|
||||
m2_tiles,
|
||||
m2_multimeshes,
|
||||
m2_instances,
|
||||
_wmo_registry.size(),
|
||||
_wmo_placement_registry.active_count(),
|
||||
])
|
||||
|
||||
|
||||
@@ -3363,7 +3367,7 @@ func _wdt_has_global_wmo() -> bool:
|
||||
func _place_global_wmo() -> void:
|
||||
if not _wdt_has_global_wmo():
|
||||
return
|
||||
if _wmo_registry.has("global"):
|
||||
if _wmo_placement_registry.contains("global"):
|
||||
return
|
||||
|
||||
_ensure_world_wmo_root()
|
||||
@@ -3387,10 +3391,7 @@ func _place_global_wmo() -> void:
|
||||
|
||||
_world_wmo_root.add_child(instance)
|
||||
_set_editor_owner_recursive(instance)
|
||||
_wmo_registry["global"] = {
|
||||
"node": instance,
|
||||
"refs": {"__global__": true},
|
||||
}
|
||||
_register_wmo_instance_reference("global", "__global__", instance)
|
||||
|
||||
|
||||
func _sync_detail_assets(state: Dictionary, focus_pos: Vector3) -> Dictionary:
|
||||
@@ -3566,10 +3567,8 @@ func _process_wmo_build_jobs() -> void:
|
||||
tile_key,
|
||||
index
|
||||
)
|
||||
if _wmo_registry.has(unique_key):
|
||||
var entry: Dictionary = _wmo_registry[unique_key]
|
||||
var entry_refs: Dictionary = entry["refs"]
|
||||
entry_refs[tile_key] = true
|
||||
if _wmo_placement_registry.contains(unique_key):
|
||||
_wmo_placement_registry.add_reference(unique_key, tile_key)
|
||||
refs.append(unique_key)
|
||||
else:
|
||||
var rel_path: String = str(wmo_names[name_id]).replace("\\", "/")
|
||||
@@ -3579,10 +3578,7 @@ func _process_wmo_build_jobs() -> void:
|
||||
if instance != null:
|
||||
_world_wmo_root.add_child(instance)
|
||||
_set_editor_owner_recursive(instance)
|
||||
_wmo_registry[unique_key] = {
|
||||
"node": instance,
|
||||
"refs": {tile_key: true},
|
||||
}
|
||||
_register_wmo_instance_reference(unique_key, tile_key, instance)
|
||||
refs.append(unique_key)
|
||||
_start_wmo_render_build(unique_key, instance, render_resource)
|
||||
else:
|
||||
@@ -3602,10 +3598,11 @@ func _process_wmo_build_jobs() -> void:
|
||||
_prepare_runtime_wmo_instance(live_instance)
|
||||
_world_wmo_root.add_child(live_instance)
|
||||
_set_editor_owner_recursive(live_instance)
|
||||
_wmo_registry[unique_key] = {
|
||||
"node": live_instance,
|
||||
"refs": {tile_key: true},
|
||||
}
|
||||
_register_wmo_instance_reference(
|
||||
unique_key,
|
||||
tile_key,
|
||||
live_instance
|
||||
)
|
||||
refs.append(unique_key)
|
||||
else:
|
||||
var scene_instance := _instantiate_wmo_scene(rel_path, scene, placement)
|
||||
@@ -3613,10 +3610,11 @@ func _process_wmo_build_jobs() -> void:
|
||||
_prepare_runtime_wmo_instance(scene_instance)
|
||||
_world_wmo_root.add_child(scene_instance)
|
||||
_set_editor_owner_recursive(scene_instance)
|
||||
_wmo_registry[unique_key] = {
|
||||
"node": scene_instance,
|
||||
"refs": {tile_key: true},
|
||||
}
|
||||
_register_wmo_instance_reference(
|
||||
unique_key,
|
||||
tile_key,
|
||||
scene_instance
|
||||
)
|
||||
refs.append(unique_key)
|
||||
if advance_job:
|
||||
job["refs"] = refs
|
||||
@@ -3994,6 +3992,17 @@ func _cancel_wmo_build_job(tile_key: String) -> void:
|
||||
_wmo_build_queue.erase(tile_key)
|
||||
|
||||
|
||||
func _register_wmo_instance_reference(
|
||||
placement_key: String,
|
||||
reference_key: String,
|
||||
instance: Node3D
|
||||
) -> void:
|
||||
if placement_key.is_empty() or reference_key.is_empty() or instance == null:
|
||||
return
|
||||
_wmo_instance_nodes_by_unique_key[placement_key] = instance
|
||||
_wmo_placement_registry.add_reference(placement_key, reference_key)
|
||||
|
||||
|
||||
func _unregister_tile_wmos(state: Dictionary) -> void:
|
||||
var refs: Array = state.get("wmo_refs", [])
|
||||
var tile_key: String = String(state.get("key", ""))
|
||||
@@ -4003,18 +4012,16 @@ func _unregister_tile_wmos(state: Dictionary) -> void:
|
||||
state["wmo_building"] = false
|
||||
return
|
||||
|
||||
for unique_key in refs:
|
||||
if not _wmo_registry.has(unique_key):
|
||||
for unique_key_variant in refs:
|
||||
var unique_key := String(unique_key_variant)
|
||||
if not _wmo_placement_registry.contains(unique_key):
|
||||
continue
|
||||
var entry: Dictionary = _wmo_registry[unique_key]
|
||||
var entry_refs: Dictionary = entry["refs"]
|
||||
entry_refs.erase(tile_key)
|
||||
if entry_refs.is_empty():
|
||||
if _wmo_placement_registry.release_reference(unique_key, tile_key):
|
||||
_cancel_wmo_render_build_job(String(unique_key))
|
||||
var node: Node = entry.get("node", null)
|
||||
if node:
|
||||
var node: Node = _wmo_instance_nodes_by_unique_key.get(unique_key, null)
|
||||
if node != null and is_instance_valid(node):
|
||||
node.queue_free()
|
||||
_wmo_registry.erase(unique_key)
|
||||
_wmo_instance_nodes_by_unique_key.erase(unique_key)
|
||||
|
||||
state["wmo_refs"] = []
|
||||
state["wmo_building"] = false
|
||||
@@ -4023,14 +4030,14 @@ func _unregister_tile_wmos(state: Dictionary) -> void:
|
||||
func _clear_wmo_registry() -> void:
|
||||
_wmo_render_build_jobs.clear()
|
||||
_wmo_render_build_queue.clear()
|
||||
for entry_variant in _wmo_registry.values():
|
||||
if not (entry_variant is Dictionary):
|
||||
for node_variant in _wmo_instance_nodes_by_unique_key.values():
|
||||
if not (node_variant is Node):
|
||||
continue
|
||||
var entry: Dictionary = entry_variant
|
||||
var node: Node = entry.get("node", null)
|
||||
if node and is_instance_valid(node):
|
||||
var node := node_variant as Node
|
||||
if is_instance_valid(node):
|
||||
_queue_free_streamed_node(node)
|
||||
_wmo_registry.clear()
|
||||
_wmo_instance_nodes_by_unique_key.clear()
|
||||
_wmo_placement_registry.clear()
|
||||
if _world_wmo_root and is_instance_valid(_world_wmo_root):
|
||||
_queue_free_streamed_node(_world_wmo_root)
|
||||
_world_wmo_root = null
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free ownership, lifecycle, dependency and timing regression for WMOs.
|
||||
|
||||
const REGISTRY_SCRIPT := preload("res://src/render/wmo/wmo_placement_registry.gd")
|
||||
const REGISTRY_PATH := "res://src/render/wmo/wmo_placement_registry.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_invalid_and_first_reference(failures)
|
||||
_verify_idempotent_and_shared_references(failures)
|
||||
_verify_owner_checked_final_release(failures)
|
||||
_verify_global_reference_and_clear(failures)
|
||||
_verify_detached_sorted_diagnostics(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("WMO_PLACEMENT_REGISTRY: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"WMO_PLACEMENT_REGISTRY PASS cases=10 iterations=100 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_invalid_and_first_reference(failures: Array[String]) -> void:
|
||||
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||
_expect_false(registry.call("add_reference", "", "tile-a"), "empty placement rejected", failures)
|
||||
_expect_false(registry.call("add_reference", "uid:7", ""), "empty reference rejected", failures)
|
||||
_expect_true(registry.call("add_reference", "uid:7", "tile-a"), "first reference registers", failures)
|
||||
_expect_true(registry.call("contains", "uid:7"), "registered placement contained", failures)
|
||||
_expect_equal(int(registry.call("active_count")), 1, "first active count", failures)
|
||||
|
||||
|
||||
func _verify_idempotent_and_shared_references(failures: Array[String]) -> void:
|
||||
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||
registry.call("add_reference", "uid:11", "tile-a")
|
||||
_expect_false(
|
||||
registry.call("add_reference", "uid:11", "tile-a"),
|
||||
"same reference idempotent",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
registry.call("add_reference", "uid:11", "tile-b"),
|
||||
"shared reference retains placement",
|
||||
failures
|
||||
)
|
||||
_expect_equal(int(registry.call("active_count")), 1, "shared active count", failures)
|
||||
_expect_false(
|
||||
registry.call("release_reference", "uid:11", "tile-a"),
|
||||
"non-final release retains placement",
|
||||
failures
|
||||
)
|
||||
_expect_true(registry.call("contains", "uid:11"), "shared placement remains", failures)
|
||||
|
||||
|
||||
func _verify_owner_checked_final_release(failures: Array[String]) -> void:
|
||||
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||
registry.call("add_reference", "uid:19", "tile-owner")
|
||||
_expect_false(
|
||||
registry.call("release_reference", "uid:19", "tile-other"),
|
||||
"non-owner release ignored",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
registry.call("release_reference", "uid:404", "tile-owner"),
|
||||
"unknown placement release ignored",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
registry.call("release_reference", "uid:19", "tile-owner"),
|
||||
"final owner release removes placement",
|
||||
failures
|
||||
)
|
||||
_expect_false(registry.call("contains", "uid:19"), "released placement absent", failures)
|
||||
|
||||
|
||||
func _verify_global_reference_and_clear(failures: Array[String]) -> void:
|
||||
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||
_expect_true(
|
||||
registry.call("add_reference", "global", "__global__"),
|
||||
"global reference registers",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
registry.call("release_reference", "global", "__global__"),
|
||||
"global final release removes",
|
||||
failures
|
||||
)
|
||||
registry.call("add_reference", "uid:3", "tile-a")
|
||||
registry.call("clear")
|
||||
registry.call("clear")
|
||||
_expect_equal(int(registry.call("active_count")), 0, "clear idempotent", failures)
|
||||
|
||||
|
||||
func _verify_detached_sorted_diagnostics(failures: Array[String]) -> void:
|
||||
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||
registry.call("add_reference", "uid:20", "tile-b")
|
||||
registry.call("add_reference", "uid:10", "tile-c")
|
||||
registry.call("add_reference", "uid:10", "tile-a")
|
||||
var snapshot: Array = registry.call("diagnostic_snapshot")
|
||||
_expect_equal(snapshot.size(), 2, "diagnostic record count", failures)
|
||||
_expect_string_equal(String(snapshot[0]["placement_key"]), "uid:10", "placement sort", failures)
|
||||
_expect_string_array(snapshot[0]["reference_keys"], ["tile-a", "tile-c"], "reference sort", failures)
|
||||
(snapshot[0]["reference_keys"] as Array).append("mutated")
|
||||
var fresh_snapshot: Array = registry.call("diagnostic_snapshot")
|
||||
_expect_string_array(
|
||||
fresh_snapshot[0]["reference_keys"],
|
||||
["tile-a", "tile-c"],
|
||||
"diagnostics detached",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
var registry_source := _read_text(REGISTRY_PATH, failures)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_PLACEMENT_REGISTRY_SCRIPT.new()"),
|
||||
"loader composes registry",
|
||||
failures
|
||||
)
|
||||
_expect_true(loader_source.contains("_wmo_placement_registry.add_reference("), "loader delegates add", failures)
|
||||
_expect_true(loader_source.contains("_wmo_placement_registry.release_reference("), "loader delegates release", failures)
|
||||
_expect_true(loader_source.contains("_wmo_placement_registry.clear()"), "loader delegates clear", failures)
|
||||
_expect_true(
|
||||
loader_source.contains("_wmo_instance_nodes_by_unique_key"),
|
||||
"loader retains node map",
|
||||
failures
|
||||
)
|
||||
_expect_false(loader_source.contains("var _wmo_registry"), "legacy mixed registry removed", failures)
|
||||
for forbidden_token in ["extends Node", "Node3D", "Resource", "RID", "Thread", "Mutex", "Semaphore"]:
|
||||
_expect_false(
|
||||
registry_source.contains(forbidden_token),
|
||||
"registry omits %s ownership" % forbidden_token,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(100):
|
||||
for placement_index in range(256):
|
||||
var placement_key := "uid:%d" % placement_index
|
||||
registry.call("add_reference", placement_key, "tile-a")
|
||||
registry.call("add_reference", placement_key, "tile-b")
|
||||
for placement_index in range(256):
|
||||
var placement_key := "uid:%d" % placement_index
|
||||
registry.call("release_reference", placement_key, "tile-a")
|
||||
registry.call("release_reference", placement_key, "tile-b")
|
||||
_expect_equal(int(registry.call("active_count")), 0, "timing cycle %d clears" % iteration, failures)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "100 shared-reference cycles remain bounded", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_equal(actual_value: int, expected_value: int, label: String, failures: Array[String]) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_string_equal(
|
||||
actual_value: String,
|
||||
expected_value: String,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_string_array(
|
||||
actual_values: Array,
|
||||
expected_values: Array,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual_values != expected_values:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_values, actual_values])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
|
||||
|
||||
func _expect_false(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if actual_value:
|
||||
failures.append("%s expected false" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dprfwayji363v
|
||||
Reference in New Issue
Block a user