This section covers both techniques for versioning your own plug-ins, and techniques for handling multiple DDImage versions.
The NDK does not explicitly provide plug-in versioning functionality, so to work around this, there are a number of common approaches for allowing multiple versions of plug-ins whilst maintaining compatibility. This section explains some of these approaches. It is not necessary reading for those just getting started with the NDK, however it helps to have an awareness of the issues associated. Note that some of the terminology used here may be unfamiliar, so this section may need revisiting once you’ve covered later topics.
NUKE stores a user’s project in a plain text file known as a NUKE script with a .nk extension. The script contains a serialised version of the Node Graph or DAG (directed acyclic graph) which represents the node tree, along with user interface settings on these nodes which differ from their default values. It does not store values which are left at their default value to minimise the size of the resultant scripts and keep load times fast. Values are stashed against their corresponding knob name, and the nodes themselves according to their name.
One common technique is to keep the node name the same for the new version (whilst stashing the version in some kind of user interface element such as the tooltip or a text knob). The node has to ensure that its underlying engine continues to output the same output for the same values, excepting results which are obviously a bug (otherwise when the client updates they’ll find their existing scripts suddenly start rendering differently, without warning them). Optionally, if the results have changed, the underlying algorithm can check the version stored in the script (by putting the version number in a disabled or hidden knob’s value with ALWAYS_SAVE set on its flags - see the Knob Flags section for more information) and use that to decide whether to render the old result or the new. On top of that, if the first version didn’t store its version in the script, then the new version can store it and assume that no version found in the script means the previous version. Not very pleasant, but retaining script compatibility comes at a price.
If a user interface element is retired, changes name, or changes behaviour under this scheme, then the new version needs to make judicious use of the Obsolete_knob knob type. This allows a value found in a script to run through NUKE’s internal expression/scripting language to convert it to a new value. For example:
For an example of a node employing the obsoleting approach, see the Noise.cpp example.
The opposite end of the scale is to explicitly break compatibility with new versions of your plug-ins, so that old scripts will not load using the new versions, and thus not render with different results. To do this, the node version is appended to its name.
A prime example of this are the Merge nodes incorporated into NUKE. You may believe that there is only one Merge node, however, try doing an ‘update’ in the all plug-ins menu or tab selector, and then looking for Merge. You’ll find that, excepting the MergeExpression, MergeGeo and MergeMat nodes, there are two main entries - one called Merge and one called Merge2. In fact, the Merge menu entry on the Merge menu actually creates the node listed as Merge2 (which is its class, available by pressing ‘i’ when the node is selected). The all plug-ins Merge entry creates a plug-in of class Merge, which is colloquially known as “Merge1.” To further confuse matters, the tab selector lists two entries for Merge, and one for Merge2. The Merge entries are differentiated by the menu they appear under, listed in square brackets after their name:
The first is the menu entry seen in the Merge menu, the second is the merge entry seen in the All Plug-ins->M menu, and the third is the Merge2 entry seen in the All Plug-ins->M menu. Selecting the first or the last creates a node of class Merge2, and using the second creates a node of class Merge.
This behaviour touches on an important point - that a node’s menu entry (as specified by the associated menu.py) does not necessarily have the same name as the underlying node’s class. Equally, a node can specify a different node name to its class for use when building the default name on the Node Graph.
If you create the two types of merge node and compare them, you’ll see that the Properties panels are significantly different, both in control layout and merge operations. Merge2 was designed as a non-backwards compatible replacement for Merge. What is important is that both versions are still shipped with NUKE, but with the old version not put included in the menus. This allows old scripts originally created using the Merge node to open using Merge1 and new scripts to open using Merge2.
There are a number of other examples of this inside of NUKE. Truelight, for one, currently ships with Truelight3. Interestingly, Truelight and Truelight2 are not shipped due to enough time having passed since they were superseded and a lack of availability of 64-bit compiles of the underlying libraries. Rather than appending a number you can alternatively change the name in some other manner. This is not generally recommended, since it can be difficult to keep track of changes in the long run, but if the differences are large enough it may have little impact on the user. For example the ColorCorrect class supersedes the CCorect class node. A final example of interest is the Tracker node, for which the current version is Tracker3. NUKE also bundles a gizmo named Tracker. This ensures backwards compatibility by providing appropriately named knobs by which very old scripts featuring a tracker can be loaded. Since this old version of Tracker did not have inbuilt functionality for stabilising or matchmoving the image, all that is required is the Properties panel to allow expression links to still work correctly.
External bundles may opt to be even more explicit in their compatibility breakage. Ocula appends both the major and minor to the node class in the form <node name>_<major version>_<minor version>.
In some circumstances users may well be able to update their scripts manually to allow the old version stored to load with the new binary. To do this they must open the script in a text editor and do a find and replace to convert the old class name to the new. By doing this they explicitly have to understand the risks that their script may well render differently.
The DDImage exposed API will change, and it will break your existing binary plug-in compiles. We try to ensure these changes only happen when NUKE itself changes major or minor version, but not when it changes v number (ie 6.1v3 to 6.2v1 requires recompilation of related plug-ins, but 6.2v1 to 6.2v2 should not).
To allow you to cope with these changes, we define a number of preprocessor macros in a header called ddImageVersionNumbers.h. Check the header for a full list of these, however the most commonly employed is the kDDImageVersionInteger. This can be tested by preprocessor hash ifs to allow multiple NDK API versions to be built from the same codebase. For example:
#if kDDImageVersionInteger >= 62100
<do NDK API call in NUKE 6.2v1 fashion>
#elif kDDImageVersionInteger < 60100
# error Pre NUKE 6.0 there was no equivalent call. Not supported.
#else
<do NDK API call in pre NUKE 6.2v1, but post NUKE 6.0v1 fashion>
#endif
In this case a new API call we want to use was introduced in Nuke6.0v1, and its function signature amended with the release of Nuke6.2v1. We use the preprocessor to pick the appropriate path to compile or to error out if there’s no equivalent call available.
Note
For legacy reasons there is also a ddImageVersion.h header file. This is obsolete and should be ignored. You may also see references in examples to DD_IMAGE_VERSION_MAJOR and DD_IMAGE_VERSION_MINOR. These symbols were defined in this obsolete file and in recent versions of NUKE have no impact.