Sunday, July 24, 2011

GSoC11 - RNA Property Updates working for AnimSys

So, on a cold winter's afternoon while waiting for the first snowfall in 3 years to come (it started falling by the time I finished, but more on that in another post :D) I set to work on a problem which people have been running into for a while: RNA property updates weren't getting run by the animation system.

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!
I should mention that the tests were conducted on the same machine one after the other by restarting Blender after performing a recompile (without changing any compile settings between builds), opening the same file and running playback without making any other changes to the file, and that timings presented here are based on observing what framerate was being consistently reported for long periods of time after letting the playback loop a few times.


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:
  1. Call update() function using Main db, current Scene, and an RNA pointer to the data where the property that was edited lives.
  2. Call update() function using Context, RNA pointer + RNA property pair
  3. Just send a notifier (add tag)
  4. Tag the relevant ID-block affected (the "if (!is_rna || (prop->flag & PROP_IDPROPERTY) ) ..." case at end of rna_property_update())
Investigating these further, we find that only the first one is actually of any interest to the Animation System. Notes:
  • 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.
Now that we're restricted the domain to essentially once class of update() calls, we still need to know many mappings between these and the RNA pointers they operate on will need to be tracked.

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:
L1 = RNA Pointers - currently these are only compared by the ID-blocks, which for most updates() is all that really matters. Typically, there's just one of these per AnimData block, except where nested data is also affected.
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.

Closing remarks
Go check out animating the subsurf on a cube. It looks quite neat! (nice fodder for another little demo vid methinks...)

3 comments:

  1. I remember that I sent a bug report a while ago , about making an object array animatable using a driven property from an empty, it would´t work, now I know why(maybe if I don´t understand the all of the machinery that´s involved).

    That kind of functions are useful for animators like me, for example, to simulate a equalizer bar (just like the example in the bug report) that´s driven by an empty which it´s animation data was taken from a operator that blender has to calculate X axis(or something like that) from an audio file... thus making the array modifier incredibly useful for motion graphics, ala aftereffects.
    hope to see this baby in action in trunk soon.

    Cheers !!!

    ReplyDelete
  2. Aligorith, I would be very greatful if you will make animatable properties of the modificators on the F-Curves. In particular - Noise modificator.
    So happens that I need to my object was animated with shaking for a while, and then was calm again.
    I know, it can be done with Noise+Envelope modifiers tandem, but it's too complicated...

    ReplyDelete
  3. Thank VERY much for adding such a necessary feature to a modern 3D animation workflow. I'm anxious to see your branch merged with Trunk ASAP!

    Its seems like more of the DNA/RNA system could be cached and threaded potentially.

    ReplyDelete