Two or more gauges calling the same update function

JB3DG

Resource contributor
#21
I suggest switching to the gauge class format of the MSDN GDI+ example. It just occurred to me that you could getting both gauge header pointers in both callbacks which is killing your DME display. Having a user data identifier can help prevent that from happening.
 
#22
So separate them out into a GaugeDeclarations.cpp (it has no header) as in the example? If that structural change is necessary, the example has the module_init, module_deinit, and the GAUGESLINKAGE table in GaugeDeclarations.cpp. I have a bunch of other systems controlled by the T37 "main" class which drives the tweet systems. So is it possible to remove the DME Indicator and Gunsight GDI+ gauge declarations out and leave a dummy bitmap with a gauge declaration in the main T37.cpp (T37.h has the preprocessor macros) along with the module_init, DLLMain, module_deinit, and the GAUGESLINKAGE table so that I don't screw up the other aircraft systems? Note: the SimConnect stuff in the example will be different as I have an adapter class that is a single POC interface to SimConnect component.
 

JB3DG

Resource contributor
#23
??? The gauge class doesn’t have to contain anything systems related. It is purely the wrapper for handling rendered displays. You can also have a regular gauge callback not part of the class for some systems handling in an invisible gauge that doesn’t have any rendering code.

But on that last point, I would actually recommend moving your systems code out of the gauge callbacks and into a simconnect data request event that is hit every sim frame. The gauge callbacks in P3D are multi threaded and are anything but stable timing wise. The sim frame data request may occasionally be hit twice in a frame but if you use the GetSimTime function from the wiki and calculate delta_t between frames, you can skip the extra events in the frame.
 
#24
Thanks for the help. I'll try and implement your proposed solution. It may not be for the first release of the Tweet, it will be on a maintenance release.
 
#25
I want to thank everyone who helped me with this issue across two threads. What I have found through testing is essentially you can have all kinds of standard gauges Altimeter, Airspeed, Throttle, UHF Radio, etc., and 1 GDI+ gauge in a single DLL. However, if you have more than one GDI+ gauge it compiles and runs, but kills the interaction with the model. Nothing will work modelwise even though you can see the Avar changes in an IOS. How that happens is a mystery to me because I can't isolate what causes that behavior in a debugger. What does work is separating out the additional GDI+ gauge(s) and use Avars and Lvars as data value objects to communicate across DLLs...thankfully the extra GDI+ gauge is a discrete unit so it only needs Avars to run. Again a big thanks to Jonathan, Doug, Eric, and Ed for all your help.
 
#26
Multi-threading works as long as you have a signalling system in place to synchronize the threads (there are many different ways to accomplish this).

The easiest way to deal with this is to have:

* FS "thread" with all systems logic etc.. doing what it does
* Drawing thread that draws the gauges to a buffer

Output of the drawing thread to the gauge is done on the FS thread, so the best way to achieve sync is to:

* Compute systems logic
* Draw the gauges to a buffer (use two buffers and alternate between them - this is called Flipping), and signal when finished each drawing cycle
* When the FS-side of rendering is ready, lock the buffer and render it to the gauge

In total, there will be three buffers - two rendering buffers, and the output buffer that is the gauge.

When you're done rendering the gauge, unlock the buffer. The locked state will temporarily halt the rendering until it is unlocked, then it can continue processing.

Code:
struct _RENDER_BUFFER
{
bool drawing_locked;
bool render_locked;
}

_RENDER_BUFFER current_render_buffer[2];
It should just involve something to the effect of:

Code:
if(current_render_buffer[0].drawing_locked == false) { current_render_buffer[0].render_locked = true;  g.DrawImage(buffer_a,x,y,0,0); current_render_buffer[0].render_locked = false; }
if(current_render_buffer[1].drawing_locked == false)  { current_render_buffer[1].render_locked = true;  g.DrawImage(buffer_b,x,y,0,0); current_render_buffer[1].render_locked = false; }
If both buffers are locked, nothing happens.

The drawing thread is responsible for switching the state of the lock prior to each drawing cycle. If both are locked (no rendering to the gauge can be done), don't do anything.

