Sunglasses | Short Synfig animation [+ detailled analysis for optimization]

Hey guys!

I’ve been quite busy learning animation. The goal was to make a very short animation featuring an original character. And here’s the result:

The way it’s done is ridiculous. Every vertex has a matrix rotation formula applied to it, so it’s basically a 3D animation made in 2D software. I pushed Synfig so hard that at one point it could not longer handle the project (the sif file for a character alone is about 35MB and has tons of formulas) so I had to modify the source code with my poor C++ skills to make it perform in an acceptable for me way. Although it is not the technique that Synfig was designed for, it does show what kind of potential the program has.

Here I’ll briefly list problems with Synfig I discovered in the process of making this animation. Some may already have been fixed since I based my modifications on a pretty old development snapshot, so try to understand. Here’s the list:

  • Synfig is painfully slow at loading projects. One reason is it constantly calling a ‘getenv’ in different places. So lots and lots of WinAPI calls (I am on bloody Windows) + string comparisons. I removed it from all the valuenodes and some other places (e.g. Canvas::on_changed, in loadcanvas.cpp), it got faster. Oh wait, this problem was recently noted. Yay.
  • The other reason is Synfig passing huge objects (like Canvas) through stack. For example, ValueNode::set_parent_canvas calls ValueNode::set_root_canvas completely unnecessary since set_parent_canvas can call x->get_root() itself. Rework it and you get a performance boost. And so in other place, like loadcanvas.cpp where you remove calls to set_root_canvas and replace it with some other procedure, let’s call it assign_root_canvas that takes ‘Canvas’ object by reference and you get another performance boost.
  • In Advanced_Outline::connect_bline_to_dilist there’s a weird check if bline has any points. It increases the loading time drastically if bline has converters (they have to be evaluated). Is it even possible to construct such erroneous outline? I removed this check and nothing broke and loading time decreased.

Those three optimizations decreased the loading of my character file from >1 hour (I gave up on waiting) to 20 seconds. So Synfig turns into a turtle if there are lots of nested converters (and I mean A LOT).

  • The performance of Synfig’s render is quite poor too. The first, most glaring issue, is that multi-threading is NOT utilized. At all. My character contains only simple vector elements (regions, outlines) and I never saw Synfig using more than one core. Need to construct some test case file and try to figure out what’s wrong.
  • The calculations of formulas can be optimized by only calculating exported values once. It’s beneficial since exported values are supposed to be used as a variable in converters and so the more converters, the more benefit of this optimization. For example, check this code I added in ‘valuenode_bline.cpp:ValueNode_BLine::operator()(Time t)const’:
    // Return early if we already processed this bline if (!get_id().empty() and LastChanged == get_time_last_changed() and ProcessedTime == t) {return ValueBase(store, get_loop());} ...calculations... store = ValueBase::List(ret_list.begin(), ret_list.end()); LastChanged = get_time_last_changed(); ProcessedTime = t; return ValueBase(store, get_loop());
    This code significantly increases performance of ‘Spline Vertex’ converter that I used a lot in my animation. And the same can be done for other converters that start formulas such as add/subtract.
  • Synfig doesn’t handle rendering of raster images properly. By some reason, when it renders raster graphic (set of pngs in my case) the CPU goes to 100% and memory consumption is ridiculously high. I remember reading something about rendering problem when compiled with cmake (which I did) but don’t quite remember the details. Gotta come up with a test file in order to illustrate the problem.
  • The procedure CanvasTreeStore::set_row that populates ‘Parameters’ panel doesn’t do a very good job if there are lots of sub-parameters (converters). In my case, switching between one layer and another could take up to 15 seconds. So you click, you wait, you click off to another layer and you wait again. The workflow was killing me, so I add a hack where it doesn’t expand bline when dealing with ‘Spline Vertex’ converter, but it’s not a proper solution. I think it should expand stuff on request (when user clicks on ‘+’ sign in a tree).
  • The Node class and its ‘*changed*’ methods are a mess. I don’t know what’s going on but it generates much more signals that are needed to be generated. In my case, the character is a 3D model, so imagine changing the angle ID named ‘CAMERA-YAW’ that is linked to ALL other YAW-ROLL-PITCH nodes. It’ll have to update all the linked sub-formulas, for arms, fingers, legs, etc. In result, there will be SO MANY queries for ‘studio::WorkArea::queue_render’ generated that Synfig would just choke on it and will be in hang mode for a VERY long time. I added a check in Node::on_changed():
    for(iter=parent_set.begin();iter!=parent_set.end();++iter) { if (time_last_changed_ != (*iter)->get_time_last_changed()) {(*iter)->on_child_changed(this, time_last_changed_);} }
    with some additional code around it. And it somehow works, but leads to errors when you enable/disable layers (they show up as a default black outline). I don’t know how to handle it properly.
  • Hmm… It could be nice if Synfig allowed to write functions. This way I could have only one exported formula for matrix rotation and all other valuenodes would call it with appropriate parameters. How to implement it though? I have no idea.

