refactor(M03): extract WMO scene resource cache state
This commit is contained in:
@@ -43,8 +43,10 @@ func _verify_runtime_cache_ownership() -> void:
|
||||
wmo_render_cache_state.call("complete_request_with_resource", "render", Resource.new())
|
||||
wmo_render_cache_state.call("remember_request", "missing", "res://missing.res")
|
||||
wmo_render_cache_state.call("complete_request_as_missing", "missing")
|
||||
loader.set("_wmo_scene_resource_cache", {"scene": PackedScene.new()})
|
||||
loader.set("_wmo_scene_cache_missing", {"missing": true})
|
||||
var wmo_scene_cache_state: RefCounted = loader.get("_wmo_scene_resource_cache_state")
|
||||
wmo_scene_cache_state.call("remember_request", "scene", "res://scene.tscn")
|
||||
wmo_scene_cache_state.call("complete_request_with_scene", "scene", PackedScene.new())
|
||||
wmo_scene_cache_state.call("mark_missing", "missing")
|
||||
loader.set("_wmo_missing_cache", {"missing": true})
|
||||
loader.set("_shared_tex_cache", {"texture": ImageTexture.new()})
|
||||
|
||||
@@ -60,8 +62,6 @@ func _verify_runtime_cache_ownership() -> void:
|
||||
"_m2_mesh_cache",
|
||||
"_m2_static_animation_cache",
|
||||
"_m2_missing_cache",
|
||||
"_wmo_scene_resource_cache",
|
||||
"_wmo_scene_cache_missing",
|
||||
"_wmo_missing_cache",
|
||||
"_shared_tex_cache",
|
||||
]:
|
||||
@@ -80,6 +80,12 @@ func _verify_runtime_cache_ownership() -> void:
|
||||
(wmo_render_snapshot["request_paths"] as Array).is_empty(),
|
||||
"WMO render requests should be empty after shutdown"
|
||||
)
|
||||
var wmo_scene_snapshot: Dictionary = wmo_scene_cache_state.call("diagnostic_snapshot")
|
||||
for state_key in ["scene_paths", "missing_paths", "request_paths"]:
|
||||
_expect(
|
||||
(wmo_scene_snapshot[state_key] as Array).is_empty(),
|
||||
"WMO scene %s should be empty after shutdown" % state_key
|
||||
)
|
||||
|
||||
loader.free()
|
||||
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free state, ownership, dependency and timing regression for cached WMO scenes.
|
||||
|
||||
const CACHE_STATE_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_scene_resource_cache_state.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"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_invalid_and_request_state(failures)
|
||||
_verify_direct_missing_and_exclusivity(failures)
|
||||
_verify_validated_scene_completion(failures)
|
||||
_verify_failed_request_completion(failures)
|
||||
_verify_transient_and_full_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_SCENE_RESOURCE_CACHE_STATE: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"WMO_SCENE_RESOURCE_CACHE_STATE PASS cases=11 iterations=100 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_invalid_and_request_state(failures: Array[String]) -> void:
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
_expect_false(
|
||||
cache_state.call("remember_request", "", "res://cache/a.tscn"),
|
||||
"empty normalized path rejected",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
cache_state.call("remember_request", "world/a.wmo", ""),
|
||||
"empty cache path rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
cache_state.call("remember_request", "world/a.wmo", "res://cache/a.tscn"),
|
||||
"valid request accepted",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
cache_state.call("has_request", "world/a.wmo"),
|
||||
"pending request observable",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
int(cache_state.call("pending_request_count")),
|
||||
1,
|
||||
"pending request count",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
cache_state.call("remember_request", "world/a.wmo", "res://cache/other.tscn"),
|
||||
"duplicate request rejected",
|
||||
failures
|
||||
)
|
||||
var request_snapshot: Dictionary = cache_state.call("request_paths_snapshot")
|
||||
_expect_string_equal(
|
||||
String(request_snapshot.get("world/a.wmo", "")),
|
||||
"res://cache/a.tscn",
|
||||
"request cache path preserved",
|
||||
failures
|
||||
)
|
||||
request_snapshot.clear()
|
||||
_expect_true(
|
||||
cache_state.call("has_request", "world/a.wmo"),
|
||||
"request snapshot detached",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_direct_missing_and_exclusivity(failures: Array[String]) -> void:
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
_expect_false(cache_state.call("mark_missing", ""), "empty missing rejected", failures)
|
||||
_expect_true(
|
||||
cache_state.call("mark_missing", "world/missing.wmo"),
|
||||
"direct missing accepted",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
cache_state.call("is_missing", "world/missing.wmo"),
|
||||
"direct missing observable",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
cache_state.call("mark_missing", "world/missing.wmo"),
|
||||
"duplicate missing rejected",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
cache_state.call(
|
||||
"remember_request",
|
||||
"world/missing.wmo",
|
||||
"res://cache/retry.tscn"
|
||||
),
|
||||
"missing path rejects request",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_validated_scene_completion(failures: Array[String]) -> void:
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
var validated_scene := PackedScene.new()
|
||||
cache_state.call("remember_request", "world/b.wmo", "res://cache/b.tscn")
|
||||
_expect_false(
|
||||
cache_state.call("complete_request_with_scene", "world/b.wmo", null),
|
||||
"null completion rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
cache_state.call("complete_request_with_scene", "world/b.wmo", validated_scene),
|
||||
"validated PackedScene completes request",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
cache_state.call("has_request", "world/b.wmo"),
|
||||
"successful completion removes request",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
cache_state.call("contains_scene", "world/b.wmo"),
|
||||
"successful completion cached",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
cache_state.call("scene_for", "world/b.wmo") == validated_scene,
|
||||
"cached PackedScene identity preserved",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
cache_state.call("mark_missing", "world/b.wmo"),
|
||||
"cached scene rejects missing state",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_failed_request_completion(failures: Array[String]) -> void:
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
_expect_false(
|
||||
cache_state.call("complete_request_as_missing", "world/unknown.wmo"),
|
||||
"unknown request completion rejected",
|
||||
failures
|
||||
)
|
||||
cache_state.call("remember_request", "world/failed.wmo", "res://cache/failed.tscn")
|
||||
_expect_true(
|
||||
cache_state.call("complete_request_as_missing", "world/failed.wmo"),
|
||||
"failed request completes as missing",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
cache_state.call("is_missing", "world/failed.wmo"),
|
||||
"failed request negative-cached",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
cache_state.call("has_request", "world/failed.wmo"),
|
||||
"failed completion removes request",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_transient_and_full_clear(failures: Array[String]) -> void:
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
var retained_scene := PackedScene.new()
|
||||
cache_state.call("remember_request", "world/retained.wmo", "res://retained.tscn")
|
||||
cache_state.call("complete_request_with_scene", "world/retained.wmo", retained_scene)
|
||||
cache_state.call("remember_request", "world/pending.wmo", "res://pending.tscn")
|
||||
cache_state.call("mark_missing", "world/missing.wmo")
|
||||
cache_state.call("clear_transient_state")
|
||||
_expect_true(
|
||||
cache_state.call("scene_for", "world/retained.wmo") == retained_scene,
|
||||
"transient clear retains PackedScene",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
int(cache_state.call("pending_request_count")),
|
||||
0,
|
||||
"transient clear removes requests",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
cache_state.call("is_missing", "world/missing.wmo"),
|
||||
"transient clear removes missing state",
|
||||
failures
|
||||
)
|
||||
cache_state.call("clear_all")
|
||||
cache_state.call("clear_all")
|
||||
_expect_false(
|
||||
cache_state.call("contains_scene", "world/retained.wmo"),
|
||||
"full clear idempotently releases PackedScene",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_detached_sorted_diagnostics(failures: Array[String]) -> void:
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
cache_state.call("remember_request", "world/z.wmo", "res://z.tscn")
|
||||
cache_state.call("remember_request", "world/a.wmo", "res://a.tscn")
|
||||
cache_state.call("mark_missing", "world/m.wmo")
|
||||
var snapshot: Dictionary = cache_state.call("diagnostic_snapshot")
|
||||
_expect_string_array(
|
||||
snapshot["request_paths"],
|
||||
["world/a.wmo", "world/z.wmo"],
|
||||
"request diagnostics sorted",
|
||||
failures
|
||||
)
|
||||
_expect_string_array(
|
||||
snapshot["missing_paths"],
|
||||
["world/m.wmo"],
|
||||
"missing diagnostics",
|
||||
failures
|
||||
)
|
||||
(snapshot["request_paths"] as Array).clear()
|
||||
var fresh_snapshot: Dictionary = cache_state.call("diagnostic_snapshot")
|
||||
_expect_equal(
|
||||
(fresh_snapshot["request_paths"] as Array).size(),
|
||||
2,
|
||||
"diagnostics detached",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
var cache_state_source := _read_text(CACHE_STATE_PATH, failures)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new()"),
|
||||
"loader composes scene cache state",
|
||||
failures
|
||||
)
|
||||
for legacy_field in [
|
||||
"var _wmo_scene_resource_cache:",
|
||||
"var _wmo_scene_cache_missing:",
|
||||
"var _wmo_scene_load_requests:",
|
||||
]:
|
||||
_expect_false(
|
||||
loader_source.contains(legacy_field),
|
||||
"loader removes %s" % legacy_field,
|
||||
failures
|
||||
)
|
||||
for loader_owned_token in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
"WMO_BUILDER_SCRIPT",
|
||||
"instantiate()",
|
||||
"Node3D",
|
||||
"RID",
|
||||
"Mutex",
|
||||
"Semaphore",
|
||||
]:
|
||||
_expect_false(
|
||||
cache_state_source.contains(loader_owned_token),
|
||||
"scene cache state omits %s dependency" % loader_owned_token,
|
||||
failures
|
||||
)
|
||||
for retained_loader_rule in [
|
||||
"wmo_max_runtime_scene_mb",
|
||||
"_get_resource_file_size(cache_path)",
|
||||
"_is_wmo_scene_cache_current(resource as PackedScene)",
|
||||
"scene.instantiate() as Node3D",
|
||||
"WMO_BUILDER_SCRIPT.is_scene_cache_current(node)",
|
||||
]:
|
||||
_expect_true(
|
||||
loader_source.contains(retained_loader_rule),
|
||||
"loader retains %s" % retained_loader_rule,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(100):
|
||||
for scene_index in range(256):
|
||||
var normalized_path := "world/object_%d.wmo" % scene_index
|
||||
if scene_index % 3 == 0:
|
||||
cache_state.call("mark_missing", normalized_path)
|
||||
else:
|
||||
cache_state.call(
|
||||
"remember_request",
|
||||
normalized_path,
|
||||
"res://cache/object_%d.tscn" % scene_index
|
||||
)
|
||||
if scene_index % 2 == 0:
|
||||
cache_state.call(
|
||||
"complete_request_with_scene",
|
||||
normalized_path,
|
||||
PackedScene.new()
|
||||
)
|
||||
else:
|
||||
cache_state.call("complete_request_as_missing", normalized_path)
|
||||
cache_state.call("clear_all")
|
||||
_expect_equal(
|
||||
int(cache_state.call("pending_request_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 scene/request 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://ccuuoqli83f6f
|
||||
Reference in New Issue
Block a user