Board index FlightGear Development Canvas

extra500 - Avidyne Entegra 9 IFD - approach

Canvas is FlightGear's new fully scriptable 2D drawing system that will allow you to easily create new instruments, HUDs and even GUI dialogs and custom GUI widgets, without having to write C++ code and without having to rebuild FlightGear.

extra500 - Avidyne Entegra 9 IFD - approach

Postby D-Leon » Sat Feb 22, 2014 4:43 pm

Hope i can enlighten my uncommented IFD code a bit.

My primary goal was to minimise all calls to the canvas api at runtime.

The object structure is based on IFD -> Page -> Widget

IFD:
    - holds the canvas instance
    - has the 2 timer loops 20Hz and 2Hz
    - the button function onClick(key)

Page:
    - parse the svg
    - init all widgets
    - provide the update20Hz(now,dt) and update2Hz(now,dt) function
    - provide the setVisible(bool) function
    - the button function onClick(key)

Widget:
    - provide the setVisible(bool) function


The setVisible(bool) function:
    - toggle the visibility of the canvas elements.
    - set/remove all listeners needed by this object.
    - register/unregister button callbacks.

So if a object is not visible there should be no call to the canvas api.

The update20Hz/update2Hz are selectively used by the widgets to avoid foreach calls in the Page.

At initialization time the page parse the svg and creates the widgets by pushing the "root-group" into the widget. I thought one svg per page is better to maintain.

The widget collects the needed elements by "getElementById()" and stores it for later use.
So at runtime there should be no "getElementById()" call and only the necessary animation calls of the visible objects are pumped.

The widget selects the way it updates the canvas.
Via update20Hz, update2Hz, listener or direct button callback.


NOTE: This sounds like a reusable framework but the encapsulation isn't as far and its optimised for the internal need.
There are some calls going over parents where no interface is "rechable" or defined.
D-Leon
 
Posts: 28
Joined: Wed Oct 03, 2012 9:44 am
Callsign: D-Leon
OS: Linux

Re: extra500 - Avidyne Entegra 9 IFD - approach

Postby Hooray » Sat Feb 22, 2014 4:47 pm

Yeah, admittedly we already spent 20 minutes examining all your code there -like I said in the other thread, we were kinda surprised seeing OOP code using design patterns solving a problem that we have been working on for several months. Actually, had we seen your code earlier, it would have been a much more mature foundation for a generic ND/MapStructure framework, because of it's OO nature. But in the meantime, we've come up with something fairly generic - mostly thanks to Philosopher's MapStructure framework.

We were actually planning on getting in touch with you, to help you generalize things a little more, and extract useful bits out into a dedicated Canvas module, while also adopting the ND code (or at least MapStructure) internally.

Is that something that you'd be interested in working out with us ?
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 12707
Joined: Tue Mar 25, 2008 9:40 am
Pronouns: THOU

Re: extra500 - Avidyne Entegra 9 IFD - approach

Postby D-Leon » Sat Feb 22, 2014 5:03 pm

Hooray wrote in Sat Feb 22, 2014 4:47 pm: had we seen your code earlier,

feel sry for that.
Hooray wrote in Sat Feb 22, 2014 4:47 pm:Is that something that you'd be interested in working out with us ?

another construction side, but i will try my best ;-)
D-Leon
 
Posts: 28
Joined: Wed Oct 03, 2012 9:44 am
Callsign: D-Leon
OS: Linux

Re: extra500 - Avidyne Entegra 9 IFD - approach

Postby Hooray » Sat Feb 22, 2014 5:12 pm

design-wise, most of the things you mentioned are also handled like that by the ND/MapStructure code - however, in a much less-organized fashion admittedly - there's really just a huge hash with predicates that have associated true/false callbacks that are evaluated once per update() loop - this will need some cleanup to better handle listeners and timers. The main issues are detailed at: http://wiki.flightgear.org/NavDisplay#Development

Technically, MapStructure layers are a bit more sophisticated than having pre-created SVG/OpenVG groups, because symbols can be cached in a separate canvas via a texture map, so that there's true "instancing" support for each cached symbol. This is something that we've been working on in the last two weeks. Also, positioned queries are handled by an abstraction layer to ensure that things are sufficiently fast using selective delta-updating. Besides, the ND/MapStructure code is intended to be reusable - and is already being used by a handful of different aircraft, so teaming up would be a win/win for all of us, i.e. improved resource usage/performance on the extra500, less code to maintain - and for us, it would mean that we would have someone else involved who already understands OOP and design patterns :-)

