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 nested_glb=1" % [ 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