Under the hood of Canvas
| The FlightGear forum has a
subforum related to: Canvas
|This article is a stub. You can help the wiki by|
abstraction layers always come with a penalty. That applies particularly when we're using them for use-cases other than what they're designed for (e.g. doing 3D stuff in 2D without any explicit support for that)
- Property Tree
- and finally, OpenGL
So, we need to keep in mind what is happening behind the scenes between the various abstraction layers, i.e. the translation overhead caused by going from one abstraction to the next one below it. The first step is Nasal (FlightGear's scripting language), it is used to provide an OSG-like high-level abstraction, but all it really does (well usually) is setting doens of properties in the global property tree under /canvas for you behind the scenes. Then, there is the property tree, it has listeners that are mappped to the Canvas subsystem which looks for a few well-defined properties and checks whether the changed properties match the heuristics to make sense (name of the property, type and value written to it, current state/mode of the element etc) - whenever that is the case, the whole Canvas must be marked as "dirty", triggering everything to be rendered. And then there is the "element" level: Each Canvas just represents a FBO/RTT context (an off-screen texture that is rendered but not normally visible), but there isn't anything to be rendered yet - it's just an empty texture with a background color, a size and a few other attributes. To become something that can be rendered, the top-level element that must be always added to a canvas is a so called "group", which represents a group of items that can be rendered, including other groups (and so on). Internally, what takes place here is that we have a base class implementing a Canvas::Element, this exposes attributes and methods that all Canvas elements have in common, including groups -because they're just that: elements inheriting from Canvas::Element. That is why all elements must be registered at the group level, e.g. images, paths or text nodes are registered by letting the Group element watch for property accesses that create new child-nodes using a corresponding name (e.g. image, path or text). The name serves as the lookup key for the lookup to get the helpers out of the factories implementing each element. Under the hood, this makes sure that the corresponding element receives "events" (property write notifications using listeners) whenever one of its child nodes is updated - but at that point, it doesn't yet know whether the event is really valid or relevant, because it still hasn't parsed the property name, the type or the value that was written to it - and checked whether those changes actually make sense in the element's given scope. Still, the property/canvas subsystem will make sure to dispatch events by invoking the element's notification handler so that it can parse/process the corresponding updates properly. That is one of the reasons why unnecessary property updates will usually affect performance of rendering the whole Canvas, because its top-level <group> element basically serves as a list of render-able elements (text, paths, images), and whenever one of those changes, the corresponding scene graph node is marked as dirty, and must be updated, and the whole Canvas must be updated/re-rendered afterwards. Thus, when dealing with primitives that may need to be separately hidden/shown, updated, animated, transformed etc - it makes sense to "group" them accordingly, i.e. by adding them to a separate group that can be directly addressed. This is also where the abstraction overhead comes in, because whenever we are dealing with such lists of render-able elements, there is not just the property tree representation in the global property tree, but also an internal representation that maps the whole property tree representation to something that makes sense to OpenSceneGraph, and in turn, to OpenGL. If we don't ever have to update a complex Canvas scenegraph after creating it, all it will do is render a textured quad - which it can do really fast, but once we introduce many state changes, we make things unnecessarily difficult and heavy, even if the state changes are redundant, because identical values are written to the corresponding nodes. Obviously, Canvas being all about modern avionics and dynamic stuff, we cannot just reuse previously lines/shapes - but what we can do is pre-allocate the corresponding data structures and clear/reuse those selectively, to reduce the allocation/re-allocation overhead. The next challenge is carefully reviewing dynamic elements of the scene, and grouping those accordingly (e.g. background images vs. elements that need to be animated)- i.e. looking at the requirements of what needs to be updated, and how that will affect the scene. For example, a group that merely needs to be transformed/translated will have much less of an impact than one that always needs to be updated/redrawn entirely. However, sometimes there are elements that merely need translations, but which still need different elements, e.g. labels - at that point, it makes sense to introduce a new, separate, group to keep the labels.
in rendering terms, a Canvas element represents an osg::Geode mapped to a property tree hierarchy. If you always end up calling removeAllChildren(), you always throw away previously created geodes and subsequently tell the Canvas to recreate those from scratch.
To selectively remove data from a Canvas, just put it into a separate top-level group - and use .hide() to make it invisible or .clear() to clear it out.
Note that you can also use these two in combination to selectively update those data structures - which helps reduce the updating overhead, i.e. no unnecessary reallocation taking place. In summary, the Canvas system really is a scenegraph - so if you have something that you'd like to be able to hide/show, clear or change selectively, just put it into a separate group and use that group as the "handle" to deal with the whole shebang (it really is a osg::Group under the hood, i.e. an osg::Transform IIRC, which is a child class inheriting from ::Group)
The point of my suggestions is to minimie internal state changes: Canvas is primarily a subsystem that works in terms of listeners, each element added to a Canvas, and all their child elements will monitor the tree for "events", i.e. property accesses/updates. It basically maps a tiny subset of the property tree to OSG primitives, so is a property-driven state machine. This may result in hundreds, or even thousands, of callbacks to be invoked recursively to propagate events properly. Whenever an element-specific property is updated (think a color, translation etc changed), this results in the element-specific geode to be marked as "dirty", so that the geode is updated. These updates can be minimized by not setting/updating certain properties, e.g. those of invariant canvas elements that will basically stay the same in between multiple frames - in such cases, the corresponding group/element-specific osg::Geode from the previous frame can be reused "as is" - which is not specific to Canvas, it's a general scenegraph thing to minimize unnecessary scenegraph traversals. So, regarding your comment quoted above, imagine how the workload of the Canvas system can be reduced if the spacecraft is stationary, even though we are still updating things per frame that never really change (orientation, altitude etc). If you really have to/want to use removeAllChildren(), don't use it per instrument, but only for those instrument parts that are variable - and put everything else into a static group that you never touch (think background images etc), and even then, consider if reusing the underlying data structures makes more sense to clearing out everything and re-allocating those from scratch. The Canvas-specific thing is that all this happening via listeners comes at a cost, which is why it is generally a good idea to check if reusing previously allocated data structures (as in, the elements/groups forming the geometry) can be reused, instead of clearing out the geometry and re-creating it from scratch - because at that point, you are throwing all optimization opportunities out of the window, because that will inevitably cause all code to re-run - whereas reusing parts of the scenegraph in the next frame, can simplify the workload tremendously - e.g. that is why we marked certain NavDisplay geometry as "static" and rendered that to a separate Canvas texture which we are treating as a texture map to get out relevant bits, at that point, it's just dealing with textured quads only - and no longer has to run any Canvas::Path or ShivaVG code to actually render the geometry.