refactor(M03): extract ADT water load pipeline state
This commit is contained in:
@@ -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