Thread safety when handling CTrack in TagData.Message() on button click
-
Hi,
I have a small plugin example of a Tag Plugin and I want to know if I handle the threading correctly, because in my production version , I encountered some freezes, espacially during a running animation or when Redhift Render View was running.Download Plugin:
test_tag_plugin.zipQuick explanation:
- tag plugin with a button and a link field
- button click has to load a sound
- When the button is clicked, I want to alternate between loading a sound into the track specified in the link field (In the official release, I calculate a sound, save it to the hard drive, and load it into the track.)
- If no track exists, one must be created.
- I handle this within the
MessageMethod as usual - and check using
c4d.threading.GeIsMainThread().
Is this the right approach, or are there other factors to consider?
Should I perform the check inside theadd_soundmethod instead,
or would it be better to check usingc4d.GeIsMainThreadAndNoDrawThread?Please bear in mind that I threw this together quickly; you shouldn't expect perfection in the code—such as error handling—as my only concern right now is thread safety.
Code:
""" Simple CTrack Sound Loader """ import c4d import os from c4d import bitmaps, plugins # be sure to use a unique ID obtained from www.plugincafe.com PLUGIN_ID = 123456789 PY_ADD_AUDIO = 1000 PY_TRACK = 1001 class TestTagPlugin(plugins.TagData): def __init__(self): self.current_sound: str | None = None def Init(self, node, isCloneInit): if not isCloneInit: self.InitAttr(node, c4d.BaseList2D, c4d.DescID(PY_TRACK)) node[PY_TRACK] = None return True def Execute(self, tag, doc, op, bt, priority, flags): return c4d.EXECUTIONRESULT_OK @staticmethod def check_track(node, sound_path): """ Checks if there is a track in the audiotrack slot, if not it creates one and puts the sound into this track Has to be called from the MainThread :param node: the tag instance :param sound_path: str - The path to the sound file """ track = node[PY_TRACK] if not track or not track.GetType() == c4d.CTsound: # create special Audio Track obj: c4d.BaseObject = node.GetObject() track = c4d.CTrack(obj, c4d.DescID(c4d.DescLevel(c4d.CTsound, c4d.CTsound, 0))) obj.InsertTrackSorted(track) node[PY_TRACK] = track track.SetName("Audio_Track") node[PY_TRACK][c4d.CID_SOUND_NAME] = "" node[PY_TRACK][c4d.CID_SOUND_NAME] = sound_path def msg_add_audio(self, node) -> bool: """ Loads the audio into the track :param node: the tag instance :return: bool """ was_animation_running = False if c4d.CheckIsRunning(c4d.CHECKISRUNNING_ANIMATIONRUNNING): # save animation state and stop animation was_animation_running = True c4d.CallCommand(12412) if self.current_sound == "base_1.wav": self.current_sound = "base_2.wav" else: self.current_sound = "base_1.wav" path = os.path.join(sample_path, self.current_sound) if not os.path.exists(path): return False self.check_track(node, path) if was_animation_running: # restart stopped Animation c4d.CallCommand(12412) return True def Message(self, node: c4d.GeListNode, type: int, data: object) -> bool: if type == c4d.MSG_DESCRIPTION_COMMAND: if data["id"][0].id == PY_ADD_AUDIO: if c4d.threading.GeIsMainThread(): self.msg_add_audio(node) return True return True # main if __name__ == "__main__": bmp = bitmaps.BaseBitmap() p_dir, file = os.path.split(__file__) fn = os.path.join(p_dir, "res", "icon.tif") bmp.InitWith(fn) sample_path = os.path.join(p_dir, "res", "samples") c4d.plugins.RegisterTagPlugin(id=PLUGIN_ID, str="test_tag_plugin", info=c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE, description="test_tag_plugin", g=TestTagPlugin, icon=bmp)An additional question regarding the tag:
I’m also curious about a scenario where I launch aGeDialogfrom the tag via a button and pass the tag to it and toggle the sound from within the dialog as well. Would usingc4d.SpecialEventAdd()keep me on the safe side—for instance. So if the dialog runs asynchronously, stores the tag as a member variable,
retrieves the class instance viaGetNodeData, and then calls theadd_soundfunction?
Is that a viable approach? It works in the official plugin.Thanks in advance.
T.B. -
Hello @ThomasB,
could you share your plugin via something like dropbox or google drive with us? You upload above seems to have failed.
Your code looks fine, you do everything correctly at first glance. You only try to modify the scene in the context of
MSG_DESCRIPTION_COMMAND(which should only be invoked from the main thread) and shield yourself against rogue actors by also checkingc4d.threading.GeIsMainThread()(slightly better would beGeIsMainThreadAndNoDrawThreadbut that is just semantics). Your node does also not do anything wildly expensive in itsInit/__init__.But I am sure you are not just imagining your performance issues. Generally speaking, your code should only run when the user clicks the button with the ID
PY_ADD_AUDIOin the description of your tag. So, this should not be able to accidently run when you render or while scene playback (which I assume is what you mean with 'running (an) animation').PY_TRACKseems to be a parameter of typeDTYPE_BASELINKwhere you just link the newly created track. I cannot really see anything going wrong with this, as this is very harmless. I also checked if there is any 'special sauce' in creating sound tracks, as they are one of the special tracks, but as far as I can see, we are doing this internally exactly like you do it. So, this also does not seem to be a case of an incorrectly allocated node, which then constantly causes internal errors.Cheers,
Ferdinand -
Oh sorry, I missed that.
Is Google Drive ok?(which I assume is what you mean with 'running (an) animation').
By "running during animation," I meant clicking the button while the animation is playing.
-
Absolutely, but you have to either grant me access or make the whole folder public.
-
@ferdinand
WTF is wrong with me? It should work now. -
It did, thanks, I will have a look.