CompositeViewer Support

From FlightGear wiki
Jump to navigation Jump to search

Started in 2020 July
Description Support for multiple independent scene views
Contributor(s) Julian Smith
Status Merged into next 2020 Nov [1]


CompositeViewer allows multiple independent views of the Flightgear scene, e.g. in independent top-level windows.

Status updates

  • 2020-10-5: Issues with window resize/close appear to be bugs in OpenSceneGraph-3.4, and are fixed by building with OpenSceneGraph-3.6.
  • 2020-9-27: Extra view windows now show textures and clouds etc, and rendering appears to be identical to the main view. This works by creating a new Compositor instance for each extra view window, and calling its update() method each frame.

General information

  • Use of CompositeViewer is enabled at runtime with: --composite-viewer=1
  • Video showing extra view windows and initial implementation of Canvas View: cvcanvas-demo.ogv
  • When enabled, CompositeViewer requires OpenSceneGraph-3.6 to work well.
  • Extra view windows:
    • The UI for creating new view windows uses new View menu items to clone the current view or take eye/target points from two earlier views. This seems to be convenient and avoids the need for a separate dialogue box or similar.
    • Can be resized.
    • Can clone Pilot View, Helicopter View, Chase View or Tower View.
    • One can create an extra view window that keeps two aircraft in view with one in the foreground. E.g. see this video from 2020-9-6 (prior to getting textures working):
    • One can create an extra view window that uses the eye points from two recent views as eye and target. For example this allows a window to show a view from one aircraft to another.
    • There doesn't seem to be any noticeable speed penalty if no extra view windows are opened.
    • Use a new view system called Sview (step view), which allows multiple instances and dynamic specification of eye and target points.
  • CompositeViewer support will allow us to render a view to a canvas and implement things like rear-view mirrors etc - see: Canvas_View_Camera_Element.


  • for the time being, use of reset/re-init and aggressive OSG threading options seems to cause stability problems even without having cloned any views [3]
  • Using OpenSceneGraph-3.6 causes problems elsewhere: OSGText Issues (under investigation as of 11/2020) [4]

Current limitations

  • users have been reporting a delay/freeze when cloning views [5]
  • Extra view windows don't have event handling, so for example one cannot change the angle of a cloned Helicopter view.
  • Sview does not support some views, e.g. Tower View AGL, Fly-past view.
  • Sview does not support aircraft-specific views.

Issues when using OpenSceneGraph-3.4

OpenSceneGraph-3.4 seems to get event handling wrong which causes problems if we open extra view windows - resize/close events get sent to our main window event handler which confuses things. There is currently no workaround for this.

These problems appear to be fixed in OpenSceneGraph-3.6.

Symptoms with OpenSceneGraph-3.4 include:

  • Creating extra view windows can cause the main window to seemingly think it has same size as the new window, so menubar and dialogues are shown partially or not at all. A manual resize of the main window seems to sort things out.
  • Attempting to closing an extra view windows sometimes closes a different extra view window.
  • Attempting to closing an extra view windows sometimes closes Flightgear down because the code thinks the main window has been closed.


Latest code can be found on the 'next' branch of flightgear, simgear and fgdata.


WIP.png Work in progress
This article or section will be worked on in the upcoming hours or days.
See history for the latest developments.

The natural way to manage a application that was two views on to two different scenes is to use a osgViewer::View for each separate scene, and then a osgViewer::CompositeViewer to manage these two scenes. These two views can share the same GraphicsWindow, or have their own. They may even be added/removed from the CompositeViewer, or have their rendering toggled on/off via NodeMask's on the master Camera for each View.[6]

The Viewer class is the simplist for of viewer and inherits from osgViewer::View, so has a single master Camera, and 0 or more slave Camera. While the CompositeViewer class contains a list of osgViewer::View, again each of these View has a single master Camera, and 0 or more slave Camera.[7]

