Properly destroy script instances

Now without memory leaks!
This commit is contained in:
Simon Lübeß
2024-01-18 21:29:11 +01:00
parent 9befd9e910
commit c906fc9667
4 changed files with 138 additions and 46 deletions
+11 -2
View File
@@ -340,6 +340,8 @@ static class ScriptEngine
Debug.Assert(s_Context == null, "StartRuntime was called twice without StopRuntime in between!");
Context = context;
ReloadAssemblies();
}
/// Stopts the script runtime and disposes of all script instances.
@@ -400,10 +402,18 @@ static class ScriptEngine
script.ScriptClassName = null;
DestroyInstance(entityId);
}
public static void DestroyInstance(UUID entityId)
{
if (_entityScriptInstances.TryGetValue(entityId, let currentInstance))
currentInstance.ReleaseRef();
}
_entityScriptInstances[entityId] = null;
internal static void UnregisterScriptInstance(UUID entityId)
{
_entityScriptInstances.Remove(entityId);
}
/// Returns an instance that can be used as a reference to the entity with the given ID in Scripts
@@ -806,7 +816,6 @@ static class ScriptEngine
return scriptClass;
}
internal static void HandleMonoException(MonoException* exception, UUID entityId)
{
MonoExceptionHelper wrappedException = new MonoExceptionHelper(exception);
+30 -16
View File
@@ -108,6 +108,21 @@ static class ScriptGlue
}
}
/// Gets the entity with the given id. Throws a mono exception, if the entity doesn't exist.
static Entity GetEntitySafe(UUID entityId)
{
Result<Entity> foundEntity = ScriptEngine.Context.GetEntityByID(entityId);
if (foundEntity case .Ok(let entity))
{
return foundEntity;
}
else
{
ThrowArgumentException(null, "The entity doesn't exist or was deleted.");
}
}
/// Gets the component of the specified type that is attached to the given entity. Or null, if the entity doesn't exist or doesn't have the specified component.
static T* GetComponentSafe<T>(UUID entityId) where T: struct, new
{
@@ -346,15 +361,22 @@ static class ScriptGlue
[RegisterCall("ScriptGlue::Entity_SetScript")]
static MonoObject* Entity_SetScript(UUID entityId, MonoReflectionType* scriptType)
{
Entity entity = ScriptEngine.Context.GetEntityByID(entityId);
Entity entity = GetEntitySafe(entityId);
ScriptComponent* scriptComponent = null;
if (!entity.HasComponent<ScriptComponent>())
{
entity.AddComponent<ScriptComponent>();
scriptComponent = entity.AddComponent<ScriptComponent>();
}
else
{
scriptComponent = entity.GetComponent<ScriptComponent>();
}
if (entity.TryGetComponent<ScriptComponent>(let scriptComponent))
{
if (scriptComponent.Instance != null)
ScriptEngine.Context.DestroyScriptDeferred(scriptComponent.Instance, false);
scriptComponent.Instance = null;
MonoType* type = Mono.mono_reflection_type_get_type(scriptType);
@@ -368,22 +390,12 @@ static class ScriptGlue
return scriptComponent.Instance.MonoInstance;
}
Log.EngineLogger.AssertDebug(false, "Failed to set script.");
return null;
}
[RegisterCall("ScriptGlue::Entity_RemoveScript")]
static void Entity_RemoveScript(UUID entityId)
{
Entity entity = ScriptEngine.Context.GetEntityByID(entityId);
ScriptComponent* scriptComponent = GetComponentSafe<ScriptComponent>(entityId);
if (entity.TryGetComponent<ScriptComponent>(let scriptComponent))
{
ScriptEngine.DestroyInstance(entity, scriptComponent);
entity.RemoveComponent<ScriptComponent>();
}
ScriptEngine.Context.DestroyScriptDeferred(scriptComponent.Instance, true);
}
[RegisterCall("ScriptGlue::Entity_GetName")]
@@ -739,6 +751,8 @@ static class ScriptGlue
RegisterCall<function bool(half)>("Math.Half::IsInfinity_Impl", (value) => value.IsInfinity);
RegisterCall<function bool(half)>("Math.Half::IsNan_Impl", (value) => value.IsNaN);
RegisterCall<function bool(half)>("Math.Half::IsSubnormal_Impl", (value) => value.IsSubnormal);
RegisterCall<function float(float, float)>("Math.Math::Atan2", (y, x) => Math.Atan2(y, x));
}
#endregion
+14 -2
View File
@@ -39,13 +39,25 @@ class ScriptInstance : RefCounter
}
private ~this()
{
Destroy();
_scriptClass?.ReleaseRef();
}
public void Destroy()
{
if (_instance != null)
{
if (ScriptEngine.ApplicationInfo.IsInPlayMode || ScriptClass.RunInEditMode)
{
InvokeOnDestroy();
Mono.mono_gchandle_free(_gcHandle);
}
_scriptClass?.ReleaseRef();
Mono.mono_gchandle_free(_gcHandle);
_instance = null;
ScriptEngine.UnregisterScriptInstance(_entityId);
}
}
public void Instantiate(UUID uuid)
+72 -15
View File
@@ -621,6 +621,8 @@ namespace GlitchyEngine.World
private append List<Entity> _destroyQueue = .();
private append List<ScriptInstance> _destroyScriptQueue = .();
public void Update(GameTime gameTime, UpdateMode mode)
{
Debug.Profiler.ProfileRendererFunction!();
@@ -634,29 +636,71 @@ namespace GlitchyEngine.World
// Run scripts
for (let (entity, script) in _ecsWorld.Enumerate<ScriptComponent>())
{
ScriptInstance scriptInstance = null;
if (!script.IsCreated)
{
if (!script.IsInitialized)
ScriptEngine.InitializeInstance(Entity(entity, this), script);
scriptInstance = script.Instance;
// Skip OnCreate and OnUpdate invocation if we didn't create an instance
// (happens, if script component has no script class associated)
// Also skip if we are in edit mode and the class doesn't have the RunInEditMode-Attribute
if (script.Instance == null || (mode.HasFlag(.EditMode) && !script.Instance.ScriptClass.RunInEditMode))
if (scriptInstance == null || (mode.HasFlag(.EditMode) && !scriptInstance.ScriptClass.RunInEditMode))
continue;
script.Instance.InvokeOnCreate();
// OnCreate can remove the script, so we have to make sure that we have a reference and it survives.
// Todo: Technically we can rely on scriptInstance surviving a delete because we always defer deletion (unless we might not?)
scriptInstance.AddRef();
scriptInstance.InvokeOnCreate();
}
else
{
scriptInstance = script.Instance..AddRef();
}
// TODO: When the entity is destroyed in OnCreate it's OnUpdate will still be called. Is this fine?
// It would require that we somehow track whether the entity is to be deleted. We technically have this info but would probably need
// some faster way. If we for some reason ever happen to implement such a fast way we can check for planned deletion here (or after on Create and just continue;).
// Update the script, if we aren't in editor or it has RunInEditMode-Attribute
if (!mode.HasFlag(.EditMode) || script.Instance.ScriptClass.RunInEditMode)
if (!mode.HasFlag(.EditMode) || scriptInstance.ScriptClass.RunInEditMode)
{
if (_updateBlockList.Contains(entity))
_updateBlockList.Remove(entity);
else
script.Instance.InvokeOnUpdate(gameTime.DeltaTime);
scriptInstance.InvokeOnUpdate(gameTime.DeltaTime);
}
scriptInstance?.ReleaseRef();
}
}
if (!_destroyScriptQueue.IsEmpty)
{
for (let scriptInstance in _destroyScriptQueue)
{
scriptInstance.ReleaseRef();
Log.EngineLogger.AssertDebug(scriptInstance.RefCount == 1, "Too many references to script instance. Did we leak it?");
ScriptEngine.DestroyInstance(scriptInstance.EntityId);
}
_destroyScriptQueue.Clear();
}
if (!_destroyQueue.IsEmpty)
{
for (let entity in _destroyQueue)
{
DestroyEntity(entity);
}
_destroyQueue.Clear();
}
if (mode.HasFlag(.Physics))
@@ -716,17 +760,6 @@ namespace GlitchyEngine.World
}
}
}
if (!_destroyQueue.IsEmpty)
{
for (let entity in _destroyQueue)
{
DestroyEntity(entity);
}
_destroyQueue.Clear();
}
}
/// Creates a new Entity with the given name.
@@ -756,6 +789,8 @@ namespace GlitchyEngine.World
{
_idToEntity.Remove(entity.UUID);
ScriptEngine.DestroyInstance(entity.UUID);
if (destroyChildren)
{
for (Entity child in entity.EnumerateChildren)
@@ -780,6 +815,28 @@ namespace GlitchyEngine.World
}
}
/**
* Marks the given scriptInstance so that it will be deleted at the end of the update-loop.
* @param scriptInstance The script instance to destroy.
* @param removeComponent If set to true the ScriptComponent will be removed from the entity.
*/
public void DestroyScriptDeferred(ScriptInstance scriptInstance, bool removeComponent)
{
_destroyScriptQueue.Add(scriptInstance..AddRef());
if (removeComponent)
{
Result<Entity> foundEntity = GetEntityByID(scriptInstance.EntityId);
Log.EngineLogger.AssertDebug(foundEntity case .Ok, "DestroyScriptDeferred: Could not find entity.");
if (foundEntity case .Ok(let entity))
{
entity.RemoveComponent<ScriptComponent>();
}
}
}
/** Creates a copy of the given entity, including all components and children.
* @param entity the entity to copy.
* @returns the newly create entity.