Files
sbox-public/engine/Sandbox.Engine/Resources/Model/Model.cs
Lorenz Junglas 54932b6725 (Shutdown) Leak Fixes (#4242)
* 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.
2026-03-09 17:02:27 +01:00

148 lines
3.0 KiB
C#

using NativeEngine;
using Sandbox.Engine;
using Sandbox.Engine.Utility.RayTrace;
namespace Sandbox;
/// <summary>
/// A model.
/// </summary>
public sealed partial class Model : Resource
{
internal IModel native;
internal bool procedural;
public override bool IsValid => native.IsValid;
/// <summary>
/// Private constructor, use <see cref="FromNative(IModel, bool, string)"/>
/// </summary>
private Model( IModel native, string name, bool procedural )
{
if ( native.IsNull ) throw new Exception( "Model pointer cannot be null!" );
this.native = native;
this.Name = name;
this.procedural = procedural;
RegisterWeakResourceId( Name );
}
internal override void Destroy()
{
if ( !native.IsNull )
{
var path = ResourcePath;
var n = native;
native = default;
MainThread.Queue( () =>
{
n.DestroyStrongHandle();
} );
}
base.Destroy();
}
/// <summary>
/// Called when the resource is reloaded. We should clear any cached values.
/// </summary>
internal override void OnReloaded()
{
_data?.Dispose();
_data = null;
_physics?.Dispose();
_physics = default;
_morphs?.Dispose();
_morphs = null;
_attachments?.Dispose();
_attachments = default;
_animationNames?.Clear();
_animationNames = default;
_sequenceNames?.Clear();
_sequenceNames = default;
_materials = default;
_bones = default;
_hitboxset = default;
_parts?.Dispose();
_parts = default;
DataCache?.Clear();
BaseModel = default;
IToolsDll.Current?.RunEvent( "model.reload", this );
foreach ( var scene in Scene.All )
{
using var scope = scene.Push();
var components = scene.GetAllComponents<IHasModel>();
foreach ( var c in components )
{
if ( c.Model != this ) continue;
c.OnModelReloaded();
}
}
}
/// <summary>
/// Whether this model is an error model or invalid or not.
/// </summary>
public bool IsError => native.IsNull || !native.IsStrongHandleValid() || native.IsError();
/// <summary>
/// Name of the model, usually being its file path.
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// Whether this model is procedural, i.e. it was created at runtime via <see cref="ModelBuilder.Create"/>.
/// </summary>
public bool IsProcedural => procedural;
/// <summary>
/// Total number of meshes this model is made out of.
/// </summary>
public int MeshCount => native.GetNumMeshes();
/// <summary>
/// Trace against the triangles in this mesh
/// </summary>
public MeshTraceRequest Trace => new() { targetModel = this };
/// <summary>
/// Base model of this model if one was used.
/// </summary>
internal Model BaseModel
{
get => field ??= FromNative( native.GetBaseModel() );
set;
}
}
internal interface IHasModel
{
/// <summary>
/// The <see cref="Model"/> associated with this object.
/// </summary>
Model Model { get; }
/// <summary>
/// Called by the engine when the associated <see cref="Model"/> is reloaded.
/// Implementing classes should clear or update any cached data that depends on the model.
/// </summary>
void OnModelReloaded();
}