saumwacht-godot/tools/luneraria/lune_benchmark.gd
Martin Stickelbroeck 8b17563814 G1: Drei-Lane-Layout mit Höhenfeld, Grotte, Aufmarschbereichen und MOBA-Kamera
Top windet sich über das Hochland, Mid quert die Kluft über die Mittelplattform,
Bottom führt durch das Tiefland über die Deltabrücke; Top und Bottom sind exakt
gleich lang, alles bleibt spiegelsymmetrisch. LuneTerrain liefert ein exakt
gespiegeltes Höhenfeld, auf dem Figuren stehen. Bossgrotte als Terrasse auf der
Achse zwischen Top und Mid, Aufmarschbereiche je Lane, perspektivische Kamera
mit Sprungtasten (F2–F4, B, G, H, O, R, Q/E). Bestandsassets wiederverwendet,
neue Formen als Blockout für G2/G4.

Geprüft: verify_g1 443/443, Regeln 91/91, M2 73/73, verify_m1 38/38,
verify_m2 14/14, Netz 43/43, Loopback 12/12, Balance 40 Partien (Seitentausch
20/20), Legacy 209/463. Vorher/Nachher-Aufnahmen und Messungen in G1-KARTE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:09:48 +02:00

114 lines
5.3 KiB
GDScript

extends Node
## Frame and simulation cost in a real match scene at representative maximum occupation:
## six armies at the supply cap and a round-13 wave on all three lanes (synthetic fixture,
## gold granted to the fixture only). Two camera setups are measured one after the other:
## a lane view (Mid, Blau side, grotto in the background) and the whole-map overview,
## where every lane, tower, base and the grotto are drawn at once. VSync off; the
## figures apply to this machine only.
## Run: godot --path . --resolution 1920x1080 res://tools/luneraria/lune_benchmark.tscn -- --out=res://docs/luneraria/logs/g1/perf.json
const Game = preload("res://scripts/luneraria/lune_game.gd")
const PHASE_SECONDS: float = 6.0
var out: String = ""
## Start of the current frame's main-thread work (set by a node that processes first).
var frame_start: int = 0
class FrameClock:
extends Node
var owner_bench: Node
func _process(_delta: float) -> void:
owner_bench.frame_start = Time.get_ticks_usec()
func _ready() -> void:
for arg: String in OS.get_cmdline_user_args():
if arg.begins_with("--out="):
out = arg.substr(6)
var clock := FrameClock.new()
clock.owner_bench = self
clock.process_priority = -1000
add_child(clock)
call_deferred("run")
func mean(values: Array) -> float:
var total: float = 0.0
for value: Variant in values:
total += float(value)
return total / maxf(1.0, values.size())
func measure(game: Node3D, state: LuneMatch) -> Dictionary:
for i: int in 30:
await get_tree().process_frame
var frames: Array[float] = []
var cpu: Array[float] = []
var gpu: Array[float] = []
var draws: Array[float] = []
var primitives: Array[float] = []
var objects: Array[float] = []
var mobile: Array[int] = []
var last: int = Time.get_ticks_usec()
var end: int = Time.get_ticks_msec() + int(PHASE_SECONDS * 1000.0)
while Time.get_ticks_msec() < end and state.phase == LuneMatch.Phase.COMBAT:
await RenderingServer.frame_post_draw
var now: int = Time.get_ticks_usec()
frames.append((now - last) / 1000.0)
last = now
# Main-thread time from the first _process of the frame until the frame was drawn.
cpu.append((now - frame_start) / 1000.0)
gpu.append(RenderingServer.viewport_get_measured_render_time_gpu(get_viewport().get_viewport_rid()))
draws.append(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))
primitives.append(Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME))
objects.append(Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME))
mobile.append(state.combat.mobile_count())
frames.sort()
return {"frames": frames.size(), "frame_ms_mean": mean(frames), "frame_ms_p95": frames[int(frames.size() * .95)] if not frames.is_empty() else -1.0,
"cpu_frame_ms_mean": mean(cpu), "gpu_ms_mean": mean(gpu), "draw_calls_mean": mean(draws), "primitives_mean": mean(primitives),
"objects_mean": mean(objects), "mobile_units_mean": mean(mobile), "camera_zoom": game.camera.zoom}
func run() -> void:
Soundscape.enabled = false
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
var t0: int = Time.get_ticks_usec()
var game: Node3D = Game.new()
add_child(game)
game.start({"seed": 5, "human_slot": 0})
var build_ms: float = (Time.get_ticks_usec() - t0) / 1000.0
var state: LuneMatch = game.match_state
state.round_number = 13
for slot: int in range(6):
state.players[slot].draft.gold = 100000
state.players[slot].draft.roster.clear()
LuneBot.plan(state, slot, "ausgewogen")
game._on_ready()
RenderingServer.viewport_set_measure_render_time(get_viewport().get_viewport_rid(), true)
game.speed_index = 1
game.camera.look_at_point(Vector3(-10, 0, 10), 34.0, true)
var lane_view: Dictionary = await measure(game, state)
game.camera.overview(true)
var overview: Dictionary = await measure(game, state)
# Pure simulation cost of one fixed step at this occupation, measured separately.
var probe := LuneMatch.new()
probe.setup({"seed": 5, "human_slot": -1})
probe.round_number = 13
for slot: int in range(6):
probe.players[slot].draft.gold = 100000
LuneBot.plan(probe, slot, "ausgewogen")
probe.begin_combat()
var start_units: int = probe.combat.mobile_count()
var s1: int = Time.get_ticks_usec()
for i: int in 200:
probe.step_combat()
var step_ms: float = (Time.get_ticks_usec() - s1) / 1000.0 / 200.0
var report := {"window": get_window().size, "renderer": RenderingServer.get_current_rendering_method(), "gpu": RenderingServer.get_video_adapter_name(),
"cpu": OS.get_processor_name(), "engine": Engine.get_version_info().string, "scene_build_ms": build_ms,
"mobile_units_at_start": start_units, "buildings": 18, "sim_step_ms": step_ms, "sim_speed": "2x (40 steps/s)",
"lane_view": lane_view, "overview": overview,
"video_mem_mb": Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED) / 1048576.0,
"texture_mem_mb": Performance.get_monitor(Performance.RENDER_TEXTURE_MEM_USED) / 1048576.0,
"static_mem_mb": Performance.get_monitor(Performance.MEMORY_STATIC) / 1048576.0}
var path: String = out if not out.is_empty() else "res://docs/luneraria/logs/perf-%dx%d.json" % [get_window().size.x, get_window().size.y]
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(path.get_base_dir()))
var file := FileAccess.open(path, FileAccess.WRITE)
file.store_string(JSON.stringify(report, "\t"))
file.close()
print("LUNE_BENCH ", JSON.stringify(report))
get_tree().quit()