refactor(M03): extract ADT water scene finalizer
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
class_name AdtWaterSceneFinalizer
|
||||
extends RefCounted
|
||||
|
||||
## Builds and attaches one ADT water subtree on the renderer main thread. The
|
||||
## supplied tile root owns the returned subtree; this service retains no state.
|
||||
|
||||
const ADT_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/adt_builder.gd")
|
||||
|
||||
|
||||
## Builds water from parsed ADT data and attaches it once to [param tile_root].
|
||||
## [param tile_origin_godot_units] is the existing Godot-world tile origin used
|
||||
## by [ADTBuilder]. When [param editor_owner] is non-null and is an ancestor,
|
||||
## ownership is assigned recursively for persisted Editor previews. Returns the
|
||||
## attached borrowed root, or null for empty/invalid input or dry ADT data.
|
||||
## This method mutates the SceneTree and must run on the main thread.
|
||||
func attach_water_scene(
|
||||
water_data: Dictionary,
|
||||
tile_origin_godot_units: Vector3,
|
||||
tile_root: Node,
|
||||
editor_owner: Node = null) -> Node3D:
|
||||
if water_data.is_empty() or tile_root == null or not is_instance_valid(tile_root):
|
||||
return null
|
||||
|
||||
var water_root: Node3D = ADT_BUILDER_SCRIPT.build_tile_water_scene(
|
||||
water_data,
|
||||
tile_origin_godot_units
|
||||
)
|
||||
if water_root == null:
|
||||
return null
|
||||
|
||||
tile_root.add_child(water_root)
|
||||
if editor_owner != null and is_instance_valid(editor_owner):
|
||||
_assign_owner_recursive(water_root, editor_owner)
|
||||
return water_root
|
||||
|
||||
|
||||
func _assign_owner_recursive(generated_node: Node, editor_owner: Node) -> void:
|
||||
generated_node.owner = editor_owner
|
||||
for child in generated_node.get_children():
|
||||
_assign_owner_recursive(child, editor_owner)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ck4sdc8a3kg00
|
||||
@@ -45,6 +45,9 @@ const TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT := preload(
|
||||
const ADT_WATER_LOAD_PIPELINE_STATE_SCRIPT := preload(
|
||||
"res://src/render/liquid/adt_water_load_pipeline_state.gd"
|
||||
)
|
||||
const ADT_WATER_SCENE_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/liquid/adt_water_scene_finalizer.gd"
|
||||
)
|
||||
const M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_unique_placement_registry.gd"
|
||||
)
|
||||
@@ -260,6 +263,7 @@ var _terrain_splat_tasks: Dictionary = {}
|
||||
var _terrain_splat_result_mutex := Mutex.new()
|
||||
var _terrain_splat_result_queue: Array = []
|
||||
var _adt_water_load_pipeline_state := ADT_WATER_LOAD_PIPELINE_STATE_SCRIPT.new()
|
||||
var _adt_water_scene_finalizer := ADT_WATER_SCENE_FINALIZER_SCRIPT.new()
|
||||
var _tile_result_mutex := Mutex.new()
|
||||
var _tile_result_queue: Array = []
|
||||
var _shared_tex_cache: Dictionary = {}
|
||||
@@ -2294,10 +2298,12 @@ func _drain_water_load_results() -> void:
|
||||
var data: Dictionary = result.get("data", {})
|
||||
if not data.is_empty():
|
||||
var origin: Vector3 = state.get("origin", Vector3.ZERO)
|
||||
var water_root: Node3D = _builder.build_tile_water_scene(data, origin)
|
||||
if water_root:
|
||||
tile_root.add_child(water_root)
|
||||
_set_editor_owner_recursive(water_root)
|
||||
_adt_water_scene_finalizer.attach_water_scene(
|
||||
data,
|
||||
origin,
|
||||
tile_root,
|
||||
_editor_owner_for_generated_nodes()
|
||||
)
|
||||
|
||||
state["water_loaded"] = true
|
||||
_tile_states[key] = state
|
||||
@@ -2448,10 +2454,12 @@ func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void:
|
||||
tile_root.position = tile_origin
|
||||
|
||||
if enable_water:
|
||||
var water_root: Node3D = _builder.build_tile_water_scene(data, tile_origin)
|
||||
if water_root:
|
||||
tile_root.add_child(water_root)
|
||||
_set_editor_owner_recursive(water_root)
|
||||
_adt_water_scene_finalizer.attach_water_scene(
|
||||
data,
|
||||
tile_origin,
|
||||
tile_root,
|
||||
_editor_owner_for_generated_nodes()
|
||||
)
|
||||
if enable_m2_placeholders:
|
||||
_build_tile_m2_placeholders(
|
||||
tile_root,
|
||||
@@ -5421,12 +5429,7 @@ func _tile_key(tx: int, ty: int) -> String:
|
||||
|
||||
|
||||
func _set_editor_owner_recursive(node: Node) -> void:
|
||||
if not Engine.is_editor_hint():
|
||||
return
|
||||
if not editor_persist_generated_nodes:
|
||||
return
|
||||
|
||||
var edited_root := get_tree().edited_scene_root
|
||||
var edited_root := _editor_owner_for_generated_nodes()
|
||||
if edited_root == null:
|
||||
return
|
||||
|
||||
@@ -5435,6 +5438,12 @@ func _set_editor_owner_recursive(node: Node) -> void:
|
||||
_set_editor_owner_recursive(child)
|
||||
|
||||
|
||||
func _editor_owner_for_generated_nodes() -> Node:
|
||||
if not Engine.is_editor_hint() or not editor_persist_generated_nodes:
|
||||
return null
|
||||
return get_tree().edited_scene_root
|
||||
|
||||
|
||||
func _make_editor_signature() -> String:
|
||||
var values := [
|
||||
extracted_dir,
|
||||
|
||||
@@ -241,8 +241,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"ClassDB.instantiate(\"ADTLoader\")",
|
||||
"WorkerThreadPool.add_task(_load_tile_water_task.bind(key, path))",
|
||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.WATER_FINALIZE",
|
||||
"_builder.build_tile_water_scene(data, origin)",
|
||||
"tile_root.add_child(water_root)",
|
||||
"_adt_water_scene_finalizer.attach_water_scene(",
|
||||
"state[\"water_loaded\"] = true",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
extends SceneTree
|
||||
|
||||
## Synthetic main-thread scene, ownership, boundary and timing regression for
|
||||
## ADT water finalization.
|
||||
|
||||
const FINALIZER_SCRIPT := preload("res://src/render/liquid/adt_water_scene_finalizer.gd")
|
||||
const FINALIZER_PATH := "res://src/render/liquid/adt_water_scene_finalizer.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_invalid_and_dry_input(failures)
|
||||
_verify_build_attach_geometry_and_owner(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("ADT_WATER_SCENE_FINALIZER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"ADT_WATER_SCENE_FINALIZER PASS cases=8 iterations=100 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_invalid_and_dry_input(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var tile_root := Node3D.new()
|
||||
_expect_null(
|
||||
finalizer.call("attach_water_scene", {}, Vector3.ZERO, tile_root, null),
|
||||
"empty water data returns null",
|
||||
failures
|
||||
)
|
||||
_expect_equal(tile_root.get_child_count(), 0, "empty data does not mutate tile", failures)
|
||||
_expect_null(
|
||||
finalizer.call("attach_water_scene", _synthetic_water_data(), Vector3.ZERO, null, null),
|
||||
"missing tile root returns null",
|
||||
failures
|
||||
)
|
||||
_expect_null(
|
||||
finalizer.call(
|
||||
"attach_water_scene",
|
||||
{"chunks": [{"origin": Vector3.ZERO, "liquids": []}]},
|
||||
Vector3.ZERO,
|
||||
tile_root,
|
||||
null
|
||||
),
|
||||
"dry parsed ADT returns null",
|
||||
failures
|
||||
)
|
||||
_expect_equal(tile_root.get_child_count(), 0, "dry data does not mutate tile", failures)
|
||||
tile_root.free()
|
||||
|
||||
|
||||
func _verify_build_attach_geometry_and_owner(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var editor_scene_root := Node3D.new()
|
||||
var tile_root := Node3D.new()
|
||||
get_root().add_child(editor_scene_root)
|
||||
editor_scene_root.add_child(tile_root)
|
||||
|
||||
var tile_origin := Vector3(10.0, 20.0, 30.0)
|
||||
var water_root: Node3D = finalizer.call(
|
||||
"attach_water_scene",
|
||||
_synthetic_water_data(tile_origin),
|
||||
tile_origin,
|
||||
tile_root,
|
||||
editor_scene_root
|
||||
)
|
||||
_expect_not_null(water_root, "synthetic liquid builds a water root", failures)
|
||||
if water_root != null:
|
||||
_expect_true(water_root.get_parent() == tile_root, "water root attached once", failures)
|
||||
_expect_equal(tile_root.get_child_count(), 1, "tile owns one water root", failures)
|
||||
_expect_true(water_root.owner == editor_scene_root, "water root editor owner", failures)
|
||||
_expect_equal(water_root.get_child_count(), 1, "one liquid surface grouped", failures)
|
||||
if water_root.get_child_count() == 1:
|
||||
var liquid_mesh_instance := water_root.get_child(0) as MeshInstance3D
|
||||
_expect_not_null(liquid_mesh_instance, "liquid child is MeshInstance3D", failures)
|
||||
if liquid_mesh_instance != null:
|
||||
_expect_true(
|
||||
liquid_mesh_instance.owner == editor_scene_root,
|
||||
"liquid child editor owner",
|
||||
failures
|
||||
)
|
||||
var liquid_mesh := liquid_mesh_instance.mesh as ArrayMesh
|
||||
_expect_not_null(liquid_mesh, "liquid surface has ArrayMesh", failures)
|
||||
if liquid_mesh != null:
|
||||
_expect_equal(
|
||||
liquid_mesh.surface_get_array_len(0),
|
||||
4,
|
||||
"one enabled liquid cell has four vertices",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
liquid_mesh.surface_get_array_index_len(0),
|
||||
6,
|
||||
"one enabled liquid cell has six indices",
|
||||
failures
|
||||
)
|
||||
editor_scene_root.free()
|
||||
|
||||
|
||||
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(
|
||||
finalizer_source.contains("ADT_BUILDER_SCRIPT.build_tile_water_scene"),
|
||||
"finalizer retains existing ADTBuilder implementation",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
finalizer_source.contains("tile_root.add_child(water_root)"),
|
||||
"finalizer owns water subtree attachment",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_adt_water_scene_finalizer.attach_water_scene("),
|
||||
2,
|
||||
"both loader water paths delegate finalization",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_source.contains("_builder.build_tile_water_scene"),
|
||||
"loader no longer builds water scenes directly",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("state[\"water_loaded\"] = true"),
|
||||
"loader retains water-loaded timing",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("WATER_FINALIZE"),
|
||||
"loader retains finalization permit ownership",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var tile_root := Node3D.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(100):
|
||||
finalizer.call("attach_water_scene", {}, Vector3.ZERO, tile_root, null)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "100 empty attempts under 1 second", failures)
|
||||
_expect_equal(tile_root.get_child_count(), 0, "timing loop creates no nodes", failures)
|
||||
tile_root.free()
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _synthetic_water_data(tile_origin: Vector3 = Vector3.ZERO) -> Dictionary:
|
||||
var liquid_mask := PackedByteArray()
|
||||
liquid_mask.resize(64)
|
||||
liquid_mask.fill(0)
|
||||
liquid_mask[0] = 1
|
||||
var liquid_heights := PackedFloat32Array()
|
||||
liquid_heights.resize(81)
|
||||
liquid_heights.fill(tile_origin.y + 5.0)
|
||||
return {
|
||||
"chunks": [{
|
||||
"origin": tile_origin,
|
||||
"liquids": [{
|
||||
"liquid_id": 7,
|
||||
"mask": liquid_mask,
|
||||
"heights": liquid_heights,
|
||||
}],
|
||||
}],
|
||||
}
|
||||
|
||||
|
||||
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_null(value: Variant, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(value == null, label, failures)
|
||||
|
||||
|
||||
func _expect_not_null(value: Variant, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(value != null, 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])
|
||||
@@ -0,0 +1 @@
|
||||
uid://c1gqfpxg7ajsh
|
||||
Reference in New Issue
Block a user