Under the hood of Canvas

From FlightGear wiki
Revision as of 14:32, 29 October 2016 by Hooray (talk | contribs)
Jump to navigation Jump to search
This article is a stub. You can help the wiki by expanding it.

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)

  • Nasal
  • Property Tree
  • Canvas
  • SGSubsystemMgr
  • OpenSceneGraph/ShivaVG
  • 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.[1]

Related

References

References
  1. Hooray  (Oct 29th, 2016).  Re: Nasal must go .