The widget selects the way it updates the canvas.
Via update20Hz, update2Hz, listener or direct button callback.

that is something that we should generalize and extract, because the ND code is doing this still in a pretty inefficient code.
Another consideration that was important in the way the ND code ended up being "structured" is that we wanted to decouple aircraft specific development from "framework" development. Currently, there's really just a single ND version coded: Boeing - but it's prepared to be easily adaptable for other types of aircraft/manufacturers.
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 12707
Joined: Tue Mar 25, 2008 9:40 am
Pronouns: THOU

Re: extra500 - Avidyne Entegra 9 IFD - approach

Postby D-Leon » Sat Mar 01, 2014 12:57 pm

This Canvas Test should show the difference performance in rendering between a group of small path and one big path.

It creates a table of fps at "paths count" and "update rate Hz".
0-1200 paths and 0-25Hz.
Which can bee copy paste into a spread-sheet for visualisation.
The fps is only recorded by a single snapshot after 3 sec.

It can loaded into any aircraft. ufo will gain max performance.

save it ~/.fgfs/Nasal/CanvasStressTest.nas so it will loaded a startup.
or load via
Code: Select all
io.load_nasal(getprop("/sim/fg-home")~"/Nasal/CanvasStressTest.nas");

#--- console 1 ---
Code: Select all
setprop("/sim/rendering/draw-otw",0);
setprop("/sim/rendering/on-screen-statistics",2);
var Test = nil;
var Test = CanvasStressTest.StressTestWindow.new(800,600);
Test.open();


#--- console 2 --- Test 1 take a while
Code: Select all
Test.setMode(1);### all icons as a single path into a group
Test.start();


#--- console 3 --- Test 2 take a while
Code: Select all
Test.setMode(2); ### all icons in one big path
Test.start();


#--- console 4 --- a single amount test
Code: Select all
setprop("/sim/rendering/draw-otw",0);
setprop("/sim/rendering/on-screen-statistics",2);
var Test = nil;
var Test = CanvasStressTest.StressTestWindow.new(1600,900);
Test.open();
Test.setMode(2);
Test.setAmount(3600);
Test.setHz(25);


~/.fgfs/Nasal/CanvasStressTest.nas
Code: Select all


print("loading ... CanvasStressTest");

var INDEX_HZ       = [0,5,10,15,20,25];
var INDEX_AMOUNT   = [0,50,100,150,200,250,300,350,400,450,500,550,600,650,700,750,800,850,900,950,1000,1050,1100,1150,1200];
#var INDEX_AMOUNT   = [0,50,100,150,200,250,300,350,400,450,500];
var MODE_NORMAL_PATH   = 1;
var MODE_ONE_PATH   = 2;

