Advertisement

Memory Integration Issue Related to Function Delegates

Started by May 29, 2020 11:10 PM
1 comment, last by WitchLord 4 years, 5 months ago

Hello,

Thanks for taking the time to read. I work on a team with a custom game engine. The engine itself has been in dev for over a decade, and a handful of games have been released on it, but we only recently added an AngelScript layer.

I mention that, because the issue I am having deals with the AngelScript memory management/ref counting system, and I suspect the issue I am about to describe has to do with how AngelScript has been integrated to play nice with our existing game object system.

Our engine uses fixed width object entity pools, and we use AngelScript in such a way that treats inline memory buffers (inside of the fixed width entity pool) as object instances in AngelScript. So imagine something like, I have a buffer that's 16kb, that's divided into 1k “slots”, and one of those slots can be passed to the AS context and used as the “this” pointer for a method call inside of AS.

We tried a few approaches for this, among them trying a custom allocator. But due to the fact that the custom allocation functions cannot discern the intent for the memory they are allocating, we could not solve this in a targeted enough way with a custom allocator. We also did not want to make any custom changes to AS internals in order to provide additional context to the allocator.

What we ended up doing was, at scene load time there is a “prototype” instance for each registered script type, which is constructed normally in AS. The memory for it is kept around throughout the scene, but those prototype instances are never updated or used as live objects. For objects that run in game, the scene loader memcpy's the prototype instance of the associated class into the desired memory slot in the fixed width pool, and changes around a few other metadata parameters needed to identify each unique instance.

This works well enough to treat each slot as a unique AS instance in our own object system. Each resulting slot stamped out from the prototype instance can be run in an AS context and the script will behave as expected.

However, I am almost certain this is running afoul of the expected reference counting behavior in AS… I half expect someone on this forum to be like “>:| y would u do dis?” Unfortunately the fixed object slot thing is non-negotiable, and we chose AngelScript because it pretty much works within these constraints.

Anyway, we are using config groups to manage the AS engine state. There are 3: Engine (for engine side always-there stuff), Logic (for game side always-there stuff), and Scene (for scripts which get included on a per scene basis). As one might infer, the Scene config group is begun as the scene is loading its referenced scripts then ended after that. When you load a new scene, the existing scene config group is removed (unloading the outgoing scripts), and a new one is begun to load the incoming scene's scripts. This works as expected most of the time.

I should add that all of our native binding types have reference counting turned off. We are also using asEP_ALLOW_UNSAFE_REFERENCES.

The issue we are having occurs when somebody uses object function delegates in a script. We would like to be able to use them, because they are very convenient for implementing state machines. But, if somebody creates a function delegate that points to a method on an object that exists inside the fixed width pools (read: an AS object which did not originate from within the AS engine itself), it seems like that function delegate is given refcounts that can never be resolved by the GC. Then, when the scene config group removal is attempted, the operation fails because there are still live references. At that point, it becomes basically impossible to reconcile the state between the game engine and the AS engine without fully tearing down and rebuilding the AS engine.

So, what I'm wondering is… is there a way I can still do this crazy fixed slot memory thing, and still register that memory in such a way that plays nice with the AngelScript GC? Or is there I can make the function delegates not require reference tracking?

Sorry for the extra long read, and thanks to anyone who can point me in the right direction!

It's difficult to say exactly what is going wrong without seeing the code. But I'd say you're on the right track when you say the refcounting isn't working too well when you use this copy of the memory.

I think for you it would be best to customize the angelscript a bit to allow it work nicely with specific memory blocks like you have.

Luckily I recently received a contribution from Marc, a.k.a “Foddex”, that does just this. He is having similar needs, as his engine is based on ECS and the data needs to be stored nicely in arrays for quick access.

I haven't had the time to integrate it into the SDK yet, but I'll share the patch he sent me so you can take a look at it and possibly use it yourself.

diff --git a/angelscript.h b/angelscript.h
index 69b3051..c853d46 100644
--- a/angelscript.h
+++ b/angelscript.h
@@ -750,6 +750,8 @@ public:
 	virtual void                   AddRefScriptObject(void *obj, const asITypeInfo *type) = 0;
 	virtual int                    RefCastObject(void *obj, asITypeInfo *fromType, asITypeInfo *toType, void **newPtr, bool useOnlyImplicitCast = false) = 0;
 	virtual asILockableSharedBool *GetWeakRefFlagOfScriptObject(void *obj, const asITypeInfo *type) const = 0;
