render: extract static M2 build resource observer

This commit is contained in:
2026-07-18 03:01:43 +04:00
parent f062ea91cd
commit 7cb3e3412f
16 changed files with 540 additions and 84 deletions
@@ -0,0 +1,126 @@
class_name M2StaticBuildResourceObserver
extends RefCounted
## Resolves or requests one static M2 build Mesh and fills a resource snapshot.
const OUTCOME_REJECTED := &"rejected"
const OUTCOME_CACHED := &"cached"
const OUTCOME_PENDING := &"pending"
const OUTCOME_REQUESTED := &"requested"
const OUTCOME_MISSING := &"missing"
## Observes cache/request state and adopts the exact static result into snapshot.
func observe(
resource_snapshot: RefCounted,
normalized_relative_path: String,
cache_directory: String,
mesh_cache_state: RefCounted,
prototype_cache_state: RefCounted,
load_pipeline_state: RefCounted
) -> StringName:
if (
resource_snapshot == null
or normalized_relative_path.is_empty()
or mesh_cache_state == null
or prototype_cache_state == null
or load_pipeline_state == null
):
return OUTCOME_REJECTED
if bool(mesh_cache_state.call("has_mesh", normalized_relative_path)):
resource_snapshot.call(
"adopt_static_observation",
mesh_cache_state.call("find_mesh", normalized_relative_path) as Mesh,
false
)
return OUTCOME_CACHED
if bool(prototype_cache_state.call("is_model_missing", normalized_relative_path)):
resource_snapshot.call("adopt_static_observation", null, true)
return OUTCOME_MISSING
if bool(load_pipeline_state.call("has_request", normalized_relative_path)):
resource_snapshot.call("adopt_static_observation", null, false)
return OUTCOME_PENDING
for cache_resource_path in cache_resource_paths(
cache_directory,
normalized_relative_path,
[".tscn", ".glb"]
):
if not ResourceLoader.exists(cache_resource_path):
continue
if (
cache_resource_path.get_extension().to_lower() == "glb"
and glb_animation_schema(cache_resource_path) == "pivot_prefix_v1"
):
continue
var request_error := ResourceLoader.load_threaded_request(
cache_resource_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE
)
if request_error == OK or request_error == ERR_BUSY:
load_pipeline_state.call(
"remember_request",
normalized_relative_path,
cache_resource_path
)
resource_snapshot.call("adopt_static_observation", null, false)
return OUTCOME_REQUESTED
break
prototype_cache_state.call("mark_model_missing", normalized_relative_path)
resource_snapshot.call("adopt_static_observation", null, true)
return OUTCOME_MISSING
## Returns historical nested/lowercase/basename cache candidates without I/O.
func cache_resource_paths(
cache_directory: String,
relative_path: String,
extensions: Array[String]
) -> PackedStringArray:
var normalized := relative_path.replace("\\", "/")
var lower := normalized.to_lower()
var stems := [
normalized.get_basename(),
lower.get_basename(),
normalized.get_file().get_basename(),
lower.get_file().get_basename(),
]
var result := PackedStringArray()
for extension in extensions:
for stem in stems:
if stem.is_empty():
continue
var path := cache_directory.path_join(stem + extension)
if not result.has(path):
result.append(path)
return result
## Reads only the OpenWC animation schema marker from a GLB JSON chunk.
func glb_animation_schema(cache_resource_path: String) -> String:
var absolute_path := ProjectSettings.globalize_path(cache_resource_path)
if not FileAccess.file_exists(absolute_path):
return ""
var file := FileAccess.open(absolute_path, FileAccess.READ)
if file == null or file.get_length() < 20:
return ""
var magic := file.get_32()
var version := file.get_32()
file.get_32()
if magic != 0x46546c67 or version != 2:
return ""
var json_length := int(file.get_32())
var chunk_type := file.get_32()
if json_length <= 0 or chunk_type != 0x4e4f534a:
return ""
var parsed: Variant = JSON.parse_string(
file.get_buffer(json_length).get_string_from_utf8()
)
if not (parsed is Dictionary):
return ""
var asset: Dictionary = (parsed as Dictionary).get("asset", {})
var extras: Dictionary = asset.get("extras", {})
return String(extras.get("openwc_m2_anim_schema", ""))
@@ -0,0 +1 @@
uid://c0e3p80ld1dfb
+19 -65
View File
@@ -70,6 +70,9 @@ const M2_BUILD_QUEUE_SCRIPT := preload("res://src/render/m2/m2_build_queue.gd")
const M2_STATIC_BATCH_MATERIALIZER_SCRIPT := preload(
"res://src/render/m2/m2_static_batch_materializer.gd"
)
const M2_STATIC_BUILD_RESOURCE_OBSERVER_SCRIPT := preload(
"res://src/render/m2/m2_static_build_resource_observer.gd"
)
const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
"res://src/render/m2/m2_runtime_mesh_finalizer.gd"
)
@@ -277,6 +280,9 @@ var _m2_group_result_mutex := Mutex.new()
var _m2_group_result_queue: Array = []
var _m2_build_queue_state := M2_BUILD_QUEUE_SCRIPT.new()
var _m2_build_dispatch_planner := M2_BUILD_DISPATCH_PLANNER_SCRIPT.new()
var _m2_static_build_resource_observer := (
M2_STATIC_BUILD_RESOURCE_OBSERVER_SCRIPT.new()
)
var _m2_unique_placement_registry := (
M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()
)
@@ -4373,14 +4379,13 @@ func _process_m2_build_jobs() -> void:
batch_count > 0
and not bool(resource_snapshot.call("has_animated_prototype"))
):
var static_mesh := _get_m2_mesh_or_request(rel_path)
resource_snapshot.call(
"adopt_static_observation",
static_mesh,
(
static_mesh == null
and _m2_prototype_cache_state.is_model_missing(normalized_rel)
)
_m2_static_build_resource_observer.observe(
resource_snapshot,
normalized_rel,
m2_cache_dir,
_m2_mesh_resource_cache_state,
_m2_prototype_cache_state,
_m2_mesh_load_pipeline_state
)
var dispatch_plan: Dictionary = _m2_build_dispatch_planner.plan_step(
batch_count,
@@ -4587,40 +4592,6 @@ func _normalize_m2_rel_path(rel_path: String) -> String:
return normalized_rel
func _get_m2_mesh_or_request(rel_path: String) -> Mesh:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if normalized_rel.is_empty():
return null
if _m2_mesh_resource_cache_state.has_mesh(normalized_rel):
return _m2_mesh_resource_cache_state.find_mesh(normalized_rel)
if _m2_prototype_cache_state.is_model_missing(normalized_rel):
return null
_request_m2_mesh_load(normalized_rel)
return null
func _request_m2_mesh_load(normalized_rel: String) -> void:
if normalized_rel.is_empty() or _m2_mesh_load_pipeline_state.has_request(normalized_rel):
return
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn", ".glb"]):
if not ResourceLoader.exists(cache_res_path):
continue
if cache_res_path.get_extension().to_lower() == "glb" and _glb_cache_animation_schema(cache_res_path) == "pivot_prefix_v1":
continue
var err := ResourceLoader.load_threaded_request(
cache_res_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE)
if err == OK or err == ERR_BUSY:
_m2_mesh_load_pipeline_state.remember_request(normalized_rel, cache_res_path)
return
break
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
if mesh == null:
return null
@@ -4820,7 +4791,12 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn", ".glb"]):
if not ResourceLoader.exists(cache_res_path):
continue
if cache_res_path.get_extension().to_lower() == "glb" and _glb_cache_animation_schema(cache_res_path) == "pivot_prefix_v1":
if (
cache_res_path.get_extension().to_lower() == "glb"
and _m2_static_build_resource_observer.glb_animation_schema(
cache_res_path
) == "pivot_prefix_v1"
):
continue
var resource: Resource = load(cache_res_path)
if resource is PackedScene:
@@ -5041,28 +5017,6 @@ func _glb_primitive_count(gltf: Dictionary) -> int:
return count
func _glb_cache_animation_schema(cache_res_path: String) -> String:
var abs_path := ProjectSettings.globalize_path(cache_res_path)
if not FileAccess.file_exists(abs_path):
return ""
var file := FileAccess.open(abs_path, FileAccess.READ)
if file == null or file.get_length() < 20:
return ""
var magic := file.get_32()
var version := file.get_32()
file.get_32()
if magic != 0x46546c67 or version != 2:
return ""
var json_len := int(file.get_32())
var chunk_type := file.get_32()
if json_len <= 0 or chunk_type != 0x4e4f534a:
return ""
var parsed = JSON.parse_string(file.get_buffer(json_len).get_string_from_utf8())
if parsed is Dictionary:
return _glb_animation_schema(parsed as Dictionary)
return ""
func _glb_animation_schema(gltf: Dictionary) -> String:
var asset: Dictionary = gltf.get("asset", {})
var extras: Dictionary = asset.get("extras", {})
@@ -160,7 +160,7 @@ func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void:
failures
)
_expect_true(
loader_source.contains("_get_m2_mesh_or_request(rel_path)")
loader_source.contains("_m2_static_build_resource_observer.observe(")
and loader_source.contains("_materialize_m2_animated_batch(")
and loader_source.contains("_materialize_m2_group_batch(")
and loader_source.contains("try_consume_permit("),
@@ -8,6 +8,7 @@ const SNAPSHOT_SCRIPT := preload(
)
const SNAPSHOT_PATH := "res://src/render/m2/m2_build_resource_snapshot.gd"
const DISPATCH_PATH := "res://src/render/m2/m2_build_dispatch_planner.gd"
const STATIC_OBSERVER_PATH := "res://src/render/m2/m2_static_build_resource_observer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
@@ -134,10 +135,10 @@ func _verify_engine_lifetime(failures: Array[String]) -> void:
func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void:
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
var dispatch_source := FileAccess.get_file_as_string(DISPATCH_PATH)
var static_observer_source := FileAccess.get_file_as_string(STATIC_OBSERVER_PATH)
var snapshot_source := FileAccess.get_file_as_string(SNAPSHOT_PATH)
for delegated_token in [
"M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(",
"\"adopt_static_observation\"",
"resource_snapshot.call(\"animated_prototype\")",
"resource_snapshot.call(\"static_mesh\")",
]:
@@ -146,6 +147,11 @@ func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void:
"loader uses %s" % delegated_token,
failures
)
_expect_true(
static_observer_source.contains("\"adopt_static_observation\""),
"static observer adopts snapshot",
failures
)
_expect_true(
dispatch_source.contains("resource_snapshot.call(\"animation_request_pending\")")
and dispatch_source.contains("resource_snapshot.call(\"has_static_mesh\")"),
@@ -6,6 +6,7 @@ extends SceneTree
const CACHE_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_cache_state.gd")
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"
func _initialize() -> void:
@@ -68,6 +69,7 @@ func _verify_clear_and_diagnostics(failures: Array[String]) -> void:
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)
_expect_true(
loader_source.contains("M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new()"),
"loader composes Mesh cache state",
@@ -81,7 +83,8 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
failures
)
_expect_equal(
loader_source.count("_m2_mesh_resource_cache_state.find_mesh("),
loader_source.count("_m2_mesh_resource_cache_state.find_mesh(")
+ observer_source.count("\"find_mesh\""),
2,
"two existing lookups delegate",
failures
@@ -0,0 +1,127 @@
extends SceneTree
const OBSERVER_SCRIPT := preload("res://src/render/m2/m2_static_build_resource_observer.gd")
const SNAPSHOT_SCRIPT := preload("res://src/render/m2/m2_build_resource_snapshot.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 PIPELINE_SCRIPT := preload("res://src/render/m2/m2_mesh_load_pipeline_state.gd")
const OBSERVER_PATH := "res://src/render/m2/m2_static_build_resource_observer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_invalid_cached_missing_pending(failures)
_verify_request_and_path_order(failures)
_verify_source_boundaries(failures)
var elapsed := _verify_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_STATIC_BUILD_RESOURCE_OBSERVER: %s" % failure)
quit(1)
return
print("M2_STATIC_BUILD_RESOURCE_OBSERVER PASS cases=11 iterations=20000 elapsed_ms=%.3f" % elapsed)
quit(0)
func _verify_invalid_cached_missing_pending(failures: Array[String]) -> void:
var observer: RefCounted = OBSERVER_SCRIPT.new()
var mesh_cache: RefCounted = MESH_CACHE_SCRIPT.new()
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
var snapshot: RefCounted = SNAPSHOT_SCRIPT.new("world/a.m2", null, false)
_expect(
observer.call(
"observe", null, "world/a.m2", "res://none",
mesh_cache, prototype_cache, pipeline
)
== OBSERVER_SCRIPT.OUTCOME_REJECTED,
"null snapshot rejected", failures
)
var mesh := ArrayMesh.new()
mesh_cache.call("store_mesh", "world/a.m2", mesh)
_expect(
observer.call(
"observe", snapshot, "world/a.m2", "res://none",
mesh_cache, prototype_cache, pipeline
)
== OBSERVER_SCRIPT.OUTCOME_CACHED, "cached outcome", failures
)
_expect(is_same(snapshot.call("static_mesh"), mesh), "cached Mesh identity", failures)
mesh_cache.call("clear")
prototype_cache.call("mark_model_missing", "world/a.m2")
_expect(
observer.call(
"observe", snapshot, "world/a.m2", "res://none",
mesh_cache, prototype_cache, pipeline
)
== OBSERVER_SCRIPT.OUTCOME_MISSING, "missing outcome", failures
)
_expect(bool(snapshot.call("static_model_missing")), "missing adopted", failures)
prototype_cache = PROTOTYPE_CACHE_SCRIPT.new()
pipeline.call("remember_request", "world/a.m2", "res://pending.tscn")
_expect(
observer.call(
"observe", snapshot, "world/a.m2", "res://none",
mesh_cache, prototype_cache, pipeline
)
== OBSERVER_SCRIPT.OUTCOME_PENDING, "pending outcome", failures
)
_expect(not bool(snapshot.call("static_model_missing")), "pending is not missing", failures)
pipeline.call("clear")
prototype_cache.call("clear_and_release")
func _verify_request_and_path_order(failures: Array[String]) -> void:
var observer: RefCounted = OBSERVER_SCRIPT.new()
var extensions: Array[String] = [".tscn", ".glb"]
var paths: PackedStringArray = observer.call(
"cache_resource_paths", "res://cache", "World/Foo/Bar.M2", extensions
)
var expected := PackedStringArray([
"res://cache/World/Foo/Bar.tscn", "res://cache/world/foo/bar.tscn",
"res://cache/Bar.tscn", "res://cache/bar.tscn",
"res://cache/World/Foo/Bar.glb", "res://cache/world/foo/bar.glb",
"res://cache/Bar.glb", "res://cache/bar.glb",
])
_expect(paths == expected, "candidate order", failures)
_expect(
observer.call("glb_animation_schema", "res://missing.glb") == "",
"missing GLB schema empty", failures
)
func _verify_source_boundaries(failures: Array[String]) -> void:
var loader := FileAccess.get_file_as_string(LOADER_PATH)
var observer := FileAccess.get_file_as_string(OBSERVER_PATH)
_expect(
loader.contains("_m2_static_build_resource_observer.observe("),
"loader delegates observer", failures
)
_expect(not loader.contains("func _get_m2_mesh_or_request"), "legacy lookup removed", failures)
_expect(not loader.contains("func _request_m2_mesh_load"), "legacy request removed", failures)
_expect(
observer.contains("ResourceLoader.load_threaded_request("),
"observer owns request", failures
)
for forbidden in ["queue_free", "RenderingServer", "WorkerThreadPool", "Mutex"]:
_expect(not observer.contains(forbidden), "observer omits %s" % forbidden, failures)
func _verify_timing(failures: Array[String]) -> float:
var observer: RefCounted = OBSERVER_SCRIPT.new()
var extensions: Array[String] = [".tscn", ".glb"]
var started := Time.get_ticks_usec()
for index in range(20000):
observer.call(
"cache_resource_paths", "res://cache",
"world/%d/model.m2" % (index % 64), extensions
)
var elapsed := float(Time.get_ticks_usec() - started) / 1000.0
_expect(elapsed < 1000.0, "path planning bounded", failures)
return elapsed
func _expect(condition: bool, label: String, failures: Array[String]) -> void:
if not condition:
failures.append(label)
@@ -0,0 +1 @@
uid://b8bxs4swxsntd