diff --git a/sources/Memory/MemoryAPI.uc b/sources/Memory/MemoryAPI.uc index 777211e..139cd32 100644 --- a/sources/Memory/MemoryAPI.uc +++ b/sources/Memory/MemoryAPI.uc @@ -209,6 +209,10 @@ public final function Free(Object objectToDeallocate) if (!objectAsAcediaObject.IsAllocated()) { return; } + objectAsAcediaObject._deref(); + if (objectAsAcediaObject._getRefCount() > 0) { + return; + } relevantPool = objectAsAcediaObject._getPool(); objectAsAcediaObject._finalizer(); } @@ -217,6 +221,10 @@ public final function Free(Object objectToDeallocate) if (!objectAsAcediaActor.IsAllocated()) { return; } + objectAsAcediaActor._deref(); + if (objectAsAcediaActor._getRefCount() > 0) { + return; + } objectAsAcediaActor._finalizer(); } // Try to store freed object in a pool diff --git a/sources/Memory/Tests/TEST_Memory.uc b/sources/Memory/Tests/TEST_Memory.uc index 40f289c..e32b77e 100644 --- a/sources/Memory/Tests/TEST_Memory.uc +++ b/sources/Memory/Tests/TEST_Memory.uc @@ -27,6 +27,7 @@ protected static function TESTS() Test_ActorConstructorsFinalizers(); Test_ObjectPoolUsage(); Test_LifeVersionIsUnique(); + Test_RefCounting(); } protected static function Test_LifeVersionIsUnique() @@ -164,6 +165,116 @@ protected static function Test_ObjectPoolUsage() TEST_ExpectTrue(obj1 != obj2); } +protected static function Test_RefCounting() +{ + Context("Testing usage of reference counting."); + SubTest_RefCountingObjectFreeSelf(); + SubTest_RefCountingObjectFree(); + SubTest_RefCountingActorFreeSelf(); + SubTest_RefCountingActorFree(); +} + +protected static function SubTest_RefCountingObjectFreeSelf() +{ + local MockObject temp; + Issue("Reference counting for `AcediaObject`s does not work correctly" + @ "with `FreeSelf()`"); + temp = MockObject(__().memory.Allocate(class'MockObject')); + temp.NewRef().NewRef().NewRef(); + TEST_ExpectTrue(temp._getRefCount() == 4); + TEST_ExpectTrue(temp.IsAllocated()); + temp.FreeSelf(); + TEST_ExpectTrue(temp._getRefCount() == 3); + TEST_ExpectTrue(temp.IsAllocated()); + temp.FreeSelf(); + TEST_ExpectTrue(temp._getRefCount() == 2); + TEST_ExpectTrue(temp.IsAllocated()); + temp.FreeSelf(); + TEST_ExpectTrue(temp._getRefCount() == 1); + TEST_ExpectTrue(temp.IsAllocated()); + temp.FreeSelf(); + TEST_ExpectTrue(temp._getRefCount() == 0); + TEST_ExpectFalse(temp.IsAllocated()); +} + +protected static function SubTest_RefCountingObjectFree() +{ + local MockObject temp; + Issue("Reference counting for `AcediaObject`s does not work correctly" + @ "with `__().memory.Free()`"); + temp = MockObject(__().memory.Allocate(class'MockObject')); + temp.NewRef().NewRef().NewRef(); + TEST_ExpectTrue(temp._getRefCount() == 4); + TEST_ExpectTrue(temp.IsAllocated()); + __().memory.Free(temp); + TEST_ExpectTrue(temp._getRefCount() == 3); + TEST_ExpectTrue(temp.IsAllocated()); + __().memory.Free(temp); + TEST_ExpectTrue(temp._getRefCount() == 2); + TEST_ExpectTrue(temp.IsAllocated()); + __().memory.Free(temp); + TEST_ExpectTrue(temp._getRefCount() == 1); + TEST_ExpectTrue(temp.IsAllocated()); + __().memory.Free(temp); + TEST_ExpectTrue(temp._getRefCount() == 0); + TEST_ExpectFalse(temp.IsAllocated()); +} + +protected static function SubTest_RefCountingActorFreeSelf() +{ + local MockActor temp; + class'MockActor'.default.actorCount = 0; + Issue("Reference counting for `AcediaActor`s does not work correctly" + @ "with `FreeSelf()`"); + temp = MockActor(__().memory.Allocate(class'MockActor')); + temp.NewRef().NewRef().NewRef(); + TEST_ExpectTrue(class'MockActor'.default.actorCount == 1); + TEST_ExpectTrue(temp._getRefCount() == 4); + TEST_ExpectTrue(temp.IsAllocated()); + temp.FreeSelf(); + TEST_ExpectTrue(class'MockActor'.default.actorCount == 1); + TEST_ExpectTrue(temp._getRefCount() == 3); + TEST_ExpectTrue(temp.IsAllocated()); + temp.FreeSelf(); + TEST_ExpectTrue(class'MockActor'.default.actorCount == 1); + TEST_ExpectTrue(temp._getRefCount() == 2); + TEST_ExpectTrue(temp.IsAllocated()); + temp.FreeSelf(); + TEST_ExpectTrue(class'MockActor'.default.actorCount == 1); + TEST_ExpectTrue(temp._getRefCount() == 1); + TEST_ExpectTrue(temp.IsAllocated()); + temp.FreeSelf(); + TEST_ExpectTrue(class'MockActor'.default.actorCount == 0); +} + +protected static function SubTest_RefCountingActorFree() +{ + local MockActor temp; + class'MockActor'.default.actorCount = 0; + Issue("Reference counting for `AcediaActor`s does not work correctly" + @ "with `Free()`"); + temp = MockActor(__().memory.Allocate(class'MockActor')); + temp.NewRef().NewRef().NewRef(); + TEST_ExpectTrue(class'MockActor'.default.actorCount == 1); + TEST_ExpectTrue(temp._getRefCount() == 4); + TEST_ExpectTrue(temp.IsAllocated()); + __().memory.Free(temp); + TEST_ExpectTrue(class'MockActor'.default.actorCount == 1); + TEST_ExpectTrue(temp._getRefCount() == 3); + TEST_ExpectTrue(temp.IsAllocated()); + __().memory.Free(temp); + TEST_ExpectTrue(class'MockActor'.default.actorCount == 1); + TEST_ExpectTrue(temp._getRefCount() == 2); + TEST_ExpectTrue(temp.IsAllocated()); + __().memory.Free(temp); + TEST_ExpectTrue(class'MockActor'.default.actorCount == 1); + TEST_ExpectTrue(temp._getRefCount() == 1); + TEST_ExpectTrue(temp.IsAllocated()); + __().memory.Free(temp); + TEST_ExpectTrue(class'MockActor'.default.actorCount == 0); +} + + defaultproperties { caseGroup = "Memory" diff --git a/sources/Types/AcediaActor.uc b/sources/Types/AcediaActor.uc index b7ae836..ca55b4c 100644 --- a/sources/Types/AcediaActor.uc +++ b/sources/Types/AcediaActor.uc @@ -26,6 +26,12 @@ class AcediaActor extends Actor // Reference to Acedia's APIs for simple access. var protected Global _; +// To make logic simpler and increase efficiency, we allow storing a reference +// to any actors in many different places. To know when we can actually +// deallocate an actor, we keep this reference counter and only move actor +// to the actor pool once nothing refers to it anymore. +var private int _refCounter; + // Store allocation status to prevent possible issues // with freeing the same object several times without reallocating it // (such as preventing finalizers or constructors being called several times) @@ -66,6 +72,7 @@ public function _constructor() { if (_isAllocated) return; _isAllocated = true; + _refCounter = 1; _ = class'Global'.static.GetInstance(); if (!default._staticConstructorWasCalled) { @@ -89,6 +96,7 @@ public function _finalizer() { if (!_isAllocated) return; _isAllocated = false; + _refCounter = 0; Finalizer(); _ = none; } @@ -179,7 +187,53 @@ private final static function CreateTextCache(optional bool forceCreation) } /** - * Acedia actors cannot be deallocated into an object pool, but they still + * This function is called each time this object is freed, to decrease it + * internal reference counter and know when it can be actually deallocated. + * + * AVOID MANUALLY CALLING IT. + */ +public final function _deref() +{ + if (!_isAllocated) { + return; + } + _refCounter = Max(0, _refCounter - 1); +} + +/** + * This function returns current reference counter for the caller actor. + * It is an amount of times it can be freed before being deallocated. + * This should correspond to the amount of places that reference it. + * + * AVOID MANUALLY CALLING IT. + */ +public final function int _getRefCount() +{ + if (!_isAllocated) { + return 0; + } + return _refCounter; +} + +/** + * Method that creates new reference to the given actor. + * Call this if you do not have ownership over the actor, but want to store + * somewhere - this way it should not get deallocated until you free your + * own reference. + * + * @return Caller actor, to allow for easier use. + */ +public final function AcediaActor NewRef() +{ + if (!_isAllocated) { + return none; + } + _refCounter = Max(0, _refCounter + 1); + return self; +} + +/** + * Acedia actors cannot be deallocated into an actor pool, but they still * support constructors and destructors and, therefore, track their own * allocation status (`AcediaActor` is considered allocated between constructor * and finalizer calls). diff --git a/sources/Types/AcediaObject.uc b/sources/Types/AcediaObject.uc index 0d955f3..6938f27 100644 --- a/sources/Types/AcediaObject.uc +++ b/sources/Types/AcediaObject.uc @@ -35,6 +35,12 @@ var public const bool usesObjectPool; // This value can be changes through Acedia's system settings. var public const int defaultMaxPoolSize; +// To make logic simpler and increase efficiency, we allow storing a reference +// to any objects in many different places. To know when we can actually +// deallocate an object, we keep this reference counter and only move object +// to the object pool once nothing refers to it anymore. +var private int _refCounter; + // Same object can be reallocated for different purposes and, as far as // users are concerned, - it should be considered a different object after each // reallocation. @@ -106,9 +112,12 @@ public final static function AcediaObjectPool _getPool() */ public final function _constructor() { - if (_isAllocated) return; + if (_isAllocated) { + return; + } _isAllocated = true; _lifeVersion += 1; + _refCounter = 1; _ = class'Global'.static.GetInstance(); if (!default._staticConstructorWasCalled) { @@ -131,6 +140,7 @@ public final function _finalizer() { if (!_isAllocated) return; _isAllocated = false; + _refCounter = 0; Finalizer(); _ = none; } @@ -220,6 +230,52 @@ private final static function CreateTextCache(optional bool forceCreation) } } +/** + * This function is called each time this object is freed, to decrease it + * internal reference counter and know when it can be actually deallocated. + * + * AVOID MANUALLY CALLING IT. + */ +public final function _deref() +{ + if (!_isAllocated) { + return; + } + _refCounter = Max(0, _refCounter - 1); +} + +/** + * This function returns current reference counter for the caller object. + * It is an amount of times it can be freed before being deallocated. + * This should correspond to the amount of places that reference it. + * + * AVOID MANUALLY CALLING IT. + */ +public final function int _getRefCount() +{ + if (!_isAllocated) { + return 0; + } + return _refCounter; +} + +/** + * Method that creates new reference to the given object. + * Call this if you do not have ownership over the object, but want to store + * somewhere - this way it should not get deallocated until you free your + * own reference. + * + * @return Caller object, to allow for easier use. + */ +public final function AcediaObject NewRef() +{ + if (!_isAllocated) { + return none; + } + _refCounter = Max(0, _refCounter + 1); + return self; +} + /** * Acedia objects can be deallocated into an object pool to be reused later and * such instances should not be used while in the pool.