The osgViewer:::CompositeViewer/Viewer architecture is designed to support one frame loop driving all the windows associated with that viewer, not multiple places trying to dispatch frame(). So you use a single timer. Or use multiple viewers.[8]

  • As a general approach, if you want multiple View's which have their own or share Scene's then the appropriate class to use is CompositeViewer as it's written specifically for this purpose.[9]
  • The usual way to manage multiple window views of a single scene graph is to use a CompositeViewer with multiple View's each view using its own or sharing a graphics window. [10]
  • What CompositeViewer provides is not so much performance improvement across the board, but rather far better granularity of design.[11]
  • The CompositeViewer and Viewer should have exactly the same performance characteristics w.r.t managing multiple cameras - as it's exactly the same ViewerBase code underneath that is managing all the threading and graphics rendering.[12]
  • CompositeViewer and Viewer share much of their implementation, the only key difference is that Viewer "is a" View, while CompositeViewer has a list of Views. All the event handling, camera manipulator and scene graph setting is done a the View level so has identical API to access. [13]


The OSG is design to allow you to rendering multiple views at once, there is no need to clone the scene graph, you simply add another View to a CompositeView to add the extra rendering. You can toggle optional View's on/off as you need them. [14]

For example, you could have a working set of View's that share the scene graph, and share the same graphics context. All these Views would be added to the CompositeViewer. At start up these View's would be disabled by setting their View Camera's NodeMask to 0x0.

When you need to render a View for a client you'd enable a View of one is attached and not enabled, enable by setting the Camera's NodeMask to 0xffffffff. If you run out of View then simply create a new one for the purpose. Potentially you could do this on demand - so have none at start up.

When you no longer need a View you could remove it, but it's lighter to just disable it via the NodeMask trick.

With this approach you aren't creating/deleting graphics contexts, will lower memory usage and you'll get better performance.[15]

Note: the slave osg::Camera aren't direct children of the View's master osg::Camera, but they have their view and projection matrices updated from the master camera.

Note II: osgViewer only threads Camera that are in the viewer, not ones enclosed in the scene graph, so your Camera in Camera won't thread.[16]

  • The right way to remove a view is outside of frame(). Not from an event handler from within the view, this will crash as you'll be deleting the object you are doing the work from.[17]
  • If you want to keep the view around for future use then perhaps the easiest way to do it would be to disable the View's camera by setting its NodeMask to 0x0 i.e. view->getCamera()->setNodeMask(0x0); [18]

In terms of closing a View, does this View have it's own GraphicsWindow(s)? If not then just setting the View's Camera's NodeMask to 0 will switch if off and set it to 0xffffffff to switch it back on again. If the View does have it's own GraphicsWindow then you'll need to close the window and switch off the rendering via the NodeMask [...] The other approach is to simply removing the View from the CompositeView and add back in a new one when you need it. The NodeMask route is lightest weight route though and is what I'd do if it's possible. [19]


The OSG has the osgDB::DatabasePager which sole purpose is to do multi-threaded paging of databases. The osgViewer::Viewer and CompositeViewer both support it out of the box, as did osgProducer::Viewer before it. There is nothing you need to do in your app other than load a paged database. Paged dabases in the OSG are ones that contain osg::PagedLOD node or loaded via the TXP plugin.[20]

  • osgViewer is set up so that the Scene object manages the scene graph and the database pager assocaited with that scene graph. There is one Scene object per scene graph, and multiple views should share the same Scene instance if there share the same scene graph. Virtue of this sharing the Scene shouldn't go out of scope while at lest on View still references, and neither should its associated DatabasePager go out of scope either.[21]
  • osgViewer::Viewer/CompositeViewer all have the DatabasePager built into them, and will automatically run the database pager thread on demand and take care of all the operations required to manage a paged database.[22]
  • it might just be far more productive to use the OSG's built in database paging support. All you need to do is decorate your subgraphs with a osg::PagedLOD or osg::ProxyNode, with osg::PagedLOD being the method of choice as it'll do load balancing for you - both loading new tiles on demand and deleting ones that are no longer required, all automatically done by osgDB::DatabasePager/osgViewer. [23]
  • The OSG has an database paging class call osgDB::DatabasePager that is built into the osgViewer::Viewer/CompositeViewer that will automatically load databases and merge them with the main scene graph during the update traversal. This is built around the osg::PagedLOD class, but you can also use the osg::ProxyNode.[24]
  • Normally one should ever need to worry about constructing or destructing the DatabasePager, it should happen behinds the scenes managed by osgViewer. osgViewer::Scene is used internally by osgViewer to manage one DatabasePager per scene graph. The Scene object will be shared automatically between Views if you assign the same Node pointer then you call View::setSceneData. This sharing is done automatically for you. When a Scene object is destructed it's DatabasePager will be destructed if no other references to it exist. If you have multiple Views that you should be using CompositeViewer, not multiple Viewer. If you are creating and destroying views regularly then you are probably best to enable/disable them by setting the View's master Camera's NodeMask to 0x0 and back to 0xffffffff, as this will switch off rendering but keep the backend around ready to be re-enabled.[25]
  • osgViewer automatically shares a single DatabasePager between Views when the Views share the same scene graph. This is required to avoid inconsistencies in the scene graph causing errors. Sharing a single DatabasePager doesn't prevent that pager from handling multiple viewpoints at the same time, if fact it knows nothing about viewpoints, it only handles database requests for tiles, so it totally agnostic to how you manage your viewpoints. Everything should basically just work out of the box without any need for specific settings from yourself.[26]

