Optimize Player UI updates with threshold-based rendering and add performance profiler autoload: implement UI_UPDATE_THRESHOLD constant to reduce unnecessary label updates, add conditional DEBUG compilation for debug prints, wrap debug output in preprocessor directives, add PerformanceProfiler autoload, switch to JoltPhysics3D engine, enable VSync, configure rendering quality settings (TAA, SSAA, anisotropic filtering, shadow atlas), and update main scene path

This commit is contained in:
William Stuckey
2026-01-03 16:07:49 -06:00
parent 0d69ad30fb
commit 04dcf8d0e6
182 changed files with 9654 additions and 8 deletions

View File

@@ -0,0 +1,306 @@
using Godot;
using System;
using System.Collections.Generic;
namespace Voider.Tools;
/// <summary>
/// Unified performance profiling and monitoring tool
/// Combines real-time overlay, spike detection, and detailed logging
///
/// Controls:
/// F9 - Toggle overlay visibility
/// F10 - Toggle spike logging (to console)
/// F11 - Dump spike log to file
/// F12 - Print performance summary
/// </summary>
public partial class PerformanceProfiler : CanvasLayer
{
#region Export Properties
[Export] public bool EnableOverlay { get; set; } = false;
[Export] public bool EnableSpikeLogging { get; set; } = false;
[Export] public float SpikeThresholdMs { get; set; } = 20.0f;
[Export] public int MaxSpikeHistory { get; set; } = 100;
[Export] public int SampleWindowSize { get; set; } = 60;
#endregion
#region UI Elements
private Label _fpsLabel;
private Label _frameTimeLabel;
private Label _processTimeLabel;
private Label _physicsTimeLabel;
private Label _memoryLabel;
private Label _drawCallsLabel;
private VBoxContainer _container;
#endregion
#region Performance Tracking
private List<double> _frameTimeSamples = new List<double>();
private List<FrameSpikeData> _spikeHistory = new List<FrameSpikeData>();
private double _lastFrameTime;
#endregion
private struct FrameSpikeData
{
public double Timestamp;
public double FrameMs;
public double ProcessMs;
public double PhysicsMs;
public int DrawCalls;
public int NodeCount;
}
public override void _Ready()
{
CreateUI();
_container.Visible = EnableOverlay;
GD.Print("[PerformanceProfiler] Initialized");
GD.Print(" F9 - Toggle overlay");
GD.Print(" F10 - Toggle spike logging");
GD.Print(" F11 - Dump spike log to file");
GD.Print(" F12 - Print summary");
}
private void CreateUI()
{
// Main container
_container = new VBoxContainer
{
Position = new Vector2(10, 10)
};
AddChild(_container);
// Title
var titleLabel = new Label
{
Text = "Performance Monitor"
};
titleLabel.AddThemeFontSizeOverride("font_size", 18);
_container.AddChild(titleLabel);
// Separator
var separator = new HSeparator();
_container.AddChild(separator);
// FPS
_fpsLabel = CreateLabel("FPS: --");
_container.AddChild(_fpsLabel);
// Frame time
_frameTimeLabel = CreateLabel("Frame: -- ms");
_container.AddChild(_frameTimeLabel);
// Process time
_processTimeLabel = CreateLabel("Process: -- ms");
_container.AddChild(_processTimeLabel);
// Physics time
_physicsTimeLabel = CreateLabel("Physics: -- ms");
_container.AddChild(_physicsTimeLabel);
// Memory
_memoryLabel = CreateLabel("Memory: -- MB");
_container.AddChild(_memoryLabel);
// Draw calls
_drawCallsLabel = CreateLabel("Draw Calls: --");
_container.AddChild(_drawCallsLabel);
// Add styling
var styleBox = new StyleBoxFlat
{
BgColor = new Color(0, 0, 0, 0.7f)
};
styleBox.SetContentMarginAll(10);
_container.AddThemeStyleboxOverride("panel", styleBox);
}
private Label CreateLabel(string text)
{
var label = new Label { Text = text };
label.AddThemeFontSizeOverride("font_size", 14);
return label;
}
public override void _Process(double delta)
{
var frameMs = delta * 1000.0;
_lastFrameTime = frameMs;
// Track frame time samples
_frameTimeSamples.Add(frameMs);
if (_frameTimeSamples.Count > SampleWindowSize)
{
_frameTimeSamples.RemoveAt(0);
}
// Get performance metrics
var processMs = Performance.GetMonitor(Performance.Monitor.TimeProcess) * 1000.0;
var physicsMs = Performance.GetMonitor(Performance.Monitor.TimePhysicsProcess) * 1000.0;
var drawCalls = (int)Performance.GetMonitor(Performance.Monitor.RenderTotalDrawCallsInFrame);
var nodeCount = (int)Performance.GetMonitor(Performance.Monitor.ObjectNodeCount);
var memoryMb = Performance.GetMonitor(Performance.Monitor.MemoryStatic) / 1_048_576.0;
// Check for spikes
if (EnableSpikeLogging && frameMs > SpikeThresholdMs)
{
LogSpike(frameMs, processMs, physicsMs, drawCalls, nodeCount);
}
// Update overlay
if (_container.Visible)
{
UpdateOverlay(frameMs, processMs, physicsMs, memoryMb, drawCalls);
}
}
private void UpdateOverlay(double frameMs, double processMs, double physicsMs, double memoryMb, int drawCalls)
{
var fps = 1000.0 / frameMs;
_fpsLabel.Text = $"FPS: {fps:F1}";
_frameTimeLabel.Text = $"Frame: {frameMs:F2} ms";
_processTimeLabel.Text = $"Process: {processMs:F2} ms";
_physicsTimeLabel.Text = $"Physics: {physicsMs:F2} ms";
_memoryLabel.Text = $"Memory: {memoryMb:F1} MB";
_drawCallsLabel.Text = $"Draw Calls: {drawCalls}";
// Color code frame time
if (frameMs > 16.67)
{
_frameTimeLabel.AddThemeColorOverride("font_color", Colors.Red);
}
else if (frameMs > 8.33)
{
_frameTimeLabel.AddThemeColorOverride("font_color", Colors.Yellow);
}
else
{
_frameTimeLabel.AddThemeColorOverride("font_color", Colors.Green);
}
}
private void LogSpike(double frameMs, double processMs, double physicsMs, int drawCalls, int nodeCount)
{
var spike = new FrameSpikeData
{
Timestamp = Time.GetTicksMsec() / 1000.0,
FrameMs = frameMs,
ProcessMs = processMs,
PhysicsMs = physicsMs,
DrawCalls = drawCalls,
NodeCount = nodeCount
};
_spikeHistory.Add(spike);
if (_spikeHistory.Count > MaxSpikeHistory)
{
_spikeHistory.RemoveAt(0);
}
GD.Print($"[SPIKE] Frame: {frameMs:F2}ms | Process: {processMs:F2}ms | Physics: {physicsMs:F2}ms | Draw Calls: {drawCalls} | Nodes: {nodeCount}");
}
private void PrintSummary()
{
if (_frameTimeSamples.Count == 0)
{
GD.Print("[PerformanceProfiler] No data to summarize");
return;
}
double avgFrame = 0, minFrame = double.MaxValue, maxFrame = 0;
foreach (var sample in _frameTimeSamples)
{
avgFrame += sample;
if (sample < minFrame) minFrame = sample;
if (sample > maxFrame) maxFrame = sample;
}
avgFrame /= _frameTimeSamples.Count;
var avgFps = 1000.0 / avgFrame;
var processMs = Performance.GetMonitor(Performance.Monitor.TimeProcess) * 1000.0;
var physicsMs = Performance.GetMonitor(Performance.Monitor.TimePhysicsProcess) * 1000.0;
var drawCalls = (int)Performance.GetMonitor(Performance.Monitor.RenderTotalDrawCallsInFrame);
var nodeCount = (int)Performance.GetMonitor(Performance.Monitor.ObjectNodeCount);
var memoryMb = Performance.GetMonitor(Performance.Monitor.MemoryStatic) / 1_048_576.0;
GD.Print("========================================");
GD.Print($"[PerformanceProfiler] Summary (last {_frameTimeSamples.Count} frames):");
GD.Print($" Frame Time: Avg={avgFrame:F2}ms, Min={minFrame:F2}ms, Max={maxFrame:F2}ms");
GD.Print($" Average FPS: {avgFps:F1}");
GD.Print($" Current Process: {processMs:F2}ms");
GD.Print($" Current Physics: {physicsMs:F2}ms");
GD.Print($" Total Nodes: {nodeCount}");
GD.Print($" Draw Calls: {drawCalls}");
GD.Print($" Memory Usage: {memoryMb:F1} MB");
GD.Print($" Spikes Logged: {_spikeHistory.Count} (>{SpikeThresholdMs}ms)");
GD.Print("========================================");
}
private void DumpSpikeLog()
{
if (_spikeHistory.Count == 0)
{
GD.Print("[PerformanceProfiler] No spikes to dump");
return;
}
var logPath = $"user://performance_spikes_{DateTime.Now:yyyyMMdd_HHmmss}.log";
using var file = Godot.FileAccess.Open(logPath, Godot.FileAccess.ModeFlags.Write);
if (file == null)
{
GD.PrintErr($"[PerformanceProfiler] Failed to create log file: {logPath}");
return;
}
file.StoreLine("=== Performance Spike Log ===");
file.StoreLine($"Total Spikes: {_spikeHistory.Count}");
file.StoreLine($"Threshold: {SpikeThresholdMs}ms");
file.StoreLine($"Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
file.StoreLine("");
file.StoreLine("Timestamp(s) | Frame(ms) | Process(ms) | Physics(ms) | Draw Calls | Nodes");
file.StoreLine("-----------------------------------------------------------------------------");
foreach (var spike in _spikeHistory)
{
file.StoreLine($"{spike.Timestamp:F2} | {spike.FrameMs:F2} | {spike.ProcessMs:F2} | {spike.PhysicsMs:F2} | {spike.DrawCalls} | {spike.NodeCount}");
}
file.Close();
var absPath = ProjectSettings.GlobalizePath(logPath);
GD.Print($"[PerformanceProfiler] Spike log written to: {absPath}");
GD.Print($"[PerformanceProfiler] Total spikes logged: {_spikeHistory.Count}");
}
public override void _Input(InputEvent @event)
{
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
{
switch (keyEvent.Keycode)
{
case Key.F9:
_container.Visible = !_container.Visible;
GD.Print($"[PerformanceProfiler] Overlay {(_container.Visible ? "SHOWN" : "HIDDEN")}");
break;
case Key.F10:
EnableSpikeLogging = !EnableSpikeLogging;
GD.Print($"[PerformanceProfiler] Spike logging {(EnableSpikeLogging ? "ENABLED" : "DISABLED")} (>{SpikeThresholdMs}ms)");
break;
case Key.F11:
DumpSpikeLog();
break;
case Key.F12:
PrintSummary();
break;
}
}
}
}