This hopefully shall be a useful reference for either myself or anyone who may need to modify this system due to changes later.
EDIT: the obligatory demo video...
RNA Updates demo video from Joshua Leung on Vimeo.
Why is this significant?
Well, as most who've encountered this will have found, it was impossible to animate or drive some modifier properties (for example subdivision levels, or number of array modifier repetitions). That was because those properties depended on their update calls to tell the depsgraph (which we've met in a previous post) that the modifier (or more accurately, the meshes they're applied on) need recalculating.
Why only now?
There are two major reasons why it's taken until now to actually fix these update problems.
1) The current animation system was originally coded when RNA was still relatively new in Blender (when most of the editors weren't back even!). Back then, there was no concept of these update callbacks, so this functionality was never considered back then.
2) Doing the simplest fix (directly calling each property's update after it has been written to) is actually incredibly slow. But coming up with a fix for it requires some time to do careful investigations. But for a few months now, schoolwork was taking up large amounts of my time, leaving only time for smaller bugfixes occasionally.
How slow is slow? The problem with the simple fix...
So you may be wondering what the fuss about the simple fix. Well, that's best illustrated by taking some frame-rate playback timings (animation updates + data recalculations) on some assets that artists would really be playing with in a production environment. Enter the Sintel Lite rig (or in the case of these tests, a modified version, but one with more rather than less).
In my crude timing tests, playing back the animation (with a few pose changes, in which every animateable control is keyed) just opening up this file, I got the following timings:
- No Property Updates = ~5 fps
- With Property Updates = 0.9 fps!
To put this timing difference into perspective, when there were no property updates, you could still see the pose changing in the viewport, giving some semblance of motion. However, once the property updates were added, the viewport would absolutely crawl, to the point that it felt like it was taking close to 2 seconds per update (hardly interactive, and more like clothsim framerates than animation playback).
Now if we consider what's going on behind the hood, the reason for this slowdown becomes quite clear.
Firstly, with production rigs, it's pretty common to get around 500 individual controls (or properties, or in Pixar-speak, "avars") or more per rig, which will need be animated. That's not to mention how many additional controls there may be attached to these controls which feed into any hidden machinery driving the rig.
Now, if we update each one of these properties, with the update action potentially not just being a simple "pin the tail on the tied-up donkey" (i.e. some of these calls end up going through the depsgraph flushing updates too everytime), then we quickly see that this process is obviously going to take some time.
Investigating the problem domain...
Clearly we cannot just go through blindly updating each property unless that's the only way. The best way forward therefore is to dive into the actual updating mechanics, and discover the true nature of the beasts that we're taming.
Firstly, what does updating a property actually entail? RNA_property_update() gives us 4 options:
- Call update() function using Main db, current Scene, and an RNA pointer to the data where the property that was edited lives.
- Call update() function using Context, RNA pointer + RNA property pair
- Just send a notifier (add tag)
- Tag the relevant ID-block affected (the "if (!is_rna || (prop->flag & PROP_IDPROPERTY) ) ..." case at end of rna_property_update())
- 2 - this is only used for Screen/WindowManager stuff, which is not animateable
- 3 - notifiers are pretty useless for use by the Animation System, as they won't get called until the next update; far too late to be of any use to immediately flush
- 4 - perhaps this case might be useful in future, but this is mostly only used for properties without their own update stuff anyways, and even then, the depsgraph can't even really care about most of those properties.
At this point, I did a quick survey of all the existing RNA update callbacks. It turns out that most of these actually end up using just the ID-block pointer in the RNA pointers, ignoring the nested data pointer. Of the ones that didn't follow this, we could rather safely disregard them, since those were likely to be non-animatable properties anyway.
The solution - a cache!
By now, we have enough information to attempt some optimising caching scheme. By caching the update callbacks instead of running them immediately, we can aim to cut out some redundant updates, batching them up and then just running a few as necessary in one go.
Since animation data is evaluated rooted to some ID-block, the cache flushing is done once per ID-block whose animation data is evaluated. This means that the flushing occurs frequently enough that the size of the cache isn't likely to start introducing searching slowness, and also mean that inter-block dependencies will still work correctly as the updates didn't all get batched until some missed out.
The cache is structured with 2 layers:
L2 = update() function references - these are the update callbacks that need to be called on the RNA pointers in L1. There may be 1-2 of these per block, but not more in general (judging by the definitions).
Optimisations and Timings
With this caching scheme in place, the frame rate was a more healthy ~1.6 fps. While still lower than ~5fps, this was at least playing back interactively (and really didn't feel quite that slow).
Of course, to get things up to a more acceptable speed, I took a second look again at whether some more fat could be trimmed. It turns out that the update() callbacks for PoseBones were actually redundant depsgraph tagging (which was only needed for UI/PyAPI editing of the property in isolation). Hence, I added an optimisation hack which basically skips updating altogether on such properties. In the production rig test, this got things back up to a healthy 3fps.
In future, I could look at making this even faster, though perhaps for most users out there this will be sufficient.
Go check out animating the subsurf on a cube. It looks quite neat! (nice fodder for another little demo vid methinks...)