Files
open-wc/src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd
T

249 lines
7.4 KiB
GDScript

extends SceneTree
## Synthetic predicate, memoization, boundary and timing regression for stale
## cached M2 runtime mesh rebuild decisions.
const CLASSIFIER_SCRIPT := preload(
"res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
)
const CLASSIFIER_PATH := "res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_billboard_decisions(failures)
_verify_uv_rotation_decisions(failures)
_verify_stage_and_index_boundaries(failures)
_verify_memoization_clear_and_diagnostics(failures)
_verify_ownership_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_RUNTIME_MESH_REBUILD_CLASSIFIER: %s" % failure)
quit(1)
return
print(
"M2_RUNTIME_MESH_REBUILD_CLASSIFIER PASS cases=12 iterations=100 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_billboard_decisions(failures: Array[String]) -> void:
_expect_false(_classify("empty", {}), "empty data does not rebuild", failures)
_expect_false(
_classify("invalid", {"batches": ["invalid", 7, null]}),
"non-Dictionary batches skipped",
failures
)
_expect_true(
_classify("flag", {"batches": [{"has_billboard": true}]}),
"billboard flag rebuilds",
failures
)
_expect_true(
_classify("vertices", {"batches": [{"billboard_vertex_count": 1}]}),
"positive billboard vertex count rebuilds",
failures
)
_expect_false(
_classify("zero_vertices", {"batches": [{"billboard_vertex_count": 0}]}),
"zero billboard vertex count does not rebuild",
failures
)
func _verify_uv_rotation_decisions(failures: Array[String]) -> void:
_expect_false(
_classify("identity", _uv_data([_rotation_transform()])),
"identity UV rotation does not rebuild",
failures
)
_expect_true(
_classify(
"rotation",
_uv_data([_rotation_transform(Vector4(0.0, 1.0, -1.0, 0.0))])
),
"non-identity UV rotation rebuilds",
failures
)
_expect_true(
_classify("positive_speed", _uv_data([_rotation_transform(Vector4(1, 0, 0, 1), 0.000002)])),
"positive UV rotation speed rebuilds",
failures
)
_expect_true(
_classify("negative_speed", _uv_data([_rotation_transform(Vector4(1, 0, 0, 1), -0.000002)])),
"negative UV rotation speed rebuilds",
failures
)
_expect_false(
_classify("epsilon", _uv_data([_rotation_transform(Vector4(1, 0, 0, 1), 0.000001)])),
"exact UV rotation epsilon does not rebuild",
failures
)
func _verify_stage_and_index_boundaries(failures: Array[String]) -> void:
var fifth_stage_transforms: Array = [
_rotation_transform(),
_rotation_transform(),
_rotation_transform(),
_rotation_transform(),
_rotation_transform(Vector4(0.0, 1.0, -1.0, 0.0)),
]
_expect_false(
_classify(
"fifth_stage",
_uv_data(fifth_stage_transforms, PackedInt32Array([0, 1, 2, 3, 4]), 5)
),
"fifth texture stage remains ignored",
failures
)
_expect_false(
_classify(
"invalid_indices",
_uv_data([_rotation_transform(Vector4(0, 1, -1, 0))], PackedInt32Array([-1, 9]), 2)
),
"invalid transform indices skipped",
failures
)
_expect_false(
_classify(
"invalid_transform",
_uv_data(["not-a-transform"], PackedInt32Array([0]), 1)
),
"non-Dictionary transform uses identity defaults",
failures
)
func _verify_memoization_clear_and_diagnostics(failures: Array[String]) -> void:
var classifier: RefCounted = CLASSIFIER_SCRIPT.new()
var billboard_data := {"batches": [{"has_billboard": true}]}
_expect_true(
classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", billboard_data),
"first true decision",
failures
)
_expect_true(
classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", {}),
"first path decision memoized",
failures
)
classifier.call("needs_runtime_mesh_rebuild", "world/rock.m2", {})
_expect_equal(int(classifier.call("cached_path_count")), 2, "cached path count", failures)
var snapshot: Dictionary = classifier.call("diagnostic_snapshot")
snapshot["world/tree.m2"] = false
_expect_true(
classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", {}),
"diagnostics detached",
failures
)
classifier.call("clear")
_expect_equal(int(classifier.call("cached_path_count")), 0, "clear count", failures)
_expect_false(
classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", {}),
"clear allows recomputation",
failures
)
func _verify_ownership_boundaries(failures: Array[String]) -> void:
var classifier_source := FileAccess.get_file_as_string(CLASSIFIER_PATH)
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
_expect_true(
loader_source.contains("M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()"),
"loader composes classifier",
failures
)
_expect_equal(
loader_source.count("_m2_runtime_mesh_rebuild_classifier.clear()"),
2,
"loader retains two classifier clear sites",
failures
)
_expect_false(
loader_source.contains("_m2_runtime_rebuild_required_cache"),
"loader removes legacy rebuild cache",
failures
)
for removed_function in [
"func _m2_raw_data_needs_runtime_mesh_rebuild",
"func _m2_raw_data_has_billboards",
"func _m2_raw_data_has_uv_rotation",
]:
_expect_false(loader_source.contains(removed_function), "loader removes %s" % removed_function, failures)
for forbidden_dependency in [
"ResourceLoader.",
"WorkerThreadPool.",
"M2_BUILDER_SCRIPT",
"ClassDB.instantiate",
"FileAccess.",
]:
_expect_false(
classifier_source.contains(forbidden_dependency),
"classifier omits %s dependency" % forbidden_dependency,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var classifier: RefCounted = CLASSIFIER_SCRIPT.new()
var ordinary_data := {"batches": [{"has_billboard": false}]}
var started_microseconds := Time.get_ticks_usec()
for iteration in range(100):
for path_index in range(256):
classifier.call(
"needs_runtime_mesh_rebuild",
"world/model_%d.m2" % path_index,
ordinary_data
)
classifier.call("clear")
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 classifications under 1 second", failures)
return elapsed_milliseconds
func _classify(normalized_path: String, raw_data: Dictionary) -> bool:
var classifier: RefCounted = CLASSIFIER_SCRIPT.new()
return bool(classifier.call("needs_runtime_mesh_rebuild", normalized_path, raw_data))
func _uv_data(
texture_transforms: Array,
texture_transform_combos: PackedInt32Array = PackedInt32Array([0]),
texture_count: int = 1) -> Dictionary:
return {
"texture_transforms": texture_transforms,
"texture_transform_combos": texture_transform_combos,
"batches": [{
"texture_count": texture_count,
"texture_transform_combo_index": 0,
}],
}
func _rotation_transform(
rotation: Vector4 = Vector4(1.0, 0.0, 0.0, 1.0),
rotation_speed: float = 0.0) -> Dictionary:
return {
"rotation": rotation,
"rotation_speed": rotation_speed,
}
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)
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])