Set the lock before clearing the other, to avoid state conflicts (due to the nature of multi-threading race conditions, as the gauge rendering thread is not spinning, it could read in between the drawing state change and see false twice, rendering twice. As much as I hate goto statements (they are actually legitimate however!), you could also write:

Code:
if(current_render_buffer[0].drawing_locked == false)  { current_render_buffer[0].render_locked = true;  g.DrawImage(buffer_a,x,y,0,0); current_render_buffer[0].render_locked = false; goto done; }
if(current_render_buffer[1].drawing_locked == false)  { current_render_buffer[1].render_locked = true;  g.DrawImage(buffer_b,x,y,0,0); current_render_buffer[1].render_locked = false; }
done:
In the drawing thread, if it has finished drawing and switches buffers but that buffer is currently in use (current_render_buffer[x].rendering_locked == true), then just spin until it is ready.
 
Last edited:
#27
Thanks, I've been thinking about asynchronous behavior for a while. What I'm hearing in this thread is that you should multithread as a matter of course. Is this particular to GDI+ (or Direcx11) gauges being run while the non "graphics" gauges run in a separate thread? Or is it common to run multithreaded for a small to medium sized aircraft with more than just classic avionics? Forgive the ignorance as this is one of the first times I have heard about running async design and implementation in development of a gauge.
 
#28
Multi-threading is actually pretty over-rated. :)

What are you trying to achieve with multithreading? Doing 2, 3, ... n things *at the same time*.

People often complain about "why does FS use only 1 core??!!". Answer: there are some things that simply can not be *parallel processed* and a simulator is one of them.

The objective of multi-threading gauge drawing with the sim is because the drawing can take longer than the time-slice allocated to the FSX process. The operating system thread scheduler allocates time slices (typically 32 ms) to a process before moving to the next thread. A multi-core (or multi-processor - they are the same thing in this context) allows unrelated tasks to be processed simultaneously, so instead of:

Take A, task B, task C...

You get:

Task A
Task B
Task C

More, in less time.

I'm sorry if this is stating the obvious so far...

In my experiments so far, it isn't possible to get the gauges to update faster than the sim will refresh the underlying drawing surface (e.g. you can not get the sim to run at 10 FPS while an ADI draws at 60 FPS). In other words, you can draw as fast as you like; the sim will draw the actual image on the instrument when it is good and ready!

As long as you can draw in less time than the sim frame rate (I aim to draw faster than 60 Hz, or 16.6667 ms per drawing frame) then the drawing will never eat so much CPU power that the sim drags to a halt.

If your drawing does seriously adversely affect performance, then offloading the drawing to a separate thread means the main FSX thread can run as it wants, but at some point the output of the drawing thread must meet with the gauge update routine of FS. You need to synchronize this in such a way that you aren't in the middle of drawing when the sim wants to start updating the gauge. This can result in flickering/blank gauge updates.

Multi-threading is itself a huge topic. The single biggest problem is avoiding race conditions of reading/writing simultaneously. A variable only ever accessed from one thread never has this problem, but as soon as two processors try to deal with the same variable, all hell breaks loose. A big problem is thread stall, where a thread just spins until the variable it is trying to access becomes available. This holds up the entire processor/core until it is free. The other thread that is making the access either reads the correct data, or reads null data.

Multi-threading is not a magic bullet to the problem of drawing taking too long. If it is the case that the drawing routines are eating serious CPU power, then you should start with looking at optimizing your drawing. At the end, we are only drawing a few 2D elements; they write Crysis 3 with higher performance. ;) Just creating a separate thread is itself not an answer, and can in fact reduce performance or create other problems if not done correctly.
 
#29
If the sim is set to render at 60 frames per second, the sim has 16.67ms to render an entire image. All of it. That means that it has to do all exterior scenery rendering, all 3D aircraft VC model rendering, as well as call the PANEL_SERVICE_PRE_DRAW for every single gauge defined and visible in a panel. All of that has to be completed within the 16.67ms time slice or the frame rate drops. If your gauge rendering is needing 16.67ms to render it's image, you've just taken 100% of that individual frame render time.
 
#30
Indeed you have. That's exactly my point - the drawing shouldn't take so long that it drags the performance of the sim down to begin with. If it does, you seriously need to optimize the drawing routines. Multi-threading is not the answer.

As I say, Crysis 3 renders far more than the few polygons we are drawing.
 
#31
Thank you both for the information. I was mistakenly thinking an async solution was being proposed. Which is a relief because doing that in enterprise applications running on servers was a nightmare and we didn't do any complex graphics rendering at all. Again thank you for the help, its greatly appreciated.
 
Top