Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login
    1. Maxon Developers Forum
    2. Aprecigout
    A
    • Profile
    • Following 0
    • Followers 0
    • Topics 1
    • Posts 6
    • Best 0
    • Controversial 0
    • Groups 0

    Aprecigout

    @Aprecigout

    0
    Reputation
    3
    Profile views
    6
    Posts
    0
    Followers
    0
    Following
    Joined Last Online

    Aprecigout Unfollow Follow

    Latest posts made by Aprecigout

    • RE: ExecutePasses performance keeps slowing down per-frame and per-run — possible cache issue?

      Hey @ferdinand

      Thank you so much for another extremely thorough reply! 😮

      As you pointed out in your previous post, I had never really tested with normal playback. In real‑world usage I guess the artists never noticed the issue because—on a 20‑minute+ scenography—they eventually stop playing the timeline in real time and just jump to key moments or scrub quickly.

      I’m aware that our MoGraph usage is a bit “extreme”, but I’m relieved a bug has been identified; it matches the artists’ feeling that “it didn’t behave like this before.”
      That said, drone shows have grown in duration, drone count, and choreography complexity in recent years, so that is probably a big factor too. 😁

      Thanks for the practical tips for the artists; I have a meeting with them later today and will brief them on the “double use of effectors in different groups” pitfall you mentioned.

      Regarding the solution: we had looked into that approach before, but apparently not correctly. Retesting with your Python code gave us significant performance gains—many thanks!

      Note: I’m aware of the limits of parallelisation, but for now that’s not an issue: every scenography is processed the same way and we don’t have any simulations. Still, I’ll keep a single‑threaded fallback in mind in case we need it later. (Our artists always have new ideas 🤣)

      Besides that, I have a small question, this morning I ran more tests and tried using a clone of the document inside each thread [1] instead of re‑loading the document [3]. On a heavy scenography the load/destroy step takes a noticeable chunk of time, whereas cloning gives even better performance [2] vs. the reload approach [4].
      Is there any downside to the usage of document cloning—e.g. hidden memory cost or something else I should watch out for?

      Thanks again for your time and the deep analysis; I’m always blown away by the quality of support here.
      Looking forward for the 2026! 😄

      Attachments

      [1] Code using LoadDocuments (little update of your script to handle a pool of thread in whole scene duration)

      """Builds the passes for all frames of a document in chunks in parallel.
      """
      
      import c4d
      import mxutils
      import time
      
      IS_DEBUG: bool = True
      
      class PassesThread (c4d.threading.C4DThread):
          """Executes the passes for a given document in a given frame range.
          """
          def __init__(self, file: str, fps: int, minFrame: int, maxFrame: int) -> None:
              """Initializes the thread with the document and frame range to process.
              """
              self._file: str = mxutils.CheckType(file, str)
              self._fps: int = mxutils.CheckType(fps, int)
              self._minFrame: int = mxutils.CheckType(minFrame, int)
              self._maxFrame: int = mxutils.CheckType(maxFrame, int)
              self._result: bool = False
      
              # Start the thread once it has been instantiated. We could also do this from the outside, 
              # and sometimes that is preferable, but I went with this option here.
              self.Start()
              print(f"PassesThread initialized for file {self._file} from frame {self._minFrame} to {self._maxFrame}.")
      
          @mxutils.TIMEIT(enabled=IS_DEBUG)
          def Main(self) -> None:
              """Called by Cinema 4D as the payload-method of the thread when it is started.
      
              We execute the passes for the given document in the given frame range and also load and
              unload the document for the chunk we process. This is necessary to avoid MoGraph issue and
              also to parallelize the execution of the passes (each pass needs its own document so that
              we can work on different frame ranges in parallel).
              """
              doc: c4d.documents.BaseDocument = c4d.documents.LoadDocument(
                  self._file, c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS)
              
              for f in range(self._minFrame, self._maxFrame + 1):
                  doc.SetTime(c4d.BaseTime(f, self._fps))
                  if not doc.ExecutePasses(self.Get(), True, True, True, c4d.BUILDFLAGS_EXPORTONLY):
                      break
      
              # I did not trust my own code here, :D, as the speed ups seemed to be too good to be true.
              # So, I checked if at least the last frame built everything correctly (to some extent). With 
              # 10 threads my CPU was at ~90% all the time and begging for mercy :D
      
              # didBuildCaches: bool = True
              # for obj in mxutils.IterateTree(doc.GetFirstObject()):
              #     # We are ignoring deform caches as it would be more complicated to figure out if 
              #     # something should have a deform cache or not. But we can check all generators for
              #     # having a cache. This of course does not check caches in caches (we would need 
              #     # mxutils.RecurseGraph for that), but this is good enough as a sanity check.
              #     if obj.GetInfo() & c4d.OBJECT_GENERATOR and not obj.GetCache():
              #         didBuildCaches = False
      
              # print(f"Did build caches: {didBuildCaches}")
      
              c4d.documents.KillDocument(doc)
              self._result = True
      
          @property
          def DidSucceed(self) -> bool:
              """Returns whether the passes were executed successfully.
              """
              return self._result
      
      @mxutils.TIMEIT(enabled=IS_DEBUG)
      def run() -> None: # Named run so that we can more easily distinguish it PassesThread.Main.
          """
          """
          file: str = c4d.storage.LoadDialog(c4d.FILESELECTTYPE_SCENES, "Select a scene file", c4d.FILESELECT_LOAD)
          if not file:
              print("No file selected.")
              return
          
          # Get the documents animation data to determine the frame range.
          doc: c4d.documents.BaseDocument = c4d.documents.LoadDocument(file, c4d.SCENEFILTER_NONE)
          fps: int = doc.GetFps()
          minFrame: int = doc[c4d.DOCUMENT_MINTIME].GetFrame(fps)
          maxFrame: int = doc[c4d.DOCUMENT_MAXTIME].GetFrame(fps)
      
          # Split up frames into chunks of X frames to avoid memory issues with very large scenes. There 
          # is currently a bug in MoGraph scenes that causes slow-downs the longer they get. In the usual
          # frame range of a few hundred frames, this is not very noticeable, but it becomes noticeable
          # for thousands or even tens of thousands of frames. So, we split things into chunk and unload
          # the document in between. The chunk size can be adjusted to taste, from the very brief tests
          # I did, 1000 frames seems to be a good value, after that things start to slow down.
          #
          # We created a fix for this MoGraph bug and it will likely be contained in a future release of
          # Cinema 4D. But as always, we cannot guarantee that this will be delivered at all or when it 
          # will be delivered, as things can always change last minute.
          chunkSize: int = 1000
          chunks: list[tuple[int, int]] = [
              (minFrame + i * chunkSize, min(maxFrame, minFrame + (i + 1) * chunkSize - 1))
              for i in range((maxFrame - minFrame + chunkSize) // chunkSize)
          ]
      
          # Thread pool of 10 threads
          maxConcurrentThreads: int = 10
          totalChunks: int = len(chunks)
          completedChunks: int = 0
          chunksToProcess: list[tuple[int, int]] = chunks.copy()
          
          # I did also sketch out the threading subject we already talked about. As mentioned, Cinema 4D 
          # will parallelize scene execution on its own. We cannot make one pass go faster. But your scene 
          # is relatively lightweight per frame, so I end up only using about 20% of my CPU when I run 
          # this frame by frame (as there is nothing left to optimize for Cinema 4D in that one frame).
          #
          # What is special about your scene, is that each frame is independent of the other frames. I.e.,
          # you can just jump to a frame X and execute its passes and get the correct result. That would 
          # for example not work for a scene which contains a (non-cached) simulation, e.g., a cloth or
          # pyro simulation. In that case, you would have to run the passes for all frames up to
          # frame X to get the correct result for frame X. 
      
      
          # So, what we are doing is parallelize the execution in its width, we are working on multiple 
          # chunks of frame ranges at the same time. This is not a speed-up for one frame, but it is a 
          # speed-up for the whole scene.
          
          # Create threads for each chunk and directly start them. Then loop over all the threads, 
          # removing them one by one as they finish, until all threads are done. Each frame chunk will
          # be processed in its own thread in parallel to the other chunks.
          activeThreads: list[PassesThread] = []
          
          print(f"Total chunks to process: {totalChunks}")
          
          for i in range(min(maxConcurrentThreads, len(chunksToProcess))):
              startFrame, endFrame = chunksToProcess.pop(0)
              thread = PassesThread(file, fps, startFrame, endFrame)
              activeThreads.append(thread)
          
          while activeThreads or chunksToProcess:
              # Update status
              totalFrames = maxFrame - minFrame + 1
              processedFrames = completedChunks * chunkSize
              c4d.gui.StatusSetText(f"Handling scene chunks ({totalFrames} Frames) ({completedChunks}/{totalChunks})")
              
              # Check if thread finished
              finishedThreads = []
              for thread in activeThreads:
                  if not thread.IsRunning():
                      if thread.DidSucceed:
                          completedChunks += 1
                          print(f"Chunk completed successfully. Progress: {completedChunks}/{totalChunks}")
                      else:
                          print(f"Chunk failed. Progress: {completedChunks}/{totalChunks}")
                      finishedThreads.append(thread)
              
              # Remove finished threads
              for thread in finishedThreads:
                  activeThreads.remove(thread)
              
              # Starts new thread to fill pool if scene is not ended
              while len(activeThreads) < maxConcurrentThreads and chunksToProcess:
                  startFrame, endFrame = chunksToProcess.pop(0)
                  thread = PassesThread(file, fps, startFrame, endFrame)
                  activeThreads.append(thread)
                  print(f"Started new thread for chunk {startFrame}-{endFrame}. Active threads: {len(activeThreads)}")
              
              time.sleep(1.0)
          
          c4d.gui.StatusSetText(f"Scene processing completed ({totalFrames} Frames) ({totalChunks}/{totalChunks})")
          print(f"All chunks processed. Total: {completedChunks}/{totalChunks}")
      
      if __name__ == '__main__':
          run()
      

      [2] Performance with Load/Destroy (i stripped some prints)

      TIMEIT: Ran 'Main()' in 17.60864 sec
      TIMEIT: Ran 'Main()' in 18.19359 sec
      TIMEIT: Ran 'Main()' in 19.2093 sec
      TIMEIT: Ran 'Main()' in 19.47807 sec
      TIMEIT: Ran 'Main()' in 19.86996 sec
      TIMEIT: Ran 'Main()' in 20.04131 sec
      TIMEIT: Ran 'Main()' in 20.18938 sec
      TIMEIT: Ran 'Main()' in 20.5281 sec
      TIMEIT: Ran 'Main()' in 20.80194 sec
      TIMEIT: Ran 'Main()' in 21.14046 sec
      TIMEIT: Ran 'Main()' in 5.43413 sec
      TIMEIT: Ran 'Main()' in 6.53501 sec
      TIMEIT: Ran 'Main()' in 5.20132 sec
      TIMEIT: Ran 'Main()' in 5.41878 sec
      TIMEIT: Ran 'Main()' in 5.54765 sec
      TIMEIT: Ran 'Main()' in 4.66633 sec
      TIMEIT: Ran 'Main()' in 4.71172 sec
      TIMEIT: Ran 'Main()' in 4.75155 sec
      TIMEIT: Ran 'Main()' in 4.82119 sec
      TIMEIT: Ran 'Main()' in 5.14475 sec
      TIMEIT: Ran 'Main()' in 5.01931 sec
      TIMEIT: Ran 'Main()' in 5.38826 sec
      TIMEIT: Ran 'Main()' in 4.73412 sec
      TIMEIT: Ran 'Main()' in 4.9887 sec
      TIMEIT: Ran 'Main()' in 5.12512 sec
      TIMEIT: Ran 'Main()' in 5.30819 sec
      TIMEIT: Ran 'Main()' in 5.43618 sec
      TIMEIT: Ran 'Main()' in 5.52055 sec
      TIMEIT: Ran 'Main()' in 5.71583 sec
      TIMEIT: Ran 'Main()' in 4.44371 sec
      TIMEIT: Ran 'Main()' in 3.422 sec
      TIMEIT: Ran 'Main()' in 2.4513 sec
      TIMEIT: Ran 'Main()' in 3.48958 sec
      TIMEIT: Ran 'Main()' in 3.50926 sec
      TIMEIT: Ran 'Main()' in 3.54951 sec
      All chunks processed. Total: 35/35
      --------------------------------------------------------------------------------
      TIMEIT: Ran 'run()' in 47.51687 sec
      

      [3] Code using doc.GetClone()

      """Builds the passes for all frames of a document in chunks in parallel.
      """
      
      import c4d
      import mxutils
      import time
      
      IS_DEBUG: bool = True
      
      class PassesThread (c4d.threading.C4DThread):
          """Executes the passes for a given document in a given frame range.
          """
          def __init__(self, originalDoc: c4d.documents.BaseDocument, fps: int, minFrame: int, maxFrame: int) -> None:
              """Initializes the thread with the document and frame range to process.
              """
              self._originalDoc = mxutils.CheckType(originalDoc, c4d.documents.BaseDocument)
              self._fps: int = mxutils.CheckType(fps, int)
              self._minFrame: int = mxutils.CheckType(minFrame, int)
              self._maxFrame: int = mxutils.CheckType(maxFrame, int)
              self._result: bool = False
      
              # Start the thread once it has been instantiated. We could also do this from the outside, 
              # and sometimes that is preferable, but I went with this option here.
              self.Start()
              print(f"PassesThread initialized from frame {self._minFrame} to {self._maxFrame}.")
      
          @mxutils.TIMEIT(enabled=IS_DEBUG)
          def Main(self) -> None:
              doc = self._originalDoc.GetClone(c4d.COPYFLAGS_DOCUMENT)
              
              for f in range(self._minFrame, self._maxFrame + 1):
                  doc.SetTime(c4d.BaseTime(f, self._fps))
                  if not doc.ExecutePasses(self.Get(), True, True, True, c4d.BUILDFLAGS_EXPORTONLY):
                      break
      
          @property
          def DidSucceed(self) -> bool:
              """Returns whether the passes were executed successfully.
              """
              return self._result
      
      @mxutils.TIMEIT(enabled=IS_DEBUG)
      def run() -> None:
          doc = c4d.documents.GetActiveDocument()
          fps: int = doc.GetFps()
          minFrame: int = doc[c4d.DOCUMENT_MINTIME].GetFrame(fps)
          maxFrame: int = doc[c4d.DOCUMENT_MAXTIME].GetFrame(fps)
          
          chunkSize: int = 1000
          chunks: list[tuple[int, int]] = [
              (minFrame + i * chunkSize, min(maxFrame, minFrame + (i + 1) * chunkSize - 1))
              for i in range((maxFrame - minFrame + chunkSize) // chunkSize)
          ]
      
          # Thread pool with maximum 10 concurrent threads
          maxConcurrentThreads: int = 10
          totalChunks: int = len(chunks)
          completedChunks: int = 0
          chunksToProcess: list[tuple[int, int]] = chunks.copy()
          activeThreads: list[PassesThread] = []
          
          print(f"Total chunks to process: {totalChunks}")
          
          # Start the first threads (up to maxConcurrentThreads)
          for i in range(min(maxConcurrentThreads, len(chunksToProcess))):
              startFrame, endFrame = chunksToProcess.pop(0)
              thread = PassesThread(doc, fps, startFrame, endFrame)
              activeThreads.append(thread)
          
          # Main thread pool loop
          while activeThreads or chunksToProcess:
              # Update status
              totalFrames = maxFrame - minFrame + 1
              processedFrames = completedChunks * chunkSize
              c4d.gui.StatusSetText(f"Handling scene chunks ({totalFrames} Frames) ({completedChunks}/{totalChunks})")
              
              # Check finished threads
              finishedThreads = []
              for thread in activeThreads:
                  if not thread.IsRunning():
                      if thread.DidSucceed:
                          completedChunks += 1
                          print(f"Chunk completed successfully. Progress: {completedChunks}/{totalChunks}")
                      else:
                          print(f"Chunk failed. Progress: {completedChunks}/{totalChunks}")
                      finishedThreads.append(thread)
              
              # Remove finished threads from the active list
              for thread in finishedThreads:
                  activeThreads.remove(thread)
              
              # Start new threads if chunks are pending and we have available slots
              while len(activeThreads) < maxConcurrentThreads and chunksToProcess:
                  startFrame, endFrame = chunksToProcess.pop(0)
                  thread = PassesThread(doc, fps, startFrame, endFrame)
                  activeThreads.append(thread)
                  print(f"Started new thread for chunk {startFrame}-{endFrame}. Active threads: {len(activeThreads)}")
              
              # Wait a bit before checking again
              time.sleep(1.0)
          
          # Update final status
          c4d.gui.StatusSetText(f"Scene processing completed ({totalFrames} Frames) ({totalChunks}/{totalChunks})")
          print(f"All chunks processed. Total: {completedChunks}/{totalChunks}")
      
      if __name__ == '__main__':
          run()
      

      [4] Performance (Document Cloning) (I stripped many useless print ;))

      Total chunks to process: 35
      TIMEIT: Ran 'Main()' in 4.73206 sec
      TIMEIT: Ran 'Main()' in 4.75516 sec
      TIMEIT: Ran 'Main()' in 5.0718 sec
      TIMEIT: Ran 'Main()' in 5.36385 sec
      TIMEIT: Ran 'Main()' in 5.32922 sec
      TIMEIT: Ran 'Main()' in 5.61483 sec
      TIMEIT: Ran 'Main()' in 6.64787 sec
      TIMEIT: Ran 'Main()' in 6.50201 sec
      TIMEIT: Ran 'Main()' in 6.47022 sec
      TIMEIT: Ran 'Main()' in 6.47917 sec
      TIMEIT: Ran 'Main()' in 4.06786 sec
      TIMEIT: Ran 'Main()' in 4.09047 sec
      TIMEIT: Ran 'Main()' in 4.2562 sec
      TIMEIT: Ran 'Main()' in 3.88825 sec
      TIMEIT: Ran 'Main()' in 4.46758 sec
      TIMEIT: Ran 'Main()' in 4.5405 sec
      TIMEIT: Ran 'Main()' in 4.29954 sec
      TIMEIT: Ran 'Main()' in 4.25477 sec
      TIMEIT: Ran 'Main()' in 4.371 sec
      TIMEIT: Ran 'Main()' in 4.40597 sec
      TIMEIT: Ran 'Main()' in 4.34581 sec
      TIMEIT: Ran 'Main()' in 4.07137 sec
      TIMEIT: Ran 'Main()' in 5.32505 sec
      TIMEIT: Ran 'Main()' in 4.6659 sec
      TIMEIT: Ran 'Main()' in 5.9585 sec
      TIMEIT: Ran 'Main()' in 4.23031 sec
      TIMEIT: Ran 'Main()' in 4.31894 sec
      TIMEIT: Ran 'Main()' in 6.03186 sec
      TIMEIT: Ran 'Main()' in 5.28033 sec
      TIMEIT: Ran 'Main()' in 5.33497 sec
      TIMEIT: Ran 'Main()' in 3.66333 sec
      TIMEIT: Ran 'Main()' in 3.74228 sec
      TIMEIT: Ran 'Main()' in 1.93532 sec
      TIMEIT: Ran 'Main()' in 2.65944 sec
      TIMEIT: Ran 'Main()' in 2.6315 sec
      All chunks processed. Total: 35/35
      --------------------------------------------------------------------------------
      TIMEIT: Ran 'run()' in 20.56978 sec
      
      posted in Bugs
      A
      Aprecigout
    • RE: ExecutePasses performance keeps slowing down per-frame and per-run — possible cache issue?

      Thanks @ferdinand ! I hope you will find something interesting to our problem 😄

      posted in Bugs
      A
      Aprecigout
    • RE: ExecutePasses performance keeps slowing down per-frame and per-run — possible cache issue?

      Hi @ferdinand ,

      Thank you for analysing our file, for your reply here, and for the e‑mail.
      Sorry for the delay—we were completely absorbed in preparing a large show this weekend and yesterday. 😅

      I tested your code and, indeed, there is a huge difference between your execution times and ours.
      Getting sub‑second performance for this operation would be a dream! 😮

      However, when I tried to apply your approach I hit reality.
      Unless I’m mistaken, the flag c4d.SCENEFILTER_NONE skews the test on this scenography because no objects or materials are loaded (as a reminder, we need to extract clone data over the full duration of the scenography).
      So I replaced the import flag in your script with SCENEFILTER_OBJECTS | SCENEFILTER_MATERIALS and tested with an incrementing frame count (see source code at [1]).

      After that change I get results much closer to what I showed in my first post [2].

      Yesterday’s feedback
      During the tests we ran for yesterday’s show, we ran onto another performance problem (which is completely normal behavior for me) : once the machine starts swapping, export performance drops sharply.
      We also noticed—quite logically again—that disabling all effectors, fields, and other objects via keyframes when they are not used helps ExecutePasses a lot. We’re exploring every possible avenue to speed up our exports. 😅

      We’ve observed that the longer the scenography runs, the higher the per‑frame time becomes.
      We’re therefore experimenting with splitting ExecutePasses into chunks of 5 000 frames and using KillDocument() / LoadDocument() between chunks, to see if that lowers the total export time. Do you think this approach can help us ?

      [1]

      """Provides a short example for how to execute the passes on all frames of a document, multiple times
      in a row, while measuring the time it takes to execute the passes.
      """
      
      import c4d
      import mxutils
      
      # A debug variable to control whether we want to see debug output or not and the file we are going
      # to load and profile over and over again.
      # 
      # Always use a global constant such as IS_DEBUG when using the mxutils.TIMEIT decorator, as the 
      # overhead especially for profiling (compared to pure timing) can be quite high. Profiling should 
      # NEVER leak into production code! With a global constant, this is much more unlikely to happen.
      IS_DEBUG: bool = True
      FILE_PATH: str = "/Users/f_hoppe/Downloads/user_scene.c4d"
      
      @mxutils.TIMEIT(enabled=IS_DEBUG, useProfiler=True, showArgs=True)
      def ExecutePasses(doc: c4d.documents.BaseDocument, fps: int, minFrame: int, maxFrame: int) -> None:
          """Executes the passes for the given document in the given frame range.
          """
          mxutils.CheckType(doc, c4d.documents.BaseDocument)
          for f in range(minFrame, maxFrame + 1):
              doc.SetTime(c4d.BaseTime(f, fps))
              doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_EXPORTONLY)
      
      def main() -> None:
          """Called by Cinema 4D to execute the script.
          """
          doc: c4d.documents.BaseDocument | None = None
          # Liste des maxFrame pour chaque tour
          max_frames_per_tour = [5000, 10000, 20000, 30000, None]  # None = Max Frame du document
          
          # We just iterate five times over the same document, to see if there are any slowdowns. 
          for i, p in enumerate([FILE_PATH] * 5):
              # Unload the current document if it exists and then load the new one.
              if isinstance(doc, c4d.documents.BaseDocument):
                  c4d.documents.KillDocument(doc)
              doc: c4d.documents.BaseDocument = c4d.documents.LoadDocument(p, c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS)
      
              # Déterminer le maxFrame pour ce tour
              fps: int = doc.GetFps()
              minFrame: int = doc[c4d.DOCUMENT_MINTIME].GetFrame(fps)
             
              if max_frames_per_tour[i] is None:
                  # Tour 5 : utiliser le Max Frame du document
                  maxFrame: int = doc[c4d.DOCUMENT_MAXTIME].GetFrame(fps)
              else:
                  # Tours 1-4 : utiliser la valeur spécifiée
                  maxFrame: int = max_frames_per_tour[i]
              c4d.gui.StatusSetText(f"Building passes for document {i + 1}/5 (frames: {minFrame}-{maxFrame}).")
      
              # Execute the passes for the loaded document with our custom function which uses the 
              # mxutils.TIMEIT decorator to profile the event.
              ExecutePasses(doc, fps, minFrame, maxFrame)
      
          c4d.gui.StatusClear()
          c4d.EventAdd()
         
      
      if __name__ == '__main__':
          main() 
      

      [2]

      TIMEIT: Ran 'ExecutePasses((<c4d.documents.BaseDocument object called  with ID 110059 at 4487911744>, 30, 0, 5000), {})' with 10006 function calls in 43.606 seconds
      
         Ordered by: cumulative time
      
         ncalls  tottime  percall  cumtime  percall filename:lineno(function)
              1    0.015    0.015   43.606   43.606 scriptmanager:17(ExecutePasses)
           5001   43.587    0.009   43.587    0.009 {method 'ExecutePasses' of 'c4d.documents.BaseDocument' objects}
           5001    0.004    0.000    0.004    0.000 {method 'SetTime' of 'c4d.documents.BaseDocument' objects}
              1    0.000    0.000    0.000    0.000 /Applications/Maxon Cinema 4D 2025/resource/modules/python/libs/python311/mxutils/mxu_base.py:101(CheckType)
              1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
              1    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
      --------------------------------------------------------------------------------
      TIMEIT: Ran 'ExecutePasses((<c4d.documents.BaseDocument object called  with ID 110059 at 21114098496>, 30, 0, 10000), {})' with 20006 function calls in 151.821 seconds
      
         Ordered by: cumulative time
      
         ncalls  tottime  percall  cumtime  percall filename:lineno(function)
              1    0.038    0.038  151.821  151.821 scriptmanager:17(ExecutePasses)
          10001  151.774    0.015  151.774    0.015 {method 'ExecutePasses' of 'c4d.documents.BaseDocument' objects}
          10001    0.010    0.000    0.010    0.000 {method 'SetTime' of 'c4d.documents.BaseDocument' objects}
              1    0.000    0.000    0.000    0.000 /Applications/Maxon Cinema 4D 2025/resource/modules/python/libs/python311/mxutils/mxu_base.py:101(CheckType)
              1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
              1    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
      --------------------------------------------------------------------------------
      TIMEIT: Ran 'ExecutePasses((<c4d.documents.BaseDocument object called  with ID 110059 at 19602281536>, 30, 0, 20000), {})' with 40006 function calls in 429.998 seconds
      
         Ordered by: cumulative time
      
         ncalls  tottime  percall  cumtime  percall filename:lineno(function)
              1    0.102    0.102  429.998  429.998 scriptmanager:17(ExecutePasses)
          20001  429.872    0.021  429.872    0.021 {method 'ExecutePasses' of 'c4d.documents.BaseDocument' objects}
          20001    0.024    0.000    0.024    0.000 {method 'SetTime' of 'c4d.documents.BaseDocument' objects}
              1    0.000    0.000    0.000    0.000 /Applications/Maxon Cinema 4D 2025/resource/modules/python/libs/python311/mxutils/mxu_base.py:101(CheckType)
              1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
              1    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
      --------------------------------------------------------------------------------
      TIMEIT: Ran 'ExecutePasses((<c4d.documents.BaseDocument object called  with ID 110059 at 21296656704>, 30, 0, 30000), {})' with 60006 function calls in 868.329 seconds
      
         Ordered by: cumulative time
      
         ncalls  tottime  percall  cumtime  percall filename:lineno(function)
              1    0.165    0.165  868.329  868.329 scriptmanager:17(ExecutePasses)
          30001  868.126    0.029  868.126    0.029 {method 'ExecutePasses' of 'c4d.documents.BaseDocument' objects}
          30001    0.039    0.000    0.039    0.000 {method 'SetTime' of 'c4d.documents.BaseDocument' objects}
              1    0.000    0.000    0.000    0.000 /Applications/Maxon Cinema 4D 2025/resource/modules/python/libs/python311/mxutils/mxu_base.py:101(CheckType)
              1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
              1    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
      --------------------------------------------------------------------------------
      TIMEIT: Ran 'ExecutePasses((<c4d.documents.BaseDocument object called  with ID 110059 at 15425544000>, 30, 0, 34650), {})' with 69306 function calls in 1131.141 seconds
      
         Ordered by: cumulative time
      
         ncalls  tottime  percall  cumtime  percall filename:lineno(function)
              1    0.196    0.196 1131.141 1131.141 scriptmanager:17(ExecutePasses)
          34651 1130.895    0.033 1130.895    0.033 {method 'ExecutePasses' of 'c4d.documents.BaseDocument' objects}
          34651    0.050    0.000    0.050    0.000 {method 'SetTime' of 'c4d.documents.BaseDocument' objects}
              1    0.000    0.000    0.000    0.000 /Applications/Maxon Cinema 4D 2025/resource/modules/python/libs/python311/mxutils/mxu_base.py:101(CheckType)
              1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
              1    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
      
      posted in Bugs
      A
      Aprecigout
    • RE: ExecutePasses performance keeps slowing down per-frame and per-run — possible cache issue?

      You cannot make an omelette without breaking eggs, so no worries. There are no silly questions.

      Thank you for your patience and clear explanations. 🙂

      I may have wandered a little off topic, but the artists’ main objective is still to export to the drone proprietary format as fast as possible (when their is last minute change on the field), whatever the method. Our current ExecutePasses‐based implementation is definitely not carved in stone.

      But it is good to know that C++ is an option for you.

      Yes, moving more logic to C++ is on the table. For context, the tool-set we give the artists is already split between C++ plugins and Python scripts/plugins. Python is the historic layer that lets us prototype and satisfy quick requests; C++ lets us dig deeper into the SDK, but obviously development time is higher.

      But without a concrete scene, it is still very hard to evaluate for me where this comes from

      I have just sent you an example document containing a drone scenography via the e-mail address listed in https://plugincafe.maxon.net/guidlines_cp#support-data.
      Hope it helps.

      Are you sure that you unload the documents between runs? I.e., something like this shown in [1]? Because if you do not, you will of course occupy more and more memory with each document loaded.

      You are absolutely right—I simply ported my small test to C++ and didn’t unload/reload the document between runs. I understand the issue and I’m working on implementing proper unloading on our actual scripts.

      When a MoGraph cloner is in ‘Multi-Instance’ mode it will actually only build the first clone concretely …

      That’s an angle I had never considered. Our artists never touch that setting—their cloners stay in Instance mode all the time. If i understand correctly the other thread, that’s actually the only reason our current ExecutePasses workflow still works: in Instance mode Cinema 4D materialises every clone, so on our call we can see full per-clone colours
      Thanks again for highlighting this!

      To faithfully read such c4d export document, you must use the Cineware AP

      I will also investigate Cineware; if I can extract the required information directly from a flattened file, that could drastically speed up the export to our proprietary format.

      Thanks again—and please don’t hesitate to tell me if, after looking at the sample scene I sent, another method for extracting the data comes to mind!

      posted in Bugs
      A
      Aprecigout
    • RE: ExecutePasses performance keeps slowing down per-frame and per-run — possible cache issue?

      Thank you for the very detailed reply—your explanation of the internal workings really helps me avoid saying more “silly things.” 🙂

      To give you even more context on why we lean so heavily on ExecutePasses: our artists make massive use of MoGraph, cloners, clones and theirs effectors to modify many aspects of the scene:

      • Clone colours (which map directly to the LEDs on the real drones)
      • Clone positions in 3-D space (hence, drone positions)
      • ...

      When it’s time to export the scenography to the drone-friendly format, we currently rely on a mix of ExecutePasses and direct MoGraph matrix access (GeGetMoData(), GetArray(c4d.MODATA_COLOR))

      Small wrinkle: For certain type of scenography we even run the scene twice—first with some effectors on, then with them off—to harvest an extra colour value we need.

      Right now, ExecutePasses is the only way I know to retrieve per-frame information for every clone. If there is a lighter-weight or more direct approach, I would be thrilled to hear about it, because I understand from your message how expensive ExecutePasses can be.

      Following your advice, I also rewrote the minimal test in C++ (so, no Python VM involved) and it looks like i had the same behaviour there—so the slowdown doesn’t appear to be a Python-specific issue ?!.

      Thanks again for such a comprehensive answer; it’s always a pleasure reading this forum!

      posted in Bugs
      A
      Aprecigout
    • ExecutePasses performance keeps slowing down per-frame and per-run — possible cache issue?

      Hello,

      After many search in the and in this forum—especially this topic: https://developers.maxon.net/forum/topic/14675/executepasses-to-slow/3?_=1751270994697&lang=en-GB—I’m running into a performance problem with ExecutePasses. The execution time grows from frame to frame and that slowdown is carried over between frames.

      To give you some context: I work for a client who produces large-scale drone shows. Their artists use Cinema 4D to architect the whole drone scenography. We have developed several internal plug-ins and tools that help those artists design inside Cinema 4D and then validate the scene against real-world constraints so everything works safely on-site.

      My issue arises during the export of these scenes to the drones’ proprietary format. Because the artists rely heavily on MoGraph to manage entire drone fleets, I call ExecutePasses at export time to extract the data I need. Export speed is critical in the field; we often have to tweak the scenography at the last minute.

      After profiling, the overwhelming bottleneck is ExecutePasses.

      To dig further, I wrote a minimal script that does nothing but run ExecutePasses over the first 10 000 frames:

      import c4d
      import time
      import timeit
      
      def main():
          # Get the active document
          doc = c4d.documents.GetActiveDocument()
          if doc is None:
              print("No active document")
              return
          
          # Get start and end frames
          start_frame = doc[c4d.DOCUMENT_MINTIME].GetFrame(doc.GetFps())
          end_frame = 10000
          #end_frame = doc[c4d.DOCUMENT_MAXTIME].GetFrame(doc.GetFps())
          fps = doc.GetFps()
          
          print(f"ExecutePasses Test - Cinema 4D")
          print(f"Document: {doc.GetDocumentName()}")
          print(f"Frames: {start_frame} to {end_frame} ({end_frame - start_frame} frames)")
          print(f"FPS: {fps}")
          print("-" * 50)
          
          # Measure total time
          start_time_total = timeit.default_timer()
          
          # Store frame times
          frame_times = []
          
          # Loop over each frame
          total_frames = end_frame - start_frame
          cumulative_time = 0
          for i, frame in enumerate(range(start_frame, end_frame)):
              # Update status bar
              progress = (i * 100) // total_frames
              c4d.StatusSetBar(progress)
              
              # Measure time for this frame
              start_time_frame = timeit.default_timer()
              
              # Set frame time
              time_val = c4d.BaseTime(frame, fps)
              doc.SetTime(time_val)
              
              # Execute passes
              doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_EXPORTONLY)
              
              # Calculate elapsed time for this frame
              frame_time = timeit.default_timer() - start_time_frame
              frame_times.append(frame_time)
              
              # Calculate cumulative time
              cumulative_time += frame_time
              
              # Display progress
              print(f"{frame};{frame_time:.4f};{cumulative_time:.4f}")
          
          # Calculate total time
          total_time = timeit.default_timer() - start_time_total
          
          # Display results
          print("-" * 50)
          print("RESULTS:")
          print(f"Total time: {total_time:.4f}s")
          print(f"Average time per frame: {sum(frame_times) / len(frame_times):.4f}s")
          print(f"Min time per frame: {min(frame_times):.4f}s")
          print(f"Max time per frame: {max(frame_times):.4f}s")
          print(f"Number of frames processed: {len(frame_times)}")
          
          # Display slowest frames
          print("\nSlowest frames:")
          indexed_times = [(i + start_frame, t) for i, t in enumerate(frame_times)]
          indexed_times.sort(key=lambda x: x[1], reverse=True)
          for i, (frame_num, frame_time) in enumerate(indexed_times[:5]):
              print(f"  Frame {frame_num}: {frame_time:.4f}s")
          
          print(f"\nTest completed - Cinema 4D {c4d.GetC4DVersion()}")
          
          # Clear status bar
          c4d.StatusClear()
      
      if __name__ == '__main__':
          main() 
      

      The test procedure is simply:

      • First run
      • Second run
      • Third run
      • Restart C4D and reload the scene
      • Fourth run

      For each run I log the ExecutePasses execution time for every frame, then plot the result:
      chart.png

      Note : I don’t have an in-depth understanding of how Cinema 4D’s internal caches work, so please forgive any misconceptions below.

      This behaviour feels like some kind of cache that never gets cleared: each run takes longer than the previous one, and the per-frame time at the start of a run matches the time at which the previous run ended.

      Following i have some questions :

      1. Is there a recommended way to flush or bypass the cache/memory buildup that seems to happen inside ExecutePasses, so execution time stays consistent?
      2. If such mechanisms exist, could someone outline the usual workflow or API calls to use—or point me to the relevant documentation?

      Because of the artists’ scenes, I unfortunately need to enable all ExecutePasses flags (cache, animation and `expression) so turning any of them off isn’t an option.

      Finally, could C4DThread help in this context, or am I barking up the wrong tree? My experiments based on the thread linked above haven’t produced conclusive results.

      Another Note : I haven’t attached the test .c4d file here because it contains a full scenography created for my client, but I can provide it privately in accordance with your procedure described here: https://plugincafe.maxon.net/guidlines_cp#support-data.

      Thanks in advance for any insights!

      posted in Bugs macos 2025 2024 python
      A
      Aprecigout