Monster Evolution in Qt: Part 1 (using Qt Script)

Speaking about JavaScript, Chrome Experiments launched some time ago show some very cool demos written in JavaScript, often combined with HTML 5 Canvas. My favorite is Monster Evolution, where a box morphs into a wildly rotating multi-tentacles monster, complete with lighting and motion blur. Written cleverly by Dean McNamee, this impressive script clearly demonstrates the potential of JavaScript to implement a 3-D engine. Seeing is believing, try it yourself.

The key for those demos are fast JavaScript execution and fast canvas implementation. No wonder the demos are running pretty smoothly under state-of-the-art browsers like Google Chrome or Safari 4. If you try to run some of them using QtWebKit-based browser, for example the web browser demo shipped with Qt, they are too slow and quite unusable. Although Qt 4.5 sports a much faster JavaScript back-end (see the previous comparisons against 4.4), our canvas implementation is not tuned yet for the maximum performance. Right now we are pretty busy with bug fixing and other high priority tasks, so canvas optimization will be carried out when the opportunity presents itself.

However, like any other good hackers (or foolish ones, depending on how you look at it), I can not afford to stay quiet in my cave and watch Chrome users enjoy the wonderful Monster demo. So I sat down and dedicated (or wasted, again depending on how you look at it) few evenings and one Creative Friday to scratch my so-far most challenging itch (read: obsession), i.e. to make Qt lovers be able to run the demo. To certain extent, I did not succeed (I know I am a lousy developer, but that is the story for another day). Here lies the proof:

As you can see, only 8 frames/second at the most intensive loop. That is far from acceptable. In fact, that is a failure. However, the rendering seems to be as good as it could be. Here lies the proof (or watch on YouTube directly or grab the video (AVI, 3 MB):

Note that the video looks smooth, but this is because it is just a time-lapsed video (i.e. not a real-time capture). So ignore the speed, just mind the rendering quality. Compare with what a web browser renders. To the curious readers: motion blur is implemented via alpha-blending. In this Qt implementation, it is as easy as keeping the default composition mode (SourceOver) and set the widget to have Qt::WA_OpaquePaintEvent attribute.

For the code, check out our usual repository under qsmonster directory. Non-git users can click on the snapshot link to grab the archive. You can use both Qt 4.4 or Qt 4.5 to give it a try. Running it using raster or OpenGL graphics system is also recommended. In addition, thanks to our very own David Boddie, you can enjoy this demo using PyQt as well.

You need an Internet connection while running the program, as it fetches the monster script directly from the original location (just like when it is running in a web browser). The program has two parts: 200-lines Qt/C++ or Qt/Python code and 100-lines of JavaScript code. QtWebKit is not involved, only QtScript is (ab)used here. It is short, it should be easily understandable by any seasoned Qt coders. Looking at the positive side, 300 lines of code to run one of the coolest Chrome experiment? Not bad at all :-)

Shocked because you can not reach 8 fps at all? Continue reading to find out the full story behind the scene.

Careful readers might notice that HTML canvas demo is something that is in Qt already, in the form of Context2D example. In fact, this is a good example of our QtScript module. Basically it is an implementation of canvas' 2-D context that can run the actual JavaScript code (Qt Script is more or less JavaScript). Does the Monster demo run with Context2D? With few minor tweaks, yes of course. However its performance is rather poor. The Canvas2D example is written for readability, not speed. After all, it is an example, not a real-world program.

After going nowhere with speeding-up Context2D example, I decided to start from scratch. Previous profiling showed that the bottleneck was for invoking QObject slots and marshalling the data. This is because Context2D uses a nice feature of Qt Script: QObject instance is available from the script world. After several failed attempts, I managed to create minimalistic canvas and context 2-d implementations in pure JavaScript which just collect the drawing commands and pass the end result to our C++ world. Since monster script relies on filling and stroking simple polygons only, my job was not so difficult. See the file called magic.js in the source code. For simplicity, I ignored all event handling (so no interaction using mouse or keyboard like the original demo).

The monster script is however very math-intensive. At its peak, it calculates the projection and lighting for around 600 little polygons per frame. If you want something like 30 fps, this is sadly too heavyweight for Qt Script. For example, profiling shows that most of the time spent in object member and property lookup. I tried to patch Qt Script with cached name id mapping, it did boost the performance a little but not significantly.

Some time ago, I gave the hint that one of our research projects is Qt Script implementation using JavaScriptCore as the back-end. Kent did a fantastic initial work, I picked it up, and voila! No source code change at all, JavaScriptCore-based QScriptEngine manages to run the complicated monster script. And running this qsmonster example with it, suddenly I got a magnitude faster rotating monster in my monitor.

To conclude: although the example runs with pristine Qt Script from Qt 4.4 and 4.5, it is rather slow. I released the code only for completeness, and so you try it again once Qt 4.6 is out.

Stay tuned for Part 2. I will show another very similar example which uses QtWebKit to run the monster script. And this will be something which you can try with your wonderful Qt 4.5 and still get a reasonable fps.

A little FAQ:

Q: Can I try the JavaScriptCore-based Qt Script implementation?
A: This is our internal experimental branch, it is fairly broken. It can't be used to run any non-trivial scripts. We would make it available for testing when we feel that it already reaches certain level of confidence.

Q: Does this example use QtWebKit?
A: No. The relation is only as I used Qt Script with JavaScriptCore backend for testing it.

Q: Can I run other Chrome experiments?
A: Very unlikely. I wrote this example specifically to run Monster Evolution (even excluding the event handling etc).

Q: Did you test with or without the JIT compiler?
A: Both. JIT compiler typically gives extra 20% boost.

Q: Why don't you use OpenGL to speed things up?
A: You do use OpenGL anyway, if you use OpenGL graphics system to launch the example. Anyway, the bottleneck is not in the graphics side. Drawing 15000 alpha-blended, anti-aliased, flat-shaded quads in a fraction of a second is not a big deal with typical graphics card these days.

Q: Does this mean QtWebKit canvas will be improved?
A: Yes and no. This example is specific only to the Monster demo. Some of tricks and hacks here are useless in other context. However, several ideas and attempts on making this example are valuable for generic canvas performance tuning.

Q: Where is the 3-D engine?
A: In the script called monster.js. This example downloads the script directly from the location you can see in the source code.

Q: Can you explain something about monster.js?
A: I got no clue, I did not write it. This qsmonster runs the script as it is.

Blog Topics: