Cinema S26 and newer is not longer threadsafe for ObjectData?
-
We've found that some callbacks for
ObjectData::Message(GeListNode* node, Int32 type, void* data) { }
is no longer executed on the main thread at least some messages like:
MSG_MULTI_DOCUMENTIMPORTED
Since our plugin requires third-party communications executing some functions on the main thread is necessary.
Is there any way to force that call on the main thread for the newer versions?
(Notice that these calls were executed on the main thread in R25 and older versions). -
Hello @victor,
Thank you for reaching out to us. While I am fairly confident that I understand your question, I am going to be very literal in my answer to avoid some ambiguities.
Some Facts
ObjectData
was never 'thread-safe'. Or to be more precise, the classic API scene graph of Cinema 4D was never thread-safe (anObjectData
instance does not store any public data itself and therefore does not have to be accessed, i.e., falls outside of the notion of thread-safe or not).- Some methods, as for example
ObjectData::GetVirtualObjects
, are never being called from the main thread and it is therefore forbidden for example to modify the scene graph or invokeEventAdd()
or drawing calls from there. - Some methods, e.g.,
::Message()
, are being called relatively often from the main thread and can therefore be used to execute things on the main thread. - However, there is and never was any guarantee that
::Message()
will always be on the main thread. You must still always check yourself with GeIsMainThread() or GeIsMainThreadAndNoDrawThread(). Some code could always send a message from any thread to your object. - The fact that
MSG_DOCUMENTINFO
can now be broadcasted outside of the main thread is likely tied to async scene loading introduced with S26.
The Problem with your Question
You state that '[your] plugin requires third-party communications' and therefore assert that 'executing some functions on the main thread is necessary' for you. I do not understand what you mean by that. You should describe more precisely what you want to do in that message function.
- Do you want to modify scene graph data; add/remove objects, tags, shaders, etc. ?
- Do you want to modify your own global data?
- Do you want to communicate with a server or something like this?
You should really clarify what your goals are and ideally share code with us either here or via sdk_suppport(at)maxon(dot).net
. Otherwise it will be very hard to help you.Possible Solutions
What the right solution is for you depends heavily on what you want to do. What however is not possible, is to force your
::Message
method only being called from the main thread.Share Write Access to your own Global Data
To do that, you should use a lock/semaphore, so that only one entity/thread at a time can access your data, e.g., write to a global log file or send data to a server. There are classic API and maxon API types which can help you with that. It is strongly recommend to use them over similar functions of the
std
library.Modify the Scene Graph
You must defer the execution of your code to the main thread here. There are in principle two ways to achieve that.
Defer by waiting: The simplest pattern is to simply store the notion that you want to do X on the main thread when you encounter the the state Y in a non-main thread. In the simplest form this could be a private field
Bool _doX;
on your object hook which you then set totrue
. The next time::Message
is being called and you are on the main thread, you then simply carry out doing X and then set the field back tofalse
. The disadvantage of this approach is that you have no control over when X is actually carried out, as you must wait for something else calling your object hook on the main thread. The advantage is that you do not hold up everything else as with the second method.Defer with ExecuteOnMainThread: With this function you defer the execution of a lambda/delegate to the main thread. The advantage of this approach is that the changes are carried out immediately (at least relatively) and you can directly 'carry on' in your code. The disadvantage is that that the function is based on
maxon::JobInterface
, i.e., there is a queue of jobs on the main thread which are solved sequentially, and you might not be first in line. Also, this is by definition blocking. So, when you are inside a thread Y which has been optimized for speed, and you then defer the computationally heavy task X to the main thread, first wait for other things to be done, and then do your task X, the thread Y is waiting all that time for you and with it everything that relies on that thread. This does not mean that you should not use the function, but you should be careful.It could look something like this (untested pseudo-code):
MyObjectData::Message(GeListNode* node, Int32 type, void* data) { // The #something event has happened, and we are not on the main thread. if ((type == ID_SOMETHING) && !GeIsMainThreadAndNoDrawThread()) { // We add a cube to the document of #node from the main thread. maxon::ExecuteOnMainThread([&node]() { iferr_scope_handler { err.CritStop(); return; }; BaseDocument* const doc = node.GetDocument(); BaseObject* const cube = BaseObject::Alloc(Ocube); if (!doc || !cube) return maxon::UnexpectedError(MAXON_SOURCE_LOCATION); doc->InsertObject(cube, nullptr, nullptr); }, maxon::WAITMODE::DONT_WAIT); } // Other code ... return SUPER::Message(node, type, data); }
You could also use other more complex approaches here, but they are always just a variation of these two (e.g., use a
MessageData
orSceneHookData
plugin).Cheers,
Ferdinand