var Test = nil;
var StressTestWindow = {
      new : func(width,height){
         var m = {parents:[StressTestWindow]};
         m._dlg          = nil;
         m._canvas       = nil;
         m._groupStatic       = nil;
         m._groupDynamic    = nil;
         m._dynamicIcon       = nil;
         m._width       = width;
         m._height       = height;
         m._hz          = 1;
         m._iconCount       = 0;
         m._iconStatic       = [];
         m._row = 0;
         m._col = 0;
         
         m._timerLoop      = nil;
         m._timerAuto       = nil;
         m._statsIndexHz    = 0;
         m._statsIndexAmount    = 0;
         m._statsSec      = 3;
         m._color = "rgb(0,0,0)";
         m._onePath = nil;
         m._mode = MODE_ONE_PATH;
         return m;
      },
      open : func(title="StressTest"){
         
         
         me._dlg = canvas.Window.new([me._width,me._height], "dialog")
               .set("title", title);
         
         settimer(func() {me._dlg._widgets[0].onClick = func me.close();} ,1);
         
         
         me._canvas = me._dlg.createCanvas();
         me._groupStatic = me._canvas.createGroup();
         me._groupDynamic = me._canvas.createGroup();
         
         
                  
         me._dynamicIcon = me._drawIcon(me._groupDynamic);
         me._dynamicIcon.setTranslation(me._width/2,me._height/2);
         
         
         me._timerAuto = maketimer(2,me,StressTestWindow._statsUpdate);
         me._timerLoop = maketimer(1/me._hz,me,StressTestWindow._update);
      
      },
      close : func(){
         #print("StressTestWindow.close() ...");
         me._timerLoop.stop();
         me._timerAuto.stop();
         
         me._canvas       = nil;
         me._groupStatic    = nil;
         me._groupDynamic    = nil;
         me._dynamicIcon    = nil;
         me._onePath       = nil;
      
         me._row = 0;
         me._col = 0;
         me._hz          = 1;
         me._iconCount       = 0;
         me._iconStatic       = [];
         me._timerLoop      = nil;
         me._timerAuto       = nil;
         me._statsIndexHz    = 0;
         me._statsIndexAmount    = 0;
         me._statsSec      = 3;
         if(me._dlg != nil){
            me._dlg.del();
            me._dlg = nil;
         }
      },
      setHz : func(hz){
         #print("StressTestWindow.setHz("~hz~")");
         if(hz > 0){
            me._hz = hz;
            me._timerLoop.restart(1/me._hz);
         }else{
            me._timerLoop.stop();
         }
         
      },
      setAmount : func(x=100){
         #print("StressTestWindow.setAmount("~x~")");
         me._groupStatic.removeAllChildren();
         me._onePath = nil;
         me._iconStatic = [];
         me._iconCount = 0;
         me._row = 0;
         me._col = 0;
         me._drawIcons(x);
      },
      addAmount : func(x=50){
         #print("StressTestWindow.addAmount("~x~")");
         me._drawIcons(x);
      },
      _drawIcons : func(x=100){
         
         if(me._mode == MODE_NORMAL_PATH){
            
            for(var i=0 ; i<x ;i+=1){
               var icon = me._drawIcon(me._groupStatic);
               icon.setTranslation(me._col*20+10,me._row*20+10);
               icon.setScale(0.25);
               append(me._iconStatic,icon);
               
               me._col+=1;
               
               if(me._col>=me._width/20){
                  me._col=0;
                  me._row+=1;
               }
            }
            
            me._iconCount = size(me._iconStatic);
            
         }elsif(me._mode == MODE_ONE_PATH){
            
            if(me._onePath == nil){
               me._onePath = me._groupStatic.createChild("path");
               me._onePath.setColor(0,0.6,0.85);
               me._onePath.setStrokeLineWidth(3);
               me._onePath.setScale(0.25);
            }
            
            for(var i=0 ; i<x ;i+=1){
               me._col+=1;
               me._drawIconIntoPath(me._onePath,me._col*70,me._row*70+25);
               if(me._col>=me._width/20){
                  me._col=0;
                  me._row+=1;
               }
               me._iconCount += 1;
            }
         }
         
         #print("StressTestWindow.createIcons() ... "~me._iconCount ~" icons created");
         
      },
      setMode : func(val){
         me._mode = val;
      },
      stop :func(){
         me._timerAuto.stop();
         me.setHz(0);
         me.setAmount(0);
      },
      start :func(sec=2){
         me._statsSec = sec;
         me._timerAuto.stop();
         me._testData = {};
         me._statsIndexHz = 0;
         me._statsIndexAmount = 0;
         me._statsCount = 0;
         foreach(var amount ; INDEX_AMOUNT){
            me._testData[amount] = {};
            foreach(var hz; INDEX_HZ){
               me._testData[amount][hz] = nil;
               me._statsCount += 1;
            }
         }
         me._timerAuto.restart(me._statsSec);
         me.setHz(0);
         me.setAmount(0);
      },
      _statsUpdate :func(){
         
         var realFPS    = getprop("/sim/frame-rate");
         
         me._timerAuto.stop();
         var amount    = INDEX_AMOUNT[me._statsIndexAmount];
         var hz      = INDEX_HZ[me._statsIndexHz];
         
         
         me._testData[amount][hz] = realFPS;
         print(sprintf("%i   %i Hz   = %3.0f fps",amount,hz,realFPS));   
         
         
         
         me._statsIndexHz += 1;
                  
         if(me._statsIndexHz >= size(INDEX_HZ)){
            me._statsIndexHz    = 0;
            me._statsIndexAmount    += 1;
         }

         if (me._statsIndexAmount >= size(INDEX_AMOUNT)){
            me._statsIndexAmount = 0;
            me.finish();
            return 0;
         }
         
         amount       = INDEX_AMOUNT[me._statsIndexAmount];
         hz      = INDEX_HZ[me._statsIndexHz];
         
         me.setHz(hz);
         me.addAmount(amount-me._iconCount);
         me._timerAuto.restart(me._statsSec);
         
      },
      finish : func(){
         me.stop();
         var row = "path";
         foreach(var hz; INDEX_HZ){
            row ~= "\t"~hz~" Hz";
         }
         print(row);
         foreach(var amount ; INDEX_AMOUNT){
            row = "" ~ amount ~ "\t";
            foreach(var hz; INDEX_HZ){
               row ~= "" ~ sprintf("%3.0f",me._testData[amount][hz]) ~ "\t";               
            }
            print(row)
         }
         
      },
      _update : func(){
         me._dynamicIcon.setColor(rand(),rand(),rand());
      },
      _drawIcon : func(group){
         var icon = group.createChild("path")
         .moveTo(-15,0)
         .line(-12.5,-7.5)
         .line(7.5,-12.5)
         .line(12.5,7.5)
         .lineTo(7.5,-12.5)
         .line(12.5,-7.5)
         .line(7.5,12.5)
         .line(-12.5,7.5)
         .lineTo(15,0)
         .lineTo(7.5,12.5)
         .vert(14.5)
         .horiz(-14.5)
         .vert(-14.5)
         .close()
         .setColor(0,0.6,0.85)
         .setStrokeLineWidth(3);
         return icon;
      },
      _drawIconIntoPath : func(path,x=0,y=0){
         path.moveTo(x-15,y+0);
         path.line(-12.5,-7.5);
         path.line(7.5,-12.5);
         path.line(12.5,7.5);
         path.lineTo(x+7.5,y-12.5);
         path.line(12.5,-7.5);
         path.line(7.5,12.5);
         path.line(-12.5,7.5);
         path.lineTo(x+15,y+0);
         path.lineTo(x+7.5,y+12.5);
         path.vert(14.5);
         path.horiz(-14.5);
         path.vert(-14.5);
         path.close();
      }
};