The existing DatabasePager functionality can page over the network, but this isn't an issue for the DatabasePager - its purely a function of the net plugin that does the loading across the http. This means paging and reading across the network are completely orthogonal and can be mixed and matched at will.

The Viewers already have support for adding and remove subgraphs from the main scene graph via the DatabasePager. You needn't add your own code as long as the database is set in a way that utilises DatabasePager.

You can write your own DatabasePager functionality, but it its likely to be alot less work just to use the built in paging support, this way the task for you is just how to build you database. Teaching you how to reimplement existing functionality really is way beyond the level of support I can provide for free.[27]


As a general note, there isn't a big difference between single threaded and multithread usage of the OSG in terms of OpenGL dispatch, with two separate windows there should be two separate graphics contexts and it should make no difference if you drive these from one thread one two, the state for each context should be kept local to each one.[28]

  • osgViewer::CompositeViewer is designed for applications that have multiple Views. The only thing to be careful of is when you are adding and removing View's from the CompositeViewer you should do is calling stopThreading() on the viewer prior to adding or removing views, then call startThreading() afterwards. If you are running SingleThreaded or CullDrawThreadPerContext you won't need to worry about stop and starting threads.[29]
  • osgViewer::CompositeViewer runs all of the views synchronously - one frame() call dispatches update, event, cull and draw traversals for all the views. [30]
  • OpenGL doesn't support multi-threading within a single graphics context, so you are constrained to doing the rendering for each context in a single thread. The threading models that the OSG provides reflect this, enabling threading of the update, event and cull traversals in parallel with the draw thread. [31]
  • if any code executed by the cull or draw threads (such as your own callbacks or custom nodes) isn't thread safe, then you must use SingleThreaded. [32]
  • as long as you have two GPU's the most efficient way to drive them should be multi-threaded - there is a caveat though, hardware and drivers aren't always up to scratch, and even then they should be able to manage the multi-threads and multi-gpus seemless they fail too.[33]
  • Cull and draw can only run in a parallel once all the dynamic geometry has been dispatched, otherwise the draw will be dispatching data that is being modified by the next frames update and cull traversals. Perhaps you have some dynamic geometry or StateSet's that are holding back the next frame. [34]

The ThreadPerCamera is just shorthand for CullThreadPerCameraDrawThreadPerContext, which will explain a bit more what's actually happening - it's meant to allow the draw thread to progress in parallel with the next frame.

There is a mechanism built into the backend to hold back the next frame if there are any Drawables or StateSet's with their DataVariance marked as DYNAMIC, however, if your whole scene is STATIC then this will allow the next frame to advance. There isn't any default additional mechanism for holding back the next frame. There are mechanisms for doing a swap ready check for multi-context systems [35]

