render: extract cached M2 animation resource observer

This commit is contained in:
2026-07-18 10:23:03 +04:00
parent 3519f183bb
commit df87619220
17 changed files with 1065 additions and 197 deletions
@@ -0,0 +1,238 @@
class_name M2CachedAnimationResourceObserver
extends RefCounted
## Observes or requests the cached-GLB animated phase of one M2 build step.
const M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT := preload(
"res://src/render/m2/m2_build_resource_snapshot.gd"
)
## Returns a snapshot containing the exact cached prototype or pending state.
## Invalid composition and terminal failures produce a non-pending snapshot.
func observe(
normalized_relative_path: String,
cache_directory: String,
maximum_primitive_count: int,
allowlist_patterns: PackedStringArray,
denylist_patterns: PackedStringArray,
prototype_cache_state: RefCounted,
load_pipeline_state: RefCounted,
debug_logging_enabled: bool = false
) -> RefCounted:
var empty_snapshot: RefCounted = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
normalized_relative_path,
null,
false
)
if (
normalized_relative_path.is_empty()
or prototype_cache_state == null
or load_pipeline_state == null
):
return empty_snapshot
var cached_prototype := prototype_cache_state.call(
"find_animated_prototype",
normalized_relative_path
) as Node3D
if cached_prototype != null:
return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
normalized_relative_path,
cached_prototype,
false
)
if bool(prototype_cache_state.call(
"is_animation_static",
normalized_relative_path
)):
return empty_snapshot
if bool(load_pipeline_state.call("has_request", normalized_relative_path)):
return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
normalized_relative_path,
null,
true
)
var cache_resource_path := find_eligible_glb_cache_path(
normalized_relative_path,
cache_directory,
maximum_primitive_count,
allowlist_patterns,
denylist_patterns,
debug_logging_enabled
)
if cache_resource_path.is_empty():
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
return empty_snapshot
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
)
return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
normalized_relative_path,
null,
true
)
prototype_cache_state.call("mark_animation_static", normalized_relative_path)
return empty_snapshot
## Returns the first eligible historical `.glb` cache candidate.
func find_eligible_glb_cache_path(
normalized_relative_path: String,
cache_directory: String,
maximum_primitive_count: int,
allowlist_patterns: PackedStringArray,
denylist_patterns: PackedStringArray,
debug_logging_enabled: bool = false
) -> String:
if not is_animation_path_allowed(
normalized_relative_path,
allowlist_patterns,
denylist_patterns
):
return ""
for cache_resource_path in cache_resource_paths(
cache_directory,
normalized_relative_path
):
if not ResourceLoader.exists(cache_resource_path):
continue
if glb_cache_is_safe_for_runtime_animation(
cache_resource_path,
maximum_primitive_count,
debug_logging_enabled
):
return cache_resource_path
return ""
## Applies the historical stripped, case-insensitive allow/deny substring policy.
func is_animation_path_allowed(
normalized_relative_path: String,
allowlist_patterns: PackedStringArray,
denylist_patterns: PackedStringArray
) -> bool:
if normalized_relative_path.is_empty() or allowlist_patterns.is_empty():
return false
var lowercase_path := normalized_relative_path.to_lower()
var allowed := false
for pattern in allowlist_patterns:
var needle := String(pattern).strip_edges().to_lower()
if not needle.is_empty() and lowercase_path.contains(needle):
allowed = true
break
if not allowed:
return false
for pattern in denylist_patterns:
var needle := String(pattern).strip_edges().to_lower()
if not needle.is_empty() and lowercase_path.contains(needle):
return false
return true
## Returns historical nested/lowercase/basename `.glb` candidates without I/O.
func cache_resource_paths(
cache_directory: String,
relative_path: String
) -> PackedStringArray:
var normalized := relative_path.replace("\\", "/")
var lowercase := normalized.to_lower()
var stems := [
normalized.get_basename(),
lowercase.get_basename(),
normalized.get_file().get_basename(),
lowercase.get_file().get_basename(),
]
var result := PackedStringArray()
for stem in stems:
if stem.is_empty():
continue
var cache_resource_path := cache_directory.path_join(stem + ".glb")
if not result.has(cache_resource_path):
result.append(cache_resource_path)
return result
## Checks animation presence, primitive limit and accepted OpenWC schema.
func glb_cache_is_safe_for_runtime_animation(
cache_resource_path: String,
maximum_primitive_count: int,
debug_logging_enabled: bool = false
) -> bool:
var gltf := read_glb_json(cache_resource_path)
if gltf.is_empty():
return false
var animations: Array = gltf.get("animations", [])
if animations.is_empty():
return false
var primitive_count := glb_primitive_count(gltf)
if maximum_primitive_count > 0 and primitive_count > maximum_primitive_count:
return false
var schema := glb_animation_schema(gltf)
if schema == "pivot_prefix_v1" or schema.is_empty():
return true
if debug_logging_enabled:
print(
"M2_ANIM_REJECT schema=%s primitives=%d cache=%s"
% [schema, primitive_count, cache_resource_path]
)
return false
## Reads the JSON chunk of a version-2 GLB or returns an empty Dictionary.
func read_glb_json(cache_resource_path: String) -> Dictionary:
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()
)
return parsed as Dictionary if parsed is Dictionary else {}
## Counts all glTF Mesh primitives using the historical safety rule.
func glb_primitive_count(gltf: Dictionary) -> int:
var count := 0
var meshes: Array = gltf.get("meshes", [])
for mesh_variant in meshes:
if mesh_variant is Dictionary:
var primitives: Array = (mesh_variant as Dictionary).get(
"primitives",
[]
)
count += primitives.size()
return count
## Returns the OpenWC animation schema marker from parsed glTF metadata.
func glb_animation_schema(gltf: Dictionary) -> String:
var asset: Dictionary = gltf.get("asset", {})
var extras: Dictionary = asset.get("extras", {})
return String(extras.get("openwc_m2_anim_schema", ""))
@@ -0,0 +1 @@
uid://d0ptej41k2smt