using System; using System.Text; using System.Threading; using ADOFAIRenderer.Core; using TMPro; using UnityEngine; using UnityEngine.UI; namespace ADOFAIRenderer.UI; /// /// On-screen overlay shown while a render is running: a statistics panel in the top-right corner and a level progress /// bar near the bottom of the screen. /// /// Created by RenderManager.StartFFmpegThread on a GameObject kept across scene loads, and reused for later /// renders. The overlay is drawn on the game screen only; it is not part of the encoded video frames. /// rebuilds the text every frame on the Unity main thread. /// /// internal class DebugMonitorUI : MonoBehaviour { // --- Progress Bar UI Configuration Constants --- // Sizes and positions are canvas units at the 1920x1080 reference resolution of the CanvasScaler. /// Width of the progress bar; the fill width is BarWidth * progress. private const float BarWidth = 800f; /// Height of the progress bar. private const float BarHeight = 28f; /// Horizontal offset of the bar center from the screen center (0 = centered). private const float BarPosX = 0f; /// Vertical offset of the bar center from the screen center; -420 is 120 units above the bottom edge (540 - 420). private const float BarPosY = -420f; // Y position relative to screen center (Reference: 1920x1080) /// Font size of the percentage text drawn on the bar. private const int BarTextFontSize = 16; // Increased font size for visibility /// Unfilled bar color: opaque near-black with a slight blue tint. private static readonly Color BarBackgroundColor = new(0.08f, 0.08f, 0.1f, 1.0f); /// Filled part color: opaque cyan (#00CCFF). private static readonly Color BarFillColor = new(0.0f, 0.8f, 1.0f, 1.0f); // Sleek cyan/blue /// Percentage text color: opaque white. private static readonly Color BarTextColor = new(1.0f, 1.0f, 1.0f, 1.0f); // --- Progress Bar Outline Configuration Constants --- /// Color of the uGUI drawn around the bar background. private static readonly Color BarOutlineColor = Color.white; // Contrasting white outline /// /// Outline.effectDistance of the bar outline: the background is redrawn shifted by +-3 units on both axes, /// which shows as a border about 3 units wide. /// private static readonly Vector2 BarOutlineDistance = new(3.0f, -3.0f); // Outline thickness /// Statistics text inside the top-right panel. private TextMeshProUGUI _text = null!; /// Progress bar background object (parent of the fill and the percentage text). private GameObject _progressBarObj = null!; /// Progress bar fill object. private GameObject _progressBarFillObj = null!; /// Fill rect, anchored to the left edge of the bar; its width is set every frame. private RectTransform _fillRect = null!; /// Text on the bar: percentage and "current floor / last floor index". private TextMeshProUGUI _progressText = null!; /// /// Builds the overlay canvas, the statistics panel and the progress bar. Layout values are canvas units at the /// 1920x1080 reference resolution. /// private void Start() { // Set UI layer (5, Unity's built-in "UI" layer) gameObject.layer = 5; // Canvas setup. Sorting order 9999 is below EscapeHandlerUI (10000) and the dialogs/windows of the mod. var canvas = gameObject.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 9999; // Dynamic resolution scaling against a 1920x1080 reference resolution; match 0.5 blends width and height var scaler = gameObject.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; scaler.matchWidthOrHeight = 0.5f; gameObject.AddComponent(); // Create background panel: 360x220 units, pivot and anchor at the top-right corner, inset 30 units from both edges var panelObj = new GameObject("BackgroundPanel"); panelObj.transform.SetParent(transform, false); panelObj.layer = 5; var image = panelObj.AddComponent(); image.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // Dark tone acrylic style var panelRect = panelObj.GetComponent(); panelRect.anchorMin = new Vector2(1, 1); panelRect.anchorMax = new Vector2(1, 1); panelRect.pivot = new Vector2(1, 1); panelRect.anchoredPosition = new Vector2(-30, -30); panelRect.sizeDelta = new Vector2(360, 220); // Create text: 16 pt rich text, top-left aligned, filling the panel minus a 20-unit margin var textObj = new GameObject("MonitorText"); textObj.transform.SetParent(panelObj.transform, false); textObj.layer = 5; _text = textObj.AddComponent(); _text.richText = true; _text.fontSize = 16; _text.alignment = TextAlignmentOptions.TopLeft; // Wrap long lines and let text that exceeds the panel height overflow instead of being truncated _text.textWrappingMode = TextWrappingModes.Normal; _text.overflowMode = TextOverflowModes.Overflow; // Apply the mod's bundled font (loaded by ModAssets.Load at mod initialization) _text.font = ModAssets.NanumSquareNeoNormal; var textRect = textObj.GetComponent(); textRect.anchorMin = Vector2.zero; textRect.anchorMax = Vector2.one; textRect.offsetMin = new Vector2(20, 20); // Padding margin textRect.offsetMax = new Vector2(-20, -20); // --- Progress Bar UI Setup --- // 1. Progress Bar Container: centered anchor/pivot, placed at (BarPosX, BarPosY) from the screen center _progressBarObj = new GameObject("ProgressBar"); _progressBarObj.transform.SetParent(transform, false); _progressBarObj.layer = 5; var bgImage = _progressBarObj.AddComponent(); bgImage.color = BarBackgroundColor; // Add progress bar outline component var bgOutline = _progressBarObj.AddComponent(); bgOutline.effectColor = BarOutlineColor; bgOutline.effectDistance = BarOutlineDistance; var barRect = _progressBarObj.GetComponent(); barRect.anchorMin = new Vector2(0.5f, 0.5f); barRect.anchorMax = new Vector2(0.5f, 0.5f); barRect.pivot = new Vector2(0.5f, 0.5f); barRect.anchoredPosition = new Vector2(BarPosX, BarPosY); barRect.sizeDelta = new Vector2(BarWidth, BarHeight); // 2. Progress Bar Fill: anchored to the left edge and stretched vertically (anchor x 0..0, y 0..1). // sizeDelta.x is the fill width in units; Update sets it to BarWidth * progress. _progressBarFillObj = new GameObject("ProgressBarFill"); _progressBarFillObj.transform.SetParent(_progressBarObj.transform, false); _progressBarFillObj.layer = 5; var fillImage = _progressBarFillObj.AddComponent(); fillImage.color = BarFillColor; _fillRect = _progressBarFillObj.GetComponent(); _fillRect.anchorMin = new Vector2(0, 0); _fillRect.anchorMax = new Vector2(0, 1); _fillRect.pivot = new Vector2(0, 0.5f); _fillRect.anchoredPosition = Vector2.zero; _fillRect.sizeDelta = new Vector2(0, 0); // width will be updated dynamically in Update() // 3. Progress Bar Text: centered over the whole bar var progressTextObj = new GameObject("ProgressBarText"); progressTextObj.transform.SetParent(_progressBarObj.transform, false); progressTextObj.layer = 5; _progressText = progressTextObj.AddComponent(); _progressText.richText = true; _progressText.fontSize = BarTextFontSize; _progressText.alignment = TextAlignmentOptions.Center; _progressText.font = ModAssets.NanumSquareNeoNormal; _progressText.color = BarTextColor; // Black outline (90% opacity, 1.5-unit offset) keeps the white text readable over both the fill and the background var textOutline = progressTextObj.AddComponent(); textOutline.effectColor = new Color(0f, 0f, 0f, 0.9f); textOutline.effectDistance = new Vector2(1.5f, -1.5f); var textRectBar = progressTextObj.GetComponent(); textRectBar.anchorMin = Vector2.zero; textRectBar.anchorMax = Vector2.one; textRectBar.offsetMin = Vector2.zero; textRectBar.offsetMax = Vector2.zero; } /// /// Refreshes the statistics text and the progress bar from and the game state. /// Values fall back to 0 (and the queue limit to RenderManager.MaxQueuedFrames) when no RenderManager exists. /// private void Update() { if (!_text) return; // Statistics lines: // Queue - frames waiting in ImageQueue for the render thread / queue frame limit // Rendered Frames - RenderManager.frameCount (frames produced by the game) // Frames Sent - RenderManager.framesSent (frames handed to the encoder; written by the render thread) // Throttling - RenderManager.throttleCount (times the main thread started waiting because ImageQueue // held more frames than its limit) // Readback failures- RenderManager.readbackFailureCount (AsyncGPUReadback requests that completed with an error) // Mono heap - GC.GetTotalMemory(false), managed heap size in MiB without forcing a collection var sb = new StringBuilder(); sb.AppendLine("ADOFAI Renderer Realtime Monitor"); sb.AppendLine(); var queueCount = 0; if (RenderManager.Instance) { // ImageQueue is shared with the render thread, so its count is read under the same lock lock (RenderManager.Instance.ImageQueue) { queueCount = RenderManager.Instance.ImageQueue.Count; } } sb.AppendLine($"• Queue: {queueCount} / {(RenderManager.Instance ? RenderManager.Instance.QueueFrameLimit : RenderManager.MaxQueuedFrames)} frames"); sb.AppendLine($"• Rendered Frames: {(RenderManager.Instance ? RenderManager.Instance.frameCount : 0)}"); sb.AppendLine($"• Frames Sent: {(RenderManager.Instance ? Interlocked.Read(ref RenderManager.Instance.framesSent) : 0)}"); sb.AppendLine(); sb.AppendLine($"• Throttling Controls: {(RenderManager.Instance ? RenderManager.Instance.throttleCount : 0)} times"); sb.AppendLine($"• Readback Hard Failures: {(RenderManager.Instance ? RenderManager.Instance.readbackFailureCount : 0)} times"); var memoryMb = GC.GetTotalMemory(false) / (1024.0 * 1024.0); sb.AppendLine($"• Mono Heap Memory: {memoryMb:F2} MB"); _text.text = sb.ToString(); // Update Progress Bar. // progress = seqID of the current floor / index of the last floor (listFloors.Count - 1), clamped to 0..1. // The text shows the percentage with one decimal and "(current / last)". var progress = 0f; var currentFloor = 0; var totalFloors = 0; if (scrController.instance && ADOBase.lm && ADOBase.lm.listFloors != null) { currentFloor = scrController.instance.currFloor ? scrController.instance.currFloor.seqID : 0; totalFloors = ADOBase.lm.listFloors.Count - 1; if (totalFloors > 0) { progress = Mathf.Clamp01((float)currentFloor / totalFloors); } } if (_fillRect) _fillRect.sizeDelta = new Vector2(BarWidth * progress, 0); if (_progressText) _progressText.text = $"{progress * 100f:F1}% ({currentFloor}/{totalFloors})"; } }