Profiler: new unified overlay module; absorb PhysicsOverlayGUI
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.
This commit is contained in:
parent
1b4ab97be5
commit
4853015eff
11 changed files with 495 additions and 335 deletions
|
|
@ -30,6 +30,7 @@ public static class Main
|
||||||
// constructor. Order here is display order in the settings panel.
|
// constructor. Order here is display order in the settings panel.
|
||||||
_registry.Register(new Modules.PhysicsOptimizer.PhysicsOptimizerModule());
|
_registry.Register(new Modules.PhysicsOptimizer.PhysicsOptimizerModule());
|
||||||
_registry.Register(new Modules.MeshLod.MeshLodModule());
|
_registry.Register(new Modules.MeshLod.MeshLodModule());
|
||||||
|
_registry.Register(new Modules.Profiler.ProfilerModule());
|
||||||
_registry.Register(new Modules.Popout.PopoutModule());
|
_registry.Register(new Modules.Popout.PopoutModule());
|
||||||
|
|
||||||
_registry.EnableConfigured();
|
_registry.EnableConfigured();
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,55 @@ static class MeshLodInjector
|
||||||
return (total, locos, total - locos);
|
return (total, locos, total - locos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LOD level distribution — called at most once per second by the Profiler overlay.
|
||||||
|
public static (int total, int locos, int freight, int atLod0, int atLod1, int atLod2, int atLod3) GetLodStats()
|
||||||
|
{
|
||||||
|
int total = 0, locos = 0, freight = 0;
|
||||||
|
int atLod0 = 0, atLod1 = 0, atLod2 = 0, atLod3 = 0;
|
||||||
|
foreach (var wr in _trackedCars)
|
||||||
|
{
|
||||||
|
if (!wr.TryGetTarget(out Car? car) || car == null || car.BodyTransform == null) continue;
|
||||||
|
total++;
|
||||||
|
bool isLoco = car is BaseLocomotive;
|
||||||
|
if (isLoco) locos++; else freight++;
|
||||||
|
switch (GetLodLevel(car))
|
||||||
|
{
|
||||||
|
case 0: atLod0++; break;
|
||||||
|
case 1: atLod1++; break;
|
||||||
|
case 2: atLod2++; break;
|
||||||
|
default: atLod3++; break; // 3 = proxy box, -1 = no group (treated same)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (total, locos, freight, atLod0, atLod1, atLod2, atLod3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect current LOD level by reading which renderer sets are active on the LODGroup.
|
||||||
|
// lods[0] = all renderers; lods[1] = lod1Keep subset; lods[2] = lod2Keep subset;
|
||||||
|
// lods[3] = bbox. Counting enabled renderers in lods[0] vs the subset sizes
|
||||||
|
// avoids storing extra per-car state.
|
||||||
|
static int GetLodLevel(Car car)
|
||||||
|
{
|
||||||
|
var lg = car.BodyTransform?.GetComponent<LODGroup>();
|
||||||
|
if (lg == null) return -1;
|
||||||
|
LOD[] lods = lg.GetLODs();
|
||||||
|
if (lods.Length < 4) return -1; // not our LODGroup
|
||||||
|
|
||||||
|
// LOD3: proxy box renderer is enabled
|
||||||
|
if (lods[3].renderers.Length > 0 && lods[3].renderers[0] is { } bbox && bbox.enabled)
|
||||||
|
return 3;
|
||||||
|
|
||||||
|
int enabled = 0;
|
||||||
|
foreach (var r in lods[0].renderers)
|
||||||
|
if (r != null && r.enabled) enabled++;
|
||||||
|
|
||||||
|
int total = lods[0].renderers.Length;
|
||||||
|
int lod2Keep = lods[2].renderers.Length;
|
||||||
|
|
||||||
|
if (enabled == total) return 0; // all renderers on: full detail
|
||||||
|
if (enabled == 0) return 3; // culled entirely: treat as LOD3
|
||||||
|
return enabled > lod2Keep ? 1 : 2; // more than structural count: LOD1, else LOD2
|
||||||
|
}
|
||||||
|
|
||||||
// Called from the UI whenever any setting changes.
|
// Called from the UI whenever any setting changes.
|
||||||
// Triggers RefreshAll after RefreshDebounce seconds with no further changes,
|
// Triggers RefreshAll after RefreshDebounce seconds with no further changes,
|
||||||
// so slider drags don't spam scene rebuilds.
|
// so slider drags don't spam scene rebuilds.
|
||||||
|
|
@ -267,18 +316,23 @@ static class MeshLodInjector
|
||||||
|
|
||||||
// ── Proxy box mesh ────────────────────────────────────────────────────────
|
// ── Proxy box mesh ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// One matte Standard material shared across all proxy boxes; tinted to the
|
// One matte material shared across all proxy boxes; tinted to the car body's
|
||||||
// car body's albedo so it looks approximately correct under any lighting.
|
// albedo so it looks approximately correct under any lighting / post-processing.
|
||||||
|
// Tries URP first since Railroader uses the Universal Render Pipeline.
|
||||||
static Material GetOrCreateBBoxMat(Material? src)
|
static Material GetOrCreateBBoxMat(Material? src)
|
||||||
{
|
{
|
||||||
if (_bboxMat != null) return _bboxMat;
|
if (_bboxMat != null) return _bboxMat;
|
||||||
var shader = Shader.Find("Standard") ?? Shader.Find("Legacy Shaders/Diffuse");
|
var shader = Shader.Find("Universal Render Pipeline/Lit")
|
||||||
|
?? Shader.Find("Universal Render Pipeline/Simple Lit")
|
||||||
|
?? Shader.Find("Standard")
|
||||||
|
?? Shader.Find("Legacy Shaders/Diffuse");
|
||||||
_bboxMat = shader != null ? new Material(shader) : new Material(src);
|
_bboxMat = shader != null ? new Material(shader) : new Material(src);
|
||||||
_bboxMat.color = src != null && src.HasProperty("_Color")
|
_bboxMat.color = src != null && src.HasProperty("_Color")
|
||||||
? src.color
|
? src.color
|
||||||
: new Color(0.45f, 0.45f, 0.45f);
|
: new Color(0.45f, 0.45f, 0.45f);
|
||||||
if (_bboxMat.HasProperty("_Metallic")) _bboxMat.SetFloat("_Metallic", 0f);
|
if (_bboxMat.HasProperty("_Metallic")) _bboxMat.SetFloat("_Metallic", 0f);
|
||||||
if (_bboxMat.HasProperty("_Glossiness")) _bboxMat.SetFloat("_Glossiness", 0.1f);
|
if (_bboxMat.HasProperty("_Glossiness")) _bboxMat.SetFloat("_Glossiness", 0.1f); // built-in
|
||||||
|
if (_bboxMat.HasProperty("_Smoothness")) _bboxMat.SetFloat("_Smoothness", 0.1f); // URP
|
||||||
return _bboxMat;
|
return _bboxMat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,8 +105,8 @@ static class RpfCommands
|
||||||
|
|
||||||
static string ToggleOverlay()
|
static string ToggleOverlay()
|
||||||
{
|
{
|
||||||
var gui = PhysicsOverlayGUI.Instance;
|
var gui = Profiler.ProfilerOverlayGUI.Instance;
|
||||||
if (gui == null) return "Overlay not initialized.";
|
if (gui == null) return "Overlay not initialized (enable the Profiler module).";
|
||||||
gui.Visible = !gui.Visible;
|
gui.Visible = !gui.Visible;
|
||||||
return $"Overlay {(gui.Visible ? "shown" : "hidden")}.";
|
return $"Overlay {(gui.Visible ? "shown" : "hidden")}.";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ public sealed class PhysicsOptimizerModule : IModule
|
||||||
public static PhysicsSettings Settings { get; private set; } = new();
|
public static PhysicsSettings Settings { get; private set; } = new();
|
||||||
|
|
||||||
private static Harmony? _harmony;
|
private static Harmony? _harmony;
|
||||||
|
private static GameObject? _hostGo;
|
||||||
|
|
||||||
// Attribute-annotated patch classes the module owns. PosCarsTimerPatch is NOT
|
// Attribute-annotated patch classes the module owns. PosCarsTimerPatch is NOT
|
||||||
// here — it has no [HarmonyPatch] attribute and is applied dynamically by
|
// here — it has no [HarmonyPatch] attribute and is applied dynamically by
|
||||||
|
|
@ -40,7 +41,7 @@ public sealed class PhysicsOptimizerModule : IModule
|
||||||
public string DisplayName => "Physics Optimizer";
|
public string DisplayName => "Physics Optimizer";
|
||||||
public string Description =>
|
public string Description =>
|
||||||
"Cuts CPU time spent on train physics (LOD fast-path + auto-freeze for far/slow consists). " +
|
"Cuts CPU time spent on train physics (LOD fast-path + auto-freeze for far/slow consists). " +
|
||||||
"Includes a profiler overlay and debug car tinting. Console: /rpf";
|
"Includes debug car tinting. Enable the Profiler module for the overlay. Console: /rpf";
|
||||||
|
|
||||||
public bool Enabled
|
public bool Enabled
|
||||||
{
|
{
|
||||||
|
|
@ -57,21 +58,17 @@ public sealed class PhysicsOptimizerModule : IModule
|
||||||
_harmony.CreateClassProcessor(t).Patch();
|
_harmony.CreateClassProcessor(t).Patch();
|
||||||
PhysicsTimer.TryPatchPositionCars(_harmony, Main.ModEntry.Logger);
|
PhysicsTimer.TryPatchPositionCars(_harmony, Main.ModEntry.Logger);
|
||||||
|
|
||||||
var go = new GameObject("S3.PhysicsOptimizer.Host");
|
_hostGo = new GameObject("[S3] PhysicsOptimizerHost");
|
||||||
UnityEngine.Object.DontDestroyOnLoad(go);
|
UnityEngine.Object.DontDestroyOnLoad(_hostGo);
|
||||||
PhysicsOverlayGUI overlay = go.AddComponent<PhysicsOverlayGUI>();
|
_hostGo.AddComponent<CarDebugVisualizer>();
|
||||||
overlay.Visible = Settings.ShowOverlay;
|
|
||||||
overlay.Opacity = Settings.OverlayOpacity;
|
|
||||||
go.AddComponent<CarDebugVisualizer>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnDisable()
|
public void OnDisable()
|
||||||
{
|
{
|
||||||
// Reserved for live toggling. Today modules apply on next launch, so this
|
|
||||||
// is not called; when it is, unpatch via _harmony.UnpatchAll("S3.physics")
|
|
||||||
// and destroy the host GameObject.
|
|
||||||
_harmony?.UnpatchAll("S3.physics");
|
_harmony?.UnpatchAll("S3.physics");
|
||||||
_harmony = null;
|
_harmony = null;
|
||||||
|
if (_hostGo != null) UnityEngine.Object.Destroy(_hostGo);
|
||||||
|
_hostGo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveSettings() => Persist();
|
public void SaveSettings() => Persist();
|
||||||
|
|
|
||||||
|
|
@ -1,286 +0,0 @@
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace S3.Modules.PhysicsOptimizer;
|
|
||||||
|
|
||||||
public class PhysicsOverlayGUI : MonoBehaviour
|
|
||||||
{
|
|
||||||
public static PhysicsOverlayGUI Instance { get; private set; }
|
|
||||||
public bool Visible = true;
|
|
||||||
public float Opacity = 1.0f;
|
|
||||||
|
|
||||||
Rect _windowRect = new(10f, 10f, 420f, 10f);
|
|
||||||
GUIStyle _blueLabel;
|
|
||||||
GUIStyle _greenLabel;
|
|
||||||
GUIStyle _yellowLabel;
|
|
||||||
GUIStyle _orangeLabel;
|
|
||||||
GUIStyle _dimLabel;
|
|
||||||
GUIStyle _fps60Label;
|
|
||||||
GUIStyle _fps30Label;
|
|
||||||
GUIStyle _onStyle; // bold green, no button background
|
|
||||||
GUIStyle _offStyle; // bold red, no button background
|
|
||||||
|
|
||||||
string _lodStatsStr = "";
|
|
||||||
string _freezeStatsStr = "";
|
|
||||||
string _debugStatsStr = "";
|
|
||||||
int _lodStatsFrame;
|
|
||||||
|
|
||||||
GUIStyle _windowStyle;
|
|
||||||
Texture2D _solidTex; // 1×1 white — lets GUI.backgroundColor.a control opacity 0–100%
|
|
||||||
|
|
||||||
// Graph rendered as a texture — avoids all GL coordinate-space issues.
|
|
||||||
// Unity textures are Y-up (row 0 = bottom pixel), IMGUI is Y-down,
|
|
||||||
// but GUI.DrawTexture just stretches the texture into the target rect,
|
|
||||||
// so we flip Y in our write formula and the result renders correctly.
|
|
||||||
Texture2D _graphTex;
|
|
||||||
Color32[] _graphPixels;
|
|
||||||
const int TexW = 300; // matches RingSize exactly — 1 pixel column per sample
|
|
||||||
const int TexH = 90;
|
|
||||||
|
|
||||||
static readonly Color32 ColBg = new(16, 16, 16, 220);
|
|
||||||
static readonly Color32 Col60 = new(210, 210, 210, 255); // 60fps reference line
|
|
||||||
static readonly Color32 Col30 = new(210, 210, 210, 255); // 30fps reference line
|
|
||||||
static readonly Color32 ColRender = new(50, 140, 215, 255); // render frame time (blue)
|
|
||||||
static readonly Color32 ColFrame = new(70, 200, 70, 255); // FixedUpdate total (green)
|
|
||||||
static readonly Color32 ColTick = new(210, 185, 50, 255); // Tick() only (yellow)
|
|
||||||
static readonly Color32 ColPosCars = new(220, 110, 30, 255); // PositionCars (orange)
|
|
||||||
|
|
||||||
static readonly int WindowId = "S3PhysicsOverlay".GetHashCode();
|
|
||||||
|
|
||||||
const float RefFps60Ms = 16.7f;
|
|
||||||
const float RefFps30Ms = 33.3f;
|
|
||||||
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
// Record render frame time each rendered frame (not each physics tick).
|
|
||||||
// Time.unscaledDeltaTime = wall-clock ms since last render frame.
|
|
||||||
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();
|
|
||||||
// Tint the solid-white window background to dark gray at the chosen opacity.
|
|
||||||
// Using a custom style (solid texture) means Opacity=1 → fully opaque, not
|
|
||||||
// capped by whatever alpha the default IMGUI skin has baked in.
|
|
||||||
Color prevBg = GUI.backgroundColor;
|
|
||||||
GUI.backgroundColor = new Color(0.063f, 0.063f, 0.063f, Opacity);
|
|
||||||
_windowRect = GUILayout.Window(WindowId, _windowRect, DrawWindow,
|
|
||||||
"Physics Profiler", _windowStyle, GUILayout.MinWidth(420f));
|
|
||||||
GUI.backgroundColor = prevBg;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DrawWindow(int _)
|
|
||||||
{
|
|
||||||
// The window background was already drawn with the opacity tint — restore
|
|
||||||
// backgroundColor so buttons and labels inside use their normal skin colors.
|
|
||||||
GUI.backgroundColor = Color.white;
|
|
||||||
GUILayout.Label(PhysicsTimer.GetReport());
|
|
||||||
|
|
||||||
// Reserve space for graph in window-local coords.
|
|
||||||
Rect localRect = GUILayoutUtility.GetRect(
|
|
||||||
GUIContent.none, GUIStyle.none,
|
|
||||||
GUILayout.Height(TexH), GUILayout.ExpandWidth(true));
|
|
||||||
|
|
||||||
if (Event.current.type == EventType.Repaint)
|
|
||||||
{
|
|
||||||
UpdateGraphTexture();
|
|
||||||
// GUI.DrawTexture uses window-local coords — no coordinate conversion needed.
|
|
||||||
GUI.DrawTexture(localRect, _graphTex, ScaleMode.StretchToFill);
|
|
||||||
|
|
||||||
// 10% headroom above the 30fps line so it's never squashed against the top pixel row.
|
|
||||||
float maxMs = Mathf.Max(PhysicsTimer.GetRingMax(), RefFps30Ms * 1.1f);
|
|
||||||
|
|
||||||
// Reference-line labels: centered vertically on the line, anchored to right edge.
|
|
||||||
float y60Gui = LocalGaugeY(localRect, maxMs, RefFps60Ms);
|
|
||||||
GUI.Label(new Rect(localRect.xMax - 54f, y60Gui - 9f, 52f, 18f), "60 fps", _fps60Label);
|
|
||||||
float y30Gui = LocalGaugeY(localRect, maxMs, RefFps30Ms);
|
|
||||||
GUI.Label(new Rect(localRect.xMax - 54f, y30Gui - 9f, 52f, 18f), "30 fps", _fps30Label);
|
|
||||||
|
|
||||||
// Current sample readout (top-left of graph).
|
|
||||||
int prev = (PhysicsTimer.RingWrite - 1 + PhysicsTimer.RingSize) % PhysicsTimer.RingSize;
|
|
||||||
GUI.Label(new Rect(localRect.x + 4f, localRect.y + 2f, 340f, 20f),
|
|
||||||
$"render:{PhysicsTimer.RingRender[prev]:F2}ms phys:{PhysicsTimer.RingFrame[prev]:F2}ms tick:{PhysicsTimer.RingTick[prev]:F2}ms",
|
|
||||||
_dimLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legend row
|
|
||||||
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();
|
|
||||||
|
|
||||||
// Physics Optimizer row: plain-text toggle (no button background), then slow-updating stats
|
|
||||||
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;
|
|
||||||
// Persist toggle state so it survives a game restart.
|
|
||||||
PhysicsOptimizerModule.Settings.OptimizerEnabled = ConsistLOD.Enabled;
|
|
||||||
PhysicsOptimizerModule.Persist();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stats refresh once per second — readable, not flickering
|
|
||||||
if (Time.frameCount != _lodStatsFrame && Time.frameCount % 60 == 0)
|
|
||||||
{
|
|
||||||
_lodStatsFrame = Time.frameCount;
|
|
||||||
if (ConsistLOD.Enabled)
|
|
||||||
{
|
|
||||||
int fast = ConsistLOD.LastFastPathCount;
|
|
||||||
int full = ConsistLOD.LastFullPathCount;
|
|
||||||
int total = fast + full;
|
|
||||||
_lodStatsStr = $"fast:{fast}/{total} full:{full}/{total} dist:{ConsistLOD.DistanceThreshold:F0}m resync:1/{ConsistLOD.ResyncInterval}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_lodStatsStr = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
int atRest = ConsistFreezer.LastAtRestCars;
|
|
||||||
int byDist = ConsistFreezer.LastDistanceCars;
|
|
||||||
_freezeStatsStr = $"stopped:{atRest} dist/spd:{byDist}";
|
|
||||||
|
|
||||||
bool anyDbg = PhysicsOptimizerModule.Settings.DebugHighlightFrozen ||
|
|
||||||
PhysicsOptimizerModule.Settings.DebugHighlightFastPath ||
|
|
||||||
PhysicsOptimizerModule.Settings.DebugHighlightFullPath;
|
|
||||||
_debugStatsStr = anyDbg
|
|
||||||
? $"dbg — frozen:{ConsistFreezer.FrozenCars.Count} fast:{ConsistLOD.FastPathCars.Count} full:{ConsistLOD.FullPathCars.Count} tinted:{CarDebugVisualizer.Instance?.TintedCount ?? 0}"
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
GUILayout.Label(_lodStatsStr, _dimLabel);
|
|
||||||
GUILayout.EndHorizontal();
|
|
||||||
|
|
||||||
if (_debugStatsStr.Length > 0)
|
|
||||||
GUILayout.Label(_debugStatsStr, _dimLabel);
|
|
||||||
|
|
||||||
// Auto-freeze row — shows cars skipping the Verlet tick each physics step
|
|
||||||
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(_freezeStatsStr, _dimLabel);
|
|
||||||
GUILayout.EndHorizontal();
|
|
||||||
|
|
||||||
GUI.DragWindow(new Rect(0f, 0f, _windowRect.width, 20f));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Y in IMGUI window-local space: top of rect = maxMs, bottom = 0ms.
|
|
||||||
static float LocalGaugeY(Rect r, float maxMs, float ms) =>
|
|
||||||
r.yMax - Mathf.Clamp01(ms / maxMs) * r.height;
|
|
||||||
|
|
||||||
void UpdateGraphTexture()
|
|
||||||
{
|
|
||||||
float maxMs = Mathf.Max(PhysicsTimer.GetRingMax(), RefFps30Ms * 1.1f);
|
|
||||||
int write = PhysicsTimer.RingWrite;
|
|
||||||
int n = PhysicsTimer.RingSize;
|
|
||||||
float[] render = PhysicsTimer.RingRender;
|
|
||||||
float[] frame = PhysicsTimer.RingFrame;
|
|
||||||
float[] tick = PhysicsTimer.RingTick;
|
|
||||||
float[] posCars = PhysicsTimer.RingPosCars;
|
|
||||||
|
|
||||||
// Background fill.
|
|
||||||
for (int i = 0; i < _graphPixels.Length; i++)
|
|
||||||
_graphPixels[i] = ColBg;
|
|
||||||
|
|
||||||
// Stacked filled areas — drawn back-to-front so later layers paint over earlier ones.
|
|
||||||
//
|
|
||||||
// Layer 1 (bottom): render frame time.
|
|
||||||
DrawFilledArea(render, null, n, write, maxMs, ColRender);
|
|
||||||
// Layer 2: FixedUpdate total, stacked on top of render.
|
|
||||||
DrawFilledArea(frame, render, n, write, maxMs, ColFrame);
|
|
||||||
// Layer 3: Tick(), painted over the lower part of the FixedUpdate band (same baseline).
|
|
||||||
DrawFilledArea(tick, render, n, write, maxMs, ColTick);
|
|
||||||
// Layer 4: PosCars, painted over the lower part of the Tick band (same baseline).
|
|
||||||
if (PhysicsTimer.HasPosCarsData)
|
|
||||||
DrawFilledArea(posCars, render, n, write, maxMs, ColPosCars);
|
|
||||||
|
|
||||||
// Reference lines on top of all data.
|
|
||||||
DrawHLine(maxMs, RefFps60Ms, Col60);
|
|
||||||
DrawHLine(maxMs, RefFps30Ms, Col30);
|
|
||||||
|
|
||||||
_graphTex.SetPixels32(_graphPixels);
|
|
||||||
_graphTex.Apply(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pixel row for a given ms value: row 0 = bottom (0ms), row TexH-1 = top (maxMs).
|
|
||||||
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);
|
|
||||||
int offset = row * TexW;
|
|
||||||
for (int x = 0; x < TexW; x++)
|
|
||||||
_graphPixels[offset + x] = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fills a solid area from [baselines[i]] to [baselines[i] + values[i]] for each sample column.
|
|
||||||
// baselines == null means 0 (fill from the bottom of the chart).
|
|
||||||
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) } };
|
|
||||||
|
|
||||||
// Custom window style: solid white background texture so GUI.backgroundColor.a
|
|
||||||
// gives true 0–100% opacity rather than being capped by the skin's baked-in alpha.
|
|
||||||
_windowStyle = new GUIStyle(GUI.skin.window);
|
|
||||||
_windowStyle.normal.background = _solidTex;
|
|
||||||
_windowStyle.onNormal.background = _solidTex;
|
|
||||||
_windowStyle.focused.background = _solidTex;
|
|
||||||
_windowStyle.onFocused.background = _solidTex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -20,10 +20,6 @@ public class PhysicsSettings
|
||||||
public float AutoFreezeDistance = 200f; // meters
|
public float AutoFreezeDistance = 200f; // meters
|
||||||
public float AutoFreezeSpeedThreshold = 0.09f; // m/s (~0.2 mph)
|
public float AutoFreezeSpeedThreshold = 0.09f; // m/s (~0.2 mph)
|
||||||
|
|
||||||
// Profiler overlay
|
|
||||||
public bool ShowOverlay = true;
|
|
||||||
public float OverlayOpacity = 0.75f;
|
|
||||||
|
|
||||||
// Locomotive exclusions
|
// Locomotive exclusions
|
||||||
public bool ExcludeLocosFromLOD = true;
|
public bool ExcludeLocosFromLOD = true;
|
||||||
public bool ExcludeLocosFromFreeze = true;
|
public bool ExcludeLocosFromFreeze = true;
|
||||||
|
|
|
||||||
|
|
@ -230,35 +230,6 @@ static class PhysicsSettingsUI
|
||||||
|
|
||||||
GUILayout.Space(12f);
|
GUILayout.Space(12f);
|
||||||
|
|
||||||
// ── Profiler Overlay ──────────────────────────────────────────────────────
|
|
||||||
GUILayout.Label("<b>Profiler Overlay</b>", GUILayout.ExpandWidth(false));
|
|
||||||
GUILayout.Space(4f);
|
|
||||||
|
|
||||||
bool newOverlay = GUILayout.Toggle(s.ShowOverlay, " Show in-game profiler overlay");
|
|
||||||
if (newOverlay != s.ShowOverlay)
|
|
||||||
{
|
|
||||||
s.ShowOverlay = newOverlay;
|
|
||||||
if (PhysicsOverlayGUI.Instance != null)
|
|
||||||
PhysicsOverlayGUI.Instance.Visible = newOverlay;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
GUILayout.Space(4f);
|
|
||||||
|
|
||||||
GUILayout.BeginHorizontal();
|
|
||||||
GUILayout.Label($"Opacity: {s.OverlayOpacity * 100f:F0}%", GUILayout.Width(110f));
|
|
||||||
float newOpacity = GUILayout.HorizontalSlider(s.OverlayOpacity, 0.1f, 1.0f, GUILayout.Width(200f));
|
|
||||||
GUILayout.EndHorizontal();
|
|
||||||
if (Mathf.Abs(newOpacity - s.OverlayOpacity) > 0.01f)
|
|
||||||
{
|
|
||||||
s.OverlayOpacity = newOpacity;
|
|
||||||
if (PhysicsOverlayGUI.Instance != null)
|
|
||||||
PhysicsOverlayGUI.Instance.Opacity = s.OverlayOpacity;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
GUILayout.Space(12f);
|
|
||||||
|
|
||||||
// ── Debug Visualization ───────────────────────────────────────────────────
|
// ── Debug Visualization ───────────────────────────────────────────────────
|
||||||
GUILayout.Label("<b>Debug Visualization</b>", GUILayout.ExpandWidth(false));
|
GUILayout.Label("<b>Debug Visualization</b>", GUILayout.ExpandWidth(false));
|
||||||
GUILayout.Space(4f);
|
GUILayout.Space(4f);
|
||||||
|
|
|
||||||
48
src/Modules/Profiler/ProfilerModule.cs
Normal file
48
src/Modules/Profiler/ProfilerModule.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
using S3.Core;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace S3.Modules.Profiler;
|
||||||
|
|
||||||
|
public sealed class ProfilerModule : IModule
|
||||||
|
{
|
||||||
|
private const string SettingsFile = "S3.profiler.json";
|
||||||
|
|
||||||
|
public static ProfilerSettings Settings { get; private set; } = new();
|
||||||
|
|
||||||
|
private static GameObject? _hostGo;
|
||||||
|
|
||||||
|
public ProfilerModule() => Settings = SettingsStore.Load<ProfilerSettings>(SettingsFile);
|
||||||
|
|
||||||
|
public string Id => "profiler";
|
||||||
|
public string DisplayName => "Profiler";
|
||||||
|
public string Description =>
|
||||||
|
"In-game performance overlay: render and physics frame-time graph, " +
|
||||||
|
"Physics Optimizer quick-toggles, and Mesh LOD car counts. " +
|
||||||
|
"Sections appear only when the corresponding module is enabled.";
|
||||||
|
|
||||||
|
public bool Enabled
|
||||||
|
{
|
||||||
|
get => Settings.enabled;
|
||||||
|
set => Settings.enabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnEnable()
|
||||||
|
{
|
||||||
|
_hostGo = new GameObject("[S3] ProfilerHost");
|
||||||
|
UnityEngine.Object.DontDestroyOnLoad(_hostGo);
|
||||||
|
var overlay = _hostGo.AddComponent<ProfilerOverlayGUI>();
|
||||||
|
overlay.Visible = Settings.visible;
|
||||||
|
overlay.Opacity = Settings.opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDisable()
|
||||||
|
{
|
||||||
|
if (_hostGo != null) UnityEngine.Object.Destroy(_hostGo);
|
||||||
|
_hostGo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveSettings() => Persist();
|
||||||
|
internal static void Persist() => SettingsStore.Save(SettingsFile, Settings);
|
||||||
|
|
||||||
|
public void DrawSettings() => ProfilerSettingsUI.Draw();
|
||||||
|
}
|
||||||
290
src/Modules/Profiler/ProfilerOverlayGUI.cs
Normal file
290
src/Modules/Profiler/ProfilerOverlayGUI.cs
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/Modules/Profiler/ProfilerSettings.cs
Normal file
13
src/Modules/Profiler/ProfilerSettings.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace S3.Modules.Profiler;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ProfilerSettings
|
||||||
|
{
|
||||||
|
public bool enabled = false;
|
||||||
|
public bool visible = true;
|
||||||
|
public float opacity = 0.85f;
|
||||||
|
public bool showPhysicsSection = true;
|
||||||
|
public bool showMeshLodSection = true;
|
||||||
|
}
|
||||||
76
src/Modules/Profiler/ProfilerSettingsUI.cs
Normal file
76
src/Modules/Profiler/ProfilerSettingsUI.cs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
using S3.Modules.MeshLod;
|
||||||
|
using S3.Modules.PhysicsOptimizer;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace S3.Modules.Profiler;
|
||||||
|
|
||||||
|
static class ProfilerSettingsUI
|
||||||
|
{
|
||||||
|
public static void Draw()
|
||||||
|
{
|
||||||
|
ProfilerSettings s = ProfilerModule.Settings;
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
GUILayout.BeginVertical();
|
||||||
|
|
||||||
|
GUILayout.Label("<b>Profiler</b> — in-game frame-time overlay with module readouts");
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
GUILayout.Label(
|
||||||
|
" Always shows the render and physics frame-time graph.\n" +
|
||||||
|
" Physics Optimizer and Mesh LOD sections appear when those modules are enabled.",
|
||||||
|
GUI.skin.label);
|
||||||
|
|
||||||
|
// ── Overlay visibility ────────────────────────────────────────────────
|
||||||
|
GUILayout.Space(10f);
|
||||||
|
GUILayout.Label("<b>Display</b>");
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
|
||||||
|
bool newVisible = GUILayout.Toggle(s.visible, " Show overlay in-game");
|
||||||
|
if (newVisible != s.visible)
|
||||||
|
{
|
||||||
|
s.visible = newVisible;
|
||||||
|
if (ProfilerOverlayGUI.Instance != null)
|
||||||
|
ProfilerOverlayGUI.Instance.Visible = newVisible;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
GUILayout.Label($"Opacity: {s.opacity * 100f:F0}%", GUILayout.Width(110f));
|
||||||
|
float newOpacity = GUILayout.HorizontalSlider(s.opacity, 0.1f, 1.0f, GUILayout.Width(200f));
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
if (Mathf.Abs(newOpacity - s.opacity) > 0.01f)
|
||||||
|
{
|
||||||
|
s.opacity = newOpacity;
|
||||||
|
if (ProfilerOverlayGUI.Instance != null)
|
||||||
|
ProfilerOverlayGUI.Instance.Opacity = newOpacity;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Section visibility ────────────────────────────────────────────────
|
||||||
|
GUILayout.Space(10f);
|
||||||
|
GUILayout.Label("<b>Sections</b>");
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
|
||||||
|
bool physAvail = PhysicsOptimizerModule.Settings.enabled;
|
||||||
|
GUI.enabled = physAvail;
|
||||||
|
bool newPhys = GUILayout.Toggle(s.showPhysicsSection && physAvail,
|
||||||
|
" Physics Optimizer" + (physAvail ? "" : " (module not enabled)"));
|
||||||
|
GUI.enabled = true;
|
||||||
|
if (newPhys != s.showPhysicsSection && physAvail)
|
||||||
|
{ s.showPhysicsSection = newPhys; changed = true; }
|
||||||
|
|
||||||
|
bool meshAvail = MeshLodModule.Settings.enabled;
|
||||||
|
GUI.enabled = meshAvail;
|
||||||
|
bool newMesh = GUILayout.Toggle(s.showMeshLodSection && meshAvail,
|
||||||
|
" Mesh LOD" + (meshAvail ? "" : " (module not enabled)"));
|
||||||
|
GUI.enabled = true;
|
||||||
|
if (newMesh != s.showMeshLodSection && meshAvail)
|
||||||
|
{ s.showMeshLodSection = newMesh; changed = true; }
|
||||||
|
|
||||||
|
GUILayout.EndVertical();
|
||||||
|
|
||||||
|
if (changed) ProfilerModule.Persist();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue