Wednesday, February 27, 2008

animating lots of things simultaneously

there's a new game in kdereview right now called kdiamond, writted by Stefan Majewsky. it's a fun little game that is much like the classic bejeweled. with any luck it'll move from kdereview into kdegames for 4.1.

while playing it, i noticed that sometimes the animations weren't overly fluid. this was something that others had noticed as well. i did a quick valgrind and saw that the actual drawing and game mechanics code wasn't really significant, though there was a lot of time being spent in driving the event loop. without even looking at the code i could guess at the problem: each diamond on the canvas was animating itself. indeed, that's what was going on.

i wrote an email to Stefan suggesting a fix (which he implemented very quickly; sweet! =) and Zack suggested that i should write a blog entry about this general issue since it may be of use to others as well. this is that blog entry. there's nothing new or even overly interesting to graphics developers, but for the rest of us ... it might be helpful.

so... why is it so nasty to have individual items animate themselves? the obvious approach to animate a bunch of items is to give each item a timer (e.g. QTimeLine) and move the animation along in time to the progress of the timeline. in the case of moving an item, this is a simple matter of taking the start and end points, the current progress of the timeline (e.g. from 0.0->1.0), figuring out how far along the item should be on a given path between the two points based on the progress value and setting the location. the math is trivial and everything gets nicely encapsulated in the animated item's class.

here's the problem, though: let's say that the item is being animated at a nice smooth 25 frames per second. that gives us a 40ms delay between frames. (that's an oversimplification: the delay will vary depending on actual system activity and other processing in the app itself, but let's go with this generalization for now.)

if we have two items animated then we have that same 40ms window, but now it is divided into two pieces averaging 20ms in length. (the real intervals may be 10ms and 30, or 6 ms and 34ms, or whatever; again, this will vary from interval to interval ... but the average in the oversimplified scenario is useful information here.)

obviously, as we add more and more items, if the distribution is somewhat random (and in practice it will be) then our time slice between animations gets smaller and smaller and as we approach 40 items we end up with an animation happening every millisecond (again, going with the oversimplified generalizations). over 40 items and obviously we're into the sub-millisecond range. the problem, however, is not in animating the movement of those 40 items (that's probably very fast) ... it's the "in between" part that causes us grief.

with every item having it's own timer, each animation step of each of those items implies going back to the event loop, checking the next timer and if it's scheduled to trigger emitting its signal (or put another way, calling the connected methods) and then returning to the event loop. suddenly there's a lot more time variance and the animations will start to appear sluggish and completely uncoordinated as the individual frame timings drift about. but that's not the worst of it.

what's really bad is that while in the event loop other things will happen. really expensive things, like repaints. using a canvas such as QGraphicsView, it will eventually decide to update its contents on one of those trips to the event loop and trigger a bunch of repaints. if this happens when N out of M total animations have stepped through their frames, then not only will you get a paint with some of the animations in step and some not, but a (relatively) huge delay will also be introduced as all the data structure traversal, math and then resulting painting necessary to update the canvas happens. while fast in the general case, this can end up being detrimental to the fluidity of the animations if triggered too often and without coordination with the animations.

besides canvas paint, user interaction events and other input data processing will end up getting in between the individual animations. it just all gets very messy and animation frame latency starts to suck.

(firing all those timers randomly without aligning them is also rather bad for power consumption as it wakes up the cpu more often, but for most apps doing animations that's often not really a priority issue.)

the solution, thankfully, is really quite simple: share an animation tick. this is nothing new, really, and has been done in graphics programming since the days of yore, when the grass was green and amigas were still impressing people with that stick figure guy juggling three ray traced balls. ;) but it's still something that i noticed gets missed, especially as more people are adding them to their apps, often for the first time.

how it works is really simple: most animations are simply updating an internal state and then using that state to affect some sort of visual change, such as the start/end point interpolation of the above movement example. so you start a single timer that triggers a step forward in the animation ("the next frame" or "a tick") and on each of those ticks every active animation is iterated over and their state is incremented (whatever that means for the given animation).

in this way the event loop is exited and entered only once for all the animations. upon re-entering the event loop the scene or canvas is free to update itself with all the animations having been updated and ready to go in a coordinated fashion. so for each 40ms slice there is one animation tick, the time required to update the animations elapses (that should be very fast, even for good numbers of items) and then the repaints needed can happen.

the end result should be a lot smoother and just "feel" better since things will be moving together at the same semi-random interval rather than each at their own semi-random interval.

16 comments:

matze said...

In the light of this, it probably makes sense to add some kind of timer event coalescing on a generic scale. Kernel people have been doing this for dyntick as well to have timers with no strict precision requirements fire with a certain granularity only. Could be an addition to QTimer, like
QTimer::setGranularity(QTimer::GranularityClass),

and have all timers of the same granularity class fire in the same mainloop turn.
The granularity class could be something like

enum { Second, TenthOfSecond, HundredthOfSecond, MaxPrecision }

Or, the granularity could be expressed in milliseconds.

Anonymous said...

uh ? where's the promised screencast ? >;[

Anonymous said...

Don't we all miss the vertical blanking interrupt feature of the Amiga? :)

Seriously, even if X11 doesn't yet implement VBI, Qt or KDE should have an API for that (e.g. notify me around 25 times per second, but synced to frame, or every frame), and until X11 has real VBI it could fake them using standard timers assuming, say 60 Hz rate.

Anonymous said...

Screencast postponed again ? :-(

Aaron J. Seigo said...

@matze: i think so. in fact, i've done something this in plasma for both DataEngines and Phase. i should probably make something general for all of KDE or just get the Trolls to do it in Qt ...

screencast: coming today, yes. will blog again this afternoon with it =)

Jeff Schiller said...

I had a question - does KDE support SVG declarative animation?

André said...

OK, so that that mean that you basically can not use QTimeLine for your animations? At least, I don't see a way to hook QTimeLine to an external (Q)timer that is sharable with other QTimeLine objects.

I think that this is an issue that should be solved in Qt, where QTimeLines can (or by default do!) use a common timer system.

piacentini said...

I *think* I understand the issues involved, but how about QTimeLine? The way I see it, the main problem is really that the QTimers are being fired by several QTimeLines, right? What is the best way to still use this facility and does not cause slowdowns?

piacentini said...

Just to clarify my previous post: I think what we need is a way to optionally synchronize QTimeLines, or replace them by another class that can optionally share a single QTimer. In the case of KBlocks (also in playground/games), I created a class called BlockAnimator, which essentially is used to avoid attaching one QTimeLine to each element, and avoids exactly this kind of problem. It works nice for this game as usually only one (or at the maximum) two animations are playing at the same time, but it would be even nicer to have a way to really make QTimeLines kind of "share" an underlying timer that can have its granularity specified, while still keeping their nice API.

Leo S said...

@andre

Just share the QTimeLine. Instead of having the items construct their own QTimeLines, I just pass them all a shared QTimeLine and they set up the animation state internally and hook up to stateChanged signals.

André said...

@leo s:
I don't get that. If I want my animations to act independently, how could I use a shared timeline? The whole idea of the QTimeLine is to provide a nice way to provide the animation states, right? I understand that you can share a timeline if you want to animate different objects synchronously, but how would you do this if you want them to be independent?

Say, you want to animate objects on the entering and exiting of the mouse pointer, and you have several of these objects on your form/canvas/whatever. That means that as your mouse moves over the application, you can have the situation where you are leaving one such object (animating a leave event) and a moment later entering another one (start animating an enter event). These animations are independent. So, how do I make them share a single QTimeLine?

Leo S said...

@andre

Yeah you're right. In my app I just use animations that are synchronized, so it's not a problem for me. Lots of items are being animated in different ways, but they all start and stop at the same time, so I can have them share a timeline. If you want independent animations starting at different times then I guess you can't use that approach.

Anonymous said...

Sounds like you QT folks really need to check out Clutter...

Anonymous said...

@André:
QTimeLine has a setCurrentTime function so that you can manually control the "time" while the timeline is in the NotRunning state. The doc gives the example of hooking up the setCurrentTime to a QSlider (eg. seek in a video), but it should also work to hook it up to a shared QTimer.

To have have multiple independant timelines:
- start the shared timer
- keep track of when each timeline is "started" (without calling start())
- when the shared timer triggers at its 40 ms interval, call setCurrentTime() on each timeline, using the current time minus the timeline's start time

Now, all of your timelines will be updated and only one timer is being used. Hopefully this clears it up a bit :)

(One thing you'll have to try is if the timelines still emit the frameChanged() and valueChanged() signals while in the NotRunning state.)

Aaron J. Seigo said...

@anonymous: "Sounds like you QT folks really need to check out Clutter"

Qt, not QT.

if you are suggesting to check Clutter out for ideas, then yes there are nice things in Clutter (though it's not exactly groundbreaking in terms of new concepts; which is ok, it doesn't have to be =); but it still means people educating themselves and that's kind of the issue here isn't it.

i'm really addressing those rolling their own bits of animations.

as for actually using Clutter, it doesn't help much when it comes to bringing animations and effects to the native widgets (e.g. QWidget), and we're seeing that more and more in KDE/Qt applications since Qt4 makes it rather easy to do so.

there are also Qt native facilities to do the sorts of things clutter aims to (without reinventing the toolkit in the process)

@anoymous: "if the timelines still emit the frameChanged() and valueChanged() signals while in the NotRunning state."

yes, it does.

i do agree that it would be awesome to have this facility built into QTimeLine though.

André said...

I have filed a bugreport with Trolltech to ask for a fix in QTimeLine itself.