Next: , Previous: MarSystem composites, Up: Architecture concepts


6.4 Linking of controls

THIS SECTION WAS JUST A COPY-PASTE FROM A LONG EMAIL FROM THE DEVELOPERS MAILING LIST - STILL NEEDS A BIT OF REVISION, BUT ALREADY PROVIDES IMPORTANT INFORMATION ABOUT THE LINKING OF CONTROLS IN MARSYAS (lgmartins)

This refactoring is the "part II" of the last refactoring to the linking mechanism, done some weeks ago.

In the first part of the refactoring we changed the way linked controls store their values (i.e their data - a real number, an integer, a string, a realvec, etc). Before this refactoring, each control had it's own data allocation, and so everytime we changed a control's value, such change had to be propagated (i.e. copied) to all the linked controls (if any). Such a copy meant that the same data would be replicated in memory a number of times equal to the number of existing links. Furthermore, keeping all those copies in sync each time we change a control value implied a lot of copying. This was really ineficient, both computationally and memory-wise, and all the code for managing such a synchronization was a mess.

So, the first step was to make all MarControls that are linked to each other share a same MarControlValue (i.e. the marsyas object that in fact stores in memory the data in MarControls). This automatically solves the synchronization problem, although it may create some others if we start thinking about multi-treaded code in Marsyas (but let's forget about multi-threads for now).

When linking a control (let's say ctrl_A) to another control (ctrl_B), it's now just a matter of instructing control_A to start using the MarControlValue used by control_B. The old MarControlValue of ctrl_A can therefore be deleted from memory, in case no other MarControl is still pointing to it (i.e. refcounting).

To do so, in marsyas pseuso-code, we would write:

     ctrl_A->linkTo(ctrl_B);

Or, in case we didn't have the direct pointer to the controls, using their pathnames and the MarSystem API:

     msys->linkControl("mrs_xxx/A", "mrs_xxx/B");

This brings us to the first important detail: during the linking of two controls, it's important the order of the linking operation. Doing ctrl_A->linkTo(ctrl_B) will discard the current value of ctrl_A, which will now use the value currently stored in ctrl_B. If we reverse it (ctrl_B->linkTo(ctrl_A);), ctrl_B value will be discarded (we can think as "overwriten") in favour of the value of ctrl_A. This is only relevant at the time of the linking operation, and users should, if nothing else, be aware of which value the want to keep when linking two controls. Of course, after the linking is done, changing the value of ctrl_B will also change the value of ctrl_A, and vice-versa, making this detail syntatically meaningless.

However, this order of the linking operation is now stored internally by all MarControls in marsyas. And the reason for this has to do with the unlinking operation of controls. This was the work done in "part II" of the linking/unlinking refactoring, just completed today.

In the first stage of the refactoring, we were not storing anywhere the order of the linking of two controls. So we just kept a table with references to all the MarControls linked together (i.e. sharing the same value/data), without any information to who linked to whom originally.

This was quite elegant in fact, allowing to easily unlink any control from a set of linked controls by just cloning in memory its current value (i.e. creating a new but equal valued MarControlValue object), removing it from the reference table of the MarControlValue object holding its value, and re-pointing the control to, from that time on, start using the new cloned data object instead.

The problem is that in Marsyas, the most interesting use of unlinking controls is a bit more demanding. The way just described of completely unlinking a control from all the other linked controls may not be desirable at all.

Suppose the following scenario: we have a composite MarSystem (i.e. a MarSystem with other MarSystems inside, connected in series, for e.g.) where we have a control (say, ctrl_X) that we want to always link to, for example, the output control of the last child MarSystem (let's say ctrl_processedData control of the last child MarSystem). This is exactly the case of the composite FlowThru, so you can refer to its .cpp/.h code for an actual implementation. So we start by linking ctrl_X to the ctrl_processedData of the last child:

     ctrl_processedData = msys->getctrl("xxx/lastChildMsys1/mrs_realvec/processedData");
     ctrl_X->linkTo(ctrl_processedData);

Semanticaly, this is the order that makes more sense (but nothing prevents us to do it the other way around - the link will work as well!), since we are overwriting ctrl_X's value with the current value of ctrl_processedData, which is in fact an "output" control (controls in Marsyas do not have an explicit in/out attribute - "output" controls are considered controls to which makes no sense writing to because they will be ignored and overwriten by their owning MarSystem; "input" controls on the other hand will be used internally by their owning MarSystem as parameters, but they may also be read to know the current control value currently being used).

Graphically, this link can be represented as:

     ctrl_processedData <--- ctrl_X

If we now need to link some other controls from other MarSystems to ctrl_X, we just need to explicitly do it:

     ctrl_Y->linkTo(ctrl_X);
     ctrl_W->linkTo(ctrl_X);
     ctrl_Z->linkTo(ctrl_W); //this control will be indirectly connected to ctrl_X, altough its link target is ctrl_W

Graphically:

     ctrl_processedData <--- ctrl_X <--- ctrl_Y
                                 ^
                                 |--- ctrl_W <--- ctrl_Z

So, after this we have ctrl_X, ctrl_Y, ctrl_W and ctrl_Z effectively all linked to and sharing the same value with ctrl_processedData, with minimal computational burden and a small memory footprint.

Now imagine that we want to add a new child to the FlowThru composite. Since we want ctrl_X (and all the controls linked to it) sharing the value of the last child in the FlowThru composite, we must update the ctrl_X —> ctrl_processedData link! In other words, we need to unlink ctrl_X from ctrl_processedData of the previous last child MarSystem, and re-link it to the ctrl_processedData of the new last child of the composite. For that we could think that the following way would cut it:

     ctrl_processedData = msys->getctrl("xxx/lastChildMsys2/mrs_realvec/processedData");
     ctrl_X->unlink();
     ctrl_X->linkTo(ctrl_processedData);

However, calling unlink in this way (i.e. the way implemented in "part I" of the refactoring) would unlink ctrl_X from all the other linked controls (i.e. ctrl_W, ctrl_Y, etc) and relinking it alone to the new ctrl_processedData (i.e. represented as (2) below) from the new last child. All the other previously linked controls would still be linked to the same ctrl_processedData of the previous last child in the composite. The only way around this would be to manually keep track of what links would have to be unlinked and relinked to the new control.

Graphically:

     ctrl_processedData(1) <--- ctrl_Y
                                  ^
                                  |--- ctrl_W <--- ctrl_Z
     
     
     ctrl_processedData(2) <--- ctrl_X

As it is easy to imagine, any minimally complex network of multiple-nested MarSystems and linked controls would be totally insane to manage! Here's an example of what we would have to explicitly do to achieve what we really wanted:

     ctrl_processedData = msys->getctrl("xxx/lastChildMsys2/mrs_realvec/processedData");
     ctrl_X->unlink();
     ctrl_X->linkTo(ctrl_processedData_X);
     ctrl_Y->unlink();
     ctrl_Y->linkTo(ctrl_X);
     ctrl_W->unlink();
     ctrl_W->linkTo(ctrl_X);
     ctrl_Z->unlink();
     ctrl_Z->linkTo(ctrl_W); //we must remind that this one was originally connected to W and not directly to X! :-\

(OK, if you managed to follow me till here without getting suicidal tendencies or falling totally asleep, please go to http://marsyas.sness.net/community/getting_involved and welcome aboard ;-)).

The best way to make Marsyas solve this automatically for us was to keep a simple record of the original link orders, as explained above. If we know who originally linked to whom, it's then easy to unlink a control from a set of controls, but keep all the controls that were originally connected (or indirectely connected) still attached to it. So after the "part II" of the refactoring, if we want to reconnect ctrl_X (and all the controls originally linked to it) to the new last child in the composite, we would only need to do:

     ctrl_X->unlinkFromTarget(); //this is in fact not needed! See below...
     ctrl_processedData = msys->getctrl("xxx/lastChildMsys2/mrs_realvec/processedData");
     ctrl_X->linkTo(ctrl_processedData);

As it's possible to see, the unlink() method is no longer used (it was deprecated) and we should now use unlinkFromTarget() for achieving the desired result. By "target" I mean the following: everytime we link a control *to* another control (and here we start to see the importance of the order of linking, other than the known issue of which control gets its value overwriten by the other as explained above), we are directly "targeting" or linking to that control. So for the case of ctrl_X, when we linked it to ctrl_processedData, we made the latter the "target" of ctrl_X. As we can see, each control, even if linked to any arbitrary number of controls, only has one "target" control, i.e. the one it was originaly linked to. So, when we call ctrl_X->unlinkFromTarget() above, we are just unlinking ctrl_X from its target (i.e. ctrl_processedData(1) and any other controls that have that control as their target) but keeping the links with all the other controls that have ctrl_X as their direct or indirect target. Graphically we get:

     ctrl_processedData(1)
     
     ctrl_processedData(2) <--- ctrl_X <--- ctrl_Y
                                     ^
                                     |--- ctrl_W <--- ctrl_Z

In truth, we do not even need to explicitly call unlinkFromTarget() before linking ctrl_X to the new target. Since each linked control can only have one target, re-linking it to other control will automatically unlink it from its previous target.

All these linked controls can be seen as a directed graph problem (but please do not suggest me using Ncuts in it ;-)), where there is no possibility of existing loops (e.g. attemping to link ctrl_X with ctrl_Z, as depicted above, would do nothing, since it's very easy to detect that they are already linked - we just need to compare the pointers to the respective MarControlValue of each MarControl: if they point to the same object it means the controls are already linked, so very easy and efficient to avoid any loops!).

In any case, if we need to completely unlink a control from all its linked controls (regardeless of targets etc - i.e. the way the now deprecated unlink() method worked), we can simply use:

     ctrl_X->unlinkFromAll();

Although not so usefull as the former version of unlinking, there are some situations in Marsyas where we need this (if you dare or if you are really curious about this, you can have a look ate MarSystem copy constructor and at the put() method ;-)).

As a conclusion, if when constructing networks of linked controls in Marsyas we take into mind the importance of the linking order, it's really easy to construct semantically meaninful links that will allow easy reconfiguration of MarSystem networks. Actually, this is a mandatory feature if we want in the future to have MarSystem insertion/deletion at runtime, without messing up all the linked controls in the network.