280 lines
10 KiB
GDScript
280 lines
10 KiB
GDScript
extends SceneTree
|
|
|
|
## Asset-free M2 build-job lifecycle, FIFO, progress, engine-lifetime, source
|
|
## boundary and bounded-timing regression.
|
|
|
|
const QUEUE_SCRIPT := preload("res://src/render/m2/m2_build_queue.gd")
|
|
const QUEUE_PATH := "res://src/render/m2/m2_build_queue.gd"
|
|
const JOB_PATH := "res://src/render/m2/m2_build_job.gd"
|
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
|
|
|
|
|
func _initialize() -> void:
|
|
var failures: Array[String] = []
|
|
_verify_invalid_and_fifo_jobs(failures)
|
|
_verify_progress_adoption(failures)
|
|
_verify_rotation_duplicate_and_stale_keys(failures)
|
|
_verify_erase_clear_and_engine_lifetime(failures)
|
|
_verify_detached_sorted_diagnostics(failures)
|
|
_verify_loader_and_engine_lifetime_boundaries(failures)
|
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
|
if not failures.is_empty():
|
|
for failure in failures:
|
|
push_error("M2_BUILD_QUEUE: %s" % failure)
|
|
quit(1)
|
|
return
|
|
print(
|
|
"M2_BUILD_QUEUE PASS cases=13 iterations=100 elapsed_ms=%.3f"
|
|
% elapsed_milliseconds
|
|
)
|
|
quit(0)
|
|
|
|
|
|
func _verify_invalid_and_fifo_jobs(failures: Array[String]) -> void:
|
|
var queue: RefCounted = QUEUE_SCRIPT.new()
|
|
var root := Node3D.new()
|
|
var groups := _make_groups("world/a.m2", 2)
|
|
_expect_false(queue.call("enqueue", "", root, groups), "empty key rejected", failures)
|
|
_expect_false(queue.call("enqueue", "1_1", null, groups), "null root rejected", failures)
|
|
_expect_false(queue.call("enqueue", "1_1", root, {}), "empty groups rejected", failures)
|
|
_expect_true(queue.call("enqueue", "2_2", root, groups), "first enqueue", failures)
|
|
_expect_true(
|
|
queue.call("enqueue", "3_3", root, _make_groups("world/b.m2", 1)),
|
|
"second enqueue",
|
|
failures
|
|
)
|
|
_expect_string_equal(String(queue.call("front_key")), "2_2", "FIFO front", failures)
|
|
_expect_equal(int(queue.call("pending_count")), 2, "FIFO pending count", failures)
|
|
_expect_equal(int(queue.call("active_job_count")), 2, "active job count", failures)
|
|
var first_job: RefCounted = queue.call("job_for", "2_2")
|
|
_expect_string_equal(String(first_job.call("tile_key")), "2_2", "typed job key", failures)
|
|
_expect_same(first_job.call("root"), root, "job retains root", failures)
|
|
_expect_same(first_job.call("groups"), groups, "job retains exact groups", failures)
|
|
_expect_string_array(first_job.call("group_keys"), ["world/a.m2"], "group-key snapshot", failures)
|
|
queue.call("clear")
|
|
root.free()
|
|
|
|
|
|
func _verify_progress_adoption(failures: Array[String]) -> void:
|
|
var queue: RefCounted = QUEUE_SCRIPT.new()
|
|
var root := Node3D.new()
|
|
queue.call("enqueue", "4_4", root, _make_groups("world/a.m2", 2))
|
|
_expect_equal(int(queue.call("group_index_for", "4_4")), 0, "initial group index", failures)
|
|
_expect_equal(int(queue.call("transform_offset_for", "4_4")), 0, "initial offset", failures)
|
|
_expect_equal(int(queue.call("batch_serial_for", "4_4")), 0, "initial serial", failures)
|
|
_expect_true(queue.call("adopt_progress", "4_4", 7, 3, 11), "progress adopted", failures)
|
|
_expect_equal(int(queue.call("group_index_for", "4_4")), 7, "adopted group index", failures)
|
|
_expect_equal(int(queue.call("transform_offset_for", "4_4")), 3, "adopted offset", failures)
|
|
_expect_equal(int(queue.call("batch_serial_for", "4_4")), 11, "adopted serial", failures)
|
|
_expect_false(
|
|
queue.call("adopt_progress", "missing", 1, 1, 1),
|
|
"stale progress rejected",
|
|
failures
|
|
)
|
|
queue.call("clear")
|
|
root.free()
|
|
|
|
|
|
func _verify_rotation_duplicate_and_stale_keys(failures: Array[String]) -> void:
|
|
var queue: RefCounted = QUEUE_SCRIPT.new()
|
|
var first_root := Node3D.new()
|
|
var second_root := Node3D.new()
|
|
var groups := _make_groups("world/a.m2", 1)
|
|
queue.call("enqueue", "5_5", first_root, groups)
|
|
queue.call("enqueue", "6_6", first_root, groups)
|
|
_expect_true(queue.call("rotate_front"), "front rotates", failures)
|
|
_expect_string_equal(String(queue.call("front_key")), "6_6", "rotated FIFO front", failures)
|
|
queue.call("enqueue", "6_6", second_root, groups)
|
|
_expect_equal(int(queue.call("pending_count")), 3, "duplicate appends FIFO key", failures)
|
|
_expect_equal(int(queue.call("active_job_count")), 2, "duplicate replaces keyed job", failures)
|
|
_expect_same(queue.call("root_for", "6_6"), second_root, "duplicate latest root wins", failures)
|
|
_expect_true(queue.call("erase_job", "6_6"), "job erased", failures)
|
|
_expect_equal(int(queue.call("pending_count")), 3, "erase preserves FIFO keys", failures)
|
|
_expect_false(queue.call("has_job", "6_6"), "front key becomes stale", failures)
|
|
_expect_string_equal(String(queue.call("pop_front")), "6_6", "stale front popped", failures)
|
|
queue.call("clear")
|
|
first_root.free()
|
|
second_root.free()
|
|
|
|
|
|
func _verify_erase_clear_and_engine_lifetime(failures: Array[String]) -> void:
|
|
var queue: RefCounted = QUEUE_SCRIPT.new()
|
|
var root := Node3D.new()
|
|
queue.call("enqueue", "7_7", root, _make_groups("world/a.m2", 1))
|
|
_expect_false(queue.call("erase_job", "missing"), "unknown erase rejected", failures)
|
|
_expect_true(queue.call("erase_job", "7_7"), "known job erased", failures)
|
|
_expect_true(is_instance_valid(root), "erase keeps root valid", failures)
|
|
_expect_equal(int(queue.call("pending_count")), 1, "erase leaves stale key", failures)
|
|
queue.call("clear")
|
|
queue.call("clear")
|
|
_expect_equal(int(queue.call("pending_count")), 0, "clear idempotent", failures)
|
|
_expect_true(is_instance_valid(root), "clear keeps root valid", failures)
|
|
root.free()
|
|
|
|
|
|
func _verify_detached_sorted_diagnostics(failures: Array[String]) -> void:
|
|
var queue: RefCounted = QUEUE_SCRIPT.new()
|
|
var root := Node3D.new()
|
|
queue.call("enqueue", "20_20", root, _make_groups("world/a.m2", 1))
|
|
queue.call("enqueue", "10_10", root, _make_groups("world/b.m2", 2))
|
|
queue.call("adopt_progress", "10_10", 1, 2, 3)
|
|
var snapshot: Dictionary = queue.call("diagnostic_snapshot")
|
|
_expect_string_array(
|
|
snapshot["queued_tile_keys"],
|
|
["20_20", "10_10"],
|
|
"diagnostic FIFO order",
|
|
failures
|
|
)
|
|
var jobs: Array = snapshot["jobs"]
|
|
_expect_string_equal(String(jobs[0]["tile_key"]), "10_10", "diagnostic job sort", failures)
|
|
_expect_equal(int(jobs[0]["batch_serial"]), 3, "diagnostic progress", failures)
|
|
(snapshot["queued_tile_keys"] as Array).append("mutated")
|
|
jobs[0]["batch_serial"] = 99
|
|
var fresh_snapshot: Dictionary = queue.call("diagnostic_snapshot")
|
|
_expect_string_array(
|
|
fresh_snapshot["queued_tile_keys"],
|
|
["20_20", "10_10"],
|
|
"diagnostic FIFO detached",
|
|
failures
|
|
)
|
|
_expect_equal(
|
|
int((fresh_snapshot["jobs"] as Array)[0]["batch_serial"]),
|
|
3,
|
|
"job diagnostic detached",
|
|
failures
|
|
)
|
|
queue.call("clear")
|
|
root.free()
|
|
|
|
|
|
func _verify_loader_and_engine_lifetime_boundaries(failures: Array[String]) -> void:
|
|
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
|
var queue_source := FileAccess.get_file_as_string(QUEUE_PATH)
|
|
var job_source := FileAccess.get_file_as_string(JOB_PATH)
|
|
_expect_true(
|
|
loader_source.contains("M2_BUILD_QUEUE_SCRIPT.new()"),
|
|
"loader composes queue",
|
|
failures
|
|
)
|
|
for delegated_call in [
|
|
"_m2_build_queue_state.enqueue(",
|
|
"_m2_build_queue_state.has_job(",
|
|
"_m2_build_queue_state.root_for(",
|
|
"_m2_build_queue_state.rotate_front()",
|
|
"_m2_build_queue_state.adopt_progress(",
|
|
"_m2_build_queue_state.erase_job(",
|
|
"_m2_build_queue_state.clear()",
|
|
]:
|
|
_expect_true(
|
|
loader_source.contains(delegated_call),
|
|
"loader delegates %s" % delegated_call,
|
|
failures
|
|
)
|
|
_expect_false(
|
|
loader_source.contains("var _m2_build_jobs"),
|
|
"legacy job Dictionary removed",
|
|
failures
|
|
)
|
|
_expect_false(
|
|
loader_source.contains("var _m2_build_queue: Array"),
|
|
"legacy FIFO Array removed",
|
|
failures
|
|
)
|
|
_expect_true(
|
|
loader_source.contains("_queue_free_streamed_node(root)")
|
|
and loader_source.contains("_m2_static_batch_materializer.materialize_batch(")
|
|
and loader_source.contains("_m2_animated_instance_materializer.materialize_batch(")
|
|
and loader_source.contains("try_consume_permit("),
|
|
"loader retains destruction, materialization and permits",
|
|
failures
|
|
)
|
|
for forbidden_token in [
|
|
"queue_free",
|
|
"RenderingServer",
|
|
"RID",
|
|
"MeshInstance3D",
|
|
"MultiMesh",
|
|
"ResourceLoader",
|
|
"FileAccess",
|
|
"WorkerThreadPool",
|
|
"Thread",
|
|
"Mutex",
|
|
]:
|
|
_expect_false(
|
|
queue_source.contains(forbidden_token) or job_source.contains(forbidden_token),
|
|
"queue/job omit %s ownership" % forbidden_token,
|
|
failures
|
|
)
|
|
|
|
|
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
|
var queue: RefCounted = QUEUE_SCRIPT.new()
|
|
var root := Node3D.new()
|
|
var groups := _make_groups("world/timing.m2", 4)
|
|
var started_microseconds := Time.get_ticks_usec()
|
|
for _iteration in range(100):
|
|
for job_index in range(256):
|
|
queue.call("enqueue", "%d_%d" % [job_index, job_index], root, groups)
|
|
for _job_index in range(256):
|
|
queue.call("rotate_front")
|
|
for job_index in range(256):
|
|
queue.call("erase_job", "%d_%d" % [job_index, job_index])
|
|
queue.call("clear")
|
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
|
_expect_true(elapsed_milliseconds < 1000.0, "100 queue cycles remain bounded", failures)
|
|
root.free()
|
|
return elapsed_milliseconds
|
|
|
|
|
|
func _make_groups(relative_path: String, transform_count: int) -> Dictionary:
|
|
var transforms: Array = []
|
|
for transform_index in transform_count:
|
|
transforms.append(
|
|
Transform3D(Basis.IDENTITY, Vector3(float(transform_index), 0.0, 0.0))
|
|
)
|
|
return {relative_path: transforms}
|
|
|
|
|
|
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
|
if actual != expected:
|
|
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
|
|
|
|
|
func _expect_string_equal(
|
|
actual: String,
|
|
expected: String,
|
|
label: String,
|
|
failures: Array[String]
|
|
) -> void:
|
|
if actual != expected:
|
|
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
|
|
|
|
|
func _expect_string_array(
|
|
actual: Array,
|
|
expected: Array,
|
|
label: String,
|
|
failures: Array[String]
|
|
) -> void:
|
|
if actual != expected:
|
|
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
|
|
|
|
|
func _expect_same(
|
|
actual: Variant,
|
|
expected: Variant,
|
|
label: String,
|
|
failures: Array[String]
|
|
) -> void:
|
|
if not is_same(actual, expected):
|
|
failures.append(label)
|
|
|
|
|
|
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
|
if not condition:
|
|
failures.append(label)
|
|
|
|
|
|
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
|
_expect_true(not condition, label, failures)
|