mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
b43ad207c1
multiplayer-next.md was a 1662-line mix of standing architecture spec and task-completion tracking, most of which was dense per-task DONE evidence for finished Phases 0-6. Split it: - MULTIPLAYER_SPEC.md (new): the locked architecture decisions, wire format, server-side input handling, prediction/reconciliation, latency/frame-rate budget, and match lifecycle state machine - standing design reference, not task-tracked. - multiplayer-next.md (trimmed 1662 -> ~370 lines): only outstanding work remains - §0 status, §7 Phase 7/8 task tables condensed to "what's left" per task, §8-11 reference material (refactoring notes, gotchas, testing, flagged items). Phases 0-6 collapsed to a pointer at git history instead of ~500 lines of DONE evidence. Also: - Repointed every `multiplayer-next.md §N` code comment (N 1-6) across Game/scripts, Game/tools and Game/tests to MULTIPLAYER_SPEC.md, since those sections moved. Task-number references (`task N.N`, §7-11) correctly still point at multiplayer-next.md. - Updated CLAUDE.md's doc index and docs/TECH_STACK.md's spec-section citations to match. - TODO.md: added a "what's left to actually finish multiplayer (human-actionable)" checklist pulled from multiplayer-next.md §0 and docs/MATCHMAKING.md - things that need a person (hardware, a design decision, a Steam App ID, hands on a controller), not more agent code.
183 lines
6.4 KiB
GDScript
183 lines
6.4 KiB
GDScript
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()])
|