refactor(M03): extract terrain chunk queue planner
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free semantic, dependency and timing regression for chunk queue planning.
|
||||
|
||||
const PLANNER_SCRIPT := preload(
|
||||
"res://src/render/terrain/terrain_chunk_geometry_queue_planner.gd"
|
||||
)
|
||||
const POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_create_remove_and_order(failures)
|
||||
_verify_tile_lod_removes_all(failures)
|
||||
_verify_fallback_origin_and_vertical_invariance(failures)
|
||||
_verify_invalid_focus(failures)
|
||||
_verify_loader_boundary(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER PASS cases=8 iterations=100 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_create_remove_and_order(failures: Array[String]) -> void:
|
||||
var planner: RefCounted = PLANNER_SCRIPT.new()
|
||||
var tile_states := {
|
||||
"A": _create_tile_state(
|
||||
{0: {"lod": 0}, 1: {"lod": 1}, 3: {"lod": 0}},
|
||||
{0: 0, 1: 2, 2: 1},
|
||||
[
|
||||
{"origin": Vector3(100.0, 0.0, 0.0)},
|
||||
{"origin": Vector3(30.0, 8000.0, 0.0)},
|
||||
{"origin": Vector3(10.0, -8000.0, 0.0)},
|
||||
]
|
||||
),
|
||||
"B": _create_tile_state({5: {"lod": 0}}, {}, []),
|
||||
}
|
||||
var plan: Dictionary = planner.call("plan", tile_states, POSITION_SCRIPT.new(0.0, 50.0, 0.0))
|
||||
var creates: Array = plan["create_requests"]
|
||||
var removes: Array = plan["remove_requests"]
|
||||
_expect_equal(creates.size(), 2, "missing and mismatched chunks create", failures)
|
||||
_expect_equal(int(creates[0]["chunk"]), 2, "nearest missing chunk first", failures)
|
||||
_expect_equal(int(creates[1]["chunk"]), 1, "farther LOD mismatch second", failures)
|
||||
_expect_equal(int(creates[1]["lod"]), 2, "mismatch uses desired LOD", failures)
|
||||
_expect_equal(removes.size(), 2, "undesired current chunks remove", failures)
|
||||
_expect_string_equal(String(removes[0]["tile"]), "A", "stable tile removal order", failures)
|
||||
_expect_equal(int(removes[0]["chunk"]), 3, "first undesired chunk", failures)
|
||||
_expect_equal(int(removes[1]["chunk"]), 5, "second tile undesired chunk", failures)
|
||||
|
||||
|
||||
func _verify_tile_lod_removes_all(failures: Array[String]) -> void:
|
||||
var tile_state := _create_tile_state({7: {"lod": 0}, 8: {"lod": 1}}, {7: 0}, [])
|
||||
tile_state["desired_tile_lod"] = 3
|
||||
var plan: Dictionary = PLANNER_SCRIPT.new().call(
|
||||
"plan",
|
||||
{"tile": tile_state},
|
||||
POSITION_SCRIPT.new(0.0, 0.0, 0.0)
|
||||
)
|
||||
_expect_true((plan["create_requests"] as Array).is_empty(), "tile LOD suppresses chunk creates", failures)
|
||||
_expect_equal((plan["remove_requests"] as Array).size(), 2, "tile LOD removes every chunk", failures)
|
||||
|
||||
|
||||
func _verify_fallback_origin_and_vertical_invariance(failures: Array[String]) -> void:
|
||||
var tile_state := _create_tile_state({}, {9: 1}, [])
|
||||
tile_state["origin"] = Vector3(4.0, 99999.0, 3.0)
|
||||
var plan: Dictionary = PLANNER_SCRIPT.new().call(
|
||||
"plan",
|
||||
{"fallback": tile_state},
|
||||
POSITION_SCRIPT.new(0.0, -99999.0, 0.0)
|
||||
)
|
||||
var request: Dictionary = (plan["create_requests"] as Array)[0]
|
||||
_expect_near(float(request["dist_sq"]), 25.0, "fallback origin uses horizontal distance", failures)
|
||||
|
||||
|
||||
func _verify_invalid_focus(failures: Array[String]) -> void:
|
||||
var plan: Dictionary = PLANNER_SCRIPT.new().call("plan", {}, null)
|
||||
_expect_true((plan["create_requests"] as Array).is_empty(), "invalid focus create empty", failures)
|
||||
_expect_true((plan["remove_requests"] as Array).is_empty(), "invalid focus remove empty", failures)
|
||||
|
||||
|
||||
func _verify_loader_boundary(failures: Array[String]) -> void:
|
||||
var source := _read_text(LOADER_PATH, failures)
|
||||
_expect_true(source.contains("TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT.new()"), "loader composes planner", failures)
|
||||
_expect_true(source.contains("_terrain_chunk_geometry_operation_planner.plan("), "loader delegates plan", failures)
|
||||
_expect_true(not source.contains("func _chunk_origin_for("), "loader omits chunk-origin planning", failures)
|
||||
_expect_true(source.contains("_remove_chunk(_chunk_remove_queue.pop_back())"), "loader retains removal-first drain", failures)
|
||||
_expect_true(source.contains("_create_chunk(_chunk_create_queue.pop_front())"), "loader retains nearest-first drain", failures)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var tile_states: Dictionary = {}
|
||||
for tile_index in range(64):
|
||||
var current_chunks: Dictionary = {}
|
||||
var desired_lods: Dictionary = {}
|
||||
var parsed_chunks: Array = []
|
||||
for chunk_index in range(16):
|
||||
current_chunks[chunk_index] = {"lod": chunk_index % 3}
|
||||
desired_lods[chunk_index] = (chunk_index + 1) % 3
|
||||
parsed_chunks.append({
|
||||
"origin": Vector3(float(tile_index * 16 + chunk_index), 0.0, float(tile_index))
|
||||
})
|
||||
tile_states["tile-%d" % tile_index] = _create_tile_state(
|
||||
current_chunks,
|
||||
desired_lods,
|
||||
parsed_chunks
|
||||
)
|
||||
var planner: RefCounted = PLANNER_SCRIPT.new()
|
||||
var focus: RefCounted = POSITION_SCRIPT.new(0.0, 0.0, 0.0)
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(100):
|
||||
planner.call("plan", tile_states, focus)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "100 multi-tile plans remain bounded", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _create_tile_state(
|
||||
current_chunks: Dictionary,
|
||||
desired_lods: Dictionary,
|
||||
parsed_chunks: Array
|
||||
) -> Dictionary:
|
||||
return {
|
||||
"chunks": current_chunks,
|
||||
"desired_lods": desired_lods,
|
||||
"desired_tile_lod": -1,
|
||||
"data": {"chunks": parsed_chunks},
|
||||
"origin": Vector3.ZERO,
|
||||
}
|
||||
|
||||
|
||||
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_equal(actual_value: int, expected_value: int, label: String, failures: Array[String]) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
|
||||
if not is_equal_approx(actual_value, expected_value):
|
||||
failures.append("%s expected %.3f, got %.3f" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_string_equal(
|
||||
actual_value: String,
|
||||
expected_value: String,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
Reference in New Issue
Block a user