champions-of-luneraria/Assets/Luneraria/View/LuneBenchmark.cs
Martin Stickelbroeck 66867f3332 U3D Abnahme: Prüfung, Feld um das HUD, Bilder, Leistung
Das G3-HUD ist abgenommen; der Meilenstein U3 ist damit fertig.

- Prüfung (Port von verify_g3, Teile A–H plus I): 718 Prüfungen ohne
  Fehler. Teil I prüft die Feld-Darstellung um das HUD (U3-Zugabe aus
  dem Motivvergleich): Aufstellungsflächen, Grotteneingang, ruhender
  Boss, Namensschilder, Eigentümer-Ring, Ancient-Balken.
- Feld um das HUD: Namensschilder für Helden (mit Stufe), Angriffs-
  champions und den eingesetzten Boss („für Blau“), Eigentümer-Ring,
  Ancient-Balken bei Schaden, ruhende Schlundkrone auf dem Podest,
  eigener Grotteneingang, Aufstellungsflächen als eigene Objekte.
- Schriften korrigiert: Die Referenz deklariert Cantarell 560/700–760
  und Cinzel 700, wendet die Gewichte aber nie an (variation_opentype
  mit dem String-Schlüssel „wght“ wird von Godot ignoriert). G3 zeigt
  Regular 400, der Port ebenso — Cinzel-Regular statt Bold, keine
  Kontur zur Aufdickung. Laufweite aus spacing_glyph umgerechnet;
  dunkler Saum als Schatten statt TextCore-Kontur.
- Werkzeuge: HudViewsTests (14 G3-Motive), HudPlaytest (Spieltest über
  die echte Oberfläche per Befehlsdateien), ui_ms_per_frame im
  LuneBenchmark. Layout folgt den echten Panelhöhen, Kamera startet wie
  Godot _ready auf der eigenen Lane.
- Tests: EditMode 23/23, PlayMode 13/13, Kerntests 402/402, Verify
  718/0. Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A,
  7B8E43AEFC0D60C5).
- Bilder: 14 Motive bei 1920×1080, Auflösungen 1152×720 bis 2560×1440,
  Prüf-Schlüsselbilder, Spieltest-Screenshots.
- Leistung (macOS-Player mit HUD, 116 Einheiten): 169 fps Übersicht,
  218 fps Mid nah; HUD-Skripte 0,090 bzw. 0,078 ms je Bild.
- Dokumentation: U3-UI (U3D, Schriftbefund), PORT-QA (U3), PORT-STATUS
  (U3 fertig, U4 nächster Schritt), KNOWN-BEHAVIOR-DIFFERENCES.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:55:06 +02:00

