mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-04-19 05:48:07 -04:00
* Remove unnecessary static singletons in MainMenu code * Empty SceneWorld delete queues during shutdown * Dresser cancel async load operation on destroy * Use reflection to null references to static native resources on shutdown This way we don't have to remember doing this manually. * Fix SoundOcclusionSystem using static lists to reference resources * Sound System: Use weak references to refer to scenes * Cleanup static logging listeners that kept strong refs to panels * UISystem Cleanup, make sure all panel/stylesheet refs are released * RenderTarget and RenderAttributes shutdown cleanup * Rework AvatarLoader, ThumbLoader & HTTPImageLoader Cache First try to go through ResourceLibrary.WeakIndex which might already hold the texture. If there is no hit, go through a second cache that caches HTTP & Steam API response bytes instead of textures. We want to cache the response bytes rather than the actual Texture, so stuff cached sits in RAM not VRAM. Before avatars and thumbs would reside in VRAM. * Fix rendertarget leak in CommandList.Attr.SetValue GetDepthTarget() / GetColorTarget() return a new strong handle (ref count +1). We need to DestroyStrongHandle() that ref. So handles don't leak. * Call EngineLoop.DrainFrameEndDisposables before shutdown * NativeResourceCache now report leaks on shutdown * Override Resource.Destroy for native resources, kill stronghandles * Deregister SceneWorld from SceneWorld.All during destruction * Ensure async texture loading cancels on shutdown * SkinnedModelRender bonemergetarget deregister from target OnDisabled * Clear shaderMaterials cache during shutdown * Refactor Shutdown code Mostly renaming methods from Clear() -> Shutdown() Adding separate GlobalContext.Shutdown function (more aggressive than GlobalContext.Reset). Clear some static input state. * Deregister surfaces from Surface.All in OnDestroy * RunAllStaticConstructors when loading a mount * Advanced managed resource leak tracker enabled via `resource_leak_tracking 1` Works by never pruning the WeakTable in NativeResourceCache. So we can check for all resources if they are still being held on to and log a callstack.
443 lines
9.3 KiB
C#
443 lines
9.3 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 )
|
|
{
|
|
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 );
|
|
}
|
|
|
|
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() )
|
|
{
|
|
SoundHandle.TickAll();
|
|
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 );
|
|
}
|
|
|
|
internal static void OnClientOutput()
|
|
{
|
|
// The editor renders it's own game scene
|
|
if ( Application.IsEditor )
|
|
{
|
|
IToolsDll.Current?.OnRender();
|
|
return;
|
|
}
|
|
|
|
var engineChain = g_pEngineServiceMgr.GetEngineSwapChain();
|
|
|
|
IGameInstanceDll.Current?.OnRender( engineChain );
|
|
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();
|
|
}
|
|
}
|
|
}
|