базовый рендер
This commit is contained in:
@@ -1,33 +1,7 @@
|
||||
extends SceneTree
|
||||
|
||||
const STREAMING_SCENE := "res://src/scenes/streaming/eastern_kingdoms_streaming.tscn"
|
||||
|
||||
const CHECKPOINTS := [
|
||||
{
|
||||
"name": "elwynn_waterfall_front",
|
||||
"camera": Vector3(16445.0, 125.0, 26295.0),
|
||||
"target": Vector3(16518.84, 68.0, 26427.27),
|
||||
"player": Vector3(16518.84, 55.0, 26427.27),
|
||||
},
|
||||
{
|
||||
"name": "elwynn_waterfall_side",
|
||||
"camera": Vector3(16670.0, 115.0, 26320.0),
|
||||
"target": Vector3(16518.84, 72.0, 26427.27),
|
||||
"player": Vector3(16518.84, 55.0, 26427.27),
|
||||
},
|
||||
{
|
||||
"name": "goldshire_inn_windows",
|
||||
"camera": Vector3(16942.0, 82.0, 26503.0),
|
||||
"target": Vector3(17042.27, 66.0, 26530.91),
|
||||
"player": Vector3(17042.27, 58.0, 26530.91),
|
||||
},
|
||||
{
|
||||
"name": "goldshire_blacksmith_windows",
|
||||
"camera": Vector3(16892.0, 79.0, 26562.0),
|
||||
"target": Vector3(16972.46, 64.0, 26526.7),
|
||||
"player": Vector3(16972.46, 58.0, 26526.7),
|
||||
},
|
||||
]
|
||||
const DEFAULT_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
||||
const M2_NATIVE_ANIMATED_BUILDER := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
@@ -36,42 +10,56 @@ func _initialize() -> void:
|
||||
|
||||
func _capture_async() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
var output_dir := _arg(args, "--output", "user://render_checkpoints")
|
||||
var manifest_path := _arg(args, "--manifest", DEFAULT_MANIFEST_PATH)
|
||||
var manifest := _load_json(manifest_path)
|
||||
if manifest.is_empty():
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var output_dir := _arg(args, "--output", "user://render_baseline")
|
||||
var only := _arg(args, "--only", "").to_lower()
|
||||
var wait_seconds := float(_arg(args, "--wait", "8.0"))
|
||||
var width := int(_arg(args, "--width", "1280"))
|
||||
var height := int(_arg(args, "--height", "900"))
|
||||
var wait_seconds := float(_arg(args, "--wait", str(manifest.get("default_wait_seconds", 8.0))))
|
||||
var measure_seconds := float(_arg(args, "--measure", str(manifest.get("default_measure_seconds", 3.0))))
|
||||
var revision := _arg(args, "--revision", "worktree")
|
||||
var cache_state := _arg(args, "--cache-state", "existing")
|
||||
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])))
|
||||
|
||||
get_root().size = Vector2i(maxi(16, width), maxi(16, height))
|
||||
var abs_output_dir := ProjectSettings.globalize_path(output_dir)
|
||||
if not dry_run:
|
||||
DirAccess.make_dir_recursive_absolute(abs_output_dir)
|
||||
elif headless and not args.has("--dry-run"):
|
||||
DirAccess.make_dir_recursive_absolute(abs_output_dir)
|
||||
if headless and not args.has("--dry-run"):
|
||||
print("RENDER_CHECKPOINT dry-run: headless display cannot capture viewport PNGs.")
|
||||
|
||||
var packed: PackedScene = load(STREAMING_SCENE)
|
||||
var scene_path := String(manifest.get("scene", ""))
|
||||
var packed: PackedScene = load(scene_path)
|
||||
if packed == null:
|
||||
push_error("Cannot load streaming scene: %s" % STREAMING_SCENE)
|
||||
push_error("Cannot load streaming scene: %s" % scene_path)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var world = packed.instantiate()
|
||||
if not (world is Node3D):
|
||||
push_error("Streaming scene root is not Node3D: %s" % STREAMING_SCENE)
|
||||
push_error("Streaming scene root is not Node3D: %s" % scene_path)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var checkpoints: Array = manifest.get("checkpoints", [])
|
||||
if checkpoints.is_empty():
|
||||
push_error("Render baseline manifest has no checkpoints")
|
||||
quit(1)
|
||||
return
|
||||
var first: Dictionary = checkpoints[0]
|
||||
var camera := Camera3D.new()
|
||||
camera.name = "CheckpointCamera"
|
||||
camera.current = true
|
||||
camera.fov = 62.0
|
||||
camera.fov = float(manifest.get("camera_fov", 62.0))
|
||||
camera.far = 50000.0
|
||||
camera.position = CHECKPOINTS[0].get("camera", Vector3.ZERO)
|
||||
camera.position = _vector3(first.get("camera", [0.0, 0.0, 0.0]))
|
||||
(world as Node3D).add_child(camera)
|
||||
world.set("camera_path", NodePath("CheckpointCamera"))
|
||||
world.set("debug_streaming", true)
|
||||
world.set("runtime_stats_enabled", true)
|
||||
get_root().add_child(world)
|
||||
await process_frame
|
||||
await process_frame
|
||||
@@ -84,51 +72,245 @@ func _capture_async() -> void:
|
||||
if visual != null:
|
||||
visual.visible = false
|
||||
|
||||
var report := {
|
||||
"schema_version": 1,
|
||||
"manifest": manifest_path,
|
||||
"profile": manifest.get("profile", "Blizzlike335"),
|
||||
"revision": revision,
|
||||
"created_utc": Time.get_datetime_string_from_system(true, true),
|
||||
"dry_run": dry_run,
|
||||
"cache_state": cache_state,
|
||||
"environment": _environment_metadata(),
|
||||
"comparison_budgets": manifest.get("comparison_budgets", {}),
|
||||
"cache_contract": manifest.get("cache_contract", {}),
|
||||
"cache_inventory": _cache_inventory(),
|
||||
"results": [],
|
||||
}
|
||||
var captured := 0
|
||||
for checkpoint_variant in CHECKPOINTS:
|
||||
var checkpoint: Dictionary = checkpoint_variant
|
||||
var checkpoint_name := String(checkpoint.get("name", "checkpoint"))
|
||||
if not only.is_empty() and not checkpoint_name.to_lower().contains(only):
|
||||
continue
|
||||
var passes := ["cold_process", "warm_revisit"]
|
||||
if dry_run:
|
||||
passes = ["dry_run"]
|
||||
|
||||
camera.global_position = checkpoint.get("camera", Vector3.ZERO)
|
||||
camera.look_at(checkpoint.get("target", Vector3.ZERO), Vector3.UP)
|
||||
if player != null:
|
||||
player.global_position = checkpoint.get("player", checkpoint.get("target", Vector3.ZERO))
|
||||
if world.has_method("_refresh_streaming_targets_at"):
|
||||
world.call("_refresh_streaming_targets_at", camera.global_position, true)
|
||||
for pass_name in passes:
|
||||
for checkpoint_variant in checkpoints:
|
||||
if not (checkpoint_variant is Dictionary):
|
||||
continue
|
||||
var checkpoint: Dictionary = checkpoint_variant
|
||||
var checkpoint_name := String(checkpoint.get("name", "checkpoint"))
|
||||
if not only.is_empty() and not checkpoint_name.to_lower().contains(only):
|
||||
continue
|
||||
|
||||
await create_timer(maxf(wait_seconds, 0.0)).timeout
|
||||
if dry_run:
|
||||
print("RENDER_CHECKPOINT dry_run name=%s camera=%s target=%s" % [
|
||||
checkpoint_name,
|
||||
camera.global_position,
|
||||
checkpoint.get("target", Vector3.ZERO),
|
||||
])
|
||||
var probe := _create_probe(checkpoint, dry_run)
|
||||
if probe != null:
|
||||
(world as Node3D).add_child(probe)
|
||||
probe.global_position = _vector3(checkpoint.get("target", [0.0, 0.0, 0.0]))
|
||||
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)
|
||||
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)))
|
||||
if world.has_method("_refresh_streaming_targets_at"):
|
||||
world.call("_refresh_streaming_targets_at", camera.global_position, true)
|
||||
|
||||
if dry_run:
|
||||
print("RENDER_CHECKPOINT dry_run name=%s coverage=%s camera=%s target=%s time=%.2f" % [
|
||||
checkpoint_name,
|
||||
str(checkpoint.get("coverage", [])),
|
||||
camera.global_position,
|
||||
_vector3(checkpoint.get("target", [0.0, 0.0, 0.0])),
|
||||
float(checkpoint.get("time_hours", 13.0)),
|
||||
])
|
||||
(report["results"] as Array).append(_result_record(checkpoint, pass_name, 0.0, {}, ""))
|
||||
captured += 1
|
||||
if probe != null:
|
||||
probe.queue_free()
|
||||
continue
|
||||
|
||||
var load_started := Time.get_ticks_usec()
|
||||
await create_timer(maxf(wait_seconds, 0.0)).timeout
|
||||
var load_time_ms := float(Time.get_ticks_usec() - load_started) / 1000.0
|
||||
var metrics := await _measure_frames(maxf(measure_seconds, 0.1), world)
|
||||
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)
|
||||
if err != OK:
|
||||
push_error("Failed to save checkpoint %s to %s (err=%d)" % [checkpoint_name, abs_path, err])
|
||||
quit(1)
|
||||
return
|
||||
var sha256 := FileAccess.get_sha256(abs_path)
|
||||
(report["results"] as Array).append(_result_record(checkpoint, pass_name, load_time_ms, metrics, sha256))
|
||||
print("RENDER_CHECKPOINT saved %s sha256=%s" % [abs_path, sha256])
|
||||
captured += 1
|
||||
continue
|
||||
if probe != null:
|
||||
probe.queue_free()
|
||||
await process_frame
|
||||
|
||||
await RenderingServer.frame_post_draw
|
||||
|
||||
var image := get_root().get_texture().get_image()
|
||||
var file_name := "%s.png" % checkpoint_name
|
||||
var abs_path := abs_output_dir.path_join(file_name)
|
||||
var err := image.save_png(abs_path)
|
||||
if err != OK:
|
||||
push_error("Failed to save checkpoint %s to %s (err=%d)" % [checkpoint_name, abs_path, err])
|
||||
quit(1)
|
||||
return
|
||||
print("RENDER_CHECKPOINT saved %s" % abs_path)
|
||||
captured += 1
|
||||
var report_path := abs_output_dir.path_join("report.json")
|
||||
var report_file := FileAccess.open(report_path, FileAccess.WRITE)
|
||||
if report_file == null:
|
||||
push_error("Cannot write render baseline report: %s" % report_path)
|
||||
quit(1)
|
||||
return
|
||||
report_file.store_string(JSON.stringify(report, " "))
|
||||
report_file.close()
|
||||
print("RENDER_BASELINE report=%s results=%d" % [report_path, captured])
|
||||
|
||||
world.queue_free()
|
||||
if captured <= 0:
|
||||
push_error("No checkpoints captured. --only filter was: %s" % only)
|
||||
push_error("No checkpoints selected. --only filter was: %s" % only)
|
||||
quit(1)
|
||||
return
|
||||
quit(0)
|
||||
|
||||
|
||||
func _measure_frames(seconds: float, world: Node) -> Dictionary:
|
||||
var frame_times: Array[float] = []
|
||||
var deadline := Time.get_ticks_usec() + int(seconds * 1000000.0)
|
||||
var previous := Time.get_ticks_usec()
|
||||
while Time.get_ticks_usec() < deadline:
|
||||
await process_frame
|
||||
var now := Time.get_ticks_usec()
|
||||
frame_times.append(float(now - previous) / 1000.0)
|
||||
previous = now
|
||||
frame_times.sort()
|
||||
var snapshot := {}
|
||||
if world.has_method("render_baseline_snapshot"):
|
||||
snapshot = world.call("render_baseline_snapshot")
|
||||
return {
|
||||
"frames": frame_times.size(),
|
||||
"frame_ms_p50": _percentile(frame_times, 0.50),
|
||||
"frame_ms_p95": _percentile(frame_times, 0.95),
|
||||
"frame_ms_p99": _percentile(frame_times, 0.99),
|
||||
"max_hitch_ms": frame_times[-1] if not frame_times.is_empty() else 0.0,
|
||||
"memory_static_bytes": int(Performance.get_monitor(Performance.MEMORY_STATIC)),
|
||||
"video_memory_bytes": int(Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED)),
|
||||
"draw_calls": int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)),
|
||||
"objects": int(Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME)),
|
||||
"streaming": snapshot,
|
||||
}
|
||||
|
||||
|
||||
func _result_record(checkpoint: Dictionary, pass_name: String, load_time_ms: float, metrics: Dictionary, sha256: String) -> Dictionary:
|
||||
return {
|
||||
"name": checkpoint.get("name", "checkpoint"),
|
||||
"coverage": checkpoint.get("coverage", []),
|
||||
"pass": pass_name,
|
||||
"camera": checkpoint.get("camera", []),
|
||||
"target": checkpoint.get("target", []),
|
||||
"player": checkpoint.get("player", []),
|
||||
"time_hours": checkpoint.get("time_hours", 13.0),
|
||||
"load_time_ms": load_time_ms,
|
||||
"metrics": metrics,
|
||||
"png_sha256": sha256,
|
||||
}
|
||||
|
||||
|
||||
func _create_probe(checkpoint: Dictionary, dry_run: bool) -> Node3D:
|
||||
var rel_path := String(checkpoint.get("probe_model", ""))
|
||||
if rel_path.is_empty() or dry_run:
|
||||
return null
|
||||
if not ClassDB.class_exists("M2Loader"):
|
||||
push_warning("M2Loader unavailable; skipping probe model %s" % rel_path)
|
||||
return null
|
||||
var extracted_dir := "res://data/extracted"
|
||||
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(rel_path))
|
||||
if not FileAccess.file_exists(abs_path):
|
||||
push_warning("Probe model missing: %s" % abs_path)
|
||||
return null
|
||||
var loader = ClassDB.instantiate("M2Loader")
|
||||
var data: Dictionary = loader.call("load_m2_animated", abs_path)
|
||||
if data.is_empty():
|
||||
push_warning("Probe model could not be parsed: %s" % rel_path)
|
||||
return null
|
||||
return M2_NATIVE_ANIMATED_BUILDER.build(data, extracted_dir)
|
||||
|
||||
|
||||
func _set_sky_time(world: Node, time_hours: float) -> void:
|
||||
var sky := world.get_node_or_null("WowSkyController")
|
||||
if sky != null:
|
||||
sky.set("use_system_time", false)
|
||||
sky.set("time_speed", 0.0)
|
||||
sky.set("fixed_time_hours", time_hours)
|
||||
|
||||
|
||||
func _environment_metadata() -> Dictionary:
|
||||
return {
|
||||
"godot_version": Engine.get_version_info(),
|
||||
"os": OS.get_name(),
|
||||
"os_version": OS.get_version(),
|
||||
"cpu": OS.get_processor_name(),
|
||||
"logical_cpu_count": OS.get_processor_count(),
|
||||
"rendering_method": RenderingServer.get_current_rendering_method(),
|
||||
"rendering_driver": RenderingServer.get_current_rendering_driver_name(),
|
||||
"video_adapter": RenderingServer.get_video_adapter_name(),
|
||||
"video_vendor": RenderingServer.get_video_adapter_vendor(),
|
||||
"video_api": RenderingServer.get_video_adapter_api_version(),
|
||||
"memory": OS.get_memory_info(),
|
||||
"viewport": [get_root().size.x, get_root().size.y],
|
||||
}
|
||||
|
||||
|
||||
func _cache_inventory() -> Dictionary:
|
||||
var paths := [
|
||||
"res://data/cache/baked_terrain_v2",
|
||||
"res://data/cache/baked_terrain_stream_v1",
|
||||
"res://data/cache/terrain_splat_v1",
|
||||
"res://data/cache/terrain_control_splat_v3",
|
||||
"res://data/cache/m2_glb",
|
||||
"res://data/cache/wmo_tscn",
|
||||
"res://data/cache/wmo_render_v1",
|
||||
]
|
||||
var inventory := {}
|
||||
for path in paths:
|
||||
var dir := DirAccess.open(path)
|
||||
inventory[path] = {
|
||||
"present": dir != null,
|
||||
"file_count": _count_files(path) if dir != null else 0,
|
||||
}
|
||||
return inventory
|
||||
|
||||
|
||||
func _count_files(path: String) -> int:
|
||||
var dir := DirAccess.open(path)
|
||||
if dir == null:
|
||||
return 0
|
||||
var count := dir.get_files().size()
|
||||
for child in dir.get_directories():
|
||||
count += _count_files(path.path_join(child))
|
||||
return count
|
||||
|
||||
|
||||
func _percentile(sorted_values: Array[float], fraction: float) -> float:
|
||||
if sorted_values.is_empty():
|
||||
return 0.0
|
||||
var index := clampi(int(ceil(fraction * sorted_values.size())) - 1, 0, sorted_values.size() - 1)
|
||||
return sorted_values[index]
|
||||
|
||||
|
||||
func _load_json(path: String) -> Dictionary:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
push_error("Cannot open JSON file: %s" % path)
|
||||
return {}
|
||||
var parsed = JSON.parse_string(file.get_as_text())
|
||||
if not (parsed is Dictionary):
|
||||
push_error("JSON root must be an object: %s" % path)
|
||||
return {}
|
||||
return parsed
|
||||
|
||||
|
||||
func _vector3(value: Variant) -> Vector3:
|
||||
if value is Array and value.size() >= 3:
|
||||
return Vector3(float(value[0]), float(value[1]), float(value[2]))
|
||||
return Vector3.ZERO
|
||||
|
||||
|
||||
func _arg(args: PackedStringArray, name: String, default_value: String) -> String:
|
||||
var idx := args.find(name)
|
||||
if idx >= 0 and idx + 1 < args.size():
|
||||
|
||||
Reference in New Issue
Block a user