From c95a59c771a7a18a34bf73af61af9ec813678c83 Mon Sep 17 00:00:00 2001 From: Jahnisse Date: Wed, 3 Jun 2026 16:10:28 +0200 Subject: [PATCH 1/3] Add Framerate Counter (capped at 60) --- Assets/_Content/Scenes/TitleScreen.unity | 233 ++++++++++++++++++ .../Scripts/Core/Utils/PerformanceOverlay.cs | 117 +++++++++ .../Core/Utils/PerformanceOverlay.cs.meta | 2 + 3 files changed, 352 insertions(+) create mode 100644 Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs create mode 100644 Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs.meta diff --git a/Assets/_Content/Scenes/TitleScreen.unity b/Assets/_Content/Scenes/TitleScreen.unity index a368ee10..3ad53f2f 100644 --- a/Assets/_Content/Scenes/TitleScreen.unity +++ b/Assets/_Content/Scenes/TitleScreen.unity @@ -241,6 +241,108 @@ RectTransform: m_CorrespondingSourceObject: {fileID: 5856627074613423090, guid: 10b589291a51b4a4f858c6c79ab6772a, type: 3} m_PrefabInstance: {fileID: 23563224} m_PrefabAsset: {fileID: 0} +--- !u!1 &431578943 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 431578944} + - component: {fileID: 431578947} + - component: {fileID: 431578946} + - component: {fileID: 431578945} + m_Layer: 0 + m_Name: Canvas_FPS + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &431578944 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 431578943} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1195733056} + m_Father: {fileID: 1741102573} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!114 &431578945 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 431578943} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.GraphicRaycaster + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &431578946 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 431578943} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.CanvasScaler + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &431578947 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 431578943} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 0 + m_UpdateRectTransformForStandalone: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 --- !u!1001 &522028756 PrefabInstance: m_ObjectHideFlags: 0 @@ -1211,6 +1313,135 @@ Transform: - {fileID: 1411514848} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1195136603 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1195136605} + - component: {fileID: 1195136604} + m_Layer: 0 + m_Name: PerfManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1195136604 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1195136603} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 508ae74360f7e5249b100f7873b182f1, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::PerformanceOverlay + displayText: {fileID: 1195733054} + sampleSize: 60 + updateInterval: 0.1 + pipelineLatencyFrames: 2 + goodFPS: 60 + warningFPS: 30 +--- !u!4 &1195136605 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1195136603} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1195733053 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1195733056} + - component: {fileID: 1195733055} + - component: {fileID: 1195733054} + m_Layer: 0 + m_Name: PerfText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1195733054 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1195733053} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Text + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 12800000, guid: 8c54d6a69a6e0604b9408d95d425ffc0, type: 3} + m_FontSize: 25 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 8 + m_MaxSize: 102 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: New Text +--- !u!222 &1195733055 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1195733053} + m_CullTransparentMesh: 1 +--- !u!224 &1195733056 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1195733053} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 431578944} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 80, y: -15} + m_SizeDelta: {x: 160, y: 30} + m_Pivot: {x: 0.5, y: 0.5} --- !u!1 &1291952569 GameObject: m_ObjectHideFlags: 0 @@ -2002,6 +2233,7 @@ Transform: m_Children: - {fileID: 630642127} - {fileID: 1505757215} + - {fileID: 431578944} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1757053106 stripped @@ -2956,3 +3188,4 @@ SceneRoots: - {fileID: 1530196629} - {fileID: 1741102573} - {fileID: 1034573946} + - {fileID: 1195136605} diff --git a/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs new file mode 100644 index 00000000..4caa2bf1 --- /dev/null +++ b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs @@ -0,0 +1,117 @@ +using UnityEngine; +using UnityEngine.UI; +using System.Collections.Generic; + +/// +/// PerformanceOverlay — displays FPS, frame time, and estimated input lag. +/// Singleton: only one instance will ever exist. Safe to place in every scene — +/// duplicates are destroyed automatically. Persists across scene loads. +/// Requires: a Canvas with a Text (or TMP_Text) component assigned in the Inspector. +/// +public class PerformanceOverlay : MonoBehaviour +{ + // ── singleton ──────────────────────────────────────────────────────────── + public static PerformanceOverlay Instance { get; private set; } + + void Awake() + { + if (Instance != null && Instance != this) + { + // A valid instance already exists — destroy this duplicate. + Destroy(gameObject); + return; + } + + Instance = this; + DontDestroyOnLoad(gameObject); + } + + [Header("UI Reference")] + [Tooltip("Assign a UI Text or TextMeshPro Text component here.")] + public Text displayText; // swap to TMP_Text if using TextMeshPro + + [Header("FPS Settings")] + [Tooltip("How many frames to average over for a smooth FPS reading.")] + public int sampleSize = 60; + + [Tooltip("How often (seconds) the display refreshes.")] + public float updateInterval = 0.1f; + + [Header("Input Lag Settings")] + [Tooltip("Frames of render pipeline latency added on top of frame time.\n" + + "Typical values: 0–1 for Forward, 1–2 for Deferred / URP with buffering.")] + public int pipelineLatencyFrames = 1; + + [Header("Thresholds (for colour coding)")] + public int goodFPS = 60; + public int warningFPS = 30; + + // ── internals ──────────────────────────────────────────────────────────── + private readonly Queue _frameTimes = new Queue(); + private float _accumulatedTime; + private float _timeSinceLastUpdate; + + // Colour tags (works with both Text and TMP richtext) + private const string GREEN = ""; + private const string YELLOW = ""; + private const string RED = ""; + private const string END = ""; + + // ── lifecycle ──────────────────────────────────────────────────────────── + void Update() + { + float dt = Time.unscaledDeltaTime; // unscaled so pausing doesn't skew readings + + // Rolling window + _frameTimes.Enqueue(dt); + _accumulatedTime += dt; + if (_frameTimes.Count > sampleSize) + _accumulatedTime -= _frameTimes.Dequeue(); + + _timeSinceLastUpdate += dt; + if (_timeSinceLastUpdate >= updateInterval) + { + _timeSinceLastUpdate = 0f; + RefreshDisplay(); + } + } + + // ── display logic ──────────────────────────────────────────────────────── + void RefreshDisplay() + { + if (displayText == null) return; + + // ── FPS ────────────────────────────────────────────────────────────── + float avgFrameTime = _accumulatedTime / _frameTimes.Count; // seconds + float fps = 1f / avgFrameTime; + float frameTimeMs = avgFrameTime * 1000f; + + // ── Input Lag estimate ─────────────────────────────────────────────── + // Input lag ≈ time from physical input to pixel on screen. + // Best estimate without hardware: frame time + pipeline latency frames. + // + // inputLag ≈ frameTime + (pipelineLatencyFrames × frameTime) + // = frameTime × (1 + pipelineLatencyFrames) + // + // Add 8 ms as a rough USB / Bluetooth HID polling floor. + float hidPollMs = 8f; // ~1 frame @ 125 Hz polling; 1 ms @ 1000 Hz + float inputLagMs = frameTimeMs * (1 + pipelineLatencyFrames) + hidPollMs; + + // ── colour coding ──────────────────────────────────────────────────── + string fpsColour = fps >= goodFPS ? GREEN + : fps >= warningFPS ? YELLOW + : RED; + + // ── format ─────────────────────────────────────────────────────────── + displayText.text = + $"{fpsColour}{fps,5:F0} FPS{END}\n" + + $"Frame {frameTimeMs,6:F2} ms\n" + + $"Lag ≈ {inputLagMs,6:F2} ms"; + } + + // ── public helpers ─────────────────────────────────────────────────────── + + /// Toggle the overlay on / off (e.g. bind to F3). + public void Toggle() => displayText.gameObject.SetActive( + !displayText.gameObject.activeSelf); +} diff --git a/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs.meta b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs.meta new file mode 100644 index 00000000..449c60d4 --- /dev/null +++ b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 508ae74360f7e5249b100f7873b182f1 \ No newline at end of file From 1385d6a954af4a4e161d0638bf34bdace2dd3c43 Mon Sep 17 00:00:00 2001 From: Jahnisse Date: Wed, 3 Jun 2026 16:22:04 +0200 Subject: [PATCH 2/3] Fix: FPS counter --- Assets/_Content/Scenes/TitleScreen.unity | 20 +- .../Scripts/Core/Utils/PerformanceOverlay.cs | 213 +++++++++--------- 2 files changed, 118 insertions(+), 115 deletions(-) diff --git a/Assets/_Content/Scenes/TitleScreen.unity b/Assets/_Content/Scenes/TitleScreen.unity index 3ad53f2f..3c660922 100644 --- a/Assets/_Content/Scenes/TitleScreen.unity +++ b/Assets/_Content/Scenes/TitleScreen.unity @@ -273,7 +273,7 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1195733056} - m_Father: {fileID: 1741102573} + m_Father: {fileID: 1195136605} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} @@ -1342,12 +1342,12 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 508ae74360f7e5249b100f7873b182f1, type: 3} m_Name: m_EditorClassIdentifier: Assembly-CSharp::PerformanceOverlay - displayText: {fileID: 1195733054} - sampleSize: 60 - updateInterval: 0.1 - pipelineLatencyFrames: 2 - goodFPS: 60 - warningFPS: 30 + _displayText: {fileID: 1195733054} + _sampleSize: 60 + _updateInterval: 0.1 + _pipelineLatencyFrames: 1 + _goodFps: 60 + _warningFps: 30 --- !u!4 &1195136605 Transform: m_ObjectHideFlags: 0 @@ -1356,11 +1356,12 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1195136603} serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 - m_Children: [] + m_Children: + - {fileID: 431578944} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1195733053 @@ -2233,7 +2234,6 @@ Transform: m_Children: - {fileID: 630642127} - {fileID: 1505757215} - - {fileID: 431578944} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1757053106 stripped diff --git a/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs index 4caa2bf1..079a712f 100644 --- a/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs +++ b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs @@ -2,116 +2,119 @@ using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; -/// -/// PerformanceOverlay — displays FPS, frame time, and estimated input lag. -/// Singleton: only one instance will ever exist. Safe to place in every scene — -/// duplicates are destroyed automatically. Persists across scene loads. -/// Requires: a Canvas with a Text (or TMP_Text) component assigned in the Inspector. -/// -public class PerformanceOverlay : MonoBehaviour +namespace OctoCore.UI { - // ── singleton ──────────────────────────────────────────────────────────── - public static PerformanceOverlay Instance { get; private set; } - - void Awake() + /// + /// Displays FPS, frame time, and estimated input lag. + /// Singleton that persists across scene loads. Safe to place in every scene — duplicates are destroyed automatically. + /// Requires a Canvas Text component assigned in the Inspector. + /// + public class PerformanceOverlay : MonoBehaviour { - if (Instance != null && Instance != this) + #region Constants and Static Fields + + public static PerformanceOverlay Instance { get; private set; } + + private const string GREEN = ""; + private const string YELLOW = ""; + private const string RED = ""; + private const string END = ""; + + #endregion + + #region Serialized Fields + + [Header("UI Reference")] + [SerializeField] private Text _displayText; + + [Header("FPS Settings")] + [Tooltip("Number of frames to average for a smooth reading.")] + [SerializeField] private int _sampleSize = 60; + + [Tooltip("How often (seconds) the display refreshes.")] + [SerializeField] private float _updateInterval = 0.1f; + + [Header("Input Lag Settings")] + [Tooltip("Render pipeline latency in frames. 1 for Built-in/URP, 2 for HDRP.")] + [SerializeField] private int _pipelineLatencyFrames = 1; + + [Header("Thresholds")] + [SerializeField] private int _goodFps = 60; + [SerializeField] private int _warningFps = 30; + + #endregion + + #region Private Instance Fields + + private readonly Queue _frameTimes = new Queue(); + private float _accumulatedTime; + private float _timeSinceLastUpdate; + + #endregion + + #region Unity Lifecycle Methods + + private void Awake() { - // A valid instance already exists — destroy this duplicate. - Destroy(gameObject); - return; + if (Instance != null && Instance != this) + { + Destroy(gameObject); + return; + } + + Instance = this; + DontDestroyOnLoad(gameObject); } - Instance = this; - DontDestroyOnLoad(gameObject); - } - - [Header("UI Reference")] - [Tooltip("Assign a UI Text or TextMeshPro Text component here.")] - public Text displayText; // swap to TMP_Text if using TextMeshPro - - [Header("FPS Settings")] - [Tooltip("How many frames to average over for a smooth FPS reading.")] - public int sampleSize = 60; - - [Tooltip("How often (seconds) the display refreshes.")] - public float updateInterval = 0.1f; - - [Header("Input Lag Settings")] - [Tooltip("Frames of render pipeline latency added on top of frame time.\n" + - "Typical values: 0–1 for Forward, 1–2 for Deferred / URP with buffering.")] - public int pipelineLatencyFrames = 1; - - [Header("Thresholds (for colour coding)")] - public int goodFPS = 60; - public int warningFPS = 30; - - // ── internals ──────────────────────────────────────────────────────────── - private readonly Queue _frameTimes = new Queue(); - private float _accumulatedTime; - private float _timeSinceLastUpdate; - - // Colour tags (works with both Text and TMP richtext) - private const string GREEN = ""; - private const string YELLOW = ""; - private const string RED = ""; - private const string END = ""; - - // ── lifecycle ──────────────────────────────────────────────────────────── - void Update() - { - float dt = Time.unscaledDeltaTime; // unscaled so pausing doesn't skew readings - - // Rolling window - _frameTimes.Enqueue(dt); - _accumulatedTime += dt; - if (_frameTimes.Count > sampleSize) - _accumulatedTime -= _frameTimes.Dequeue(); - - _timeSinceLastUpdate += dt; - if (_timeSinceLastUpdate >= updateInterval) + private void Update() { - _timeSinceLastUpdate = 0f; - RefreshDisplay(); + float dt = Time.unscaledDeltaTime; + + _frameTimes.Enqueue(dt); + _accumulatedTime += dt; + if (_frameTimes.Count > _sampleSize) + _accumulatedTime -= _frameTimes.Dequeue(); + + _timeSinceLastUpdate += dt; + if (_timeSinceLastUpdate >= _updateInterval) + { + _timeSinceLastUpdate = 0f; + RefreshDisplay(); + } } + + #endregion + + #region Public Methods + + /// Toggles the overlay on / off. + public void Toggle() => _displayText.gameObject.SetActive(!_displayText.gameObject.activeSelf); + + #endregion + + #region Private Methods + + private void RefreshDisplay() + { + if (_displayText == null) return; + + float avgFrameTime = _accumulatedTime / _frameTimes.Count; + float fps = 1f / avgFrameTime; + float frameTimeMs = avgFrameTime * 1000f; + + // HID polling floor (~8 ms for 125 Hz USB; ~1 ms for 1000 Hz) + float inputLagMs = frameTimeMs * (1 + _pipelineLatencyFrames) + 8f; + + string fpsColour = fps >= _goodFps ? GREEN + : fps >= _warningFps ? YELLOW + : RED; + + _displayText.text = + $"{fpsColour}{fps,5:F0} FPS{END}\n" + + $"Frame {frameTimeMs,6:F2} ms\n" + + $"Lag ≈ {inputLagMs,6:F2} ms"; + } + + #endregion } - - // ── display logic ──────────────────────────────────────────────────────── - void RefreshDisplay() - { - if (displayText == null) return; - - // ── FPS ────────────────────────────────────────────────────────────── - float avgFrameTime = _accumulatedTime / _frameTimes.Count; // seconds - float fps = 1f / avgFrameTime; - float frameTimeMs = avgFrameTime * 1000f; - - // ── Input Lag estimate ─────────────────────────────────────────────── - // Input lag ≈ time from physical input to pixel on screen. - // Best estimate without hardware: frame time + pipeline latency frames. - // - // inputLag ≈ frameTime + (pipelineLatencyFrames × frameTime) - // = frameTime × (1 + pipelineLatencyFrames) - // - // Add 8 ms as a rough USB / Bluetooth HID polling floor. - float hidPollMs = 8f; // ~1 frame @ 125 Hz polling; 1 ms @ 1000 Hz - float inputLagMs = frameTimeMs * (1 + pipelineLatencyFrames) + hidPollMs; - - // ── colour coding ──────────────────────────────────────────────────── - string fpsColour = fps >= goodFPS ? GREEN - : fps >= warningFPS ? YELLOW - : RED; - - // ── format ─────────────────────────────────────────────────────────── - displayText.text = - $"{fpsColour}{fps,5:F0} FPS{END}\n" + - $"Frame {frameTimeMs,6:F2} ms\n" + - $"Lag ≈ {inputLagMs,6:F2} ms"; - } - - // ── public helpers ─────────────────────────────────────────────────────── - - /// Toggle the overlay on / off (e.g. bind to F3). - public void Toggle() => displayText.gameObject.SetActive( - !displayText.gameObject.activeSelf); -} +} \ No newline at end of file From 08755ce2cdf9e61b1479fa6fe96597c6ecec47bf Mon Sep 17 00:00:00 2001 From: Jahnisse Date: Wed, 3 Jun 2026 16:38:20 +0200 Subject: [PATCH 3/3] Fix : Uncapped FPS ; Removed : Estimated Input lag --- .../Scripts/Core/Utils/PerformanceOverlay.cs | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs index 079a712f..e6e85584 100644 --- a/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs +++ b/Assets/_Content/Scripts/Core/Utils/PerformanceOverlay.cs @@ -4,21 +4,13 @@ using System.Collections.Generic; namespace OctoCore.UI { - /// - /// Displays FPS, frame time, and estimated input lag. - /// Singleton that persists across scene loads. Safe to place in every scene — duplicates are destroyed automatically. - /// Requires a Canvas Text component assigned in the Inspector. - /// public class PerformanceOverlay : MonoBehaviour { #region Constants and Static Fields public static PerformanceOverlay Instance { get; private set; } - private const string GREEN = ""; - private const string YELLOW = ""; - private const string RED = ""; - private const string END = ""; + private const string GREEN = "", YELLOW = "", RED = "", END = ""; #endregion @@ -31,13 +23,9 @@ namespace OctoCore.UI [Tooltip("Number of frames to average for a smooth reading.")] [SerializeField] private int _sampleSize = 60; - [Tooltip("How often (seconds) the display refreshes.")] + [Tooltip("How often, in seconds, the display refreshes.")] [SerializeField] private float _updateInterval = 0.1f; - [Header("Input Lag Settings")] - [Tooltip("Render pipeline latency in frames. 1 for Built-in/URP, 2 for HDRP.")] - [SerializeField] private int _pipelineLatencyFrames = 1; - [Header("Thresholds")] [SerializeField] private int _goodFps = 60; [SerializeField] private int _warningFps = 30; @@ -64,18 +52,31 @@ namespace OctoCore.UI Instance = this; DontDestroyOnLoad(gameObject); + + if (_displayText == null) + _displayText = GetComponent(); + + ConfigureText(); + } + + private void OnValidate() + { + _sampleSize = Mathf.Max(1, _sampleSize); + _updateInterval = Mathf.Max(0.02f, _updateInterval); } private void Update() { - float dt = Time.unscaledDeltaTime; + float deltaTime = Time.unscaledDeltaTime; + + _frameTimes.Enqueue(deltaTime); + _accumulatedTime += deltaTime; - _frameTimes.Enqueue(dt); - _accumulatedTime += dt; if (_frameTimes.Count > _sampleSize) _accumulatedTime -= _frameTimes.Dequeue(); - _timeSinceLastUpdate += dt; + _timeSinceLastUpdate += deltaTime; + if (_timeSinceLastUpdate >= _updateInterval) { _timeSinceLastUpdate = 0f; @@ -85,36 +86,37 @@ namespace OctoCore.UI #endregion - #region Public Methods - - /// Toggles the overlay on / off. - public void Toggle() => _displayText.gameObject.SetActive(!_displayText.gameObject.activeSelf); - - #endregion - #region Private Methods + private void ConfigureText() + { + if (_displayText == null) return; + + _displayText.supportRichText = true; + _displayText.horizontalOverflow = HorizontalWrapMode.Overflow; + _displayText.verticalOverflow = VerticalWrapMode.Overflow; + _displayText.alignment = TextAnchor.UpperLeft; + } + private void RefreshDisplay() { if (_displayText == null) return; + if (_frameTimes.Count == 0) return; float avgFrameTime = _accumulatedTime / _frameTimes.Count; + if (avgFrameTime <= 0f) return; + float fps = 1f / avgFrameTime; float frameTimeMs = avgFrameTime * 1000f; - // HID polling floor (~8 ms for 125 Hz USB; ~1 ms for 1000 Hz) - float inputLagMs = frameTimeMs * (1 + _pipelineLatencyFrames) + 8f; - string fpsColour = fps >= _goodFps ? GREEN : fps >= _warningFps ? YELLOW : RED; _displayText.text = - $"{fpsColour}{fps,5:F0} FPS{END}\n" + - $"Frame {frameTimeMs,6:F2} ms\n" + - $"Lag ≈ {inputLagMs,6:F2} ms"; + $"{fpsColour}{fps:F0} FPS{END}\n" + + $"Frame {frameTimeMs:F2} ms"; } - #endregion } } \ No newline at end of file