New in 0.1.2: - Debug color overlay: tint cars by physics state (yellow=frozen, cyan=fast-path, magenta=full-accuracy), individual toggles per state - Exclude locomotives from Physics Optimizer and Auto Freeze - Debug stats line in overlay showing live counts per state - Resync quality 1/16 option added Defaults tuned for new installs: - Resync quality: 1/4 -> 1/8 - Speed threshold: ~0.7 mph -> 0.2 mph - Overlay opacity: 100% -> 75% Rename: Physics Overhaul -> Physics Optimizer throughout
286 lines
13 KiB
C#
286 lines
13 KiB
C#
using UnityEngine;
|
||
|
||
namespace RailroaderPhysicsOverhaul;
|
||
|
||
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 = "RPFOverlay".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.
|
||
Main.Settings.OptimizerEnabled = ConsistLOD.Enabled;
|
||
Main.Settings.Save(Main.ModEntry);
|
||
}
|
||
|
||
// 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 = Main.Settings.DebugHighlightFrozen ||
|
||
Main.Settings.DebugHighlightFastPath ||
|
||
Main.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;
|
||
Main.Settings.AutoFreezeEnabled = ConsistFreezer.AutoFreezeEnabled;
|
||
Main.Settings.Save(Main.ModEntry);
|
||
}
|
||
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;
|
||
}
|
||
}
|