Files
ADOFAIRenderer/UI/DebugMonitorUI.cs
T

248 lines
12 KiB
C#

using System;
using System.Text;
using System.Threading;
using ADOFAIRenderer.Core;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace ADOFAIRenderer.UI;
/// <summary>
/// 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.
/// <para>
/// Created by <c>RenderManager.StartFFmpegThread</c> 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.
/// <see cref="Update"/> rebuilds the text every frame on the Unity main thread.
/// </para>
/// </summary>
internal class DebugMonitorUI : MonoBehaviour
{
// --- Progress Bar UI Configuration Constants ---
// Sizes and positions are canvas units at the 1920x1080 reference resolution of the CanvasScaler.
/// <summary>Width of the progress bar; the fill width is <c>BarWidth * progress</c>.</summary>
private const float BarWidth = 800f;
/// <summary>Height of the progress bar.</summary>
private const float BarHeight = 28f;
/// <summary>Horizontal offset of the bar center from the screen center (0 = centered).</summary>
private const float BarPosX = 0f;
/// <summary>Vertical offset of the bar center from the screen center; -420 is 120 units above the bottom edge (540 - 420).</summary>
private const float BarPosY = -420f; // Y position relative to screen center (Reference: 1920x1080)
/// <summary>Font size of the percentage text drawn on the bar.</summary>
private const int BarTextFontSize = 16; // Increased font size for visibility
/// <summary>Unfilled bar color: opaque near-black with a slight blue tint.</summary>
private static readonly Color BarBackgroundColor = new(0.08f, 0.08f, 0.1f, 1.0f);
/// <summary>Filled part color: opaque cyan (#00CCFF).</summary>
private static readonly Color BarFillColor = new(0.0f, 0.8f, 1.0f, 1.0f); // Sleek cyan/blue
/// <summary>Percentage text color: opaque white.</summary>
private static readonly Color BarTextColor = new(1.0f, 1.0f, 1.0f, 1.0f);
// --- Progress Bar Outline Configuration Constants ---
/// <summary>Color of the uGUI <see cref="Outline"/> drawn around the bar background.</summary>
private static readonly Color BarOutlineColor = Color.white; // Contrasting white outline
/// <summary>
/// <c>Outline.effectDistance</c> of the bar outline: the background is redrawn shifted by +-3 units on both axes,
/// which shows as a border about 3 units wide.
/// </summary>
private static readonly Vector2 BarOutlineDistance = new(3.0f, -3.0f); // Outline thickness
/// <summary>Statistics text inside the top-right panel.</summary>
private TextMeshProUGUI _text = null!;
/// <summary>Progress bar background object (parent of the fill and the percentage text).</summary>
private GameObject _progressBarObj = null!;
/// <summary>Progress bar fill object.</summary>
private GameObject _progressBarFillObj = null!;
/// <summary>Fill rect, anchored to the left edge of the bar; its width is set every frame.</summary>
private RectTransform _fillRect = null!;
/// <summary>Text on the bar: percentage and "current floor / last floor index".</summary>
private TextMeshProUGUI _progressText = null!;
/// <summary>
/// Builds the overlay canvas, the statistics panel and the progress bar. Layout values are canvas units at the
/// 1920x1080 reference resolution.
/// </summary>
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>();
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<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0.5f;
gameObject.AddComponent<GraphicRaycaster>();
// 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>();
image.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); // Dark tone acrylic style
var panelRect = panelObj.GetComponent<RectTransform>();
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<TextMeshProUGUI>();
_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<RectTransform>();
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<Image>();
bgImage.color = BarBackgroundColor;
// Add progress bar outline component
var bgOutline = _progressBarObj.AddComponent<Outline>();
bgOutline.effectColor = BarOutlineColor;
bgOutline.effectDistance = BarOutlineDistance;
var barRect = _progressBarObj.GetComponent<RectTransform>();
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<Image>();
fillImage.color = BarFillColor;
_fillRect = _progressBarFillObj.GetComponent<RectTransform>();
_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<TextMeshProUGUI>();
_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<Outline>();
textOutline.effectColor = new Color(0f, 0f, 0f, 0.9f);
textOutline.effectDistance = new Vector2(1.5f, -1.5f);
var textRectBar = progressTextObj.GetComponent<RectTransform>();
textRectBar.anchorMin = Vector2.zero;
textRectBar.anchorMax = Vector2.one;
textRectBar.offsetMin = Vector2.zero;
textRectBar.offsetMax = Vector2.zero;
}
/// <summary>
/// Refreshes the statistics text and the progress bar from <see cref="RenderManager"/> and the game state.
/// Values fall back to 0 (and the queue limit to <c>RenderManager.MaxQueuedFrames</c>) when no RenderManager exists.
/// </summary>
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("<b><color=#00ccff>ADOFAI Renderer Realtime Monitor</color></b>");
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: <b>{queueCount}</b> / {(RenderManager.Instance ? RenderManager.Instance.QueueFrameLimit : RenderManager.MaxQueuedFrames)} frames");
sb.AppendLine($"• Rendered Frames: <b>{(RenderManager.Instance ? RenderManager.Instance.frameCount : 0)}</b>");
sb.AppendLine($"• Frames Sent: <b>{(RenderManager.Instance ? Interlocked.Read(ref RenderManager.Instance.framesSent) : 0)}</b>");
sb.AppendLine();
sb.AppendLine($"• Throttling Controls: {(RenderManager.Instance ? RenderManager.Instance.throttleCount : 0)} times");
sb.AppendLine($"• Readback Hard Failures: <color=#ff3333>{(RenderManager.Instance ? RenderManager.Instance.readbackFailureCount : 0)}</color> 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})";
}
}