Add a standalone Profiler module (S3.profiler.json, disabled by default) that hosts the in-game frame-time overlay previously owned by Physics Optimizer. The overlay now adapts to whichever modules are enabled: - Always shows: render + physics frame-time graph, timing report. - Physics Optimizer section (if enabled): LOD fast-path and auto-freeze quick-toggles with live stats, debug car count line. - Mesh LOD section (if enabled): total tracked cars, loco/freight split, per-LOD-level counts refreshed once per second. PhysicsOptimizerModule retains only the Harmony patches and CarDebugVisualizer; ShowOverlay/OverlayOpacity removed from PhysicsSettings. MeshLodInjector gains GetLodStats() and GetLodLevel() for the overlay. BBox material shader search now tries URP/Lit before Standard. /rpf overlay toggle redirected to ProfilerOverlayGUI.Instance.
290 lines
12 KiB
C#
290 lines
12 KiB
C#
using S3.Modules.MeshLod;
|
|
using S3.Modules.PhysicsOptimizer;
|
|
using UnityEngine;
|
|
|
|
namespace S3.Modules.Profiler;
|
|
|
|
public class ProfilerOverlayGUI : MonoBehaviour
|
|
{
|
|
public static ProfilerOverlayGUI? Instance { get; private set; }
|
|
public bool Visible = true;
|
|
public float Opacity = 0.85f;
|
|
|
|
Rect _windowRect = new(10f, 10f, 420f, 10f);
|
|
|
|
GUIStyle _blueLabel;
|
|
GUIStyle _greenLabel;
|
|
GUIStyle _yellowLabel;
|
|
GUIStyle _orangeLabel;
|
|
GUIStyle _dimLabel;
|
|
GUIStyle _fps60Label;
|
|
GUIStyle _fps30Label;
|
|
GUIStyle _onStyle;
|
|
GUIStyle _offStyle;
|
|
GUIStyle _windowStyle;
|
|
Texture2D _solidTex;
|
|
|
|
Texture2D _graphTex;
|
|
Color32[] _graphPixels;
|
|
const int TexW = 300;
|
|
const int TexH = 90;
|
|
|
|
static readonly Color32 ColBg = new(16, 16, 16, 220);
|
|
static readonly Color32 Col60 = new(210, 210, 210, 255);
|
|
static readonly Color32 Col30 = new(210, 210, 210, 255);
|
|
static readonly Color32 ColRender = new(50, 140, 215, 255);
|
|
static readonly Color32 ColFrame = new(70, 200, 70, 255);
|
|
static readonly Color32 ColTick = new(210, 185, 50, 255);
|
|
static readonly Color32 ColPosCars = new(220, 110, 30, 255);
|
|
|
|
static readonly int WindowId = "S3ProfilerOverlay".GetHashCode();
|
|
|
|
const float RefFps60Ms = 16.7f;
|
|
const float RefFps30Ms = 33.3f;
|
|
|
|
// Per-second stats cache
|
|
string _physLodStr = "";
|
|
string _physFreezeStr = "";
|
|
string _physDebugStr = "";
|
|
string _meshLodStr = "";
|
|
int _statsFrame;
|
|
|
|
void Awake()
|
|
{
|
|
Instance = this;
|
|
_graphTex = new Texture2D(TexW, TexH, TextureFormat.RGBA32, false)
|
|
{ filterMode = FilterMode.Point, hideFlags = HideFlags.HideAndDontSave };
|
|
_graphPixels = new Color32[TexW * TexH];
|
|
_solidTex = new Texture2D(1, 1, TextureFormat.RGBA32, false)
|
|
{ hideFlags = HideFlags.HideAndDontSave };
|
|
_solidTex.SetPixel(0, 0, Color.white);
|
|
_solidTex.Apply();
|
|
}
|
|
|
|
void LateUpdate() => PhysicsTimer.RecordRenderFrame(Time.unscaledDeltaTime * 1000f);
|
|
|
|
void OnDestroy()
|
|
{
|
|
if (_graphTex != null) Destroy(_graphTex);
|
|
if (_solidTex != null) Destroy(_solidTex);
|
|
Instance = null;
|
|
}
|
|
|
|
void OnGUI()
|
|
{
|
|
if (!Visible) return;
|
|
EnsureStyles();
|
|
Color prevBg = GUI.backgroundColor;
|
|
GUI.backgroundColor = new Color(0.063f, 0.063f, 0.063f, Opacity);
|
|
_windowRect = GUILayout.Window(WindowId, _windowRect, DrawWindow,
|
|
"Profiler", _windowStyle, GUILayout.MinWidth(420f));
|
|
GUI.backgroundColor = prevBg;
|
|
}
|
|
|
|
void DrawWindow(int _)
|
|
{
|
|
GUI.backgroundColor = Color.white;
|
|
|
|
// ── Frame-time report ─────────────────────────────────────────────────
|
|
GUILayout.Label(PhysicsTimer.GetReport());
|
|
|
|
Rect graphRect = GUILayoutUtility.GetRect(
|
|
GUIContent.none, GUIStyle.none,
|
|
GUILayout.Height(TexH), GUILayout.ExpandWidth(true));
|
|
|
|
if (Event.current.type == EventType.Repaint)
|
|
{
|
|
UpdateGraphTexture();
|
|
GUI.DrawTexture(graphRect, _graphTex, ScaleMode.StretchToFill);
|
|
|
|
float maxMs = Mathf.Max(PhysicsTimer.GetRingMax(), RefFps30Ms * 1.1f);
|
|
GUI.Label(new Rect(graphRect.xMax - 54f, LocalGaugeY(graphRect, maxMs, RefFps60Ms) - 9f, 52f, 18f), "60 fps", _fps60Label);
|
|
GUI.Label(new Rect(graphRect.xMax - 54f, LocalGaugeY(graphRect, maxMs, RefFps30Ms) - 9f, 52f, 18f), "30 fps", _fps30Label);
|
|
|
|
int prev = (PhysicsTimer.RingWrite - 1 + PhysicsTimer.RingSize) % PhysicsTimer.RingSize;
|
|
GUI.Label(new Rect(graphRect.x + 4f, graphRect.y + 2f, 340f, 20f),
|
|
$"render:{PhysicsTimer.RingRender[prev]:F2}ms phys:{PhysicsTimer.RingFrame[prev]:F2}ms tick:{PhysicsTimer.RingTick[prev]:F2}ms",
|
|
_dimLabel);
|
|
}
|
|
|
|
GUILayout.BeginHorizontal();
|
|
GUILayout.Label(" ■ Render", _blueLabel);
|
|
GUILayout.Label(" ■ FixedUpdate", _greenLabel);
|
|
GUILayout.Label(" ■ Tick()", _yellowLabel);
|
|
if (PhysicsTimer.HasPosCarsData)
|
|
GUILayout.Label(" ■ PosCars", _orangeLabel);
|
|
GUILayout.Label($" [{PhysicsTimer.RingSize} frames]", _dimLabel);
|
|
GUILayout.EndHorizontal();
|
|
|
|
// Refresh per-second stats cache
|
|
if (Time.frameCount != _statsFrame && Time.frameCount % 60 == 0)
|
|
{
|
|
_statsFrame = Time.frameCount;
|
|
RefreshStats();
|
|
}
|
|
|
|
// ── Physics Optimizer section ─────────────────────────────────────────
|
|
if (PhysicsOptimizerModule.Settings.enabled && ProfilerModule.Settings.showPhysicsSection)
|
|
{
|
|
GUILayout.Space(4f);
|
|
GUILayout.BeginHorizontal();
|
|
GUILayout.Label("Physics Optimizer", _dimLabel, GUILayout.Width(130f));
|
|
if (GUILayout.Button(ConsistLOD.Enabled ? "ON" : "OFF",
|
|
ConsistLOD.Enabled ? _onStyle : _offStyle, GUILayout.Width(34f)))
|
|
{
|
|
ConsistLOD.Enabled = !ConsistLOD.Enabled;
|
|
PhysicsOptimizerModule.Settings.OptimizerEnabled = ConsistLOD.Enabled;
|
|
PhysicsOptimizerModule.Persist();
|
|
}
|
|
GUILayout.Label(_physLodStr, _dimLabel);
|
|
GUILayout.EndHorizontal();
|
|
|
|
GUILayout.BeginHorizontal();
|
|
GUILayout.Label("Auto Freeze", _dimLabel, GUILayout.Width(130f));
|
|
if (GUILayout.Button(ConsistFreezer.AutoFreezeEnabled ? "ON" : "OFF",
|
|
ConsistFreezer.AutoFreezeEnabled ? _onStyle : _offStyle, GUILayout.Width(34f)))
|
|
{
|
|
ConsistFreezer.AutoFreezeEnabled = !ConsistFreezer.AutoFreezeEnabled;
|
|
PhysicsOptimizerModule.Settings.AutoFreezeEnabled = ConsistFreezer.AutoFreezeEnabled;
|
|
PhysicsOptimizerModule.Persist();
|
|
}
|
|
GUILayout.Label(_physFreezeStr, _dimLabel);
|
|
GUILayout.EndHorizontal();
|
|
|
|
if (_physDebugStr.Length > 0)
|
|
GUILayout.Label(_physDebugStr, _dimLabel);
|
|
}
|
|
|
|
// ── Mesh LOD section ──────────────────────────────────────────────────
|
|
if (MeshLodModule.Settings.enabled && ProfilerModule.Settings.showMeshLodSection)
|
|
{
|
|
GUILayout.Space(4f);
|
|
GUILayout.Label(_meshLodStr, _dimLabel);
|
|
}
|
|
|
|
GUI.DragWindow(new Rect(0f, 0f, _windowRect.width, 20f));
|
|
}
|
|
|
|
void RefreshStats()
|
|
{
|
|
bool physOn = PhysicsOptimizerModule.Settings.enabled;
|
|
|
|
if (physOn && ConsistLOD.Enabled)
|
|
{
|
|
int fast = ConsistLOD.LastFastPathCount;
|
|
int full = ConsistLOD.LastFullPathCount;
|
|
_physLodStr = $"fast:{fast}/{fast + full} full:{full}/{fast + full} dist:{ConsistLOD.DistanceThreshold:F0}m resync:1/{ConsistLOD.ResyncInterval}";
|
|
}
|
|
else
|
|
{
|
|
_physLodStr = "";
|
|
}
|
|
|
|
if (physOn)
|
|
{
|
|
_physFreezeStr = $"stopped:{ConsistFreezer.LastAtRestCars} dist/spd:{ConsistFreezer.LastDistanceCars}";
|
|
|
|
bool anyDbg = PhysicsOptimizerModule.Settings.DebugHighlightFrozen ||
|
|
PhysicsOptimizerModule.Settings.DebugHighlightFastPath ||
|
|
PhysicsOptimizerModule.Settings.DebugHighlightFullPath;
|
|
_physDebugStr = anyDbg
|
|
? $"dbg frozen:{ConsistFreezer.FrozenCars.Count} fast:{ConsistLOD.FastPathCars.Count} full:{ConsistLOD.FullPathCars.Count} tinted:{CarDebugVisualizer.Instance?.TintedCount ?? 0}"
|
|
: "";
|
|
}
|
|
else
|
|
{
|
|
_physFreezeStr = _physDebugStr = "";
|
|
}
|
|
|
|
if (MeshLodModule.Settings.enabled)
|
|
{
|
|
var (tot, locos, freight, l0, l1, l2, l3) = MeshLodInjector.GetLodStats();
|
|
_meshLodStr = tot == 0
|
|
? "Mesh LOD — no cars loaded yet"
|
|
: $"Mesh LOD — {tot} tracked ({locos}L / {freight}C) LOD0:{l0} LOD1:{l1} LOD2:{l2} LOD3:{l3}";
|
|
}
|
|
else
|
|
{
|
|
_meshLodStr = "";
|
|
}
|
|
}
|
|
|
|
// ── Graph rendering ───────────────────────────────────────────────────────
|
|
|
|
void UpdateGraphTexture()
|
|
{
|
|
float maxMs = Mathf.Max(PhysicsTimer.GetRingMax(), RefFps30Ms * 1.1f);
|
|
int write = PhysicsTimer.RingWrite;
|
|
int n = PhysicsTimer.RingSize;
|
|
|
|
for (int i = 0; i < _graphPixels.Length; i++)
|
|
_graphPixels[i] = ColBg;
|
|
|
|
DrawFilledArea(PhysicsTimer.RingRender, null, n, write, maxMs, ColRender);
|
|
DrawFilledArea(PhysicsTimer.RingFrame, PhysicsTimer.RingRender, n, write, maxMs, ColFrame);
|
|
DrawFilledArea(PhysicsTimer.RingTick, PhysicsTimer.RingRender, n, write, maxMs, ColTick);
|
|
if (PhysicsTimer.HasPosCarsData)
|
|
DrawFilledArea(PhysicsTimer.RingPosCars, PhysicsTimer.RingRender, n, write, maxMs, ColPosCars);
|
|
|
|
DrawHLine(maxMs, RefFps60Ms, Col60);
|
|
DrawHLine(maxMs, RefFps30Ms, Col30);
|
|
|
|
_graphTex.SetPixels32(_graphPixels);
|
|
_graphTex.Apply(false);
|
|
}
|
|
|
|
static float LocalGaugeY(Rect r, float maxMs, float ms) =>
|
|
r.yMax - Mathf.Clamp01(ms / maxMs) * r.height;
|
|
|
|
static int MsToRow(float ms, float maxMs) =>
|
|
Mathf.Clamp((int)(ms / maxMs * TexH), 0, TexH - 1);
|
|
|
|
void DrawHLine(float maxMs, float ms, Color32 color)
|
|
{
|
|
int row = MsToRow(ms, maxMs);
|
|
for (int x = 0; x < TexW; x++)
|
|
_graphPixels[row * TexW + x] = color;
|
|
}
|
|
|
|
void DrawFilledArea(float[] values, float[]? baselines, int n, int write, float maxMs, Color32 color)
|
|
{
|
|
for (int x = 0; x < TexW; x++)
|
|
{
|
|
int si = (write + (int)((float)x / TexW * n)) % n;
|
|
float bot = baselines != null ? baselines[si] : 0f;
|
|
int rowBot = MsToRow(bot, maxMs);
|
|
int rowTop = MsToRow(bot + values[si], maxMs);
|
|
int lo = Mathf.Min(rowBot, rowTop);
|
|
int hi = Mathf.Max(rowBot, rowTop);
|
|
for (int r = lo; r <= hi; r++)
|
|
_graphPixels[r * TexW + x] = color;
|
|
}
|
|
}
|
|
|
|
void EnsureStyles()
|
|
{
|
|
if (_blueLabel != null) return;
|
|
_blueLabel = new GUIStyle(GUI.skin.label) { normal = { textColor = new Color(0.2f, 0.55f, 0.85f) } };
|
|
_greenLabel = new GUIStyle(GUI.skin.label) { normal = { textColor = new Color(0.4f, 1f, 0.4f) } };
|
|
_yellowLabel = new GUIStyle(GUI.skin.label) { normal = { textColor = new Color(1f, 0.85f, 0.2f) } };
|
|
_orangeLabel = new GUIStyle(GUI.skin.label) { normal = { textColor = new Color(0.9f, 0.45f, 0.1f) } };
|
|
_dimLabel = new GUIStyle(GUI.skin.label) { normal = { textColor = new Color(0.55f, 0.55f, 0.55f) } };
|
|
_fps60Label = new GUIStyle(GUI.skin.label) { normal = { textColor = new Color(0.82f, 0.82f, 0.82f) } };
|
|
_fps30Label = new GUIStyle(GUI.skin.label) { normal = { textColor = new Color(0.82f, 0.82f, 0.82f) } };
|
|
_onStyle = new GUIStyle(GUI.skin.label)
|
|
{
|
|
normal = { textColor = new Color(0.4f, 1f, 0.4f) },
|
|
hover = { textColor = new Color(0.7f, 1f, 0.7f) },
|
|
};
|
|
_offStyle = new GUIStyle(GUI.skin.label)
|
|
{
|
|
normal = { textColor = new Color(1f, 0.35f, 0.3f) },
|
|
hover = { textColor = new Color(1f, 0.6f, 0.55f) },
|
|
};
|
|
_windowStyle = new GUIStyle(GUI.skin.window);
|
|
_windowStyle.normal.background = _solidTex;
|
|
_windowStyle.onNormal.background = _solidTex;
|
|
_windowStyle.focused.background = _solidTex;
|
|
_windowStyle.onFocused.background = _solidTex;
|
|
}
|
|
}
|