Files
sbox-public/engine/Sandbox.AppSystem/AppSystem.cs
Lorenz Junglas 6808d8768e Shutdown fixes (#3553)
* Stop generating solutions via -test flag add -generatesolution

* Add TestAppSystem remove Application.InitUnitTest

Avoids some hacks and also makes sure our tests are as close to a real AppSystem as possible.

* Add shutdown unit test

shuts down an re-inits the engine

* Properly dispose native resources hold by managed during shutdown

Should fix a bunch of crashes

* Fix filesystem and networking tests

* StandaloneTest does proper Game Close

* Make sure package tests clean up properly

* Make sure menu scene and resources are released on shutdown

* Report leaked scenes on shutdown

* Ensure DestroyImmediate is not used on scenes

* Fix unmounting in unit tests not clearing native refs

* Force destroy native resource on ResourceLib Clear
2025-12-08 15:55:11 +01:00

299 lines
7.5 KiB
C#

using Sandbox.Diagnostics;
using Sandbox.Engine;
using Sandbox.Internal;
using Sandbox.Network;
using Sandbox.Rendering;
using System;
using System.Globalization;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
namespace Sandbox;
public class AppSystem
{
protected Logger log = new Logger( "AppSystem" );
internal CMaterialSystem2AppSystemDict _appSystem { get; set; }
[DllImport( "user32.dll", CharSet = CharSet.Unicode )]
private static extern int MessageBox( IntPtr hWnd, string text, string caption, uint type );
/// <summary>
/// We should check all the system requirements here as early as possible.
/// </summary>
public void TestSystemRequirements()
{
if ( !OperatingSystem.IsWindows() )
return;
// AVX is on any sane CPU since 2011
if ( !Avx.IsSupported )
{
MessageBox( IntPtr.Zero, "Your CPU needs to support AVX instructions to run this game.", "Unsupported CPU", 0x10 );
Environment.Exit( 1 );
}
// check core count, ram, os?
// rendersystemvulkan ends up checking gpu, driver, vram later on
MissingDependancyDiagnosis.Run();
}
public virtual void Init()
{
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
NetCore.InitializeInterop( Environment.CurrentDirectory );
}
void SetupEnvironment()
{
CultureInfo culture = (CultureInfo)CultureInfo.CurrentCulture.Clone();
//
// force GregorianCalendar, because that's how we're going to be parsing dates etc
//
if ( culture.DateTimeFormat.Calendar is not GregorianCalendar )
{
culture.DateTimeFormat.Calendar = new GregorianCalendar();
}
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
}
/// <summary>
/// Create the Menu instance.
/// </summary>
protected void CreateMenu()
{
MenuDll.Create();
}
/// <summary>
/// Create the Game (Sandbox.GameInstance)
/// </summary>
protected void CreateGame()
{
GameInstanceDll.Create();
}
/// <summary>
/// Create the editor (Sandbox.Tools)
/// </summary>
protected void CreateEditor()
{
Editor.AssemblyInitialize.Initialize();
}
public void Run()
{
try
{
SetupEnvironment();
Application.TryLoadVersionInfo( Environment.CurrentDirectory );
//
// Putting ErrorReporter.Initialize(); before Init here causes engine2.dll
// to be unable to load. I dont know wtf and I spent too much time looking into it.
// It's finding the assemblies still, The last dll it loads is tier0.dll.
//
Init();
NativeEngine.EngineGlobal.Plat_SetCurrentFrame( 0 );
while ( RunFrame() )
{
BlockingLoopPumper.Run( () => RunFrame() );
}
Shutdown();
}
catch ( System.Exception e )
{
ErrorReporter.Initialize();
ErrorReporter.ReportException( e );
ErrorReporter.Flush();
Console.WriteLine( $"Error: ({e.GetType()}) {e.Message}" );
Environment.Exit( 1 );
}
}
protected virtual bool RunFrame()
{
EngineLoop.RunFrame( _appSystem, out bool wantsToQuit );
return !wantsToQuit;
}
public virtual void Shutdown()
{
// Make sure game instance is closed
IGameInstanceDll.Current?.CloseGame();
// Send shutdown event, should allow us to track successful shutdown vs crash
{
var analytic = new Api.Events.EventRecord( "Exit" );
analytic.SetValue( "uptime", RealTime.Now );
// We could record a bunch of stats during the session and
// submit them here. I'm thinking things like num games played
// menus visited, time in menus, time in game, files downloaded.
// Things to give us a whole session picture.
analytic.Submit();
}
ConVarSystem.SaveAll();
IToolsDll.Current?.Exiting();
IMenuDll.Current?.Exiting();
IGameInstanceDll.Current?.Exiting();
SoundFile.Shutdown();
SoundHandle.Shutdown();
DedicatedServer.Shutdown();
// Flush API
Api.Shutdown();
ConVarSystem.ClearNativeCommands();
// Whatever package still exists needs to fuck off
PackageManager.UnmountAll();
// Clear static resources
Texture.DisposeStatic();
Model.DisposeStatic();
Material.UI.DisposeStatic();
Gizmo.GizmoDraw.DisposeStatic();
CubemapRendering.DisposeStatic();
Graphics.DisposeStatic();
TextRendering.ClearCache();
NativeResourceCache.Clear();
// Renderpipeline may hold onto native resources, clear them out
RenderPipeline.ClearPool();
// Run GC and finalizers to clear any resources held by managed
GC.Collect();
GC.WaitForPendingFinalizers();
// Run the queue one more time, since some finalizers queue tasks
MainThread.RunQueues();
// print each scene that is leaked
foreach ( var leakedScene in Scene.All )
{
log.Warning( $"Leaked scene {leakedScene.Id} during shutdown." );
}
// Shut the engine down (close window etc)
NativeEngine.EngineGlobal.SourceEngineShutdown( _appSystem, false );
if ( _appSystem.IsValid )
{
_appSystem.Destroy();
_appSystem = default;
}
if ( steamApiDll != IntPtr.Zero )
{
NativeLibrary.Free( steamApiDll );
steamApiDll = default;
}
// Unload native dlls:
// At this point we should no longer need them.
// If we still hold references to native resources, we want it to crash here rather than on application exit.
Managed.SandboxEngine.NativeInterop.Free();
// No-ops if editor isn't loaded
Managed.SourceTools.NativeInterop.Free();
Managed.SourceAssetSytem.NativeInterop.Free();
Managed.SourceHammer.NativeInterop.Free();
Managed.SourceModelDoc.NativeInterop.Free();
Managed.SourceAnimgraph.NativeInterop.Free();
EngineFileSystem.Shutdown();
Application.Shutdown();
}
protected void InitGame( AppSystemCreateInfo createInfo, string commandLine = null )
{
commandLine ??= System.Environment.CommandLine;
commandLine = commandLine.Replace( ".dll", ".exe" ); // uck
_appSystem = CMaterialSystem2AppSystemDict.Create( createInfo.ToMaterialSystem2AppSystemDictCreateInfo() );
if ( createInfo.Flags.HasFlag( AppSystemFlags.IsEditor ) )
{
_appSystem.SetInToolsMode();
}
if ( createInfo.Flags.HasFlag( AppSystemFlags.IsUnitTest ) )
{
_appSystem.SetInTestMode();
}
if ( createInfo.Flags.HasFlag( AppSystemFlags.IsStandaloneGame ) )
{
_appSystem.SetInStandaloneApp();
}
if ( createInfo.Flags.HasFlag( AppSystemFlags.IsDedicatedServer ) )
{
_appSystem.SetDedicatedServer( true );
}
_appSystem.SetSteamAppId( (uint)Application.AppId );
if ( !NativeEngine.EngineGlobal.SourceEnginePreInit( commandLine, _appSystem ) )
{
throw new System.Exception( "SourceEnginePreInit failed" );
}
Bootstrap.PreInit( _appSystem );
if ( createInfo.Flags.HasFlag( AppSystemFlags.IsStandaloneGame ) )
{
Standalone.Init();
}
if ( !NativeEngine.EngineGlobal.SourceEngineInit( _appSystem ) )
{
throw new System.Exception( "SourceEngineInit returned false" );
}
Bootstrap.Init();
}
protected void SetWindowTitle( string title )
{
_appSystem.SetAppWindowTitle( title );
}
IntPtr steamApiDll = IntPtr.Zero;
/// <summary>
/// Explicitly load the Steam Api dll from our bin folder, so that it doesn't accidentally
/// load one from c:\system32\ or something. This is a problem when people have installed
/// pirate versions of Steam in the past and have the assembly hanging around still. By loading
/// it here we're saying use this version, and it won't try to load another one.
/// </summary>
protected void LoadSteamDll()
{
if ( !OperatingSystem.IsWindows() )
return;
var dllName = $"{Environment.CurrentDirectory}\\bin\\win64\\steam_api64.dll";
if ( !NativeLibrary.TryLoad( dllName, out steamApiDll ) )
{
throw new System.Exception( "Couldn't load bin/win64/steam_api64.dll" );
}
}
}