189 lines
8.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Luneraria.Core;
using UnityEngine;
namespace Luneraria.View
{
/// <summary>
/// Frame-time benchmark of a player build (U2D). Active only with the command-line argument
/// "-luneBenchmark &lt;out.json&gt;": runs the match of seed 9 from round 21 (the golden fixture
/// high_wave_21, the most units) at normal speed, VSync off, and measures two windows once the
/// armies meet: the whole map (20 s) and a close view of the Mid lane (10 s). Then it quits.
/// Since U3 the scene carries the HUD (watching seat 1 of the bot match); its own scripts'
/// cost per frame comes through UiMs (layout and drawing of the panel count in the threads).
/// </summary>
[DefaultExecutionOrder(-100)]
public sealed class LuneBenchmark : MonoBehaviour
{
public LuneMatchDriver Driver;
public LuneMatchView View;
public LuneCameraRig Rig;
sealed class Window
{
public string Label;
public float Seconds;
public float Zoom;
public Vec2 Focus;
public readonly List<double> FrameMs = new List<double>();
public double Simulation;
public double Views;
public double Ui;
public int Units;
public int GcStart;
public int GcEnd;
public int SlowFrames;
public int SlowWithStep;
public readonly List<double> MainThreadMs = new List<double>();
public readonly List<double> RenderThreadMs = new List<double>();
public readonly List<double> GpuMs = new List<double>();
public readonly List<double> PresentWaitMs = new List<double>();
}
readonly FrameTiming[] _timings = new FrameTiming[1];
string _out;
readonly List<Window> _windows = new List<Window>();
int _index = -1;
float _elapsed;
void Awake()
{
string[] args = Environment.GetCommandLineArgs();
int at = Array.IndexOf(args, "-luneBenchmark");
if (at < 0 || at + 1 >= args.Length)
{
enabled = false;
return;
}
_out = args[at + 1];
Running = true;
// The benchmark runs unattended; it must not pause when its window loses focus.
Application.runInBackground = true;
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = -1;
_windows.Add(new Window { Label = "overview (zoom 64)", Seconds = 20f, Zoom = 64f, Focus = new Vec2(0, 0) });
_windows.Add(new Window { Label = "mid lane close (zoom 22)", Seconds = 10f, Zoom = 22f, Focus = new Vec2(0, 0) });
}
/// <summary>True in a benchmark run: the game controller keeps the bot match (no human seat).</summary>
public static bool Running { get; private set; }
/// <summary>Main-thread cost of the HUD scripts in the last frame (set by the game controller, U3).</summary>
public static Func<double> UiMs;
void Start()
{
Driver.HumanSlot = -1;
Driver.Seed = 9;
Driver.StartRound = 21;
Driver.ExtraGold = 4000;
Driver.Speed = 1f;
Driver.Restart();
Rig.Interactive = false;
Rig.Overview(true);
}
void Update()
{
LuneMatch match = Driver.Runner.Match;
if (_index < 0)
{
if (Driver.Runner.Current == LuneMatchRunner.Stage.Combat && match.Combat.Elapsed >= 10.0)
Begin(0);
return;
}
if (_index >= _windows.Count)
return;
Window window = _windows[_index];
double ms = Time.unscaledDeltaTime * 1000.0;
window.FrameMs.Add(ms);
// Where the time goes (needs "Frame Timing Stats" in the player settings).
FrameTimingManager.CaptureFrameTimings();
if (FrameTimingManager.GetLatestTimings(1, _timings) > 0)
{
window.MainThreadMs.Add(_timings[0].cpuMainThreadFrameTime);
window.RenderThreadMs.Add(_timings[0].cpuRenderThreadFrameTime);
window.GpuMs.Add(_timings[0].gpuFrameTime);
window.PresentWaitMs.Add(_timings[0].cpuMainThreadPresentWaitTime);
}
// Frames above 1/60 s, and how many of them also ran a simulation step.
if (ms > 1000.0 / 60.0)
{
window.SlowFrames++;
if (Driver.Runner.StepsThisFrame > 0)
window.SlowWithStep++;
}
window.Simulation += Driver.LastAdvanceMs;
window.Views += View.LastSyncMs;
window.Ui += UiMs != null ? UiMs() : 0.0;
if (match.Combat != null && window.FrameMs.Count % 30 == 1)
window.Units = Math.Max(window.Units, CountAlive(match.Combat));
_elapsed += Time.unscaledDeltaTime;
if (_elapsed >= window.Seconds)
{
window.GcEnd = GC.CollectionCount(0);
if (_index + 1 < _windows.Count)
Begin(_index + 1);
else
Finish();
}
}
static string Describe(List<double> values)
{
if (values.Count == 0)
return "n/a";
double[] sorted = values.OrderBy(v => v).ToArray();
return string.Format(CultureInfo.InvariantCulture, "avg {0:0.00} / median {1:0.00} / p95 {2:0.00} ms", sorted.Average(), sorted[sorted.Length / 2], sorted[(int)(sorted.Length * .95)]);
}
static int CountAlive(LuneCombat combat)
{
int count = 0;
foreach (LuneUnit unit in combat.Units)
if (unit.Alive && unit.IsMobile())
count++;
return count;
}
void Begin(int index)
{
_index = index;
_elapsed = 0f;
Window window = _windows[index];
window.GcStart = GC.CollectionCount(0);
Rig.LookAt(window.Focus, window.Zoom, true);
}
void Finish()
{
_index = _windows.Count;
var text = new System.Text.StringBuilder("{\n");
text.AppendFormat(" \"build\": \"{0} player, {1}, {2}\",\n", Application.platform, Application.unityVersion, Debug.isDebugBuild ? "development" : "release");
text.AppendFormat(" \"device\": \"{0}; {1}; {2}\",\n", SystemInfo.deviceModel, SystemInfo.processorType, SystemInfo.graphicsDeviceName);
text.AppendFormat(" \"graphics_api\": \"{0}\",\n \"resolution\": \"{1}x{2}\",\n \"vsync\": {3},\n", SystemInfo.graphicsDeviceType, Screen.width, Screen.height, QualitySettings.vSyncCount);
text.Append(" \"match\": \"seed 9 from round 21 (fixture high_wave_21), speed 1x, measured after 10 s of combat\",\n \"windows\": [\n");
for (int i = 0; i < _windows.Count; i++)
{
Window window = _windows[i];
double[] sorted = window.FrameMs.OrderBy(v => v).ToArray();
double total = window.FrameMs.Sum();
text.AppendFormat(CultureInfo.InvariantCulture,
" {{\"view\": \"{0}\", \"frames\": {1}, \"fps\": {2:0.0}, \"frame_ms_avg\": {3:0.00}, \"frame_ms_median\": {4:0.00}, \"frame_ms_p95\": {5:0.00}, \"frame_ms_p99\": {6:0.00}, \"frame_ms_max\": {7:0.00}, \"frames_over_16_7_ms\": {8}, \"of_them_with_simulation_step\": {9}, \"gc_collections\": {10}, \"simulation_ms_per_frame\": {11:0.000}, \"views_ms_per_frame\": {12:0.000}, \"ui_ms_per_frame\": {15:0.000}, \"units_alive_max\": {13}}}{14}\n",
window.Label, sorted.Length, sorted.Length / (total / 1000.0), total / sorted.Length, sorted[sorted.Length / 2], sorted[(int)(sorted.Length * .95)], sorted[(int)(sorted.Length * .99)], sorted[sorted.Length - 1],
window.SlowFrames, window.SlowWithStep, window.GcEnd - window.GcStart, window.Simulation / sorted.Length, window.Views / sorted.Length, window.Units, i < _windows.Count - 1 ? "," : "", window.Ui / sorted.Length);
text.AppendFormat(CultureInfo.InvariantCulture, " // timings {0}: main thread {1}, render thread {2}, gpu {3}, present wait {4}\n", window.Label,
Describe(window.MainThreadMs), Describe(window.RenderThreadMs), Describe(window.GpuMs), Describe(window.PresentWaitMs));
}
text.Append(" ]\n}\n");
Directory.CreateDirectory(Path.GetDirectoryName(_out));
File.WriteAllText(_out, text.ToString());
Application.Quit();
}
}
}