e887c3bad5
Work-Package: M03-RND-STREAMING-PLANNER-001 Agent: sindo-main-codex Tests: planner/facade/focus/coordinate/manifest/shutdown contracts, seven-checkpoint dry-run, repository gates Fidelity: preserves wanted/retained radii, boundary prefetch, editor preview, queue priority and cache/render behavior
212 lines
9.0 KiB
GDScript
212 lines
9.0 KiB
GDScript
extends SceneTree
|
|
|
|
## Asset-free M03 regression for pure runtime/editor ADT target planning.
|
|
|
|
const PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
|
const POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
|
|
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
|
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 PLANNER_PATH := "res://src/render/streaming/streaming_target_planner.gd"
|
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
|
const PERFORMANCE_ITERATIONS := 250
|
|
const MAXIMUM_AVERAGE_PLAN_MILLISECONDS := 4.0
|
|
|
|
|
|
func _initialize() -> void:
|
|
var failures: Array[String] = []
|
|
var planner = PLANNER_SCRIPT.new()
|
|
_verify_center_radii_and_isolation(planner, failures)
|
|
_verify_corner_prefetch_and_catalog_filter(planner, failures)
|
|
_verify_policy_clamps(planner, failures)
|
|
_verify_editor_plan(planner, failures)
|
|
_verify_scene_free_extraction(failures)
|
|
var elapsed_milliseconds := _measure_planner_iterations(planner, failures)
|
|
|
|
if not failures.is_empty():
|
|
for failure in failures:
|
|
push_error("STREAMING_TARGET_PLANNER: %s" % failure)
|
|
quit(1)
|
|
return
|
|
|
|
print("STREAMING_TARGET_PLANNER PASS cases=5 iterations=%d elapsed_ms=%.3f average_ms=%.3f" % [
|
|
PERFORMANCE_ITERATIONS,
|
|
elapsed_milliseconds,
|
|
elapsed_milliseconds / float(PERFORMANCE_ITERATIONS),
|
|
])
|
|
quit(0)
|
|
|
|
|
|
func _verify_center_radii_and_isolation(planner, failures: Array[String]) -> void:
|
|
var available_tiles := _tile_catalog(28, 36, 44, 52)
|
|
var focus_position = _tile_position(32, 48, 0.5, 0.5)
|
|
var policy = POLICY_SCRIPT.new(0, 1, 0, 1, 1, 0.35)
|
|
var plan = planner.plan_runtime(available_tiles, focus_position, policy)
|
|
var wanted_tile_keys: Dictionary = plan.wanted_tile_keys()
|
|
var retained_tile_keys: Dictionary = plan.retained_tile_keys()
|
|
|
|
_expect_equal(plan.focus_tile.tile_x, 32, "center focus tile X", failures)
|
|
_expect_equal(plan.focus_tile.tile_y, 48, "center focus tile Y", failures)
|
|
_expect_equal(wanted_tile_keys.size(), 25, "center wanted 5x5", failures)
|
|
_expect_equal(retained_tile_keys.size(), 49, "center retained 7x7", failures)
|
|
_expect_true(wanted_tile_keys.has("30_46"), "wanted includes north-west radius edge", failures)
|
|
_expect_true(wanted_tile_keys.has("34_50"), "wanted includes south-east radius edge", failures)
|
|
_expect_true(retained_tile_keys.has("29_45"), "retained includes hysteresis edge", failures)
|
|
|
|
_expect_true(wanted_tile_keys.is_read_only(), "wanted result is immutable", failures)
|
|
_expect_true(retained_tile_keys.is_read_only(), "retained result is immutable", failures)
|
|
_expect_equal(available_tiles.size(), 81, "available catalog remains unchanged", failures)
|
|
|
|
|
|
func _verify_corner_prefetch_and_catalog_filter(planner, failures: Array[String]) -> void:
|
|
var available_tiles := {
|
|
"32_48": "tile-a",
|
|
"33_48": "tile-b",
|
|
"32_49": "tile-c",
|
|
"33_49": "tile-d",
|
|
"31_48": "outside-prefetch",
|
|
}
|
|
var focus_position = _tile_position(32, 48, 0.99, 0.99)
|
|
var policy = POLICY_SCRIPT.new(0, 0, 0, 0, 0, 0.35)
|
|
var plan = planner.plan_runtime(available_tiles, focus_position, policy)
|
|
_expect_set_equal(
|
|
plan.wanted_tile_keys(),
|
|
["32_48", "33_48", "32_49", "33_49"],
|
|
"corner prefetch cross product",
|
|
failures
|
|
)
|
|
_expect_set_equal(
|
|
plan.retained_tile_keys(),
|
|
["32_48", "33_48", "32_49", "33_49"],
|
|
"corner retained cross product",
|
|
failures
|
|
)
|
|
|
|
|
|
func _verify_policy_clamps(planner, failures: Array[String]) -> void:
|
|
var policy = POLICY_SCRIPT.new(16, 2, -5, -3, -4, 0.9)
|
|
_expect_equal(policy.visible_tile_radius(), 2, "visible radius keeps tile minimum", failures)
|
|
_expect_equal(policy.load_tile_radius(), 2, "negative prewarm clamps to zero", failures)
|
|
_expect_equal(policy.retained_tile_radius(), 2, "negative retain clamps to zero", failures)
|
|
_expect_near(policy.clamped_boundary_prefetch_threshold(), 0.49, "threshold upper clamp", failures)
|
|
|
|
var plan = planner.plan_runtime(
|
|
_tile_catalog(30, 34, 46, 50),
|
|
_tile_position(32, 48, 0.5, 0.5),
|
|
policy
|
|
)
|
|
_expect_equal(plan.wanted_tile_keys().size(), 25, "clamped policy wanted radius", failures)
|
|
_expect_equal(plan.retained_tile_keys().size(), 25, "clamped policy retained radius", failures)
|
|
|
|
|
|
func _verify_editor_plan(planner, failures: Array[String]) -> void:
|
|
var available_tiles := _tile_catalog(31, 33, 47, 49)
|
|
var focus_position = _tile_position(32, 48, 0.5, 0.5)
|
|
var negative_radius_plan = planner.plan_editor(available_tiles, focus_position, -4)
|
|
_expect_set_equal(
|
|
negative_radius_plan.wanted_tile_keys(),
|
|
["32_48"],
|
|
"editor negative radius clamps to focus",
|
|
failures
|
|
)
|
|
_expect_set_equal(
|
|
negative_radius_plan.retained_tile_keys(),
|
|
["32_48"],
|
|
"editor retained matches wanted",
|
|
failures
|
|
)
|
|
|
|
var radius_one_plan = planner.plan_editor(available_tiles, focus_position, 1)
|
|
_expect_equal(radius_one_plan.wanted_tile_keys().size(), 9, "editor radius one 3x3", failures)
|
|
_expect_set_equal(
|
|
radius_one_plan.retained_tile_keys(),
|
|
radius_one_plan.wanted_tile_keys().keys(),
|
|
"editor radius retained matches wanted",
|
|
failures
|
|
)
|
|
|
|
|
|
func _verify_scene_free_extraction(failures: Array[String]) -> void:
|
|
var planner_source := _read_text(PLANNER_PATH, failures)
|
|
_expect_true(planner_source.contains("extends RefCounted"), "planner is RefCounted", failures)
|
|
_expect_true(not planner_source.contains("extends Node"), "planner does not inherit Node", failures)
|
|
_expect_true(not planner_source.contains("Vector3"), "planner keeps typed position boundary", failures)
|
|
for forbidden_text in ["WorkerThreadPool", "RenderingServer", "ResourceLoader", "_tile_load_queue"]:
|
|
_expect_true(not planner_source.contains(forbidden_text), "planner omits %s" % forbidden_text, failures)
|
|
|
|
var loader_source := _read_text(LOADER_PATH, failures)
|
|
_expect_true(loader_source.contains("_streaming_target_planner.plan_runtime"), "loader delegates runtime planning", failures)
|
|
_expect_true(loader_source.contains("_streaming_target_planner.plan_editor"), "loader delegates editor planning", failures)
|
|
_expect_true(not loader_source.contains("func _predictive_focus_tiles"), "loader no longer owns prefetch planner", failures)
|
|
|
|
|
|
func _measure_planner_iterations(planner, failures: Array[String]) -> float:
|
|
var available_tiles := _tile_catalog(24, 40, 40, 56)
|
|
var focus_position = _tile_position(32, 48, 0.99, 0.99)
|
|
var policy = POLICY_SCRIPT.new(40, 5, 1, 3, 4, 0.42)
|
|
var started_microseconds := Time.get_ticks_usec()
|
|
for _iteration in PERFORMANCE_ITERATIONS:
|
|
planner.plan_runtime(available_tiles, focus_position, policy)
|
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
|
var average_plan_milliseconds := elapsed_milliseconds / float(PERFORMANCE_ITERATIONS)
|
|
_expect_true(
|
|
average_plan_milliseconds <= MAXIMUM_AVERAGE_PLAN_MILLISECONDS,
|
|
"average plan stays within %.1f ms (actual %.3f)" % [
|
|
MAXIMUM_AVERAGE_PLAN_MILLISECONDS,
|
|
average_plan_milliseconds,
|
|
],
|
|
failures
|
|
)
|
|
return elapsed_milliseconds
|
|
|
|
|
|
func _tile_catalog(minimum_tile_x: int, maximum_tile_x: int, minimum_tile_y: int, maximum_tile_y: int) -> Dictionary:
|
|
var available_tiles: Dictionary = {}
|
|
for tile_y in range(minimum_tile_y, maximum_tile_y + 1):
|
|
for tile_x in range(minimum_tile_x, maximum_tile_x + 1):
|
|
available_tiles["%d_%d" % [tile_x, tile_y]] = "synthetic"
|
|
return available_tiles
|
|
|
|
|
|
func _tile_position(tile_x: int, tile_y: int, east_fraction: float, south_fraction: float) -> GodotWorldPosition:
|
|
var tile_coordinate = ADT_TILE_COORDINATE_SCRIPT.new(tile_x, tile_y)
|
|
var tile_local_position = ADT_TILE_LOCAL_POSITION_SCRIPT.new(
|
|
east_fraction * COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS,
|
|
0.0,
|
|
south_fraction * COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
|
)
|
|
return COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(tile_coordinate, tile_local_position)
|
|
|
|
|
|
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_set_equal(actual_set: Dictionary, expected_keys: Array, label: String, failures: Array[String]) -> void:
|
|
var actual_keys := actual_set.keys()
|
|
actual_keys.sort()
|
|
var sorted_expected_keys := expected_keys.duplicate()
|
|
sorted_expected_keys.sort()
|
|
if actual_keys != sorted_expected_keys:
|
|
failures.append("%s expected %s, got %s" % [label, sorted_expected_keys, actual_keys])
|
|
|
|
|
|
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_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
|
if not actual_value:
|
|
failures.append("%s expected true" % label)
|