Sharing scenes

  • Each View has one scene graph, and can share its scene graph between other instances of View. The View can also share the same GraphicsWindow, or have its own GraphicsWindow. The View also has a master Camera, and an optional list of slave Camera so you can scale from simple views up to complete distortion correction or multiple display output setups. Each View has its own event handlers and cameras handlers. Its extremely flexible and configurable. [36]
  • sharing a scene between View's is OK within one CompositeViewer as they will Views on the same scene will share the same FrameStamp i.e. there will be all at the same point in time. Sharing one scene between multiple Viewers will hit up against the problem that in one set of traversals the scene graph is one time and then the traversals from the other viewer will try to change the time back - and likely to cause a mess. This timing issue isn't likely to cause problems with high level rendering though - it should just mess up things like particle systems and sequences.[37]
  • When sharing a scene graph the osgViewer library automatically assigns an osgViewer::Scene for each unique scene graph, while sharing the Scene if you assign that scene graph to multiple View's. The Scene holds the DatabasePager for that scene graph so there isn't any overlap with multiple pagers trying to load stuff for the scene graph in an uncoordinated way.[38]
  • One way that could break this mechanism is sharing portions of the scene graph and assigning the subgraph to each View as the Scene won't pick up on the fact that it's the same overall scene graph. If you have this situationist then sharing the same complete scene graph and use a combination of NodeMask and TraversalMask for each View to make sure on the part you want visible in each View is seen.[39]
  • The best thing we could do would be to create a single GraphicsWindow and then share this between all our Views, we then won't have any problems with rendering order and sharing of textures or FBOs as it'll all be on one graphics context. See the sogcompositeviewer example for how to set up the Views/Camera & GraphicsWindow.[40]
  • Sharing a single window between multiple views is demonstated in the osgcompositeviewer example - you simply assign the same GraphicsWindow to the Camera's in each of the Views. You change views you can stop the viewer threads and then add/remove views you need then restart the threading, this will drop a few frames though due to stopping/start of threads. The other way is to switch off the rendering of the view by setting its Camera's NodeMask to 0x0 to disable it.[41]

Context sharing

The OSG allows you to create graphics context, make the context current and then dispatch rendering and do swap buffers yourself but this requires you to individually set everything up yourself.

What osgViewer does is provide all the basic functionality that 99% of users need out of the box, including handling context creation, multiple contexts, handling of database threading, viewer threading, event handling etc. It makes what would be a complicated task trivial, but with encapsulating all this functionality it has to make some assumptions about the way it's used[42]

  • It's possible to share contexts in the OSG [...] As for general desirability of share GL objects between contexts, yes it can reduce memory usage, but it forces you to use the OSG single threaded otherwise two contexts will be contended for the same resources that deliberately aren't mutex locked for performance reasons. There is also on a limited set of cases where drivers/hardware will actually share OpenGL contexts. [43]
  • Neither the OSG or OpenGL can provide thread safe sharing of GL objects when sharing contexts. If you want to run multiple context with multiple threads you will have to keep these contexts independent. [44]
  • If all your views share the same graphics context then it's only possible to single thread the draw dispatch. With this usage you'll be able to use DrawThreadPerContext which will allow the update and cull traversals to overlap the previous frames draw traversal, but overlap will only extend from the dispatch of the last dynamic object in the draw traversal being dispatched. If you have a large static scene then the overlap can be the whole frame, if you have lots of StateSet and Geometry with a DataVariance of DYNAMIC then the scope for threading is reduced, and at worst case will essentially be serialized and equivilant to SingleThreaded. Things that affect the draw traversals sometimes need draw threads to be stopped completely. Things like adding views to a CompositeViewer, or changing the graphics context on a camera, or things like that. It's pretty rare you need to do this. It's also pretty costly, because stopThreading() will only return once the draw threads have been stopped and deleted, and startThreading() only returns once new draw threads have been created and started.[45]
  • an OpenGL context is tied to a single window or pixel buffer. [46]
  • Sharing contexts is also something the forces a few limits on how you use the graphics contexts, such as it's only really safe to use them single threaded. [47]
  • If you are creating new graphics contexts and applying and old scene graph to it then you can't use the Texture::setUnRefImageDataAfterApply(true) feature of osg::Texture as this will discard the imagery once it's applied to all the graphics contexts that it knows about. [48] The typical problem is that the scene graph has been set up to unref texture images after apply so when it comes to reloading the texture images there aren't the to download. [49]

Image Sharing

  • Sharing of images is possible by using a frame buffer copy to osg::Image, or just having multiple FBO's all within one graphics context. If you can have the frames all done synchronously then perhaps you could have one frame loop and just disable the cameras via camera->setNodeMask(0x0); that you don't need updating on each frame, i.e. main viewer runs at 60Hz, and the other RTT cameras run at 20Hrz so get update on frame in 3.[50]
  • ImageStream is updated by a background thread. The xine-lib and QuickTime plugins both subclass from ImageStream and OpenThreads::Thread to provide a class thatautomatically runs updates on itself - this thread updates the image data on the ImageStream and then calls dirty to tell the rendering thread that it needs to download the data to any associated texture.[51]
  • It's better to update the data stored in the Image directly and call dirty, or to allocate the image memory separately and disable the deletion of the data on the image so the osg::Image(Stream) never calls delete on the data.[52]

