How do I share data between C++ gauges in a shared cockpit scenario?

Based on the lack of references to "process_shared_event_out()" and "GAUGE_HEADER_FS1000" in a popular search engine and the fact that I've gotten a few questions on the subject, I'm guessing that the understanding of how to share data between two instances of the same C++ gauge in a shared cockpit scenario is quite limited.

First let me describe a possible scenario:

Let's say you're flying along in a multiplayer session in the C172 and your friend Mary (who is also in the session, even though she lives on the other side of the country) joins your aircraft. If you click on the "SELECT" button on the clock to switch between time-of-day and stopwatch mode, Mary sees the clock change modes on her machine. Likewise, if she clicks on the "SELECT" button, you see the clock change mode on your end. How is this magic happening?

First let's think about how most gauges work, and then we'll look into how the clock is a special case.

Most gauges simply reflect gauge simulation state. For example, an airspeed gauge doesn't have to do any work to stay in sync with the other user's machine; the airspeed is automatically synced by the multiplayer system. Similarly, a gear lever automatically stays in sync because the GEAR_TOGGLE event (like most key events) are automatically sent to the other machine in the shared cockpit by the multiplayer system.

However, in some cases with default aircraft (and with many super-detailed 3rd-party aircraft), gauges maintain some of their own state data. The clock in the C172 is an example. The simulation engine doesn't know anything about the clock at all; hence it must not only maintain its own state data, but also send data to the other machine in the shared cockpit scenario in order to keep both machines in sync.

So how do we send this data? Let's take a look basic premise behind the synchronization system in FSX and beyond.

When two users' aircraft are first joined to a shared cockpit session, the state of the host's gauges is sent to the other user's machine. This is called "serialization". A few milliseconds later (or many, depending on network latency), the non-host receives this data, which must then be "deserialized". Once the two machines are initially synchronized, gauge state only needs to be updated when something changes; for example, when a user on either side clicks on the "SELECT" button. However, not all state needs to be synchronized; we can simply send an event to signal that the "SELECT" button was pressed. Although in this situation we don't need to send any additional data, in some other situations it is both necessary and possible.

So, if we look at the new-fangled GAUGE_HEADER_FS1000 macro, we see that there are parameters that we can use to specify the serialization function, deserialization function, and event processing function. In addition to this, we also see that we can specify the size of the buffers that will be allocated to pass data both for serialization and for events. Lastly, the macro also requires a GUID so that each gauge can be uniquely identified on each machine.

#define GAUGE_HEADER_FS1000( \
gaugehdr_var_name, \
default_size_mm, \
gauge_name, \
element_list, \
pmouse_rect, \
pgauge_callback, \
user_data, \
usage, \
guid, \
serialize_size_callback,\
serialize_callback, \
deserialize_callback, \
event_size_callback, \
process_event_callback) \

So, in the case of the clock, the setup and declaration of the gauge would look something like this:

#define GAUGEHDR_VAR_NAME gaugehdr_clock

#define GAUGE_ITEM clock

#define GAUGE_NAME "Clock"

#define GAUGE_W 54

char gauge_name_clock[] = GAUGE_NAME;

extern PELEMENT_HEADER list_clock;

extern MOUSERECT mouse_rect_clock[];

extern GAUGE_CALLBACK gcb_clock;

SERIALIZE_SIZE_CALLBACK sscb_clock;

SERIALIZE_CALLBACK scb_clock;

DESERIALIZE_CALLBACK dcb_clock;

EVENT_SIZE_CALLBACK escb_clock;

PROCESS_EVENT_CALLBACK pecb_clock;

BOOL clock_control_button( PGAUGEHDR gauge_header);

BOOL clock_select_button( PGAUGEHDR gauge_header);

BOOL clock_toggle_temp_volts_button( PGAUGEHDR gauge_header);

// {7B293842-E2C2-4800-A190-33A7F20385AB}

GUID clock_guid = { 0x7b293842, 0xe2c2, 0x4800, { 0xa1, 0x90, 0x33, 0xa7, 0xf2, 0x3, 0x85, 0xab } };

