test(M00): calibrate paired renderer checkpoints

Work-Package: M00-QAR-VISUAL-DIFF-001
Agent: sindo-main-codex
Tests: synthetic JPG/PNG diff, manifest, coordination and documentation gates
Fidelity: five build 12340 references compared; coordinate and placement gaps recorded
This commit is contained in:
2026-07-11 18:23:07 +04:00
parent a4f60dcb06
commit d467ffee7f
9 changed files with 95 additions and 34 deletions
+13 -3
View File
@@ -25,7 +25,9 @@ func _capture_async() -> void:
var headless := DisplayServer.get_name().to_lower() == "headless"
var dry_run := args.has("--dry-run") or headless
var viewport: Array = manifest.get("viewport", [1280, 900])
get_root().size = Vector2i(maxi(16, int(viewport[0])), maxi(16, int(viewport[1])))
var viewport_width := int(_arg(args, "--viewport-width", str(viewport[0])))
var viewport_height := int(_arg(args, "--viewport-height", str(viewport[1])))
get_root().size = Vector2i(maxi(16, viewport_width), maxi(16, viewport_height))
var abs_output_dir := ProjectSettings.globalize_path(output_dir)
DirAccess.make_dir_recursive_absolute(abs_output_dir)
@@ -107,7 +109,7 @@ func _capture_async() -> void:
probe.scale = Vector3.ONE * float(checkpoint.get("probe_scale", 1.0))
camera.global_position = _vector3(checkpoint.get("camera", [0.0, 0.0, 0.0]))
camera.look_at(_vector3(checkpoint.get("target", [0.0, 0.0, 0.0])), Vector3.UP)
_orient_camera_without_roll(camera, _vector3(checkpoint.get("target", [0.0, 0.0, 0.0])))
if player != null:
player.global_position = _vector3(checkpoint.get("player", checkpoint.get("target", [0.0, 0.0, 0.0])))
_set_sky_time(world, float(checkpoint.get("time_hours", 13.0)))
@@ -135,7 +137,6 @@ func _capture_async() -> void:
await RenderingServer.frame_post_draw
var image := get_root().get_texture().get_image()
image.flip_y()
var file_name := "%s__%s.png" % [checkpoint_name, pass_name]
var abs_path := abs_output_dir.path_join(file_name)
var err := image.save_png(abs_path)
@@ -196,6 +197,15 @@ func _measure_frames(seconds: float, world: Node) -> Dictionary:
}
func _orient_camera_without_roll(camera: Camera3D, target_position: Vector3) -> void:
var forward := (target_position - camera.global_position).normalized()
var right := forward.cross(Vector3.UP).normalized()
if right.is_zero_approx():
right = Vector3.RIGHT
var corrected_up := right.cross(forward).normalized()
camera.global_basis = Basis(right, corrected_up, -forward)
func _result_record(checkpoint: Dictionary, pass_name: String, load_time_ms: float, metrics: Dictionary, sha256: String) -> Dictionary:
return {
"name": checkpoint.get("name", "checkpoint"),
+40 -20
View File
@@ -1,6 +1,6 @@
extends SceneTree
## Compares paired PNG checkpoint directories without storing proprietary reference images.
## Compares original-client JPG/PNG references with OpenWC checkpoint PNGs.
## Usage: godot --headless --path . --script res://src/tools/compare_render_checkpoints.gd --
## --reference <directory> --candidate <directory> [--output <report.json>]
## [--pixel-threshold 0.04] [--mean-threshold 0.015] [--changed-ratio-threshold 0.01]
@@ -73,22 +73,29 @@ func _parse_arguments(raw_arguments: PackedStringArray) -> Dictionary:
func _compare_directories(reference_directory: String, candidate_directory: String, options: Dictionary) -> Dictionary:
var reference_files := _png_file_names(reference_directory)
reference_directory = ProjectSettings.globalize_path(reference_directory)
candidate_directory = ProjectSettings.globalize_path(candidate_directory)
var reference_files := _reference_image_file_names(reference_directory)
var results: Array[Dictionary] = []
var failed_count := 0
var missing_count := 0
for file_name in reference_files:
var reference_path := reference_directory.path_join(file_name)
var candidate_path := candidate_directory.path_join(file_name)
if not FileAccess.file_exists(candidate_path):
results.append({"name": file_name, "status": "missing_candidate"})
var checkpoint_name := file_name.get_basename()
var candidate_file_names := _candidate_file_names(candidate_directory, checkpoint_name)
if candidate_file_names.is_empty():
results.append({"checkpoint": checkpoint_name, "reference": file_name, "status": "missing_candidate"})
missing_count += 1
continue
var comparison := _compare_images(reference_path, candidate_path, options)
comparison.name = file_name
results.append(comparison)
if comparison.status != "passed":
failed_count += 1
for candidate_file_name in candidate_file_names:
var candidate_path := candidate_directory.path_join(candidate_file_name)
var comparison := _compare_images(reference_path, candidate_path, options)
comparison.checkpoint = checkpoint_name
comparison.reference = file_name
comparison.candidate = candidate_file_name
results.append(comparison)
if comparison.status != "passed":
failed_count += 1
var no_reference_images := reference_files.is_empty()
return {
"schema_version": REPORT_SCHEMA_VERSION,
@@ -149,18 +156,32 @@ func _perceptual_color_error(reference_color: Color, candidate_color: Color) ->
return 0.2126 * red_delta + 0.7152 * green_delta + 0.0722 * blue_delta + 0.25 * alpha_delta
func _png_file_names(directory_path: String) -> Array[String]:
func _reference_image_file_names(directory_path: String) -> Array[String]:
var directory := DirAccess.open(directory_path)
if directory == null:
return []
var file_names: Array[String] = []
for file_name in directory.get_files():
if file_name.to_lower().ends_with(".png"):
var lower_name := file_name.to_lower()
if not lower_name.begins_with("diagnostic_") and (lower_name.ends_with(".png") or lower_name.ends_with(".jpg") or lower_name.ends_with(".jpeg")):
file_names.append(file_name)
file_names.sort()
return file_names
func _candidate_file_names(candidate_directory: String, checkpoint_name: String) -> Array[String]:
var candidates: Array[String] = []
for pass_name in ["cold_process", "warm_revisit"]:
var file_name := "%s__%s.png" % [checkpoint_name, pass_name]
if FileAccess.file_exists(candidate_directory.path_join(file_name)):
candidates.append(file_name)
if candidates.is_empty():
var exact_file_name := "%s.png" % checkpoint_name
if FileAccess.file_exists(candidate_directory.path_join(exact_file_name)):
candidates.append(exact_file_name)
return candidates
func _write_report(output_path: String, report: Dictionary) -> bool:
var absolute_path := ProjectSettings.globalize_path(output_path)
var error := DirAccess.make_dir_recursive_absolute(absolute_path.get_base_dir())
@@ -186,14 +207,13 @@ func _run_self_test() -> int:
reference_image.fill(Color(0.25, 0.5, 0.75, 1.0))
var changed_image := reference_image.duplicate()
changed_image.set_pixel(0, 0, Color.WHITE)
for pair in [
[reference_directory, reference_image],
[identical_directory, reference_image],
[changed_directory, changed_image],
]:
var save_error: Error = pair[1].save_png(String(pair[0]).path_join("synthetic.png"))
if save_error != OK:
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: cannot save fixture")
if reference_image.save_jpg(reference_directory.path_join("synthetic.jpg"), 1.0) != OK:
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: cannot save JPG reference")
return 1
for pass_name in ["cold_process", "warm_revisit"]:
if reference_image.save_png(identical_directory.path_join("synthetic__%s.png" % pass_name)) != OK:
return 1
if changed_image.save_png(changed_directory.path_join("synthetic__%s.png" % pass_name)) != OK:
return 1
var options := {
"pixel_threshold": DEFAULT_PIXEL_THRESHOLD,
+4 -3
View File
@@ -5,6 +5,7 @@
"map": "Azeroth",
"quality_preset": "High",
"viewport": [1280, 900],
"fidelity_comparison_viewport": [2560, 1440],
"camera_fov": 62.0,
"default_wait_seconds": 8.0,
"default_measure_seconds": 3.0,
@@ -57,7 +58,7 @@
{
"name": "goldshire_dense_m2",
"coverage": ["dense_m2"],
"camera": [16835.0, 135.0, 26435.0],
"camera": [16956.666, 150.0, 26466.666],
"target": [17015.0, 62.0, 26525.0],
"player": [17015.0, 58.0, 26525.0],
"time_hours": 13.0
@@ -65,7 +66,7 @@
{
"name": "goldshire_inn_large_wmo",
"coverage": ["large_wmo"],
"camera": [16942.0, 82.0, 26503.0],
"camera": [17013.666, 72.0, 26451.666],
"target": [17042.27, 66.0, 26530.91],
"player": [17042.27, 58.0, 26530.91],
"time_hours": 13.0
@@ -73,7 +74,7 @@
{
"name": "elwynn_waterfall_liquid",
"coverage": ["liquid"],
"camera": [16445.0, 125.0, 26295.0],
"camera": [16481.666, 190.0, 26366.666],
"target": [16518.84, 68.0, 26427.27],
"player": [16518.84, 55.0, 26427.27],
"time_hours": 13.0
@@ -20,6 +20,8 @@ func _initialize() -> void:
_expect(int(manifest.get("schema_version", 0)) == 1, "manifest schema must be version 1", failures)
_expect(String(manifest.get("profile", "")) == "Blizzlike335", "manifest profile must be Blizzlike335", failures)
_expect(ResourceLoader.exists(String(manifest.get("scene", ""))), "streaming scene must exist", failures)
var fidelity_viewport: Array = manifest.get("fidelity_comparison_viewport", [])
_expect(fidelity_viewport.size() == 2 and int(fidelity_viewport[0]) > 0 and int(fidelity_viewport[1]) > 0, "fidelity comparison viewport must contain positive width and height", failures)
var covered := {}
var names := {}