Thread Safety

3ds Max is not thread-safe. You should not call any function from multiple threads concurrently. That said plug-ins that relate to the renderer have to implement specific methods in a thread-safe manner. This is because the renderer launches several threads of execution when rendering an image. Therefore the same method could get called from several threads at the same time. If one of the methods is writing a value that the other is reading or writing this can interfere with valid values being accessed.

Plug-in materials, texture maps and atmospheric effects are the plug-in types that have methods that need to be. Any method of these plug-ins that is called while rendering, like Texmap::EvalColor(), Mtl::Shade(), Atmospheric::Shade(), etc. must be written in a way that is thread safe.

The methods listed above can be re-entered from another thread while the renderer is processing. For example, while Texmap::EvalColor() was executing it could get called again asynchronously. At a certain time this method could be changing a variable, and another thread could be changing that same variable at the same time. Or it could be setting the value while another thread is attempting to read it. This can cause invalid values to be set or read.

The main rule for class variables is if a method changes a variable it won't be thread safe unless it takes specific steps to ensure it is. Any variables in the class must not be written to without taking steps. This is because both threads can see the same variable and could write to it at the same time.

How To Ensure a Function is Thread Safe

The code fragments below are from the 3ds Max fog atmospheric effect. The code for this plug-in can be found in the SDK in \MAXSDK\HOWTO\SOURCECODEEXAMPLES\FOG.CPP . The synchronization object used to make the method thread safe here is called CRITICAL_SECTION. Developers can look in the Win32 API for documentation for this and other synchronization objects.

First a developer declares a CRITICAL_SECTION. This is a data type that is declared, initialized, and deleted when one is finished with it. When the class is created, like in the class constructor this object is initialized. Below are a pair of code fragment from the Fog Plug-in where the CRITICAL_SECTION is declared and initialized:

This creates the CRITICAL_SECTION object.

class FogAtmos : public StdFog
{
   private:
   CRITICAL_SECTION csect;
   //...
}

Here it is initialized in the constructor.

FogAtmos::FogAtmos()
{
   //...
   InitializeCriticalSection(&csect);
   //...
}

In code where data that is accessible to several threads is to be modified, you call a Win32 API named EnterCriticalSection() and pass the CRITICAL_SECTION object.

For example, thread A enters the code and EnterCriticalSection() is called. This sets a state in the CRITICAL_SECTION data structure. Now thread B enters the same section of code. It also calls EnterCriticalSection() and passes in the CRITICAL_SECTION object. The CRITICAL_SECTION object knows that another thread is already in that code. Therefore it will sit and wait until thread A finishes and calls LeaveCriticalSection(). When thread A calls LeaveCriticalSection() this will trigger thread B that it may return from EnterCriticalSection(). At this point it can then enter the code. Thus bracketing code with EnterCriticalSection() and LeaveCriticalSection() will make the code thread safe.

Below is a fragment from a method of the fog plug-in that is made thread safe using this technique.

void FogAtmos::UpdateCaches(TimeValue t)
{
   EnterCriticalSection(&csect);
   if (!valid.InInterval(t))
   {
     valid = FOREVER;
     pblock->GetValue(PB_COLOR,t,fogColor,valid);
     //...
   }
   LeaveCriticalSection(&csect);
}

Note that at the beginning of the method EnterCriticalSection() is called. Just before it exits LeaveCriticalSection() is called.

When a plug-in uses a CRITICAL_SECTION object it can call DeleteCriticalSection(). Here this is done in the destructor for the Fog Plug-in:

~FogAtmos()
{
   DeleteCriticalSection(&csect);
}

Key Points when using Critical Sections

1. Critical sections, or any other synchronization objects, should only surround the part of the code that requires synchronization. Otherwise, if the critical section surrounds a large block of computationally intensive code unnecessarily, performance will suffer.

As an example, if an algorithm needs to write to a class variable, do a long computation, then write to another class variable, it would be bad to structure the code like this:

 EnterCritical Section
 writeto var1
 dolong computation...
 writeto var2
 LeaveCritical Section

This structure stops any other processor from accessing this routine until the long computation is done. It would be better to structure the code like this:

 EnterCritical Section
 writeto var1
 LeaveCritical Section
 dolong computation...
 EnterCritical Section
 writeto var2
 LeaveCritical Section

2. Synchronization problems often don't show up until you are running on a multi-processor system. All plug-in developers should test their code on multi-processor systems as much as possible. Intermittent crashes or erratic behavior is often a symptom of non-reentrancy.

  1. Critical sections must be private. If they are not then synchronization cannot be guaranteed.

Single Threaded Sub-Systems of 3ds Max

3ds Max's reference system and node evaluation system are both single threaded. Thus trying to work with these sub-systems from multiple threads is unsafe and can lead to issues such as race conditions or crashes.