qar: add M03 renderer closeout gates

This commit is contained in:
2026-08-01 16:49:43 +04:00
parent ef6d324b4f
commit d9d20cb53f
4 changed files with 407 additions and 2 deletions
+2 -2
View File
@@ -176,14 +176,14 @@ func _bake_glb_animation_cache(
if not force and FileAccess.file_exists(abs_out_glb):
return true
var abs_converter := ProjectSettings.globalize_path(converter)
var abs_output := ProjectSettings.globalize_path(output_dir)
var converter_output_directory := abs_out_glb.get_base_dir()
if not FileAccess.file_exists(abs_converter):
push_warning("M2 GLB converter not found: %s" % converter)
return false
var stdout := []
var exit_code := OS.execute(
python_exe,
[abs_converter, abs_m2, abs_output],
[abs_converter, abs_m2, converter_output_directory],
stdout,
true,
false)
@@ -0,0 +1,173 @@
extends SceneTree
## M03 closeout gate for cache versions and worker/main-thread render boundaries.
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
const M2_CACHE_BAKER_PATH := "res://src/tools/bake_m2_cache.gd"
const WORKER_FUNCTIONS: Array[String] = [
"_load_tile_task",
"_load_terrain_splat_task",
"_load_tile_water_task",
"_group_tile_m2_task",
]
const WORKER_FORBIDDEN_TOKENS: Array[String] = [
"RenderingServer",
".add_child(",
".queue_free(",
".free(",
"Node3D.new(",
"MeshInstance3D.new(",
"MultiMeshInstance3D.new(",
"ArrayMesh.new(",
"build_tile_water_scene(",
"build_tile_coarse_render_payload(",
]
const MAIN_THREAD_FRAME_STEPS: Array[String] = [
"_drain_tile_load_results()",
"_drain_terrain_upgrade_results()",
"_drain_terrain_control_splat_cache_results()",
"_drain_terrain_splat_cache_results()",
"_drain_terrain_splat_results()",
"_process_water_load_queue()",
"_drain_water_load_results()",
"_process_queues()",
"_drain_m2_group_results()",
"_drain_m2_animation_loads()",
"_drain_m2_mesh_loads()",
"_process_m2_build_jobs()",
"_process_wmo_build_jobs()",
"_process_wmo_render_build_jobs()",
"_process_detail_asset_queue()",
]
func _initialize() -> void:
var failures: Array[String] = []
var loader_source := _read_text(LOADER_PATH, failures)
var process_source := _function_source(loader_source, "_process", failures)
_verify_frame_entrypoint(process_source, failures)
_verify_worker_boundaries(loader_source, failures)
_verify_rendering_server_boundary(loader_source, failures)
_verify_cache_contract(loader_source, failures)
_verify_m2_glb_cache_output_contract(failures)
if not failures.is_empty():
for failure in failures:
push_error("RENDERER_CLOSEOUT_CONTRACTS: %s" % failure)
quit(1)
return
print("RENDERER_CLOSEOUT_CONTRACTS PASS workers=%d frame_steps=%d cache_versions=7" % [
WORKER_FUNCTIONS.size(),
MAIN_THREAD_FRAME_STEPS.size(),
])
quit(0)
func _verify_frame_entrypoint(process_source: String, failures: Array[String]) -> void:
var scheduler_index := process_source.find("_render_budget_scheduler.begin_frame(")
if scheduler_index < 0:
failures.append("_process does not begin a render-budget frame")
return
for step in MAIN_THREAD_FRAME_STEPS:
var step_index := process_source.find(step)
if step_index < 0:
failures.append("_process is missing main-thread step %s" % step)
elif step_index < scheduler_index:
failures.append("%s runs before render-budget frame admission" % step)
func _verify_worker_boundaries(loader_source: String, failures: Array[String]) -> void:
for function_name in WORKER_FUNCTIONS:
var function_source := _function_source(loader_source, function_name, failures)
for forbidden_token in WORKER_FORBIDDEN_TOKENS:
if function_source.contains(forbidden_token):
failures.append("worker %s contains main-thread token %s" % [
function_name,
forbidden_token,
])
func _verify_rendering_server_boundary(loader_source: String, failures: Array[String]) -> void:
var remaining_source := loader_source
for function_name in ["_create_render_instance", "_free_render_instance"]:
var function_source := _function_source(loader_source, function_name, failures)
remaining_source = remaining_source.replace(function_source, "")
if remaining_source.contains("RenderingServer.instance_create("):
failures.append("RenderingServer instance creation escaped the owned adapter")
if remaining_source.contains("RenderingServer.free_rid("):
failures.append("RenderingServer RID release escaped the owned adapter")
func _verify_cache_contract(loader_source: String, failures: Array[String]) -> void:
var manifest_source := _read_text(MANIFEST_PATH, failures)
var parsed_manifest = JSON.parse_string(manifest_source)
if not (parsed_manifest is Dictionary):
failures.append("baseline manifest is not a Dictionary")
return
var cache_contract: Dictionary = parsed_manifest.get("cache_contract", {})
var expected_versions := {
"baked_terrain": 5,
"streaming_terrain": 2,
"terrain_splat": 1,
"terrain_control_splat": 3,
"wmo_streaming": 2,
"wmo_builder": 2,
"m2_material": 2,
}
if cache_contract.size() != expected_versions.size():
failures.append("cache contract key count changed: expected=%d actual=%d" % [
expected_versions.size(),
cache_contract.size(),
])
for cache_name in expected_versions:
if not cache_contract.has(cache_name):
failures.append("cache contract is missing %s" % cache_name)
continue
var cache_record: Dictionary = cache_contract[cache_name]
if int(cache_record.get("version", -1)) != int(expected_versions[cache_name]):
failures.append("cache version changed for %s" % cache_name)
var required_loader_declarations := [
"const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5",
"const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1",
"const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3",
"STREAMING_TILE_SCRIPT.FORMAT_VERSION",
"WMO_STREAMING_SCRIPT.FORMAT_VERSION",
]
for declaration in required_loader_declarations:
if not loader_source.contains(declaration):
failures.append("loader cache-version boundary is missing %s" % declaration)
func _verify_m2_glb_cache_output_contract(failures: Array[String]) -> void:
var baker_source := _read_text(M2_CACHE_BAKER_PATH, failures)
if not baker_source.contains(
"var converter_output_directory := abs_out_glb.get_base_dir()"):
failures.append("M2 GLB converter output is not derived from the nested cache path")
if not baker_source.contains(
"[abs_converter, abs_m2, converter_output_directory]"):
failures.append("M2 GLB converter does not receive the nested output directory")
func _function_source(source: String, function_name: String, failures: Array[String]) -> String:
var signature := "func %s(" % function_name
var start_index := source.find(signature)
if start_index < 0:
failures.append("missing function %s" % function_name)
return ""
var next_function_index := source.find("\nfunc ", start_index + signature.length())
if next_function_index < 0:
return source.substr(start_index)
return source.substr(start_index, next_function_index - start_index)
func _read_text(path: String, failures: Array[String]) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot read %s" % path)
return ""
var content := file.get_as_text()
file.close()
return content
@@ -0,0 +1 @@
uid://nqj0mu6r8omf