OK, enough with performance issues, let’s move to general bugs/requests:

  • ‘Z depth’ is screwing with me. Something wrong when a lot of layers are in root of canvas, and they all animated with ‘constant’ interpolation (or maybe interpolation is not important). At some point, I had to change z depth of a layer a bunch of waypoints earlier! So in order to change z depth at 12s, I had to make a change at 11s, how’s that supposed to work? This issue almost drove me insane and I cannot tell you what caused it. Gotta investigate it.
  • When rendering through console application (synfig.exe), quality of animation is not possible to set (-Q switch)
  • Dashed outline works weird, it doesn’t stretch dashes when vertexes are animated, but rather shifts it. This behaviour does not allow making textures out of dashed outlines because it gets messed up when you start animating.
  • The width points on ‘Advanced outline’ layer are NOT looped which results in a non-optimized path-finding when you animate them (they can take the longest path from A to B). I got so tired correcting the outlines, so I just gave up. You can see in the animation, that lines are far from perfect.
  • ‘Warp’ layer has anti-aliasing enabled for it and it should not be because it means you cannot sew two or more warp layers together neatly. You can see this artefact in my animation at 00:06. Look at right-upper corner, you’ll see a line, it’s where two warp layers meet.
  • ‘Curve warp’ layer is so buggy and slow that it’s just useless at this point. And this layer, theoretically, allows you to do insane stuff, like implementing wind and animating folds on clothes. So it would be cool to port it to new render.
  • Synfig sometimes hangs when you right-click on a waypoint. It happens on some and doesn’t on others. Don’t understand why, needs investigation.
  • The blasted parameters for ‘Lock feature/past keyframes’ along with ‘Default interpolation’ are not saved in the meta of sif file like, for example, grid setting are. God, this small detail means I have to change those setting every single time Synfig is restarted. And it wouldn’t be such a big problem, but you see, Synfig likes to crash… sometimes a lot. I think those setting should be saved in the meta of the sif file.
  • Those same ‘Lock feature-past keyframes/Default interpolation’ parameters are global. It means if you have, say, 3 canvases open and you change mentioned settings they’ll change for ALL canvases which made me mess up interpolation quite often because I remembered that “this canvas has ‘linear’ interpolation”, but oops, it was automatically changed to ‘constant’ when I was working with ‘Z depth’ in other canvas. So, those settings should be per canvas.
  • Preview window cannot calculate start and end time properly if you specify start time of the project anything different from zero.
  • The exported symbols of the parent canvas cannot be resolved in child canvases. Imagine you have a file named ‘Scene.sif’ which has an exported valuenode called ‘GlobalZoom’ and the file imports two other files called ‘Char1.sif’ and ‘Char2.sif’ which both reference ‘GlobalZoom’. This combination won’t work, but it should, since DEFS section of ‘Scene.sif’ is resolved. I did a very dirty hack for it that generates a lot of errors when canvases are removed and sometimes crashes Synfig on closing.
  • The ‘Plant’ layer could be improved by allowing particles to be other shapes than a square or maybe even accept exported canvases (think using ‘Plant’ layer to generate leafs for a tree).
  • The ‘Bevel’ layer could be improved by controlling its steps (how many clones are created, 6 is the current default) and allowing to set their transparency to one value to make it look more 2D-ish
  • The ‘Shade’ layer could be improved by adding ‘Transformation’ widget to it (I may do it myself, gotta contribute something)

I may have forgotten something, but oh well.
Gonna take a break from animation, maybe make a bunch of issues on GitHub explaining some of the problems listed here in more detail.
thumbnail

6 Likes

Kudos for all these details in analysis!
Let’s hope this will help us to fix all of this quick :+1:

Actually, the point there is to check if “bline” parameter is actually a valuenode that provides a list of blinepoints. usually, it wouldn’t be a problem, but it can be with a malformed .sif file, that would make Synfig Studio crash. However, it computes 4 times, instead of only one (twice for Width Point and twice for Dash Item).

I created a PR that should fix it: perf: avoid double computation by rodolforg · Pull Request #2849 · synfig/synfig · GitHub

2 Likes

This is not right. ValueNode::set_root_canvas is not just about making root_canvas_=x->get_root();.
If it is a LinkableValueNode (most of them are) or ValueNode_Bone*, there is more stuff to do.

1 Like

Hi!

Can you add issue with test file to Github? This will help a lot.
Or just share it here - I will add issue myself.

1 Like

Thanks, I will test it.

It isn’t? As I said, I am not good with C++ but the code of the procedure is this:

void
ValueNode::set_root_canvas(etl::loose_handle<Canvas> x)
{
        DEBUG_LOG("SYNFIG_DEBUG_SET_PARENT_CANVAS",
                "%s:%d set_root_canvas of %p to %p - ", __FILE__, __LINE__, this, x.get());

        root_canvas_=x->get_root();

        DEBUG_LOG("SYNFIG_DEBUG_SET_PARENT_CANVAS",
                "now %p\n", root_canvas_.get());
}

Ignoring debug statements, there’s just one one line root_canvas_=x->get_root();, I don’t see it’s doing more stuff.

Sure, I’ll try to construct a test file. I initially planned to spend some time making issues on Github with test files and everything.

Edit BobSynfig: Better code display :wink:

1 Like

It isn’t. There are method overrides for derived classes, like:

and

Anyway, the Canvas object is not passed as argument, but just a “pointer”.

Oh, OK. Deep stuff.

In this case I am lost and don’t understand why I got an improvement in performance when I rework part of valuenode.cpp to use a pointer. I made a procedure identical to set_root_canvas instead of the definition, which looks like this: virtual void assign_root_canvas(etl::loose_handle<Canvas> *x);
Then I made CanvasParser::parse_value_node call assign_root_canvas instead of set_root_canvas.

The issue is open: Parameter panel hangs when there are lots of converters · Issue #2853 · synfig/synfig · GitHub

Because the original code calls set_root_canvas() for every valuenode link, and you replace it to just affect itself.

Edit: AFAICS, it’s only important if we are using ValueNode_Bone in the Canvas. So you can safely work with your custom code as long as you don’t use Skeleton Layers and bones. One day, we hope, Skeletons will be redone/refactored…

1 Like