feat: complete native client rendering parity updates
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
# 登录 → 选人 → 进游戏,把 RenderGame 的绘制命令(python_3d_surface.gd)画出来并截图。
|
||||
#
|
||||
# godot --path project --script res://python_game_render_test.gd
|
||||
# MT_FAKE_MOB_COUNT=24 MT_PROFILE_FRAME=1 MT_PROFILE_COMBAT=1 script/python_game_render_test.sh
|
||||
#
|
||||
# 服务器与账号只从环境变量读(MT_LIVE_HOST / MT_LIVE_LOGIN / MT_LIVE_PASSWORD,可选 MT_LIVE_AUTH_PORT
|
||||
# 11000、MT_LIVE_GAME_PORT 13000、MT_LIVE_SLOT 0);Python 侧也用 os.environ 取,不进源码文本。
|
||||
@@ -72,8 +73,12 @@ func run() -> void:
|
||||
var output := OS.get_environment("MT_RENDER_OUTPUT")
|
||||
if output.is_empty():
|
||||
output = ProjectSettings.globalize_path("res://../build/rendering/python-game-%d" % Time.get_unix_time_from_system())
|
||||
elif not output.is_absolute_path():
|
||||
output = ProjectSettings.globalize_path("res://../" + output)
|
||||
DirAccess.make_dir_recursive_absolute(output)
|
||||
var report := {"output": output, "headless": DisplayServer.get_name() == "headless"}
|
||||
if OS.has_environment("MT_FAKE_MOB_COUNT"):
|
||||
report["fake_mob_count"] = int(OS.get_environment("MT_FAKE_MOB_COUNT"))
|
||||
|
||||
var host := OS.get_environment("MT_LIVE_HOST")
|
||||
if host.is_empty() or OS.get_environment("MT_LIVE_LOGIN").is_empty() or OS.get_environment("MT_LIVE_PASSWORD").is_empty():
|
||||
@@ -132,9 +137,63 @@ func run() -> void:
|
||||
# The main character's draws reach the 3D surface.
|
||||
var drawn := in_game and await _pump_until(10, func(): return world.draw_count > 0)
|
||||
_check(drawn, "RenderGame draws shown (%d)" % world.draw_count)
|
||||
# Let the WAIT motion run a little before the picture.
|
||||
for i in 30:
|
||||
# Let the WAIT motion run a little before the picture, and let GamePhase (3 packets/frame)
|
||||
# finish draining the entergame burst + GC_PING when MT_FAKE_MOB_COUNT is large.
|
||||
var warmup_frames := maxi(30, int(report.get("fake_mob_count", 1)) + 20)
|
||||
for i in warmup_frames:
|
||||
await process_frame
|
||||
if OS.has_environment("MT_PROFILE_FRAME"):
|
||||
var frame_ms: Array[float] = []
|
||||
var ui_update_ms: Array[float] = []
|
||||
var ui_draw_ms: Array[float] = []
|
||||
var world_update_ms: Array[float] = []
|
||||
var previous := Time.get_ticks_usec()
|
||||
for i in 240:
|
||||
await process_frame
|
||||
var current := Time.get_ticks_usec()
|
||||
frame_ms.append(float(current - previous) / 1000.0)
|
||||
ui_update_ms.append(ui.profile_ui_update_ms)
|
||||
ui_draw_ms.append(ui.profile_draw_ms)
|
||||
world_update_ms.append(world.profile_update_ms)
|
||||
previous = current
|
||||
frame_ms.sort()
|
||||
ui_update_ms.sort()
|
||||
ui_draw_ms.sort()
|
||||
world_update_ms.sort()
|
||||
var total := 0.0
|
||||
for sample in frame_ms:
|
||||
total += sample
|
||||
report["frame_profile"] = {
|
||||
"samples": frame_ms.size(),
|
||||
"mean_ms": total / frame_ms.size(),
|
||||
"p50_ms": frame_ms[119],
|
||||
"p95_ms": frame_ms[227],
|
||||
"ui_update_p50_ms": ui_update_ms[119],
|
||||
"ui_draw_p50_ms": ui_draw_ms[119],
|
||||
"world_update_p50_ms": world_update_ms[119],
|
||||
}
|
||||
var command_kinds := {}
|
||||
var image_texts := {}
|
||||
for command in Metin2PythonHost.ui_render_commands():
|
||||
var kind: String = str(command.get("kind", "unknown"))
|
||||
command_kinds[kind] = int(command_kinds.get(kind, 0)) + 1
|
||||
if kind == "image":
|
||||
var image_name: String = str(command.get("text", ""))
|
||||
image_texts[image_name] = int(image_texts.get(image_name, 0)) + 1
|
||||
report["frame_profile"]["ui_commands"] = command_kinds
|
||||
report["frame_profile"]["image_texts"] = image_texts
|
||||
var batched_commands := 0
|
||||
var glyph_batches := 0
|
||||
var batched_glyphs := 0
|
||||
for command in Metin2PythonHost.ui_render_commands_batched():
|
||||
batched_commands += 1
|
||||
if command.get("kind", "") == "glyph_batch":
|
||||
glyph_batches += 1
|
||||
batched_glyphs += int(command.get("glyph_count", 0))
|
||||
report["frame_profile"]["batched_commands"] = batched_commands
|
||||
report["frame_profile"]["glyph_batches"] = glyph_batches
|
||||
report["frame_profile"]["batched_glyphs"] = batched_glyphs
|
||||
print("FRAME_PROFILE ", report["frame_profile"])
|
||||
report["draw_count"] = world.draw_count
|
||||
report["unlit_stand_in_count"] = world.unlit_stand_in_count
|
||||
report["terrain"] = world.terrain_report
|
||||
@@ -170,14 +229,25 @@ func run() -> void:
|
||||
var draws: Array = Metin2PythonHost.render3d_draws()
|
||||
var native_lit_draws := 0
|
||||
var view_finite := true
|
||||
var actor_foot_cm := INF
|
||||
var foot_xy := Vector2.ZERO
|
||||
# Identify character, monster and NPC draws (excluding map objects, buildings, trees and roofs).
|
||||
var actor_draws: Array = []
|
||||
for draw in draws:
|
||||
var tex: String = String(draw.get("texture0", "")).to_lower().replace("\\", "/")
|
||||
for k in ["pc/", "pc2/", "monster/", "npc/"]:
|
||||
if k in tex:
|
||||
actor_draws.append(draw)
|
||||
break
|
||||
|
||||
for draw in draws:
|
||||
if draw["light0"]:
|
||||
native_lit_draws += 1
|
||||
for value in draw["view"]:
|
||||
if not is_finite(value):
|
||||
view_finite = false
|
||||
|
||||
var actor_foot_cm := INF
|
||||
var foot_xy := Vector2.ZERO
|
||||
for draw in actor_draws:
|
||||
var world_xform: Transform3D = world.d3d_transform(draw["world"])
|
||||
for vertex in draw["positions"]:
|
||||
var p: Vector3 = world_xform * vertex
|
||||
@@ -187,14 +257,14 @@ func run() -> void:
|
||||
# Every actor, not only the main one, stands on the map. An actor's parts (body, head, hair, teeth)
|
||||
# share its world matrix; the lowest vertex over those parts meets sample_height.
|
||||
var actor_low := {}
|
||||
for draw in draws:
|
||||
for draw in actor_draws:
|
||||
var draw_xform: Transform3D = world.d3d_transform(draw["world"])
|
||||
var key := str(draw["world"])
|
||||
for vertex in draw["positions"]:
|
||||
var p: Vector3 = draw_xform * vertex
|
||||
if not actor_low.has(key) or p.z < actor_low[key].z:
|
||||
actor_low[key] = p
|
||||
if world.terrain:
|
||||
if world.terrain and not actor_low.is_empty():
|
||||
var worst_actor_gap_cm := 0.0
|
||||
for low in actor_low.values():
|
||||
var ground := float(world.terrain.call("sample_height", low.x / 100.0, -low.y / 100.0)) * 100.0
|
||||
@@ -244,7 +314,7 @@ func run() -> void:
|
||||
var distinct_textures := {}
|
||||
for instance in world.find_children("Draw*", "MeshInstance3D", false, false):
|
||||
if instance.visible and instance.mesh:
|
||||
var material: StandardMaterial3D = instance.mesh.surface_get_material(0)
|
||||
var material: StandardMaterial3D = instance.get_active_material(0)
|
||||
if material and material.albedo_texture:
|
||||
surfaces_textured += 1
|
||||
distinct_textures[material.albedo_texture] = true
|
||||
@@ -353,7 +423,7 @@ func run() -> void:
|
||||
for frame in 600:
|
||||
await process_frame
|
||||
now = dog_screen.call()
|
||||
if now.distance_to(last) < 0.5:
|
||||
if now.x >= 0.0 and now.distance_to(last) < 0.5:
|
||||
break
|
||||
last = now
|
||||
return now
|
||||
@@ -377,15 +447,60 @@ func run() -> void:
|
||||
await process_frame
|
||||
button.call(true, at)
|
||||
var mid_shot := false
|
||||
var dead := await _pump_until(20, func():
|
||||
var combat_condition := func():
|
||||
var now: Vector2 = dog_screen.call()
|
||||
if now.x >= 0.0:
|
||||
at = now
|
||||
move.call(at)
|
||||
if not mid_shot and Metin2PythonHost.evaluate("33 in _target_hp") == "True" and DisplayServer.get_name() != "headless":
|
||||
if not mid_shot and Metin2PythonHost.evaluate("33 in _target_hp") == "True" and DisplayServer.get_name() != "headless" and not OS.has_environment("MT_PROFILE_COMBAT"):
|
||||
mid_shot = true
|
||||
root.get_texture().get_image().save_png(output.path_join("combat_mid.png"))
|
||||
return Metin2PythonHost.evaluate("_target_hp[-1:] == [0]") == "True")
|
||||
return Metin2PythonHost.evaluate("_target_hp[-1:] == [0]") == "True"
|
||||
var dead := false
|
||||
if OS.has_environment("MT_PROFILE_COMBAT"):
|
||||
var combat_frames: Array[float] = []
|
||||
var combat_ui_update: Array[float] = []
|
||||
var combat_ui_draw: Array[float] = []
|
||||
var combat_world_update: Array[float] = []
|
||||
var combat_draw_counts: Array[int] = []
|
||||
var deadline := Time.get_ticks_msec() + 20000
|
||||
var previous := Time.get_ticks_usec()
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
if combat_condition.call():
|
||||
dead = true
|
||||
break
|
||||
await process_frame
|
||||
var current := Time.get_ticks_usec()
|
||||
combat_frames.append(float(current - previous) / 1000.0)
|
||||
combat_ui_update.append(ui.profile_ui_update_ms)
|
||||
combat_ui_draw.append(ui.profile_draw_ms)
|
||||
combat_world_update.append(world.profile_update_ms)
|
||||
combat_draw_counts.append(world.draw_count)
|
||||
previous = current
|
||||
combat_frames.sort()
|
||||
combat_ui_update.sort()
|
||||
combat_ui_draw.sort()
|
||||
combat_world_update.sort()
|
||||
combat_draw_counts.sort()
|
||||
if not combat_frames.is_empty():
|
||||
var n := combat_frames.size()
|
||||
report["combat_profile"] = {
|
||||
"samples": n,
|
||||
"frame_p50_ms": combat_frames[n / 2],
|
||||
"frame_p95_ms": combat_frames[mini(n - 1, int(n * 0.95))],
|
||||
"frame_max_ms": combat_frames[n - 1],
|
||||
"ui_update_p50_ms": combat_ui_update[n / 2],
|
||||
"ui_update_p95_ms": combat_ui_update[mini(n - 1, int(n * 0.95))],
|
||||
"ui_draw_p50_ms": combat_ui_draw[n / 2],
|
||||
"ui_draw_p95_ms": combat_ui_draw[mini(n - 1, int(n * 0.95))],
|
||||
"world_update_p50_ms": combat_world_update[n / 2],
|
||||
"world_update_p95_ms": combat_world_update[mini(n - 1, int(n * 0.95))],
|
||||
"draw_count_p50": combat_draw_counts[n / 2],
|
||||
"draw_count_max": combat_draw_counts[n - 1],
|
||||
}
|
||||
print("COMBAT_PROFILE ", report["combat_profile"])
|
||||
else:
|
||||
dead = await _pump_until(20, combat_condition)
|
||||
button.call(false, at)
|
||||
if DisplayServer.get_name() != "headless":
|
||||
root.get_texture().get_image().save_png(output.path_join("combat.png"))
|
||||
|
||||
Reference in New Issue
Block a user