extends Node # One-off GPU frame-time profiling harness for task 0.15b's real-hardware # follow-up (MULTIPLAYER_SPEC.md §5.5.1) — the automated Mac passes gave # inconsistent, sometimes implausible numbers (stale-process contention, # and Apple Silicon's tile-based GPU architecture is a poor stand-in for the # target reference hardware). Run this directly on a machine with a real # discrete desktop GPU instead: # # godot --path Game res://tools/gpu_profile_harness.tscn # # On a Linux box with no attached physical display, wrap it in a virtual # framebuffer so it still gets a real windowing/rendering context (NOT # --headless — that uses a dummy renderer with no GPU rendering at all, # see CLAUDE.md's "Headless smoke test" note): # # xvfb-run -a --server-args="-screen 0 1920x1080x24" \ # godot --path Game res://tools/gpu_profile_harness.tscn # # Prints a report to stdout and also writes it to # user://gpu_profile_report.txt — on Linux that's typically # ~/.local/share/godot/app_userdata/Cosmic Clash/gpu_profile_report.txt; the # exact resolved path is printed at the end of the run, so just paste that # back. Quits itself automatically when done (~90 seconds total). const SAMPLE_SECONDS := 4.0 const SETTLE_SECONDS := 1.5 var _match: Node var _env: Environment var _shadow_lights: Array[Light3D] = [] var _postfx: CanvasItem var _viewport: Viewport var _report_lines: Array[String] = [] func _ready() -> void: var adapter := RenderingServer.get_video_adapter_name() var vendor := RenderingServer.get_video_adapter_vendor() _log("GPU adapter: %s (%s)" % [adapter, vendor]) if not ("NVIDIA" in adapter.to_upper() or "NVIDIA" in vendor.to_upper()): _log("WARNING: this doesn't look like a real NVIDIA GPU context.") _log(" If this is llvmpipe/softpipe/Mesa software rendering, every") _log(" number below is meaningless for GPU profiling purposes —") _log(" check `glxinfo | grep -i renderer` and your Xorg/Xvfb/driver") _log(" setup before trusting this report.") var match_scene := load("res://scenes/match.tscn") as PackedScene _match = match_scene.instantiate() # 3v3 = 6 ships, matching the scenario MULTIPLAYER_SPEC.md §5.5 measures. _match.team_size = 3 # Direct-scene-run fallback path (see match_mode.gd:_make_opponent_controller) # — gives every AI ship a real trained policy so thruster VFX/movement # load matches actual play, not six stationary hulls. _match.bot_model_path = "res://bots/promoted/medium.json" add_child(_match) _log("Waiting for kickoff and bots to start moving...") await get_tree().create_timer(5.0).timeout _viewport = get_tree().root var world_env := _match.arena.get_node_or_null("WorldEnvironment") as WorldEnvironment if not world_env or not world_env.environment: _log("ERROR: no WorldEnvironment found on the spawned arena — aborting.") get_tree().quit(1) return _env = world_env.environment for light in _match.arena.find_children("*", "Light3D", true, false): if (light as Light3D).shadow_enabled: _shadow_lights.append(light) var rig := get_tree().get_first_node_in_group("ship_camera") _postfx = rig.get_node_or_null("PostProcess/PostFX") if rig else null if not _postfx: _log("WARNING: PostFX node not found — that pass won't be profiled.") await _run_all_configs() var report := "\n".join(_report_lines) var f := FileAccess.open("user://gpu_profile_report.txt", FileAccess.WRITE) if f: f.store_string(report) f.close() _log("") _log("Report written to: %s" % ProjectSettings.globalize_path("user://gpu_profile_report.txt")) get_tree().quit() func _log(s: String) -> void: _report_lines.append(s) print(s) func _run_all_configs() -> void: var base_sdfgi := _env.sdfgi_enabled var base_ssil := _env.ssil_enabled var base_ssao := _env.ssao_enabled var base_glow := _env.glow_enabled var base_msaa := _viewport.msaa_3d var base_aa := _viewport.screen_space_aa var base_postfx_visible: bool = _postfx.visible if _postfx else true await _measure("baseline_all_on") _env.sdfgi_enabled = false _env.ssil_enabled = false _env.ssao_enabled = false _env.glow_enabled = false for l in _shadow_lights: l.shadow_enabled = false _viewport.msaa_3d = Viewport.MSAA_DISABLED _viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED if _postfx: _postfx.visible = false await _measure("all_off_floor") _restore_baseline(base_sdfgi, base_ssil, base_ssao, base_glow, base_msaa, base_aa, base_postfx_visible) _env.sdfgi_enabled = false await _measure("sdfgi_off") _env.sdfgi_enabled = base_sdfgi _env.ssil_enabled = false await _measure("ssil_off") _env.ssil_enabled = base_ssil _env.ssao_enabled = false await _measure("ssao_off") _env.ssao_enabled = base_ssao _env.glow_enabled = false await _measure("glow_off") _env.glow_enabled = base_glow for l in _shadow_lights: l.shadow_enabled = false await _measure("shadows_off_all_%d_lights" % _shadow_lights.size()) for l in _shadow_lights: l.shadow_enabled = true _viewport.msaa_3d = Viewport.MSAA_DISABLED await _measure("msaa_off") _viewport.msaa_3d = base_msaa _viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED await _measure("fxaa_off") _viewport.screen_space_aa = base_aa if _postfx: _postfx.visible = false await _measure("postfx_off") _postfx.visible = base_postfx_visible func _restore_baseline(sdfgi: bool, ssil: bool, ssao: bool, glow: bool, msaa: Viewport.MSAA, aa: Viewport.ScreenSpaceAA, postfx_visible: bool) -> void: _env.sdfgi_enabled = sdfgi _env.ssil_enabled = ssil _env.ssao_enabled = ssao _env.glow_enabled = glow for l in _shadow_lights: l.shadow_enabled = true _viewport.msaa_3d = msaa _viewport.screen_space_aa = aa if _postfx: _postfx.visible = postfx_visible # Raw get_process_delta_time() per rendered frame, not Performance.TIME_FPS — # TIME_FPS is itself a smoothed/rounded value, which would understate exactly # the p99 variance this is trying to measure. func _measure(label: String) -> void: await get_tree().create_timer(SETTLE_SECONDS).timeout var samples: PackedFloat32Array = [] var elapsed := 0.0 while elapsed < SAMPLE_SECONDS: await get_tree().process_frame var dt := get_process_delta_time() samples.append(dt * 1000.0) elapsed += dt samples.sort() var p50 := samples[samples.size() / 2] var p99 := samples[mini(int(samples.size() * 0.99), samples.size() - 1)] _log("%-28s p50=%6.2fms p99=%6.2fms fps(p50)=%6.1f n=%d" % [label, p50, p99, 1000.0 / p50, samples.size()])