refactor(M03): extract M2 build batch planner
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free contract, dependency and timing regression for M2 batch planning.
|
||||
|
||||
const PLANNER_SCRIPT := preload("res://src/render/m2/m2_build_batch_planner.gd")
|
||||
const PLANNER_PATH := "res://src/render/m2/m2_build_batch_planner.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_static_batch_limit(failures)
|
||||
_verify_animated_batch_limit(failures)
|
||||
_verify_non_positive_limit_clamp(failures)
|
||||
_verify_remaining_count_cap(failures)
|
||||
_verify_exact_group_completion(failures)
|
||||
_verify_empty_and_past_end(failures)
|
||||
_verify_historical_negative_offset(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_BUILD_BATCH_PLANNER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_BUILD_BATCH_PLANNER PASS cases=8 iterations=20000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_static_batch_limit(failures: Array[String]) -> void:
|
||||
var plan := _plan(300, 0, false, 8, 128)
|
||||
_expect_plan(plan, 128, 128, 128, false, "static batch", failures)
|
||||
|
||||
|
||||
func _verify_animated_batch_limit(failures: Array[String]) -> void:
|
||||
var plan := _plan(300, 0, true, 8, 128)
|
||||
_expect_plan(plan, 8, 8, 8, false, "animated batch", failures)
|
||||
|
||||
|
||||
func _verify_non_positive_limit_clamp(failures: Array[String]) -> void:
|
||||
var animated_plan := _plan(4, 0, true, 0, 128)
|
||||
_expect_plan(animated_plan, 1, 1, 1, false, "zero animated limit", failures)
|
||||
var static_plan := _plan(4, 0, false, 8, -10)
|
||||
_expect_plan(static_plan, 1, 1, 1, false, "negative static limit", failures)
|
||||
|
||||
|
||||
func _verify_remaining_count_cap(failures: Array[String]) -> void:
|
||||
var plan := _plan(140, 128, false, 8, 128)
|
||||
_expect_plan(plan, 128, 12, 0, true, "remaining cap", failures)
|
||||
|
||||
|
||||
func _verify_exact_group_completion(failures: Array[String]) -> void:
|
||||
var plan := _plan(16, 8, true, 8, 128)
|
||||
_expect_plan(plan, 8, 8, 0, true, "exact completion", failures)
|
||||
|
||||
|
||||
func _verify_empty_and_past_end(failures: Array[String]) -> void:
|
||||
var empty_plan := _plan(0, 0, false, 8, 128)
|
||||
_expect_plan(empty_plan, 128, 0, 0, true, "empty group", failures)
|
||||
var past_end_plan := _plan(10, 11, false, 8, 128)
|
||||
_expect_plan(past_end_plan, 128, 0, 0, true, "past end", failures)
|
||||
|
||||
|
||||
func _verify_historical_negative_offset(failures: Array[String]) -> void:
|
||||
var plan := _plan(10, -2, false, 8, 4)
|
||||
_expect_plan(plan, 4, 4, 2, false, "negative offset", failures)
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
var planner_source := _read_text(PLANNER_PATH, failures)
|
||||
var build_loop_source := _source_between(
|
||||
loader_source,
|
||||
"func _process_m2_build_jobs()",
|
||||
"func _materialize_m2_group_batch(",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
build_loop_source.count("_m2_build_batch_planner.plan_batch("),
|
||||
1,
|
||||
"one loader planner adapter",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
not build_loop_source.contains("var batch_size:"),
|
||||
"loader omits batch-size formula",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
not build_loop_source.contains("mini(batch_size"),
|
||||
"loader omits batch-count formula",
|
||||
failures
|
||||
)
|
||||
for forbidden_dependency in [
|
||||
"WorkerThreadPool",
|
||||
"RenderingServer",
|
||||
"ResourceLoader",
|
||||
"MultiMesh",
|
||||
"Node",
|
||||
"Mutex",
|
||||
]:
|
||||
_expect_true(
|
||||
not planner_source.contains(forbidden_dependency),
|
||||
"planner omits %s" % forbidden_dependency,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var planner := PLANNER_SCRIPT.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(20000):
|
||||
var plan: Dictionary = planner.plan_batch(
|
||||
512,
|
||||
iteration % 512,
|
||||
iteration % 3 == 0,
|
||||
8,
|
||||
128
|
||||
)
|
||||
if int(plan.get("batch_count", -1)) < 0:
|
||||
failures.append("timing iteration returned negative batch count")
|
||||
break
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "20000 batch plans bounded", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _plan(
|
||||
transform_count: int,
|
||||
current_offset: int,
|
||||
has_animated_prototype: bool,
|
||||
animated_batch_limit: int,
|
||||
static_batch_limit: int
|
||||
) -> Dictionary:
|
||||
var planner := PLANNER_SCRIPT.new()
|
||||
return planner.plan_batch(
|
||||
transform_count,
|
||||
current_offset,
|
||||
has_animated_prototype,
|
||||
animated_batch_limit,
|
||||
static_batch_limit
|
||||
)
|
||||
|
||||
|
||||
func _expect_plan(
|
||||
plan: Dictionary,
|
||||
expected_batch_size: int,
|
||||
expected_batch_count: int,
|
||||
expected_next_offset: int,
|
||||
expected_group_complete: bool,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
_expect_equal(
|
||||
int(plan.get("effective_batch_size", -1)),
|
||||
expected_batch_size,
|
||||
"%s effective batch size" % label,
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
int(plan.get("batch_count", -1)),
|
||||
expected_batch_count,
|
||||
"%s batch count" % label,
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
int(plan.get("next_offset", -1)),
|
||||
expected_next_offset,
|
||||
"%s next offset" % label,
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(plan.get("group_complete", not expected_group_complete))
|
||||
== expected_group_complete,
|
||||
"%s group complete" % label,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
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 _source_between(
|
||||
source: String,
|
||||
start_marker: String,
|
||||
end_marker: String,
|
||||
failures: Array[String]
|
||||
) -> String:
|
||||
var start_offset := source.find(start_marker)
|
||||
var end_offset := source.find(end_marker, start_offset + start_marker.length())
|
||||
if start_offset < 0 or end_offset <= start_offset:
|
||||
failures.append("cannot isolate source between %s and %s" % [start_marker, end_marker])
|
||||
return ""
|
||||
return source.substr(start_offset, end_offset - start_offset)
|
||||
|
||||
|
||||
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_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
Reference in New Issue
Block a user