Pilot für eine generierte Figur: der Kohlenbeißer g01 kommt als Meshy-Ergebnis in den Port und ersetzt dort das Phase-4-Modell. Eine Figur genügt, um Pipeline, Bild und Leistung zu bewerten; Anatomie, Gesichter und Haare bleiben Künstler-Sculpts oder lizenzierte Packs (Vorgabe vom 1. Oktober). - Meshy-Pipeline unter tools/meshy/: API-Client (meshy.py, Ledger, Schlüssel nie geloggt) und Blender-Generator (build_figure.py) aus den unveränderten Downloads in assets-source/meshy/g01/. Export über den Manifest-Eintrag kohlenbeisser mit "replaces": "g01"; LuneUnitLibraryBuilder hängt die Figur zusätzlich unter der Godot-ID ein. - UV-Material AI_Kohlenbeisser (Familie authored): die Karten der Figur selbst, Lava-Glut aus der Albedo-Alpha (Meshy 7.1 ohne Emission-Map) mit _Glow 1,25. - Schatten ohne Normalen-Versatz (LuneShadowBias, _ShadowNormalScale 0): die UV-Naht-Kopien tragen geknickte Normalen; URP ApplyShadowBias zog helle Nähte durch den Schatten. Die Figur wirft nur mit Tiefen-Bias. - GPU-Batched-Skinning (PlayerSettings.meshDeformation, LuneProjectSetup): 35 Meshy-Kohlenbeißer kosteten mit CPU-Skinning rund 1 ms Haupt- und 1,7 ms Render-Thread je Bild. - LuneBenchmark: -luneBenchmarkRound und -luneBenchmarkArmy (Figuren-Stress). LuneAssetPreview: LUNE_ONLY für Vorher/Nachher, Trefferframe aus der Kontaktzeit des Manifests. FigureViewsTests (Explicit) für die vier Spielmomente der Figur. - Tests: EditMode 29/29, PlayMode 14/14 (6 Explicit), Verify 718/0, Player-Selbsttest 30/30, Kerntests 457/457, Fingerabdrücke unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). - Leistung (A/B im selben Fenster, 35x g01): Meshy -21 % Mid (109,7 zu 138,8) und -13 % Karte (91,7 zu 105,6) gegenüber Phase-4. GPU-Skinning hebt Mid um 7-27 % und entlastet Haupt- und Render-Thread. Standard-Partie ab Runde 21 ohne g01 im Bild: drei Varianten bis 6 %. - Doku: U5-VISUAL (U5.5), PORT-QA, PORT-STATUS, KNOWN-BEHAVIOR-DIFFERENCES; Bilder screenshots/u5.5/ und u5.5-gpu/, Rohdaten logs/u5.5/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
251 lines
12 KiB
C#
251 lines
12 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 <out.json>": 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).
|
|
/// U5.5: "-luneBenchmarkRound <n>" starts from another round, and "-luneBenchmarkArmy
|
|
/// <unit>" is a figure stress test: every seat whose Bund owns the unit fields only that
|
|
/// unit up to the supply cap (a fixture like those of the HUD view tests: public match state
|
|
/// and planning commands; the other seats keep their bot plans). The report lists the moving
|
|
/// units by definition at the start of the first window.
|
|
/// </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;
|
|
int _round = 21;
|
|
string _army;
|
|
int _fielded;
|
|
string _census = "";
|
|
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];
|
|
int roundAt = Array.IndexOf(args, "-luneBenchmarkRound");
|
|
if (roundAt >= 0 && roundAt + 1 < args.Length && int.TryParse(args[roundAt + 1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int round) && round > 0)
|
|
_round = round;
|
|
int armyAt = Array.IndexOf(args, "-luneBenchmarkArmy");
|
|
if (armyAt >= 0 && armyAt + 1 < args.Length)
|
|
_army = args[armyAt + 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 = _round;
|
|
Driver.ExtraGold = 4000;
|
|
Driver.Speed = 1f;
|
|
Driver.Restart();
|
|
if (!string.IsNullOrEmpty(_army))
|
|
_fielded = FieldArmy(Driver.Runner.Match, _army);
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The figure stress fixture: seats whose Bund owns the unit replace their planned army by
|
|
/// that unit, bought with planning commands up to the supply cap. Returns how many.
|
|
/// </summary>
|
|
static int FieldArmy(LuneMatch match, string typeId)
|
|
{
|
|
int total = 0;
|
|
foreach (Player player in match.Players)
|
|
{
|
|
Draft draft = player.Draft;
|
|
var planned = new List<RosterEntry>(draft.Roster);
|
|
int gold = draft.Gold;
|
|
draft.Roster.Clear();
|
|
draft.Gold = 100000;
|
|
int bought = 0;
|
|
while (bought < 40 && match.Submit(player.Slot, Command.Buy(typeId, player.Lane)).Ok)
|
|
bought++;
|
|
if (bought == 0)
|
|
draft.Roster.AddRange(planned);
|
|
draft.Gold = gold;
|
|
total += bought;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/// <summary>Moving units alive by definition, most first ("g01 24, w01 20, ...").</summary>
|
|
static string Census(LuneCombat combat)
|
|
{
|
|
var counts = new Dictionary<string, int>();
|
|
foreach (LuneUnit unit in combat.Units)
|
|
{
|
|
if (!unit.Alive || !unit.IsMobile())
|
|
continue;
|
|
counts.TryGetValue(unit.Def.Id, out int count);
|
|
counts[unit.Def.Id] = count + 1;
|
|
}
|
|
return string.Join(", ", counts.OrderByDescending(p => p.Value).ThenBy(p => p.Key, StringComparer.Ordinal).Select(p => p.Key + " " + p.Value));
|
|
}
|
|
|
|
void Begin(int index)
|
|
{
|
|
_index = index;
|
|
_elapsed = 0f;
|
|
if (index == 0 && Driver.Runner.Match.Combat != null)
|
|
_census = Census(Driver.Runner.Match.Combat);
|
|
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);
|
|
string army = string.IsNullOrEmpty(_army) ? "" : string.Format(CultureInfo.InvariantCulture, ", army {0} x{1}", _army, _fielded);
|
|
text.AppendFormat(CultureInfo.InvariantCulture, " \"match\": \"seed 9 from round {0}{1}{2}, speed 1x, measured after 10 s of combat\",\n", _round,
|
|
_round == 21 && army.Length == 0 ? " (fixture high_wave_21)" : "", army);
|
|
text.AppendFormat(" // moving units at the start of the first window: {0}\n", _census);
|
|
text.Append(" \"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();
|
|
}
|
|
}
|
|
}
|