UNSOLVED 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 (an ObjectData 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 invoke EventAdd() 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 to true. 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 to false. 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 or SceneHookData plugin).

Cheers,
Ferdinand