var Test = nil;

### example calls ###

# var Test = CanvasStressTest.StressTestWindow.new(800,600);
# Test.open();
# Test.setMode(2);
# Test.setAmount(350);
# Test.setHz(25);
# Test.addAmount(30);
# Test.start();
# Test.stop();
# Test.finish();
# Test.close();


# Test.setMode(2);
# Test.start();

# io.load_nasal(getprop("/sim/fg-home")~"/Nasal/CanvasStressTest.nas");

### console example ###
# setprop("/sim/rendering/draw-otw",0);
# setprop("/sim/rendering/on-screen-statistics",2);
# var Test = nil;
# var Test = CanvasStressTest.StressTestWindow.new(800,600);
# Test.open();

# Test.setMode(1);
# Test.start();

# Test.setMode(2);
# Test.start();

# var Test = CanvasStressTest.StressTestWindow.new(1600,900);
# Test.open();
# Test.setMode(2);
# Test.setAmount(3600);
# Test.setHz(25);




D-Leon
 
Posts: 28
Joined: Wed Oct 03, 2012 9:44 am
Callsign: D-Leon
OS: Linux

Re: extra500 - Avidyne Entegra 9 IFD - approach

Postby D-Leon » Sat Mar 01, 2014 1:05 pm

In the extra500 I could gain some more rendering speed by :

Merging groups of paths into on big path like pitch-, speed- and alt-ladder and the compass-rose.
Merging groups of text into one big path also decrease the draw time.


Element count of PFD page:
Code: Select all
       before   merged
id      567      174
group   115      53
path    354      69
text    196      104
tf      840      330
cmd     3809     7261
coord   14716    30631


faced me to the overhead until the the elements are drawn.

CanvasStressTest.nas http://forum.flightgear.org/viewtopic.php?f=71&t=22183#p202203

Sitting in the ufo at nowhere draw-otw=false on-screen-statistics=2:
In the first mode which adds an amount of DME icons as a single path to a group.
My v-sync 60 fps starts to decrease at ~350-400 paths and end up at 1200 path by 25 fps.
In the secound mode the DME icon is added into one big path.
I can't really determine the rendering.
At 3600 DME icons in one path stable 60 fps and the draw time (on-screen-statistics) is increase by 0.8.



--------c/c++---------

Afer a one evening dive into the c++ of canvas. So far I had nothing to do with osg and ShivaVG. Thx for the nice readable code thumbs up.
---
at this point my understanding is:

