fix(M00): release GUI capture render resources
Work-Package: M00-QAR-GUI-RID-SHUTDOWN-001 Agent: sindo-main-codex Tests: render baseline dry-run; runtime cache shutdown; comparator self-test; coordination and documentation gates Fidelity: shutdown-only ownership fix; no visual or Blizzlike335 behavior change
This commit is contained in:
@@ -374,6 +374,39 @@ func _exit_tree() -> void:
|
||||
_shutting_down = true
|
||||
_wait_for_tile_tasks()
|
||||
_clear_streamed_world()
|
||||
_release_runtime_caches_for_shutdown()
|
||||
|
||||
|
||||
## Frees detached prototype nodes and releases render resources owned by this
|
||||
## loader after all asynchronous work has completed. Map refreshes retain these
|
||||
## caches; only scene shutdown destroys them.
|
||||
func _release_runtime_caches_for_shutdown() -> void:
|
||||
_free_detached_node_cache(_m2_scene_cache)
|
||||
_free_detached_node_cache(_m2_animated_scene_cache)
|
||||
_free_detached_node_cache(_wmo_prototype_cache)
|
||||
_m2_mesh_cache.clear()
|
||||
_m2_static_animation_cache.clear()
|
||||
_m2_missing_cache.clear()
|
||||
_wmo_render_cache.clear()
|
||||
_wmo_scene_resource_cache.clear()
|
||||
_wmo_render_missing_cache.clear()
|
||||
_wmo_scene_cache_missing.clear()
|
||||
_wmo_missing_cache.clear()
|
||||
_shared_tex_cache.clear()
|
||||
|
||||
|
||||
func _free_detached_node_cache(node_cache: Dictionary) -> void:
|
||||
for cached_value in node_cache.values():
|
||||
if not (cached_value is Node):
|
||||
continue
|
||||
var cached_node := cached_value as Node
|
||||
if not is_instance_valid(cached_node):
|
||||
continue
|
||||
if cached_node.is_inside_tree():
|
||||
cached_node.queue_free()
|
||||
else:
|
||||
cached_node.free()
|
||||
node_cache.clear()
|
||||
|
||||
|
||||
func _apply_quality_preset() -> void:
|
||||
@@ -2781,15 +2814,27 @@ func _clear_streamed_world() -> void:
|
||||
_free_render_instance(chunk_rid)
|
||||
var tile_root: Node = state.get("root", null)
|
||||
if tile_root:
|
||||
tile_root.queue_free()
|
||||
_queue_free_streamed_node(tile_root)
|
||||
|
||||
_tile_states.clear()
|
||||
_clear_tile_mesh_cache()
|
||||
_clear_quality_terrain_mesh_cache()
|
||||
_clear_wmo_registry()
|
||||
_ensure_world_wmo_root()
|
||||
if enable_wmo:
|
||||
_place_global_wmo()
|
||||
if not _shutting_down:
|
||||
_ensure_world_wmo_root()
|
||||
if enable_wmo:
|
||||
_place_global_wmo()
|
||||
|
||||
|
||||
## During world shutdown, parented nodes are recursively owned by the world
|
||||
## root. Queueing them separately from `_exit_tree()` can detach them from that
|
||||
## deletion and leave empty Node objects alive at engine teardown.
|
||||
func _queue_free_streamed_node(node: Node) -> void:
|
||||
if node == null or not is_instance_valid(node):
|
||||
return
|
||||
if _shutting_down and node.is_inside_tree():
|
||||
return
|
||||
node.queue_free()
|
||||
|
||||
|
||||
func _reset_terrain_root_children() -> void:
|
||||
@@ -3889,10 +3934,10 @@ func _clear_wmo_registry() -> void:
|
||||
var entry: Dictionary = entry_variant
|
||||
var node: Node = entry.get("node", null)
|
||||
if node and is_instance_valid(node):
|
||||
node.queue_free()
|
||||
_queue_free_streamed_node(node)
|
||||
_wmo_registry.clear()
|
||||
if _world_wmo_root and is_instance_valid(_world_wmo_root):
|
||||
_world_wmo_root.queue_free()
|
||||
_queue_free_streamed_node(_world_wmo_root)
|
||||
_world_wmo_root = null
|
||||
|
||||
|
||||
@@ -4458,7 +4503,7 @@ func _cancel_m2_build_job(key: String) -> void:
|
||||
var job: Dictionary = _m2_build_jobs[key]
|
||||
var root: Node = job.get("root", null)
|
||||
if root != null and is_instance_valid(root):
|
||||
root.queue_free()
|
||||
_queue_free_streamed_node(root)
|
||||
_m2_build_jobs.erase(key)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
extends SceneTree
|
||||
|
||||
const STREAMING_WORLD_LOADER := preload("res://src/scenes/streaming/streaming_world_loader.gd")
|
||||
const ADT_BUILDER := preload("res://addons/mpq_extractor/loaders/adt_builder.gd")
|
||||
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_verify_empty_water_root_ownership()
|
||||
_verify_runtime_cache_ownership()
|
||||
|
||||
if _failures > 0:
|
||||
push_error("Render runtime cache shutdown verification failed: %d issue(s)" % _failures)
|
||||
quit(1)
|
||||
return
|
||||
print("Render runtime cache shutdown verification passed.")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_empty_water_root_ownership() -> void:
|
||||
var node_count_before := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||
var empty_water_root: Node3D = ADT_BUILDER.build_tile_water_scene({"chunks": []}, Vector3.ZERO)
|
||||
var node_count_after := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||
_expect(empty_water_root == null, "empty ADT liquid payload should not produce a water root")
|
||||
_expect(node_count_after == node_count_before, "empty ADT liquid payload should release its temporary water root")
|
||||
|
||||
|
||||
func _verify_runtime_cache_ownership() -> void:
|
||||
var loader: Node = STREAMING_WORLD_LOADER.new()
|
||||
var static_m2_prototype := Node3D.new()
|
||||
var animated_m2_prototype := Node3D.new()
|
||||
var wmo_prototype := Node3D.new()
|
||||
|
||||
loader.set("_m2_scene_cache", {"static": static_m2_prototype})
|
||||
loader.set("_m2_animated_scene_cache", {"animated": animated_m2_prototype})
|
||||
loader.set("_wmo_prototype_cache", {"wmo": wmo_prototype})
|
||||
loader.set("_m2_mesh_cache", {"mesh": ArrayMesh.new()})
|
||||
loader.set("_m2_static_animation_cache", {"static": true})
|
||||
loader.set("_m2_missing_cache", {"missing": true})
|
||||
loader.set("_wmo_render_cache", {"render": Resource.new()})
|
||||
loader.set("_wmo_scene_resource_cache", {"scene": PackedScene.new()})
|
||||
loader.set("_wmo_render_missing_cache", {"missing": true})
|
||||
loader.set("_wmo_scene_cache_missing", {"missing": true})
|
||||
loader.set("_wmo_missing_cache", {"missing": true})
|
||||
loader.set("_shared_tex_cache", {"texture": ImageTexture.new()})
|
||||
|
||||
loader.call("_release_runtime_caches_for_shutdown")
|
||||
|
||||
_expect(not is_instance_valid(static_m2_prototype), "static M2 prototype should be freed")
|
||||
_expect(not is_instance_valid(animated_m2_prototype), "animated M2 prototype should be freed")
|
||||
_expect(not is_instance_valid(wmo_prototype), "WMO prototype should be freed")
|
||||
for cache_name in [
|
||||
"_m2_scene_cache",
|
||||
"_m2_animated_scene_cache",
|
||||
"_wmo_prototype_cache",
|
||||
"_m2_mesh_cache",
|
||||
"_m2_static_animation_cache",
|
||||
"_m2_missing_cache",
|
||||
"_wmo_render_cache",
|
||||
"_wmo_scene_resource_cache",
|
||||
"_wmo_render_missing_cache",
|
||||
"_wmo_scene_cache_missing",
|
||||
"_wmo_missing_cache",
|
||||
"_shared_tex_cache",
|
||||
]:
|
||||
var cache: Dictionary = loader.get(cache_name)
|
||||
_expect(cache.is_empty(), "%s should be empty after shutdown" % cache_name)
|
||||
|
||||
loader.free()
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
if condition:
|
||||
return
|
||||
_failures += 1
|
||||
push_error(message)
|
||||
Reference in New Issue
Block a user