+	virtual void                  CreateScriptObjectInPlace(const asITypeInfo *type, void* ptr) = 0;
+	virtual void                  DestroyScriptObjectInPlace(const asITypeInfo* type, void* ptr) = 0;

 	// Context pooling
 	virtual asIScriptContext      *RequestContext() = 0;
diff --git a/as_scriptengine.cpp b/as_scriptengine.cpp
index 56e11fb..2878235 100644
--- a/as_scriptengine.cpp
+++ b/as_scriptengine.cpp
@@ -5007,6 +5007,27 @@ int asCScriptEngine::RefCastObject(void *obj, asITypeInfo *fromType, asITypeInfo
 	return asSUCCESS;
 }

+void asCScriptEngine::CreateScriptObjectInPlace(const asITypeInfo* type, void* ptr) {
+	if (type == 0 || ptr == 0) return;
+
+	asCObjectType* objType = const_cast<asCObjectType*>(reinterpret_cast<const asCObjectType*>(type));
+	if (objType->flags & asOBJ_SCRIPT_OBJECT) {
+		ScriptObject_Construct(objType, (asCScriptObject*)ptr);
+	}
+}
+
+void asCScriptEngine::DestroyScriptObjectInPlace(const asITypeInfo* type, void* ptr) {
+	if (type == 0 || ptr == 0) return;
+
+	asCObjectType* objType = const_cast<asCObjectType*>(reinterpret_cast<const asCObjectType*>(type));
+	if (objType->flags & asOBJ_SCRIPT_OBJECT) {
+		asCScriptObject* self = (asCScriptObject*)ptr;
+		asASSERT(self->GetRefCount() == 1);		// if this fails, someone kept a reference to this object which is not allowed!
+		self->callFree = 0;						// this is hacky, but it prevents userFree from being called, this ptr is not allocated by AngelScript anyway!
+		self->Release();
+	}
+}
+
 // interface
 void *asCScriptEngine::CreateScriptObject(const asITypeInfo *type)
 {
diff --git a/as_scriptengine.h b/as_scriptengine.h
index 98cbf48..8291bb9 100644
--- a/as_scriptengine.h
+++ b/as_scriptengine.h
@@ -169,6 +169,8 @@ public:
 	virtual void                   AddRefScriptObject(void *obj, const asITypeInfo *type);
 	virtual int                    RefCastObject(void *obj, asITypeInfo *fromType, asITypeInfo *toType, void **newPtr, bool useOnlyImplicitCast = false);
 	virtual asILockableSharedBool *GetWeakRefFlagOfScriptObject(void *obj, const asITypeInfo *type) const;
+	virtual void                  CreateScriptObjectInPlace(const asITypeInfo* type, void* ptr);
+	virtual void                  DestroyScriptObjectInPlace(const asITypeInfo* type, void* ptr);

 	// Context pooling
 	virtual asIScriptContext *RequestContext();
diff --git a/as_scriptobject.cpp b/as_scriptobject.cpp
index cbb6a12..ba55eb7 100644
--- a/as_scriptobject.cpp
+++ b/as_scriptobject.cpp
@@ -338,6 +338,7 @@ asCScriptObject::asCScriptObject(asCObjectType *ot, bool doInitialize)
 	objType = ot;
 	objType->AddRef();
 	isDestructCalled = false;
+	callFree = true;
 	extra = 0;
 	hasRefCountReachedZero = false;

@@ -397,7 +398,8 @@ void asCScriptObject::Destruct()

 	// Free the memory
 #ifndef WIP_16BYTE_ALIGN
-	userFree(this);
+	if (callFree)
+		userFree(this);
 #else
 	// Script object memory is allocated through asCScriptEngine::CallAlloc()
 	// This free call must match the allocator used in CallAlloc().
diff --git a/as_scriptobject.h b/as_scriptobject.h
index cf6e134..35e650c 100644
--- a/as_scriptobject.h
+++ b/as_scriptobject.h
@@ -129,11 +129,13 @@ public:
 //=============================================
 protected:
 	friend class asCContext;
+	friend class asCScriptEngine;
 	asCObjectType    *objType;

 	mutable asCAtomic refCount;
 	mutable asBYTE    gcFlag:1;
 	mutable asBYTE    hasRefCountReachedZero:1;
+	mutable asBYTE    callFree : 1;
 	bool              isDestructCalled;

 	// Most script classes instances won't have neither the weakRefFlags nor

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

This topic is closed to new replies.

Advertisement