render: extract M2 Mesh resource finalizer
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
class_name M2MeshResourceFinalizer
|
||||
extends RefCounted
|
||||
|
||||
## Polls static M2 ResourceLoader requests and publishes prepared Mesh outcomes.
|
||||
## The caller owns permits, pipeline/cache lifetime and MultiMesh materialization.
|
||||
|
||||
var _mesh_resource_extractor: RefCounted
|
||||
var _runtime_mesh_finalizer: RefCounted
|
||||
var _raw_model_repository: RefCounted
|
||||
var _resource_loader_adapter: Object
|
||||
|
||||
|
||||
func _init(
|
||||
mesh_resource_extractor: RefCounted,
|
||||
runtime_mesh_finalizer: RefCounted,
|
||||
raw_model_repository: RefCounted,
|
||||
resource_loader_adapter: Object = null
|
||||
) -> void:
|
||||
_mesh_resource_extractor = mesh_resource_extractor
|
||||
_runtime_mesh_finalizer = runtime_mesh_finalizer
|
||||
_raw_model_repository = raw_model_repository
|
||||
_resource_loader_adapter = resource_loader_adapter
|
||||
|
||||
|
||||
## Moves terminal threaded requests into the pipeline finalize FIFO.
|
||||
## Empty resource paths retain the historical immediate missing-model 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_model_missing",
|
||||
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 and finalizes one terminal record. A true result means a record was
|
||||
## consumed, including cached, failed and invalid outcomes.
|
||||
func finalize_next_resource(
|
||||
load_pipeline_state: RefCounted,
|
||||
mesh_resource_cache_state: RefCounted,
|
||||
prototype_cache_state: RefCounted,
|
||||
extracted_directory: String
|
||||
) -> bool:
|
||||
if (
|
||||
load_pipeline_state == null
|
||||
or mesh_resource_cache_state == null
|
||||
or prototype_cache_state == null
|
||||
):
|
||||
return false
|
||||
var terminal_record: Dictionary = load_pipeline_state.call(
|
||||
"pop_finalize_record"
|
||||
)
|
||||
if terminal_record.is_empty():
|
||||
return false
|
||||
var normalized_relative_path := String(
|
||||
terminal_record.get("normalized", "")
|
||||
)
|
||||
if (
|
||||
normalized_relative_path.is_empty()
|
||||
or bool(mesh_resource_cache_state.call(
|
||||
"has_mesh",
|
||||
normalized_relative_path
|
||||
))
|
||||
):
|
||||
return true
|
||||
if int(terminal_record.get(
|
||||
"status",
|
||||
ResourceLoader.THREAD_LOAD_FAILED
|
||||
)) != ResourceLoader.THREAD_LOAD_LOADED:
|
||||
prototype_cache_state.call(
|
||||
"mark_model_missing",
|
||||
normalized_relative_path
|
||||
)
|
||||
return true
|
||||
|
||||
var resource_path := String(terminal_record.get("path", ""))
|
||||
var loaded_resource := load_threaded_get(resource_path)
|
||||
var mesh := _mesh_resource_extractor.call(
|
||||
"extract_first_mesh",
|
||||
loaded_resource
|
||||
) as Mesh
|
||||
if mesh == null:
|
||||
prototype_cache_state.call(
|
||||
"mark_model_missing",
|
||||
normalized_relative_path
|
||||
)
|
||||
return true
|
||||
mesh_resource_cache_state.call(
|
||||
"store_mesh",
|
||||
normalized_relative_path,
|
||||
prepare_mesh_for_runtime(
|
||||
normalized_relative_path,
|
||||
mesh,
|
||||
extracted_directory
|
||||
)
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
## Applies the existing refresh-version/raw-data/rebuild path to one Mesh.
|
||||
func prepare_mesh_for_runtime(
|
||||
normalized_relative_path: String,
|
||||
mesh: Mesh,
|
||||
extracted_directory: String
|
||||
) -> Mesh:
|
||||
if mesh == null:
|
||||
return null
|
||||
if (
|
||||
_runtime_mesh_finalizer == null
|
||||
or not bool(_runtime_mesh_finalizer.call(
|
||||
"requires_raw_data_for_refresh",
|
||||
mesh
|
||||
))
|
||||
):
|
||||
return mesh
|
||||
var raw_model_data: Dictionary = {}
|
||||
if _raw_model_repository != null:
|
||||
raw_model_data = _raw_model_repository.call(
|
||||
"load_static_model_data",
|
||||
extracted_directory,
|
||||
normalized_relative_path
|
||||
)
|
||||
return _runtime_mesh_finalizer.call(
|
||||
"finalize_mesh",
|
||||
normalized_relative_path,
|
||||
mesh,
|
||||
raw_model_data,
|
||||
extracted_directory
|
||||
) as Mesh
|
||||
|
||||
|
||||
## 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://de4xl1fywvqfw
|
||||
@@ -108,6 +108,9 @@ const M2_MESH_RESOURCE_CACHE_STATE_SCRIPT := preload(
|
||||
const M2_MESH_RESOURCE_EXTRACTOR_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_mesh_resource_extractor.gd"
|
||||
)
|
||||
const M2_MESH_RESOURCE_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_mesh_resource_finalizer.gd"
|
||||
)
|
||||
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
||||
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
|
||||
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
|
||||
@@ -309,6 +312,11 @@ var _m2_raw_model_repository := M2_RAW_MODEL_REPOSITORY_SCRIPT.new()
|
||||
var _m2_prototype_cache_state := M2_PROTOTYPE_CACHE_STATE_SCRIPT.new()
|
||||
var _m2_mesh_resource_cache_state := M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new()
|
||||
var _m2_mesh_resource_extractor := M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()
|
||||
var _m2_mesh_resource_finalizer := M2_MESH_RESOURCE_FINALIZER_SCRIPT.new(
|
||||
_m2_mesh_resource_extractor,
|
||||
_m2_runtime_mesh_finalizer,
|
||||
_m2_raw_model_repository
|
||||
)
|
||||
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()
|
||||
@@ -4268,41 +4276,19 @@ func _drain_m2_animation_loads() -> void:
|
||||
|
||||
|
||||
func _drain_m2_mesh_loads() -> void:
|
||||
for pending in _m2_mesh_load_pipeline_state.request_records_snapshot():
|
||||
var normalized_rel := String(pending.get("normalized", ""))
|
||||
var path: String = String(pending.get("path", ""))
|
||||
if path.is_empty():
|
||||
_m2_mesh_load_pipeline_state.discard_request(normalized_rel)
|
||||
_m2_prototype_cache_state.mark_model_missing(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_mesh_load_pipeline_state.complete_request(normalized_rel, status)
|
||||
_m2_mesh_resource_finalizer.poll_terminal_requests(
|
||||
_m2_mesh_load_pipeline_state,
|
||||
_m2_prototype_cache_state
|
||||
)
|
||||
|
||||
while _m2_mesh_load_pipeline_state.has_finalize_record() and _render_budget_scheduler.try_consume_permit(
|
||||
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE):
|
||||
var pending: Dictionary = _m2_mesh_load_pipeline_state.pop_finalize_record()
|
||||
var normalized_rel := String(pending.get("normalized", ""))
|
||||
if (
|
||||
normalized_rel.is_empty()
|
||||
or _m2_mesh_resource_cache_state.has_mesh(normalized_rel)
|
||||
):
|
||||
continue
|
||||
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
|
||||
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
|
||||
continue
|
||||
|
||||
var path := String(pending.get("path", ""))
|
||||
var resource: Resource = ResourceLoader.load_threaded_get(path)
|
||||
var mesh := _m2_mesh_resource_extractor.extract_first_mesh(resource)
|
||||
if mesh != null:
|
||||
_m2_mesh_resource_cache_state.store_mesh(
|
||||
normalized_rel,
|
||||
_prepare_m2_mesh_for_runtime(normalized_rel, mesh)
|
||||
)
|
||||
else:
|
||||
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
|
||||
_m2_mesh_resource_finalizer.finalize_next_resource(
|
||||
_m2_mesh_load_pipeline_state,
|
||||
_m2_mesh_resource_cache_state,
|
||||
_m2_prototype_cache_state,
|
||||
extracted_dir
|
||||
)
|
||||
|
||||
|
||||
func _process_m2_build_jobs() -> void:
|
||||
@@ -4606,30 +4592,17 @@ func _normalize_m2_rel_path(rel_path: String) -> String:
|
||||
return normalized_rel
|
||||
|
||||
|
||||
func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
|
||||
if mesh == null:
|
||||
return null
|
||||
if not _m2_runtime_mesh_finalizer.requires_raw_data_for_refresh(mesh):
|
||||
return mesh
|
||||
var data := _m2_raw_model_repository.load_static_model_data(
|
||||
extracted_dir,
|
||||
normalized_rel
|
||||
)
|
||||
return _m2_runtime_mesh_finalizer.finalize_mesh(
|
||||
normalized_rel,
|
||||
mesh,
|
||||
data,
|
||||
extracted_dir
|
||||
)
|
||||
|
||||
|
||||
func _prepare_m2_node_for_runtime(normalized_rel: String, root: Node3D) -> void:
|
||||
if root == null:
|
||||
return
|
||||
for mesh_instance in _m2_animated_scene_finalizer.mesh_instances_in_subtree(root):
|
||||
if mesh_instance == null or mesh_instance.mesh == null:
|
||||
continue
|
||||
var prepared := _prepare_m2_mesh_for_runtime(normalized_rel, mesh_instance.mesh)
|
||||
var prepared := _m2_mesh_resource_finalizer.prepare_mesh_for_runtime(
|
||||
normalized_rel,
|
||||
mesh_instance.mesh,
|
||||
extracted_dir
|
||||
)
|
||||
if prepared != null:
|
||||
mesh_instance.mesh = prepared
|
||||
|
||||
@@ -4643,7 +4616,11 @@ func _get_or_load_m2_mesh(rel_path: String) -> Mesh:
|
||||
return null
|
||||
var mesh := _m2_mesh_resource_extractor.find_first_mesh_in_subtree(prototype)
|
||||
if mesh != null and not normalized_rel.is_empty():
|
||||
mesh = _prepare_m2_mesh_for_runtime(normalized_rel, mesh)
|
||||
mesh = _m2_mesh_resource_finalizer.prepare_mesh_for_runtime(
|
||||
normalized_rel,
|
||||
mesh,
|
||||
extracted_dir
|
||||
)
|
||||
_m2_mesh_resource_cache_state.store_mesh(normalized_rel, mesh)
|
||||
return mesh
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ extends SceneTree
|
||||
const PIPELINE_SCRIPT := preload("res://src/render/m2/m2_mesh_load_pipeline_state.gd")
|
||||
const PIPELINE_PATH := "res://src/render/m2/m2_mesh_load_pipeline_state.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
const RESOURCE_FINALIZER_PATH := "res://src/render/m2/m2_mesh_resource_finalizer.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
@@ -86,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 loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
var resource_finalizer_source := FileAccess.get_file_as_string(
|
||||
RESOURCE_FINALIZER_PATH
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("M2_MESH_LOAD_PIPELINE_STATE_SCRIPT.new()"),
|
||||
"loader composes pipeline state",
|
||||
@@ -105,15 +109,19 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"two existing clear sites delegate",
|
||||
failures
|
||||
)
|
||||
for retained_loader_rule in [
|
||||
for retained_renderer_rule in [
|
||||
"ResourceLoader.load_threaded_request(",
|
||||
"ResourceLoader.load_threaded_get_status(path)",
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
"ResourceLoader.load_threaded_get_status(resource_path)",
|
||||
"ResourceLoader.load_threaded_get(resource_path)",
|
||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE",
|
||||
"_m2_mesh_resource_cache_state.store_mesh(",
|
||||
"_m2_prototype_cache_state.mark_model_missing(normalized_rel)",
|
||||
"\"store_mesh\"",
|
||||
"\"mark_model_missing\"",
|
||||
]:
|
||||
_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.",
|
||||
|
||||
@@ -7,6 +7,7 @@ const CACHE_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_cache_state.
|
||||
const CACHE_PATH := "res://src/render/m2/m2_mesh_resource_cache_state.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
const STATIC_OBSERVER_PATH := "res://src/render/m2/m2_static_build_resource_observer.gd"
|
||||
const RESOURCE_FINALIZER_PATH := "res://src/render/m2/m2_mesh_resource_finalizer.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
@@ -70,6 +71,9 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var cache_source := FileAccess.get_file_as_string(CACHE_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
var observer_source := FileAccess.get_file_as_string(STATIC_OBSERVER_PATH)
|
||||
var resource_finalizer_source := FileAccess.get_file_as_string(
|
||||
RESOURCE_FINALIZER_PATH
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new()"),
|
||||
"loader composes Mesh cache state",
|
||||
@@ -77,7 +81,8 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
)
|
||||
_expect_false(loader_source.contains("var _m2_mesh_cache:"), "legacy Mesh cache removed", failures)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_mesh_resource_cache_state.store_mesh("),
|
||||
loader_source.count("_m2_mesh_resource_cache_state.store_mesh(")
|
||||
+ resource_finalizer_source.count("\"store_mesh\""),
|
||||
2,
|
||||
"two existing stores delegate",
|
||||
failures
|
||||
@@ -95,14 +100,18 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"final shutdown clear delegates",
|
||||
failures
|
||||
)
|
||||
for retained_loader_rule in [
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
"_m2_mesh_resource_extractor.extract_first_mesh(resource)",
|
||||
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
|
||||
"_m2_prototype_cache_state.mark_model_missing(normalized_rel)",
|
||||
for retained_renderer_rule in [
|
||||
"ResourceLoader.load_threaded_get(resource_path)",
|
||||
"\"extract_first_mesh\"",
|
||||
"prepare_mesh_for_runtime(",
|
||||
"\"mark_model_missing\"",
|
||||
"_m2_prototype_cache_state.adopt_static_prototype(",
|
||||
]:
|
||||
_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.",
|
||||
|
||||
@@ -6,6 +6,7 @@ extends SceneTree
|
||||
const EXTRACTOR_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_extractor.gd")
|
||||
const EXTRACTOR_PATH := "res://src/render/m2/m2_mesh_resource_extractor.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
const RESOURCE_FINALIZER_PATH := "res://src/render/m2/m2_mesh_resource_finalizer.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
@@ -94,6 +95,9 @@ func _verify_packed_scene_mesh_and_temporary_lifetime(failures: Array[String]) -
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var extractor_source := FileAccess.get_file_as_string(EXTRACTOR_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
var resource_finalizer_source := FileAccess.get_file_as_string(
|
||||
RESOURCE_FINALIZER_PATH
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()"),
|
||||
"loader composes Mesh resource extractor",
|
||||
@@ -110,7 +114,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_mesh_resource_extractor.extract_first_mesh("),
|
||||
resource_finalizer_source.count("\"extract_first_mesh\""),
|
||||
1,
|
||||
"threaded Resource finalization delegates",
|
||||
failures
|
||||
@@ -121,14 +125,18 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"synchronous fallback delegates",
|
||||
failures
|
||||
)
|
||||
for retained_loader_rule in [
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
|
||||
"_m2_mesh_resource_cache_state.store_mesh(",
|
||||
for retained_renderer_rule in [
|
||||
"ResourceLoader.load_threaded_get(resource_path)",
|
||||
"prepare_mesh_for_runtime(",
|
||||
"\"store_mesh\"",
|
||||
"M2_BUILDER_SCRIPT.build(data, extracted_dir)",
|
||||
"_m2_prototype_cache_state.mark_model_missing(normalized_rel)",
|
||||
"\"mark_model_missing\"",
|
||||
]:
|
||||
_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.",
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
extends SceneTree
|
||||
|
||||
const FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_mesh_resource_finalizer.gd"
|
||||
)
|
||||
const PIPELINE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_mesh_load_pipeline_state.gd"
|
||||
)
|
||||
const MESH_CACHE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_mesh_resource_cache_state.gd"
|
||||
)
|
||||
const PROTOTYPE_CACHE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_prototype_cache_state.gd"
|
||||
)
|
||||
const FINALIZER_PATH := "res://src/render/m2/m2_mesh_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 FakeMeshResourceExtractor extends RefCounted:
|
||||
var mesh_by_resource: Dictionary = {}
|
||||
var resources: Array[Resource] = []
|
||||
|
||||
func extract_first_mesh(resource: Resource) -> Mesh:
|
||||
resources.append(resource)
|
||||
return mesh_by_resource.get(resource, null) as Mesh
|
||||
|
||||
|
||||
class FakeRuntimeMeshFinalizer extends RefCounted:
|
||||
var raw_data_required: bool = false
|
||||
var finalized_mesh: Mesh = null
|
||||
var requirement_meshes: Array[Mesh] = []
|
||||
var finalize_calls: Array[Dictionary] = []
|
||||
|
||||
func requires_raw_data_for_refresh(mesh: Mesh) -> bool:
|
||||
requirement_meshes.append(mesh)
|
||||
return raw_data_required
|
||||
|
||||
func finalize_mesh(
|
||||
normalized_relative_path: String,
|
||||
mesh: Mesh,
|
||||
raw_model_data: Dictionary,
|
||||
extracted_directory: String
|
||||
) -> Mesh:
|
||||
finalize_calls.append({
|
||||
"normalized": normalized_relative_path,
|
||||
"mesh": mesh,
|
||||
"raw_model_data": raw_model_data,
|
||||
"extracted_directory": extracted_directory,
|
||||
})
|
||||
return finalized_mesh
|
||||
|
||||
|
||||
class FakeRawModelRepository extends RefCounted:
|
||||
var result: Dictionary = {}
|
||||
var calls: Array[Dictionary] = []
|
||||
|
||||
func load_static_model_data(
|
||||
extracted_directory: String,
|
||||
normalized_relative_path: String
|
||||
) -> Dictionary:
|
||||
calls.append({
|
||||
"extracted_directory": extracted_directory,
|
||||
"normalized": normalized_relative_path,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
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_current_mesh_adoption(failures)
|
||||
_verify_stale_mesh_preparation(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_MESH_RESOURCE_FINALIZER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_MESH_RESOURCE_FINALIZER PASS "
|
||||
+ "cases=31 iterations=1000 elapsed_ms=%.3f" % elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_polling(failures: Array[String]) -> void:
|
||||
var dependencies := _dependencies()
|
||||
var service: RefCounted = dependencies.service
|
||||
var resource_loader: FakeResourceLoaderAdapter = dependencies.resource_loader
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
for request in [
|
||||
["world/pending.m2", "res://pending.tres"],
|
||||
["world/loaded.m2", "res://loaded.tres"],
|
||||
["world/failed.m2", "res://failed.tres"],
|
||||
]:
|
||||
pipeline.call("remember_request", request[0], request[1])
|
||||
resource_loader.statuses = {
|
||||
"res://pending.tres": ResourceLoader.THREAD_LOAD_IN_PROGRESS,
|
||||
"res://loaded.tres": ResourceLoader.THREAD_LOAD_LOADED,
|
||||
"res://failed.tres": 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 retained", failures)
|
||||
_expect_equal(int(pipeline.call("finalize_record_count")), 2, "two finalize records", failures)
|
||||
_expect_string_array(
|
||||
resource_loader.status_paths,
|
||||
["res://pending.tres", "res://loaded.tres", "res://failed.tres"],
|
||||
"status insertion order",
|
||||
failures
|
||||
)
|
||||
prototype_cache.call("clear_and_release")
|
||||
|
||||
|
||||
func _verify_empty_path_polling(failures: Array[String]) -> void:
|
||||
var dependencies := _dependencies()
|
||||
var pipeline := EmptyPathPipeline.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
_expect_equal(
|
||||
int(dependencies.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_model_missing", "world/empty.m2")),
|
||||
"empty resource path marks missing",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
dependencies.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 dependencies := _dependencies()
|
||||
var service: RefCounted = dependencies.service
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var mesh_cache: RefCounted = MESH_CACHE_SCRIPT.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
_expect_false(
|
||||
bool(service.call(
|
||||
"finalize_next_resource",
|
||||
pipeline,
|
||||
mesh_cache,
|
||||
prototype_cache,
|
||||
"res://data/extracted"
|
||||
)),
|
||||
"empty finalize FIFO",
|
||||
failures
|
||||
)
|
||||
_enqueue_terminal(
|
||||
pipeline,
|
||||
"world/failed.m2",
|
||||
"res://failed.tres",
|
||||
ResourceLoader.THREAD_LOAD_FAILED
|
||||
)
|
||||
_expect_true(
|
||||
bool(service.call(
|
||||
"finalize_next_resource", pipeline, mesh_cache,
|
||||
prototype_cache, "res://data/extracted"
|
||||
)),
|
||||
"failed record consumed",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(prototype_cache.call("is_model_missing", "world/failed.m2")),
|
||||
"failed record marks missing",
|
||||
failures
|
||||
)
|
||||
var cached_mesh := ArrayMesh.new()
|
||||
mesh_cache.call("store_mesh", "world/cached.m2", cached_mesh)
|
||||
_enqueue_terminal(
|
||||
pipeline,
|
||||
"world/cached.m2",
|
||||
"res://cached.tres",
|
||||
ResourceLoader.THREAD_LOAD_LOADED
|
||||
)
|
||||
service.call(
|
||||
"finalize_next_resource", pipeline, mesh_cache,
|
||||
prototype_cache, "res://data/extracted"
|
||||
)
|
||||
_enqueue_terminal(
|
||||
pipeline,
|
||||
"world/null.m2",
|
||||
"res://null.tres",
|
||||
ResourceLoader.THREAD_LOAD_LOADED
|
||||
)
|
||||
service.call(
|
||||
"finalize_next_resource", pipeline, mesh_cache,
|
||||
prototype_cache, "res://data/extracted"
|
||||
)
|
||||
_expect_true(
|
||||
bool(prototype_cache.call("is_model_missing", "world/null.m2")),
|
||||
"null Resource marks missing",
|
||||
failures
|
||||
)
|
||||
_expect_string_array(
|
||||
dependencies.resource_loader.load_paths,
|
||||
["res://null.tres"],
|
||||
"cached record skips terminal get",
|
||||
failures
|
||||
)
|
||||
mesh_cache.call("clear")
|
||||
prototype_cache.call("clear_and_release")
|
||||
|
||||
|
||||
func _verify_current_mesh_adoption(failures: Array[String]) -> void:
|
||||
var dependencies := _dependencies()
|
||||
var resource := Resource.new()
|
||||
var mesh := ArrayMesh.new()
|
||||
dependencies.resource_loader.resources["res://current.tres"] = resource
|
||||
dependencies.extractor.mesh_by_resource[resource] = mesh
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var mesh_cache: RefCounted = MESH_CACHE_SCRIPT.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
_enqueue_terminal(
|
||||
pipeline,
|
||||
"world/current.m2",
|
||||
"res://current.tres",
|
||||
ResourceLoader.THREAD_LOAD_LOADED
|
||||
)
|
||||
_expect_true(
|
||||
bool(dependencies.service.call(
|
||||
"finalize_next_resource", pipeline, mesh_cache,
|
||||
prototype_cache, "res://custom/extracted"
|
||||
)),
|
||||
"current Mesh record consumed",
|
||||
failures
|
||||
)
|
||||
_expect_same(
|
||||
mesh_cache.call("find_mesh", "world/current.m2"),
|
||||
mesh,
|
||||
"current Mesh exact adoption",
|
||||
failures
|
||||
)
|
||||
_expect_equal(dependencies.raw_repository.calls.size(), 0, "current Mesh skips raw", failures)
|
||||
_expect_equal(
|
||||
dependencies.runtime_finalizer.finalize_calls.size(),
|
||||
0,
|
||||
"current Mesh skips finalize",
|
||||
failures
|
||||
)
|
||||
mesh_cache.call("clear")
|
||||
prototype_cache.call("clear_and_release")
|
||||
|
||||
|
||||
func _verify_stale_mesh_preparation(failures: Array[String]) -> void:
|
||||
var dependencies := _dependencies()
|
||||
dependencies.runtime_finalizer.raw_data_required = true
|
||||
dependencies.raw_repository.result = {"vertices": [1]}
|
||||
var resource := Resource.new()
|
||||
var stale_mesh := ArrayMesh.new()
|
||||
var prepared_mesh := ArrayMesh.new()
|
||||
dependencies.runtime_finalizer.finalized_mesh = prepared_mesh
|
||||
dependencies.resource_loader.resources["res://stale.tres"] = resource
|
||||
dependencies.extractor.mesh_by_resource[resource] = stale_mesh
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var mesh_cache: RefCounted = MESH_CACHE_SCRIPT.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
_enqueue_terminal(
|
||||
pipeline,
|
||||
"world/stale.m2",
|
||||
"res://stale.tres",
|
||||
ResourceLoader.THREAD_LOAD_LOADED
|
||||
)
|
||||
dependencies.service.call(
|
||||
"finalize_next_resource", pipeline, mesh_cache,
|
||||
prototype_cache, "res://custom/extracted"
|
||||
)
|
||||
_expect_same(
|
||||
mesh_cache.call("find_mesh", "world/stale.m2"),
|
||||
prepared_mesh,
|
||||
"prepared Mesh adoption",
|
||||
failures
|
||||
)
|
||||
_expect_equal(dependencies.raw_repository.calls.size(), 1, "one raw read", failures)
|
||||
_expect_string_equal(
|
||||
String(dependencies.raw_repository.calls[0].normalized),
|
||||
"world/stale.m2",
|
||||
"raw normalized path",
|
||||
failures
|
||||
)
|
||||
_expect_string_equal(
|
||||
String(dependencies.raw_repository.calls[0].extracted_directory),
|
||||
"res://custom/extracted",
|
||||
"raw extracted directory",
|
||||
failures
|
||||
)
|
||||
_expect_same(
|
||||
dependencies.runtime_finalizer.finalize_calls[0].mesh,
|
||||
stale_mesh,
|
||||
"runtime finalizer source Mesh",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
dependencies.runtime_finalizer.finalize_calls[0].raw_model_data
|
||||
== dependencies.raw_repository.result,
|
||||
"runtime finalizer raw data",
|
||||
failures
|
||||
)
|
||||
mesh_cache.call("clear")
|
||||
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_mesh_loads()")
|
||||
var drain_end := loader_source.find("func _process_m2_build_jobs()", drain_start)
|
||||
var drain_source := loader_source.substr(drain_start, drain_end - drain_start)
|
||||
_expect_true(
|
||||
drain_source.contains("_m2_mesh_resource_finalizer.poll_terminal_requests("),
|
||||
"loader delegates polling",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
drain_source.contains("_m2_mesh_resource_finalizer.finalize_next_resource("),
|
||||
"loader delegates terminal finalization",
|
||||
failures
|
||||
)
|
||||
for removed_token in [
|
||||
"ResourceLoader.load_threaded_get_status",
|
||||
"ResourceLoader.load_threaded_get(",
|
||||
"_m2_mesh_resource_extractor.extract_first_mesh",
|
||||
"_m2_mesh_resource_cache_state.store_mesh",
|
||||
"_m2_prototype_cache_state.mark_model_missing",
|
||||
]:
|
||||
_expect_false(
|
||||
drain_source.contains(removed_token),
|
||||
"loader drain omits %s" % removed_token,
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_source.contains("func _prepare_m2_mesh_for_runtime("),
|
||||
"legacy loader preparation removed",
|
||||
failures
|
||||
)
|
||||
for required_token in [
|
||||
"ResourceLoader.load_threaded_get_status",
|
||||
"ResourceLoader.load_threaded_get(",
|
||||
"\"extract_first_mesh\"",
|
||||
"\"store_mesh\"",
|
||||
"\"mark_model_missing\"",
|
||||
"\"load_static_model_data\"",
|
||||
"\"finalize_mesh\"",
|
||||
]:
|
||||
_expect_true(
|
||||
service_source.contains(required_token),
|
||||
"service owns %s" % required_token,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var dependencies := _dependencies()
|
||||
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):
|
||||
dependencies.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 bounded", failures)
|
||||
prototype_cache.call("clear_and_release")
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _dependencies() -> Dictionary:
|
||||
var resource_loader := FakeResourceLoaderAdapter.new()
|
||||
var extractor := FakeMeshResourceExtractor.new()
|
||||
var runtime_finalizer := FakeRuntimeMeshFinalizer.new()
|
||||
var raw_repository := FakeRawModelRepository.new()
|
||||
return {
|
||||
"resource_loader": resource_loader,
|
||||
"extractor": extractor,
|
||||
"runtime_finalizer": runtime_finalizer,
|
||||
"raw_repository": raw_repository,
|
||||
"service": FINALIZER_SCRIPT.new(
|
||||
extractor,
|
||||
runtime_finalizer,
|
||||
raw_repository,
|
||||
resource_loader
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
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_same(
|
||||
actual: Variant,
|
||||
expected: Variant,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
_expect_true(is_same(actual, expected), 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://by0rlnw5g8hjr
|
||||
@@ -7,6 +7,7 @@ const REPOSITORY_PATH := "res://src/render/m2/m2_raw_model_repository.gd"
|
||||
const NATIVE_OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_native_animation_resource_observer.gd"
|
||||
)
|
||||
const MESH_FINALIZER_PATH := "res://src/render/m2/m2_mesh_resource_finalizer.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
@@ -60,6 +61,7 @@ func _verify_rejected_inputs(failures: Array[String]) -> void:
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var repository_source := FileAccess.get_file_as_string(REPOSITORY_PATH)
|
||||
var native_observer_source := FileAccess.get_file_as_string(NATIVE_OBSERVER_PATH)
|
||||
var mesh_finalizer_source := FileAccess.get_file_as_string(MESH_FINALIZER_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(
|
||||
loader_source.contains("M2_RAW_MODEL_REPOSITORY_SCRIPT.new()"),
|
||||
@@ -67,7 +69,8 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_raw_model_repository.load_static_model_data("),
|
||||
loader_source.count("_m2_raw_model_repository.load_static_model_data(")
|
||||
+ mesh_finalizer_source.count("\"load_static_model_data\""),
|
||||
2,
|
||||
"two static callers delegate",
|
||||
failures
|
||||
|
||||
Reference in New Issue
Block a user