A canvas element register its osg:drawable in osg.
The update, driven by subsystem loop collects the dirty bits and mark it for osg.
osg comes back and render the draw able via ShivaVG.

---

The rendering is going strait to the hardware. Can't see there any bricks.

I suspect that the osg callback overhead for each element is the bottleneck.

drawing a path of 4-10 coords, let a lot of gears turn;-)

Is it possible to register only the canvas itself as a drawable in osg?

And let the canvas iterate/draw over all. To get a more linear approach to the hardware and exclude osg form the internal rendering.

The main question would this gain more fps?

Sure we will loose the multi threading capability of the rendering process.
D-Leon
 
Posts: 28
Joined: Wed Oct 03, 2012 9:44 am
Callsign: D-Leon
OS: Linux

Re: extra500 - Avidyne Entegra 9 IFD - approach

Postby Hooray » Sat Mar 01, 2014 5:50 pm

Just briefly, thanks for doing these stress tests and benchmarks, really appreciated.
Then again, quite a few of the problems you mentioned were already solved via MapStructure, i.e. identical paths for different symbols are simply instanced by using a raster image (canvas) as a cache.

I'm sure that TheTom will also comment here once he finds some time.
Sure we will loose the multi threading capability of the rendering process.

BTW: I don't think we're currently really using any multi-threaded OSG rendering. Our property tree code isn't really thread safe, and neither are the corresponding extension functions.
It may be worthwhile to explore threading at some point, especially for canvases that don't have any external dependencies (i.e. no referenced canvas that would require serializing).
For OSG it would then be friendlier to use a real scenegraph for each group/element, so that optimizations become possible.
Technically, we would need to use one dedicated SGPropertyNode for each canvas and use serialization at the property tree level. That would then allow us to run all updates asynchronously, and only serialize getprop() accesses to the main property tree. For example, each canvas would have its own property tree (SGPropertyNode_ptr), and we could have a single dedicated update thread that runs all updates.
For all "self-contained" canvases, that would mean that they could be updated in parallel - without stuff having to run in the main loop.
But for this to be worthwhile, we would need to adopt more OSG:: data structures to map Canvas elements/groups, e.g. onto osg::switch etc
This would require a bit of Nasal-level restructuring, to ensure that we use different APIs for accessing the canvas property tree (thread) and the main tree.
But overall this should be pretty doable thanks to the property tree-centric nature of canvas. We would just need to make sure that each canvas has its own tree.
If anybody is interested in exploring this, let me know, I can probably help a bit with this and come up with a basic prototype that is only serialized with the main loop to ensure that changes take effect.
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 12707
Joined: Tue Mar 25, 2008 9:40 am
Pronouns: THOU

Re: extra500 - Avidyne Entegra 9 IFD - approach

Postby D-Leon » Sun Mar 02, 2014 4:38 pm

MapStructure - SymbolCache

this is pretty fast! 1200 images at ~55fps in the ufo. force me to rethink about my former suspect.
If the image calls are so fast then I exclude the osg callback.
There is something between osg and the coords rendering of a path what I can't see atm.

In the SymbolCache i had to make a small change. flip the y axis of the bounding box, to grab the desired icon.
Where should I push this in a separate branch?


for the extra500 PFD I'm thinking about to split the page into some layer(cached canvas).

- background all static images
- event/loop driven animations

I don't know if this make sense it will cost a lot of mem. Will try a more instrument orientated approach.
D-Leon
 
Posts: 28
Joined: Wed Oct 03, 2012 9:44 am
Callsign: D-Leon
OS: Linux

Re: extra500 - Avidyne Entegra 9 IFD - approach

Postby Hooray » Sun Mar 02, 2014 11:07 pm

MapStructure.nas should contain a few examples on stress-testing.
Regarding additional layers for the PFD, I would prefer things to be coordinated with MapStructure in mind, but also with Gijs' PFD in mind.

http://wiki.flightgear.org/Canvas_PFD_Framework
http://wiki.flightgear.org/Canvas_Animation_Framework

The main problems of the existing PFD.nas file are detailed at: http://wiki.flightgear.org/Howto:Coding ... _Framework
It's basically not yet using OOP at all.
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 12707
Joined: Tue Mar 25, 2008 9:40 am
Pronouns: THOU


Return to Canvas

Who is online

Users browsing this forum: No registered users and 0 guests