GAUGE_HEADER_FS1000(GAUGEHDR_VAR_NAME, GAUGE_W, gauge_name_clock, &list_clock, mouse_rect_clock, gcb_clock, 0L, 0L, clock_guid, sscb_clock, scb_clock, dcb_clock, escb_clock, pecb_clock);

So, what do these callback functions do? In the case of the clock, the process is fairly simple. First, be aware that the clock has some of its own enums and a struct:

typedef enum {

  CLOCK_MODE_CLOCK,

  CLOCK_MODE_TIMER,

} CLOCK_MODE;

typedef enum {

  TEMP_VOLTS_MODE_TEMP,

  TEMP_VOLTS_MODE_VOLTS,

} TEMP_VOLTS_MODE;

typedef enum {

  CLOCK_FORMAT_12,

  CLOCK_FORMAT_24,

} CLOCK_FORMAT;

typedef enum {

  CLOCK_EVENT_CONTROL,

  CLOCK_EVENT_SELECT,

  CLOCK_EVENT_TOGGLE_TEMP_VOLTS,

} CLOCK_EVENT;

typedef struct {

  CLOCK_MODE clock_mode;

  TEMP_VOLTS_MODE temp_volts_mode;

  CLOCK_FORMAT format;

  FLOAT64 clock_timer_start;

  FLOAT64 timer_accumulated;

  BOOL timer_running;

  BOOL timer_stopped;

} CLOCK_DATA;

And now let's look at the callback function definitions. Note that the amount of data sent for serialization and deserialization is significantly greater than the amount of data (one 32-bit number) that's sent with an event.

 

// serialization size (return length of buffer)

void sscb_clock(PGAUGEHDR gauge_header, UINT32* nSize)

{

    *nSize = sizeof(CLOCK_DATA) + sizeof(FLOAT64);

}

// serialize

void scb_clock(PGAUGEHDR gauge_header, BYTE* pBuf)

{

    CLOCK_DATA* data = (CLOCK_DATA*)gauge_header->user_data;

       

    memcpy(pBuf, data, sizeof(CLOCK_DATA));

}

// deserialize

bool dcb_clock(PGAUGEHDR gauge_header, BYTE* pBuf )

{

    CLOCK_DATA* data = (CLOCK_DATA*)gauge_header->user_data;

    memcpy(data, pBuf, sizeof(CLOCK_DATA));

       

    return true;

}

// event size

void escb_clock(PGAUGEHDR gauge_header, UINT32* nSize)

{

    if (!nSize)

        return;

    *nSize = sizeof(CLOCK_EVENT);

}

// process event - return true if successful

bool pecb_clock(PGAUGEHDR gauge_header, BYTE* pBuf)

{

    CLOCK_EVENT event;

    memcpy(&event, pBuf, sizeof(CLOCK_EVENT));

    switch(event)

    {

        // to pass to the mouse callback, we cast to a PPIXPOINT.

        case CLOCK_EVENT_CONTROL:

            return clock_control_button(gauge_header) ? true : false;

            break;

        case CLOCK_EVENT_SELECT:

            return clock_select_button(gauge_header) ? true : false;

            break;

        case CLOCK_EVENT_TOGGLE_TEMP_VOLTS:

            return clock_toggle_temp_volts_button(gauge_header) ? true : false;

            break;

        default:

            return false;

            break; // should NOT hit this.

    }

    return false; // should also NOT hit this.

}

And finally, the function that sends an event from one machine to the other.

BOOL clock_select_button_mouse( PPIXPOINT relative_point, FLAGS32 /* mouse_flags */ )

{

  PGAUGEHDR gauge_header = GAUGEHDR_FOR_MOUSE_CALLBACK(relative_point);

  CLOCK_EVENT event = CLOCK_EVENT_SELECT;

  process_shared_event_out(gauge_header, (BYTE*)&event, sizeof(CLOCK_EVENT));

 

  return TRUE;

}

 

So, if you have a gauge that you want to be shared in a shared cockpit scenario but doesn't work currently because the gauge maintains some of its own state, give this a try! Bear in mind that the above example doesn't contain all of the code for the C172 clock, but it should be enough to get across the concepts of serialization and event sharing.

 

Next time I'll talk about data sharing in XML gauges, so stay tuned!