Files
sbox-public/engine/Sandbox.Engine/Core/EngineLoop.cs
Matt Stevens 6aaa90fea2 Add Tracy profiler integration (non-public) (#5028)
Adds Tracy 0.13.1 support which is a profiler designed for game engines
that can track both CPU and GPU in frame boundaries at a much higher
resolution than ETW based profilers like Superluminal.

Most of this is restoring VPROF defines in 1000 places, hooking up GPU
zones, memory tracking and more.

This is only enabled on non-public developer builds due to the overhead
in this sort of profiling.

https://files.facepunch.com/matt/1b0811b1/tracy-profiler_P9pUMg6Y8n.png
2026-06-08 14:40:00 +01:00

460 lines
9.9 KiB
C#

using NativeEngine;
using Sandbox.Audio;
using Sandbox.Engine;
using Sandbox.Engine.Settings;
using Sandbox.Network;
using Sandbox.Rendering;
using Sandbox.TextureLoader;
using Sandbox.UI;
using Sandbox.Utility;
using Sandbox.VR;
using System.Threading.Channels;
namespace Sandbox;
[SkipHotload]
internal static class EngineLoop
{
static double previousTime;
static Superluminal _runFrame = new Superluminal( "RunFrame", "#4d5e73" );
static Superluminal _frameStart = new Superluminal( "FrameStart", "#2c3541" );
static Superluminal _frameEnd = new Superluminal( "FrameEnd", "#2c3541" );
internal static void RunFrame( CMaterialSystem2AppSystemDict appDict, out bool wantsQuit )
{
if ( Application.WantsExit )
{
SoundHandle.Shutdown();
MixingThread.DrainDisposals();
g_pEngineServiceMgr.ExitMainLoop();
}
double time = RealTime.NowDouble;
FastTimer frameTimer = FastTimer.StartNew();
using ( _runFrame.Start() )
{
RealTime.Update( time );
Time.Update( RealTime.Now, RealTime.Delta );
DebugOverlay.Reset();
try
{
using ( _frameStart.Start() )
{
FrameStart();
}
}
catch ( System.Exception e )
{
Log.Error( e );
}
using ( PerformanceStats.Timings.Render.Scope() )
{
wantsQuit = !EngineGlobal.SourceEngineFrame( appDict, time, previousTime );
}
if ( wantsQuit )
{
SoundHandle.Shutdown();
MixingThread.DrainDisposals();
}
try
{
using ( _frameEnd.Start() )
{
FrameEnd();
}
IToolsDll.Current?.RunFrame();
}
catch ( System.Exception e )
{
Log.Error( e );
}
}
SleepForFrameRateClamp( frameTimer );
previousTime = time;
}
static Superluminal _sleepForFrameCap = new Superluminal( "Sleep For Max FPS", Color.Gray );
static double GetMaxFrameRate()
{
if ( Application.IsBenchmark ) return -1;
if ( Application.IsHeadless ) return 60;
int maxFps = RenderSettings.Instance.MaxFrameRate;
if ( InputSystem.IsAppActive() ) return maxFps;
// only use maxinactive if it's over 0 and lower than maxfps
int maxInactive = RenderSettings.Instance.MaxFrameRateInactive;
if ( maxInactive <= 0 ) return maxFps;
if ( maxInactive > maxFps ) return maxFps;
return maxInactive;
}
static void SleepForFrameRateClamp( FastTimer frameTime )
{
double maxFps = GetMaxFrameRate();
if ( maxFps <= 0 ) return;
using var inst = _sleepForFrameCap.Start();
double targetMilliseconds = 1000.0 / maxFps;
if ( targetMilliseconds > 100 ) targetMilliseconds = 100; // min is 10fps
if ( frameTime.ElapsedMilliSeconds >= targetMilliseconds ) return; // no sleep needed
var sleepMs = targetMilliseconds - frameTime.ElapsedMilliSeconds;
if ( sleepMs > 1.0 )
{
System.Threading.Thread.Sleep( (int)sleepMs );
}
// sleep is inaccurate (to nearest 1ms, we call timeBeginPeriod in engine)
// so bleed off any residual fractions of a millisecond
while ( frameTime.ElapsedMilliSeconds < targetMilliseconds )
{
// wait
}
}
/// <summary>
/// Pumps the input system
/// </summary>
static void UpdateInput()
{
using var __ = PerformanceStats.Timings.Input.Scope();
g_pInputService.Pump();
}
internal static void FrameStart()
{
ThreadSafe.AssertIsMainThread();
//
// Let the Steam API and Steam Game Server API think
//
NativeEngine.Steam.SteamGameServer_RunCallbacks();
NativeEngine.Steam.SteamAPI_RunCallbacks();
//
// Update performance stats (should be called every frame)
//
UpdatePerformance();
DebugOverlay.Draw();
UpdateInput();
//
// Dispatch callbacks for any changed files
//
FileWatch.Tick();
//
// Update any animated textures
//
using ( PerformanceStats.Timings.Video.Scope() )
{
Texture.Tick();
}
//
// Update VR
//
VRSystem.FrameStart();
//
// Expire any unused resources
//
NativeResourceCache.Tick();
Game.Resources.PruneWeakIndex();
Mounting.MountUtility.TickPreviewRenders();
//
// Run Tasks
//
RunAsyncTasks();
//
// Let the context's tick
//
IMenuDll.Current?.Tick();
IGameInstanceDll.Current?.Tick();
IMenuDll.Current?.LateTick();
IToolsDll.Current?.Tick();
//
// Run Tasks
//
RunAsyncTasks();
//
// Misc client systems
//
if ( !Application.IsHeadless )
{
using ( IGameInstanceDll.Current?.PushScope() )
{
VoiceManager.Tick();
Sandbox.TextRendering.Tick();
}
}
//
// If we have any queued console messages, we can print them now
//
Logging.PushQueuedMessages();
//
// Allow the events to push if they want
//
Api.Events.TickEvents();
Api.Stats.TickStats();
Sandbox.Services.Messaging.ProcessMessages();
// Simulate UI last. This works out all the styles and shit, so we want
// that to be reflected right BEFORE the frame is rendered.
using ( PerformanceStats.Timings.Ui.Scope() )
{
SimulateUI();
}
// Give each sound handle an opportunity to for a frame think
using ( PerformanceStats.Timings.Audio.Scope() )
{
MixingThread.UpdateGlobals();
}
//
// Update the mouse visibility status
//
if ( !Application.IsHeadless )
{
Engine.InputRouter.Frame();
}
// Keep room up to date
PartyRoom.Current?.Tick();
Audio.AudioEngine.Tick();
}
public static void RunAsyncTasks()
{
using ( PerformanceStats.Timings.Async.Scope() )
{
using var sceneScope = IGameInstanceDll.Current?.PushScope();
ThreadSafe.AssertIsMainThread();
MainThread.RunQueues();
SyncContext.MainThread?.ProcessQueue();
}
}
internal static void FrameEnd()
{
ThreadSafe.AssertIsMainThread();
//
// Run Tasks
//
Engine.Streamer.CurrentService?.Tick();
RunAsyncTasks();
//
// Update VR
//
VRSystem.FrameEnd();
//
// Free strings allocated by Interop shit, and let us know how many
//
int count = Interop.Free();
if ( count > 10 )
{
//log.Trace( $"Interop Free: {count}" );
}
//
// Run threaded stuff that needed to
// happen on the main thread
//
MainThread.RunQueues();
//
// Trigger recompile of Project
//
Project.Tick();
//
// Free anything that needs to be disposed of at end of frame
//
DrainFrameEndDisposables();
// Free render targets
RenderTarget.EndOfFrame();
}
static unsafe void UpdatePerformance()
{
PerformanceStats.Frame();
Api.Performance.Frame();
}
static Superluminal _simulateUiGame = new Superluminal( "Simulate GameUI", "#2c3541" );
static Superluminal _simulateUiMenu = new Superluminal( "Simulate GameUI", "#2c3541" );
private static void SimulateUI()
{
ThreadSafe.AssertIsMainThread();
VideoTextureLoader.TickVideoPlayers();
TooltipSystem.Frame();
PanelRealTime.Update();
using ( _simulateUiGame.Start() )
{
IGameInstanceDll.Current?.SimulateUI();
}
using ( _simulateUiMenu.Start() )
{
IMenuDll.Current?.SimulateUI();
}
}
private static Logger nativeLogger = Logging.GetLogger( "Native" );
static string partial = "";
internal static void Print( int severity, string logger, string message )
{
partial += message;
if ( !partial.Contains( "\n" ) )
return;
if ( partial.EndsWith( '\n' ) )
{
message = partial;
partial = "";
}
else
{
var i = partial.LastIndexOf( '\n' );
message = partial.Substring( 0, i );
partial = partial.Substring( i );
}
message = message.TrimEnd( new[] { '\n', '\r' } );
NLog.LogLevel level = severity switch
{
0 => NLog.LogLevel.Info,
1 => NLog.LogLevel.Info,
2 => NLog.LogLevel.Warn,
3 => NLog.LogLevel.Warn,
4 => NLog.LogLevel.Error,
5 => NLog.LogLevel.Fatal,
_ => NLog.LogLevel.Info,
};
var logName = $"engine/{logger}";
nativeLogger.WriteToTargets( level, null, $"{message}", logName );
}
internal static void Print( bool debug, string message )
{
message = message.TrimEnd( new[] { '\n', '\r' } );
if ( debug )
{
nativeLogger.Trace( message );
}
else
{
nativeLogger.Info( message );
}
}
/// <summary>
/// A console command has arrived, or a convar has changed
/// </summary>
internal static void DispatchConsoleCommand( string name, string args, long flaglong )
{
var convar = ConVarSystem.Find( name );
if ( convar is null )
{
Log.Warning( $"Unknown Command: {name}" );
return;
}
convar.Run( args );
}
static Superluminal _clientOutput = new Superluminal( "OnClientOutput", "#3a6ea5" );
static Superluminal _toolsRender = new Superluminal( "Tools Render", "#6e6e3a" );
static Superluminal _gameRender = new Superluminal( "Game Render", "#3a6e4d" );
static Superluminal _menuRender = new Superluminal( "Menu Render", "#6e3a6e" );
internal static void OnClientOutput()
{
using var _outputScope = _clientOutput.Start();
// The editor renders it's own game scene
if ( Application.IsEditor )
{
using ( _toolsRender.Start() )
IToolsDll.Current?.OnRender();
return;
}
var engineChain = g_pEngineServiceMgr.GetEngineSwapChain();
using ( _gameRender.Start() )
IGameInstanceDll.Current?.OnRender( engineChain );
using ( _menuRender.Start() )
IMenuDll.Current?.OnRender( engineChain );
}
/// <summary>
/// Called right at the end of a view being submitted, so everything CPU is done and it's handed off to the GPU.
/// This is also called for any dependent views.
/// </summary>
internal static void OnSceneViewSubmitted( ISceneView view )
{
RenderPipeline.OnSceneViewSubmitted( view );
}
static Channel<IDisposable> FrameEndDisposables = Channel.CreateUnbounded<IDisposable>();
/// <summary>
/// Queue something to be disposed of after the frame has ended and everything has finished rendering.
/// </summary>
internal static void DisposeAtFrameEnd( IDisposable disposable ) => FrameEndDisposables.Writer.TryWrite( disposable );
/// <summary>
/// Drain all queued frame-end disposables immediately. Called during shutdown
/// since no more frames will run to process them naturally.
/// </summary>
internal static void DrainFrameEndDisposables()
{
while ( FrameEndDisposables.Reader.TryRead( out var disposable ) )
{
disposable.Dispose();
}
}
}