novaHost - GUI Frame Processing
As previously mentioned the Nova Host GUI is closed source to streamline the source code provided in the SDK to focus on CIGI development.
A key Nova Host feature that isn't directly related to CIGI but is very important for IG performance is how Nova Host processes a frame and updates it's timestamps.
Note that Nova Host is an engineering tool and the ideas presented here are guidelines for best practices. Nova Host is not a host replacement and you may notice performance issues.
Synchronous Mode
As described in Section 2.2.2 Synchronous Operation of the CIGI_ICD_3.3.pdf, "this mechanism makes the host data available at the beginning of every IG frame, eliminating additional latency".
In contrast, Section 2.2.1 Asynchronous Operation, describes how creeping can occur. "This “creeping” of the frame offset is a common phenomenon that occurs when the Host and IG frame rates are not exactly equal. Depending upon the direction of the creep, this will frequently cause the IG to receive either zero or two Host-to-IG messages during a frame, causing frame jitter."
While Nova Host supports both modes of operation as described in the Application Settings, synchronous operation is preferred and is described here as a guideline for host developers.
Nova Host Synchronous Frame Loop
The following code is an example of the Nova Host synchronous frame loop key elements.
double Worker::getUnixTimestamp()
{
FILETIME ftime;
GetSystemTimePreciseAsFileTime( &ftime );
const unsigned __int64 time_t = ( ( (unsigned __int64)ftime.dwHighDateTime ) << 32 ) + ftime.dwLowDateTime;
// Windows reports times in the epoch starting in 1601. Our cigi packet uses the Unix epoch, which starts in 1970
const unsigned __int64 jan1970 = 116444736000000000LL;
// each lsb measures 100ns
return static_cast<double>( ( time_t - jan1970 ) * 1e-7 );
}
void Worker::handleSof( CigiBasePacket *const packet )
{
//----------------------------------------------------
// TIMESTAMP
//----------------------------------------------------
// get the timestamp at the start of the frame
_currUnixTimestamp = getUnixTimestamp();
//----------------------------------------------------
// SEND TO IG
//----------------------------------------------------
// perform top of frame send
if ( _udp.send( _sendBuff, _sendPacketSize ) < 0 )
{
sdkError( __FUNCTION__, "Socket receive error; " + QString::number( WSAGetLastError() ) );
}
// calculate the frame delta time
_frameDeltaTime = _currUnixTimestamp - _lastUnixTimestamp;
_lastUnixTimestamp = _currUnixTimestamp;
// update the frame rate and timestamp
_frameRate = 1.0 / _frameDeltaTime;
_icdLoader.host2pcnova()->h2ig.cnt.exe_nframe++;
_icdLoader.host2pcnova()->h2ig.cnt.exe_host_timestamp = _currUnixTimestamp;
_icdLoader.host2pcnova()->h2ig.cnt.exe_host_timestamp_valid = true;
// save the visok and the IG frame
CigiBaseSOF *pUpdate = static_cast<CigiBaseSOF *>( packet );
_icdLoader.pcnova2host()->ig2ios.cnt.exe_visok = static_cast<char>( pUpdate->GetIGMode() );
_icdLoader.pcnova2host()->ig2h.cnt.exe_nframe = pUpdate->GetFrameCntr();
}
void Worker::synchronousRunLoop()
{
// initialize the timing elements
promise<void> stopSignal;
future<void> futureStop = stopSignal.get_future();
time_point<steady_clock> scheduledTime( steady_clock::now() );
// track the frame count to ensure we only tick if we got a SOF
long frameCntr = _icdLoader.host2pcnova()->h2ig.cnt.exe_nframe;
// purge any CIGI messages that are lingering on the port
int bytes = 0;
_recvPacketSize = 0;
while ( ( bytes = _udp.recv( _recvBuff, _recvBuffSize ) ) > 0 )
{
_recvPacketSize += bytes;
}
// loop while waiting for futureStop to signal thread to return
do
{
// sleep for a small amount of time
scheduledTime += microseconds( 15 );
//----------------------------------------------------
// RECEIVE FROM IG
//----------------------------------------------------
int bytes = 0;
_recvPacketSize = 0;
while ( ( bytes = _udp.recv( _recvBuff, _recvBuffSize ) ) > 0 )
{
// accumulate the packet size
_recvPacketSize += bytes;
// translate the CIGI packets to aePcnova2Host
_rosetta.translate_incoming( _icdLoader.pcnova2host(), _recvBuff, bytes );
}
//----------------------------------------------------
// TICK IF START-OF-FRAME (SOF) RECEIVED
//----------------------------------------------------
if ( _recvPacketSize > 0 && ( frameCntr != _icdLoader.host2pcnova()->h2ig.cnt.exe_nframe ) )
{
//----------------------------------------------------
// INPUTS
//----------------------------------------------------
_kbm.update();
_joystick.update();
//----------------------------------------------------
// UPDATE THE PLUGINS
//----------------------------------------------------
Q_EMIT updated( _frameDeltaTime );
//----------------------------------------------------
// TRANSLATE OUTGOING DATA
//----------------------------------------------------
// support for using the ownship as a camera
six_dof ownPosOri = _icdLoader.host2pcnova()->h2ig.dat.ownship;
if ( GuiObjectApi::getStealthCameraEnabled() )
{
GuiObjectApi::getStealthCameraPosOri( _icdLoader.host2pcnova()->h2ig.dat.ownship );
}
_sendPacketSize = _rosetta.translate_outgoing( _icdLoader.host2pcnova(), _sendBuff, _sendBuffSize );
if ( GuiObjectApi::getStealthCameraEnabled() )
{
_icdLoader.host2pcnova()->h2ig.dat.ownship = ownPosOri;
}
// save the frame count
frameCntr = _icdLoader.host2pcnova()->h2ig.cnt.exe_nframe;
// update the UI
Q_EMIT frameRateUpdate( _frameDeltaTime, _frameRate );
}
}
while ( ( futureStop.wait_until( scheduledTime ) == std::future_status::timeout ) && !_shutdown );
sdkMessage( "WorkerThread", "Synchronous run loop ended" );
// tell the GUI we're done
Q_EMIT done();
}
Notice that per Section 2.2.2 Synchronous Operation of the CIGI_ICD_3.3.pdf we immediately timestamp and send an update to the IG when a SOF packet is received.
High Resolution Timestamps
In the above code note that we get a high resolution UNIX timestamp in the host2pcnova->h2ig.cnt.exe_host_timestamp ICD field.
The need for this is described in the Aechelon Nova_CIGI_3_Definition.pdf as follows:
"pC-Nova supports high-precision (64-bit) timestamps using a Component Control in lieu of the 32-bit timestamp in the IG Control packet. The timestamp is specified as a double-precision floating-point number representing the number of seconds since midnight UTC, January 1, 1970. High precision timestamps enable precise extrapolation of entity position/orientation, as well as correlation of temporal special effects (such as sea state) between simulator systems. Proper use of high-precision timestamps requires that the clocks of simulation systems be synchronized to a high degree of precision. We recommend using the Precision Time Protocol (PTP) to accomplish precise synchronization."
The aeRosetta code that sends this component control to the IG is shown below. Please see the aeRosetta source code for more specifics.
if ( !bufferCompCtrl(
CigiBaseCompCtrl::SystemV3,
SYSTEMV3_HIRES_TIMESTAMP_ENTITYID, // EntityID
0, // compId
0, // compState
_host2pcnova->h2ig.cnt.exe_host_timestamp,
0,
0 ) )
{
return false;
}
PTP
The Precision Time Protocol (PTP) can be used to synchronize the IG and host machine clocks. Please contact support@aechelon.com for further details.