refactor(M03): extract M2 runtime mesh finalizer

Work-Package: M03-RND-M2-RUNTIME-MESH-FINALIZER-001
Agent: sindo-main-codex
Tests: 37 headless renderer/coordinate contracts pass; checkpoint dry-run 7/7; documentation and coordination gates pass
Fidelity: preserves refresh version 2, classifier, rebuild and fallback transitions; no visual parity claim
This commit is contained in:
2026-07-17 15:01:52 +04:00
parent 951bd54fff
commit ece7724a28
15 changed files with 626 additions and 85 deletions
@@ -0,0 +1,78 @@
class_name M2RuntimeMeshFinalizer
extends RefCounted
## Finalizes stale cached M2 Meshes using caller-supplied raw data. The caller
## owns raw-file I/O, cache adoption, missing state and materialization.
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
const M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT := preload(
"res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
)
const M2_MESH_RESOURCE_EXTRACTOR_SCRIPT := preload(
"res://src/render/m2/m2_mesh_resource_extractor.gd"
)
const MATERIAL_REFRESH_VERSION := 2
const MATERIAL_REFRESH_VERSION_META := "wow_m2_material_refresh_version"
var _runtime_mesh_rebuild_classifier := M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()
var _mesh_resource_extractor := M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()
## Returns whether one Mesh is stale and the caller should load raw M2 data.
func requires_raw_data_for_refresh(mesh: Mesh) -> bool:
return (
mesh != null
and int(mesh.get_meta(MATERIAL_REFRESH_VERSION_META, 0)) < MATERIAL_REFRESH_VERSION
)
## Marks, rebuilds or falls back one stale Mesh using already-loaded raw M2
## data. Current Meshes return unchanged and never inspect raw data.
func finalize_mesh(
normalized_relative_path: String,
mesh: Mesh,
raw_m2_data: Dictionary,
extracted_directory: String) -> Mesh:
if mesh == null:
return null
if not requires_raw_data_for_refresh(mesh):
return mesh
if raw_m2_data.is_empty() or not _runtime_mesh_rebuild_classifier.needs_runtime_mesh_rebuild(
normalized_relative_path,
raw_m2_data):
_mark_mesh_current(mesh)
return mesh
var rebuilt_mesh := _rebuild_mesh(raw_m2_data, extracted_directory)
if rebuilt_mesh != null:
return rebuilt_mesh
_mark_mesh_current(mesh)
return mesh
## Clears memoized per-path rebuild decisions at the existing map/shutdown sites.
func clear() -> void:
_runtime_mesh_rebuild_classifier.clear()
## Returns the memoized rebuild-decision count for diagnostics and verification.
func cached_rebuild_decision_count() -> int:
return _runtime_mesh_rebuild_classifier.cached_path_count()
func _mark_mesh_current(mesh: Mesh) -> void:
mesh.set_meta(MATERIAL_REFRESH_VERSION_META, MATERIAL_REFRESH_VERSION)
func _rebuild_mesh(raw_m2_data: Dictionary, extracted_directory: String) -> Mesh:
var temporary_prototype: Node3D = M2_BUILDER_SCRIPT.build(
raw_m2_data,
extracted_directory
)
if temporary_prototype == null:
return null
var rebuilt_mesh := _mesh_resource_extractor.find_first_mesh_in_subtree(
temporary_prototype
)
temporary_prototype.free()
return rebuilt_mesh
@@ -0,0 +1 @@
uid://bn6vvtmqo0don
+11 -29
View File
@@ -60,8 +60,8 @@ const M2_PLACEMENT_GROUPER_SCRIPT := preload(
const M2_BUILD_BATCH_PLANNER_SCRIPT := preload(
"res://src/render/m2/m2_build_batch_planner.gd"
)
const M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT := preload(
"res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
"res://src/render/m2/m2_runtime_mesh_finalizer.gd"
)
const M2_MESH_LOAD_PIPELINE_STATE_SCRIPT := preload(
"res://src/render/m2/m2_mesh_load_pipeline_state.gd"
@@ -78,7 +78,6 @@ const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/rend
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
const M2_MATERIAL_REFRESH_VERSION := 2
const WMO_MATERIAL_REFRESH_VERSION := 10
const RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS := 5000.0
@@ -259,9 +258,7 @@ var _m2_unique_placement_registry := (
var _m2_placement_transform_resolver := M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()
var _m2_placement_grouper := M2_PLACEMENT_GROUPER_SCRIPT.new()
var _m2_build_batch_planner := M2_BUILD_BATCH_PLANNER_SCRIPT.new()
var _m2_runtime_mesh_rebuild_classifier := (
M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()
)
var _m2_runtime_mesh_finalizer := M2_RUNTIME_MESH_FINALIZER_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_load_pipeline_state := M2_MESH_LOAD_PIPELINE_STATE_SCRIPT.new()
@@ -2355,7 +2352,7 @@ func _wait_for_tile_tasks() -> void:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_m2_mesh_load_pipeline_state.clear()
_m2_runtime_mesh_rebuild_classifier.clear()
_m2_runtime_mesh_finalizer.clear()
for pending in _m2_animation_load_requests.values():
var path: String = String(pending.get("path", ""))
@@ -3005,7 +3002,7 @@ func _clear_streamed_world() -> void:
_m2_build_queue.clear()
_m2_unique_placement_registry.clear()
_m2_mesh_load_pipeline_state.clear()
_m2_runtime_mesh_rebuild_classifier.clear()
_m2_runtime_mesh_finalizer.clear()
_m2_animation_load_requests.clear()
_m2_animation_finalize_queue.clear()
_wmo_build_jobs.clear()
@@ -4654,19 +4651,15 @@ func _request_m2_mesh_load(normalized_rel: String) -> void:
func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
if mesh == null:
return null
if int(mesh.get_meta("wow_m2_material_refresh_version", 0)) >= M2_MATERIAL_REFRESH_VERSION:
if not _m2_runtime_mesh_finalizer.requires_raw_data_for_refresh(mesh):
return mesh
var data := _load_m2_raw_data_for_refresh(normalized_rel)
if data.is_empty() or not _m2_runtime_mesh_rebuild_classifier.needs_runtime_mesh_rebuild(
return _m2_runtime_mesh_finalizer.finalize_mesh(
normalized_rel,
data):
mesh.set_meta("wow_m2_material_refresh_version", M2_MATERIAL_REFRESH_VERSION)
return mesh
var rebuilt := _rebuild_m2_mesh_from_data(data)
if rebuilt != null:
return rebuilt
mesh.set_meta("wow_m2_material_refresh_version", M2_MATERIAL_REFRESH_VERSION)
return mesh
mesh,
data,
extracted_dir
)
func _prepare_m2_node_for_runtime(normalized_rel: String, root: Node3D) -> void:
@@ -4693,17 +4686,6 @@ func _load_m2_raw_data_for_refresh(normalized_rel: String) -> Dictionary:
return data if data is Dictionary else {}
func _rebuild_m2_mesh_from_data(data: Dictionary) -> Mesh:
if data.is_empty():
return null
var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir)
if prototype == null:
return null
var rebuilt := _m2_mesh_resource_extractor.find_first_mesh_in_subtree(prototype)
prototype.free()
return rebuilt
func _get_or_load_m2_mesh(rel_path: String) -> Mesh:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if _m2_mesh_resource_cache_state.has_mesh(normalized_rel):
@@ -117,8 +117,8 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
)
_expect_equal(
loader_source.count("_m2_mesh_resource_extractor.find_first_mesh_in_subtree("),
2,
"rebuild and synchronous fallback delegate",
1,
"synchronous fallback delegates",
failures
)
for retained_loader_rule in [
@@ -0,0 +1,217 @@
extends SceneTree
## Synthetic current/stale/ordinary/rebuild/fallback/clear/boundary/timing
## regression for runtime M2 Mesh material finalization.
const FINALIZER_SCRIPT := preload("res://src/render/m2/m2_runtime_mesh_finalizer.gd")
const FINALIZER_PATH := "res://src/render/m2/m2_runtime_mesh_finalizer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const REFRESH_META := "wow_m2_material_refresh_version"
func _initialize() -> void:
var failures: Array[String] = []
_verify_current_and_empty_raw_data(failures)
_verify_ordinary_and_rebuild_paths(failures)
_verify_rebuild_failure_fallback_and_clear(failures)
_verify_ownership_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_RUNTIME_MESH_FINALIZER: %s" % failure)
quit(1)
return
print(
"M2_RUNTIME_MESH_FINALIZER PASS cases=11 iterations=100 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_current_and_empty_raw_data(failures: Array[String]) -> void:
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
_expect_same(finalizer.call("finalize_mesh", "world/null.m2", null, {}, ""), null, "null Mesh returns null", failures)
var current_mesh := ArrayMesh.new()
current_mesh.set_meta(REFRESH_META, 2)
_expect_false(
finalizer.call("requires_raw_data_for_refresh", current_mesh),
"current Mesh skips raw data",
failures
)
_expect_same(
finalizer.call("finalize_mesh", "world/current.m2", current_mesh, _rebuild_raw_data(), ""),
current_mesh,
"current Mesh returns unchanged",
failures
)
_expect_equal(int(finalizer.call("cached_rebuild_decision_count")), 0, "current Mesh skips classifier", failures)
var stale_mesh := ArrayMesh.new()
_expect_true(
finalizer.call("requires_raw_data_for_refresh", stale_mesh),
"stale Mesh requests raw data",
failures
)
_expect_same(
finalizer.call("finalize_mesh", "world/stale.m2", stale_mesh, {}, ""),
stale_mesh,
"empty raw data falls back to original",
failures
)
_expect_equal(int(stale_mesh.get_meta(REFRESH_META, 0)), 2, "empty raw fallback marks current", failures)
func _verify_ordinary_and_rebuild_paths(failures: Array[String]) -> void:
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
var ordinary_mesh := ArrayMesh.new()
var ordinary_raw_data := {"batches": [{"has_billboard": false}]}
_expect_same(
finalizer.call("finalize_mesh", "world/rock.m2", ordinary_mesh, ordinary_raw_data, ""),
ordinary_mesh,
"ordinary raw data retains original",
failures
)
_expect_equal(int(ordinary_mesh.get_meta(REFRESH_META, 0)), 2, "ordinary Mesh marks current", failures)
var stale_billboard_mesh := ArrayMesh.new()
var rebuilt_mesh: Mesh = finalizer.call(
"finalize_mesh",
"world/tree.m2",
stale_billboard_mesh,
_rebuild_raw_data(),
""
)
_expect_true(rebuilt_mesh != null, "billboard raw data rebuilds Mesh", failures)
_expect_false(is_same(rebuilt_mesh, stale_billboard_mesh), "rebuild replaces original Mesh", failures)
_expect_equal(int(rebuilt_mesh.get_meta(REFRESH_META, 0)), 2, "rebuilt Mesh carries refresh version", failures)
func _verify_rebuild_failure_fallback_and_clear(failures: Array[String]) -> void:
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
var fallback_mesh := ArrayMesh.new()
var incomplete_billboard_data := {"batches": [{"has_billboard": true}]}
_expect_same(
finalizer.call(
"finalize_mesh",
"world/incomplete.m2",
fallback_mesh,
incomplete_billboard_data,
""
),
fallback_mesh,
"failed rebuild falls back to original",
failures
)
_expect_equal(int(fallback_mesh.get_meta(REFRESH_META, 0)), 2, "failed rebuild marks fallback current", failures)
_expect_equal(int(finalizer.call("cached_rebuild_decision_count")), 1, "classifier decision retained", failures)
finalizer.call("clear")
finalizer.call("clear")
_expect_equal(int(finalizer.call("cached_rebuild_decision_count")), 0, "clear drops decisions", failures)
func _verify_ownership_boundaries(failures: Array[String]) -> void:
var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH)
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
_expect_true(
loader_source.contains("M2_RUNTIME_MESH_FINALIZER_SCRIPT.new()"),
"loader composes runtime Mesh finalizer",
failures
)
_expect_equal(
loader_source.count("_m2_runtime_mesh_finalizer.clear()"),
2,
"two existing clear sites delegate",
failures
)
_expect_false(loader_source.contains("const M2_MATERIAL_REFRESH_VERSION"), "refresh version leaves loader", failures)
_expect_false(loader_source.contains("func _rebuild_m2_mesh_from_data("), "legacy rebuild adapter removed", failures)
for retained_loader_rule in [
"func _load_m2_raw_data_for_refresh(",
"FileAccess.file_exists(abs_path)",
"ClassDB.instantiate(\"M2Loader\")",
"loader.call(\"load_m2\", abs_path)",
"_m2_mesh_resource_cache_state.store_mesh(",
"_m2_missing_cache[normalized_rel]",
]:
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
for required_finalizer_rule in [
"const MATERIAL_REFRESH_VERSION := 2",
"M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()",
"M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()",
"M2_BUILDER_SCRIPT.build(",
"temporary_prototype.free()",
]:
_expect_true(finalizer_source.contains(required_finalizer_rule), "finalizer owns %s" % required_finalizer_rule, failures)
for forbidden_dependency in [
"ResourceLoader.",
"FileAccess.",
"ClassDB.",
"load_m2",
"_m2_mesh_resource_cache_state",
"_m2_missing_cache",
"MultiMesh",
]:
_expect_false(
finalizer_source.contains(forbidden_dependency),
"finalizer omits %s ownership" % forbidden_dependency,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
var meshes: Array[Mesh] = []
for _path_index in range(256):
meshes.append(ArrayMesh.new())
var ordinary_raw_data := {"batches": [{"has_billboard": false}]}
var started_microseconds := Time.get_ticks_usec()
for _iteration in range(100):
for path_index in range(256):
var mesh := meshes[path_index]
mesh.set_meta(REFRESH_META, 0)
finalizer.call(
"finalize_mesh",
"world/model_%d.m2" % path_index,
mesh,
ordinary_raw_data,
""
)
finalizer.call("clear")
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 ordinary finalizations under 1 second", failures)
return elapsed_milliseconds
func _rebuild_raw_data() -> Dictionary:
return {
"vertices": PackedVector3Array([
Vector3(0.0, 0.0, 0.0),
Vector3(1.0, 0.0, 0.0),
Vector3(0.0, 1.0, 0.0),
]),
"indices": PackedInt32Array([0, 1, 2]),
"batches": [{
"index_start": 0,
"index_count": 3,
"material_id": -1,
"texture_combo_index": -1,
"has_billboard": true,
}],
}
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_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_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
if not is_same(actual, expected):
failures.append(label)
@@ -0,0 +1 @@
uid://r1yv88u0rykp
@@ -7,6 +7,7 @@ const CLASSIFIER_SCRIPT := preload(
"res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
)
const CLASSIFIER_PATH := "res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
const FINALIZER_PATH := "res://src/render/m2/m2_runtime_mesh_finalizer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
@@ -152,16 +153,17 @@ func _verify_memoization_clear_and_diagnostics(failures: Array[String]) -> void:
func _verify_ownership_boundaries(failures: Array[String]) -> void:
var classifier_source := FileAccess.get_file_as_string(CLASSIFIER_PATH)
var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH)
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
_expect_true(
loader_source.contains("M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()"),
"loader composes classifier",
finalizer_source.contains("M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()"),
"runtime Mesh finalizer composes classifier",
failures
)
_expect_equal(
loader_source.count("_m2_runtime_mesh_rebuild_classifier.clear()"),
loader_source.count("_m2_runtime_mesh_finalizer.clear()"),
2,
"loader retains two classifier clear sites",
"loader retains two finalizer clear sites",
failures
)
_expect_false(