Files
mtgodot-poc/project/rendering_scenario_test.gd
T

130 lines
6.3 KiB
GDScript

# Offline real-resource stress/viewport report; NOT 40250 visual parity or
# server reconnect acceptance. Run with a renderer, --quit-after 3600.
extends SceneTree
const GameScene = preload("res://game_scene.gd")
const Fixtures = preload("res://gamescene_test.gd")
const PlayerView = preload("res://ui/player_view.gd")
const MobView = preload("res://ui/mob_view.gd")
class ScenarioClient extends Fixtures.FakeClient:
signal warp(pos: Vector3, same_server: bool)
signal world_reset()
var failures: Array[String] = []
var output := ""
func check(ok: bool, label: String) -> void:
if not ok:
failures.append(label)
printerr("FAIL: " + label)
func _init() -> void:
call_deferred("run")
func run() -> void:
var run_start := Time.get_ticks_msec()
var sample_frames := 90
if OS.has_environment("MT_RENDER_SAMPLE_FRAMES"):
sample_frames = clampi(int(OS.get_environment("MT_RENDER_SAMPLE_FRAMES")), 90, 36000)
output = OS.get_environment("MT_RENDER_OUTPUT")
if output == "":
output = ProjectSettings.globalize_path("res://../build/rendering/scenarios-followup")
DirAccess.make_dir_recursive_absolute(output)
root.size = Vector2i(1280, 720)
var report := {"scope": "offline real maps, CPU/GPU animated crowd and viewport layout; no server writes or 40250 parity",
"engine": Engine.get_version_info(), "backend": DisplayServer.get_name(), "samples": [],
"sample_frames": sample_frames, "model_builds": []}
var client := ScenarioClient.new()
root.add_child(client)
var scene := GameScene.new()
root.add_child(scene)
await scene.setup(client, AssetRoot.path(), "OutdoorA1/metin2_map_a1")
client.main = 1000
client.ents[1000] = {"vid": 1000, "name": "Scenario", "race": 0, "pos_cm": Vector2(454600, 934000),
"pos": Vector3(4546, 0, -9340), "is_main": true, "func": 0, "moving": false,
"angle_deg": 0.0, "hp": 100, "max_hp": 100, "dead": false}
for i in 30: await process_frame
check(scene._model_built and scene._map_loaded(), "A1 main character and world loaded")
var centre: Vector3 = scene.player.position
for mode in ["0", "1"]:
OS.set_environment("MTGODOT_GPUSKIN", mode)
var crowd := Node3D.new()
scene.add_child(crowd)
for count in [1, 8, 16]:
while crowd.get_child_count() < count:
var index := crowd.get_child_count()
var view := PlayerView.new()
var build_start := Time.get_ticks_usec()
check(view.build(AssetRoot.path(), index % 8), "crowd race %d build" % (index % 8))
report.model_builds.append({"gpu_skin": mode == "1", "race": index % 8,
"build_ms": (Time.get_ticks_usec() - build_start) / 1000.0,
"note": "synchronous build only; OS/shader caches may already be warm"})
crowd.add_child(view)
view.position = centre + Vector3((index % 4 - 1.5) * 2, 0, (index / 4 + 1) * 2)
view.position.y = scene.world.sample_height(view.position.x, view.position.z)
view.set_anim_state("run")
for i in 12: await process_frame
var times: Array[float] = []
var switches: Array[float] = []
var memory_start := Performance.get_monitor(Performance.MEMORY_STATIC)
var previous := Time.get_ticks_usec()
for i in sample_frames:
if i > 0 and i % 30 == 0:
var switch_start := Time.get_ticks_usec()
var state: String = ["run", "attack", "wait"][(i / 30) % 3]
for view in crowd.get_children(): view.set_anim_state(state)
switches.append((Time.get_ticks_usec() - switch_start) / 1000.0)
await process_frame
var now := Time.get_ticks_usec()
times.append((now - previous) / 1000.0)
previous = now
for view in crowd.get_children():
var bounds: AABB = view.model.get_visual_aabb()
check(bounds.position.is_finite() and bounds.size.is_finite() and bounds.size.length() > 0,
"finite posed bounds after run/attack/wait blend")
times.sort()
switches.sort()
report.samples.append({"gpu_skin": mode == "1", "extra_players": count,
"median_ms": times[sample_frames / 2], "p95_ms": times[int((sample_frames - 1) * 0.95)], "max_ms": times[-1],
"motion_switches": switches.size(), "motion_switch_max_ms": switches[-1],
"memory_static_start": memory_start, "memory_static_end": Performance.get_monitor(Performance.MEMORY_STATIC),
"clip_cache": Metin2AnimPlayer.get_clip_cache_stats(),
"note": "%d window-frame intervals with repeated motion transitions; not GPU timestamp, RSS or FPS guarantee" % sample_frames})
print("SCENARIO_SAMPLE gpu=%s count=%d frames=%d max_ms=%.3f" % [mode, count, sample_frames, times[-1]])
crowd.free()
await process_frame
OS.set_environment("MTGODOT_GPUSKIN", "0")
var mobs := Node3D.new()
scene.add_child(mobs)
for race in [101, 110, 20001]:
var view := MobView.new()
if not view.build(AssetRoot.path(), null, race):
check(false, "NPC/monster build race=%d" % race)
view.free()
continue
mobs.add_child(view)
view.position = centre + Vector3(mobs.get_child_count() * 2, 0, 3)
view.position.y = scene.world.sample_height(view.position.x, view.position.z)
view.set_anim_state("wait")
check(not view.model.get_visual_aabb().size.is_zero_approx(), "NPC/monster real geometry")
for size in [Vector2i(1280, 720), Vector2i(1920, 1080), Vector2i(2560, 1440)]:
root.size = size
for i in 5: await process_frame
if scene.quickbar and scene.quickbar._root:
var rect: Rect2 = scene.quickbar._root.get_global_rect()
check(Rect2(Vector2.ZERO, root.get_visible_rect().size).encloses(rect), "quickbar inside viewport %s" % size)
if DisplayServer.get_name() != "headless":
await RenderingServer.frame_post_draw
check(root.get_texture().get_image().save_png(output.path_join("scene-%dx%d.png" % [size.x, size.y])) == OK, "screenshot saved")
# Late server location update exercises the production map-correction path.
client.ents[1000].pos_cm = Vector2(959109, 269267)
client.ents[1000].pos = Vector3(9591.09, 0, -2692.67)
client.world_reset.emit()
client.warp.emit(client.ents[1000].pos, false)
for i in 60: await process_frame
check(scene.map_path == "OutdoorC1/metin2_map_c1" and scene._map_loaded(), "A1 to C1 map correction")
report["final_map"] = scene.map_path
scene.free()
client.free()
await process_frame
report["failures"] = failures
report["duration_seconds"] = (Time.get_ticks_msec() - run_start) / 1000.0
FileAccess.open(output.path_join("report.json"), FileAccess.WRITE).store_string(JSON.stringify(report, "\t"))
print("rendering_scenario_test: failures=%d report=%s" % [failures.size(), output])
quit(1 if not failures.is_empty() else 0)