refactor(M03): extract terrain quality mesh cache
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
class_name TerrainQualityMeshCache
|
||||
extends RefCounted
|
||||
|
||||
## Session-memory LRU owner for full-quality terrain Mesh references retained
|
||||
## across tile unload/revisit. It owns no tile state, Node, RID, task or file.
|
||||
|
||||
const EXCLUDED_QUALITY_SOURCES: Array[String] = [
|
||||
"control_splat_cache",
|
||||
"splat_cache",
|
||||
"splat",
|
||||
]
|
||||
|
||||
var _entries_by_tile_key: Dictionary = {}
|
||||
var _tile_keys_oldest_to_newest: Array[String] = []
|
||||
|
||||
|
||||
## Stores an admitted Mesh reference, touches it as newest and trims oldest
|
||||
## entries to the supplied non-negative capacity. Returns false when rejected.
|
||||
func store(tile_key: String, mesh: Mesh, source: String, capacity: int) -> bool:
|
||||
if tile_key.is_empty() or mesh == null or EXCLUDED_QUALITY_SOURCES.has(source):
|
||||
return false
|
||||
_entries_by_tile_key[tile_key] = {"mesh": mesh, "source": source}
|
||||
_touch(tile_key)
|
||||
_trim(maxi(0, capacity))
|
||||
return _entries_by_tile_key.has(tile_key)
|
||||
|
||||
|
||||
## Returns a fresh record containing the retained Mesh/source and touches the key
|
||||
## as newest. Missing or invalid records return empty and are removed.
|
||||
func restore(tile_key: String) -> Dictionary:
|
||||
if not _entries_by_tile_key.has(tile_key):
|
||||
return {}
|
||||
var entry: Dictionary = _entries_by_tile_key[tile_key]
|
||||
var source := String(entry.get("source", "baked_full"))
|
||||
var mesh := entry.get("mesh") as Mesh
|
||||
if mesh == null or EXCLUDED_QUALITY_SOURCES.has(source):
|
||||
_remove(tile_key)
|
||||
return {}
|
||||
_touch(tile_key)
|
||||
return {"mesh": mesh, "source": source}
|
||||
|
||||
|
||||
## Releases every retained Mesh reference and resets LRU order.
|
||||
func clear() -> void:
|
||||
_entries_by_tile_key.clear()
|
||||
_tile_keys_oldest_to_newest.clear()
|
||||
|
||||
|
||||
## Returns detached scalar diagnostics without exposing retained Mesh references.
|
||||
func diagnostic_snapshot() -> Dictionary:
|
||||
var entries: Array[Dictionary] = []
|
||||
for tile_key in _tile_keys_oldest_to_newest:
|
||||
var entry: Dictionary = _entries_by_tile_key.get(tile_key, {})
|
||||
entries.append({"tile_key": tile_key, "source": String(entry.get("source", ""))})
|
||||
return {
|
||||
"entry_count": entries.size(),
|
||||
"entries_oldest_to_newest": entries,
|
||||
}
|
||||
|
||||
|
||||
func _touch(tile_key: String) -> void:
|
||||
_tile_keys_oldest_to_newest.erase(tile_key)
|
||||
_tile_keys_oldest_to_newest.append(tile_key)
|
||||
|
||||
|
||||
func _trim(capacity: int) -> void:
|
||||
while _tile_keys_oldest_to_newest.size() > capacity:
|
||||
_remove(_tile_keys_oldest_to_newest.front())
|
||||
|
||||
|
||||
func _remove(tile_key: String) -> void:
|
||||
_entries_by_tile_key.erase(tile_key)
|
||||
_tile_keys_oldest_to_newest.erase(tile_key)
|
||||
@@ -0,0 +1 @@
|
||||
uid://y576l2omqwox
|
||||
@@ -18,6 +18,7 @@ const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordina
|
||||
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
|
||||
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
|
||||
const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
|
||||
const TERRAIN_QUALITY_MESH_CACHE_SCRIPT := preload("res://src/render/terrain/terrain_quality_mesh_cache.gd")
|
||||
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
||||
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
|
||||
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
|
||||
@@ -241,8 +242,7 @@ var _last_refresh_focus_pos := Vector3.ZERO
|
||||
var _has_refresh_focus := false
|
||||
var _tile_mesh_cache: Dictionary = {}
|
||||
var _tile_mesh_cache_order: Array = []
|
||||
var _terrain_quality_mesh_cache: Dictionary = {}
|
||||
var _terrain_quality_mesh_cache_order: Array = []
|
||||
var _terrain_quality_mesh_cache: RefCounted = TERRAIN_QUALITY_MESH_CACHE_SCRIPT.new()
|
||||
var _wmo_prototype_cache: Dictionary = {}
|
||||
var _wmo_render_cache: Dictionary = {}
|
||||
var _wmo_render_missing_cache: Dictionary = {}
|
||||
@@ -5500,53 +5500,28 @@ func _clear_tile_mesh_cache() -> void:
|
||||
|
||||
|
||||
func _restore_quality_terrain_mesh(tile_key: String, state: Dictionary) -> Dictionary:
|
||||
if not _terrain_quality_mesh_cache.has(tile_key):
|
||||
var cached: Dictionary = _terrain_quality_mesh_cache.call("restore", tile_key)
|
||||
if cached.is_empty():
|
||||
return state
|
||||
var cached: Dictionary = _terrain_quality_mesh_cache[tile_key]
|
||||
var source := String(cached.get("source", "baked_full"))
|
||||
if source == "control_splat_cache" or source == "splat_cache" or source == "splat":
|
||||
_terrain_quality_mesh_cache.erase(tile_key)
|
||||
_terrain_quality_mesh_cache_order.erase(tile_key)
|
||||
return state
|
||||
var mesh: Mesh = cached.get("mesh", null)
|
||||
if mesh == null:
|
||||
_terrain_quality_mesh_cache.erase(tile_key)
|
||||
_terrain_quality_mesh_cache_order.erase(tile_key)
|
||||
return state
|
||||
state["quality_terrain_mesh"] = mesh
|
||||
state["quality_terrain_source"] = source
|
||||
_touch_quality_terrain_mesh_cache(tile_key)
|
||||
return state
|
||||
|
||||
|
||||
func _store_quality_terrain_mesh(tile_key: String, mesh: Mesh, source: String) -> void:
|
||||
if tile_key.is_empty() or mesh == null:
|
||||
return
|
||||
if source == "control_splat_cache" or source == "splat_cache" or source == "splat":
|
||||
return
|
||||
_terrain_quality_mesh_cache[tile_key] = {
|
||||
"mesh": mesh,
|
||||
"source": source,
|
||||
}
|
||||
_touch_quality_terrain_mesh_cache(tile_key)
|
||||
_trim_quality_terrain_mesh_cache()
|
||||
|
||||
|
||||
func _touch_quality_terrain_mesh_cache(tile_key: String) -> void:
|
||||
_terrain_quality_mesh_cache_order.erase(tile_key)
|
||||
_terrain_quality_mesh_cache_order.append(tile_key)
|
||||
|
||||
|
||||
func _trim_quality_terrain_mesh_cache() -> void:
|
||||
var limit := maxi(0, terrain_quality_mesh_cache_limit)
|
||||
while _terrain_quality_mesh_cache_order.size() > limit:
|
||||
var oldest: String = _terrain_quality_mesh_cache_order.pop_front()
|
||||
_terrain_quality_mesh_cache.erase(oldest)
|
||||
_terrain_quality_mesh_cache.call(
|
||||
"store",
|
||||
tile_key,
|
||||
mesh,
|
||||
source,
|
||||
terrain_quality_mesh_cache_limit
|
||||
)
|
||||
|
||||
|
||||
func _clear_quality_terrain_mesh_cache() -> void:
|
||||
_terrain_quality_mesh_cache.clear()
|
||||
_terrain_quality_mesh_cache_order.clear()
|
||||
_terrain_quality_mesh_cache.call("clear")
|
||||
|
||||
|
||||
func _position_camera_over_world() -> void:
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
extends SceneTree
|
||||
|
||||
## Scene-free contract, admission and LRU regression for terrain revisit meshes.
|
||||
|
||||
const CACHE_SCRIPT := preload("res://src/render/terrain/terrain_quality_mesh_cache.gd")
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_cache_contract(failures)
|
||||
_verify_loader_boundary(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("TERRAIN_QUALITY_MESH_CACHE: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print("TERRAIN_QUALITY_MESH_CACHE PASS contract_cases=15 loader_calls=3")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_cache_contract(failures: Array[String]) -> void:
|
||||
var cache: RefCounted = CACHE_SCRIPT.new()
|
||||
var mesh_a := ArrayMesh.new()
|
||||
var mesh_b := ArrayMesh.new()
|
||||
var mesh_c := ArrayMesh.new()
|
||||
_expect_true(not bool(cache.call("store", "", mesh_a, "baked_full", 2)), "empty key rejected", failures)
|
||||
_expect_true(not bool(cache.call("store", "a", null, "baked_full", 2)), "null mesh rejected", failures)
|
||||
for excluded_source in ["control_splat_cache", "splat_cache", "splat"]:
|
||||
_expect_true(not bool(cache.call("store", "a", mesh_a, excluded_source, 2)), "%s rejected" % excluded_source, failures)
|
||||
_expect_true(bool(cache.call("store", "a", mesh_a, "baked_full", 2)), "first store", failures)
|
||||
_expect_true(bool(cache.call("store", "b", mesh_b, "baked_full", 2)), "second store", failures)
|
||||
var restored_a: Dictionary = cache.call("restore", "a")
|
||||
_expect_true(restored_a.get("mesh") == mesh_a, "restore retains mesh reference", failures)
|
||||
_expect_true(String(restored_a.get("source", "")) == "baked_full", "restore retains source", failures)
|
||||
_expect_true(bool(cache.call("store", "c", mesh_c, "baked_full", 2)), "third store", failures)
|
||||
_expect_true((cache.call("restore", "b") as Dictionary).is_empty(), "oldest evicted after touch", failures)
|
||||
_expect_true(not (cache.call("restore", "a") as Dictionary).is_empty(), "touched entry retained", failures)
|
||||
var diagnostics: Dictionary = cache.call("diagnostic_snapshot")
|
||||
_expect_true(int(diagnostics.get("entry_count", 0)) == 2, "bounded count", failures)
|
||||
(diagnostics.get("entries_oldest_to_newest") as Array).clear()
|
||||
_expect_true(int((cache.call("diagnostic_snapshot") as Dictionary).get("entry_count", 0)) == 2, "diagnostics detached", failures)
|
||||
_expect_true(not bool(cache.call("store", "zero", ArrayMesh.new(), "baked_full", -1)), "negative capacity clamps to zero", failures)
|
||||
cache.call("clear")
|
||||
_expect_true(int((cache.call("diagnostic_snapshot") as Dictionary).get("entry_count", -1)) == 0, "clear releases entries", failures)
|
||||
|
||||
|
||||
func _verify_loader_boundary(failures: Array[String]) -> void:
|
||||
var source := _read_text(LOADER_PATH, failures)
|
||||
_expect_true(source.contains("TERRAIN_QUALITY_MESH_CACHE_SCRIPT.new()"), "loader composes cache", failures)
|
||||
_expect_true(source.contains('_terrain_quality_mesh_cache.call("restore", tile_key)'), "loader delegates restore", failures)
|
||||
_expect_true(source.contains('_terrain_quality_mesh_cache.call("clear")'), "loader delegates clear", failures)
|
||||
for removed_symbol in ["_terrain_quality_mesh_cache_order", "func _touch_quality_terrain_mesh_cache", "func _trim_quality_terrain_mesh_cache"]:
|
||||
_expect_true(not source.contains(removed_symbol), "loader omits %s" % removed_symbol, failures)
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://khd1ths2b4ya
|
||||
Reference in New Issue
Block a user