render: extract WMO render resource finalizer

This commit is contained in:
2026-07-18 14:03:27 +04:00
parent fb7c9f174e
commit a86f8f2212
12 changed files with 751 additions and 56 deletions
@@ -0,0 +1,90 @@
class_name WmoRenderResourceFinalizer
extends RefCounted
## Polls lightweight WMO render-cache requests and publishes terminal outcomes.
## Request admission, cache lifetime and WMO materialization remain caller-owned.
var _expected_resource_script: Script
var _minimum_format_version: int
var _resource_loader_adapter: Object
func _init(
expected_resource_script: Script,
minimum_format_version: int,
resource_loader_adapter: Object = null
) -> void:
_expected_resource_script = expected_resource_script
_minimum_format_version = minimum_format_version
_resource_loader_adapter = resource_loader_adapter
## Polls a detached pending snapshot in insertion order. Only terminal requests
## are removed and published as an exact Resource reference or missing outcome.
func poll_terminal_requests(render_resource_cache_state: RefCounted) -> int:
if render_resource_cache_state == null:
return 0
var completed_request_count := 0
var request_paths: Dictionary = render_resource_cache_state.call(
"request_paths_snapshot"
)
for normalized_relative_path_variant in request_paths.keys():
var normalized_relative_path := String(normalized_relative_path_variant)
var resource_path := String(request_paths[normalized_relative_path_variant])
var load_status := load_threaded_get_status(resource_path)
if (
load_status != ResourceLoader.THREAD_LOAD_LOADED
and load_status != ResourceLoader.THREAD_LOAD_FAILED
):
continue
if load_status != ResourceLoader.THREAD_LOAD_LOADED:
render_resource_cache_state.call(
"complete_request_as_missing",
normalized_relative_path
)
completed_request_count += 1
continue
var loaded_resource := load_threaded_get(resource_path)
if is_current_render_resource(loaded_resource):
render_resource_cache_state.call(
"complete_request_with_resource",
normalized_relative_path,
loaded_resource
)
else:
render_resource_cache_state.call(
"complete_request_as_missing",
normalized_relative_path
)
completed_request_count += 1
return completed_request_count
## Accepts only the exact configured script and its current-or-newer format.
func is_current_render_resource(resource: Resource) -> bool:
return (
resource != null
and _expected_resource_script != null
and resource.get_script() == _expected_resource_script
and int(resource.get("format_version")) >= _minimum_format_version
)
## Production ResourceLoader status boundary; injectable for 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 result boundary; injectable for synthetic 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://dtvba6g1grttk
+10 -22
View File
@@ -24,6 +24,9 @@ const WMO_RENDER_BUILD_QUEUE_SCRIPT := preload(
const WMO_RENDER_RESOURCE_CACHE_STATE_SCRIPT := preload(
"res://src/render/wmo/wmo_render_resource_cache_state.gd"
)
const WMO_RENDER_RESOURCE_FINALIZER_SCRIPT := preload(
"res://src/render/wmo/wmo_render_resource_finalizer.gd"
)
const WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT := preload(
"res://src/render/wmo/wmo_scene_resource_cache_state.gd"
)
@@ -361,6 +364,10 @@ var _tile_mesh_cache_order: Array = []
var _terrain_quality_mesh_cache: RefCounted = TERRAIN_QUALITY_MESH_CACHE_SCRIPT.new()
var _wmo_prototype_cache: Dictionary = {}
var _wmo_render_resource_cache_state := WMO_RENDER_RESOURCE_CACHE_STATE_SCRIPT.new()
var _wmo_render_resource_finalizer := WMO_RENDER_RESOURCE_FINALIZER_SCRIPT.new(
WMO_STREAMING_SCRIPT,
WMO_STREAMING_SCRIPT.FORMAT_VERSION
)
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 = {}
@@ -3725,28 +3732,9 @@ func _drain_wmo_scene_loads() -> void:
func _drain_wmo_render_loads() -> void:
var request_paths := _wmo_render_resource_cache_state.request_paths_snapshot()
for normalized_rel_variant in request_paths.keys():
var normalized_rel := String(normalized_rel_variant)
var path := String(request_paths[normalized_rel])
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
continue
if status != ResourceLoader.THREAD_LOAD_LOADED:
_wmo_render_resource_cache_state.complete_request_as_missing(normalized_rel)
continue
var resource: Resource = ResourceLoader.load_threaded_get(path)
if (
resource != null
and resource.get_script() == WMO_STREAMING_SCRIPT
and int(resource.get("format_version")) >= WMO_STREAMING_SCRIPT.FORMAT_VERSION
):
_wmo_render_resource_cache_state.complete_request_with_resource(
normalized_rel,
resource
)
else:
_wmo_render_resource_cache_state.complete_request_as_missing(normalized_rel)
_wmo_render_resource_finalizer.poll_terminal_requests(
_wmo_render_resource_cache_state
)
func _get_wmo_render_or_request(rel_path: String) -> Resource:
@@ -7,6 +7,7 @@ const CACHE_STATE_SCRIPT := preload(
"res://src/render/wmo/wmo_render_resource_cache_state.gd"
)
const CACHE_STATE_PATH := "res://src/render/wmo/wmo_render_resource_cache_state.gd"
const FINALIZER_PATH := "res://src/render/wmo/wmo_render_resource_finalizer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
@@ -209,6 +210,7 @@ func _verify_detached_sorted_diagnostics(failures: Array[String]) -> void:
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)
var finalizer_source := _read_text(FINALIZER_PATH, failures)
_expect_true(
loader_source.contains("WMO_RENDER_RESOURCE_CACHE_STATE_SCRIPT.new()"),
"loader composes cache state",
@@ -241,13 +243,13 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
failures
)
_expect_true(
loader_source.contains("resource.get_script() == WMO_STREAMING_SCRIPT"),
"loader retains script validation",
finalizer_source.contains("resource.get_script() == _expected_resource_script"),
"finalizer owns script validation",
failures
)
_expect_true(
loader_source.contains("WMO_STREAMING_SCRIPT.FORMAT_VERSION"),
"loader retains cache format validation",
"loader injects cache format version",
failures
)
@@ -0,0 +1,356 @@
extends SceneTree
## Asset-free terminal-I/O, validation, adoption, boundary and timing regression
## for the lightweight WMO render Resource finalizer.
const FINALIZER_SCRIPT := preload(
"res://src/render/wmo/wmo_render_resource_finalizer.gd"
)
const CACHE_STATE_SCRIPT := preload(
"res://src/render/wmo/wmo_render_resource_cache_state.gd"
)
const FINALIZER_PATH := "res://src/render/wmo/wmo_render_resource_finalizer.gd"
const CACHE_STATE_PATH := "res://src/render/wmo/wmo_render_resource_cache_state.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
class FakeWmoRenderResource extends Resource:
var format_version: int = 0
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_resource_adoption(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_RENDER_RESOURCE_FINALIZER: %s" % failure)
quit(1)
return
print(
"WMO_RENDER_RESOURCE_FINALIZER PASS cases=24 iterations=1000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_null_and_non_terminal_poll(failures: Array[String]) -> void:
var adapter := FakeResourceLoaderAdapter.new()
var expected_resource := FakeWmoRenderResource.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(
expected_resource.get_script(),
2,
adapter
)
_expect_equal(
int(finalizer.call("poll_terminal_requests", null)),
0,
"null cache state is rejected",
failures
)
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
cache_state.call(
"remember_request",
"world/pending.wmo",
"res://cache/pending.res"
)
_expect_equal(
int(finalizer.call("poll_terminal_requests", cache_state)),
0,
"non-terminal request remains pending",
failures
)
_expect_true(
bool(cache_state.call("has_request", "world/pending.wmo")),
"pending request retained",
failures
)
_expect_string_array(
adapter.status_paths,
["res://cache/pending.res"],
"pending path polled once",
failures
)
_expect_equal(adapter.loaded_paths.size(), 0, "pending request is not loaded", failures)
func _verify_failed_and_loaded_poll_order(failures: Array[String]) -> void:
var adapter := FakeResourceLoaderAdapter.new()
var valid_resource := FakeWmoRenderResource.new()
valid_resource.format_version = 2
var finalizer: RefCounted = FINALIZER_SCRIPT.new(
valid_resource.get_script(),
2,
adapter
)
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
var requests := {
"world/pending.wmo": "res://cache/pending.res",
"world/failed.wmo": "res://cache/failed.res",
"world/loaded.wmo": "res://cache/loaded.res",
}
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.res": ResourceLoader.THREAD_LOAD_FAILED,
"res://cache/loaded.res": ResourceLoader.THREAD_LOAD_LOADED,
}
adapter.resource_by_path["res://cache/loaded.res"] = valid_resource
_expect_equal(
int(finalizer.call("poll_terminal_requests", cache_state)),
2,
"failed and loaded requests complete",
failures
)
_expect_string_array(
adapter.status_paths,
[
"res://cache/pending.res",
"res://cache/failed.res",
"res://cache/loaded.res",
],
"detached insertion order preserved",
failures
)
_expect_string_array(
adapter.loaded_paths,
["res://cache/loaded.res"],
"only loaded terminal Resource retrieved",
failures
)
_expect_true(
bool(cache_state.call("is_missing", "world/failed.wmo")),
"failed request publishes missing",
failures
)
_expect_true(
cache_state.call("resource_for", "world/loaded.wmo") == valid_resource,
"loaded Resource identity adopted",
failures
)
_expect_true(
bool(cache_state.call("has_request", "world/pending.wmo")),
"non-terminal sibling stays pending",
failures
)
func _verify_loaded_resource_rejections(failures: Array[String]) -> void:
var expected_resource := FakeWmoRenderResource.new()
var adapter := FakeResourceLoaderAdapter.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(
expected_resource.get_script(),
2,
adapter
)
var stale_resource := FakeWmoRenderResource.new()
stale_resource.format_version = 1
var rejected_resources := {
"null": null,
"wrong_script": Resource.new(),
"stale": stale_resource,
}
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.res" % 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)),
3,
"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 Resource publishes missing" % rejection_name,
failures
)
func _verify_current_resource_adoption(failures: Array[String]) -> void:
var current_resource := FakeWmoRenderResource.new()
current_resource.format_version = 2
var newer_resource := FakeWmoRenderResource.new()
newer_resource.format_version = 5
var adapter := FakeResourceLoaderAdapter.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(
current_resource.get_script(),
2,
adapter
)
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
for resource_name in ["current", "newer"]:
var cache_path := "res://cache/%s.res" % resource_name
cache_state.call(
"remember_request",
"world/%s.wmo" % resource_name,
cache_path
)
adapter.status_by_path[cache_path] = ResourceLoader.THREAD_LOAD_LOADED
adapter.resource_by_path["res://cache/current.res"] = current_resource
adapter.resource_by_path["res://cache/newer.res"] = newer_resource
finalizer.call("poll_terminal_requests", cache_state)
_expect_true(
cache_state.call("resource_for", "world/current.wmo") == current_resource,
"current exact Resource adopted",
failures
)
_expect_true(
cache_state.call("resource_for", "world/newer.wmo") == newer_resource,
"newer exact Resource adopted",
failures
)
func _verify_validation_contract(failures: Array[String]) -> void:
var resource := FakeWmoRenderResource.new()
resource.format_version = 2
var valid_finalizer: RefCounted = FINALIZER_SCRIPT.new(resource.get_script(), 2)
_expect_true(
bool(valid_finalizer.call("is_current_render_resource", resource)),
"exact script and minimum format accepted",
failures
)
var missing_script_finalizer: RefCounted = FINALIZER_SCRIPT.new(null, 2)
_expect_false(
bool(missing_script_finalizer.call("is_current_render_resource", resource)),
"missing expected script rejects Resource",
failures
)
_expect_false(
bool(valid_finalizer.call("is_current_render_resource", null)),
"null Resource rejected",
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_RENDER_RESOURCE_FINALIZER_SCRIPT.new("),
"loader composes finalizer",
failures
)
_expect_true(
loader_source.contains("_wmo_render_resource_finalizer.poll_terminal_requests("),
"loader delegates terminal polling",
failures
)
_expect_false(
loader_source.contains("resource.get_script() == WMO_STREAMING_SCRIPT"),
"loader releases script validation",
failures
)
for required_token in [
"ResourceLoader.load_threaded_get_status(resource_path)",
"ResourceLoader.load_threaded_get(resource_path)",
"resource.get_script() == _expected_resource_script",
"complete_request_with_resource",
"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 resource := FakeWmoRenderResource.new()
resource.format_version = 2
var adapter := FakeResourceLoaderAdapter.new()
var finalizer: RefCounted = FINALIZER_SCRIPT.new(
resource.get_script(),
2,
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.res" % 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 _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])
@@ -0,0 +1 @@
uid://corj7g3e1mush