refactor(M03): extract ADT water load pipeline state
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
class_name AdtWaterLoadPipelineState
|
||||
extends RefCounted
|
||||
|
||||
## Owns ADT water request, active-task and worker-result bookkeeping. The caller
|
||||
## owns WorkerThreadPool operations, ADT parsing, permits and scene finalization.
|
||||
|
||||
var _pending_requests: Array[Dictionary] = []
|
||||
var _queued_tile_keys: Dictionary = {}
|
||||
var _active_task_ids_by_tile_key: Dictionary = {}
|
||||
var _result_mutex := Mutex.new()
|
||||
var _results: Array[Dictionary] = []
|
||||
|
||||
|
||||
## Appends one tile/path request in FIFO order. Invalid, queued or active tile
|
||||
## keys are rejected without mutation.
|
||||
func enqueue_request(tile_key: String, adt_path: String) -> bool:
|
||||
if tile_key.is_empty() or adt_path.is_empty():
|
||||
return false
|
||||
if _queued_tile_keys.has(tile_key) or _active_task_ids_by_tile_key.has(tile_key):
|
||||
return false
|
||||
_pending_requests.append({
|
||||
"key": tile_key,
|
||||
"path": adt_path,
|
||||
})
|
||||
_queued_tile_keys[tile_key] = true
|
||||
return true
|
||||
|
||||
|
||||
## Removes and returns the oldest request, also releasing its queued-key marker.
|
||||
## An empty Dictionary means no pending request exists.
|
||||
func take_next_request() -> Dictionary:
|
||||
if _pending_requests.is_empty():
|
||||
return {}
|
||||
var request: Dictionary = _pending_requests.pop_front()
|
||||
_queued_tile_keys.erase(String(request.get("key", "")))
|
||||
return request
|
||||
|
||||
|
||||
## Removes all pending requests for one tile without affecting active work.
|
||||
## Returns the number of removed FIFO entries.
|
||||
func cancel_pending_requests(tile_key: String) -> int:
|
||||
if tile_key.is_empty():
|
||||
return 0
|
||||
_queued_tile_keys.erase(tile_key)
|
||||
var removed_count := 0
|
||||
for request_index in range(_pending_requests.size() - 1, -1, -1):
|
||||
var request: Dictionary = _pending_requests[request_index]
|
||||
if String(request.get("key", "")) != tile_key:
|
||||
continue
|
||||
_pending_requests.remove_at(request_index)
|
||||
removed_count += 1
|
||||
return removed_count
|
||||
|
||||
|
||||
## Records one caller-started worker task. The opaque task ID is retained until
|
||||
## the caller waits for and completes it.
|
||||
func remember_active_task(tile_key: String, task_id: int) -> bool:
|
||||
if tile_key.is_empty() or _active_task_ids_by_tile_key.has(tile_key):
|
||||
return false
|
||||
_active_task_ids_by_tile_key[tile_key] = task_id
|
||||
return true
|
||||
|
||||
|
||||
## Returns whether a worker task is active for the tile.
|
||||
func has_active_task(tile_key: String) -> bool:
|
||||
return not tile_key.is_empty() and _active_task_ids_by_tile_key.has(tile_key)
|
||||
|
||||
|
||||
## Returns the opaque active task ID, or -1 for an unknown tile.
|
||||
func active_task_id_for(tile_key: String) -> int:
|
||||
return int(_active_task_ids_by_tile_key.get(tile_key, -1))
|
||||
|
||||
|
||||
## Removes and returns one completed task ID, or -1 for an unknown tile.
|
||||
func complete_active_task(tile_key: String) -> int:
|
||||
if tile_key.is_empty() or not _active_task_ids_by_tile_key.has(tile_key):
|
||||
return -1
|
||||
var task_id := int(_active_task_ids_by_tile_key[tile_key])
|
||||
_active_task_ids_by_tile_key.erase(tile_key)
|
||||
return task_id
|
||||
|
||||
|
||||
## Returns a detached snapshot of opaque task IDs for orderly shutdown waiting.
|
||||
func active_task_ids_snapshot() -> Array:
|
||||
return _active_task_ids_by_tile_key.values().duplicate()
|
||||
|
||||
|
||||
## Publishes one parsed ADT result from a worker thread. Payload identity is
|
||||
## retained; the caller must not mutate it after publication.
|
||||
func publish_result(tile_key: String, adt_path: String, water_data: Dictionary) -> bool:
|
||||
if tile_key.is_empty() or adt_path.is_empty():
|
||||
return false
|
||||
_result_mutex.lock()
|
||||
_results.append({
|
||||
"key": tile_key,
|
||||
"path": adt_path,
|
||||
"data": water_data,
|
||||
})
|
||||
_result_mutex.unlock()
|
||||
return true
|
||||
|
||||
|
||||
## Returns whether a worker result is currently ready.
|
||||
func has_result() -> bool:
|
||||
_result_mutex.lock()
|
||||
var result_available := not _results.is_empty()
|
||||
_result_mutex.unlock()
|
||||
return result_available
|
||||
|
||||
|
||||
## Removes and returns the oldest worker result. An empty Dictionary means no
|
||||
## result was ready. Intended for the serialized renderer main thread.
|
||||
func pop_result() -> Dictionary:
|
||||
_result_mutex.lock()
|
||||
var result: Dictionary = {}
|
||||
if not _results.is_empty():
|
||||
result = _results.pop_front()
|
||||
_result_mutex.unlock()
|
||||
return result
|
||||
|
||||
|
||||
## Returns pending plus active load count for existing renderer metrics.
|
||||
func total_load_count() -> int:
|
||||
return _pending_requests.size() + _active_task_ids_by_tile_key.size()
|
||||
|
||||
|
||||
## Returns the pending request count.
|
||||
func pending_request_count() -> int:
|
||||
return _pending_requests.size()
|
||||
|
||||
|
||||
## Returns the active worker-task count.
|
||||
func active_task_count() -> int:
|
||||
return _active_task_ids_by_tile_key.size()
|
||||
|
||||
|
||||
## Clears bookkeeping and queued results without interrupting worker tasks.
|
||||
## Orderly shutdown must wait the task-ID snapshot before calling this method.
|
||||
func clear() -> void:
|
||||
_pending_requests.clear()
|
||||
_queued_tile_keys.clear()
|
||||
_active_task_ids_by_tile_key.clear()
|
||||
_result_mutex.lock()
|
||||
_results.clear()
|
||||
_result_mutex.unlock()
|
||||
|
||||
|
||||
## Returns detached FIFO/task/result scalar diagnostics without water payloads.
|
||||
func diagnostic_snapshot() -> Dictionary:
|
||||
var pending_requests: Array[Dictionary] = []
|
||||
for request in _pending_requests:
|
||||
pending_requests.append(request.duplicate())
|
||||
var active_tile_keys: Array = _active_task_ids_by_tile_key.keys()
|
||||
active_tile_keys.sort()
|
||||
_result_mutex.lock()
|
||||
var result_records: Array[Dictionary] = []
|
||||
for result in _results:
|
||||
result_records.append({
|
||||
"key": String(result.get("key", "")),
|
||||
"path": String(result.get("path", "")),
|
||||
})
|
||||
_result_mutex.unlock()
|
||||
return {
|
||||
"pending_requests": pending_requests,
|
||||
"active_tile_keys": active_tile_keys.duplicate(),
|
||||
"result_records": result_records,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://pwqm3n25pnn7
|
||||
@@ -42,6 +42,9 @@ const TERRAIN_CHUNK_LOD_PLANNER_SCRIPT := preload("res://src/render/terrain/terr
|
||||
const TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT := preload(
|
||||
"res://src/render/terrain/terrain_chunk_geometry_queue_planner.gd"
|
||||
)
|
||||
const ADT_WATER_LOAD_PIPELINE_STATE_SCRIPT := preload(
|
||||
"res://src/render/liquid/adt_water_load_pipeline_state.gd"
|
||||
)
|
||||
const M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_unique_placement_registry.gd"
|
||||
)
|
||||
@@ -256,11 +259,7 @@ var _terrain_splat_cache_tasks: Dictionary = {}
|
||||
var _terrain_splat_tasks: Dictionary = {}
|
||||
var _terrain_splat_result_mutex := Mutex.new()
|
||||
var _terrain_splat_result_queue: Array = []
|
||||
var _water_load_queue: Array = []
|
||||
var _water_load_queued: Dictionary = {}
|
||||
var _water_load_tasks: Dictionary = {}
|
||||
var _water_result_mutex := Mutex.new()
|
||||
var _water_result_queue: Array = []
|
||||
var _adt_water_load_pipeline_state := ADT_WATER_LOAD_PIPELINE_STATE_SCRIPT.new()
|
||||
var _tile_result_mutex := Mutex.new()
|
||||
var _tile_result_queue: Array = []
|
||||
var _shared_tex_cache: Dictionary = {}
|
||||
@@ -999,7 +998,7 @@ func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bo
|
||||
_terrain_upgrade_tasks.size(),
|
||||
_terrain_control_splat_cache_tasks.size(),
|
||||
_terrain_splat_queue_size(),
|
||||
_water_load_queue.size() + _water_load_tasks.size(),
|
||||
_adt_water_load_pipeline_state.total_load_count(),
|
||||
_detail_asset_queue.size(),
|
||||
_m2_group_tasks.size(),
|
||||
_m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
|
||||
@@ -1030,7 +1029,7 @@ func render_baseline_snapshot() -> Dictionary:
|
||||
"terrain_upgrade": _terrain_upgrade_tasks.size(),
|
||||
"terrain_control": _terrain_control_splat_cache_tasks.size(),
|
||||
"terrain_splat": _terrain_splat_queue_size(),
|
||||
"water": _water_load_queue.size() + _water_load_tasks.size(),
|
||||
"water": _adt_water_load_pipeline_state.total_load_count(),
|
||||
"detail": _detail_asset_queue.size(),
|
||||
"m2_task": _m2_group_tasks.size(),
|
||||
"m2_animation": _m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
|
||||
@@ -1305,7 +1304,7 @@ func _tick_runtime_stats(delta: float) -> void:
|
||||
_terrain_upgrade_tasks.size(),
|
||||
_terrain_control_splat_cache_tasks.size(),
|
||||
_terrain_splat_queue_size(),
|
||||
_water_load_queue.size() + _water_load_tasks.size(),
|
||||
_adt_water_load_pipeline_state.total_load_count(),
|
||||
terrain_baked_full,
|
||||
terrain_control_splat,
|
||||
terrain_splat,
|
||||
@@ -2230,24 +2229,20 @@ func _request_tile_water_load(state: Dictionary) -> void:
|
||||
var path := String(state.get("path", ""))
|
||||
if key.is_empty() or path.is_empty():
|
||||
return
|
||||
if _water_load_tasks.has(key) or _water_load_queued.has(key):
|
||||
return
|
||||
_water_load_queue.append({
|
||||
"key": key,
|
||||
"path": path,
|
||||
})
|
||||
_water_load_queued[key] = true
|
||||
_adt_water_load_pipeline_state.enqueue_request(key, path)
|
||||
|
||||
|
||||
func _process_water_load_queue() -> void:
|
||||
if not enable_water:
|
||||
return
|
||||
var limit := maxi(1, max_concurrent_water_tasks)
|
||||
while _water_load_tasks.size() < limit and not _water_load_queue.is_empty():
|
||||
var request: Dictionary = _water_load_queue.pop_front()
|
||||
while (
|
||||
_adt_water_load_pipeline_state.active_task_count() < limit
|
||||
and _adt_water_load_pipeline_state.pending_request_count() > 0
|
||||
):
|
||||
var request: Dictionary = _adt_water_load_pipeline_state.take_next_request()
|
||||
var key := String(request.get("key", ""))
|
||||
_water_load_queued.erase(key)
|
||||
if key.is_empty() or _water_load_tasks.has(key):
|
||||
if key.is_empty() or _adt_water_load_pipeline_state.has_active_task(key):
|
||||
continue
|
||||
if not _tile_states.has(key):
|
||||
continue
|
||||
@@ -2255,7 +2250,7 @@ func _process_water_load_queue() -> void:
|
||||
if path.is_empty():
|
||||
continue
|
||||
var task_id: int = WorkerThreadPool.add_task(_load_tile_water_task.bind(key, path))
|
||||
_water_load_tasks[key] = task_id
|
||||
_adt_water_load_pipeline_state.remember_active_task(key, task_id)
|
||||
|
||||
|
||||
func _load_tile_water_task(key: String, adt_path: String) -> void:
|
||||
@@ -2264,13 +2259,7 @@ func _load_tile_water_task(key: String, adt_path: String) -> void:
|
||||
if loader:
|
||||
data = loader.call("load_adt", adt_path)
|
||||
|
||||
_water_result_mutex.lock()
|
||||
_water_result_queue.append({
|
||||
"key": key,
|
||||
"path": adt_path,
|
||||
"data": data,
|
||||
})
|
||||
_water_result_mutex.unlock()
|
||||
_adt_water_load_pipeline_state.publish_result(key, adt_path, data)
|
||||
|
||||
|
||||
func _drain_water_load_results() -> void:
|
||||
@@ -2278,20 +2267,20 @@ func _drain_water_load_results() -> void:
|
||||
if not _render_budget_scheduler.has_remaining_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.WATER_FINALIZE):
|
||||
return
|
||||
|
||||
_water_result_mutex.lock()
|
||||
while not _water_result_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
|
||||
while _adt_water_load_pipeline_state.has_result() and _render_budget_scheduler.try_consume_permit(
|
||||
RENDER_BUDGET_SCHEDULER_SCRIPT.WATER_FINALIZE):
|
||||
results.append(_water_result_queue.pop_front())
|
||||
_water_result_mutex.unlock()
|
||||
results.append(_adt_water_load_pipeline_state.pop_result())
|
||||
|
||||
for result in results:
|
||||
var key := String(result.get("key", ""))
|
||||
if key.is_empty():
|
||||
continue
|
||||
if not _water_load_tasks.has(key):
|
||||
if not _adt_water_load_pipeline_state.has_active_task(key):
|
||||
continue
|
||||
WorkerThreadPool.wait_for_task_completion(int(_water_load_tasks[key]))
|
||||
_water_load_tasks.erase(key)
|
||||
WorkerThreadPool.wait_for_task_completion(
|
||||
_adt_water_load_pipeline_state.active_task_id_for(key)
|
||||
)
|
||||
_adt_water_load_pipeline_state.complete_active_task(key)
|
||||
if _shutting_down or not _tile_states.has(key):
|
||||
continue
|
||||
|
||||
@@ -2419,14 +2408,9 @@ func _wait_for_tile_tasks() -> void:
|
||||
_terrain_splat_result_queue.clear()
|
||||
_terrain_splat_result_mutex.unlock()
|
||||
|
||||
for task_id in _water_load_tasks.values():
|
||||
for task_id in _adt_water_load_pipeline_state.active_task_ids_snapshot():
|
||||
WorkerThreadPool.wait_for_task_completion(int(task_id))
|
||||
_water_load_tasks.clear()
|
||||
_water_load_queue.clear()
|
||||
_water_load_queued.clear()
|
||||
_water_result_mutex.lock()
|
||||
_water_result_queue.clear()
|
||||
_water_result_mutex.unlock()
|
||||
_adt_water_load_pipeline_state.clear()
|
||||
|
||||
|
||||
func _should_accept_loaded_tile(request: Dictionary) -> bool:
|
||||
@@ -2961,11 +2945,7 @@ func _release_tile(key: String) -> void:
|
||||
_terrain_control_splat_cache_tasks.erase(key)
|
||||
_terrain_splat_cache_tasks.erase(key)
|
||||
_terrain_splat_tasks.erase(key)
|
||||
_water_load_queued.erase(key)
|
||||
for i in range(_water_load_queue.size() - 1, -1, -1):
|
||||
var pending: Dictionary = _water_load_queue[i]
|
||||
if String(pending.get("key", "")) == key:
|
||||
_water_load_queue.remove_at(i)
|
||||
_adt_water_load_pipeline_state.cancel_pending_requests(key)
|
||||
var tile_lod_rid: RID = state.get("tile_lod_rid", RID())
|
||||
if tile_lod_rid.is_valid():
|
||||
_free_render_instance(tile_lod_rid)
|
||||
@@ -2996,12 +2976,7 @@ func _clear_streamed_world() -> void:
|
||||
_terrain_splat_result_mutex.lock()
|
||||
_terrain_splat_result_queue.clear()
|
||||
_terrain_splat_result_mutex.unlock()
|
||||
_water_load_queue.clear()
|
||||
_water_load_queued.clear()
|
||||
_water_load_tasks.clear()
|
||||
_water_result_mutex.lock()
|
||||
_water_result_queue.clear()
|
||||
_water_result_mutex.unlock()
|
||||
_adt_water_load_pipeline_state.clear()
|
||||
_detail_asset_queue.clear()
|
||||
_detail_asset_queued.clear()
|
||||
for key in _m2_build_jobs.keys():
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free lifecycle, threading, boundary and timing regression for ADT water loads.
|
||||
|
||||
const PIPELINE_SCRIPT := preload(
|
||||
"res://src/render/liquid/adt_water_load_pipeline_state.gd"
|
||||
)
|
||||
const PIPELINE_PATH := "res://src/render/liquid/adt_water_load_pipeline_state.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_invalid_dedupe_and_fifo(failures)
|
||||
_verify_active_task_lifecycle(failures)
|
||||
_verify_result_mailbox_and_payload_identity(failures)
|
||||
_verify_worker_publication(failures)
|
||||
_verify_pending_cancellation(failures)
|
||||
_verify_clear_and_detached_diagnostics(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("ADT_WATER_LOAD_PIPELINE_STATE: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"ADT_WATER_LOAD_PIPELINE_STATE PASS cases=12 iterations=100 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_invalid_dedupe_and_fifo(failures: Array[String]) -> void:
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
_expect_false(
|
||||
pipeline.call("enqueue_request", "", "res://tile.adt"),
|
||||
"empty tile key rejected",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
pipeline.call("enqueue_request", "31:49", ""),
|
||||
"empty path rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
pipeline.call("enqueue_request", "31:49", "res://a.adt"),
|
||||
"first request accepted",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
pipeline.call("enqueue_request", "31:49", "res://duplicate.adt"),
|
||||
"queued duplicate rejected",
|
||||
failures
|
||||
)
|
||||
pipeline.call("enqueue_request", "32:49", "res://b.adt")
|
||||
_expect_equal(int(pipeline.call("pending_request_count")), 2, "pending count", failures)
|
||||
var first_request: Dictionary = pipeline.call("take_next_request")
|
||||
var second_request: Dictionary = pipeline.call("take_next_request")
|
||||
_expect_string_equal(String(first_request["key"]), "31:49", "FIFO first key", failures)
|
||||
_expect_string_equal(String(second_request["key"]), "32:49", "FIFO second key", failures)
|
||||
_expect_true(
|
||||
pipeline.call("enqueue_request", "31:49", "res://retry.adt"),
|
||||
"dequeued key can be requested again",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_active_task_lifecycle(failures: Array[String]) -> void:
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
_expect_false(
|
||||
pipeline.call("remember_active_task", "", 7),
|
||||
"empty active key rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
pipeline.call("remember_active_task", "31:49", 101),
|
||||
"active task accepted",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
pipeline.call("remember_active_task", "31:49", 202),
|
||||
"duplicate active task rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
pipeline.call("has_active_task", "31:49"),
|
||||
"active task observable",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
int(pipeline.call("active_task_id_for", "31:49")),
|
||||
101,
|
||||
"opaque task ID retained",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
pipeline.call("enqueue_request", "31:49", "res://active.adt"),
|
||||
"active tile rejects enqueue",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
int(pipeline.call("complete_active_task", "31:49")),
|
||||
101,
|
||||
"completion returns task ID",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
int(pipeline.call("complete_active_task", "31:49")),
|
||||
-1,
|
||||
"unknown completion rejected",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_result_mailbox_and_payload_identity(failures: Array[String]) -> void:
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var water_data := {"chunks": [{"liquid": true}]}
|
||||
_expect_false(
|
||||
pipeline.call("publish_result", "", "res://a.adt", water_data),
|
||||
"empty result key rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
pipeline.call("publish_result", "31:49", "res://a.adt", water_data),
|
||||
"valid result published",
|
||||
failures
|
||||
)
|
||||
_expect_true(pipeline.call("has_result"), "published result observable", failures)
|
||||
var result: Dictionary = pipeline.call("pop_result")
|
||||
_expect_string_equal(String(result["key"]), "31:49", "result key", failures)
|
||||
_expect_string_equal(String(result["path"]), "res://a.adt", "result path", failures)
|
||||
_expect_true(result["data"] == water_data, "water payload identity retained", failures)
|
||||
_expect_false(pipeline.call("has_result"), "result pop empties mailbox", failures)
|
||||
_expect_true(
|
||||
(pipeline.call("pop_result") as Dictionary).is_empty(),
|
||||
"empty result pop is safe",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_worker_publication(failures: Array[String]) -> void:
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var worker := Thread.new()
|
||||
var start_error := worker.start(
|
||||
Callable(pipeline, "publish_result").bind(
|
||||
"worker",
|
||||
"res://worker.adt",
|
||||
{"worker": true}
|
||||
)
|
||||
)
|
||||
_expect_equal(int(start_error), OK, "worker publication starts", failures)
|
||||
if start_error == OK:
|
||||
worker.wait_to_finish()
|
||||
_expect_true(pipeline.call("has_result"), "worker result safely visible", failures)
|
||||
var result: Dictionary = pipeline.call("pop_result")
|
||||
_expect_string_equal(String(result.get("key", "")), "worker", "worker result key", failures)
|
||||
|
||||
|
||||
func _verify_pending_cancellation(failures: Array[String]) -> void:
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
pipeline.call("enqueue_request", "31:49", "res://a.adt")
|
||||
pipeline.call("enqueue_request", "32:49", "res://b.adt")
|
||||
pipeline.call("remember_active_task", "31:49-active", 12)
|
||||
_expect_equal(
|
||||
int(pipeline.call("cancel_pending_requests", "31:49")),
|
||||
1,
|
||||
"tile cancellation removes pending request",
|
||||
failures
|
||||
)
|
||||
_expect_equal(int(pipeline.call("pending_request_count")), 1, "other pending retained", failures)
|
||||
_expect_true(
|
||||
pipeline.call("has_active_task", "31:49-active"),
|
||||
"pending cancellation leaves active work",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
int(pipeline.call("cancel_pending_requests", "unknown")),
|
||||
0,
|
||||
"unknown cancellation unchanged",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_clear_and_detached_diagnostics(failures: Array[String]) -> void:
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
pipeline.call("enqueue_request", "32:49", "res://b.adt")
|
||||
pipeline.call("enqueue_request", "31:49", "res://a.adt")
|
||||
pipeline.call("remember_active_task", "z", 2)
|
||||
pipeline.call("remember_active_task", "a", 1)
|
||||
pipeline.call("publish_result", "result", "res://result.adt", {"secret": "payload"})
|
||||
var snapshot: Dictionary = pipeline.call("diagnostic_snapshot")
|
||||
_expect_string_array(
|
||||
snapshot["active_tile_keys"],
|
||||
["a", "z"],
|
||||
"active diagnostics sorted",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
(snapshot["pending_requests"] as Array).size(),
|
||||
2,
|
||||
"pending diagnostics count",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
(snapshot["result_records"][0] as Dictionary).has("data"),
|
||||
"diagnostics omit water payload",
|
||||
failures
|
||||
)
|
||||
(snapshot["pending_requests"] as Array).clear()
|
||||
var fresh_snapshot: Dictionary = pipeline.call("diagnostic_snapshot")
|
||||
_expect_equal(
|
||||
(fresh_snapshot["pending_requests"] as Array).size(),
|
||||
2,
|
||||
"diagnostics detached",
|
||||
failures
|
||||
)
|
||||
pipeline.call("clear")
|
||||
pipeline.call("clear")
|
||||
_expect_equal(int(pipeline.call("total_load_count")), 0, "clear load count", failures)
|
||||
_expect_false(pipeline.call("has_result"), "clear result mailbox", failures)
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
var pipeline_source := _read_text(PIPELINE_PATH, failures)
|
||||
_expect_true(
|
||||
loader_source.contains("ADT_WATER_LOAD_PIPELINE_STATE_SCRIPT.new()"),
|
||||
"loader composes pipeline state",
|
||||
failures
|
||||
)
|
||||
for legacy_field in [
|
||||
"var _water_load_queue:",
|
||||
"var _water_load_queued:",
|
||||
"var _water_load_tasks:",
|
||||
"var _water_result_mutex",
|
||||
"var _water_result_queue:",
|
||||
]:
|
||||
_expect_false(loader_source.contains(legacy_field), "loader removes %s" % legacy_field, failures)
|
||||
for retained_loader_rule in [
|
||||
"ClassDB.instantiate(\"ADTLoader\")",
|
||||
"WorkerThreadPool.add_task(_load_tile_water_task.bind(key, path))",
|
||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.WATER_FINALIZE",
|
||||
"_builder.build_tile_water_scene(data, origin)",
|
||||
"tile_root.add_child(water_root)",
|
||||
"state[\"water_loaded\"] = true",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
for forbidden_token in [
|
||||
"WorkerThreadPool.",
|
||||
"ADTLoader",
|
||||
"RenderBudgetScheduler",
|
||||
"Node3D",
|
||||
"Mesh",
|
||||
"RID",
|
||||
"_tile_states",
|
||||
]:
|
||||
_expect_false(
|
||||
pipeline_source.contains(forbidden_token),
|
||||
"pipeline omits %s ownership" % forbidden_token,
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
_count_occurrences(loader_source, "_adt_water_load_pipeline_state.clear()"),
|
||||
2,
|
||||
"loader retains two clear sites",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(100):
|
||||
for tile_index in range(256):
|
||||
var tile_key := "tile:%d" % tile_index
|
||||
pipeline.call("enqueue_request", tile_key, "res://tile_%d.adt" % tile_index)
|
||||
while int(pipeline.call("pending_request_count")) > 0:
|
||||
var request: Dictionary = pipeline.call("take_next_request")
|
||||
var tile_key := String(request["key"])
|
||||
pipeline.call("remember_active_task", tile_key, tile_key.hash())
|
||||
pipeline.call("publish_result", tile_key, String(request["path"]), {})
|
||||
while pipeline.call("has_result"):
|
||||
var result: Dictionary = pipeline.call("pop_result")
|
||||
pipeline.call("complete_active_task", String(result["key"]))
|
||||
pipeline.call("clear")
|
||||
_expect_equal(
|
||||
int(pipeline.call("total_load_count")),
|
||||
0,
|
||||
"timing cycle %d clears" % iteration,
|
||||
failures
|
||||
)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(
|
||||
elapsed_milliseconds < 1000.0,
|
||||
"100 request/task/result cycles remain bounded",
|
||||
failures
|
||||
)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
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 _count_occurrences(text: String, token: String) -> int:
|
||||
return text.split(token).size() - 1
|
||||
|
||||
|
||||
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_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_string_array(
|
||||
actual_values: Array,
|
||||
expected_values: Array,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual_values != expected_values:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_values, actual_values])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
|
||||
|
||||
func _expect_false(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if actual_value:
|
||||
failures.append("%s expected false" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bb3v38yw47utf
|
||||
Reference in New Issue
Block a user