Multiple Viewers

If you don't want the main rendering loop to wait for the rendering of all these extra views then you'll need to use a separate viewer(or compositeviewer) with it's own threading. You'll need to manage your own frame loops in the secondary viewer.

The only reason to copy data is if it the data is being modified by the different threads. [53]

this should probably be possible given separate threads for each of the viewers frame loops i.e. run them all in a background thread.[54]

You could easily just create an entirely separate viewer for doing the screenshots. You can have this run in the background with no need to affect the main viewer's threading/graphic contexts.[55]

you will be able to do is use two separate Viewer's. You are likely to want to run two threads for each of the viewers frame loops as well. To get the render to image result to the second viewer all you need to do is assign the same osg::Image to the first viewer's Camera for it to copy to, and then attach the same osg::Image to a texture in the scene of the second viewer. The OSG should automatically do the glReadPixels to the image data, dirty the Image, and then automatically the texture will update in the second viewer. You could potentially optimize things by using an PBO but the off the shelf osg::PixelBufferObject isn't suitable for read in this way so you'll need to roll you own support for this.[56]

For viewers to share the same node group, you need take care of explicitly sync'ing the FrameStamp between each traversal as otherwise the state of the scene graph can get thrashed between different times.[57]


Experimental CompositeViewer Support showing 3 cloned views, with Draw masks set (only skydome shown) + OSG stats (~ 120 fps/window) while using CullThreadPerCameraDrawThreadPerContext [58]

Until mid-2020, FlightGear only supported one view position at a time. Multiple independent view positions (e.g. one screen for the tower view and a second screen for the plane) would complicate a "locked to cache" flag quite a lot...[59].

Aircraft can define their own views and so on. But only one view can be active at a time. So no matter how many windows and cameras you define in Defaults.xml, they all are relative to the current view in FG (i.e. cockpit, tower...). [60]

Back in 2008, Tim Moore provided a patch (mailing lists search for CameraGroup FlightGear Mailing Lists) to use the osgViewer class to set up windows, manage the main camera, etc. [61]

However, these windows have to use the same camera group as the main window so can only show the view from the same eye position, though typically at a different angle/offset so that one can emulate things like side windows of a cockpit displayed in a different window or monitor.[62]

Mathias Fröhlich used the slave camera feature of osgViewer to provide a "video wall" style of multiple displays that was demonstrated at LinuxTag for years. Later on, Tim generalized this to support general monitor arrangements (like a panoramic arc) and general combinations of screens and graphics cards. [63]

The default OSG model is that slave cameras are different views offset from a common viewpoint. This is easy to understand when considering a camera's view matrix, but not necessarily intuitive when thinking about the projection matrix. Because FG has its own view system we mostly treat the slaves as independent. It seems that most other uses of cameras during rendering -- for example, render to texture cameras for effects -- are best handled by slave cameras with independent views as well.[64]

People requiring multiple independent views on the same scenery, e.g. cockpit and tower view [...] these each need their own camera groups and so require OSG's CompositeViewer.[65]

And that's not really supported by the current architecture, neither by the tile cache nor by osgViewer::Viewer. We would need to move to a CompositeViewer model, which supports several scene graphs, and rely completely on the osg database paging machinery.[66]

That would require a change in current fg architecture to use a CompositeViewer instead of a single Viewer, but we're contemplating that anyway.[67]

The cameras in a camera group don't need to render directly to the screen. They can render to a texture which can be used either in the scene, like in a video screen in the instrument panel, or for distortion correction in a projected or dome environment. [68]

Open Scene Graph supports a CompositeViewer object that supports rendering from several widely separated viewpoints, complete with support for multiple terrain pager threads. We could move to CompositeViewer and support simultaneous views from e.g., the tower, AI models, drones, etc.[69]

Neither of these are supported at the present time, but it would be a good project. We would have to start using a different OSG class, CompositeViewer, to support multiple views from independent view points. Our terrain pager would need a complete overhaul to use the PagedLOD scheme of OSG, and the Flightgear View manager would need to be aware multiple active views.[70]






  70. =