rnd(M03): extract streaming target planner
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
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
class_name StreamingTargetPlan
|
||||
extends RefCounted
|
||||
|
||||
## Immutable result of one runtime or editor target-planning operation.
|
||||
## Tile-key sets use the streamer's existing `<tile_x>_<tile_y>` representation.
|
||||
|
||||
var focus_tile: AdtTileCoordinate:
|
||||
get:
|
||||
return _focus_tile
|
||||
|
||||
var _focus_tile: AdtTileCoordinate
|
||||
var _wanted_tile_keys: Dictionary
|
||||
var _retained_tile_keys: Dictionary
|
||||
|
||||
|
||||
## Takes ownership of planner-created sets and freezes them. The planner creates
|
||||
## these dictionaries independently from its catalog input, so no copy is needed.
|
||||
func _init(
|
||||
focus_tile_value: AdtTileCoordinate,
|
||||
wanted_tile_keys_value: Dictionary,
|
||||
retained_tile_keys_value: Dictionary
|
||||
) -> void:
|
||||
_focus_tile = focus_tile_value
|
||||
_wanted_tile_keys = wanted_tile_keys_value
|
||||
_retained_tile_keys = retained_tile_keys_value
|
||||
_wanted_tile_keys.make_read_only()
|
||||
_retained_tile_keys.make_read_only()
|
||||
|
||||
|
||||
## Returns the immutable wanted tile-key set.
|
||||
func wanted_tile_keys() -> Dictionary:
|
||||
return _wanted_tile_keys
|
||||
|
||||
|
||||
## Returns the immutable retained tile-key set.
|
||||
func retained_tile_keys() -> Dictionary:
|
||||
return _retained_tile_keys
|
||||
@@ -0,0 +1 @@
|
||||
uid://fq1ax8tdju2q
|
||||
@@ -0,0 +1,104 @@
|
||||
class_name StreamingTargetPlanner
|
||||
extends RefCounted
|
||||
|
||||
## Scene-free deterministic calculator for runtime/editor ADT target sets.
|
||||
## It creates no tasks or renderer resources and never mutates its inputs.
|
||||
|
||||
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 TARGET_PLAN_SCRIPT := preload("res://src/render/streaming/streaming_target_plan.gd")
|
||||
|
||||
|
||||
## Builds runtime wanted and retained sets, including the existing edge/corner
|
||||
## prefetch centers. `available_tile_paths_by_key` is read only for membership.
|
||||
func plan_runtime(
|
||||
available_tile_paths_by_key: Dictionary,
|
||||
focus_position: GodotWorldPosition,
|
||||
policy: StreamingTargetPolicy
|
||||
) -> StreamingTargetPlan:
|
||||
var focus_tile = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(focus_position)
|
||||
var prefetch_centers := _prefetch_centers(focus_position, focus_tile, policy)
|
||||
var wanted_tile_keys := _available_keys_within_radius(
|
||||
available_tile_paths_by_key,
|
||||
prefetch_centers,
|
||||
policy.load_tile_radius()
|
||||
)
|
||||
var retained_tile_keys := _available_keys_within_radius(
|
||||
available_tile_paths_by_key,
|
||||
prefetch_centers,
|
||||
policy.retained_tile_radius()
|
||||
)
|
||||
return TARGET_PLAN_SCRIPT.new(focus_tile, wanted_tile_keys, retained_tile_keys)
|
||||
|
||||
|
||||
## Builds the editor square preview set. Editor wanted and retained sets are
|
||||
## identical and a negative radius is clamped to the focus tile.
|
||||
func plan_editor(
|
||||
available_tile_paths_by_key: Dictionary,
|
||||
focus_position: GodotWorldPosition,
|
||||
editor_preview_tile_radius: int
|
||||
) -> StreamingTargetPlan:
|
||||
var focus_tile = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(focus_position)
|
||||
var centers: Array[AdtTileCoordinate] = [focus_tile]
|
||||
var wanted_tile_keys := _available_keys_within_radius(
|
||||
available_tile_paths_by_key,
|
||||
centers,
|
||||
maxi(0, editor_preview_tile_radius)
|
||||
)
|
||||
return TARGET_PLAN_SCRIPT.new(focus_tile, wanted_tile_keys, wanted_tile_keys)
|
||||
|
||||
|
||||
func _prefetch_centers(
|
||||
focus_position: GodotWorldPosition,
|
||||
focus_tile: AdtTileCoordinate,
|
||||
policy: StreamingTargetPolicy
|
||||
) -> Array[AdtTileCoordinate]:
|
||||
var centers: Array[AdtTileCoordinate] = [focus_tile]
|
||||
var threshold := policy.clamped_boundary_prefetch_threshold()
|
||||
if threshold <= 0.0:
|
||||
return centers
|
||||
|
||||
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(focus_position)
|
||||
var local_x_fraction: float = tile_local_position.east_yards / COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
||||
var local_z_fraction: float = tile_local_position.south_yards / COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
||||
var tile_x_offsets: Array[int] = [0]
|
||||
var tile_y_offsets: Array[int] = [0]
|
||||
|
||||
if local_x_fraction <= threshold:
|
||||
tile_x_offsets.append(-1)
|
||||
elif local_x_fraction >= 1.0 - threshold:
|
||||
tile_x_offsets.append(1)
|
||||
|
||||
if local_z_fraction <= threshold:
|
||||
tile_y_offsets.append(-1)
|
||||
elif local_z_fraction >= 1.0 - threshold:
|
||||
tile_y_offsets.append(1)
|
||||
|
||||
for tile_x_offset in tile_x_offsets:
|
||||
for tile_y_offset in tile_y_offsets:
|
||||
if tile_x_offset == 0 and tile_y_offset == 0:
|
||||
continue
|
||||
centers.append(ADT_TILE_COORDINATE_SCRIPT.new(
|
||||
focus_tile.tile_x + tile_x_offset,
|
||||
focus_tile.tile_y + tile_y_offset
|
||||
))
|
||||
return centers
|
||||
|
||||
|
||||
func _available_keys_within_radius(
|
||||
available_tile_paths_by_key: Dictionary,
|
||||
centers: Array[AdtTileCoordinate],
|
||||
tile_radius: int
|
||||
) -> Dictionary:
|
||||
var available_tile_keys: Dictionary = {}
|
||||
for center in centers:
|
||||
for tile_y in range(center.tile_y - tile_radius, center.tile_y + tile_radius + 1):
|
||||
for tile_x in range(center.tile_x - tile_radius, center.tile_x + tile_radius + 1):
|
||||
var tile_key := _tile_key(tile_x, tile_y)
|
||||
if available_tile_paths_by_key.has(tile_key):
|
||||
available_tile_keys[tile_key] = true
|
||||
return available_tile_keys
|
||||
|
||||
|
||||
func _tile_key(tile_x: int, tile_y: int) -> String:
|
||||
return "%d_%d" % [tile_x, tile_y]
|
||||
@@ -0,0 +1 @@
|
||||
uid://crqjbgoe3f6ea
|
||||
@@ -0,0 +1,108 @@
|
||||
class_name StreamingTargetPolicy
|
||||
extends RefCounted
|
||||
|
||||
## Immutable runtime tile-radius and boundary-prefetch policy consumed by
|
||||
## [StreamingTargetPlanner]. Values preserve scene configuration exactly;
|
||||
## effective radii and thresholds apply the existing renderer clamps.
|
||||
|
||||
const CHUNKS_PER_ADT_TILE := 16
|
||||
|
||||
var lod2_radius_chunks: int:
|
||||
get:
|
||||
return _lod2_radius_chunks
|
||||
|
||||
var lod2_tile_radius: int:
|
||||
get:
|
||||
return _lod2_tile_radius
|
||||
|
||||
var warm_tile_margin: int:
|
||||
get:
|
||||
return _warm_tile_margin
|
||||
|
||||
var prewarm_tile_margin: int:
|
||||
get:
|
||||
return _prewarm_tile_margin
|
||||
|
||||
var retain_tile_margin: int:
|
||||
get:
|
||||
return _retain_tile_margin
|
||||
|
||||
var boundary_prefetch_threshold: float:
|
||||
get:
|
||||
return _boundary_prefetch_threshold
|
||||
|
||||
var _lod2_radius_chunks: int
|
||||
var _lod2_tile_radius: int
|
||||
var _warm_tile_margin: int
|
||||
var _prewarm_tile_margin: int
|
||||
var _retain_tile_margin: int
|
||||
var _boundary_prefetch_threshold: float
|
||||
|
||||
|
||||
## Captures one renderer configuration snapshot for a deterministic plan.
|
||||
func _init(
|
||||
lod2_radius_chunks_value: int,
|
||||
lod2_tile_radius_value: int,
|
||||
warm_tile_margin_value: int,
|
||||
prewarm_tile_margin_value: int,
|
||||
retain_tile_margin_value: int,
|
||||
boundary_prefetch_threshold_value: float
|
||||
) -> void:
|
||||
_lod2_radius_chunks = lod2_radius_chunks_value
|
||||
_lod2_tile_radius = lod2_tile_radius_value
|
||||
_warm_tile_margin = warm_tile_margin_value
|
||||
_prewarm_tile_margin = prewarm_tile_margin_value
|
||||
_retain_tile_margin = retain_tile_margin_value
|
||||
_boundary_prefetch_threshold = boundary_prefetch_threshold_value
|
||||
|
||||
|
||||
## Returns the current visible ADT radius derived from chunk and tile settings.
|
||||
func visible_tile_radius() -> int:
|
||||
return calculate_visible_tile_radius(_lod2_radius_chunks, _lod2_tile_radius, _warm_tile_margin)
|
||||
|
||||
|
||||
## Returns the visible radius plus the non-negative prewarm margin.
|
||||
func load_tile_radius() -> int:
|
||||
return calculate_load_tile_radius(
|
||||
_lod2_radius_chunks,
|
||||
_lod2_tile_radius,
|
||||
_warm_tile_margin,
|
||||
_prewarm_tile_margin
|
||||
)
|
||||
|
||||
|
||||
## Returns the load radius plus the non-negative hysteresis margin.
|
||||
func retained_tile_radius() -> int:
|
||||
return load_tile_radius() + maxi(0, _retain_tile_margin)
|
||||
|
||||
|
||||
## Returns the edge fraction accepted by the existing prefetch policy.
|
||||
func clamped_boundary_prefetch_threshold() -> float:
|
||||
return clampf(_boundary_prefetch_threshold, 0.0, 0.49)
|
||||
|
||||
|
||||
## Calculates the visible radius without allocating a policy. The streamer uses
|
||||
## this hot-path form while finalizing tiles between planning refreshes.
|
||||
static func calculate_visible_tile_radius(
|
||||
lod2_radius_chunks_value: int,
|
||||
lod2_tile_radius_value: int,
|
||||
warm_tile_margin_value: int
|
||||
) -> int:
|
||||
var chunk_tile_radius := int(ceil(
|
||||
float(lod2_radius_chunks_value) / float(CHUNKS_PER_ADT_TILE)
|
||||
)) + warm_tile_margin_value
|
||||
return maxi(chunk_tile_radius, lod2_tile_radius_value)
|
||||
|
||||
|
||||
## Calculates the load radius without allocating a policy.
|
||||
static func calculate_load_tile_radius(
|
||||
lod2_radius_chunks_value: int,
|
||||
lod2_tile_radius_value: int,
|
||||
warm_tile_margin_value: int,
|
||||
prewarm_tile_margin_value: int
|
||||
) -> int:
|
||||
return calculate_visible_tile_radius(
|
||||
lod2_radius_chunks_value,
|
||||
lod2_tile_radius_value,
|
||||
warm_tile_margin_value
|
||||
) + maxi(0, prewarm_tile_margin_value)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfrad5wr668w5
|
||||
@@ -17,6 +17,8 @@ const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot
|
||||
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 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 REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
|
||||
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
|
||||
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
|
||||
@@ -174,6 +176,7 @@ const QUALITY_HIGH := "High"
|
||||
@export var hitch_profiler_threshold_ms: float = 20.0
|
||||
|
||||
var _builder
|
||||
var _streaming_target_planner := STREAMING_TARGET_PLANNER_SCRIPT.new()
|
||||
var _map_dir := ""
|
||||
var _abs_map_dir := ""
|
||||
var _wdt_info: Dictionary = {}
|
||||
@@ -720,57 +723,47 @@ func _refresh_editor_streaming_targets_at(focus_pos: Vector3, force: bool) -> vo
|
||||
if not force and not _should_refresh_focus(focus_pos):
|
||||
return
|
||||
|
||||
var focus_tile := _world_to_tile(focus_pos)
|
||||
var total_tile_radius: int = maxi(0, editor_preview_tile_radius)
|
||||
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
|
||||
var target_plan = _streaming_target_planner.plan_editor(
|
||||
_available_tiles,
|
||||
typed_focus_position,
|
||||
editor_preview_tile_radius
|
||||
)
|
||||
var wanted_tiles: Dictionary = target_plan.wanted_tile_keys()
|
||||
|
||||
var wanted_tiles: Dictionary = {}
|
||||
for ty in range(focus_tile.y - total_tile_radius, focus_tile.y + total_tile_radius + 1):
|
||||
for tx in range(focus_tile.x - total_tile_radius, focus_tile.x + total_tile_radius + 1):
|
||||
var key := _tile_key(tx, ty)
|
||||
if _available_tiles.has(key):
|
||||
wanted_tiles[key] = true
|
||||
|
||||
_apply_streaming_target(wanted_tiles, wanted_tiles, focus_pos)
|
||||
_apply_streaming_target(wanted_tiles, target_plan.retained_tile_keys(), focus_pos)
|
||||
_note_refresh_focus(focus_pos)
|
||||
|
||||
if force or debug_streaming:
|
||||
print("StreamingWorld(editor): focus_tile=%s wanted=%d queued=%d" % [
|
||||
focus_tile, wanted_tiles.size(), _tile_load_queue.size()])
|
||||
Vector2i(target_plan.focus_tile.tile_x, target_plan.focus_tile.tile_y),
|
||||
wanted_tiles.size(),
|
||||
_tile_load_queue.size(),
|
||||
])
|
||||
|
||||
|
||||
func _refresh_streaming_targets_at(focus_pos: Vector3, force: bool) -> void:
|
||||
if not force and not _should_refresh_focus(focus_pos):
|
||||
return
|
||||
|
||||
var focus_tile := _world_to_tile(focus_pos)
|
||||
|
||||
# Chunk-LOD radius → tile radius (ceiling), plus margin for pre-warming.
|
||||
var focus_tiles := _predictive_focus_tiles(focus_pos)
|
||||
var total_tile_radius: int = _runtime_load_tile_radius()
|
||||
var retain_tile_radius: int = _runtime_retain_tile_radius()
|
||||
|
||||
var retained_tiles: Dictionary = {}
|
||||
for center in focus_tiles:
|
||||
for ty in range(center.y - retain_tile_radius, center.y + retain_tile_radius + 1):
|
||||
for tx in range(center.x - retain_tile_radius, center.x + retain_tile_radius + 1):
|
||||
var retained_key := _tile_key(tx, ty)
|
||||
if _available_tiles.has(retained_key):
|
||||
retained_tiles[retained_key] = true
|
||||
|
||||
var wanted_tiles: Dictionary = {}
|
||||
for center in focus_tiles:
|
||||
for ty in range(center.y - total_tile_radius, center.y + total_tile_radius + 1):
|
||||
for tx in range(center.x - total_tile_radius, center.x + total_tile_radius + 1):
|
||||
var key := _tile_key(tx, ty)
|
||||
if _available_tiles.has(key):
|
||||
wanted_tiles[key] = true
|
||||
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
|
||||
var target_plan = _streaming_target_planner.plan_runtime(
|
||||
_available_tiles,
|
||||
typed_focus_position,
|
||||
_streaming_target_policy()
|
||||
)
|
||||
var wanted_tiles: Dictionary = target_plan.wanted_tile_keys()
|
||||
var retained_tiles: Dictionary = target_plan.retained_tile_keys()
|
||||
|
||||
_apply_streaming_target(wanted_tiles, retained_tiles, focus_pos)
|
||||
_note_refresh_focus(focus_pos)
|
||||
|
||||
if force or debug_streaming:
|
||||
print("StreamingWorld: focus_tile=%s wanted=%d queued=%d" % [
|
||||
focus_tile, wanted_tiles.size(), _tile_load_queue.size()])
|
||||
Vector2i(target_plan.focus_tile.tile_x, target_plan.focus_tile.tile_y),
|
||||
wanted_tiles.size(),
|
||||
_tile_load_queue.size(),
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -2891,50 +2884,24 @@ func _compute_desired_lods(state: Dictionary, cam_pos: Vector3) -> Dictionary:
|
||||
return desired
|
||||
|
||||
|
||||
func _runtime_visible_tile_radius() -> int:
|
||||
var chunk_tile_radius: int = int(ceil(float(lod2_radius_chunks) / 16.0)) + warm_tile_margin
|
||||
return maxi(chunk_tile_radius, lod2_tile_radius)
|
||||
|
||||
|
||||
func _runtime_load_tile_radius() -> int:
|
||||
return _runtime_visible_tile_radius() + maxi(0, prewarm_tile_margin)
|
||||
return STREAMING_TARGET_POLICY_SCRIPT.calculate_load_tile_radius(
|
||||
lod2_radius_chunks,
|
||||
lod2_tile_radius,
|
||||
warm_tile_margin,
|
||||
prewarm_tile_margin
|
||||
)
|
||||
|
||||
|
||||
func _runtime_retain_tile_radius() -> int:
|
||||
return _runtime_load_tile_radius() + maxi(0, retain_tile_margin)
|
||||
|
||||
|
||||
func _predictive_focus_tiles(focus_pos: Vector3) -> Array[Vector2i]:
|
||||
var base := _world_to_tile(focus_pos)
|
||||
var result: Array[Vector2i] = [base]
|
||||
var threshold: float = clampf(boundary_prefetch_threshold, 0.0, 0.49)
|
||||
if threshold <= 0.0:
|
||||
return result
|
||||
|
||||
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
|
||||
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_focus_position)
|
||||
var local_x: float = tile_local_position.east_yards / TILE_SIZE
|
||||
var local_z: float = tile_local_position.south_yards / TILE_SIZE
|
||||
var x_offsets: Array[int] = [0]
|
||||
var z_offsets: Array[int] = [0]
|
||||
|
||||
if local_x <= threshold:
|
||||
x_offsets.append(-1)
|
||||
elif local_x >= 1.0 - threshold:
|
||||
x_offsets.append(1)
|
||||
|
||||
if local_z <= threshold:
|
||||
z_offsets.append(-1)
|
||||
elif local_z >= 1.0 - threshold:
|
||||
z_offsets.append(1)
|
||||
|
||||
for ox in x_offsets:
|
||||
for oz in z_offsets:
|
||||
if ox == 0 and oz == 0:
|
||||
continue
|
||||
result.append(Vector2i(base.x + ox, base.y + oz))
|
||||
|
||||
return result
|
||||
func _streaming_target_policy() -> StreamingTargetPolicy:
|
||||
return STREAMING_TARGET_POLICY_SCRIPT.new(
|
||||
lod2_radius_chunks,
|
||||
lod2_tile_radius,
|
||||
warm_tile_margin,
|
||||
prewarm_tile_margin,
|
||||
retain_tile_margin,
|
||||
boundary_prefetch_threshold
|
||||
)
|
||||
|
||||
|
||||
func _compute_desired_tile_lod(state: Dictionary, cam_pos: Vector3) -> int:
|
||||
|
||||
@@ -19,7 +19,8 @@ const ALLOWED_LEGACY_NAME_PATHS: Array[String] = [
|
||||
]
|
||||
const REQUIRED_MAPPER_CALLS := {
|
||||
"res://src/scenes/sky/wow_sky_controller.gd": ["godot_to_canonical", "godot_to_adt_tile", "godot_to_adt_tile_local"],
|
||||
"res://src/scenes/streaming/streaming_world_loader.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_godot"],
|
||||
"res://src/scenes/streaming/streaming_world_loader.gd": ["godot_to_adt_tile", "adt_tile_local_to_godot"],
|
||||
"res://src/render/streaming/streaming_target_planner.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local"],
|
||||
"res://src/scenes/player/third_person_wow_controller.gd": ["adt_tile_local_to_godot"],
|
||||
"res://src/gameplay/terrain/adt_terrain_query.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_chunk"],
|
||||
"res://src/tools/probe_render_terrain_height.gd": ["godot_to_adt_tile"],
|
||||
@@ -60,7 +61,7 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("COORDINATE_CONVERSION_BOUNDARIES PASS files=%d native_boundary=1 oracle=1 consumers=5 classifier_cases=6" % source_paths.size())
|
||||
print("COORDINATE_CONVERSION_BOUNDARIES PASS files=%d native_boundary=1 oracle=1 consumers=6 classifier_cases=6" % source_paths.size())
|
||||
quit(0)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b5unt23vi8krk
|
||||
Reference in New Issue
Block a user